The hub · build status

event.clinic

The events platform the rest of the suite reads from. Generated from 07-delivery/BUILD-LEDGER.md, which is itself generated from files on disk in this repository and in event-clinic-app.

generated 2026-10-01 23:40 UTC tree eca81f6 (uncommitted changes present) 623 slices from 07-delivery/BUILD-LEDGER.md

Acceptance receipt: 391 suites, taken 2026-10-01T23:34:57.737Z

Overall

72%complete
448 complete169 on production, no suite2 built, suite never run2 in progress2 not started 623 total

Phases

Phase 1 - The spine

W1 – W18 · 18 slices

16 complete · 2 on production, no suite

Phase 2 - The MVP modules

W19 – W33 · 15 slices

14 complete · 1 built, suite never run

Phase 3 - Content, directory and import

W34 – W43 · 10 slices

9 complete · 1 on production, no suite

Phase 4 - Media, marketing and the siblings

W44 – W49 · 6 slices

6 complete

Phase 5 - The public directory

W50 – W86 · 37 slices

15 complete · 22 on production, no suite

Phase 6 - Operator surfaces and the brand system

W87 – W100 · 12 slices

12 on production, no suite

Phase 7 - The event workspace, its money and the artist

W120 – W152 · 25 slices

5 complete · 20 on production, no suite

Phase 8 - The client portal

W153 – W199 · 37 slices

25 complete · 10 on production, no suite · 2 in progress

Phase 9 - The suite boundary and accreditation

W200 – W301 · 91 slices

55 complete · 34 on production, no suite · 1 built, suite never run · 1 not started

Phase 10 - The owner's own working day, and the project above the event

W302 – W335 · 31 slices

1 complete · 29 on production, no suite · 1 not started

Phase 11 - Doors for the engines

W342 – W500 · 139 slices

128 complete · 11 on production, no suite

Phase 12 - The words the product stores

W501 – W542 · 39 slices

39 complete

Phase 13 - The showcalling ecosystem

W600 – W698 · 86 slices

85 complete · 1 on production, no suite

Phase 14 - The safety planning programme

W699 – W776 · 77 slices

50 complete · 27 on production, no suite

Every module

SliceNameStatusThe ledger’s note
Phase 1 - The spine
W1RLS isolation testcompleteThe first tenant-isolation harness. *Evidence: 1 migration, acceptance suite, 3 design docs.*
W2RLS isolation test, second passcompleteRetires a token later suites expect to be gone, so run order matters. *Evidence: 1 migration, acceptance suite, 3 design docs.*
W3Cascade testcompleteDelete-cascade behaviour across the tenant tree. *Evidence: 1 migration, acceptance suite.*
W4Spend thresholdcomplete*Evidence: 1 migration, acceptance suite.*
W5Quote pricingon production*Evidence: 1 migration.*
W6Engagement bank detailscomplete*Evidence: 1 migration, acceptance suite, 1 design doc.*
W7Quote createcomplete*Evidence: 1 migration, acceptance suite, 1 design doc.*
W8Feature category graphon production*Evidence: 2 migrations, 1 design doc.*
W9Supplier proposed linescomplete*Evidence: 1 migration, acceptance suite, 1 design doc.*
W10Quote line authoringcomplete*Evidence: 1 migration, acceptance suite, 1 design doc.*
W11Unit systemscomplete*Evidence: 1 migration, acceptance suite, 1 design doc.*
W12Quote line notificationscomplete*Evidence: 1 migration, acceptance suite.*
W13Event and event spacecomplete*Evidence: 1 migration, acceptance suite.*
W14Notification preferencecomplete*Evidence: 1 migration, acceptance suite, 1 design doc.*
W15Theme preferencecompleteThe first slice with a production apply script. *Evidence: 1 migration, 1 production apply, acceptance suite, 1 design doc.*
W16Bank reference read permissioncomplete*Evidence: 1 migration, acceptance suite.*
W17Quote financial read grantscomplete*Evidence: 1 migration, acceptance suite.*
W18User preference tablecomplete*Evidence: 1 migration, acceptance suite.*
Phase 2 - The MVP modules
W19M-01 Projectcompleteapp.project, the container above the event. *Evidence: 1 migration, 1 production apply, acceptance suite.*
W20M-09 Person and contact directorycompleteDelivered 2026-07-26. *Evidence: 1 migration, 1 production apply, acceptance suite.*
W21M-02 Venue directorycompleteRoutes and screens over the venue / building / floor / space tree. *Evidence: 1 migration, 1 production apply, acceptance suite.*
W22M-03 Agendas and run of showcompleteThe three-level tree: agenda, segment, session line item. *Evidence: 1 migration, 1 production apply, acceptance suite, 2 design docs.*
W23M-05 Workforce and shiftscompleteEight tables. Delivered 2026-07-27. *Evidence: 1 migration, 1 production apply, acceptance suite, 1 design doc.*
W24M-04 PerformerscompleteDelivered 2026-07-27. Split, and W24.1 is also on production. *Evidence: 2 migrations, 2 production apply, acceptance suite, 2 design docs.*
W25M-06 Guests and registrationscompleteShared attendee record by flag, invitations, RSVP. *Evidence: 1 migration, 1 production apply, acceptance suite, 1 design doc.*
W26M-07 Tasks and checklistscompleteSplit three ways. W26.1 unified task target search (D-385). W26.2 per-caller searchable columns (Q-246). All three on production. *Evidence: 3 migrations, 3 production apply, acceptance suite, 1 design doc.*
W27M-08 Budget and budget line itemcompleteSplit four ways (D-411, D-493). W27.1-.3 all on production. The cross-domain hub wired to quote lines. *Evidence: 5 migrations, 4 production apply, acceptance suite, 4 design docs.*
W28M-12 File and attachmentcompleteSplit four ways (D-413). The D-012 one-file model and D-016 polymorphic attachment. W28.1-.3 on production. *Evidence: 4 migrations, 4 production apply, acceptance suite, 3 design docs.*
W29M-11 Product, inventory item, assetcompleteSplit three ways (D-427). The three-tier chain and its allocation. *Evidence: 3 migrations, 3 production apply, acceptance suite, 3 design docs.*
W30M-10 -> The public directorycompleteThe plan says accreditation and check-in. That is not what W30 became. It is /directory/v1, the no-login surface Procurevent and the wedding portal read. No migration, because the slice adds routes, a pre-auth surface and the app_public lane only. Verified live in production. *Evidence: 1 migration, 1 production apply, acceptance suite, 1 design doc.*
W31M-13 Messaging, threads and the Inboxunverifiedmigration + acceptance suite, w31 SKIPPED 2 assertion group(s), 52 ran. A SKIP IS NOT A PASS, so this is not complete. Split ten ways. W31 through W31.9, every one with its own production apply script. W31.4 opened tenant-private venues. *Evidence: 10 migrations, 10 production apply, acceptance suite, 1 design doc.*
W32M-14 NotificationscompleteDelivered 2026-08-01, applied to production on the owner's explicit instruction. *Evidence: 1 migration, 1 production apply, acceptance suite, 1 design doc.*
W33M-16 Dashboard roll-ups and reportingcompleteNo slice was ever built. GET /dashboard/summary exists and has its own acceptance suite, but that suite's own header calls it *a correction, not a slice*. The reporting essentials have no implementation. *Evidence: 1 design doc.*
Phase 3 - Content, directory and import
W34Glossarycomplete*Evidence: 2 migrations, 1 production apply, acceptance suite.*
W35Moment frameworkcomplete*Evidence: 1 migration, 1 production apply, acceptance suite.*
W36Glossary cross-referencescomplete*Evidence: 1 migration, 1 production apply, acceptance suite.*
W37Country spellingscomplete*Evidence: 1 migration, 1 production apply, acceptance suite.*
W38Hungarian glossarycomplete26 migrations, one per letter group. Four carry production apply scripts, and the rest were applied through them. *Evidence: 26 migrations, 4 production apply, acceptance suite.*
W39Glossary mergecomplete*Evidence: 1 migration, 1 production apply, acceptance suite.*
W40Event wizardon productionREMOVED BY W416 ON 2026-09-12, AT THE OWNER'S REQUEST. The five /events/wizard/* routes are gone from the Worker. This slice's acceptance suite drove only those routes, so it failed 28 assertions from that day on, unseen because no battery ran again until 2026-09-13, and W442 retired it. The migration and its tables stay on every database. *Evidence: 1 migration, 1 production apply, 1 design doc.*
W41IATA number and slugcomplete*Evidence: 1 migration, 1 production apply, acceptance suite.*
W42Venue importcomplete11 migrations. The bulk venue load, addresses and country corrections. *Evidence: 11 migrations, 1 production apply, acceptance suite.*
W43Strategy playbookscomplete*Evidence: 1 migration, 1 production apply, acceptance suite.*
Phase 4 - Media, marketing and the siblings
W44Marketing plancompleteAll six steps complete. W44.1 adds marketing_plan_row.task_id, ON DELETE SET NULL. The task export is per-track and opt-in. *Evidence: 2 migrations, 1 production apply, acceptance suite, 1 design doc.*
W45Venue mediacomplete13 migrations, ending in the repoint to R2. These were the 13 missing from production at 15:20 on 2026-08-03. They have since been applied. *Evidence: 13 migrations, 1 production apply, acceptance suite.*
W46Supplier event readcompleteAnswers Mise's cross-product request. On production. *Evidence: 1 migration, 1 production apply, acceptance suite.*
W47Dietary requirement vocabularycompleteMigration on all three databases. The product commit carrying W47-W49 is local only and has not been pushed. *Evidence: 1 migration, acceptance suite.*
W48Supplier programme readcompleteMigration on all three databases. Product commit unpushed, see W47. *Evidence: 1 migration, acceptance suite.*
W49Cultural dietary restrictionscompleteMigration on all three databases. Product commit unpushed, see W47. *Evidence: 1 migration, acceptance suite.*
Phase 5 - The public directory
W50Per-field provenancecompleteOne jsonb column on venue and company. Asked for independently by Mise, Estate and Rigfold, and gates everything that writes a field. *Evidence: 1 migration, acceptance suite.*
W51Venue city and country importcompleteFrom an Addresses export nobody had imported. Reuses W42.11's own predicate rather than re-introducing the US-state-code defect. *Evidence: 1 migration, acceptance suite.*
W52Venue media kindscompleteinterior, exterior, space, detail, floorplan, logo. *Evidence: 1 migration, acceptance suite.*
W53Supplier company importcomplete35 191 names, all not_published. Ids are uuid5 of a fixed namespace, so they match on every database. *Evidence: 4 migrations, acceptance suite.*
W54Supplier publication baron productionName, plus a website or a country, plus a category. Entry only, so nothing already public is taken down. *Evidence: 1 migration.*
W55Field proposals and venue mergescompleteThe proposal queue, and venue ids that survive a merge. *Evidence: 1 migration, acceptance suite.*
W56Venue media URLcompleteThe function that turns a bare key into a URL. Why the directory has photographs. *Evidence: 1 migration, acceptance suite.*
W57Venue overview importcomplete7 810 venues from the 2026-08-04 re-export's details_by_perplexity, BBCode converted to markdown once at import. Provenance ai_direct, not imported: Perplexity wrote the text and the route it travelled does not change that. *Evidence: 4 migrations, acceptance suite.*
W58Supplier enrichmenton production27 731 websites, 13 253 descriptions, 33 355 Place ids, 29 662 category labels. Websites are validated, not trusted: 20 of them were phone numbers and street addresses that would also have satisfied the W54 bar. *Evidence: 23 migrations.*
W59Supplier publication guardon productionD-600. Unpublishes by property rather than by id, and verify-published-suppliers.sh asks the question neither existing guard asked. *Evidence: 1 migration.*
W60Venue amenitiescomplete32 907 links, 834 venues, 61 terms. The data the 2026-08-01 export said did not exist. A separate vocabulary from the supplier feature tree, per W54's own rule. *Evidence: 6 migrations, acceptance suite.*
W61Per-space per-layout capacitycompleteD-601. 964 rooms, 2 944 capacities, 16 layouts. Loaded through Spaces.seatingCapacities, because the capacity row's own space column resolves to nothing. *Evidence: 4 migrations, acceptance suite.*
W62Venue slug shapecompleteSix published venues had a slug ending in a hyphen and could not be opened. The slug was fixed, not the regex. *Evidence: 1 migration, acceptance suite.*
W63The full Venues exportcomplete17 614 venue types, 19 917 overviews, 16 527 time zones, 25 809 Place ids, 44 990 Bubble ids. The directory has a TYPE FILTER for the first time. Three fields were refused: capacity_max_pax is 31 540 copies of 9999, time_zone is 10 099 copies of the failed-lookup default, and wheelchair access is 96.5 per cent yes. *Evidence: 26 migrations, acceptance suite.*
W64Operating statuscompleteGoogle's business_status, stored so the fact survives a re-import. 290 permanently closed venues were published, and are not now. Temporarily closed stay published. *Evidence: 3 migrations, acceptance suite.*
W65Junk city correctioncomplete892 venues had a city of yes or no, faithfully imported from a source column shift. Nulled rather than parsed out of the formatted address, per D-598. Its real finding is that W51's probe named one venue by id and broke when that venue was legitimately corrected. *Evidence: 1 migration, acceptance suite.*
W66Supplier categorieson production26 879 suppliers mapped from the imported label onto the 3 167-node tree, so 20 926 can now clear the W54 publication bar where none could before. An explicit 394-label table, not AI, so D-597's accuracy bar does not apply. *Evidence: 4 migrations.*
W67The AI workercompleteD-596. Descriptions and venue types, metered against the API's own usage and stopped at $25 BEFORE each call. --dry-run prices a job with no credential and no spend. The acceptance test is negative: a number, street or year absent from the venue's own record refuses the answer. No LLM credential exists on this machine, so no run has happened. *Evidence: 1 migration, acceptance suite.*
W68Overview provenance correctedcompleteD-603 overturned by the owner. The 7 810 Perplexity overviews W57 loaded are relabelled ai_direct to imported, so 7 651 published descriptions read as source data rather than inferred. Merges them with W63's 15 760 into one cohort of 23 570. Refuses on any count but the measured 7 810, because once W67 runs its output is indistinguishable from W57's and re-running this would promote model text to source data. *Evidence: 1 migration, acceptance suite.*
W69Company enrichment proposalson productionThe AI worker's company output lands in app.company_enrichment_proposal, never in app.company_feature_category. Forced by the VERIFICATION case: re-checking an imported category means sometimes disagreeing with it, and a disagreement has nowhere to live in a link table. Platform-admin only on both sides, because a proposal is not a fact. 26 879 companies carry exactly one category and none carry two. *Evidence: 1 migration.*
W70Company enrichment workeron productionCategorises, VERIFIES and describes the 35 192 imported suppliers, writing only to W69's proposal queue. Measured across 15 307 companies: 93 per cent of the imported categories held up (12 700 confirms, 951 contradicts) against the owner's predicted 90, plus 21 446 additions and 9 828 overviews. 2.39 categories per company where every company in the database carries exactly one. The run breached D-596 at $26.65 of a $25.00 ceiling, which is what a submission cap enforced by projection does when the projection is 7.5 per cent low. Four answers named real industry categories the 3 167-node tree does not contain, which the vocabulary check surfaced rather than wrote. About $60 batched for all 35 192. *Evidence: 1 migration.*
W71Destination Management categoryon productionOne node. 299 of W70's 474 genuine-gap proposals asked for a DMC and the 3 167-node tree contained nothing like it at any level, so 251 suppliers were correctly identified and every identification was discarded. Placed at level 2 under Management / Planning / Operations beside Vendors Management and Venue Finding. The other 153 gap names are mostly single figures, and the list overstates the gap: Vendor Management was refused while Vendors Management already existed. *Evidence: 1 migration.*
W72Feature category aliaseson production"lighting" now finds "Light". An alias table rather than more categories, because two nodes named Light and Lighting would SPLIT the suppliers between them and make both searches return half the market. alias_norm is the primary key so one string can never resolve to two categories. 72 aliases seeded from what 15 508 suppliers made W70 reach for, at 100 per cent stem coverage only: at 50 per cent the same matcher proposed "Event production" to "Production Duplication". *Evidence: 1 migration.*
W73Alias revert and curationon productionW72's 72 derived aliases were wrong and are deleted. The owner read them: "Coach Hire and Skip Hire are absolutely different kinds of animals". Two failure modes a stem matcher cannot see: HOMONYMS (MICE to Mice & Trackballs, an entire industry segment sent to computer peripherals) and OVER-NARROWING (Sound to Sound Processor, Staffing to Waiting Staff). The rule is that an alias may never be broader than its target, and breadth is semantic. 28 survive, each read individually. *Evidence: 1 migration.*
W74Tree catch-alls removedon production71 nodes named "Related X Products" deleted, verified at 0 companies, 0 proposals and 0 children. Unfillable by construction: no company is a Related Computer Product. Ice-Related Products & Services was EXCLUDED by name because it matches the pattern by coincidence and is a real catering category with 3 children and 14 proposals. Deleting it would have destroyed data because of a hyphen. *Evidence: 1 migration.*
W75Tree orphans adoptedon production13 nodes had no parent and a level above 1, so nothing reached them by walking down from the roots. Six were a complete projection screen family whose parent already existed. MUST precede any level recompute: the API reads its top level with where fc.level = 1, and a recompute deriving level from the graph would have called these 13 roots and put Structural engineers on the front page beside AV. There were 36 before W74 removed 23 as a by-product. *Evidence: 1 migration.*
W76Compound cycles brokenon productionTwo families of three nodes were wired as complete 3-cliques, every member a parent of the other two: Marketing / PR / Advertisement and Photo / Video / Event Coverage & Documentation. Measured graph-wide, those six were the only nodes in the taxonomy that are their own ancestor. A clique with no parent above it cannot be reached by walking down from a root, so the picker could never display any of it: 113 nodes were invisible, carrying Marketing with 376 suppliers and a 93-node photo and video subtree with 1 789. The compound-split signature. Each compound was split into its atoms and every fragment re-attached to every OTHER fragment rather than to the original node. *Evidence: 1 migration, 1 design doc.*
W77Category level recomputedon productionapp.feature_category.level had drifted from the edges: 875 edges had a child whose level was not its parent's plus one. Derived by SHORTEST PATH, 1 + the fewest edges from any root, because the taxonomy is a multi-parent graph and 326 nodes are reachable at more than one depth. The picker descends edges and is unaffected; level is load bearing for where fc.level = 1 on the front page, for the public /directory/v1/categories?level=N filter that Procurevent builds against, and for the asset search order. Refuses unless W75 and W76 have both run. *Evidence: 1 migration.*
W78Compound-fragment edges removedon production217 edges across 77 children, not the 31 the handover predicted. The count rose because W76 reconnected the Marketing and Photo branches and their children carry the same defect, which was uncountable while they were unreachable. A compound such as Fog, Hazer, Bubble, Foam and Effect Machines was split into atoms and every child re-attached to EVERY atom, so Fans and Ventilators is recorded as a child of Fog, of Hazer and of Bubble at once. The fingerprint is a child under TWO OR MORE ATOMS OF THE SAME COMPOUND, because multi-parenthood is legitimate here (DQ-099). The atom list is enumerated, not detected at apply time: the detector was run, read, and was wrong where it mattered, proposing to dissolve Security of Security & Crowd Management, a real category with 17 children and 48 companies. *Evidence: 1 migration.*
W79Three non-categories deletedon productionMac Pro and MacBook, product models from one vendor's catalogue, and Management fee, a line on an invoice. D4 in the audit. Attachments were checked across all thirteen columns that reference app.feature_category, not the obvious two, and all three nodes carry zero rows in every one. The reason this is not a bare delete: Management fee has seven children and five have no other parent, and feature_category_parent cascades, so deleting the node alone would have silently orphaned Event director, Event manager, Project manager, Rider Management and Stunt Coordination. *Evidence: 1 migration.*
W80The supplier directory made publicon production20 926 suppliers published. W53 imported 35 191 companies as not_published and W54 gave them a quality bar, but nothing ever published any of them, so the supplier directory was complete, categorised and entirely invisible: zero rows readable by app_public. The owner chose published, the open-web value, over published_authenticated. Visibility is enforced by RLS and not by the endpoint: /directory/v1/companies carries no publication filter in its SQL at all, because company_public_published_only is RESTRICTIVE and a restrictive policy ANDs. Verified on production before writing, since a permissive policy there would have meant all 35 192 were already public. *Evidence: 1 migration.*
W81Enrichment proposals acceptedon production21 721 adds and 12 700 confirms accepted; the 951 contradicts deliberately held. W70 produced 45 200 proposals and W69 gave them a queue that is pointedly NOT app.company_feature_category, so AI output could never masquerade as a confirmed fact. This is the slice that ends one-category-per-company: measured on production first, every one of 26 879 categorised companies carried exactly one category and never two, with six categories holding 72.6 per cent of all assignments. Provenance is ai_direct: accepting a proposal makes it visible and attributable, not confirmed. Contradicts need a human, because resolving one overwrites something already recorded. *Evidence: 1 migration.*
W82Company country derivedon production14 835 suppliers gained a country, read out of the <span class="country-name"> already stored in their address. Not what D-598 refused: nothing is invented, looked up or fetched, and the provenance is derived. The naive parse would have written garbage and it is worth recording why: the obvious "text after the last comma" implementation hits California (254), New York (134), Florida (96) and Texas (94), so it would have stamped 254 suppliers with a country of California. Resolution runs against app.country, the 250-row ISO table, and an unresolved name is LEFT NULL rather than guessed. 99 of 100 distinct names resolve. *Evidence: 1 migration.*
W83Address markup strippedon production14 841 published companies carried raw microformat <span> markup in a field the public directory renders literally, a Bubble export artifact imported by W53 and never cleaned. The structure is discarded on purpose: the owner chose one clean address_line over parsing it into columns. Order is load bearing and the manifest enforces it: W82 extracts the one field worth keeping, country, BEFORE this file destroys the tags it reads. *Evidence: 1 migration.*
W84Unspecified layout and capacity rangeson productionRenumbered from W78, which another session had already committed six hours earlier (bbb6821); the number is a label and the applied schema is unchanged. Erzsébet Hotel Paks publishes eight rooms as "Befogadóképesség: 15-20 fő", a headcount with no seating arrangement and a range rather than a number, which venue_space_capacity could represent neither of: layout_code is NOT NULL against a table of real arrangements, and capacity is one integer. All eight rooms were correctly dropped by the extractor, because inventing a layout to fit the schema is how a guess becomes a fact a feasibility gate later trusts. Adds a seventeenth layout code unspecified, which is deliberately not a seating arrangement, and a nullable capacity_min. Both additive: capacity keeps its meaning and all 2 944 pre-existing rows stay correct untouched. Putting the minimum in the existing column would have silently reinterpreted every historic row. *Evidence: 1 migration.*
W85Overview fragments nulledon production287 venue descriptions were a fragment rather than a description and 39 were live: mank-gallery served "contemporary art" as its entire overview, alongside "M", "closed", "``N/A`" and a run of sports club names on stadium rows. The same defect class W65 found in city`, one column along, and found by the W57 acceptance suite rather than by anyone looking. Nulled under D-598 with the provenance key removed alongside the value. The threshold is 20 characters, not 60, and that choice is the slice: 277 rows are under 20 and 714 under 60, where the "142" quoted for the 60 case was the published subset rather than the row count. Deliberately did NOT fold in the 43 published AI refusal overviews ("I'm sorry, but I can't assist with that."), which are model output rather than a column shift and need their own rule. *Evidence: 1 migration.*
W86Model-failure descriptions nulledon production87 venue descriptions were the model reporting failure and 42 were live: "I'm sorry, but I can't assist with that." and "It appears there is no information available about X", plus 205 supplier descriptions that were scraped page headings ("About", "Welcome!", "…"). Not W85's defect: the value is in the right column and came from the right place, it is simply what the upstream model returned. One row decided the pattern. ^there is no reads as a failure 44 times out of 45; the forty-fifth is "There is no better place for a true Arizona wedding than The Paseo." So the rule is "there is no INFORMATION/DESCRIPTION/DETAILS/VENUE", and an assertion proves that row survives. Markdown was deliberately NOT stripped: the public worker renders bold as <strong>, verified live, so 7 555 rows were left alone. *Evidence: 1 migration.*
Phase 6 - Operator surfaces and the brand system
W87Image review workstationon production75 414 attachments carry 70 839 distinct images and one image sits on 764 venues, so the queue is keyed on checksum and ordered by blast radius: 34 images cover every cluster of ten or more. A verdict flags an image everywhere and deletes nothing. The R2 preview streams through the Worker because that bucket also holds contracts under EU jurisdiction and must never have a public origin; the ref is validated by a database lookup and not a prefix test, which would have made it a reader for signed contracts. Two buckets share the name eventclinic-files, one EU and one default, and the logos are in the default one: reading the wrong one reported all 13 492 as missing in production while local looked fine. *Evidence: 1 migration.*
W88Batch detachon productionRemoves the venue_media row for every junk-flagged image, archiving it first, both in one transaction. The row is deleted rather than soft-flagged: a detached_at column would need every consumer to add a filter, there are at least four, and any that forgot would keep serving the junk while the workstation reported it gone. The archive carries every column needed to restore it, proven by taking 664 rows out and back to exactly 75 414. Files in R2 and Images are untouched. The owner detached 2 366 attachments on production the same day. *Evidence: 1 migration.*
W89Screen review queueon productionEvery navigable destination, reviewed against four fixed questions. The list of screens is not stored anywhere: it is read from the app's own nav model at run time, so a screen added tomorrow appears here unreviewed with no migration. A seeded table would be wrong the day a screen is added and nothing would notice, which is the defect the migration ledger exists to prevent one layer down. *Evidence: 1 migration.*
W90Venue group modelon productionChains, collections and associations as one self-referencing table, plus the logo fallback in a single function. A hotel has ONE brand and may hold SEVERAL associations, enforced by a partial unique index rather than documented. Not named hotel_* because the owner widened it to restaurants and theme parks the same day. display_allowed defaults false: a chain logo is a registered trademark and a takedown must be one UPDATE, not a deploy. *Evidence: 1 migration.*
W91Brand seedon production191 parents, brands, collections and associations. Ids derive from the slug with md5(slug)::uuid, so they are identical on every database and a later logo migration attaches without a lookup. Design Hotels is filed as a Marriott brand and not an association, which is a correction: it reads like a membership body and Marriott owns it. *Evidence: 1 migration.*
W92Venue to brand matchingon production1 734 brand links and 284 collection links, every one unconfirmed, because "Sonesta Resort Hilton Head Island" is a Sonesta on an island called Hilton Head. hotel_chain was junk in one direction and useful in the other: spurious on non-hotels ("Capitol Theater" to "Capitol Records") and mostly right on hotels, carrying chains never seeded. One value was contaminated wholesale: 805 rows said "The Ritz-Carlton" against a real portfolio of about 120, so the matcher proposed 634 and now proposes 66. An assertion written as "no Ritz link without the name" fired on a true row, Penha Longa Resort, and had encoded the assumption the column exists to relax. *Evidence: 1 migration.*
W94Logo variantson productionFive named slots per brand, one file each. Superseded by W96 within hours: a brand kit is not five files, and the unique key meant choosing which four of forty to keep. *Superseded by W96.* *Evidence: 1 migration.*
W96Brand asset libraryon productionNo slot limit, and the file answers its own parameters: format from the magic bytes, size from the header, orientation derived, vector from being an SVG, and CMYK from a JPEG's four-component SOF, which browsers render with inverted colours. Proven on an SVG declaring width="100%", measured 600x150 from its viewBox, and on a PNG named .jpg. What no parser can read, mono and background, stays null rather than guessed: a white wordmark and a transparent one are identical bytes. Plus brand_colour with roles. *Evidence: 1 migration.*
W97Source files and typefaceson productionPDF, AI, EPS, ZIP and DWG are storable and never displayable. The owner's report that the uploader was broken was the file picker's accept list: those files could not be selected at all. AI is PDF-shaped, so the extension breaks the tie between two formats that are both non-displayable. Adds brand_font, names not files, because a manual names its typefaces and does not license them onward. *Evidence: 1 migration.*
W98Brand expansionon production333 groups, up from 191, merged from the owner's Bubble hotel chains export plus the brands they named. Adopts their own five-band segment scale. The export names only 87 of its 166 rows: managed_brand is populated on 87 and franchised_brand on none, so 79 arrived with a holding company and no name. Also closes an old question: "Fab Escape" is a row in that table and is the spurious hotel_chain found on a venue called "Escape Club". *Evidence: 1 migration.*
W99Slot-aware logo choiceon productionA white vertical mark was served into a square slot on a light background. Three failures in one thumbnail, all preventable by data already held: the shape was asked for as horizontal, the surface as any, the kind as logo. any was never meant to be a default, and every caller reached for it because it was easiest, so W96's assertion that a dark-only mark never reaches a light slot was true and useless: nothing asked for a light slot. The parameter loses its default and the client sends its live theme. *Evidence: 1 migration.*
W100Flyout folds and the record inspectoron productionTwo jobs from one owner instruction, and the second is the Bubble parity gap. The guest panel held seven sections and forty-eight dietary checkboxes in one column, about three thousand pixels of drawer; collapsed it is 641 and fits one screen. The mechanism is a Fold component on native <details>, applied to fourteen screens, and its summary line carries the section's VALUE so a closed section still answers the question it was opened to ask. A prop bound to chosen.length === 0 shut the allergen picker while the reader was ticking it: <details> is uncontrolled against user clicks, not against a prop that changes, so defaultOpen is now frozen in state on mount. The inspector is /settings/data, platform-admin only: every table, every row, every foreign key in BOTH directions, read from pg_catalog so a table added by a migration next month appears in it that afternoon. fetch_types: false bit twice, once encoding an array out and once decoding one back, and the second was silent: an undecoded {id,display_name} is a STRING, so .includes("name") matched a substring and picked a column that does not exist. Read only by design; writes stay behind the five gates. Reachable from a "See this record's connections" link on the guest, crew, performer, asset, session and inventory drawers, rendered only for a platform operator because the route answers 404 to anybody else and a dead link is worse than no link. *Evidence: 1 design doc.*
Phase 7 - The event workspace, its money and the artist
W120Music genres and dress codes become vocabularieson productionTwo free-text columns become closed lists with their own label tables, the shape W31.9 gave venue type. Free text on a genre is not a small untidiness: it is what makes Drum and Bass, drum & bass and DnB three different genres to every filter that reads them. *Evidence: 1 migration.*
W123The genre vocabulary grows from 26 to 172on productionW120's list was the codes already in use; this is the list the domain actually has. Additive by construction, so no stored value is invalidated, and the picker gained a search that matches accented names, which is the defect that surfaced it: a genre nobody could type was a genre nobody could pick. *Evidence: 1 migration.*
W125The tenant owner cannot see its own fileson productionD-633. The owner role held files.publish and not files.view, so the one person who could publish a file could not open it. Measured rather than assumed: verify as an Event director, because the owner's own grant list reads as complete until you check which of the two permissions each name maps to. *Evidence: 1 migration.*
W127The public profile belongs to the artist, not the bookingon productionD-631. A profile hung off the engagement, so the same performer carried a different public page per booking and none of them was theirs. Re-anchored to the artist, which is the record that persists between engagements. *Evidence: 1 migration.*
W128Production's tenant owner is levelled up to localon productionD-637, closes Q-393. Production's owner role had drifted below the local definition. Cloned roles inherit and look unprivileged, so the gap is only visible through effective_permissions() rather than by reading the role's own grants. *Evidence: 1 migration.*
W129The artist profile becomes publicly readableon productionD-635, closes Q-396. The profile existed and no unauthenticated caller could read it, so the public artist page was a page about nothing. Read is granted by RLS to app_public, not by an endpoint filter, which is the pattern W80 established for suppliers. *Evidence: 1 migration.*
W130A published artist carries its own copy of the photograph's keyon productionD-640. The photo lived behind a grant on app.file that the public directory does not hold, so the projection carries the key itself rather than widening the grant. This file reached production by hand and was in NEITHER manifest, which is how a real structural migration stayed invisible to the ledger until the directory was listed against the manifest rather than the manifests against each other. *Evidence: 1 migration.*
W131An event cannot move away from its own programmeon productionD-643, closes Q-387. Re-parenting an event to another project silently orphaned its sessions, which kept pointing at the programme it had left. Refused at the database rather than in the route, because the route is not the only writer. *Evidence: 1 migration.*
W132An event must have a time zoneon productionD-644, closes Q-388. An event without one is not an event in UTC, it is an event whose times mean nothing. Resolved at creation from a cascade: the venue's zone, then the tenant default, which is NOT NULL, so the chain cannot end in null. *Evidence: 1 migration.*
W133Why a budget line costs nothing, in its own fieldon productionD-645, closes Q-384. A zero on a budget line meant free, included, absorbed or not yet priced, and the number could not tell them apart. The reason becomes a column with a closed list, and W138 gave it the control without which it would have been another write path with no way in. *Evidence: 1 migration.*
W134A project has four states at once, not oneon productionD-646, answers Q-380 for project status. status held ONE of nineteen values across four parallel lifecycles, so a project that had been Won, was In Production and had been Invoiced could record one of the three and erased the other two. Four columns, one per lifecycle, each foreign-keyed to the reference and each policed by a trigger, because a foreign key proves a code exists and only the trigger proves it belongs in the column it went into. Could not drop the old column: the API and both screens still read it. *Evidence: 1 migration.*
W135The single status column goeson productionD-647, closes Q-401. The other half of W134, and the only file in this phase with a deploy ORDER: the Worker that no longer reads status must ship first, because there is no window in which an old Worker and a dropped column are both correct. Guarded so it refuses outright if any project carries a status the per-lifecycle columns do not already hold, which makes a re-run on a database whose backfill never ran a refusal rather than silent data loss. *Evidence: 1 migration.*
W136A budget line carries its own currency and rateon productionD-648, answers Q-386. line_total is in the LINE's money, which is the right number on a supplier's quote and the wrong one to add up: a DKK line inside an HUF budget would have contributed 7 777 to a total labelled HUF. line_total_base is the generated conversion beside it, masked by budgets.view_financial because dividing it by the rate returns the price exactly. The negative control corrected its author: an unknown code is refused by the TRIGGER, not the foreign key, because a BEFORE trigger runs while the key check is still pending. *Evidence: 1 migration.*
W137The rollup adds up the converted figureon productionD-648, and the rest of W136. Two places summed the line's own money, contributionOf in the web and app.budget_rollup in four places. The function body was read back with pg_get_functiondef rather than retyped, after confirming all three databases held byte-identical definitions. *Evidence: 1 migration.*
W139The time zone is a user preferenceon productionD-651, closes Q-399. Adds no column, and the first draft got that wrong: app.app_user still carries three preference columns, which is the RETIRED pattern. B-191 moved preferences to a key-value store and those columns survive only as a mirror, so a new preference is a new KEY. Values seeded FROM pg_timezone_names, so the allow-list cannot disagree with the engine that will interpret it. The user's zone sits BELOW the venue's and ABOVE the tenant's, so an event at a Lisbon venue is in Lisbon whoever created it. *Evidence: 1 migration.*
W140A venue has many types, and a control to say soon productionD-653, closes Q-408. One of D-653's three items already existed: the foreign key has constrained app.venue.venue_type to the 43-code vocabulary since W31.9, and the survey that said otherwise was wrong. The real gap was CARDINALITY, with hotel_resort describing roughly half of 30 077 typed venues. app.venue.venue_type stays as the PRIMARY, the join table holds the full set, and a trigger in each direction keeps the primary a member of it, so this is not two answers to one question. The write policies do not reuse the read predicate: read delegates to the venue's own row security because the public directory has to filter by type, and every published venue is readable by everyone, so the same predicate on a write would let any tenant reclassify any directory venue. *Evidence: 1 migration.*
W142The seven synonym type labels become one name eachon productionD-655. The D-649 survey counted 17 slash-joined labels as one problem; measured on production they are two. Seven are pure synonyms and become one readable name each, which the D-653 multi-select made urgent by rendering all 43 in one list where they are read together. The other TEN name genuinely different things — performing_arts_venue carries 1 568 venues under six names including Theatre, Concert Hall and Movie Theatre — and are untouched, held for D-656, with an assertion that FAILS if that count is not exactly 10, because half a taxonomy split is the silent failure D-656 exists to avoid. Shortening a label would have been a regression on its own: the vocabulary endpoint searches labels, so dropping "Chateau" would mean a French speaker finds nothing. Every word removed lands in app.venue_type.aliases, matched and never displayed, and carries every word of the original including the ones kept. Verified on production after deploy: chateau, glamping, garage and congress all resolve, a made-up word returns zero. No venue changes meaning; only English labels move. *Evidence: 1 migration.*
W144The venue media loads are re-runnable againon productionD-660, closes Q-406. The ten loads end on conflict (venue_id, ref) do nothing, which is the primary key and is idempotent right up until a ref CHANGES — and W45.13 rewrites 2486 of them in place. A rewritten row stops colliding with its own loader. Measured row by row against w45-2: 8000 pairs, 7726 stored exactly, 274 would insert, and all 274 were the other ref form of a row already present, zero genuinely new. The logos among them carry display_order 0, so they hit venue_media_one_primary and the run DIES rather than duplicating quietly, which is how the 2026-08-07 dev catch-up failed. Fixed at the DATABASE rather than in ten 8000-line files: a unique index on the NORMALISED ref, stating the invariant once. The manifest entry sits between w45-1 and w45-2, not at the end, because a rebuild from empty needs the index before the loads run. Proven by re-running three parts on local: 0 rows moved, every assertion passing. *Evidence: 1 migration.*
W145The precise venue typeson productionD-656 part 1 of 3. Adds 24 codes read OUT OF the ten compound labels, never invented, each recording which label it came from. It moves not one venue and assertion 3 proves it: the compound codes are not retired, relabelled or removed, so the precise options simply become selectable and a venue can be classified by hand while the bulk move waits for the proposal queue and D-657. It takes the vocabulary from 43 to 67, which invalidated a premise living in a COMMENT on /directory/v1/venue-types and depended on by a picker two files away: the default limit is 50, so the single picker would have shown 50 of 67 and made 17 options unpickable. Both pickers now send an explicit limit, deployed BEFORE this file. The hold on production was a vocabulary decision, not a technical one, and it was discharged on 2026-08-10: applied to prod with all five assertions running and passing, taking prod from 43 codes to 67. All three databases now measure 281 probes with every migration present. *Evidence: 1 migration.*
W146The venue type review queueon productionD-656 parts 2 and 3, and D-661. The queue is deliberately NOT app.venue_venue_type, the shape W69 and W81 proved on the company taxonomy, so a derived guess can never be read as a confirmed classification; an assertion proves none reached the live table. The first source is the venue's own NAME rather than a model, which is a choice and not a lesser version of the W67/W70 pass: it is deterministic and carries its own evidence. 9961 proposals over 9734 venues. Grouped by RULE, because 9961 individual decisions never get made while "528 venues named Theatre" is one judgement; five sample names travel with each rule and immediately earned it by surfacing "A Night At The Opera" under opera_house. D-657 is implemented as an ORDER: accepting promotes the precise code to primary and THEN removes the broad one, because W140 refuses to delete a current primary and the reverse raises EC410 on every row. Rejection is recorded, so a re-run does not revive it. Proven end to end on local: temple 117→111 and 0→6, embassy rejected, and a re-run left both decisions standing. On production since 2026-08-10, migration first and deploy second, which is the standing order because the routes read tables production did not have. Production produced the same 9961 proposals as local, which is evidence the two databases' venue names are in parity and not only their schemas. The 14 rules are open and are the owner's to decide. *Evidence: 1 migration.*
W147A rule can be curated, not only accepted wholecompleteThe follow-on D-656 needed and did not have. W146 gave a reviewer two verbs per rule, accept all or reject all, and the owner then read the samples and accepted nothing, because the rules are imperfect in a predictable way: the opera_house rule is right about roughly 175 venues and wrong about "A Night At The Opera". Two verbs make that a choice between moving a venue wrongly and abandoning 175 correct moves, so all 14 rules stayed open. The third verb is per-venue rejection: open a rule in full, page through it, tick the wrong ones, and accepting afterwards moves only the rest, because W146's queue already treats a rejected row as decided. An accept-a-subset verb is refused with a 400 rather than added, being a second way to say the same thing with none of D-657's ordering. The accept was a loop and the biggest rule would never have drained: two statements per venue, proven on a rule six venues wide, against a largest rule of 8013 — 16 026 sequential round trips in ONE transaction, where a failure rolls back and every retry fails identically. Now two set statements independent of group size, measured on local at 2.5s + 163ms + 99ms with all four end-state assertions passing and rolled back. And the screen was telling a Tenant owner they could decide. may_decide was venues.edit, which they hold; the write policy needs may_edit_venue. Measured: of 9949 open proposals ZERO are tenant scoped and ZERO are claimed, so every non-operator write was refused — and an UPDATE refused by row security does not raise, it matches nothing, which the route reported as affected: 0 and a reviewer reads as an empty rule. W147.1 adds a filter inside a rule, because per-venue rejection is only usable on a rule a person can read: hotel is 8013 proposals, 161 pages, and hotel plus resort are 9064 of the 9961 in the queue, so paging alone reached 9% of the work by volume. The response carries TWO counts, what the filter shows and how big the rule really is, so a filter hiding 8010 rows can never read as a rule that shrank to 3. Sabotage tested with a paired control: weakening the uuid guard turns one assertion red and nothing else. W148 closed D-667 on top of it: the accept path never wrote provenance, so 8892 venues a human had approved still recorded imported (7118) or ai_direct (1774), and NONE said ai_approved, which app.provenance_rank defines at 40 for exactly that state. The second cost was live rather than cosmetic: ai_direct is rank 10 and provenance_may_write permits an EQUAL rank, so the venue-type model pass designed the same day could have silently overwritten 1774 human decisions. Route stamped, 8892 backfilled on production, and verified afterwards at 0 overwritable and 0 unapproved venues claiming approval. The regression test is an INVARIANT over real data rather than a re-run of the statement, so it fails the moment an accept path forgets the stamp. *Evidence: acceptance suite.*
W149Flex, Anchor and StickycompleteD-675, D-681 and D-682, answering Q-381. A running order absorbs a twenty-minute overrun without a human retyping every following session, which is the single most common thing that happens on a show day and which the rebuild had nothing for: app.agenda_item held two timestamps and no dependency between rows of any kind. The three modes are quoted verbatim from the Bubble cards, so their meanings are Observed rather than invented. A CYCLE CANNOT BE REPRESENTED, so there is no cycle rule: a sticky item may point only at an ANCHOR and an anchor points at nothing, so the graph is one level deep and a cycle needs a path of length two. Enforced by a COMPOSITE FOREIGN KEY over (id, time_mode) with a generated target-mode column rather than by a trigger, so it holds on every path including psql, and it also refuses to DEMOTE an anchor other rows depend on. That one-level guarantee is what makes the propagation trigger safe to write at all: it touches only sticky items and a sticky item is never an anchor, so it cannot fire itself. **Propagation is PLAN to PLAN and never from actual_*, so a stopwatch cannot rewrite a published programme. A hand-written start on a dependent is REFUSED with EC420 on the W140 precedent, and the route turns it into a 409 carrying the sentence rather than a 500. D-675 also makes a segment's window DERIVE from its items, so the two clocks cannot disagree, at the named cost that an empty segment can no longer reserve one. The migration's own five assertions ran against 12 flex rows and prove only the SHAPE**; the 17-assertion suite builds an anchor with dependents and moves it, and is what proves the behaviour. Sabotage tested with a paired control: dropping the propagation trigger turns exactly two assertions red. *Evidence: 1 migration, acceptance suite.*
W150An answer names a question, not a positioncompleteD-686, answering Q-434. app.event_wizard_answer.question_n was a foreign key to an ORDINAL, because app.event_moment_question had n as its PRIMARY KEY. Inserting a question in the middle, or reordering the list, silently re-pointed every stored answer at a different question: the rows do not move, the meaning underneath them does, nothing raises or logs, and the data still reads as valid. Measured on production before writing the file: 0 answers and 0 runs, so on the database that matters there was nothing to migrate and nothing to guess, and after the wizard's first real run it would have become a migration that must GUESS which question each live answer meant. Local's 23 demo answers are what actually exercised the backfill. The wire format is deliberately unchanged: the screen still sends and receives 1 to 14, and the ROUTE resolves the position to a stable id at the moment of writing, while the position is still true, so only the mapping is positional. n also loses its primary key, which is what finally makes the ordinal ORDINARY: while it was a key it could not be renumbered at all, and the defect was hidden behind that impossibility rather than fixed by it. Found while designing the form builder, not by testing W40, because it is the exact mistake a form builder is most likely to repeat. The suite swaps two positions and asserts every answer keeps its own question, with a second assertion that the position really did move so the first is not passing on a no-op. *Evidence: 1 migration, acceptance suite.*
W151The form buildercompleteD-676, D-687 and D-688. A platform capability, not a call for papers feature: Form Builders is its own absent capability row in E-448 and the same mechanism serves the CFP, guest registration, supplier questionnaires and workforce declarations, so building it inside the CFP builds it four times. Five tables. An answer cannot name a field from a different version than its response, and no trigger enforces that: two composite foreign keys sharing form_version_id make the state unrepresentable, the same move W149 used for anchors. An answer names a field by its STABLE ID and nothing references display_order, which is W150's lesson made structural and is asserted by checking the catalogue for any key over that column. A version somebody has answered is FROZEN with EC430, because a form that gains a question after it was answered has changed what the answers mean and a required field with no answer cannot be told from one that did not exist yet. The freeze exempts display_order and only display_order, compared as the whole row minus that column rather than as a list, so a column added later is frozen by default: the first draft refused reordering an answered form and contradicted the design it implements. show_when must name a real key in the same version (EC433) and cannot self-reference (EC432), because a rule that never fires leaves its field always or never shown with nothing saying which. D-687's file answer keeps its stored object and its display name apart. D-688 adds forms.edit, granted to four administrative roles only, asserted as the INVERSE because a permission granted too widely is invisible in a count that only rises. Tenant internal only: the counterparty path needs D-671's widened grant_token, which is designed and unbuilt, and inventing a policy for a caller who cannot exist would be a policy nothing tests. *Evidence: 1 migration, acceptance suite.*
W152A grant concerns exactly one subjectcompleteD-671, answering Q-422. app.grant_token was scoped to membership_id and engagement_id, an engagement being a SUPPLIER engagement, and a call-for-papers submission is neither. One mechanism widened rather than a second token table added: one place to revoke, one to audit, one lifecycle already proven. ONE column, not two, and measuring settled that: the obvious reading is that a submitter's grant points at their submission and a reviewer's at their review, and both are wrong, because a submitter's token must exist BEFORE the submission they create and a reviewer has many. Both point at the CALL, so what a holder may reach is decided by DATA rather than enumerated by a token. The whole file turns on one check constraint whose else false branch makes a grant type nobody has declared a subject for impossible to insert: grant_type is free text and a typo previously produced a token that pointed at nothing and that nothing refused. app.cfp_call is in the same file because the foreign key needs it and is MINIMAL BY INTENT, carrying the form version, the window, blind_review defaulting to true per D-670, and the declared keys that tell an acceptance path which answers become an agenda item's name, refused by EC440 when a key is not a field of the version. It enables nothing yet and says so: a grant hangs off a membership, a membership carries a role, and the seven external roles of D-673 and D-684 are decided, unbuilt and blocked on Q-439. *Evidence: 1 migration, acceptance suite.*
Phase 8 - The client portal
W153The seven external rolescompleteD-694 and D-695, building D-673, D-684 and D-691. S1 of the client portal, and its real content was never the bootstrap's Organisation, Team and six-digit login: it is the seven external roles, decided on 2026-08-11 and unbuilt, which is exactly what W152 said it could not enable. A grant hangs off a membership and a membership carries a role, so until this file a grant token could be modelled and issued to nobody. Speaker, Exhibitor, Client, Reviewer, Vendor and Freelancer join Supplier. Every set built by ADDING to nothing, and the migration asserts cloned_from is null on all seven rather than trusting its own inserts, because a role copied from an internal one silently gains whatever the next migration adds to its parent. Measured through app.effective_permissions() and never through app.role_permission, which reads 0 for a cloned role while it inherits 7 in fact: Supplier 7, Client 3, Exhibitor 3, Speaker 3, Freelancer 2, Vendor 2, Reviewer 1, and 0 administrative codes across all seven. The sets are small because the SURFACES do not exist, and Reviewer holding one code is the honest number rather than a thin one: a submission's content is a form response and the catalogue has no code for reading one, which is raised as Q-442 instead of invented. A Client holds budgets.approve and NOT budgets.view per D-672, and the guard makes the internal budget read impossible for it rather than merely ungranted. Two refusals in the database rather than review habits: EC441, a membership on a retired role, on the update path as well as the insert, which is the path a screen-level filter never sees, and EC442, an administrative allow on any external role, judged by a family-shaped predicate so a code added by a later migration is caught by its own name rather than slipping through a list nobody extended. The three roles that granted nothing, External collaborator, Supplier portal admin and Venue portal admin, are RETIRED and not deleted, since deleting a role a live membership points at is how a person loses access with no event recording it; 0 memberships held any of them, re-measured at the moment it happened rather than trusted from the 2026-08-11 count. Adds no route and no screen, correctly: there is nothing to share yet, which is S2. *Evidence: 1 migration, acceptance suite.*
W154Template roles stop being tenant-writablecompleteD-696, B-196, and Q-249's third shape. Every tenant could rewrite the permissions of every PLATFORM TEMPLATE role. spine-schema.sql creates one policy on app.role_permission, for all with NO with check, so Postgres reuses its using as the write check, and that expression carries an is_template branch which exists so every tenant can READ the templates the access matrix renders. Measured as app_rw with a real tenant in force, not as owner: an insert of plan.administer onto the Viewer template returned INSERT 0 1, and a delete stripped 152 rows from the Tenant owner template. A template belongs to no tenant and every tenant reads it, so one tenant's write changes what every other tenant's role grants. Latent, because no route writes the table, and D-685 adds the first one, which is why this lands before the editable matrix rather than after it. The read is carried over VERBATIM as a select-only policy, since narrowing it would make a cloned role resolve as unprivileged through SECURITY INVOKER effective_permissions(), the exact failure W153 asserts against; three command-scoped write policies each constrain the role the row POINTS AT rather than reusing the read predicate. The old name is RETIRED rather than reused, and that is the interesting choice: recreating it as for select would make it multiply defined, and verify-policy-shape.sh guards those by asserting a clause only the last definition carries, which cannot exist when both definitions are textually identical and differ only in COMMAND. Retiring puts it on the branch that script derives by itself. Proved able to FAIL: the for all policy was resurrected on local, the shape check reported RETIRED POLICY IS BACK and the suite went to 4 failures, then re-running W154 returned both to green. Consequence for D-685: a tenant wanting a different Viewer clones it, which is D-178 made enforceable. *Evidence: 1 migration, acceptance suite.*
W155The access matrix becomes editablecompleteD-685 and D-698, answering Q-413. W143 shipped the matrix read only and Bubble's is a settings surface, so a customer expects it to work. D-685's three rules are TRIGGERS, not route code, and that is the whole shape of this slice: a rule in one handler is a rule the SECOND handler to write app.role_permission has to remember, and this is the one write path in the system where forgetting is a privilege escalation rather than a wrong number. So the caller is brought into the database as a fifth GUC, app.current_role, beside the four withTenant already sets. EC443 refuses editing the role you are acting under, checked BEFORE rule 1 because a caller editing their own role passes rule 1 by construction: every permission they could grant themselves is one they already hold. EC444 refuses granting a permission you do not hold. Rule 1 covers deleting a DENY as well as writing an ALLOW, which the obvious implementation misses and working the cases surfaced: under D-224 a stored deny BLOCKS an inherited allow, so removing it restores the parent's allow to a role the caller may never have been able to grant it to. Writing a deny and revoking an allow both narrow and need no entitlement. The audit trigger is AFTER, not BEFORE, so a change refused by any guard writes nothing, and it records the direction, grant, deny or revoke, rather than only the end state. The exemption is the soft spot and is measured in both directions: a connection with no acting role is exempt, which is what lets every role migration seed at all, and it is bounded by W154's row security plus the fact that a caller-less change writes no audit row so a rerun cannot flood the log. Four guards now cover this table, each answering a different question: W153's EC442 what an external role may hold, W154's row security which roles a tenant may touch, and these two what a caller may do to them. The screen's cell becomes a control only where a write could succeed, cycling through three stops rather than two because "nobody decided" and "somebody decided no" are different answers, and a template offers no control at all since the clone is the answer. The three retired roles stop being columns. *Evidence: 1 migration, acceptance suite.*
W156A form field can be identifyingcompleteD-699, answering Q-438. The THIRD hole in D-670's blind review, and W151 created it. D-670 separated author from content into two tables because row security cannot hide a COLUMN of a visible row, and D-687 then found that a file called Rosen-vienna-study-final.docx identifies its author anyway. The design said a third hole should be looked for rather than waited for. Here it is: the form is now USER DEFINED, so an organiser adds a field called "Presenter name", the answer carries the author's name, a reviewer's grant reaches the answers by design, and cfp_submission_author sitting in its own table protects nothing. Not hypothetical, and the single most likely field anybody building a submission form adds. It defaults to FALSE and that IS the decision rather than an oversight: defaulting to true fails closed, which is this project's usual reflex and is wrong here, because it hides most of a submission, organisers switch blind review off WHOLESALE to get their forms back, and a protection nobody leaves enabled protects less than a leaky one everybody uses. On the FIELD and not the call, because one form may serve a blind call and an open one. W151's freeze covers the new column for free and this is the first migration to rely on that: the freeze compares the whole row minus display_order rather than a list, so identifying cannot be changed on an answered version, which would otherwise retroactively change what a reviewer already saw. Measured rather than trusted, by reading the function body for the shape. Two functions in one place: what a blind call withholds, which returns NOTHING for an open call because blindness is the condition rather than the mark, and the publish-time warning, which is deliberately a small heuristic and a warning rather than a refusal, since the organiser is the one who knows whether "Affiliation" identifies anybody in their field. It cannot promise blindness and the suite asserts that it cannot: a name typed into a free-text abstract is caught by nothing here, asserted so a green run is never read as anonymity. *Evidence: 1 migration, acceptance suite.*
W157A resource names the roles it shares withcompleteD-697 answering Q-443, and D-691's one axis. S2 of the client portal, and the slice arrived specifying a folder tree this system refuses: Folder, parent_folder, inheritance on create, fan-out on add, fan-in on remove and Membership.source. Removing that left nothing that was a spine, so the shape had to be DECIDED rather than recovered, which is what Q-443 asked and D-697 answered. A polymorphic share join, not a central app.resource table, because every object that would become a resource already exists with its own identity, its own policies and its own migrations, and a spine table means a backfill plus a second identity for each of them. Not per-slice sharing either, because four slices sharing themselves is four chances to disagree and "who can see this" would stop having one answer. The accepted cost is that (resource_type, resource_id) carries no foreign key, and the whole justification of the design is that a trigger discharges it: EC445 refuses an undeclared type, the case having no fallthrough so a free-text column cannot carry a typo pointing at nothing, which is W152's else false one table across; EC446 refuses an id naming nothing, on insert AND on repoint, because a share created legitimately and then moved is the path a screen never sees; EC447 refuses sharing with a RETIRED role, since W153's EC441 stops one reaching a membership so the share would reach nobody and never could. The guard is deliberately NOT security definer: it runs as the caller, so sharing a resource the caller cannot read is refused as an orphan, and definer rights would let them confirm the row exists. One read written once, resource_is_shared_with_role(), which takes a role rather than a GUC so an organiser can preview what a Speaker sees. It shares nothing yet and the file says so, the same honest state W152 shipped in and the point of building the spine first. *Evidence: 1 migration, acceptance suite.*
W158A Client reads a projection, and approving one freezes itcompleteD-672 answering Q-430 and D-683 answering Q-432. W153 gave Client budgets.approve and deliberately NOT budgets.view, so until this file that role held the right to approve something it could not read. The projection is a TABLE and not a permission-filtered view, which is the whole of D-672: budgets.view_financial and budgets.view_margin already withhold the cost and margin FIGURES, and they do not withhold line-level STRUCTURE, so a client reading "Catering, 40 covers" learns something real with the figure hidden. A projection carries only what was CHOSEN for them. The line has no cost, markup, received-discount or supplier column and their absence is the design: a column that does not exist cannot be exposed by a forgotten permission check, and the assertions state that as an absence BY NAME with a positive control, so a cost column added later fails the build rather than leaking quietly. label is written rather than copied, because an internal line name can itself be disclosive. The snapshot is CONTENT and not a reference, D-683: a projection is derived and moves when the budget moves, and a client who approved 40 covers at one price has not approved 60 at another. Taken by a TRIGGER at the moment of decision rather than by the route, for W155's reason: a snapshot the route must remember is one the second route to decide an approval forgets, and the failure is silent until a dispute, which is the one moment it cannot be repaired. Append-only twice over, by trigger and by the deliberate absence of any update or delete policy. It is S2's first consumer and it proves D-697 was right: a Client reaches a projection because the projection is SHARED with the Client role through W157, and adding budget_projection as a ninth type is an edit to a reviewed case, exactly as W157 said. A DRAFT projection cannot be shared, because a draft that could be shared and then published would have been readable while it was being written. An internal approval on a budget with no published projection still succeeds and takes no snapshot, the absence being the record that nothing was published. *Evidence: 1 migration, acceptance suite.*
W159A venue type proposal replaces or addscompleteD-668, completing Q-409 and answering Q-416. The replacement pass and the additive breadth pass become ONE worker answering both questions per venue, at half the token cost, with the model holding the record once. The two halves are constrained DIFFERENTLY and that asymmetry is the decision: primary REPLACES, so a wrong answer REMOVES a type and it stays narrowed to the codes the venue's own compound splits into; also ADDS, so a wrong answer removes nothing and is the error a reviewer spots fastest, and it takes the open vocabulary. Asymmetric constraint for asymmetric consequence. kind defaults to replace and that is not a coin toss: every row W146 ever wrote was a replacement, so the default makes all of them correct with no backfill, where a default of add would silently reinterpret them and the accept path would insert types instead of moving them. An additive row still names its origin, because from_code records which classification the venue held when the addition was proposed, and it may not equal code, since "add the type it already has" is a no-op a reviewer would accept believing they had done something. The accept path branches on the ROW's kind and not on the caller, because a route that had to choose is a route that could choose wrongly, and the replace branch inserts BEFORE it deletes so a failure leaves the venue with the type it had rather than with neither. Its cohort turned out to be nearly finished and the handover's figure was stale: measured on production, 0 venues are legacy-only, 9 064 proposals are accepted and 897 are open. The 14 920 untyped venues are a different cohort and cannot enter this queue at all, which is Q-444. *Evidence: 1 migration, acceptance suite.*
W160The accept function promotes the primary firston productionFixes W159, found by rehearsing the 897 open proposals against production inside a transaction that rolled back. W159 described its replace branch as W147's promote-then-delete and implemented only the delete: it inserted the precise code into app.venue_venue_type and removed the broad one, never touching app.venue.venue_type. W140's venue_type_keeps_primary trigger refuses to delete a venue's CURRENT primary from its type set, so on every venue whose legacy column still held the broad code, which was all 897, the accept raised outright. It had also dropped W148's ai_approved provenance stamp, which is what stops a later ai_direct pass at rank 10 overwriting a human decision at rank 40. THE ORDER IS THE FIX: promote the primary, letting W140's forward trigger add the new code to the set, and only then delete the broad one, which is no longer the primary. A replacement whose origin is not the current primary now raises EC449 rather than marking itself accepted having done nothing. The additive branch touches neither the primary nor the provenance, because provenance.venue_type describes the primary and an addition does not change it. The suite passed against the broken function and that is the part worth keeping: its fixture created a venue with venue_type NULL, and W140's guard only fires when the row being deleted IS the current primary, so thirteen assertions passed against a shape that does not occur in production. The fixture now carries a primary, and the fix was proved by restoring W159's original function on local and confirming the suite can no longer pass. The migration asserts the ORDER of the two statements rather than their presence, because a function carrying both in the wrong order satisfies any check that only asks whether they exist. B-199. *Evidence: 1 migration.*
W161The opera keyword stops matching a resident companyon productionWhere the 2026-08-13 review's one systematic defect came from. W146 proposes with v.name ilike k.kwd and the map carries %opera%, so in 22 of 176 cases it fired on a parenthesis naming the opera company RESIDENT in the building rather than the building's type. The fix is PER KEYWORD and not global, and that is the whole point: stripping parentheticals for everything would lose six genuine theatres, measured, because a theatre's parenthetical is a TRANSLATION and real evidence -- Bayreuth Festspielhaus (Bayreuth Festival Theatre), Det Kongelige Teater (Royal Danish Theatre). Strip those and the venue's own name carries no theatre word at all. A plain ILIKE strip then broke four more, which only surfaced by measuring against production: Deutsche Oper Berlin, Oper Leipzig, Alte Oper and Hamburgische Staatsoper say "Oper", and %opera% caught them ONLY BY ACCIDENT, through the English translation sitting in the parenthesis it now strips. So the opera keyword became the POSIX pattern the review had already validated, oper\M matching a word end without matching "operation". The corrected rule is strictly better: it drops 22 false positives AND finds 6 genuine opera houses the original missed entirely, Komische Oper Berlin, Neue Oper Wien, Staatsoper Hannover among them. The rule moves into app.propose_venue_types() so there is ONE of it, W146 remaining as the history of how the queue was first filled. The assertions name venues in both directions rather than filtering on kind: the first draft selected kind = add as "the resident-company rows" and reported six failures that were genuine opera houses, because the owner's decision on the 42 competing pairs had made those additive too. A filter that happened to select the right set yesterday is not a test. *Evidence: 1 migration.*
W162A city is not yes in any languageon productionReported by VELA 2026-08-13, extending W65. W65 already decided this in ENGLISH ONLY and that is the whole defect: it nulled 892 venues whose city was yes or no, having found a boolean in Addresses.city, and its list carries none of the Hungarian words the same export also holds. 49 venues carried a city of Igen and 2 of nem, plus 63 in region, which W65 never looked at. It is NOT the column misalignment the report reasonably feared, and that was measured rather than assumed: every one of the 51 carries a correct address_line, so only the two columns fed from the form's yes/no answers moved. Nor is it Hungarian-only -- 28 are HU, which is why the report saw 28 while paging country=HU, and the rest are ES, PT, IT, PL, IE, MT, SE, US, CA, CR and six with no country at all, all carrying the Hungarian word because the FORM was Hungarian rather than the venue. Nulled, not parsed, D-598 and W65 unchanged: the real city is legible in the address and parsing it is a guess, and W82 measured what the obvious parse does, making California a country on 254 rows. A wrong city is worse than a missing one because it puts a venue in the wrong search results rather than in none. The rule is a FUNCTION now, so the next language is one line in one place, and its token list is OBSERVED with each entry naming the migration that found it, never guessed. The cost of a token list is that a real place can share a word, checked rather than assumed: none of the 51 has an address mentioning any place called Igen. *Evidence: 1 migration.*
W163A message is for the participants or internal onlycompleteS3 of the client portal, implementing the slice's own instruction exactly: notes and messages are ONE TABLE with a discriminator, because splitting them duplicates every read path and is how internal content eventually leaks. app.chat and app.message already existed from W31 and are polymorphic on target_type, the same shape S2's share join uses, so S3 EXTENDS rather than introduces. THE ORDER IS THE WHOLE ARGUMENT. messaging is an INTERNAL surface, so no external caller reaches a message at all today and the gate condition is satisfied trivially right now. verify-policy-shape.sh records W28.1 and W29.1 as the two most expensive reversions in this project, and in BOTH a surface was opened to counterparties and the policy widened AFTERWARDS in a separate migration, so re-applying the earlier file alone exposed a tenant's internal files and its equipment plan. So the invariant goes in BEFORE the surface opens, and this file enables nobody exactly as W152 and W157 did. visibility defaults to participants, so every message W31 ever wrote keeps its audience: a default of internal would retroactively hide a tenant's history from its own counterparties the day the portal opens. The refusal is row security and not route code, because the gate asks for invisibility through the UI AND the API, and a route filter satisfies the second only by being written correctly in every route forever. current_role_class fails CLOSED, defaulting to external, so a call site that forgets gets the smaller audience. Both read and write carry the clause, since a counterparty who could POST an internal note writes into a channel it cannot read. EC450 a mention must be a participant; EC451 an internal note cannot mention an external one, because the mention would tell them it exists. The suite proves it as app_rw with two callers who are BOTH participants and differ only in class, and checks that selecting the note BY ID returns nothing, because a filtered list is a weaker guarantee than an unreachable row. *Evidence: 1 migration, acceptance suite.*
W164A task field names the roles that may see itcompleteS4 and D-703. Most of this slice already existed under different names, which checking rather than renaming is what found: Task is W26's app.task, Subtask is its parent_task_id, Board is app.checklist and List is app.status_lane. The gap was the one the gate condition tests, custom fields and who may see them. THE DESIGN CHANGED, D-703. S4 specifies visible_to_client and visible_to_supplier, which is two named AUDIENCES, and D-691 settled that sharing has one axis and audience is not a concept above role. The skill's invariant 6 is not in conflict and reading it closely is what resolved it: it guards against a single visible_externally flag that exposes a field to a Supplier the moment somebody shows it to a Client, and a ROW PER ROLE satisfies that for all seven roles rather than two. Two columns also do not survive the vocabulary, since seven roles means seven columns and an eighth means a migration over user data. Not app.field_policy either: that is gate 5, mapping a fixed column to a permission code, and a tenant cannot mint a permission code. Absence is hidden, so a field arrives invisible to every external role without anybody setting anything, which is what "defaults to false" means when the flag is a row. The DEFINITION is readable and the VALUE is not, because knowing a field called "Internal margin note" exists is not the disclosure and hiding the definition too would render a board with holes nobody can explain. EC452 refuses naming an internal role, which would change nothing while a reviewer believed it had. Enforced in row security as the slice insists, since a hidden field filtered in the client is not hidden. The suite proves the slice's sentence word for word, and 2e is the clause a shared flag would fail: after exposing the field to the Client, the Supplier still sees nothing. *Evidence: 1 migration, acceptance suite.*
W165The approval spine, S5's gatecompleteClient portal S5, D-693. Approvals as a first-class subject with ordered steps and a mode, any_one or all, over W26's tasks and the resources S2 defined. The gate condition is asserted against the database rather than through a screen. *Evidence: 1 migration, acceptance suite.*
W166The requirement register and the audit exportcompleteThe rest of S5's gate. Requirements carry an acceptance test and an owner, and the audit export answers what was decided, by whom, and when, as a query a human can run. *Evidence: 1 migration, acceptance suite.*
W167Client quotes and the invoice file lockcompleteS6's gate, D-705. Quote to acceptance to a finalised invoice, and the deliverable does not unlock until payment is recorded. **Q-447 is open on whether a Hungarian *elolegszamla* may be ISSUED**, so generated invoices stay draft until an accountant answers. The arithmetic is proved; the document's legal status is not this project's call. *Evidence: 1 migration, acceptance suite.*
W168The event overview ships the query that produced itcompleteS7's gate taken literally: app.event_overview() returns metric, value and source_query, and the value is produced BY that statement. The suite runs each published query and compares it to the number printed beside it, so a dashboard and its own documentation cannot drift. requirement_compliance_pct is NULL and not 0 on an event with no requirements, because a client-facing "0% compliant" is a wrong number. Repaired 2026-08-17: it borrowed the tenant's first event and measured a proportion over a population W181 had since added to, reading 60 where it asserts 50. It seeds its own event now. *Evidence: 1 migration, acceptance suite.*
W170The delivery phase, whose state is derivedcompleteS4. A phase's state is computed from the tasks inside it rather than stored, so a board and its phase header can never disagree. *Evidence: 1 migration, acceptance suite.*
W171Phase progress joins the overviewon productionW168 deliberately left phase progress out and said so in its own header: a progress number over a table that did not exist would be the wrong kind of wrong. W170 created the table, so this adds the metric, with its own published query like every other. *Evidence: 1 migration.*
W172The crowd spinecompleteD-708. Nested zones with declared capacity, four input sources, and an occupancy that is a PERIMETER calculation rather than a running total per gate. Built from the owner's real UEFA fan zone data: Gate 4 took 28 792 entrances against 36 807 exits because the crowd entered at one end of Varosliget and left at another, and any model keeping a per-passage total goes negative on it. The fixture reproduces the asymmetry at 1:1000. *Evidence: 1 migration, acceptance suite.*
W174A secret is not a hashon productionCorrecting W172. crowd.feed.secret_hash promised a property the mechanism cannot have: an HMAC signature must be verified against the secret itself, so the column could never hold a hash. Renamed to signing_secret, which is what it is. *Evidence: 1 migration.*
W175Alerts that do not spam, and an acknowledgement that names a personcompleteA zone parked above its threshold raises ONE alert across five evaluations, not five. Escalation from warn to critical is new information and opens a second; falling back closes both and raises nothing. Repaired 2026-08-17: its fixture borrowed an event that already carried the demo data's own recipient, so every delivery count doubled and 2b read 6 where it asserts 3. *Evidence: 1 migration, acceptance suite.*
W176Queues, dwell, and the reportcompleteDwell time by Little's Law from occupancy and throughput, so nobody is ever followed. The report is the surface a sponsor stand's dwell can already be read from, if the stand is its own zone. *Evidence: 1 migration, acceptance suite.*
W177SMS needs a number, and an alert may point at an approvalon productionOwner decisions 2026-08-15: Brevo for SMS, and an alert stays its own record and links to the approval it caused rather than raising one automatically. EC465 refuses a recipient configured for SMS with no number, because that is a recipient who is set up and silently never texted. *Evidence: 1 migration.*
W178A wait estimate needs a sampleon productionFound by using it. A marshal recorded a queue of 240 on a gate that had admitted ONE person in the trailing fifteen minutes and the screen said "About a 3428.6 minute wait at this rate". A rate computed from a sample too small to be a rate is not an estimate, and the screen says so instead. *Evidence: 1 migration.*
W185Outbound webhooks, the spec implemented literallycompleteS7. Full event catalogue, a signing secret, three retries at 0, 10 and 100 seconds, and a delivery log that stores the endpoint's own response body, which is the thing that makes an integration debuggable. withWebhookJob is the elevated wrapper, narrowed to four tables. *Evidence: 1 migration, acceptance suite.*
W186Two permission codes for webhook administrationon productionW185 shipped the spine with no permission of its own and the first route written against it guessed at tenancy.view and tenancy.manage. Neither exists. A guessed code is not a weak control, it is no control: has_permission on a code nobody holds refuses everybody, and on a code that does not exist it refuses in a way no grant can ever open. *Evidence: 1 migration.*
W187A rejection reopens the subjectcompleteS5's last gate clause, per subject type. A rejected approval does not merely record a no; it returns the subject to a state where the work can be redone, which is the difference between an audit trail and a workflow. *Evidence: 1 migration, acceptance suite.*
W188API tokens as a grant typecompleteS7. Bearer tokens shown exactly once, hung off a MEMBERSHIP rather than off a tenant, so a token sees exactly what its holder sees. Measured over HTTP and recorded in the suite as a comment because it cannot be reproduced without it: a token returned EIGHT events while its tenant owns seven, and the eighth is an app.event_venue_share the same membership sees in session. That is the whole security argument for the design. The list endpoint never returns the token or its hash. *Evidence: 1 migration, acceptance suite.*
W189A crowd alert reaches the bellon productionQ-451, owner decision 2026-08-17. app.notification_type held thirteen codes; twelve carried both email and in_app and crowd_alert carried neither, so W12 reported it in as many words: "nobody could ever be told". in_app ALONE, and that is the decision rather than an omission: the dispatcher ACTUALLY SENDS on the email channel while the crowd module already delivers its own email and SMS through crowd.alert_delivery, so wiring email for consistency would be a second message about an alert the recipient had already been emailed and texted about. Not mandatory, because a channel that cannot be silenced is the one operators learn to ignore. The missing LABEL was found by this change, which is the argument for it: notification_type_label had no crowd_alert row, and nothing surfaced it because a type with no channel never reaches the preference matrix, so the blank name was invisible for exactly as long as the alert could not reach anybody. The three already-composed notifications are backfilled with an in_app/sent delivery, which records something TRUE because for the in-app channel the row IS the delivery. Applied to local only. *Evidence: 1 migration.*
W190The calendar feedcompleteS7's last in-scope item, D-710 to D-712. One-way ICS at /calendar/v1/<token>.ics for the subscriber's dated tasks and the delivery phases of events they can see. Its own grant type and NOT an api_access token, which is the decision: a calendar URL is pasted into Google or Outlook, stored there, and polled unattended for years, and W188's token is a full REST credential for its membership. The scope was measured rather than assumed -- the first version returned 562 entries, every dated task belonging to everybody, because row security scopes to the tenant and tasks.view is tenant-wide. The all-day DTEND is exclusive, which is the rule every naive ICS writer gets wrong and reads as an off-by-one nobody can find because the stored data is correct: a phase ending on the 3rd emits the 4th, asserted in a unit test and again over HTTP. Diverges from portal invariant 8 by having no passcode, because a calendar client cannot prompt for one; recorded as D-712 and needs owner confirmation. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W191The quote vocabulary, and the invoice that may leavecompleteD-713 and D-714, the two owner decisions of 2026-08-18 that needed code. Both were measured before being implemented and one of them did not survive it. D-714 says the code writes proposed where production reads submitted; api/src/index.ts writes submitted at 9194 and 9204, POST /quotes/submit, both branches, and proposed is W9's SEPARATE stage, a line the supplier proposed which an internal user confirms into draft before it is priced. index.ts:10034 promotes exactly proposed, and W10 already fixed a bug born of treating the two as one, with the note still in PortalQuotes.tsx:721. A literal rename would make the confirm statement match quotes a supplier had already priced and silently reopen them. So no screen and no route changed, because none had to: what was missing is the ONE thing D-714 names as the trap, the constraint. quote_state_known pins draft, proposed, submitted, removed, accepted. rejected is excluded because nothing writes or reads it and W187 reopens to draft; accepted is included because index.ts:9811 reasons about it by name. Nothing was migrated, no row on any target being outside the set. D-713 had no code lock either. W184 already made draft -> issued legal and put Finalise on the screen, so the lock was a PROHIBITION IN PROSE, and its only executable copy was the Q-447 warning inside app.new_quote_invoice. pg_proc.prosrc holds the BODY and nothing above it, which is why W167's warning sat where no reader on the database could see it and why lifting it needed the citation moved INSIDE the body rather than an edit to a file. A generated invoice still arrives draft: D-713 permits issuing, it does not pre-issue. Opens Q-455, which was invisible until the lock came off: an issued invoice still carries no invoice_number, app.allocate_number exists and nothing calls it. On local, dev and PRODUCTION, 2026-08-18. Production held exactly what Q-450 measured on 2026-08-16, 1 draft and 3 submitted, both inside the set, and holds the same four rows after: the constraint refused nothing and nothing was migrated. Assertions 2 and 3 write their probe rows inside a plpgsql subtransaction that unwinds on the way out, verified on production afterwards at 0 rows surviving. migration-ledger.sh reports 317 probes, every migration present on prod and on dev. *Evidence: 1 migration, acceptance suite.*
W192The custom fields W164 built and nothing readcompleteS4's UI half, and a correction to the slice sheet that recorded it. MEASURED 2026-08-18: grep task_field api/src returned nothing and grep show_on_card web/src returned nothing. W164 shipped three tables with per-role visibility on 2026-08-14, proved at the database layer by twelve assertions, and for four days no route and no screen touched any of it. THE SLICE SHEET WAS WRONG ABOUT WHY. It recorded show_on_card as unread "because nothing renders a card yet" and listed Kanban and My Tasks as not started. ChecklistScreen has had a Kanban board with lanes and cards since W26 on 2026-07-29 and TasksMineScreen is My Tasks. The board was never missing; the routes were. Three rows on S4 are corrected rather than left to be inherited again. INTERNAL AND STAYING INTERNAL. tasks is internal in surface.ts and nothing here changes it. W164's external read half stays proved where W164 proved it, because opening a surface to make a policy demonstrable is the order verify-policy-shape.sh exists to prevent, and the portal read path is blocked on Q-442. The read still does not filter by hand, so the day an external caller can reach a board the answer is already right. TWO DEFECTS ITS OWN SUITE FOUND. A duplicate key left as 503 "retry: true", which is a lie in the one direction that costs most: retrying can never work because the row being duplicated is still there. Fixed in the SHARED asRefusal so every sibling caller gets it, 23505 to a 409 naming the constraint and never the values. And visible_to_role_ids arrived as the STRING "{}" because this driver runs with fetch_types off on Workers, so the suite's own includes() was a substring match on {uuid} that reported a working share while the empty case was undetectable. Two flat queries and a Map, and the assertions now check the shape as well as the contents. DRIVEN END TO END IN THE BROWSER, not inferred from a build: a field created through the screen, toggled onto the card, a value typed in the panel, and the value appearing on that one task's row while the other four read "not set". *Evidence: acceptance suite.*
W194An issued invoice carries a number, or it is not issuedcompleteD-718, closing Q-455. D-713 permitted issuing on 2026-08-18 and that made the gap visible: invoice_number was nullable, W28.3 built allocate_number and number_series, and nothing called either. Allocated on the draft to issued TRANSITION and not at creation, because the allocator increments a counter, so allocating at creation burns a number on every abandoned draft and leaves gaps, which is the first thing an auditor asks about. Ambiguity refuses rather than guesses, owner 2026-08-19: no active series refuses, more than one refuses and NAMES THE CODES, because "you have two" is not actionable and "you have HU and UK" says which to deactivate. Measured before building and it changes how this reads: there are ZERO active invoice series on local and ZERO on production, so the refusal is what every tenant meets until somebody creates one, which is only fair because NumberingScreen exists. Local carries 67 invoice series and every one is INACTIVE, left by W28.3s suite, so without the active filter this database alone would refuse every issue as ambiguous. 11 assertions plus all three route paths driven over HTTP. *Evidence: acceptance suite.*
W196Sponsors and exhibitors, and two grey entries stop being greycompleteM-24, the owner-confirmed brief at E-310 and D-086 to D-088. Three of D-621s five unbuilt navigation entries were this module, rendered greyed precisely so the shape of an event with holes in it is visible; two of them now render. One account model with a type discriminator, which is the briefs own instruction because many partners do both and two tables would make such a partner two records that disagree. The entitlement LINE is the load-bearing idea: a sponsor never asks whether Gold was delivered, they ask whether their two newsletters went out, so package is a label on a line and fulfilment status lives on the line alone. Progress is computed, never stored, W170s shape, and entitlement_progress is SECURITY INVOKER so a caller who cannot see a line does not have it counted for them. Both vocabularies are pinned the day they are known, which is the D-714 lesson applied forward rather than after a third spelling appears. Its own suite found three defects: a permission code granted to NOBODY so every route answered denied at gate 3, an INNER JOIN to the platform-scoped app.company that silently returned an empty list while the rows existed, and a create form that did not exist behind an endpoint the suite had just proved worked. *Evidence: 1 migration, acceptance suite.*
W197The commercial grants W196 forgoton productionA code nobody holds is as unreachable as a code that does not exist, and it fails LATER, at gate 3, where it reads like a role misconfiguration rather than a missing migration. Granted BY NAME across tenants rather than to the template alone, because a role cloned before the migration has its own row that no template grant reaches. Edit to the roles that run the relationship, view to Finance admin because the briefs own split is that finance sees margin and sales runs the deal, and nothing to Contributor so the refusal branch has somebody to refuse. *Evidence: 1 migration.*
W198The sign-in shim, and Ory without a subscriptionpartialQ-458 answered by NOT creating the surface it asks about. The owner refused a paid Ory plan on 2026-08-22, and that is what decides the architecture: with no custom domain the browser cannot reach Ory at all, because CORS is closed for every origin including localhost:5173 and the session cookie is scoped to projects.oryapis.com. Both are browser rules, and a Worker is not a browser, which the Q-460 probe had already proved by calling Ory forty times from Cloudflare with no CORS anywhere. So sign-in gets its own Worker on its own hostname, which is Q-458's own fallback answer, and api.event.clinic gains no route and no PRE_AUTH_SURFACES entry. It holds no Ory API key and still carries a four-value allowlist, because defence that rests on a single absent secret is one rotation away from being nothing. One renderer over ui.nodes IS all four flows, measured at 4, 8, 3 and 2 nodes, so a second factor or a passkey later changes no code on either side. Two things it got wrong first and now records: the dotted node names need nesting for the JSON flow, done once in the shim so no client reimplements it and with prototype keys dropped because the body is browser-supplied; and the submit node carries method, so a renderer that draws buttons and posts nothing for them answers 4010003 and cannot sign anybody in. *Evidence: 1 design doc.*
W199Google sign-in through the shimpartialCloses the gap W198 left open, and changes no Worker code at all: the shim already passes a submitted body through, so this is web-only and nothing was redeployed. W198 refused to draw the Google button because Ory's hosted redirect ends by setting a cookie on projects.oryapis.com that no browser sends to api.event.clinic. The path that works was measured before it was built: Ory's API flow takes method=oidc with a provider and an id_token, and a junk token answers 403 "Could not verify id_token", which is the endpoint rejecting the TOKEN rather than the METHOD. The provider id is read off the flow and never constructed, because Ory appends a random suffix and a bare google 400s, which the registration checklist records costing a round trip with Google. The client id was derived from Ory's real redirect rather than retyped. renderButton and not One Tap, because prompt() is suppressed by cookie policy, FedCM state or a previous dismissal with no signal to the page, and a sign-in that silently does not appear is the worst failure this screen can have. One button logs in OR registers, retried once and narrowly: a 403 naming the token is never retried. *Evidence: 1 design doc.*
Phase 9 - The suite boundary and accreditation
W200The sellable unit gains a kindnot builtD-721 answering Q-461, and it opened Q-462. ITS SCHEMA SHIPPED INSIDE W201 AND THIS ROW READ not built FOR SIX DAYS. Measured 2026-08-28 on local, dev AND production: app.layout_unit carries kind and doc_item_id, all three D-721 checks, the stand-label unique index and the transitional app.layout_seat view, and W201's own section 3 is headed *THE SELLABLE UNIT, in its D-721 shape*. W201 absorbed this slice when D-722 moved the engine into this spine. The row was not wrong about the REPOSITORY - status is derived from files matching w200-* and there is no such file - it was wrong about the world, which is the failure mode a generated board is supposed to prevent and the reason this note is here rather than a status flip. And it is not pending in Estate either. This row said the branch was not merged; the branch REF is not an ancestor of master, which is what that measured, but the CONTENT is on master as 349ce7c, a SQUASH-merged pull request. A squash merge leaves its source branch permanently un-merged by ancestry. So D-721 was implemented TWICE, in two chains, converging on the same shape by luck rather than design. Q-489. The half that was genuinely missing, the write path, is W225. *Evidence: 1 design doc.*
W201The shared layout engine moves into this spineon productionD-722, answering Q-462. app.floor_layout, app.layout_unit and app.instantiate_layout sit in event.clinic's app schema and are read and written by event.clinic's routes, and were created by ESTATE's migration chain. Measured 2026-08-22: no spine file created any of them and no probe named them, so migration-ledger.sh prod would have exited 0 with the whole engine absent. What forced it rather than leaving it a tidiness question: W196's entitlement line has to point a foreign key at a sellable unit, and spine-rerunnable builds from THIS spine alone, so a foreign key whose target is created in another repository cannot be declared in a file CI loads by itself. *Evidence: 1 migration, 1 design doc.*
W202An entitlement line names the stand it soldon productionD-721's payoff, and the foreign key itself. Until this, W196 could sell "a 3 by 2 corner stand" as free text with nothing connecting it to anything on a drawing. It could not be written until D-722: app.layout_unit was created by Estate's chain, so a references clause would have failed CI on a table that repository never loads. W201 moved the engine in and this is the first thing that depends on it. Three rules enforced: the unit exists, it is a stand, and it belongs to the same tenant. *Evidence: 1 migration.*
W203A stand is sold oncecompleteW202 stopped a line naming a nonexistent stand and did not stop two lines naming the SAME stand - the error a picker makes easy: two salespeople, two accounts, one corner unit, and nothing objects until an exhibitor arrives to find somebody already in their space. A unique index and not a check in the route, because the screen greying out a taken stand is a courtesy rather than a control: it is defeated by a stale page, a second tab, or the next route somebody writes. A constraint is fooled by none of those. *Evidence: 1 migration, acceptance suite.*
W204Three reference tables get their RLSon productionAnswers Q-466, found by verify-repo-guards.sh exiting 1. Measured on dev AND production, identical on both: dress_code_reference, music_genre_reference and budget_line_zero_reason_reference had no RLS at all and zero policies, and webhook_event_type had it enabled but NOT FORCED, so the owner bypasses it. Hygiene rather than a leak, and the distinction was measured rather than assumed: none of the four carries tenant_id or any tenant-scoped key, so no tenant data can escape through them. They are outliers rather than a declared exemption - nine sibling reference tables carry the same two-policy shape. *Evidence: 1 migration.*
W205Two roles may read what they publishon productionD-726, answering Q-470 and extending D-633. Admin and Venue portal admin held files.publish and **no other files.* grant, on local, dev and production, so each could publish a file it could not read. This is D-633 seventeen days late, not a new rule:** Q-392 asked the same question on 2026-08-06 for Tenant owner and D-633 answered *"GRANT ALL FOUR. It was a seed omission, not a policy"*, naming one role while the identical omission sat on two more. files.view only, and the narrowness is the decision. Closed by GRANT and not by exemption - an exemption line would have recorded the defect as intended behaviour. *Evidence: 1 migration.*
W206Three branches become childrenon productionOwner instruction 2026-08-23. Utilities under Venue and Site, Special Effects and Rigging under AV. All three were level-1 roots with no parent edge at all, carrying 11, 14 and 15 direct children, so this moves three whole subtrees down a level and the taxonomy goes from 22 top-level branches to 19. Identifying that "AV rigging" meant the existing Rigging root took measuring rather than guessing. *Evidence: 1 migration.*
W207Marketing, media and PR get a service taxonomyon productionOwner 2026-08-23. The gap was measured before it was designed: Marketing, PR, Advertisement carried 82 nodes against AV's 865, and almost none were services - Media & Press held 23 children that were ALL EQUIPMENT (broadcast clock, camera platform, SNG uplink, press riser, mult box), Brand Activation more equipment, and Marketing, Advertisement, PR and Publications were bare leaves. 57 service categories added, marketing 82 to 139 nodes. It deliberately did NOT merge PR with the near-duplicate Public Relations / PR, because merging repoints supplier rows and needs a count first - Q-473. *Evidence: 1 migration.*
W208Admin receives what the seed never gave iton productionQ-471 answered by option (b), the reviewable list, D-733. spine-seed bulk-grants whatever the catalogue holds at the instant it runs, and on conflict do nothing means it can neither notice nor repair a shortfall later, so production never received seventeen codes a build has. The list had already been transcribed as sixteen: Q-471 writes files.create/delete/edit/view, four codes in one slash token, and a hand-off expanded it to three. This file counts its own array and fails if it does not hold seventeen. Measured before choosing: re-running the seed would now be 33 grants across four roles, not 17, and would hand estate.casualty.personal to two roles by way of a select that never mentions it. That code is special-category health data held by NO role anywhere, so it is excluded and the exclusion is ASSERTED rather than remembered. Admin's gap is now exactly one code on all three databases. Its first floor of 118 catalogue codes would have failed CI: a spine-only build holds 117, because the 50 estate.* codes come from Estate's chain. *Evidence: 1 migration.*
W209The accreditation spine, all four pillarson productionRoadmap slice W30. 13 tables in a schema of its own, accred (D-727), nine permission codes, accred.pass_decision(), and a 16-control gate condition CI runs as app_rw. Category model with a per-organisation quota, numbered colour-coded access zones with time windows and the category-against-zone matrix, the request-to-pass lifecycle on the Approval Engine, and live enforcement at a gate with an append-only log. Its centre is that it did NOT build a second occupancy engine (D-728): the 2026-07-16 model proposed a ZoneOccupancy counter and left it an open question, and W172 had answered it three days earlier with a figure computed over a PERIMETER and returned with its own age and a health verdict. So a band names a crowd.zone, a gate names a crowd.passage, and a denied scan is not a passage (D-729) - the bridge counts only an allow, proved in both directions by sabotage. The QR token is stored only as a hash, quota used is counted and never stored, and the holder is app.person with no second identity table. Three defects a full CI simulation caught: nine codes declared record_scoped = false, which is how a write opts out of the per-record ACL entirely; an exact count of = 13 forced-RLS tables that W210 falsified within hours, taking re-runnability and the ledger probe with it; and accreditation.view_identity_document reaching the Viewer role on PASS 2, because spine-seed sweeps every non-financial view code. It is registered in app.field_policy now, which is the seed's own exclusion mechanism, and the file REPAIRS rather than only asserting - a migration that merely detects an exposure can never be applied to the database that has it. *Evidence: 1 migration.*
W210The accreditation compliance layeron productionQ-477, the follow-up W209 raised against itself: it stored a passport photograph, an identity document and a police screening result with no consent record, no retention limit and no parental consent path. Consent with withdrawal, terms and their acceptance, reissue, sanctions, and retention. A pass cannot be issued while a required consent is missing and the DATABASE refuses it, naming the missing type (D-736) - a consent table nothing consults is a table of good intentions, and the screen that forgets to check it is the screen that ships. has_consent() tests granted AND not withdrawn, because a check reading granted alone returns true forever after somebody says no. A reissue revokes the old pass in the same statement (D-737); two application steps leave a window in which both cards work. Erasure nulls columns, never deletes rows, and never rests on a cascade (D-738): nobody deletes a person who holds a credential, so a cascade-based Article 17 erasure erases nothing and looks implemented, while the pass and the scan log are safety records an inquiry reads. Still switched off, deliberately (D-739): the retention period is nullable with no default, because inventing ninety days would be a legal position taken by a migration, and nothing calls the erasure yet. Two sabotages caught by name. *Evidence: 1 migration.*
W211The accreditation accessory layeron productionQ-476 in part, design section 13, from the MotoGP and WorldSBK security handbooks. The base pass grants a zone; the most sensitive sub-areas are granted by a separate quota-controlled accessory stacked on top - a bib, a vest, a tabard - and that is the mechanism keeping the field of play and the grid to a hard headcount. The quota is enforced by the DATABASE and not by the screen (D-740), naming the accessory and both numbers, for the reason W203 settled about a stand being sold once: the desk that issues these is under time pressure on a race morning with two tabs open and a stale count in one of them. An accessory grants only while issued, so a returned bib shuts the grid on the next scan (D-741), and a POOLED one grants nobody access while still counting against the quota, because four bibs in a box are four people who can reach the signalling wall whatever the database thinks (D-742). accred.pass_decision() was rewritten to read one set of effective zones (D-743), the pass's own plus its live accessories, because a gate admitting on one rule while the printed card lists another disagree the first time somebody is refused at a door their pass says they may enter. W209's and W210's gate conditions were re-run against the rewrite and both still pass. The vehicle credential class is unbuildable rather than deferred: app.vehicle and app.delivery do not exist. *Evidence: 1 migration.*
W212Validation, and the centre where it happenson productionQ-476 in part, design section 14, from the Sydney 2000 and LOC 2011 Olympic manuals. It closed a hole W209 left, and the hole was MEASURED rather than inferred: probed on local, with validation_required = true a pass could be created directly as active, so the whole pre-valid model was skippable by minting the pass in the state it was supposed to have to earn. The Olympic model is a TWO-PART act - electronic activation of the record, and physical activation of the card at a centre where identity is checked against the photo - and W209 had built the consequence of the second half and neither half itself. A pass is now born pre_valid where validation is required and only an activated validation record moves it on (D-744), covering BOTH edges, an insert beyond pre_valid and an update out of it, because one alone leaves the other open. An activation requires an identity check (D-745): a desk recording one while ticking nothing has produced a row that looks like diligence and is not. The log is append-only (D-746), being the answer to "who let this person in, and did they check" asked weeks later by somebody who is not friendly. The control that matters is a POSITIVE: an event that never asked for validation must still mint an active pass, and it is the only thing separating an additive migration from a trigger that refuses everybody. It also forced a correction to W209's own gate condition, which had been creating active passes on a validation-required event. *Evidence: 1 migration.*
W213The pass design, its approval, and the authority spliton productionQ-476 finished except the vehicle class. Design section 13: for a federation event the governing body owns the credential system for the core areas and the local promoter may issue only for peripheral ones, with written authorisation and a pass design approved ahead of the event. W209 stored profile.authority_owner and nothing ever read it. A design is approved through the Approval Engine, not by a boolean (D-747): a design carrying its own approved flag would be a second approval mechanism with no approver and no decision record, and the question a federation asks afterwards is not whether it was approved but by whom and what they saw. The gate is opt-in (D-748), which is what keeps this additive, and the control proving that is the only thing separating it from a trigger that refuses everybody. The authority split is RECORDED and not enforced, and the schema says so (D-749) - enforcing it needs to know which body the current actor represents, and a membership belongs to a tenant rather than to a federation, so an assertion fails if the column comment stops saying RECORDED, NOT ENFORCED. Q-481. Its gate condition caught a real integration gap (D-750): app.approval_subject_exists is a case over subject_type with an else-raise, so routing a design through M-23 answered EC456 and the engine would have refused every one. W213 extends it and reproduces every branch W177 and its predecessors established, because two migrations that both replace one function do not merge. *Evidence: 1 migration.*
W214The retention period is ninety dayson productionOwner decision D-751, answering the half of Q-477 that was waiting for a number. W210 shipped the erasure mechanism complete and switched OFF, and said why: the retention schedule per consent type and per jurisdiction was the data model's own open question, and inventing ninety days there would have been a legal position taken by a migration. D-739 refused to take it and ASSERTED that no default existed, so nobody could add one quietly. The owner has now taken it, and the assertion guarding the absence is replaced by one pinning the value. Ninety because crowd.event_policy.checkin_retention_days already defaults to 90: one number across both personal-data modules is easier to defend in a DPIA, and to explain to a venue, than two answers to the same question about the same event. It broke three artefacts that asserted the absence - W210's own assertion, its ledger probe and its negative-control fixture - which is the fourth instance in one session of an exact count or an absence asserted as universal and then falsified by a later slice. The replacement asserts null or 90 rather than a number, so it survives the next decision. *Evidence: 1 migration.*
W215Pin what the two retention erasures may touchon productionOwner decision D-752, and a migration that creates no object. assertMaintenanceScope reads the SQL a scheduled job is about to run, and withMaintenance sets no tenant, so anything it touches it touches for every tenant at once. Running the retention erasures meant adding two names to MAINTENANCE_FUNCTIONS, which was empty by design because *a body is invisible here*: the guard sees select accred.erase_expired_personal_data() and cannot see that the body updates two tables. The trust is replaced by a check rather than accepted. This file reads both function bodies on every build and refuses any object outside accred.request, accred.pass and crowd.checkin, plus dynamic SQL and any unqualified write that would hide from it. All four assertions are proved able to fail. The alternative was measured and rejected: inlining the UPDATEs would let the guard see the tables directly and would put a GDPR erasure rule in two places, where the copy the migration asserts on is not the copy that runs. *Evidence: 1 migration.*
W216Every create is unscoped, on every databaseon productionQ-478's side finding, promoted, and it was the more serious half. w3-gate4-read-cascade sets record_scoped = false where action = 'create' at manifest position 3 and calls the exemption structural rather than a judgement: gate 4 asks whether THIS record was narrowed away, and before a create there is no this. A blanket update cannot reach a row that does not exist yet. quotes.create arrives in W7, files.create in W28.1, accreditation.request in W209, each after the only statement that would have corrected it, each taking the column default of true and keeping it. Measured 2026-08-25: production 5, dev 5, a clean two-pass build 2 - three databases, three answers, which is spine-seed's own confession about seed-run timing in a different file. Running the manifest twice rescues only codes whose own file does not rewrite them on pass 2, which is why a clean build lands on 2 rather than 0. Latent, not live, and repaired anyway: gates.ts runs gate 4 only when a record is passed and a create passes none, so nobody was refused - but five rows contradicted a rule the spine states as structural, and cascadeCodesFor returns the counterpart rather than [] for exactly those five. The check is deliberately NOT in this file. W216 sits at position 216 and a create added by W300 is past it exactly as these five were past W3, so the durable assertion is assertion 5 of w3-cascade-test.sql, which the manifest runs AFTER BOTH PASSES. It counts rows that FAIL rather than naming survivors, because a list is the thing somebody forgets to extend. All three assertions proved able to fail; the sabotage also caught a wrong claim in this file's own header, which had asserted a clean build held zero. *Evidence: 1 migration.*
W217One PR category, not twoon productionQ-473, closed. PR and Public Relations / PR were separate categories meaning the same thing, both children of Marketing, PR, Advertisement. The data chose the survivor, not a naming preference: Public Relations / PR carries 36 companies and the only child, CSR; PR carried 2 and none, so keeping PR would have moved 36 links and reparented a child to save 2. Both PR links were ai_direct and neither company was already on the survivor, checked before writing anything - had either been staff or ai_approved this file would have stopped, because moving a link a person made is a different decision from tidying a duplicate the enrichment produced. The name becomes an ALIAS rather than disappearing, which is W72's whole point and the standing rule that an upstream name is a label and never a key: PR will arrive again in the next import and now resolves. The post-flight asserts the END STATE, so it is honest on a re-run where there is nothing left to move; a file asserting 2 rows moved fails its own second run and reads as a regression. Its best assertion exists because of a sabotage: two of the first four were UNFALSIFIABLE, asserting orphan states that foreign keys already forbid, and the sabotage found them by being unable to construct one. They were replaced by the one check that separates a merge from a deletion - every company that held PR must now hold the survivor - which fires alone when the move is removed and the delete left in. *Evidence: 1 migration.*
W218Three roles receive the seed drift, less twoon productionQ-475, answered by the owner 2026-08-25. The same defect W208 repaired for Admin, on the three roles it left: spine-seed is purely additive and grants whatever the catalogue holds AT THE INSTANT IT RUNS, so a code declared after production's last seed run was never offered. Not rot; nothing decayed. Fifteen granted, two refused. estate.casualty.personal reaches Tenant owner through rule a1, a select that never mentions it, and Q-474 already settled that no role holds it BY OWNER DECISION. quotes.set_bank_details reaches Finance admin through rule a3, every financial permission, and it is a WRITE - scope describes sensitivity, not verb. The two refusals are not the same shape and the first draft treated them as if they were, asserting no role may hold either and turning red on its own first run: Admin, Supplier and Tenant owner hold set_bank_details on production, and a supplier setting its OWN bank details is the ordinary case. So one asserts held-by-nobody and the other withheld-from-one-role, because a declined grant is not a revocation. The withholding had to be ACTIVE: a clean build DOES grant it to Finance admin via a3, so a passive refusal would hold on production and fail in CI, and the delete is re-applied every pass - W216's structure again. Its floor said 170 and failed CI, which is the second time that trap has fired here: a spine-only build holds 126, not 176, because the 50 estate.* codes come from Estate's chain, and W208's own comment records the first instance. Four assertions proved able to fail with a paired control. *Evidence: 1 migration.*
W219A quote expires, and the date is enforced where it is readcompleteS6's last commercial item needing no owner decision. The slice said valid_until exists and nothing expires anything, and it was worse than a display problem: app.accept_client_quote checked only state <> 'sent' and NEVER READ THE DATE, so a quote six months past its expiry was still in sent and still acceptable at its stale price, while expired was a legal state nothing produced. A job alone would not have fixed it, which is why this is not only a job: a stored state is always BEHIND the date, and between midnight on valid_until and the next cron the row still reads sent. So the authority is a PREDICATE, app.client_quote_is_live, in W190's shape - stable over the row, touching no table, which is the only shape that can honestly answer the scope guard - and the job is a MATERIALISER reading the same function, so the column and the rule cannot disagree. The control is a TRIGGER, not an edit to the function: the first draft rewrote accept_client_quote and invented a helper to do it, and that function is 211 lines whose signature the draft had guessed wrong. W213 already records what rewriting one costs. The trigger is fifteen lines, holds on every path including psql, and judges old because new.state is already accepted. Six controls, five sabotages; the footprint is pinned the way W215 pins the erasures. Its notices said ok N and CI counted zero, failing as asserted-NOTHING: the runner greps NOTICE: ok: with a colon. *Evidence: 1 migration, acceptance suite.*
W220The service catalogue, priced per currencyon productionS6, and the only item in its not-built table whose reason reads Not started rather than naming a blocker: the card gateway needs an owner decision, checkout links follow it, and RecurringInvoice would mint financial documents on a schedule while Q-447 is still open on whether a Hungarian deposit invoice may be issued at all. THE ONE DECISION IS THAT A MISSING CURRENCY REFUSES AND NEVER CONVERTS, EC478, on the reasoning this system had already settled in gates.ts for the spend threshold: a rate is an external, time-varying, tenant-configurable number, and a converted price is a figure nobody approved on a document that goes to a client. Cross-tenant safety is STRUCTURAL rather than a policy predicate: a composite (service_id, tenant_id) foreign key against a matching unique key, because a price row with an HONEST tenant_id pointing at ANOTHER tenants service is exactly the state a with check on tenant_id alone permits - the value it checks is correct and the foreign reference is the defect. W149 used the same move for anchors and W151 for form versions. No unit columns on the service: app.feature_category already carries the unit vocabulary over 4000 rows and the line carries its own, so a third copy would be a column nobody maintains. Seven controls; control 2 is the positive one and a resolver raising unconditionally fails 2 and 3 ALONE while passing 4 and 5, which is what it is there to catch. Six sabotages, each isolating its own rule. *Evidence: 1 migration.*
W221The service catalogue gets a surfacecompleteW220 built app.service and app.service_price and nothing reached them. This follows immediately rather than later because W164 is the precedent: it shipped three tables with per-role visibility, proved at the database layer by twelve assertions, and for FOUR DAYS no route and no screen touched any of it. A migration nobody can reach is a table, not a feature. Five routes, every read gated on services.view and every write on its own code through require() rather than mayDo(), which skips the per-record ACL; may_edit is a hint for the screen and the write routes re-ask. The API surface gate caught the first run, which is the guard working: a route whose first path segment is not a declared surface answers unmapped_surface. services is INTERNAL, and the reasoning is worth keeping - a client reads PRICES on a quote or invoice sent to them, but the CATALOGUE is the tenant's own price list across every currency and every service including ones this client was never quoted, so an external caller would see what other clients are charged. Setting the same currency twice is an UPSERT, because a correction is not a duplicate. 13 checks, and the refusals are the point: gate 3 with its own positive control, since a suite proving only that the owner can write passes on a system with no gate at all, and EC478 answering 409 with the sentence and NO price. The suite counts its own checks and that guard fired for real, 12 declared against 13 run, so the count was corrected rather than the guard removed. A 401 proved nothing about the deploy: a nonsense path answers 401 too, because auth runs before routing, so the bundle was built from the deployed commit and grepped instead. *Evidence: acceptance suite.*
W223A quote line comes from the cataloguecompleteS6 asks for a catalogue *auto-selected by the invoice currency*. W220 built it, W221 gave it routes, W222 a screen, and nothing drew from it - a price list nobody quotes from is a list, the same defect one level up from the one W222 fixed. The price is resolved SERVER-SIDE and never accepted from the caller: a screen sending unit_price beside service_id would quote whatever it had cached, and the point of a catalogue is that one figure is authoritative. The line records WHICH service it came from, and that is the whole migration - the route could have written a resolved price with no schema change, and the question asked three months later would have no answer. It is PROVENANCE AND NOT A LIVE LINK: unit_price is still what the quote totals, because a quote is a statement made on a date, and reading the price through the service at render time would silently restate every historic quote. ON DELETE SET NULL and not cascade, so removing a catalogue entry does not take priced lines with it; the tenant guard is the composite (service_id, tenant_id) W220 established. AND THE REFUSAL DID NOT SURVIVE THE TRIP: /client-quotes/lines had no asRefusal, so EC478 reached the outer catch and came back as 503 *retry: true* - the same lie this file has corrected three times, in the worst direction, because retrying can never work. Nine checks, four of them refusals or their controls, the last asserting NO LINE WAS WRITTEN. Removing asRefusal again turns 3 red, including one that exists only to say NOT a 503. *Evidence: 1 migration, acceptance suite.*
W224The event overview becomes reachablecompleteRoadmap slice W33, M-16, the half of it that was never built. Two findings, both measured before anything was written. app.event_overview() HAD NO CALLER: W168 built it 2026-08-14 as S7's gate condition, W171 extended it the next day, and a grep over api/src, api/test and web/src on 2026-08-27 found two references, both inside w168-acceptance.mjs. S7 calls the overview *the screen the client logs in for* and it was a database function with no door, which is B-198's count of unreachable write paths happening to a READ path. And EventDashboard.tsx still counted ten capped lists, saying in its own header that it had to because *there is no event rollup route*; that route had arrived a fortnight earlier. EIGHT OF THE TEN LIST ROUTES END IN A CAP and each returns slice(0, cap) with NO has_more, so a full page and a complete one are the same payload: an event with 501 agenda items rendered 500 in a tile whose only job is to say how many there are. Twelve metrics become 22, in the SAME function rather than a second one, because W168's design is what these numbers needed and two roll-ups over one event drift. Each count reproduces its list route's predicate MINUS THE CAP, and two are joins because app.agenda_item and app.file carry no event_id, measured against information_schema. D-753: the event RECORD check gates the whole route and each metric is then omitted on the code its own list route requires, because mayDo alone would refuse a narrowed caller the agenda LIST and hand them an agenda COUNT. Section 2 of the suite is the suite: the defect is invisible below a cap, so it seeds 105 budgets past the cap of 100 and asserts the roll-up and the list DISAGREE by exactly the truncation, with a below-cap control where they must agree. Six sabotages, and two of them found defects in the SUITE rather than the route: blanking source_query originally threw inside section 1 and took sections 2 to 6 down with it, so 2c, the only assertion that catches this defect, never ran; and the unknown-metric guard cannot be fired without replacing the function mid-run on a shared database, which the file says rather than implies. W171's control fired and that is the finding of the run: the full battery went red on w168 1a, an exact metric count carrying W171's comment that *a thirteenth metric arriving without a decision turns this red*. It arrived as ten, before anything was pushed, and was widened to 22 in the same commit rather than turned into a range check. Third recording of the same rule after W24.1 and W171. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W225A drawn item becomes a sellable standcompleteCompletes W200 and D-721. W202 let an entitlement line name a stand and W203 stopped a stand being sold twice, over a row NOTHING COULD CREATE. Measured 2026-08-28: zero stands on local, dev and prod, no INSERT into app.layout_unit anywhere in api/src, and SponsorsScreen rendering a picker over /commercial/stands that could only ever be empty. B-198 counts write paths no control can reach; this is that count arriving from the other end, a SCREEN WITH NO WRITE PATH BEHIND IT. Promotion is explicit and that was checked before it was chosen: no category id or name in floorplan.html contains stand, booth or exhib, and D-217 draws a stand as an ordinary size-fluid rectangle, so nothing in the drawing says which rectangles are for sale. An item carrying sellable becomes a layout_unit of kind stand, projected from doc->items the same way instantiate_layout projects units from a template, RECONCILED IN THE SAME TRANSACTION AS THE SAVE so the drawing and the stand list cannot disagree and a refusal takes the doc with it. D-754: it ASKS before it writes. The entitlement foreign key is ON DELETE RESTRICT, catching that raise cannot rescue the transaction, and the SQLSTATE is not even the same on both databases - PostgreSQL 18 on production reports 23001 where the local 16 reports 23503 - so the function counts the sold stands it would remove and refuses by NAME first. Five refusals EC479 to EC483, each naming the item or stand, because the reader is looking at a floor plan. The control is a checkbox in server-bridge.js calling the planner's own snapshot() so it joins the undo stack; floorplan.html is byte-identical, the standing rule for that directory. Nine SQL controls as app_rw and 15 HTTP assertions. The sabotage worth keeping removed ONLY the sold-stand pre-check: control 8 alone goes red and the message becomes a constraint name instead of a sentence naming the stand, so a test asserting only *this is refused* would have passed on both. And the smoke test found a defect before any test existed: a temp table with on commit drop drops at COMMIT, not statement end, so the second call in one transaction failed. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W226A recurring invoice, minting draftscompleteS6's last commercial in-scope item that needed no gateway decision, and THE BLOCK ON IT WAS ON A DIFFERENT DOCUMENT. Every handout listed it as blocked because it would mint financial documents on a schedule while Q-447 is open. Q-447 is about the eloleg-szamla, the DEPOSIT invoice W167 generates on acceptance, and its open question is that invoice's VAT treatment; a recurring invoice bills a PERIOD, not a deposit. And it mints DRAFTS, which keeps it out of the question entirely, because W184 made draft-to-issued a human act and only an issued invoice is payable. D-755: it does not run itself, and that is deliberate. Putting the generator on the Sunday cron means adding app.invoice to MAINTENANCE_TABLES, which is the difference between an unattended job NULLING a photograph and an unattended job CREATING financial documents; every entry on that list today either nulls a personal column or stamps a state. withMaintenance also sets no tenant, so a cron caller would mint across every tenant with RLS bypassed. Q-490 carries it and the cron is one line when it is answered. Idempotence is a unique index, not a timestamp: (recurring_invoice_id, period_start) on the invoice itself, so a second press cannot insert whatever the caller believed, and cross-tenant safety is W220's composite key. It catches up ONE PERIOD AT A TIME with a ceiling of 24 that REFUSES rather than truncating, EC484 naming the schedule. A schedule with no template lines is SKIPPED rather than raised, a deliberately different answer from EC484's: an empty draft is deletable and the CONSUMED PERIOD IS NOT, but raising would stop forty-nine good schedules billing because the fiftieth is half configured, so it skips, keeps its date and names itself. Nine SQL controls, 26 HTTP assertions. A GREEN SABOTAGE HERE MEANT REDUNDANT DEFENCE, NOT A TEST GAP: removing the outer due-date filter leaves all nine green because the inner loop also guards it, and only removing BOTH reds controls 5, 6 and 7. Five defects were found by driving it before any test existed: a plpgsql OUT parameter SHADOWS a column of the same name so the on conflict target would not parse and cannot be qualified to disambiguate; any($1::uuid[]) is not how this driver takes an array, the house form is tx(ids); a date leaves as a UTC-midnight timestamp unless cast, rendering a day early west of Greenwich; a backtick inside a SQL comment ENDED THE TEMPLATE LITERAL and broke the build, the third recorded occurrence; and pressing Run before adding lines consumed a period for ever. AND ITS SUITE WAS WRONG ON ONE DAY IN THIRTY-ONE, found 2026-08-31, D-849. monthsAgo used d.setMonth(d.getMonth() - n) on today's date, and on the 31st that asks JavaScript for 31 June, which ROLLS FORWARD to 1 July rather than clamping to the 30th. The fixture then had TWO missed billing periods where 2a requires three, and 2a, 2c and 4b went red together while the code was untouched. It surfaced attached to W249 and cost a revert on the shared local database to clear. The day is now pinned to the FIRST of the month, the arithmetic runs in Date.UTC, and the helper is asserted over all 365 days of 2026 rather than over today. And the new skip rule then reddened THREE FIXTURES that were creating the state it forbids, plus control 9, which had been green only because control 8 was failing and never set the state that poisons it. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W227A delivery phase can be createdcompleteW170 shipped the phase on 2026-08-15 with TWO READERS AND NO WRITER, which makes it the worst of this week's four dead-end slices rather than another example. Measured 2026-08-28: 0 rows on local, dev AND production across 3 events, no insert into app.delivery_phase anywhere in api/src or web/src, and not one permission code containing phase, so no caller could have been authorised to try. The two readers are W190's calendar export and app.event_overview(), whose four phase metrics W224 put on a dashboard the day before and which could only ever report zero. A CORRECTION TO THE BRIEF THIS SLICE WAS GIVEN: it stated event_id has no foreign key, and measured on all three databases it does, delivery_phase_event_id_fkey referencing app.event(id). The conclusion was right and the reason was wrong, which matters because the wrong reason suggests the wrong fix: a referential check runs with row security bypassed and asks only whether the event EXISTS, and another tenant's event exists. D-756: three composite keys, following W19's own written instruction that if a second table needs the guarantee it should be built structurally rather than proved twice, and copying two shipped precedents, D-231 on app.membership and service_price_same_tenant in W220. The join table carried the same hole TWICE and both are closed. The keys do not close the within-tenant case and are not claimed to: a key cannot see a record ACL, so the route requires events.view ON THE EVENT RECORD, W224's strict form, and the suite asserts the two halves separately because they fail separately. D-757: no external role got any phase code, and the measured reason is that it would be DECORATIVE, since events is an internal surface and an external caller is refused on class first. agenda.view is already held by Speaker, Client and Freelancer, so the precedent pointed the other way. Q-491. D-758 fills the Milestones tab slot D-376 deferred, whose reason has expired: W170 gave the object dates and W190 put it in the calendar. Five tabs against a cap of six. Q-493. 9 self-assertions, 9 SQL controls, 40 HTTP assertions. Failure measured in NINE directions, and the load-bearing one is REDUCING the composite key to a single-column (event_id) key, the shipped substance, which reds control 2 alone while the other eight stay green. A TEST GAP WAS FOUND BY SABOTAGE, NOT REVIEW: typed when foreign_key_violation handlers let a check violation propagate out of the block, so controls 4 THROUGH 9 never ran and the file ended on a raw error rather than nine lines. Every handler now catches others and classifies by constraint name. And two numbers are both called a percentage: the overview counts COMPLETE PHASES while delivery_phase_progress counts DONE TASKS in one phase, so nine of ten tasks reads 0 on the dashboard and 90 in the panel. Invisible until today because no phase had ever existed, and found because the suite asserted them equal and failed. Q-492. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W228Reporting across events, and the audit export reaches a callercompleteRoadmap slice W33, M-16, the other half. W224 built the per-event overview. This is what S7 named as still open: reporting ACROSS events, exportable. app.audit_export() shipped in W166 on 2026-08-14 with ZERO callers, while production held 44 app.audit_event rows, so it was a working export with no door. Adds app.portfolio_overview(), ten tenant-wide measures on W224's (metric, value, source_query) contract, a /reports screen and CSV download. Three defects found on the way, and the third is the one that generalises. The export's default page was the OLDEST 500, because the function ends order by id ascending over 25 325 rows with no cursor. order by id desc then sorted LEXICOGRAPHICALLY, because the output alias outranks the FROM column, so 9999 beat 30313. And spine-seed.sql hands the audit trail to Viewer through wildcard grants, so on the second manifest load, which is exactly what CI does, the narrow grant was decorative. Caught by the battery, not by review. D-761 to D-768, Q-494 to Q-496. *Evidence: 1 migration, acceptance suite.*
W229A declared webhook event type reaches a subscribercompleteW185 and W186 shipped nine event codes and nothing could ever fire one. app.emit_webhook had ZERO callers in api/src, its only callers being its own suite. Registration worked the whole time, three routes and WebhooksScreen.tsx, so a tenant could subscribe to nine types and receive nothing for ever with nothing reporting a fault. Six of the nine now fire from real state changes. The emit is in the database and not in the route, on measurement rather than preference: TWO routes mark an invoice paid and only one honours the transition table, and app.approval.state has no route writer at all, so a route emit would have fired for one caller and not the other. event.published and the two crowd codes are NOT built, because app.event has no publication_state column to change. It also fixed ci-simulate-locally.sh, which had been running its ledger stage against the WRONG DATABASE since it was written, and the quiet direction is the bad one: a migration absent from the fresh chain read as present whenever the developer's own database happened to carry it, which is almost always. D-769 to D-773, Q-498 to Q-500. *Evidence: 1 migration, acceptance suite.*
W230A published form can be answeredcompleteThe fifth instance of the shape FormBuilderScreen.tsx counts three of. W151 shipped five form tables and six routes and every route was authoring, so app.form_response was written zero times outside two suites and the builder's response count could only ever render 0. W156's app.cfp_withheld_fields, which withholds a field marked identifying from a reviewer, also had no caller, so the Q-438 mitigation did not happen. The centre is that the withholding is a RESTRICTIVE policy and not a route projection, which is not a style choice: W151's policy is permissive and grants the whole tenant, and permissive policies OR together, so the same predicate written permissive withholds NOTHING. Answers Q-442, open since 2026-08-13, in the negative: reading a response needs its own codes, because forms.edit is administrative and W153's EC442 trigger refuses it to every external role, so Reviewer could never have read a submission through it. Three latent defects corrected, the largest being app.grant_state() inner-joining app.engagement, so all four engagement-free grant types read as REVOKED and a live cfp_submission link told its holder it had been withdrawn. Latent since 2026-08-12 because all three call sites are quote routes. D-774 to D-780, Q-501 to Q-505. *Evidence: 1 migration, acceptance suite.*
W231The floor plan becomes reachablecompleteFour routes shipped with no caller and killed four slices at once. Measured 2026-08-29: /layouts, /layouts/detail, /layouts/save and /layouts/instantiate had ZERO references anywhere under web/src, and syncStands is called from those four and nowhere else, so W201, W202, W203 and W225 were all live on production and all unreachable by a person. A screen alone would not have been enough, and that is the finding. /layouts/detail answered with SEATS ONLY, so every stand W225 produces was invisible to the one route that reads a layout - a screen built on the old shape could render a floor plan and could not render the thing the floor plan was extended to carry. stands becomes its own key beside seats rather than widening seats, because the two differ in what they even have: the table CHECK requires a section, a row and coordinates of a seat, and a stand carries a doc_item_id and no coordinates at all. Sold state is a left join on the entitlement_one_line_per_stand unique partial index, so it cannot double count. WHO bought it is gated on commercial.view and OMITTED rather than nulled, D-193: a null sold_line says nobody has bought this stand, which is the opposite of you may not be told, and the Contributor role is the caller that proves it because it holds venues.view and not commercial.view. READ-FIRST BY DECISION, D-782. A list, a detail view and the existing minimal authoring, with no plan editor - reaching the feature is a different job from finishing it. The geometry is a POSITION MAP and the screen says so on its face, D-783, because every item resolves its true shape from a catalogue of about two hundred entries that lives in the prototype and porting it is the slice the owner warned against. NO MIGRATION, D-788, and inventing one would put a ledger probe on something that did not change. 34 assertions and eight sabotages, each redding a distinct set: the stand query asking for the wrong kind, the seat read losing its kind = seat filter, the mask off, the mask nulling instead of omitting, may_instantiate hardcoded, is_sold hardcoded, instantiate answering 200, and the revision never bumping. Driven end to end in the browser at 1280 and 375 and in both themes, including deriving an event plan from the template and watching the copy report times_used 1 on its parent. And the drive found a bug the typechecker could not: /events answers {events: []} where every other list route answers {items: []}, and the first draft read .items ?? [], which typechecked, coalesced to empty and rendered no events - a wrong key wearing the costume of an empty database. *Evidence: acceptance suite.*
W232An inbound email can actually arrivecompleteThe reachability sweep's fifth catch, and the one where the producer was the whole missing half. W168 built app.receive_inbound_email, app.inbound_email and the entire allow-list on 2026-08-14. W182 built the review screen and its two routes. Between them S7's gate clause looked delivered. Measured 2026-08-29: the function had ZERO callers in api/src and web/src, there was no insert into app.inbound_email anywhere outside api/test/w168-acceptance.mjs, and production held zero rows. The gate was proved only by the suite that called the function directly, and the quarantine screen could only ever be empty. NO MIGRATION, D-796, and it was measured rather than assumed: app_rw already holds EXECUTE on the function on local and on dev, and app.chat's policy already admits the platform-admin flag, so nothing in the database needed to change and inventing a file would have put a no-op in spine-rerunnable.manifest forever. The recipient address names the conversation and the caller never names the tenant, D-792: reply+<chat uuid>@<domain>, anchored at both ends, and the tenant is read from the app.chat row exactly as crowd.feed carries the sensor path's. The suite posts a payload naming another tenant out loud and asserts the row landed under the chat's tenant, which is a positive assertion and not a count of zero. Two defects found by reading the diff back, both in this slice's own new code. First, a segment declared in PRE_AUTH_SURFACES makes index.ts answer 404 for every path under it that sits below the isPreAuthSurface catch-all, so the declaration that documents a route as reachable is what made it unreachable - the route moved above the catch-all beside the five already there, and the two-part rule is now written above the set. Second, INBOUND_EMAIL_DOMAIN was optional and passing no domain SKIPS the recipient domain check, so a missing secret closed the route while a missing domain silently widened it; both are now required and configured is one binary fact. A fourth elevated lane of TWO objects, D-794, with app.inbound_email deliberately absent so the route cannot write the queue directly and skip the allow-list, and both scope registry tests changed to DERIVE the guard list from db.ts exports so the seventh lane is covered the day it is written. The negative control breaks the guard rather than decorating it: the same sender, no longer a participant, is held instead of posted. D-790 to D-799, Q-506, Q-507. *Evidence: acceptance suite.*
W234A file can actually be locked behind an invoicecompleteS6's gate condition, the provider half. app.file_invoice_lock is a RESTRICTIVE select policy reading invoice.state = 'paid' live on every read, so the Contact half needed no code at all: there is no trigger, no job and no propagation step, and /portal/files inherits the refusal through ordinary row security rather than through a rule of its own. Auto-unlock was checked before the screen was designed, precisely so a screen would not be built over a gap. What was missing was only the ability to PRODUCE the locked state, and the acceptance suite's section 4 now proves both directions starting from an unlocked file that a person locks, rather than from a fixture that inserts the column. One thing is named rather than fixed: a Contact sees a locked file simply ABSENT from /portal/files, with no statement that payment is what withholds it. That is inaccessible, so the gate passes, but it is silent where /files/for names its withholdings out loud. Q-508 carries it. D-810. This row was added by W235, which found the board refusing to generate because W234 shipped without one. *Evidence: 1 migration, acceptance suite.*
W235A skipped assertion is counted and namedcompleteThe measuring instrument itself, and everything this project believes about its own state comes through it. run-all-acceptance.mjs classified every suite into four states and none of them was skipped. Five suites deliberately skip sections whose preconditions are absent and then exit 0, which is CORRECT, because a suite's exit code answers "did anything I measured come out wrong" and not "did I measure everything". The runner read that exit code and called them clean passes. Measured 2026-08-29 before the fix: w232 ran 2 of its 40 assertions and printed as pass, inside a summary reading FAILED 0, did not run 0, no assertions 0. The only surviving trace was a smaller assertion total, and nothing distinguishes 3514 from 3550 except remembering what the number should have been. This is the READ ALL FOUR COLUMNS defect in its PARTIAL form, and the partial form is worse: a suite that ran nothing looks broken, a suite that ran two thirds looks healthy. It was not one suite. w28-1 skips 2 groups including the 1 GB upload path, w31 skips 3 and w28-3 skips its cross-tenant read isolation, in every battery on record, so every assertion count quoted in this workspace before today is a FLOOR rather than a count. Q-509 and Q-510. The classifier moved into harness-preconditions.mjs because the runner spawns 118 children on import, which made the rule deciding whether everything else counted the one rule no test could reach. A skip does NOT change the exit code, D-812, and that is the one judgement that could re-create the defect one level up: three of the six skips are structurally permanent, w31 cannot reach a verb no route offers, so a skip that reddened the run would make exit 0 unreachable on any machine in any configuration and the repair anyone would reach for is an allowlist of blessed skips. No threshold, no allowlist, no default. The runner counts and names, and this file downgrades a skipped slice to unverified rather than crediting it complete. D-811 to D-814. *Evidence: acceptance suite.*
W237The city the address already sayscompleteReported by VELA 2026-08-29, their request 1 of 3, extending W162. The defect is not missing data, it is data in the wrong field. 182 published Hungarian venues carried a null city while holding a perfectly good town name in address_line, and a null city puts a venue on no VELA city page at all, Budapest included. The rule is STRUCTURAL rather than a bare split_part because 3 of the rows are the single string "Hungary", which a naive first-component parse writes into the city field as a country. Fills only blanks, so a second run writes nothing. Added to this table on 2026-08-30, having shipped on 2026-08-30 without an entry. *Evidence: 1 migration, acceptance suite.*
W238The banquet figure says it is a banquet figurecompleteVELA read seated_max_pax, saw Mupa Budapest return 150 against a hall seating about 1 700 and filed the field as broken. 150 is Mupa's largest BANQUET figure; its largest theatre space is 425. They compared banquet against theatre, which is what a field called seated_max_pax invites, because a theatre seats people too. So the slice renames nothing and changes no value: it adds banquet_max_pax beside the old key and serves it on DETAIL as well as the list. Ships NO migration and D-831 records why. Added to this table on 2026-08-30, having shipped on 2026-08-30 without an entry. *Evidence: acceptance suite.*
W239A narrowing on an event reaches that event's filescompleteQ-484, answered by the owner 2026-08-30, and the question's premise was false for three of the four routes it named. Measured 2026-08-30: /budgets gates through requireBudgetScope, /assets through requireAssetEvent and /checklists through requireChecklistParents, all three introduced 2026-07-28 and 2026-07-29, a month BEFORE the 2026-08-27 measurement that produced the question, whose grep did not follow a helper call. /files was the only loose route and is the whole of this slice. The gate is CONDITIONAL because event_id there is an optional filter and not an anchor. Ships no migration. Nobody loses access on production: app.record_acl holds zero rows there, and the one internal role holding files.view without events.view has zero memberships. D-833. *Evidence: acceptance suite.*
W240The Admin may name the actoron productionQ-495, answered by the owner 2026-08-30, amending W228 section 3. An audit trail that never names the actor is close to useless to the person expected to investigate with it, and Admin is the role that investigates. One grant, five assertions. Its cost is that W228 assertion 3's population becomes ZERO: exactly three roles hold audit.view and the other two already held the actor column, so no template role is left for gate 5 to mask. Assertion 3 does not go red in CI, because W228 evaluates it at load position 1722 and this file loads after, so a green ledger stopped saying anything about that invariant. The proof moved into w228-acceptance.mjs, which now builds a throwaway role holding audit.view without audit.view_actor and drives the real route with it. D-834. *Evidence: 1 migration.*
W241A narrowing on an event reaches the event's own detailcompleteThe finding W239 could not reach, because W239 swept the four routes Q-484 NAMED. Sweeping every GET route instead found 32 that take an event_id from the caller and ELEVEN that never let gate 4 see it, and the worst was /events/detail. It called require(tx, "events.view") with no record, and gates.ts runs gate 4 only if (record && ...), so gates 2 and 3 ran and 4 was skipped on the very id the route is anchored to. Its own comment said otherwise, ending "by having already required events.view above", which described a protection the code did not have - worse than no comment, because the next reader checks the cascade, finds a sentence saying it is handled, and moves on. THREE routes, not one, because the defect was a SHAPE and not a typo: /events/wizard/questions and /events/marketing-plan are identical, and patching only the named one repeats Q-484's mistake from the other direction. Ships no migration. Nothing changes on production today, where app.record_acl holds ZERO rows against 202 on local, which is also why nobody had noticed. The risk that had to be cleared first was the opposite one: gate 4 DENIES a caller with no membership (D-233, fail closed), so passing a record could have locked people out - but requireTenant() only passes when resolvedTenantId is set, and that is set only inside the branch that also sets activeMembershipId, so the two cannot disagree. Three of the eight remaining loose routes CANNOT be fixed as code: /crowd/floor-plan, /crowd/report and /crowd/zones load no session at all, and there is no crowd.* code in app.permission to ask for - adding one would throw permission_undeclared at gate 4 and take all three screens down. That is Q-517. /requirements is the same shape and already recorded as deliberate. The suite encodes the survey, requiring the loose set to be exactly the eight recorded verdicts, so a new route with this defect reddens on the day it lands rather than waiting for the next sweep. 26 assertions, and two independent sabotages: removing the record reddens 5 and leaves both comment assertions green, restoring the false sentence reddens exactly 2 and leaves all 24 behaviour assertions green. One sabotage alone would not have shown the two layers are independently asserted. D-837. *Evidence: acceptance suite.*
W242A conversation is opened from the record it is aboutcompleteThe missing CONTROL, not a broken route, and an HTTP-only suite would have been green on every day it was missing. W31 shipped POST /messaging/threads/new and MessagingScreen deliberately declined to offer it, correctly: opening a thread requires a target, the picker for fourteen record types is its own design, and an inbox is where you READ. The record screen that was supposed to carry it never got one. Measured 2026-08-30: threads/new had ZERO references anywhere under web/src, and production held 0 chats, 0 participants, 0 messages and 0 inbound emails, so the endpoint worked, the inbox worked, W232's inbound mail path worked, and NO PERSON COULD OPEN A THREAD AT ALL. The W231 pathology. WEB ONLY, AND THE FIRST DRAFT WAS NOT: it added may_start_conversation to /events/detail before noticing that /messaging/threads, the route the card already fetches, answers exactly that question as may_post. Reverted; a second name for one fact is two things that can disagree. THREE FINDINGS CAME OUT OF THE SUITE AND TWO WERE IN THE SLICE'S OWN WORK. The route returns id and not chat_id, which both the card and the suite guessed wrong - the suite reddened while every React key was silently undefined. A SABOTAGE THAT FAILED TO FAIL: deleting the caller outright left the suite GREEN, because the file's own comments name the route while explaining that it once had none, so prose about an identifier satisfied a test for the identifier; every source assertion now strips comments first. AND A SECOND: hardcoding setMayPost(true) also stayed green because may_post still appeared in the apiGet TYPE ANNOTATION, so the check now requires the server's value to reach the setter. Both sabotages redden now, one assertion each. 18 assertions, driven in a browser at 1280 where the click wrote the row, and verified on production by grepping the SERVED bundle rather than trusting a version id. D-840. *Evidence: acceptance suite.*
W243A received message keeps its line breakscompleteReported by the owner on the first real inbound email: the formatting has been lost. NOTHING WAS LOST. IT WAS NEVER DISPLAYED. The body arrived and was stored intact, measured on production at 5137 characters carrying 110 newlines, and MessagingScreen rendered it in a bare <p>, whose default white-space: normal collapses every newline into a single space. The text was in the database the whole time. Adds .msg-body, which is deliberately NOT .mail-body: that is a monospace review box for raw mail and for secrets at --text-xs inside a scroll well, while a message in a conversation is prose. overflow-wrap: anywhere because mail carries long URLs and pre-wrap alone lets one unbroken URL push the card wider than the screen; measured at 375px with no text overflow and no sideways page scroll. STILL TEXT AND NEVER MARKUP: inbound-email.ts takes RawTextBody and never stores RawHtmlBody, because a thread body can be written by anyone who learns the address, and the suite asserts dangerouslySetInnerHTML stays absent. 11 assertions across BOTH layers because they fail independently and only one was ever broken: the API and the database always kept the newlines and the screen threw them away at paint time, so a data-only suite would have been green throughout the bug - the W242 lesson one slice later. Two sabotages, one per layer. Found in passing that --radius-md is a PHANTOM TOKEN used 4 times in components.css, so those four rules carry no border radius at all; not fixed here. D-841. *Evidence: acceptance suite.*
W244A reply reaches the other participantscompleteStage 1 of D-842, and it CLOSES Q-507. Until now /messaging/messages/new contained NO mailer call at all: posting emailed nobody, so a colleague learned of a message only by opening the app, and an inbound email could arrive with no way to answer it in kind. MailMessage gains replyTo, which W232 refused to add because a field nothing sets is the same unreachability defect that slice existed to fix; this is the caller that makes it real. THE REPLY-TO IS THE THREAD, NEVER THE AUTHOR, because replying to the author takes the conversation OUT of the system, which is the failure the feature exists to end. It is deliberately NOT redirected by NOTIFY_REDIRECT_TO: that valve replaces the RECIPIENT so a test cannot reach a real counterparty, and a reply-to is not a recipient. Recipients are gathered INSIDE the transaction and mailed OUTSIDE it, because holding a transaction open across a mail provider is how a slow third party becomes a lock on the messages table. The author is excluded by user_id and not membership_id, since one person can hold two memberships. Every mail failure is swallowed: the message is already written, and a provider outage must not turn a posted message into a 500 the client then retries. W244.1 IS A DISCLOSURE FOUND AN HOUR LATER, while setting up the production test: the recipient query asked only that somebody be a participant with an email, not that they could READ the thread, and the mail carries the message BODY. Measured on production, BOTH Supplier memberships are legitimate people in the tenant and NEITHER holds messaging.view. The route already made that argument twenty lines above, about mentions, and W244 failed to apply it to itself. 14 unit tests on the pure composer plus 12 acceptance assertions. A SABOTAGE FAILED TO FAIL AND EXPOSED A GAP IN THE SUITE: removing the swallow stayed green, because the opener is the only participant on a fresh thread and the author is excluded, so the recipient list was EMPTY and nothing was ever attempted to be caught. Third time that pattern has fired here. D-843. *Evidence: acceptance suite.*
W245The reply panel says where its mail goescompleteReported by the owner on the first real use: *it sounds like the email is going to be sent to this email address, which is in the drop down. However, it is going to be sent to the external email.* THE READING WAS REASONABLE AND THE SCREEN EARNED IT. A box headed Reply sat directly above a panel headed *Reply by email* showing an address, with a Send button under both, and NOTHING said where the message from Send went. W244 made the wrong reading look confirmed: before it, Send emailed nobody, so the ambiguity was harmless; after it, Send does send mail, and the owner watched mail leave and reasonably concluded it went to the address on display. Three copy changes: the composer gains a hint, the fold becomes *Email into this conversation* so the title carries the DIRECTION, and the address label stops reading as an instruction for the button above it. NO COUNT IS CLAIMED and the suite asserts none appears, because the mail goes to participants holding messaging.view and the participant list carries no such flag, so a number would be a guess. 10 assertions, two sabotages. D-844. *Evidence: acceptance suite.*
W246The composer sits with its own buttoncompleteW245 REWROTE THE COPY AND THE OWNER READ THE SCREEN THE SAME WAY ANYWAY, correctly: *there should be separation or rearrangement of the send button.* The copy fix was the wrong tool. The Send button is pinned in the Drawer FOOTER, so whatever sits lowest in the body reads as its subject, and the reply box was followed by the inbound address panel AND the participant list -- two unrelated blocks between a composer and its own button, leaving the ADDRESS nearest to Send. Words cannot beat proximity. The body order is now messages, then an *About this conversation* heading standing over both folds, then the Reply composer LAST, directly above the footer. ORDER IS ASSERTED BY POSITION IN THE SOURCE, not by matching strings that happen to exist: a file can contain every correct word in the wrong order, which is exactly the state W245 left behind. Comments are stripped first, which matters more than usual because the header of the suite contains the words it searches for. 8 assertions including that Send is still the pinned footer, since the premise collapses if it moves into the body. Sabotage moving the composer back above the folds reddens the three ordering assertions and nothing else. D-844. *Evidence: acceptance suite.*
W247A SECURITY DEFINER function is not PUBLICon productionNOT THIS PROJECT'S WORK, AND NOT YET APPLIED. Written by a Mise session on 2026-08-30 and left in the spine for event.clinic to review and run, because Mise does not write in a sibling schema. Postgres grants EXECUTE on a function to PUBLIC by DEFAULT, so a SECURITY DEFINER function is callable by anyone unless somebody revokes it; the file revokes PUBLIC on two of them and asserts app_rw keeps its own EXECUTE, so an over-broad revoke fails loudly rather than quietly removing access. Owner approved the same day as the precondition on ratifying D-781, Mise in production. RENUMBERED FROM W241 BY ANOTHER SESSION, content unchanged: W241 was already taken at ~13:00 the same day, and build-ledger.py globs w241* for both migration and acceptance evidence, so this file was being paired with that slice and published under its name as carrying a migration it does not have. The number matched, so nothing refused. Q-519. REVIEWED, DRY-RUN AND APPLIED 2026-08-30, after the owner chose dev-then-production. The exposure was reproduced first on local, then the file was run inside a transaction and rolled back to confirm both of its assertions fire, then applied to local, dev and prod. Read back on all three: PUBLIC holds EXECUTE on neither function and app_rw keeps it on both. Production carried the same exposure and mise_rw did not exist there yet, so the hole closed before anything could walk through it, which is exactly why Mise called this the precondition on D-781. Registered in BOTH manifests, because a migration in neither is consistent to the ledger cross-check and is therefore never measured; the probe requires a NON-NULL acl AND no PUBLIC entry, since a null acl means default privileges and for a function that MEANS PUBLIC may execute. Probe proved load-bearing by demanding 3 functions instead of 2 and watching it report MISSING. All three databases measured 352 probes when this landed. REWRITTEN 2026-09-01, D-860, AND IT WAS HALF OF WHY CI COULD NOT BUILD THE SPINE FROM EMPTY. Both functions are ESTATE's, so on the empty database spine-rerunnable builds neither exists and the two bare REVOKEs died at line 114, from the day this file landed until now. Its ledger probe read MISSING for the same reason and by a different mechanism, demanding count(*) = 2 over two functions a spine-only build has none of, so the job failed twice over one cause and reported once. The revokes are now issued where the function EXISTS and the probe asks the PROPERTY rather than the population, with existence asked separately by the new external probes, which gate on dev and prod. IT WAS DELIBERATELY NOT MADE A BLANKET SWEEP the way W249 was, because this file's own text argues that nothing loses access: it revokes the two that WRITE and leaves the seventeen readers to W249, which grants three roles FIRST, so a blanket revoke here would strip them one file before their protection arrives. Section 3 also now names its own vacuous case, because it counts VIOLATORS and a database with neither function passes it perfectly for no reason at all. Q-528 Cause B. *Evidence: 1 migration.*
W248A reply saves what was written, not the thread underneathcompleteD-842 stage 2, and it was built on the cheapest day it ever will be. W232 read RawTextBody, which is the WHOLE delivered mail: a real reply carries the entire prior conversation quoted underneath it plus a signature, so each round trip re-saves the thread and the cost compounds per reply. Brevo already computes the message without any of that and W232 discarded it. MEASURED ON PRODUCTION FIRST, and the measurement changes how this reads. The whole of app.inbound_email is one row: 5137 characters, 110 newlines, and ZERO lines beginning with a quote marker, because that message is a thread OPENER. The defect has not fired once. This is not a regression report, it is the last moment before the first real reply. A COLUMN NOTHING READS IS THE DEFECT THIS PROJECT KEEPS NAMING, so mail_message_id arrives with the thing that reads it: a Message-ID already seen ON THE SAME CONVERSATION returns the existing row and posts nothing. The precedent is one week old - W232 refused MailMessage.replyTo and W244 added it WITH its first caller. The redelivery case is real: the route writes through the function BEFORE it answers the provider, so a response Brevo never sees is exactly when the mail is already posted and the retry posts it twice. mail_in_reply_to IS STORED WITH NO READER AND SAYS SO IN THREE PLACES - the column comment, the migration header and the TypeScript type. Its consumer is an outbound threading header, which needs a Brevo capability nobody here has exercised, and Q-508 exists because acting on an inferred provider capability cost a round trip once already. Q-521. THE SIGNATURE CHANGE FORCED W247's LESSON ONE DAY OLD. create or replace with a new argument list creates an OVERLOAD rather than replacing, and two of them make every five-argument call ambiguous, so the old function is dropped - and a freshly created function takes whatever pg_default_acl says. The file REVOKES from PUBLIC and GRANTS to app_rw explicitly instead of trusting a default W247 found absent on nineteen functions. ?? WOULD HAVE GOT THE FALLBACK WRONG. An extraction that finds nothing yields "", which is a string, so a nullish chain takes it and stores an empty message while the text sits in the raw body. Blank falls through. Its cost is stated rather than discovered: a reply carrying only an attachment re-saves the quoted thread, which is what every message does today. 12 unit assertions and 17 acceptance assertions, and THIRTEEN SABOTAGES, every one red. Five against the migration's own assertions, three against the parser, five against the route and the function. Three findings came out of them. has_function_privilege RAISES on a function that does not exist rather than returning false, so the assertion block ABORTED on an un-migrated database and reported one failure where seven were asked for - the BLOCKED shape, now guarded by to_regprocedure. Omitting the revoke would NOT have reddened the PUBLIC assertion on any of these three databases, because their default ACL already excludes PUBLIC, so it had to be proved by granting the exposure deliberately - the assertion is for the database that has no such default, which is the one W247 found. And section 4 of the suite is not decoration: making de-duplication GLOBAL rather than per-conversation reds it alone, while is not distinct from - the realistic slip - collapses every mail carrying no id into one and reds section 5 alone. Neither layer can prove the other. A perfect parser wired to a route still sending the old field passes all twelve unit assertions and saves the quoted thread anyway, which is W243 one layer along. Every acceptance assertion reads the row back out of the database after driving the real endpoint; none matches on source text. APPLIED TO LOCAL, DEV AND PRODUCTION 2026-08-31, and the API half CANNOT BE PROVED FROM OUTSIDE. Everything past the inbound door needs INBOUND_EMAIL_SECRET, and everything before it answers 401 identically before and after, so this surface has NO unauthenticated discriminator and /health carries no build marker. That is the second consecutive slice in that position and it is now a pattern rather than an instance. Q-522. What WAS proved on production: the shape read straight back - one signature, pronargs 7, a non-null ACL, PUBLIC refused, app_rw holding EXECUTE, both columns - and then the function DRIVEN on production inside a rolled-back transaction, twice with one Message-ID, posting once and returning the same row id. That is the assertion no shape check can make: app.inbound_email is force row level security, so the guard is a SELECT that depends on the definer bypassing it, and a definer that stopped would find zero rows, never fire, and leave every shape assertion green. Paired with a deliberately invalid control on production, also rolled back: a DIFFERENT Message-ID posts, and two mails carrying NO id post twice. A guard refusing every second message would satisfy the proof perfectly. Production ended at 1 inbound row and 3 messages, exactly where it started. D-847, Q-521, Q-522. *Evidence: 1 migration, acceptance suite.*
W249A definer READER is not PUBLIC eitheron productionW247's P2, closed, and the reason it was P2 turned out to be the whole slice. D-846 shut the two SECURITY DEFINER functions that WRITE and named the seventeen READERS it was leaving: *revoking those is a larger change with callers to find*. It was right, and the callers have now been found. THE EXPOSURE WAS REPRODUCED, NOT INHERITED. On local against the real mise_rw, inside a rolled-back transaction: select count(*) from app.role raises permission denied for table role, and app.role_matrix() returns 3705 rows naming nineteen roles. Read those two lines together - the role cannot read one row of the table and is handed a description of every role in it. TEN OF THE SEVENTEEN ARE RLS POLICY PREDICATES, evaluated as the QUERYING role, so a revoke there does not close a door: it can make an ordinary SELECT fail for a role that has read that table for months. AND THREE ROLES HELD EXECUTE THROUGH PUBLIC ALONE, measured twice by two routes that agree - once by asking who holds an explicit grant, once by asking which policies each role's own table grants make it evaluate. app_public on user_holds_permission, reached through product_tenant_rw on app.product; app_supplier on event_engages_supplier and event_shared_with_caller. The obvious blanket revoke would have broken the public directory and the supplier portal, and sabotage S1 is exactly that revoke: it reds three assertions, one per role, each naming what it lost. THREE ROLES NEED NOTHING AND THAT IS MEASURED TOO: app_rw already holds an explicit grant on all seventeen; estate_rw IS A MEMBER of app_rw and inherits every one, which is asserted because a membership is the one dependency here that no grant would record; nru_mirror has no usage on schema app at all. THE FIRST DRAFT'S ASSERTION 1 COULD NOT FAIL AND A SABOTAGE SHOWED IT. It listed the seventeen names and checked PUBLIC was gone from each - but a renamed function makes the REVOKE statement above it raise outright, so the check only ever restated what had already succeeded. It is now a SWEEP: is ANY directly callable definer function in app still PUBLIC-executable at this position in the chain, with the message naming the offenders. THE STANDING GUARD IS A SCRIPT, DELIBERATELY. verify-definer-not-public.sh asks the same question after the WHOLE chain, on any target, because a migration asserts the world at ITS OWN position and a definer function created by a LATER file would be invisible to it - the guard would read green while the eighteenth instance of this defect shipped. It was proved able to fail by running it on all three databases BEFORE the migration, where it exits 1 and names seventeen. NO ACCEPTANCE SUITE, AND THE HONEST INSTRUMENT IS THE WHOLE BATTERY: 129 suites drive the real routes as app_rw and app_public against a local database carrying this file, so a revoke that took too much reddens them in numbers. A suite of its own could only re-assert the ACL it had just written. app_supplier is granted two functions it cannot currently use - it has NO usage on schema app and cannot log in - because a security revoke must preserve what a role has and must not be the thing that retires it. Q-523. IT WAS ACCUSED OF BREAKING w226 AND WAS INNOCENT, AND CLEARING IT COST MORE THAN THE SLICE. The battery came back 127 passed, 1 FAILED, with three assertions of w226-acceptance.mjs red. It reproduced on a re-run, so it was not machine load at 244. It was the DATE: monthsAgo used d.setMonth(d.getMonth() - n), and on 31 August that asks JavaScript for 31 June, which ROLLS FORWARD to 1 July rather than clamping, so the fixture had two missed billing periods where assertion 2a requires three. Correct on thirty days out of thirty-one, and red on the one night a migration touching seventeen functions landed. D-849. PROVING IT MEANT REVERTING W249 ON A DATABASE FIVE OTHER SESSIONS SHARE, AND THE REVERT WAS THE REAL FINDING. The undo was generated from the database's CURRENT state -- grant PUBLIC on every definer function -- rather than by inverting this file's own seventeen revokes, so it re-opened all five that W247, W248, w40 and w26 had each closed. verify-definer-not-public.sh, written thirty minutes earlier in this same slice, is what caught it, which is a better argument for the sweep than anything written above it. Repaired by re-running those four files; local measures 0 exposed again. An undo derived from the current state is not the inverse of the change. THE TIMING IS W247's ARGUMENT AGAIN: mise_rw already exists on local and dev, and on production it does not, so the hole shuts before the role arrives. D-848. THE SEVENTEEN BECAME A PROPERTY ON 2026-09-01, D-860, and the list is what had been breaking. Four of the seventeen name functions ESTATE creates, so a spine-only build aborted at line 113 on the grant above them, and CI had been red on main for two days. Section 2 is now one loop over pg_proc where prosecdef issued through oid::regprocedure, which is the same question a1 has always asked and the same question verify-definer-not-public.sh asks after the whole chain. a1 STOPPED BEING COVERAGE THE MOMENT THE SWEEP LANDED, which is precisely the criticism a1's own comment levels at the draft it replaced, so it is now labelled a post-condition: it can still fail where a revoke by a non-owner warns and changes nothing, and nowhere else. a2a IS WHAT MAKES THE SWEEP SAFE and it is the assertion that earns the change. A definer function with a DEFAULT acl gives app_rw EXECUTE through PUBLIC and through nothing else, so a sweep takes the application's own access away, and a seventeen-name list is by definition blind to a function nobody added to it. Proved by giving a stand-in schema a definer function called a_function_nobody_added_to_the_list: the sweep revoked it, a2a raised app_rw LOST EXECUTE, the file exited 3, and the transaction rolled back with access intact. Re-run on local, dev and production: exit 0 and revoked 0 on all three, which is the measured form of altering no privilege anywhere. Probes are now 367, not 352, because seven external entries were written for the first time since that category was built. Q-528 Cause B. *Evidence: 1 migration.*
W250A venue is found by what it is, not only by what it is calledcompleteQ-512's first slice, and the owner chose the full build over a cheap overview match. Until this, /directory/v1/venues?q= was name ilike '%q%' or city ilike '%q%' over 30 845 published venues: a couple searching for a lakeside castle found one only if the word castle was in its NAME. VELA renders 1 316 public pages off this route. IT WORKS, AND THE PROOF IS A VENUE WHOSE NAME SAYS NOTHING. *Kasteel Van Huizingen* and *Landhaus Absalonshorst* both come back for "a lakeside castle for a wedding", carrying none of those words. bge-m3 is multilingual, measured: the English phrase against its Hungarian translation scores 0.696 and against "industrial warehouse for a trade fair" 0.418. ★ ONE HALF OF Q-512's OWN EXAMPLE CANNOT BE ANSWERED AND THE SLICE SAYS SO. The example is "for eighty guests". Measured on production, capacity_max_pax is NULL for ALL 30 845 published venues and per-layout capacity exists for THIRTY-FIVE. There is no capacity data to embed. Q-524. THE RE-EMBED PATH IS DERIVED, WHICH IS THE PART Q-512 CALLED EASY TO FORGET. No trigger, no queue table, no invalidation flag: a venue is stale when the md5 of the text it would be embedded from disagrees with the md5 stored beside its vector, so an edit to a name, a city, an overview or a type re-queues it by itself, and so does changing the model. ONE definition of that text, called by both the job and the view, because two definitions of one string eventually disagree and then every row is permanently stale or permanently fresh and wrong. THE QUERY WAS REWRITTEN AFTER MEASURING IT, NOT AFTER GUESSING. The obvious version put the distance in the WHERE and the ORDER BY: EXPLAIN ANALYZE said Seq Scan, 2261 ms, index untouched, on 12 000 embedded venues. Two causes and only one is obvious - a case around the distance stops the planner matching the index, and a vector(1024) is 4100 bytes, OVER THE TOAST THRESHOLD, so every row was fetched out of line for a distance then discarded. The scan cost was detoasting, not arithmetic. Candidates now come from a CTE the index can serve and the page joins to it: 0.37 to 0.49 s warm through the Worker, inference included, at machine load 233. hnsw.ef_search is raised to match the candidate limit, because its default of 40 is a CEILING ON ROWS and asking for 300 with it silently yields 40. THE DISTANCE CEILING WAS MEASURED. Six real questions against four strings of nonsense: nothing real lands above 0.4459 and nothing meaningless below 0.5681, so 0.55 sits between them and the nonsense queries match zero or one venue apiece. It is a floor on RELEVANCE, not the ranking, and without it a meaningless query returns the twenty least-unrelated venues and looks like a working search that is simply wrong. FOURTEEN SABOTAGES, AND THREE OF THEM CAME BACK GREEN THE FIRST TIME. Each green one was a gap in the suite, not a redundant defence. Deleting the semantic clause left the suite reporting *9 passed, 0 failed, 1 SKIPPED* - a skip is for a precondition the SUITE cannot meet, never for the endpoint failing to do the thing it is for, and it is now a failure. Removing the literal-first ordering stayed green TWICE: a venue name embeds nearest its own vector, so the rule and distance ordering agreed by accident, and the second attempt returned only literal matches so the partition held over an empty half; the fixture is now a word where the two orderings measurably disagree. Removing the minimum query length stayed green because ab has thousands of substring matches that fill page one, so the fixture became a two-character string matching nothing at all. AND THE CI IMAGE CHANGED WITH THE SLICE: vector.control has no trusted flag, so the extension needs a superuser and belongs in a migration (D-838), and migrations run in CI where postgres:16 does not carry pgvector. Now pgvector/pgvector:pg16. The embed job WRITES to the database rather than emitting SQL like W67 and W70, D-851: an embedding is not a claim about the world, it is recomputable, nobody can review 1024 floats, and this corpus would be 250 MB of text. A backtick in a SQL comment ended the tagged template for the fourth recorded time; the typechecker caught it. VERIFIED ON PRODUCTION 2026-08-31 AFTER THE DEPLOY, AND THIS SLICE HAS A REAL DISCRIMINATOR - unlike W248, whose surface is secret-gated. 30 845 of 30 845 published venues embedded, queue empty. q=a lakeside castle for a wedding returns eight venues of which FOUR carry none of those words: *Landhaus Absalonshorst*, *Kasteel Heeze*, *Chateau De Resteigne* and *Powel Crosley Museum*. The two castles are Dutch and French; no substring search could ever reach them. AND Q-512's OWN EXAMPLE IS ANSWERED: the same question with country=HU returns *Vörös Kastély* and *Sir David Balaton Kastély* - a castle on Hungary's lake. The Hungarian phrasing works too, which is the reason bge-m3 was chosen over a second pipeline. FOUR INVALID CONTROLS, all on production: nonsense returns 0, a two-character query returns 0 because it is never embedded, the country filter still excludes everything outside it, and a literal name is still first. The embedding never appears in a response. THE PRODUCTION CORPUS RUN DIED AT 14 480 OF 30 845 with SSL SYSCALL error: Can't assign requested address, which is the local socket layer rather than the database - about a thousand psql connections over forty minutes and one of them loses. The work was never at risk, because the queue is derived and a re-run resumes exactly where it stopped; what was at risk was the UNATTENDED run. The generator now retries transient connection failures and refuses to retry anything else, since retrying a real fault is how one becomes five. D-850, D-851, Q-524. *Evidence: 1 migration, acceptance suite.*
W251A company is found by what it does, not only by what it is calledcompleteQ-512's second slice, and the numbers are the argument. /directory/v1/companies?q= was legal_name ilike or trading_name ilike over 27 284 published suppliers. Measured on production the morning this was built: q=videographer returned TWO companies and 619 are videographers; q=florist returned 15 and 242 are florists; q=catering returned 1 093 of 1 485. That is not a shortfall, it is the search unable to answer its own directory's most obvious question, with nothing anywhere saying so. THE FIELD THAT ANSWERS ALL THREE WAS SITTING THERE UNREAD. source_category is carried by 25 579 of the 27 284 and /directory/v1/companies had never once looked at it. It is an upstream LABEL and not a key, which is exactly why it belongs in an embedding: 602 distinct values, with "Wedding Planner" and "Wedding planner" as separate entries totalling 3 264 rows. A vocabulary that disagrees with itself about capitalisation cannot be a key; as language the two are identical, and that is the property being used. W250's COUNTRY JOIN WAS NOT COPIED ACROSS, AND THE FIRST DRAFT'S REASON FOR COPYING IT WAS INVENTED. The draft claimed 2 923 published companies carry a country code and no address line. Measured: ZERO. Of 27 284 published rows, 0 have a country without an address, 5 467 have an address without a country, 18 880 have both and 2 937 have neither -- and a company address_line is free text ending in the country spelled out, "..., 2800 Mechelen, Belgium". The join would have earned one thing, USA reading as United States, for a correlated subquery evaluated twice per row across the corpus by the queue view above it. A venue is different and W250 is still right, because its place comes from separate city, region and two-letter country columns, so without the join a Hungarian venue's text really does end in the letters HU. TWO COLUMNS THIS DIRECTORY SERVES ARE EMPTY FOR EVERY PUBLISHED ROW. sectors is 0 of 27 284 and is a KEY IN THE PUBLIC PAYLOAD, so every consumer reads an empty array and cannot tell that from "this company has no sectors" -- Q-525, the same shape as the two defects VELA reported in August, where the response looked like an answer to the question asked. trading_name is also 0, so one of the two ilike clauses in the search this slice widened matches nothing, ever -- Q-526. Neither is repaired here and the slice deliberately routes around both. THE SQL IS PARALLEL TO W250's AND NOT SHARED, D-853: two tables, two column sets, and a shared function's body would be a CASE on which table called it. The sharing happens where it costs nothing -- ONE generator embeds both through an --entity flag, ONE ceiling governs both searches, ONE scheduled job re-embeds both. FIVE ROUTE SABOTAGES AND TWO MIGRATION SABOTAGES, EVERY ONE RED, and the suite was written against W250's three green ones: deleting the semantic clause is a FAILURE and not a SKIP, the ordering fixture is a word where literal and distance ordering measurably disagree and the suite refuses to run the partition unless both classes are on the page, and the short-query needle is checked against the corpus first. After every sabotage the source was diffed byte-for-byte against a pristine copy. VERIFIED ON PRODUCTION AFTER THE DEPLOY, WITH A REAL DISCRIMINATOR. 27 284 of 27 284 published companies embedded, queue empty, one model. q=someone to film our wedding day returns ten companies of which FIVE carry none of those words -- *HDpicture*, *The Longest Wave*, *The Amazing Crew* -- and the old substring search returns zero for that string. *Szloboda Bianka fotografus | eskuvo foto-video*, a Hungarian wedding videographer, is found by an English sentence, which is why bge-m3 was chosen over a second pipeline. q=videographer now pages to exactly 300 where it returned 2. Four invalid controls on production: nonsense 0, a two-character query 0, digits-only 0, and country=HU returns 20 results every one of them HU. A literal name is still first. AND THE 300 IS NOW IN THE DISCOVERY DOCUMENT, because a consumer paging a question would otherwise find a page that suddenly went empty -- the candidate CTE is limit 300 and that bound is what took the venue query from 2261 ms to 0.56 s. W250 changed what q MEANS on a public endpoint and wrote it down nowhere; this states it for both endpoints at once. D-853, Q-525, Q-526, Q-527. *Evidence: 1 migration, acceptance suite.*
W252A stale embedding re-embeds itselfcompleteQ-512's third slice, and the half W250 deliberately left open. W250 and W251 made staleness DERIVED -- a row is stale when the md5 of the text it would be embedded from disagrees with the md5 stored beside its vector -- so nothing has to remember to enqueue anything. Nothing drained the queue either. An edited venue answered semantic searches from a vector computed from text that no longer existed until a person ran a Python generator on a laptop. Q-512 called the re-embed path "the half that is easy to forget"; deriving the queue removed the forgetting and left the waiting. HOURLY, AND THE CADENCE IS A MEASUREMENT RATHER THAN A HABIT. Reading either queue computes each row's source text, three joins for a venue: 8.1 seconds over 30 845 published venues on production, returning zero. That rules out the minute cron this Worker already runs. The Sunday cron would leave a Monday edit stale until the weekend. An hour bounds it close to the directory's own s-maxage of 900 seconds. THE WRITE GOES THROUGH TWO FUNCTIONS SO THAT TWO TABLES STAY OFF AN ALLOWLIST, AND THAT IS THE WHOLE DESIGN. A plain UPDATE from the Worker would need app.venue and app.company on MAINTENANCE_TABLES, whose own doc says adding to it is a decision: every future maintenance query could then delete from app.venue, unattended, with RLS bypassed and no user present, against a public directory of 30 845 venues and 27 284 companies. With the setters the scheduled path gains exactly one capability -- set four derived columns on one published row -- and the delete stays refused. The precedent is D-752's, which allowlisted the two retention erasures as functions for the same reason. ★ NOT security definer, AND THAT IS THE POINT RATHER THAN AN OMISSION. withMaintenance has already satisfied the row policy before it calls anything, since venue_update begins app.is_platform_admin() or ..., so an ORDINARY function does the job and keeps a property a definer one destroys: app_public gets permission denied and an unelevated app_rw is refused by the policy. W247 and W249 spent an entire session shutting twenty-two definer functions to PUBLIC, and the definer sweep still measures exactly 22 after this batch. EXECUTE is revoked from PUBLIC anyway, as insurance against the one-word edit that would undo the paragraph above. THE TRUST IS REPLACED BY A CHECK, following w215-erasure-footprint: the migration reads prosrc on every build and refuses a body that is definer, that holds more than one UPDATE, that stops restricting itself to published rows, or that stops setting any of the four columns. All four were watched go red -- security definer added, the published-only clause removed, embedding_source_md5 dropped from the SET, and a second UPDATE smuggled in. THREE JOB SABOTAGES, ALSO ALL RED, and one of them is the hazard this slice most easily creates: removing the early return from the hourly branch means the weekly retention purge, the two GDPR erasures and the orphan sweep run TWENTY-FOUR TIMES A DAY, and the suite caught it by counting the purge's own audit rows. A mismatched cron expression reds three assertions at once. AND THE SUITE'S FIRST DRAFT WAS WRONG IN A WAY WORTH KEEPING. It fired /__scheduled and read the database immediately, and reported that the cron does not drain the queue. It drains it in about fourteen seconds: the endpoint answers in 2 to 5 milliseconds because the handler hands its work to ctx.waitUntil and returns. The harmless half of that race is a false negative; the dangerous half is the same race on an assertion of ABSENCE, which passes instantly on a broken build before the thing it forbids has had time to happen -- so section 4's pause IS the assertion rather than a convenience. THE GENERATOR NOW WRITES THROUGH THE SAME SETTERS, D-855, because two jobs writing the same four columns through two hand-written statements is two places for a fifth column to be right in one and wrong in the other; it also narrows the generator, which connects as the owner and bypasses RLS, so it can no longer put a vector on an unpublished row. VERIFIED ON PRODUCTION. All three schedules registered in the deploy output, 0 * * * * among them. The setter was handed a real unpublished production company inside a rolled-back transaction and returned NULL and changed nothing; PUBLIC may execute neither setter and app_rw may execute both. AND THE CRON WAS PROVED TO RUN ON PRODUCTION, NOT MERELY REGISTERED. A version id and a trigger line say a deploy happened, not that the job works. So one production company's embedding_source_md5 was set to the sentinel w252probe... at 10:31 UTC -- a DERIVED column nobody serves, so the name, the address and the overview were untouched and no consumer could see anything -- which made that row, and only that row, stale. The queue depth was 1. The cron fired at 11:00 UTC and at 11:01:09 the row carried a fresh md5 matching its own text, the model stamp, and both queues were empty again. Nobody ran anything. AND THE PROGRESS CHECK BECAME THE PROBLEM. A watcher polling count(*) from app.company_embedding_queue every sixty seconds -- the expensive derived scan -- was competing with the production embed on the same branch: 0.8 rows/s with the watcher, 10 rows/s without it. The handout's rule about not running anything beside the job arrived from an unexpected direction, as the instrumentation. D-854, D-855. *Evidence: 1 migration, acceptance suite.*
W253An emailed attachment lands in the project filescompleteD-842 stage 3. Stage 1 shipped as W244, stage 2 as W248. Brevo sends Attachments carrying Name, ContentType, ContentLength and a DownloadToken, and inbound-email.ts never read the key -- so every attachment anybody has ever mailed into a conversation was discarded without a trace. Not refused, not recorded: discarded. A reviewer reading the quarantine screen could not tell that the message in front of them had ever carried a file. THE WORKER CANNOT WRITE app.file ITSELF AND THAT DECIDED THE WHOLE SHAPE. withInboundEmail may touch exactly two objects, reading app.chat by primary key and calling one function, and that allowlist is not tidiness: there is NO SESSION on this lane, because the sender is the thing being identified, so RLS has no tenant to filter on and any direct write would be a write across every tenant behind a shared secret. The Worker therefore does only what a database cannot -- fetch bytes over HTTP and put them in R2 -- and both decisions stay in SECURITY DEFINER functions that re-derive the tenant from app.chat exactly as W168 does. AN ATTACHMENT NEVER OUTRUNS THE ALLOW-LIST. app.receive_inbound_email returns the same id whether the mail posted or was quarantined, deliberately, so the answer is not an oracle; app.record_inbound_attachments therefore reads that outcome FOR ITSELF and hands back nothing at all for a mail no human has admitted. A held attachment keeps its metadata and none of its content, so the reviewer sees a file was sent without a byte of it being fetched, and a stranger's file cannot reach a project's file list while their words are still in the queue. ★ THE DOWNLOAD TOKEN IS NEVER STORED, AND THE ABSENCE IS ASSERTED TWICE. It is a bearer credential: whoever holds it can fetch that attachment from Brevo. The function returns ORDINALS, which index into the array the Worker already holds, so the database never learns a token and cannot leak what it does not have. Self-assertion a4 fails the migration if any column of the new table is ever named for one, and the suite additionally proves no token VALUE reaches any text column -- a token written into reason would pass the shape assertion perfectly. FETCHING IS INLINE AND NOT IN ctx.waitUntil, WHICH IS W252'S LESSON INVERTED. W252 fired /__scheduled and read the database two milliseconds later; the harmless half of that race is a false negative, and the dangerous half is an assertion of ABSENCE, which passes instantly on a broken build. Section 2 asserts that a quarantined sender's file is absent, so the work happens before the response and it is safe to be slow because W248 made a redelivery post nothing. LIMITS ARE THE OWNER'S AND ARE Q-529, shipped as stated defaults rather than blocking: ten attachments, 25 MB each, named constants in one place, and no type restriction -- a mail only posts when its sender holds messaging.post and is already a participant, so this is an upload by a member through a slower door, and a second narrower type policy would mean two answers to what a member may store. A refusal and a failure are recorded as DIFFERENT states on purpose: a run of refused says the numbers are too tight, a run of failed says the provider or the store is unwell. FORCED RED IN FOUR DIRECTIONS. Removing the quarantine guard reds section 2 alone; removing the count limit reds section 3; removing the declared-size check reds section 4; and swallowing the failure -- the silent discard this slice exists to end -- reds TEN assertions. AND THE LEDGER PROBE FOUND A DEFECT THE MIGRATION SHOULD HAVE CAUGHT, D-859: a mid-build signature change left a stale seven-argument settle_inbound_attachment beside the new one, which made a probe branch yield two rows, and having count(*) = 4 counts rows rather than checking which conditions matched -- so dropping a constraint brought the total back to 4 and read as PRESENT. The file now drops every overload by name and asserts exactly one of each survives, and every probe branch yields at most one row. VERIFIED ON PRODUCTION. Post-flight rehearsed against dev first and every value matched: RLS forced, one policy, the constraint present, one function of each name, both definer, PUBLIC on neither, zero columns naming a token. The definer sweep now measures 24 where it measured 22, because both new functions are counted and closed. AND THE DEPLOY WAS PROVED AGAINST THE ARTIFACT RATHER THAN THE EXIT CODE: a dry-run build from the deployed commit reports Total Upload 1634.26 KiB, byte-identical to what the real deploy reported, and that bundle contains inbound/attachments, record_inbound_attachments and settle_inbound_attachment with a deliberately invalid control at zero occurrences. The route itself cannot be driven from outside without the inbound secret, which is the owner's; it was confirmed to answer 401 on a wrong secret and NOT 503, which would mean it had lost its configuration. AND ONE DEFECT WAS FOUND BY READING db.ts RATHER THAN BY A FAILURE. inTransaction opens a FRESH postgres client per call and releases it with an UNAWAITED sql.end(), so the first draft's per-attachment settle opened one client per attachment with overlapping closes, against a runtime that caps simultaneous outbound connections. The suite was green at 30 of 30 the whole time, because wrangler dev does not enforce that cap -- the same shape as the PBKDF2 lesson. Every settle now runs in ONE transaction after the loop, which is also the smaller code, and a crash mid-loop stays recoverable because record returns only what is still pending and Brevo retries exactly what a dead Worker produces. Disabling that batch still reds ten assertions, so the refactor did not make the suite vacuous. D-858, D-859, Q-529. *Evidence: 1 migration, acceptance suite.*
W254The anonymous read stops at the published gateon productionQ-535, and the shape is the whole slice. app_public holds SELECT on 45 tables in app. Thirty carry a BLANKET PERMISSIVE using (true) read policy, and twenty five of those carry no owner column and are reference data. THREE carry one and had no gate: company_feature_category 4 585 rows on not_published companies, venue_amenity 2 760 and venue_group_member 478 on not_published venues. 7 823 rows describing records the public directory deliberately withholds. IT WAS LATENT AND THE PLANNED FEATURE IS WHY IT WAS CLOSED NOW. No route served them: company_feature_category is read by NOTHING in api/src, the one public venue_amenity read sits inside a route whose venue lookup RLS had already filtered and is then scoped to that venue, and every venue_group_member read is past line 7300 on the TENANT connection while the last withPublic call is line 2908. **But the comment sitting on that amenity read says the amenity filter is a directory feature *this directory does not have yet*, and a filter route queries the table DIRECTLY rather than once per venue. ★ THREE OBVIOUS REPAIRS ARE ALL WRONG AND ONE OF THEM WAS THIS SLICE'S OWN FIRST ANSWER. A second PERMISSIVE policy changes nothing, because permissive policies are combined with OR and the blanket true still admits every row. That was recommended in Q-535 before polpermissive was read, and reading it is what corrected it. Narrowing the blanket policy itself would govern the TENANT application too, since it is TO PUBLIC and is the only read path on those tables for every role. And copying app.venue_readable(venue_id) from the sibling is wrong a third way: that is the AUTHENTICATED gate, its third arm is publication_state <> 'not_published', so it admits published_authenticated, which the public directory withholds on purpose. THE CORRECT SHAPE WAS NOT INVENTED, IT WAS FOUND IN THE SCHEMA. venue_media_public_published_only and artist_profile_public_published_only already pair a blanket permissive policy with a RESTRICTIVE one scoped to app_public. Restrictive policies are combined with AND and are invisible to every other role. MEASURED, NOT ASSUMED, WITH TWO NEGATIVE CONTROLS. On local, app_public went 48616 to 44031, 32907 to 30147 and 2392 to 1914, which is exactly the withheld rows and no others, while app_rw stayed at 48616 / 32907 / 2392 and mise_rw at 32907. FORCED RED: creating the three policies PERMISSIVE instead reds assertion 2 and the file RAISES, so the transaction rolls back and the database keeps the correct policies, which is the refusal doing its job rather than a mutation to clean up. Assertion 4 is the paired control and requires the three blanket policies to SURVIVE, because removing one would narrow the tenant application and would look like a fix. VERIFIED ON PRODUCTION AGAINST THE LIVE API**: jakarta-convention-center, a published venue, still returns all 9 of its amenities through /directory/v1/venues/detail, and an unpublished venue's slug still answers not found. D-866. *Evidence: 1 migration.*
W255The overview says what language it is incompleteASKED FOR BY VELA, AND THE ASK WAS THE RIGHT ONE. VELA renders this catalogue as 1 228 public pages under <html lang="hu"> and then prints an English paragraph inside, which is a page that changes language halfway down for a reader and a document whose declared language contradicts its content for a crawler. They asked for a TAG rather than a rewrite. NOBODY HAS TO REWRITE 20 781 ROWS. ★ THE FIELD IS MIXED, AND THAT IS THE WHOLE JUSTIFICATION FOR A PER ROW TAG. VELA's two samples found no confirmed Hungarian description and concluded there were none. Measured over the FULL corpus rather than a sample: 19 183 English, 63 Hungarian, 1 535 undetermined. Gundel Palota, Szépia Bio & Art Hotel and Hotel Délibáb are fluent Hungarian prose, and 95 Hungarian venues carry at least one Hungarian marker. So a consumer hardcoding lang="en" would be wrong for those rows, which is the failure a tag prevents and a blanket assumption cannot. AND IT IS NOT A HUNGARIAN PROBLEM: 93 per cent of German, 98 of French, 99 of Italian and 100 of Portuguese overviews are English too, sampled and read to confirm the detector was not mislabelling foreign text. VELA saw Hungary because Hungary is what VELA renders. THE DETECTOR ABSTAINS, AND ABSTAINING IS THE FEATURE. VELA's letter says their own detector is sound enough for a floor and not sound enough to drive a user-visible attribute, *which would then be wrong for exactly the borderline cases*. So the rule is two directional and refuses the middle: hu>=2 and en=0 gives hu, en>=4 and hu=0 gives en, anything else is NULL and the key is absent. AN ABSENT KEY MEANS UNDETERMINED AND NEVER MEANS ENGLISH, stated in the function comment, the route comment and the reply. A FUNCTION AND NOT A COLUMN, so there is nothing to backfill and nothing that can go stale: the answer is a pure function of the text, which is the staleness W252 needed a checksum to solve and this does not have. ONE REGEX PASS PER LANGUAGE, NOT ONE PER MARKER: the first draft cost 64.9 ms for a 100 row page against 18.7 ms for this, and count(distinct) is what keeps the two equivalent, since az az az is one marker three times and must not reach a threshold of two. Proved identical to the 32 pass form on all 23 523 rows at zero disagreements. SERVED ON BOTH SURFACES, which is Q-513's lesson: overview is on the list and the detail, so its language is too. FORCED RED THREE WAYS. Dropping the abstention reds assertion 3 alone. Adding hotel to the English markers and loosening the threshold reds assertion 6, the German negative control, which is precisely the *marker that fires on another language* failure the comment warns about. Defaulting the API to en instead of an empty map reds suite assertion 2c and nothing else. AND THE BACKTICK TRAP CAUGHT IT FOR THE FIFTH RECORDED TIME: the route comments named fields in backticks inside a tagged template literal and broke the build. The typechecker found it. D-867, Q-536. *Evidence: 1 migration, acceptance suite.*
W256A venue overview is served in a second languagecompleteASKED FOR BY THE OWNER 2026-09-02, AND HALF OF THE ASK WAS ALREADY TRUE. The ask included copying the 136 Hungarian overviews *as a Hungarian translation with a properly labeled HU label*. Measured: they already carry a correct hu label, because W255 made the language map a FUNCTION over the text rather than a column, so app.overview_language() answers hu for them on every route today. Copying them would create a second row that can disagree with the first, which is precisely the staleness W255 was built to avoid. What those 136 actually lack is the ENGLISH direction. app.venue_overview_translation HOLDS ONLY TRANSLATIONS. app.venue.overview stays the original and is never copied into it, so nothing can drift out of step with its source, and a row may never claim to be a translation into its own source language. Both directory routes serve overview_translations keyed by field, name each translation's language in the SAME language map W255 built, and the detail route merges machine_translation into provenance so a consumer can never mistake a machine translation for prose somebody wrote. confidence reads from the merged map rather than the stored column, because deriving it from a different map than the one served is how the two come to disagree. ★ THE RESTRICTIVE PUBLIC GATE IS IN THE FILE THAT CREATES THE TABLE, NOT ADDED LATER. W254 shipped the same morning to close exactly this hole on three tables that had shipped without it, and a translation of a withheld venue's overview IS that venue's overview. ★ AND THE EXCLUSION CANNOT BE A REGEX, WHICH IS MEASURED RATHER THAN ASSERTED. The owner asked that strictly technical descriptions not be translated. A keyword sweep was run first on production and its precision is terrible: it matched almost entirely GOOD prose, because words like *not available* and *does not* occur inside real descriptions. So translatable is the first field of every response and a translation is returned only when it is true, which costs nothing extra because the model is already reading the text. What it has to catch is broader than placeholders: 11 LLM meta-responses, one reading *I understand you would like me to stay quiet since there was no readable user message*; three FABRICATED records whose stored description is of a different business entirely; status lines; scraped UI fragments; and 58 markdown-heading artefacts. Translating a fabricated description into a second language is how it stops looking imported and starts looking native. Q-538. FORCED RED THREE WAYS. The migration's assertion 3 fires on a gate created PERMISSIVE and the file RAISES, so the transaction rolls back and the database keeps the correct policy. The suite's 1c AND 2a red under the same sabotage, with the restore armed BEFORE the sabotage because local is shared, and 2a is behavioural: app_public saw the withheld venue's translation. 2b stayed green throughout, which is W254 repair 2's guard. Removing the language-map merge reds 3b alone. BEHAVIOURALLY PROVED: on the same two fixtures in one transaction, app_public sees 1 and app_rw sees 2. THE TRANSLATIONS THEMSELVES ARE NOT PRODUCED YET. ANTHROPIC_API_KEY is unset in this environment and a secret is the owner's. The worker is built, dry-run against production and costed at $1.53 for all 857 on the Batch API, and 07-delivery/OWNER-ACTION-01-anthropic-api-key.md is the two commands that start it. D-870. *Evidence: 1 migration, acceptance suite.*
W257Nine overviews that describe another business are retractedunverifiedmigration + acceptance suite, w257 SKIPPED 5 assertion group(s), 2 ran. A SKIP IS NOT A PASS, so this is not complete. A WRONG DESCRIPTION IS WORSE THAN AN ABSENT ONE, AND ABSENT IS ALREADY THE NORM: 21 088 venues carry no overview at all. Nine published listings were attributing invented facilities, and in one case an invented commercial partnership, to NAMED REAL THIRD PARTIES. app.venue.overview goes null and the overview key is REMOVED from provenance; the text, its old label, its length, the venue name, country, city, publication state, the class of defect and a written reason all go into app.audit_event FIRST, in the same statement and so the same snapshot. NOTHING IS DESTROYED, so Q-538 option (b), re-enrichment, stays open and is better informed, and the file header carries the one statement that reverses the whole pass from the trail. Two classes kept apart, because Q-538 asks for one decision per row: SIX describe another business (ADRENALIN PARK SVETA ANA as *Lokatoda play&fun*; The View Hotel, Ioannina as the Navajo-owned hotel in Monument Valley, Arizona; Lemon Meringue Pie as *A Padaria Portuguesa* with an invented Galp partnership; Salty Olives as *Salt Food Atelier*; Arena Varna as *Cinema Arena*; A casa do Messias as *Casa do Ti Messias* in another town) and THREE are not descriptions at all (two assistant meta-replies, and Horton Gellery which says no information is available and then names SIX COMPETING VENUES on its own listing). ★ THE FINDING UNDER THE FINDING: imported WAS PROTECTING THE BAD TEXT. All nine carried provenance -> overview = 'imported', rank 20, which app.provenance_rank documents as *exact, from the Bubble export*. The TEXT was written by a model and only its TRANSPORT was the export. W67 writes overviews as ai_direct, rank 10, and provenance_may_write requires greater-or-equal, so W67 and every later correction pass was REFUSED on exactly the rows that most needed one. ★ AND W256's TWO COUNTS WERE BOTH WRONG, WHICH IS WHY THIS IS NINE AND NOT THREE. *11 LLM meta-responses* was inflated by a probe with no word boundaries: as an AI matched inside *has an air conditioning system* and *as well as an airport shuttle*, and four of seven hits were ordinary hotel prose. Bounded, that class is THREE. The fabrication count was low for the opposite reason, that no keyword can find a fabrication: two more were found only by a structural probe and one only by widening the meta sweep to *I will remain silent*. ★ AND NO CHEAP PROBE CAN ENUMERATE THE REST. The structural probe, that an overview never names its own venue, returns 3 098 published rows and a sample of fourteen was almost entirely ordinary first-person blurbs. Far too imprecise to sweep on, so the true population is LARGER THAN NINE and needs the model pass W256 already builds. Recorded in Q-538 rather than guessed at. ★ THE FOREIGN KEY COULD NOT HAVE DONE THIS. venue_overview_translation_venue_id_fkey is ON DELETE CASCADE on the VENUE and no venue is deleted here, so the cascade never fires on a column being emptied. Measured: nulling an overview leaves its translation in place. Without W257's explicit delete a W256 machine translation would outlive the fabrication it was made from and go on being served as native prose. Zero rows today, in the file so it is already right on the day the batch runs. ★ THE FIRST FORM OF THIS SLICE STAMPED provenance -> overview = 'retracted' AND THE BATTERY REFUSED IT, which was the right answer and is why the key is now DELETED. The stamp reddened FOUR assertions in THREE unrelated suites: w57 1a and w68 2b, *no venue claims an overview it does not have*, WORD FOR WORD in two separately authored files, and w63 1a and 1b over all six imported columns, the second adding that a label the ladder does not recognise files real source data under inferred by accident. Two independent suites converging on one sentence is the system saying the invariant is intended, not three stale assertions to exempt. Removing the key loses nothing: provenance_rank(null) already falls to else 0, so the field stays refillable, and telling a retraction from a venue that never had an overview moves to app.audit_event, APPEND-ONLY and so a stronger record than a jsonb key any later statement could overwrite. The file carries a labelled ONE-TIME REPAIR for the nine rows that briefly wore the stamp, idempotent and a no-op on any database built from empty. FORCED RED FIVE WAYS, every restore armed BEFORE the sabotage because local is shared. Restoring one fabricated overview reds 1b and 1c alone and names *Salty Olives*. Making an ABSENT label outrank ai_direct reds 3a alone, and putting a label back beside a null overview reds 1c alone. A trigger that really does delete translations when an overview is nulled reds 4a alone. A translation of a retracted overview reds 5a alone. ★ THE TRAIL TURNED OUT TO BE UNFALSIFIABLE, learned by FAILING to sabotage it: app.audit_event carries an append-only trigger and refused both the UPDATE and the DELETE, so assertions 2 and 6 had to be reddened by INSERTING a bad record instead. ON AN EMPTY DATABASE FIVE OF THE SIX ASSERTIONS ARE VACUOUS and the file says so out loud; the ledger probe is a pass marker, because per-venue records cannot exist there. Q-538 answered, D-871. *Evidence: 1 migration, acceptance suite.*
W258Programme elements and placements become approvableon productionThe October 23 microsite's data layer. app.approval gains agenda_item and event_placement as subjects, and app.approval_snapshot holds what was SUBMITTED, so an approver votes on a frozen text and the website builder keeps reading the last approved version while a correction is under review (D-875). Both approvers must say yes, all_required. Applied to local, dev and production 2026-09-02 with 12 self-assertions each. Shipped a live defect: rejecting a programme element raised EC466 because reopen_approval_subject had no agenda_item branch; W261 fixed it. *Evidence: 1 migration.*
W261The ESSZ is an asset, and an asset gets a production cycleon productionThe owner corrected W258: an ESSZ IS an asset. app.event_placement is folded into app.asset keeping every id, with each original written to app.audit_event first. Adds the third approval decision changes_requested system-wide with a comment required (D-878), the nine-stage production cycle whose two review stages are entered by submission and left by decision (D-879), and the columns a printed product needs. Also fixed W258's live rejection defect (D-881). *Evidence: 1 migration.*
W262The placement table is droppedon productionThe contract half of W261's expand-migrate-contract, split out so it runs after the Worker that read app.event_placement was replaced. Its probe asserts the table's ABSENCE. *Evidence: 1 migration.*
W263A technical specification that fits any asseton productionData, not columns. app.asset_spec_field declares what to ask per product type, app.asset.spec holds the answers, and the write MERGES key by key so one type's screen cannot erase another's answers (D-882). *Evidence: 1 migration.*
W264Six starter product typeson production31 field definitions across six types, the sixth deliberately not physical: bespoke software asks about platform and personal data, which proves the design fits more than things with a width (D-883). Every word was chosen by a developer, Q-541. Its ledger probe named the product in Hungarian; W269 renamed it and the probe read MISSING until W271 widened it. *Evidence: 1 migration.*
W265One code per idea, and nineteen asset typeson productionCorrects W264's three codes for one idea: a product-specific field OVERRIDES the common one of the same code, so material always means material with a different list per type (D-884). Thirteen more types from published trade sources, and outdoor, wind rating and ballast as COMMON safety fields (D-885, D-886). *Evidence: 1 migration.*
W266A product type can suppress a common fieldon productionFound by reading the form W265 produced: custom software was being asked its material and its wind rating. A product-specific row with a suppressed flag removes the question through the same override mechanism (D-887). *Evidence: 1 migration.*
W267Weather, fixing, and the hazards they raiseon productionWater resistance as a five-value question, weather limits with a named decision-maker, and fixing consumables as common fields (D-888). app.hazard is the catalogue and app.risk_assessment the polymorphic register, in app and not estate.risk because that is scoped to a property and has no event_id (D-889). Eight hazards seeded per tenant. *Evidence: 1 migration.*
W268An asset's answers raise its risks automaticallyon productionA trigger that ONLY EVER INSERTS: outdoors raises the wind pair and the fixing question, power outdoors raises water-to-electrical, over 25 kg raises manual handling. It never edits or deletes an assessment, which is the whole safety property (D-890). 6 self-assertions in both directions. *Evidence: 1 migration.*
W269English is the base languageon productionEvery base row becomes English and Hungarian moves into asset_spec_field_label, hazard_label and product_label, copied in the same statement before the base row is rewritten so nothing is lost (D-891). It created hazard_label and seeded no rows in it, which W271's acceptance suite found. *Evidence: 1 migration.*
W270A membership can be scoped to named eventson productionapp.membership_event_scope is an allow-list enforced as gate 4b after the role and never instead of it; a membership with no rows behaves exactly as before (D-892). 0 rows on production until the October 23 accounts are created, Q-544. *Evidence: 1 migration.*
W271The production board, specification and risks reach every eventcompleteThe owner's first unbuilt item of 2026-09-03. MicrositeAssets.tsx becomes AssetProductionBoard.tsx, mounted on the microsite AND on a new Production tab of /assets, with a product-type picker and a risk panel per asset. Three routes on a new internal surface risks: read, assess, raise; gate 4 sees the event on every call, a cross-event raise is refused by the route, and one select serves list, echo and raise so a caller reads back exactly what it wrote. English becomes the base language of the chrome (D-893): system/i18n.tsx keys a dictionary on the English sentence, the shell translates every nav name at render, and a language switch sits beside the unit system. The acceptance suite found app.hazard_label EMPTY on production; the migration seeds the Hungarian hazard catalogue. 39 HTTP assertions cross-checking input against output; the dictionary test watched go red both ways. *Evidence: 1 migration, acceptance suite.*
W272The mini menu, and only the approvers see the approval surfacescompleteThe owner's second unbuilt item of 2026-09-03. A microsite host now renders MicrositeShell instead of the full shell: the event's name and logo, Programme, Assets, and -- for the editor and for anyone NAMED on an approval of this event -- Approved material and My approvals with a pending count, plus the language switch and the way out. The rule lives in approvalVisibility.ts, tested as a function with negative controls, and the screen's approval panel and handover tab use the same rule. /approvals/mine?event_id= answers the two facts the menu needs, named_on_event and a per-event count. Three routes the previous night shipped without passing the event to gate 4 were caught by W241's survey on the first battery (/assets/production, /agenda/approved-export, /portal/programme) and are gated. The W260 walk's last four steps, dead since W261 removed the placement routes (Q-547), are rewritten against the asset routes and green. 19 HTTP assertions; the approver rule watched go red by sabotage. *Evidence: acceptance suite.*
W273The risk register has a screencompleteThe owner's third unbuilt item of 2026-09-03, and the answer to Q-545's first half. /risks?event_id= lists every assessment on the event, open rows first then by the generated score, with a state filter carrying the API's counts and a warning naming how many are unanswered. A row opens a drawer with the shared RiskAnswerForm -- the same form the asset's own panel uses, so the two cannot disagree -- plus owner and review dates. *Raise a risk* files a hazard from the tenant's catalogue against an asset, a programme element, a space or the event itself, idempotently. In the navigation as a child of Assets, with a new warning mark. w273-acceptance.mjs reads the source in both directions: every /risks route the API declares is called by a screen and every route a screen calls is declared, with a negative control, because W19, W21 and W26 each shipped a write path no control reached. Walked in a browser: raise, answer, 6 → 2. *Evidence: acceptance suite.*
W274The user manual, and a help drawer on every screencompleteThe owner's fourth unbuilt item of 2026-09-03 and Q-543's second and fourth rules. The manual lives in 07-delivery/user-manual/, English base and Hungarian edition, eight sections covering the five procedures the standing task named as undocumented. The manual IS the help: build-help-seed.py renders both editions into w274-help-topics, seeding app.glossary_term rows kept out of the public glossary by is_public = false, paragraphs as definitions, Hungarian in the label tables, and app.help_topic mapping each section to the screen paths it is offered on. GET /help on a new surface declared *any*, read through the tenant path after the elevated path's guard refused the first draft. A help button in the top bar of both shells. 16 assertions: a paragraph read back over HTTP is word for word a paragraph of the manual on disk in both languages; the public glossary never lists a help term, with a positive control. Forced red by publishing a help term. *Evidence: 1 migration, acceptance suite.*
W275The event menu becomes families of short listscompleteOwner 2026-09-03: too many items in one view on the second-level menu. D-620's one column had grown to 22 top-level rows. The column is cut into six families on the rail -- This event, Programme, People and partners, Production, Budget and quotes, Control room -- each a panel of two to six rows, named after PARTS of an event and kept in build order, with the unbuilt blocks still visible in their families, so D-620, D-621 and D-622 all survive. One surface list, one placement each, picked by id; the unit suite and this file both refuse a surface placed twice or nowhere and a family over six. Walked in a browser at 1280 and 662 wide. *Evidence: acceptance suite.*
W276Every screen speaks HungariancompleteThe handover's second unbuilt item of 2026-09-03: only the microsite, the board, the register, the help drawer and the shell translated. web/i18n-wrap.mjs reads the TypeScript AST and wraps every bare English JsxText, labelled attribute, labelled object property, JSX conditional and notice literal in t(); the same detector in --check mode is a CI step, so a new screen with bare English fails the build. The Hungarian for every key is in locales/hu.ts, kept complete in both directions by web/test/i18n.test.ts. Template literals and fragments beside an expression are the stated ceiling (D-902). *Evidence: acceptance suite.*
W277The October 23 microsite auditcompleteThe owner, 2026-09-03: make oktober23.event.clinic perfect and say how the first real users get in. Walked on a phone and a laptop, in both languages, signed out and as each of the three roles. Fixed: status and stage maps translated at module load (a switch to English kept them Hungarian); <html lang="en"> under a Hungarian page; a tab titled event.clinic; three <select multiple> approver boxes replaced by one checkbox picker; a 56px header that clipped the language switch and the way out at 390px; the website builder's page keyed on Hungarian; no security headers and an indexable invitation-only site; an exact-case email match at first sign-in that would have split an invited person from their seat. Built oktober23-access.sh, which creates the three tenant roles and gives an address a seat with its event scope in one transaction; roles are on local and production (D-903). *Evidence: acceptance suite.*
W278Every screen is its own chunkcompleteThe microsite served a 1.55 MB bundle to render one screen. src/App.tsx keeps no static ./screens/ import: all 69 become lazy(() => import(...)), named exports mapped through .then(m => ({ default: m.X })), which is the one form Rollup can split -- a static import is reachable from the entry at BUILD time whatever the router decides at runtime. Three Suspense boundaries, all INSIDE the shells so the navigation stays on screen while a chunk arrives. Entry chunk 1 550 940 bytes to 689 399, 433 KB gzipped to 220 KB, across 93 chunks; two of the three people invited to October 23 never open a second screen and were downloading the budget tree, the seat map and seventy others they cannot reach. The same defect was in three verifiers and that is why they are fixed together: each assumed one build meant one file. verify-w28-1-controls.sh and verify-w29-1-controls.sh read ls dist/assets/*.js and took the FIRST -- after the split that is AccessMatrixScreen-*.js, and W28.1 reported 19 ABSENT controls against a screen that has none of them, on a build where nothing was wrong. That one is the dangerous one: it carries an INVERTED assertion, three routes that must be absent, and an inverted assertion aimed at the wrong file passes for the reason it exists to refuse. deploy-w260.sh fetched /assets/index-*.js and grepped it; risks/assess and agenda/approved-export both moved into a route chunk, so it now fetches every chunk with curl -Z and greps the concatenation, with PROBE_EXTRA_CHUNK as the handle that forces it red. verify-bundle-has-app.sh gains an 850 000-byte ceiling on the ENTRY -- not on the total, which is supposed to grow -- and a refusal of a single-chunk build, both forced red against a doctored dist/. Full battery unchanged at 135 passed, 3 744 assertions, the same four red. D-904. *Evidence: 1 design doc.*
W279An event-scoped membership can do its job, and only its jobcompleteThe October 23 programme editor could not edit an asset of the one event they are scoped to. owningEvent() declared case "asset"; all four call sites in index.ts spelled it "assets", the switch fell to default: return null, and null for a scoped membership is outside_event_scope. Nine further spellings reached it undeclared and CHAT_TARGETS was a third vocabulary again. 620 unit tests and 143 suites passed over it: every gate-4b test used agenda, which was spelled right, and app.membership_event_scope held ZERO rows anywhere until the day before -- W270 shipped the mechanism in August and the first scoped membership in the world was made for this microsite. resourceType is now a RecordType union, the switch is exhaustive over it through a never assignment, and the compiler found all four typos on its first run. The other half was invisible while the first stood: /assets/production/stage, /assets/production/link, /assets/production/detail and /approvals/submit called require() with NO RECORD, and gates 4 and 4b run only if (record && ...), so fixing the vocabulary alone would have shipped a scoping feature that appears to work, on the four routes the scoped account uses most, that narrows nothing. The file arm resolves through app.file_attachment rather than returning null, because the production board submits files for approval; two candidate events refuse rather than pick one, since picking would let a scoped account reach a foreign file by attaching it to an event of their own. 10 HTTP assertions both ways plus an unscoped negative control, forced red twice with sabotages that redden DISJOINT sets. D-905. *Evidence: acceptance suite.*
W280Seven dead error states, three shared components, and a write-only fieldcompleteSeven screens showed "loading" for ever on a failed first fetch. Each set error correctly and each had an {error ? ...} block, placed after the if (!data) return <Loading/> early return, so on a first load it was unreachable. Walked in a browser with the API genuinely stopped -- the only way an error state is visible -- and all seven now name their failure in Hungarian. Field: 91 of 185 call sites passed no htmlFor, and the label is a SIBLING of the control, so those 91 named nothing; it now adopts a single element child with useId, in the shared component, because 91 edits can be forgotten by the 186th caller. Drawer: always rendered and moved off-canvas with right: -480px, so a CLOSED drawer kept seven controls in the tab order inside an aria-hidden subtree; now inert, proven by .focus() leaving focus on BODY. Table: scope="col" on every header. /settings/chains was reachable by nothing -- W96 built the chain editor and it had no nav entry and no link anywhere; it now has a Settings tile using the screen's own already-translated strings. valid_until was write-only AND erased: rendered nowhere, and the update body omitted the key while the route writes it unconditionally, so changing a quote's state NULLED the expiry; also cast to text, because a bare date serialises at UTC midnight and read as the day before for every Hungarian reader. D-906. *Evidence: 1 design doc.*
W281An identifier column holds an identifieron production8 530 companies carried the words NO RESULT in place_id -- a failed Google Places lookup writing its own failure message into the identifier column, and the commonest value there by a factor of 656, so all 8 530 collided with each other on any join or dedupe. Nulled, which is what the lookup meant, with a CHECK constraint refusing whitespace and anything under 10 characters -- deliberately not a Google-format whitelist, since we do not own that format and it has changed once already. Also removed: an app.event_role_reference row code="undefined" that was a JavaScript undefined reaching a string context and was SELECTABLE in any picker fed by that table, a published company linking to example.com, and a phone of -. Rehearsed on local, applied to local, dev and prod, and probed on production against a deliberately invalid control. W58-17's probe was inflated by the failures: it required 30 019 companies with a non-empty place_id, a floor only ever met by counting 8 530 failures as successes, and reported the file MISSING the moment the data was repaired. W23's assertion 8b had the same shape and now compares the route against the catalogue rather than a literal. D-907. *Evidence: 1 migration.*
W282People and access, as a screencompleteThe owner, 2026-09-04: an admin UI to create and invite users, and to add, edit and revoke access. Until today the only way to do any of it was oktober23-access.sh, run from a laptop against production as the database owner. /settings/people, gated on users.view to read and users.invite to write -- permissions this system already declared and Tenant owner and Admin already hold. MEASURED FIRST as app_rw with a tenant set, which is what the Worker is: five of the six operations an admin screen needs are already governed correctly by RLS and needed nothing. Only insert app_user is refused, and correctly -- user_visible is a for all policy with no with check, so using governs writes and a person with no membership yet satisfies none of its disjuncts, which is the identity chicken-and-egg. So the migration adds ONE narrow door and not a wider policy, which would have opened the same hole for every code path in the Worker for ever. security definer alone would not have been enough -- all four tables force RLS, which binds the owner too -- so the file asserts the bypass rather than assuming it, and both grant assertions were watched go red. TWO THINGS THE SCREEN SAYS OUT LOUD: a seat is not an account, so the list carries has_signed_in because *invited and never arrived* is a different state from *broken*; and NO EVENTS TICKED MEANS EVERY EVENT, since membership_event_scope is an allow-list and an empty selection is the WIDEST setting rather than the narrowest. Revoking is suspended and never removed. The array trap fired again and was caught by measuring: scoped_events as array_agg crossed the wire as the STRING {uuid}, whose length is 38, so the screen rendered "38 events" for a seat scoped to one and nothing looked wrong. 29 assertions with two controls -- a Viewer who may look and not change, a Contributor who may not look -- and the suite restores the owner's own seat unconditionally, because sabotaging the self-edit guard succeeds in suspending the demo owner on a shared database. D-908. *Evidence: 1 migration, acceptance suite.*
W283An agenda arrives as a spreadsheetcompleteThe owner, 2026-09-04, with a real spreadsheet. An existing agenda is uploaded, parsed IN THE BROWSER, and written only after a person has confirmed what every column means. Reads .xlsx (every tab, merged ranges expanded from the file’s own declaration), .csv, .tsv, plain text and pasted tables, with no library: DecompressionStream and DOMParser are both native, and a Worker has a CPU budget an .xlsx does not respect. What crosses the wire is rows of strings, which is what makes the mapper independent of the format and leaves PDF and DOCX as a READER to add rather than a redesign. IT PROPOSES AND NEVER DECIDES: on the real file the venue column has NO HEADING and a column headed *questions* holds a category, so every column arrives with its guess, its confidence and three real values. Times are deliberately not parsed into instants -- 10.23. 09.20-13.00 has no year and no time zone, the same programme is dated 10.22 on one sheet and 09.22 on another, and one cell reads 09.23 10..0-13.00Délelőtt. Measured against 1.0 PROGRAMTÁBLA.xlsx: 9 sheets, 1 493 cells, five of them headings with no rows; the Google Sheets export of the same document is NOT the same file (8 sheets, 1 499 cells). Driven end to end in a browser: 115 agenda items and 20 groups from the 139-row station sheet, with 30 venue names reported as unmatched rather than invented. The defect it found in itself is the one worth keeping: fill-down ran from row zero, so the HEADING seeded it and 115 rows were imported carrying Imported time: Pontos időpont -- and the first assertion written for that bug PASSED WITH THE BUG RESTORED, because the fixture’s first data row carried its own value and could never inherit anything. 18 HTTP assertions, 208 web unit tests, two sabotages reddening disjoint sets. D-909. *Evidence: 1 migration, acceptance suite.*
W288Who reaches this event, and who may see this linecompleteThe owner, 2026-09-04: the seat screen at the event level, and a small control on every line. One pair of routes serves both. GET/POST /record-access lists the workspace's seats for ONE record and writes a per-person deny on that record's .view code; GET/POST /events/access reports per seat whether the role permits events at all, whether the seat is event-scoped, whether THIS event is among its scopes, and whether it is blocked here. Four facts, because three of the four possible answers look identical in a tick-box -- and because membership_event_scope is an ALLOW-LIST, removing somebody's last scope WIDENS them to every event, which the route refuses and names. app.record_acl is deny-only by constraint, so the control is worded Block and a role that lacks the code is shown out of reach with no control at all. No blanket deny is offered: gate 4 exempts nobody, so one click on an everyone-row could lock out the person who clicked. LOCKABLE in authz/gates.ts carries the view code, the edit code and the table for twenty type spellings and is deliberately Partial -- an unlisted type is refused by name. may_edit asks the RECORD, not the role: mayDo() stops at gate 3, and every other assertion passed while it was wrong. The route accepted any uuid and wrote deny rows against records that do not exist, found by probing rather than by a test. AND SEVEN OF THE EIGHT LOCKS RESTRICTED NOBODY while 48 assertions were green: gate 4 matches resource_type as an exact string and this vocabulary has several spellings per table (workforce 13 call sites, event_participant 0), while agenda_item, document, budget_line and budget reached no gate at all. acl canonicalises the spelling and six routes owned by other slices now pass their record to gate 4. The lock is wired onto agenda lines, assets, performers, workforce, tasks, documents, budget lines and inventory. 50 assertions, 7 sabotages, 7 correct reds, plus a cross-route section that denies through the lock and then asks a route this slice did not write. *Evidence: acceptance suite.*
W289The sheet stays in stepcompleteThe owner, 2026-09-05: a scheduled run against the Google Sheet that finds updates, additions and deletions. Then, decisively: “dont add an ID, its not our file.” That one sentence shapes everything: the sheet is an uploaded .xlsx owned by an address outside the workspace, so a row has no stable identity and a rename cannot be told from a delete-plus-add. The engine matches on a SIGNATURE and renders a rename honestly as one held removal beside one addition. Additions and edits apply themselves; a removal is NEVER applied — a revoked share link and an emptied sheet arrive as the same bytes, so the run files a question and only a signed-in person answering it may delete a line. The fetch needs no Google credentials, measured: both URL forms return the real 167 679 bytes unauthenticated. A scheduled run has no browser, and W283 parses in one on purpose, so the DOM-free half was reproduced for the Worker and cross-checked against an independent reader over all 5 526 cells of the real 12-sheet file — identical, after two bugs that threw nothing: a paired-first regex swallowed self-closing cells (1034 cells wrong, the header read 24, 28, 32), and unnormalised CRLF would have reported an edit on every multi-line cell of the first run after an upload. The identity columns are a property of the file: (station, name, when) left 89 distinct rows of 139 and deleting one produced two phantom edits three rows later; adding the task column took it to 137, and the run now reports how many rows it cannot tell apart. Two more caught by the suite: a NUL separator failed the first live run outright and join("") would have made ("ab","c") and ("a","bc") the same line; and an edited row was re-stamped from the stored item, whose fields are narrower than identity, turning every edit into a permanent phantom pair. Proven against the owner's real sheet over HTTP: 115 lines in, then silent. 34 acceptance assertions, 3 sabotages, 38 unit tests. D-914. *Evidence: 1 migration, acceptance suite.*
W290The unit picker that said “null”completeThe owner, 2026-09-05: the unit dropdown shows no Hungarian and looks empty — “Kérem, önállóan hozd helyre ezt a problémát.” Two defects behind one symptom. /assets/vocabulary held a DRIFTED COPY of the unit query — the canonical one coalesces a missing label and this one did not, so 59 of 72 units rendered the literal string “null”. It now calls the shared listUnitReference, and the copy is deleted rather than repaired. And the labels are DB-sourced, so web/test/i18n.test.ts could not see them: that test scans SOURCE literals, and a string assembled at runtime is invisible to it — which is why a 100% green i18n gate sat beside a dropdown with no Hungarian in it. The 72 units are checked in as web/src/system/unit-vocabulary.ts so the dictionary has something to be held against, and unitLabel/unitName route every unit through t() with a fallback so a null can never again reach the screen as a word. *Evidence: acceptance suite.*
W291“Méretek” is plural, and now there are threecompleteThe owner, 2026-09-05, on two screens. The asset drawer said Méretek and offered ONE field: “ellenőrizz, hogy adatbázis szinten milyen részletezettségű ez az adat és hozzáigazítsd a user interface részletezettségét is.” Measured: app.asset has carried width_mm, height_mm and depth_mm all along. The UI was the thin part, not the schema, so nothing was migrated. DimensionFields offers the three sides with ONE unit picker for the form — m, cm, mm, km, as asked — because a unit per field would let a width in metres sit beside a depth in millimetres and derive an area wrong by a thousand while looking entirely reasonable. Millimetres stay the stored unit and the picker is a lens; area and volume are shown as text and never written, and a stored footprint_sqm that contradicts the sides is REPORTED rather than overwritten — the column predates the sides and may include clearance a bounding box does not. /assets/update uses case when 'width_mm' in body so a cleared field clears; everything else on that route coalesces, which cannot. Three of my own test expectations were wrong and are corrected in place with their reasons. The second screen — the owner: “itt 2 paraméter van és csak mm-ben lehet megadni” — had depth_mm on its row type and on BOTH its routes already, so a person could read a depth that no control on that screen could set; it now uses the same component, keyed on the placement because the editor around it is never remounted. 13 acceptance assertions, 15 unit tests. *Evidence: acceptance suite.*
W292The tab nobody imported, and the phone that was a numbercompleteThe owner, 2026-09-05: “Van rajta egy Kontaktok fül, azonban a kontaktszemélyek nem kerültek importálásra,” plus the general form — a way to bring in what an earlier import left behind. It needed no new machinery: app.agenda_import.payload holds the whole parsed workbook from the moment of upload, so a sheet nobody chose was never consumed; POST /agenda/import/people takes the SAME import id and a different sheet index. They land as workforce on app.event_participant because app.person is CHECK-constrained to platform scope and app.company admits a tenant only through a venue claim. Their email and phone go in COLUMNS: the older entity route packed both into prose under a comment claiming the table has neither, and guest_email (citext) and guest_phone have been there since W25 — the earlier search looked for email and phone and the guest_ prefix hid them. That route is corrected with this one, and w283's assertion 6e, which had been measuring the old behaviour, now checks the column with a second assertion guarding the prose. PeopleImportPanel is why the route counts as shipped — it was deployed, correct, and reachable from nothing, which this project has done three times before (W19, W21, W26). It opens any earlier upload, committed ones included because that is exactly when somebody notices a tab was missed; it guesses the tab and the heading row in both languages, shows the rows it would create from the SAME mapping the commit sends, and writes only on the last button. AND A PHONE ARRIVED AS 3.075719869E9: a number typed with no spaces is stored by the spreadsheet as a NUMBER, and both xlsx readers passed <v> through verbatim. Fixed in the one cell function each reader routes through — blank test FIRST, because Number("") is 0 and not NaN — and the two copies are pinned by the same expectations on both sides, because two copies of a unit query drifted exactly this way in W290. All 722 text columns on production were swept: that one value was the only one affected, and it is repaired to what the corrected reader returns. Then the owner: “importáld be a kontaktokat az október 23 rendezvénybe.” Twenty-one contacts written to the live event by a script mirroring the route exactly, run as the database owner because the production API needs a Clerk session that process does not have. The dry run came first (21 to create, 0 already there, 0 nameless); the first commit failed outright on type "citext" does not exist and rolled back, leaving production at its starting 34 — the cast was unresolvable AND unnecessary, since the extension lives in app, off the owner connection's search_path, and a citext column coerces text on insert. Verified after: 55 participants, 21 from this sheet, 18 with an email and 9 with a phone in columns, tenant_id correct on all 21, no duplicate name introduced, and the 127 agenda items untouched. 27 acceptance assertions, plus 4 reader unit tests on each side. *Evidence: acceptance suite.*
W293A guest invitation becomes a ticketcompleteThe owner, 2026-09-05: “sürgőssé vált egy funkció kifejlesztése, tömeges meghívó kiküldés QR kóddal” — and, asked which half must be live first, the bulk send. The ticket rides on app.guest_invite, NOT on accred.pass, which already had a QR column: measured, accred.pass.person_id is NOT NULL against app.person, which is CHECK-constrained to platform scope, so five hundred party guests would be five hundred permanent rows in the shared cross-tenant directory — refused twice before, at W283 and W292. The code is STORED rather than hashed, the opposite of accred’s choice, because an invitation gets RESENT and every resend must carry the same QR; a hash forces a re-issue and kills the guest who saved the first mail. Accreditation makes the other choice deliberately, which is why accred.reissue is a table. 25 characters of Crockford base32 — no I, L, O or U, the confusables folded on the way in — because the owner asked for typing as one of the three ways in; 25 and not 26 purely so it groups into five fives, since 26 stranded a pair on its own line on a phone. The public lane gets NO table grant: a first draft would have granted app_public four columns on app.event and thereby made W30’s own written safety note false, so instead it may EXECUTE one security-definer function returning nine columns of one row. The PNG is written here rather than pulled in — SVG does not render in Gmail or Outlook and data: URIs are stripped, so a mail QR must be a PNG at a URL; sixty lines over CompressionStream, decoded back and compared module by module against what uqr encoded. The wording is content, not code: every message module in src/notify is English only and this API has no dictionary, so no template means the send REFUSES rather than mailing Hungarian guests in English. The send is batched and restartable — 25 at a time, each outcome recorded before the next starts. And = any(${ids}::uuid[]) DOES NOT WORK IN THIS WORKER: it works from Node against the same database, and db.ts sets fetch_types: false because postgres.js otherwise hangs on Workers, so the driver cannot resolve the array type and sends joined text. Two earlier notes in this file said “use in ${tx(ids)}” without the reason, so a third route made it a third time. 36 acceptance assertions, plus 27 unit tests over the code and the PNG. *Evidence: 1 migration, acceptance suite.*
W294The guest wallet, assembled from modulescompleteThe owner, 2026-09-05, with a mockup of a phone: a guest’s name and event, a QR, a cloakroom number, a diet badge, cocktail coupons with one already used, raffle tickets with their serials — and, named beside them, online voting “és egyéb interakciós és gamification funkciók”. The cloakroom tag, the cocktail coupon and the raffle ticket are ONE table, and that is a finding rather than a shortcut: written out they are the same sentence — a numbered thing given to a guest, handed back / spent / drawn once. Three tables would be three copies of issue-and-spend, and two copies of one unit query already drifted far enough in W290 to print the word “null” fifty-nine times on a live screen. What differs is the wording and the treatment, and both are configuration. So a new module is a ROW, not a migration, and the suite proves it by adding a shuttle-seat type nothing in the Worker has heard of and asserting it renders in its configured position. Voting is deliberately NOT in that table: an item is issued TO a guest and spent, while a vote is a choice the guest MAKES, its options belong to the question, and its constraint is one-per-guest-per-poll — forcing it in would mean a serial that is really an option id. wallet_by_token REPLACES ticket_by_token rather than joining it, because two public functions over one row — one narrow, one wide — is the W290 drift waiting to happen, and on this lane a drift is a disclosure rather than a rendering bug; app_public still holds no grant on any table and may execute exactly two functions, both asserted. The vote is the only write and the only POST: the read side must stay read-only because mail clients PREFETCH, so a GET that recorded anything would record it for every guest the moment the invitation was delivered. A backtick inside a CSS comment ended the template literal it sat in — the third time in this project, twice in SQL and now once in HTML. Verified on production end to end with a probe invite created and removed in the same run. 24 acceptance assertions. *Evidence: 1 migration, acceptance suite.*
W295Codes that are not ourscompleteThe owner, 2026-09-05: “lehetővé kell tenni automatikus QR és kód generálást, kód sorozat importálását (pld. külső jegyértékesítési vagy akkreditációs rendszerből), és élő API kapcsolatot ... kódokhoz, webhook-kal a jegyek érvényesítéséhez.” This undoes part of W293, which shipped the same morning, and the reason is worth keeping: W293 put the code on app.guest_invite and argued for it — the ticket belonged to an invitation. That argument holds for codes WE mint and collapses the moment one arrives from somewhere else. Five thousand codes from a ticketing system have no invitee, no RSVP and no plus-ones; a code checked LIVE has no local row at all until somebody scans it. The alternative — a second table for external codes — is the one that costs: two code spaces, and a door that has to ask both and be right when a code is in both, which is the shape of every drift this project has recorded. Measured before rewriting: ZERO tickets on production, four on local, all carried across; a week later this would have been a data migration with real guests holding printed codes. And external codes do not look like ours. The reader enforced 25 characters of Crockford and would have refused TIX-2024-00123 outright, so a ticket now stores the code AS THE SOURCE WROTE IT beside a separate match key — and the Crockford fold is applied ONLY to codes we minted, because our alphabet excludes I, L and O and a partner’s may not: folding theirs merges two distinct codes into one, which at a door means admitting the wrong person. The rule lives in Postgres and in the Worker and one suite section holds them together case for case; a scan that resolves to two different tickets is REFUSED rather than guessed. Two defects the tests found: an accreditation code containing slashes was cut down to its last path segment by the URL reader, and the path segment was never percent-decoded, so codes with slashes or spaces 404’d on tickets that existed. A third was found by an end-to-end probe on production rather than by a test: an imported code was being regrouped into fives, destroying the only structure it had. Credentials are NOT in the database — a live source names a Worker secret, and the route refuses to save one whose secret is not actually set, refuses http and refuses a private address, so a dump of ticket_source is worth nothing. The validation webhook is two rows in the catalogue that already exists, with its signing, retries and delivery log; nothing fires them yet, which is honest, because validation happens at a door and the scanner is the next slice. Split into w295 (adds and carries across) and w295-2 (drops the old columns) so the Worker serving production kept working between them. W229’s “the catalogue still declares nine event types” now derives its count from three NAMED lists, so a new code must be classified rather than incrementing a number. 33 acceptance assertions. *Evidence: 2 migrations, acceptance suite.*
W296The check-in ledger: the door, the zone, the room and the sessioncompleteThe owner, 2026-09-05, mid-build: had the access-zone check-ins, the session check-ins and the kiosk/terminal structure from the Bubble import been applied? They had not, and the gate was about to be built without them. 02-bubble-source/bubble-data-types/fields-accreditation-m10, E-057 to E-063, owner-confirmed, read before writing another line. check-in-out (E-059) is ONE record spanning event entry, ZONE entry, SPACE entry and SESSION check-in, with in and out times, both stations and a duration — and its own Proposed target says to split the in-and-out row into two directional events, which is done: a person enters and leaves a zone repeatedly, a crash between two halves of one row leaves it half-written, and the occupancy counters are sums over directions. check-in-out-station (E-061) is a kiosk or handheld with DELEGATED RIGHTS — which zones it admits into and out of, kept separate because an exit turnstile must never admit anyone — plus operating hours, personnel, and a device pointing at the physical Inventory Item; accred.access_point had one zone and a free-text device_id, which is what made it useless for any of that. Event_access_zone (E-060) is a rules OVERLAY, not a label: time windows, participation, role, ticket source and capacity over one or more spaces, with overlapping zones legitimate. Built in app and not in accred: measured, accred holds 24 tables and ZERO rows on every database, and accred.scan_event.pass_id is NOT NULL against accred.pass, whose person_id is NOT NULL against the platform-scoped app.person — the constraint W295 already refused for a ticket. accred.scan_event must not gain a second writer; when accreditation is built, accred.pass becomes a ticket source. Two new permissions, because operating a door is not reading the guest list — reusing guests.view would have let every marshal read every invited guest's contact details, and Bubble agrees: workforce carries a check-in kiosk admin flag. The ledger is append-only at the GRANT (app_rw holds INSERT and SELECT only): a scan is what happened, and correcting one means recording a correction. And a claim made earlier the same day was corrected: writing crowd.checkin does NOT make the occupancy screens work — they read crowd.count_event, a different table with a different writer — so the door writes both. Three defects the suite found: capacity was checked BEFORE direction, so somebody already inside who scanned again was refused as capacity with their own party counted twice against the limit; a replay returned no holder name, which is the entire reason the name is returned; and an explicit NULL occurred_at overrode the column default rather than falling back to it. A backtick in a SQL comment inside a template literal, for the fourth time in this project. 27 acceptance assertions, 3 sabotages, 3 correct reds. *Evidence: 1 migration, acceptance suite.*
W298The technical fields follow the product typecompleteThe owner, 2026-09-05, with a screenshot of the new-asset form: choosing a plant still asked for the print technique, “hiszen az nem releváns.” The cause was two parallel field systems, only one of them right. W263's asset_spec_field already declared exactly the right questions per type — print_process and lamination for a printed item, species and watering_interval_days for a plant — and beside it app.asset carried print_technique and mounting as plain columns, hard-coded into the form and rendered for EVERY type. So the assertion that matters is a negative one: what a plant is NOT asked. A test that only checked a plant gets its own five fields would have passed with the print block still sitting underneath them. Two more defects the suite found on the way: a code declared both commonly and on the product was offered TWICE, because the create form ran its own query with no de-duplication beside the detail view's single one — two answers to one question is the W290 drift — and a first draft duplicated the /assets/spec-fields route that already existed rather than upgrading it, which was deleted. Dropping the two columns broke a trigger nothing in this slice had touched: asset_edit_reopens_review referenced old.print_technique, so the first apply failed with record “old” has no field “print_technique”. The battery caught it, not a person, and the trigger now watches spec. Measured before dropping: zero rows on dev and zero on production carried a value in either column, so the drop destroys nothing. 14 acceptance assertions. *Evidence: 1 migration, acceptance suite.*
W300An event may accompany another eventcompleteThe owner, 2026-09-05: “Lehet azt, hogy a rendezvénynek vannak kísérő rendezvényei, vagy pedig egy projekt alatt ... több különböző rendezvény van, mindegyik egy-egy vagy akár több rendezvényhelyszínnel?” The answer was already yes and half-wired. app.event.parent_event_id has had a foreign key since W19 and the detail route has RETURNED the parent's name since W19 — and no route accepted it and no screen offered it. The FOURTH complete path in this project reachable by no control (W19's project_id, W21's venue_space_id, W26's assign route, this). And the measurement that reframed the whole question: the October 23 event's 22 “event spaces” are not rooms, they are 22 separate venues — Országház, Müpa, az Operaház, Hősök tere, három kávéház, a 301-es parcella, “Iskolák országszerte”. The import filed each as a space because that was the only field it had. event.venue_id holds ONE venue deliberately, so an event in twenty-two places is not one event with twenty-two venues; it is a project with one event per venue, which is what the answer recommends. event_venue_share is measured and ruled out by name: it carries grantee_tenant_id, state and four shares_* booleans, so it is a consent contract between two tenants, not a multi-venue link, and it holds 0 rows on production. The write path is the project's own shape, not a new one: the same three steps /projects/update applies to parent_project_id — self-link, existence under RLS, then a recursive ancestor walk with a cycle guard so a loop already in the data cannot make the loop-detector hang. And the missing other direction: parent_event_name said what an event accompanies and nothing said what accompanies IT, so an event with six satellites and one with none rendered identically. The detail route now returns them. The migration adds event_not_own_parent, which app.project has carried all along and app.event never had — a rule that lives only in the Worker holds only while the Worker is the only writer, and psql and the importers also write this table. Its own self-assertion had a real defect caught before it shipped: the negative-control UPDATE was inside a plpgsql begin ... exception block, which rolls back ONLY when an exception leaves it — so had the constraint failed to fire, the forbidden write would have COMMITTED and the assertion would have corrupted a production row while reporting the failure. Both branches now throw. 16 acceptance assertions, forced red three ways before being believed: with the write removed (6 red, and 1a still passed because the route returns 200 while storing nothing, which is why 1b asks the database), with the cycle guard disabled (3 red, including the one that proves the loop was actually written), and with the satellite list emptied (1 red). *Evidence: 1 migration, acceptance suite, 1 design doc.*
W301The splitter: one event becomes several, and nothing is left pointing two wayscompleteThe owner, 2026-09-06: “Build the splitter.” W300 measured that the October 23 event's 22 “event spaces” are 22 separate venues; this is the tool that turns that one record into a project with one event per venue. IT IS NOT A LOOP OVER /events/new, AND THE MEASUREMENT SAYS WHY: nine tables carry a foreign key to app.event_space and seven of them also carry their own event_id — agenda, asset, budget_line, checkin_event, checkin_station, performer, and event_space itself. Move a space and stop, and rows exist whose event_id names one event and whose event_space_id points into another. Three things the build found that no amount of design could have: (1) asset_tenant_rw's WITH CHECK already requires es.event_id = asset.event_id, so for asset, performer and agenda the split-brain row is not a silent corruption but a REFUSAL — good news that inverts the write order, since the space must move first; the first draft had it backwards and got a 503 that said nothing. (2) event_space_parent_same_event is a BEFORE ROW, non-deferrable trigger, so where id in (parent, child) refuses depending on which row Postgres reaches first — the spaces move one at a time, parent-first, out of the planner's breadth-first walk. (3) app_rw holds INSERT and SELECT on checkin_event and nothing else, because W296 revoked update and delete on purpose: “THE LEDGER IS APPEND-ONLY AT THE GRANT, not by good intentions.” A door scan cannot be re-filed under an event that did not exist when it happened, so a split that would move a scanned space is refused with the count rather than half-done. THE PLANNER IS PURE AND SEPARATE, because every way a split can be wrong — a session claimed twice, a space from another event, a nested space whose parent would stay behind, a group that would create an empty event — is decidable from data alone; 16 unit assertions run it with no database, no Worker and no fixtures. AND IT NEVER GUESSES WHICH SESSION GOES WHERE. The names carry the venue sometimes (“Bem téri érkezés”, “301-es parcella”) and not otherwise (“Fáklyás menet”, “Ünnepi díszülés”), so the server proposes by exact name match, the screen shows the proposal, and a person confirms. What is LEFT BEHIND is reported at least as loudly as what moves — on October 23 that will be most of the programme, and discovering it afterwards is the failure the screen exists to prevent. The dry run is the same code path as the run, stopping before the writes, so a clean preview means a clean split. 27 acceptance assertions and 16 unit assertions, each forced red: with the dependents left behind (the naive splitter, 2 red), with every space swept along (3 red), with the descendant walk removed (2 red) and with the parent and duplicate guards disabled (5 red). One assertion was itself wrong and was corrected rather than accommodated: it compared starts_at::text and read a correct 19:00Z as a failure because Postgres renders a timestamptz in the session's zone. *Evidence: acceptance suite.*
Phase 10 - The owner's own working day, and the project above the event
W302The venue fact was a dead end, the roster hid the scope of work, and a blank page had no words on iton productionThree things, two reported by the owner on 2026-09-06. The venue on the event page WAS shown -- as the words “Not set”, which is true and useless. The control that sets it has existed since W299 and lives inside the edit drawer's “Venue and place” fold, so a person reading “Not set” had no way to learn where to go; the fact now carries the action, and a place gets its own line, because a map reference is a real answer to “where is this” and was reading as no answer at all. The roster hid the scope of work: Department IS a column, scope_of_work is not, and the row has carried it all along -- on a roster that is mostly placeholders it is the ONLY thing telling one row from another. Rendered as a sub-line rather than a sixth column, because the table is already at five and a sixth pushes Shifts off a laptop screen. And a page that renders nothing now says so. Measured on production with matched conditions -- same bundle, same browser, same 18-second wait -- app.event.clinic had Clerk.loaded true and rendered the sign-in card while oktober23.event.clinic had it FALSE and rendered 0 characters: both <SignedIn> and <SignedOut> are false while the auth provider loads and stay false forever when it never loads, so the whole application renders an empty document with no console error and a 200 from the edge. That is W297's failure arriving again from one layer up. This does not fix the cause and does not pretend to -- a production Clerk instance only runs on the domains registered with it, which is a change in the owner's dashboard and is W304 -- it fixes the symptom being invisible: after twelve seconds the page says it could not start, says it is not the reader's fault, and names the address to quote. Twelve and not two, because Clerk on a cold cache can legitimately take several seconds. *Evidence: 1 design doc.*
W304The accented hostname redirects, because Clerk will not take iton productionThe owner, 2026-09-06: “Nem lehet ékezetes subdomain-t hozzáadni, nem fogadja el a clerk.” So oktober23.event.clinic is registered in Clerk and now works -- verified on production, Clerk.loaded true, the microsite landing rendering in Hungarian and naming the event -- and its accented twin never will. Measured, not assumed: on the punycode host the bundle and the stylesheet load perfectly and every asset is byte-for-byte correct, so nothing about the serving is broken and one service simply does not know the hostname. Here, and not in a Worker: the web deployment is assets-only, wrangler.jsonc declaring assets and no main, and adding a Worker script for one redirect would change the shape of the asset serving the whole application stands on, including not_found_handling: single-page-application. A Cloudflare Redirect Rule is also out -- the token this project holds covers DNS and Worker domains, not Redirect Rules. And before first paint, not in React: it sits in index.html's existing synchronous script above the theme block, so there is no flash and the bundle never starts on a hostname it cannot finish on. Path, query and hash travel with it via replace(), so Back does not bounce into the dead host, and the try/catch means a broken redirect can never take the page down with it. No loop is possible: after the jump the hostname no longer matches. *Evidence: 1 design doc.*
W305The splitter could not move a single one of the 66 sessionson productionFound by opening the page and using it, not by reading the diff. Every session move would have failed. app.agenda_item's WITH CHECK demands that a segment be on THIS agenda and a space be on THIS agenda's event, and both break the instant an item changes event -- a segment belongs to the agenda it was written in and cannot travel, a space belongs to the event it sits on -- so the item arrives illegal, Postgres refuses the statement, and it reached the caller as a bare 503 that says nothing at all. Measured on production: ALL 66 of the sessions left on the October 23 event sit in a segment. Not some. All of them. W301's second pass, shipped that morning precisely so those 66 could be placed, would have failed on every single one -- and it had passed on production earlier only because that run went in as the database owner, which bypasses row security. The move now clears the segment, and clears the space unless the same group is moving the space too; both are counted and reported, because silently dropping somebody's running order block is found weeks later. Why the suite stayed green: its fixture items carried neither a segment nor a space. A seeded fixture that skips what every real row carries tests the easy half; the fixture now builds a segment, puts every item in it, and stands the first item in a space that deliberately does NOT travel, and restoring the old behaviour reds it at 2a with the owner's own 503. The venue fact, again: W302 made it actionable on the “Adatok” tab and the DASHBOARD is the default tab, which carried an identical untouched “Nincs megadva” -- fixing one of two identical renderings is the shape of a fix that reads as done and is not. And /events was being fetched on every event page: 450 ms and 10.7 KB for sixteen events, twenty-one fields per row of which a dropdown uses five, for a control inside a drawer most visits never open. Full battery 153 passed, 4 323 assertions; the one red is w63, pre-existing, unrelated and LOCAL ONLY. *Evidence: 1 design doc.*
W306Adding the Google Maps key to productionnot builtNot a slice: a runbook for the one job an agent must not do. Four surfaces in the app need three Google APIs on one key -- Maps Embed for VenueMap, Places (New) for address autocomplete and asset place details, Maps JavaScript for PlacementMap -- and the key is a change to the owner's own Google Cloud account that can be billed. Nothing breaks without it, which is the reason it sat unnoticed: every surface says in its own words that it has no key, so the product degrades quietly rather than failing. This row carries a design document and no migration and no suite, because there is nothing here for either to measure. *Evidence: 1 design doc.*
W307The new-venue form promised Google had filled it in and left the city and the country emptyon productionThe owner, 2026-09-06, creating “Hadik Kávéház” from a Google suggestion while working in production: the name and the address arrived, VÁROS and ORSZÁG did not -- under a line saying the form had been checked against what Google suggested. Both were initialised to "" and nothing ever wrote them. The city is not parsed out of the address text, and that is the whole decision. Google's secondary line has no fixed component order: the owner's own example reads “Budapest, Bartók Béla Way, Hungary” -- city FIRST -- where the usual shape is street, city, country, so taking the first component would be right there and wrong on the common one and taking the second-to- last would be the reverse. A wrong city on a venue record is worse than an empty one somebody fills in, and it would be wrong silently. The authoritative source is used instead: the place's own addressComponents, which name the locality and carry the country already as the two-letter code the field wants. And without a Maps key it still does what it can -- production has no VITE_GOOGLE_MAPS_KEY (W306), so the details call cannot run, but the last component of the address is reliably the country name and /directory/v1/countries turns it into a code with no key and no auth. The city is then left to the person WITH A LINE SAYING WHY, instead of being left blank under a promise that it was filled. Measured before building: that endpoint answers Hungary to HU, Austria to AT, Germany to DE, and does NOT answer “Magyarország”, so the match is exact-name only and leaves the field empty rather than guessing when Google is asked in another language. *Evidence: 1 design doc.*
W308Sortable column headers on the events list, defaulting to last modifiedon productionThe owner, 2026-09-06: chevrons in the table header, defaulting to most recently modified. The sort goes to the server, and the 100-row cap is why. /events has always been order by coalesce(starts_at, created_at) desc limit 100, so sorting that page in the browser would answer “which was edited last” with the most recent of an arbitrary hundred the server had already picked by start date -- the genuinely newest edit can sit outside the page entirely. The API orders before it truncates, so the answer is true. An allowlist, not the caller's string: an ORDER BY is not a value and cannot be parameterised, so a caller-supplied fragment would be injected SQL by construction; four names map to four fixed clauses and ?sort=DROP%20TABLE returns the normal 17 rows in the default order. Every order ends with a tiebreak on name, so two events touched in the same second do not swap places between two identical requests -- a list whose rows move on their own reads as broken data. The table component learned to sort opt-in: a column becomes clickable only if it declares a sortKey, and the table does NOT reorder rows itself, because the screens that need this sort on the server. The chevron pair is faint on sortable columns and lit on the one in force, because a single arrow only says where you are while the pair says the others are available. Descending only, deliberately -- and W314 is where that reasoning turned out to have been generalised past the one column it was true of. *Evidence: 1 design doc.*
W309The document header nothing could editon productionFound by sweeping all 256 POST routes against every file in web/src for fields no web code ever names; five came back, four false positives and one real. /documents/update accepts name, document_type_code, confidential, document_number, description_of_change, issue_date, effective_date and expiration_date -- and NO SCREEN CALLED IT AT ALL. Not one field. Once a document existed it could not be renamed, numbered, dated, or taken out of confidentiality, and the New document drawer renders a DISABLED “not numbered” input, so the number could not be set at creation either. Worse, DocumentsScreen's own header comment said “POST /documents/update -- Document drawer, Save”. There was no such call and had not been -- a false comment is worse than none, which is the W241 lesson arriving in another file. The masked field is handled as the gate intends: description_of_change hides behind documents.approve and gates.ts DELETES a masked key rather than nulling it, because “a null says this field exists and is empty, which is a different and false statement”, so the caller is shown why the field is absent and given no input and the payload omits the key entirely -- an empty box they could save would blank somebody else's negotiating note. Verified end to end through the UI: a number and an issue date typed into the drawer, saved, both read back from the database, reverted. *Evidence: 1 design doc.*
W310The table column that crushed itself into a stack of letterson productionThe owner, 2026-09-06, with a screenshot of the agenda: “a táblázatban függőlegesen egymás alá kerültek a betűk.” “Nincs lekötve senki” rendered one character per line, straight down the cell. Three correct things combined into a wrong one, which is why it survived review: table-layout: fixed so a <col> width is honoured rather than treated as a suggestion; overflow-wrap: anywhere so a long unbroken title wraps instead of overflowing; and a column with no width, which in fixed layout takes what is LEFT. The agenda declares 8.5 + 13 + 12 + 9 + 9.5 rem of fixed columns, so inside a panel narrower than that, what is left is nothing, the unsized column collapses to a sliver, and overflow-wrap faithfully breaks every word into single characters. A fixed table now carries a min-width built as a calc() of its own declared widths plus a readable floor for each unsized column, so nothing has to parse rem, px or ch -- whatever unit a screen wrote, the browser adds it up -- and the wrapper already scrolled horizontally, so the table overflows instead of crushing itself. The fix is in the shared component, so every table with column widths is covered. Measured before and after at a 900px viewport: the “on stage” column went from a sliver to 208px, and zero cells render taller than a line with short text. *Evidence: 1 design doc.*
W311An event can be deleted, and the sixteen tables that do not go with itcompleteThe owner, 2026-09-07: a bin icon, tucked away, double confirmation, and the data hanging off the event cleared with it. 74 tables point at app.event and they do not all behave alike. Measured on production first: 58 CASCADE and 16 SET NULL, and nothing blocks -- so a delete always succeeds, which is exactly why it has to be explained before it runs. The 16 are a designed protection and are not overridden: invoices, purchase orders, budget lines, stock movements, documents, checklists, this event's satellites, and nine estate.* tables belonging to the venue portal. Money and another product's records must not disappear because somebody tidied up an event, so the danger zone names both halves -- showing only what goes would be a true sentence that misleads. Two barriers, not a dialog: window.confirm is suppressed in this harness and unverifiable, so the confirmation is data and the typed name must match the event's own exactly. events.delete had been in the permission catalogue since the permissions were written with NO route requiring it; this is the first. The deletion is audited BEFORE it runs and the audit outlives it, because app.audit_event has no foreign key to app.event. 16 acceptance assertions, sabotaged twice: dropping the name barrier reds five, and deleting the satellites along with the parent reds the two that exist to catch exactly that. Five web gates green -- typecheck 0, 262 tests, i18n-wrap 0, design-parity 0, build 0. *Evidence: acceptance suite.*
W312A venue created from a map suggestion now actually reaches the eventon productionThe bug the owner hit while working, 2026-09-07, having created “Budapesti Műszaki Egyetem” from a Google suggestion and saved the event with it: the event page still offered “Helyszín kiválasztása”, as if no venue existed. It existed. /venues/new succeeded and the row was written; the only thing done with the returned id was setVenueId, SaveAsVenue's OWN state, which exists to enable the offer-it-to-the- directory button. Nothing ever told the event form, so the event saved with venue_id null and the screen was reporting the truth. A create path that writes a row and hands it to nobody is the same defect as a column with no control, one step later. The id now travels SaveAsVenue -> PlacePicker -> the form, and the place is cleared when it lands, because a venue and a place are two answers to one question. And the same form was dropping the NAME: VenuePicker hands its onChange a label and the form ignored it, relying on a venueLabel prop -- the name the screen knew when it opened -- so picking any venue showed the old name until a full refetch. The country is shown by name and stored as a code: Intl carries every country in every language, so no table, no fetch and no translation file to go stale, and a typed name is resolved back before it reaches a char(2) column, with nothing fuzzy -- “Magyar” resolves to nothing rather than to Hungary. What Google answered is no longer editable, because a corrected city on a record whose place_id still points at the original is a row that disagrees with itself. A test example was corrected rather than accommodated: ZZ is a REAL CLDR code meaning Unknown Region, so it resolves; QQ and XX are the genuinely unassigned ones. *Evidence: 1 design doc.*
W313The project screen gets an add control and sortable columnson productionFrom the owner's list of 2026-09-07. The project's event table sorts in the BROWSER, unlike /events which sorts on the server, and that is the same rule applied to a different fact: /events is capped at 100 rows, this query has no cap, so the rows in hand are the whole truth. A new AddButton: a plus icon whose label is always in the DOM and is revealed by a max-width that opens on hover AND on focus, so a screen reader reads it and a keyboard reaches it -- linking to /events?new=1&project_id=..., which opens the create drawer with the project already chosen. The event name no longer appears twice on the event screen; the hero copy goes, and the page title and context bar both survive a tab switch, which was the owner's own test for which one to keep. The four destination buttons are compact with leading icons and keep their words -- an icon for “split an event into several” is a puzzle. W322 is the correction this slice earned: an icon in front of a full label is WIDER than the label alone, so it saved no space at all. *Evidence: 1 design doc.*
W314A sort finally has a directionon productionA defect this slice's own browser test found, and it was in the shared table component rather than in either screen. aria-sort was hardcoded to “descending” for whichever column was active, and the second click on a header did nothing -- while the server had been written honestly, order by e.name and order by e.status being ASCENDING, so two of the four columns sorted one way while the markup asserted the other. A sighted reader could see the rows and catch it; a reader using a screen reader had only the assertion. The comment defending “descending only” was reasoning about ONE column, updated, where it was right, and had been generalised to the other three without looking at them. Fixed at the root so both tables and the API move together: Column gains sortDefault, the table hands back the direction it wants next, and the server allowlists dir exactly as it allowlists sort -- eight ORDER BY clauses written out, because a direction is a keyword and postgres.js cannot parameterise it either. Also fixed in passing: AddButton's reveal opened to 18ch while the first real label needed 162px, so hovering chopped the last four characters. 269 web tests (7 new), and the new comparator was forced RED three ways -- direction ignored fails 3, the undated exception pulled inside the flip fails 1, the tiebreak flipped fails 4. On the live API dir=BANANA, dir= and a drop-table payload in both sort and dir all fall back to the key's default and return 200 with all 16 rows still there. *Evidence: 1 design doc.*
W315One venue search box, and the tenant’s own directory is asked firston productionThe owner, 2026-09-07: “ha rákeresünk egy helyszínre, és az már szerepel a saját adatbázisunkban, akkor azt kellene felajánlani”. And the reason it matters is not tidiness: TWO BOXES MADE DUPLICATION THE DEFAULT. The Google field was the one that looked like a search, so a building already in the directory got re-created from a map pin and then existed twice -- once with rooms an event's spaces could be linked to, once as a bare coordinate -- and nothing in the form said the first one was there. VenuePicker and PlacePicker become one VenueOrPlacePicker. Both sources are still searched, every time: own database first is an ORDER, not a gate, because suppressing Google whenever the directory returned anything would hide the right answer behind a word that merely happened to match. The group heading is not the attribution class, and the first draft got that wrong -- both groups were titled with .places-note muted, the same class the Google attribution footnote uses, so a heading and a footnote rendered identically and picking a Google row for a building already in the directory stayed exactly as easy as before. PlaceHitRows and GoogleAttribution are split out of PlaceSuggestions so the merged list and the standalone one on /venues cannot drift apart, and the venues screen renders byte- identically. Verified in the browser against the local directory: “Rhein” returns 8 directory venues then a rule then 5 Google hits; “Eiffel” is the owner's case exactly. venue_id set and place_id empty were read back in the database each time, not inferred from the UI. *Evidence: 1 design doc.*
W316The “+” pattern surveyed across the app, and applied where it actually fitson productionThe owner, 2026-09-07, after approving the plus button on the project screen: “Ezt általánosságban meg kellene vizsgálni a többi felületen is.” Surveyed: there are about forty create controls across the screens, and converting them all would be applying the SHAPE of the decision without its reason. The owner's reason was avoiding a crowded surface, and that only bites where the words are redundant with what is already on screen. It fits an add that sits beside a HEADING with the list it adds to underneath -- “+” next to “Terek” can only mean one thing. It does not fit a screen's single primary action in a page head, because hiding the main thing behind an icon buries it and the design system asks for one visible primary action per view; nor a form's submit; nor a label carrying information an icon cannot; nor the agenda's card title rows, where converting only the add would leave three buttons in two styles. One surface matched exactly, and with it the empty state underneath, which had told a first-time reader to press a button by a name they cannot see until they hover -- a small lie in the one place they most depend on being told the truth. *Evidence: 1 design doc.*
W317A session’s pickers open inside their own section, and the end cannot precede the starton productionThe owner, 2026-09-07, with a screenshot of a calendar open on 7 September for an event on 23 October. W107 had already built the three-way start/duration/end binding and the continue-after-the-last-session default, and those parts were working; what was missing was everything around them. nextStartAfter returns null when nothing in scope has an end, the field was then left empty, and an empty bounded field falls back to TODAY -- which is the photograph. seedStart keeps that rule's real point, never pre-fill with the current time, and adds the answers that are not guesses: the end of the last session in this section, else the SECTION'S own start, else the EVENT'S. The section bounds the picker, not just the event: two nested containments, the bound is whichever is tighter, and a section with no times of its own narrows nothing -- choosing one section on the demo agenda leaves exactly ONE selectable day and strikes out 29. The end cannot precede its own start, which agenda_item_interval_ordered already refused in the database, so until now the only feedback was a refused save after the drawer had been filled in. Also fixed, and found only because the new hint sat beside it: the event's bounds hint was built from raw English literals and so was invisible to the i18n checker, so a Hungarian reader got “Within Demo Cup Final: ... to no end set” under Hungarian labels. 277 tests (8 new), forced RED three ways. *Evidence: 1 design doc.*
W318Combine two events into one, with everything attached to themon productionThe owner, 2026-09-07: combine two events including agendas, spaces, workforce, performers, guests, budget and assets. This is the splitter run backwards, and it is NOT its mirror image: a split moves a CHOSEN SUBSET and everything unnamed stays put, while a merge has no subset -- the source ceases to exist, so anything left behind is orphaned, and that one difference drives every decision. The table list is discovered, not written down: 42 tables in app carry an event_id and the request named seven, so the route reads information_schema and the DEFAULT IS MOVE, the safe direction to be wrong in, with a table that must NOT move named in BLOCKED with its reason. Three tables cannot move, measured rather than assumed: app_rw holds no UPDATE on checkin_event, stock_movement or membership_event_scope, and the splitter had learned this for one of the three. Seven tables carry a uniqueness rule scoped to the event, so both sides are scanned first and the merge refuses naming the value -- inventing “VIP (2)” would put a record in the customer's data nobody wrote. The move order is read out of pg_policy: eight tables have a WITH CHECK comparing a sibling's event_id and FOUR of those pairs are backwards alphabetically, proven load-bearing because with the order removed the live route returns 503 and the log names the RLS policy. Two real defects the tests found first. Date.parse returns NaN for a Postgres timestamp -- starts_at::text renders a space instead of the ISO T and a two-digit offset ISO 8601 does not allow -- so the window calculation silently took the SOURCE's dates on every call, a merge that NARROWED the target. And MOVE_FIRST was decoration: emptying it broke nothing, because plain a-z already satisfied the only pair the test asserted. Also corrected: the route required events.update, a permission that does not exist, so gate 3 refused every call with “role capability is unset”, which reads like a misconfigured role rather than a typo. Verified end to end on a throwaway fixture through the HTTP route so RLS applied as app_rw: 11 rows moved, 0 orphaned, the satellite re-parented, the window widened, the audit row written, the source gone, all six cross-references still consistent; then repeated through the browser in Hungarian, fixture removed, 0 rows left. Four refusals probed against the live route. 598 API tests (16 new) forced RED five ways; 277 web tests; battery w63 and w295 red, both pre-existing. *Evidence: 1 design doc.*
W319Three policies used a visibility rule as a write ruleon productionPostgres reuses a FOR ALL policy's USING expression as its write check when no WITH CHECK is given. That is convenient and almost always wrong: USING answers “may I SEE this row”, a write needs “may I CREATE this row”. Ten policies in app were in that shape; six say only app.is_platform_admin(), where the two questions have the same answer. Three were REPRODUCED as real holes, as app_rw with row security enforced, inside rolled-back transactions. app_user: an ordinary member could REWRITE A COLLEAGUE'S EMAIL, the USING clause admitting anyone with a membership in the same tenant -- right for reading a name off a list, an account-takeover shape as a write rule. tenant: any active member could rename the workspace, and no route and no screen writes that table at all. participant_dietary_requirement: a member could attach a life_threatening dietary requirement -- health data -- to a participant in another tenant, by stamping their own tenant_id on the row, because a foreign key is verified as the TABLE OWNER and not under row security, so being unable to SEE the target never stopped referencing it. None was reachable through the API, and that is not a reason to leave them: the whole design of this database is that row security is the backstop for a mistake in the Worker. ALTER POLICY, not drop and recreate, because dropping leaves a window with no policy and recreating from memory is how a policy comes back subtly different; the migration asserts both that the three write checks exist AND that the three USING clauses survived. The negative control is eight controls, FOUR OF THEM POSITIVE, because four of the eight assert something did NOT happen and all four pass just as well against policies tightened into uselessness. Watched go red in SEVEN directions, each sabotage inside ONE transaction with the control and a rollback, so a kill never leaves the shared database with a weakened policy. verify-write-checks.sh is a RATCHET, not a pass/fail: a wider sweep found 160 (table, pointer) pairs whose write check never names a foreign key to another tenant-scoped table, 148 remain, and the gate says the numbers may not GROW while saying plainly that it does not prove the 148 are safe. Applied to local, dev and production; the control runs locally only, because Neon's owner cannot SET ROLE app_rw. *Evidence: 1 migration.*
W320Professional tips anchored to the thing they are abouton productionThe owner, 2026-09-07: “add these types of professional ideas into the system, hidden behind tip icons when it comes to specific event elements.” No second knowledge store. app.glossary_term already holds 2 854 terms and glossary_definition 2 901, both with per-locale label tables, and /help already reads them; a separate tip table would fork that. A tip IS a glossary term -- what was missing is an ANCHOR. app.help_topic keys knowledge to a SCREEN PATH, which answers “how does this page work” and cannot answer “how much food does a 360 degree buffet consume”; knowledge_anchor keys on (subject_kind, subject_key) instead, so one buffet tip reaches the programme, the budget and the catering sheet written once. The two tables sit side by side and neither replaces the other. The wording is ours. The tips were written from a food and beverage guide published by Experient: the prose in that guide is theirs, the craft is nobody's, so every tip is written from scratch, STATES THE ASSUMPTIONS its numbers depend on, and carries a citation the popover displays -- glossary_definition gains three nullable source columns, leaving all 2 901 existing rows untouched. And the units are converted, not copied: the source counts coffee in US gallons at 20 cups each, and this product sells in Europe. Verified: the route returns 4 tips for service_style:buffet and 1 for service_style:passed, so the anchor discriminates rather than merely rendering; a tip anchored to two requested subjects appears once; malformed subjects, a semicolon payload and an empty request are each refused with a specific message. Two traps hit while building it, both already in the notes and both hit anyway: a backtick inside a SQL template ends the template, three times in one day, which is now a repo guard (verify-no-backticks-in-sql.sh, forced red against the exact line); and an array parameter dies on Workers, tx.array() producing “malformed array literal” because fetch_types is false, replaced with an IN over the joined kind:key pair -- which also fixes a bug two separate arrays would have had, matching a subject assembled from one half of each. Applied to local, dev and production; W319's ratchet still reads 7 and 148 on prod, confirming these new policies carry their write checks. *Evidence: 1 migration.*
W321A service style on an agenda item, because W320 had nowhere to put its tipson productionMeasured 2026-09-07: this product has no food and beverage concept at all -- no catering, menu or meal table, and agenda_item.item_kind holds exactly two values. There was no row a buffet tip could sit beside, so agenda_item gains a nullable service_style against a reference table. It is NOT an F&B module and must not grow into one by accretion; that is a product decision with an owner. NULL means “nobody said” and none means “no food served”, because a caterer reading a programme needs to tell an unanswered question from an answered one. In the browser: choosing Buffet makes the tip icon appear and the popover shows four tips with the citation, choosing “no food served” removes the icon entirely, and a bad service_style is refused by the API with a specific message while app.event still has its 18 rows. *Evidence: 1 migration.*
W322A button row that finally saves spaceon productionThe owner, and W313 earned the correction: “I was explicitly asking you to reduce the sizes of the buttons with icons. You added the icons (great), but increased the size of the buttons, so it's not saving any UI surface and space at all.” An icon in front of a full label is wider than the label alone. W313 added icons to four pills and W318 added a fifth, and the row wrapped onto a second line: space is saved by removing WORDS or removing CONTROLS, and W313 did neither and called it done. The deeper fault was that the row mixed two kinds of thing -- the programme and the access list are PLACES YOU GO, splitting and merging and editing are THINGS YOU DO, and five identical lozenges ranked them equally so nothing stood out. Three controls now, with one overflow menu holding the three occasional ones, which also puts a deliberate second step in front of splitting and merging, both of which rearrange an event permanently. Measured in the browser: 3 controls, a 28px row on one line, and the header collapsed onto the title's line, recovering about three lines above the content. MoreMenu is NOT role="menu", deliberately: a real ARIA menu owes roving arrow-key focus and typeahead, half of which cannot be tested in this harness at all because it delivers key events with an empty e.key. A disclosure button over ordinary links is honest about what it is and works with Tab today; Escape closes it and RETURNS FOCUS to the trigger, and an outside mousedown closes it, both verified by dispatching real events. *Evidence: 1 design doc.*
W323One programme across every event in a projecton productionThe owner, 2026-09-07: a project with ten events across two days and fifteen venues needs one combined agenda, with a UI to select which events are in it. The unscheduled section is the feature, not an edge case. Measured on production before writing any of it: the Október 23. project holds 107 agenda items across 27 events and FIVE carry a start time, so a screen that merged by time and stopped would have rendered five convincing rows, omitted a hundred and two, and looked finished. The untimed half gets its own section grouped by event, and the header states the count whether it flatters the data or not. The day is the event's own local day, resolved in SQL with the event's zone, because two events in different zones may put one instant on different dates and a programme is read by somebody standing at the venue. An empty selection is an empty programme, not every event -- the tempting shortcut of skipping the filter when the list is empty inverts the feature, so unticking everything would print the whole project. Exclusions are stored, not inclusions, and that is the whole of the migration: both models put the same ticks on the same screen and differ only in what happens to an event added LATER. Inclusions make a new event INVISIBLE until somebody remembers to tick it, which on a project where events arrive over weeks is a printed programme quietly missing a venue; exclusions make the failure “something unexpected is listed”, which a reader SEES. Keyed on (project_id, event_id) although an event has one project, so an exclusion cannot follow an event moved to another project. The write check constrains BOTH pointers, which is W319's lesson applied to a table written the day after it, and W319's ratchet confirms it -- 148 on prod before and after, so this table did not join the backlog. PDF is the browser's print engine, the decision W31.5 already made and wrote down, and printAs is extracted from documentExport rather than copied. Two defects found by checking rather than reading: pa-unscheduled was used in the markup and defined in no stylesheet, caught by the phantom-class gate, and a print rule named .page-head, which PageHead does not render -- found by testing all 25 print selectors against the live DOM, of which 24 matched. Applied to local, dev and production. *Evidence: 1 migration.*
W324The section guard that was never written, and four format defectson productionDiagnosed by the owner: “the section was outside of the event ... the guard to protect the section modification was correct, but there was no guard to protect the section modification outside of the event's time frame.” Exactly right, and the shape is worth keeping. W102 guarded the SESSION against its event and against its section, and both held; the SECTION was left free to sit anywhere. So a person refused a 09:00 session widened the section to 09:00 -- which SAVED, because nothing checked it -- and then found the session refused again, by the EVENT, which they had not touched: the system accepted a change that could not possibly help, then refused them naming a different container from the one they had just edited. An hour, on a Sunday. containmentError and eventBounds already existed and were already used two routes away; this is the six lines nobody wrote, on both segment routes, and on update it checks the merged pair, because sending only a new start is exactly the edit that walks a section out of its event one end at a time. The hint now carries times, and that is the other half of the same hour: it said “Fri, Oct 23 - Fri, Oct 23”, which a 09:00 session on that day satisfies perfectly, while the bound actually refusing it was 11:00 -- and when a section narrowed the window the hint REPLACED the event's instead of adding to it, so the rule doing the refusing had stopped being mentioned. Two things on one stage are flagged, with the owner's exception that a session and an activity may share a stage; back-to-back is NOT a clash, because a naive inclusive comparison would flag every boundary in a programme of half-hour slots and the alert would be worthless within a minute; an item with no end is a POINT; and it is scoped to the space, never the venue, or one event's programme would flag itself entirely. Forced red five ways, and the fifth found a real gap: overlaps has a branch for “a is the point” and one for “b is the point”, every test happened to put the point second, and a sabotage disabling the first branch passed the whole suite. The PDF had no page margin, found by generating an actual PDF with headless Chrome and looking at it while every gate was green: position: absolute; top: 0; left: 0 resolves against the initial containing block, which in paged media is the PAGE BOX and not the page content box, so @page margins were stepped over. Removing the chrome from the FLOW makes the sheet an ordinary block and gives EVERY page a margin, which absolute positioning could never do; the same repair is applied to the document exporter, which had the identical bug. Also: a body background PROPAGATES TO THE CANVAS in print, and body carries a grey, so every part of the paper the sheet did not cover printed grey. *Evidence: 1 design doc.*
W325The combined project budget, and one set of paged rules for both sheetson productionThe owner, 2026-09-07: “the exact same way you created the merged agenda ... I need the same mechanism.” So the selection table is generalised rather than copied: W323's project_agenda_exclusion already answers which events feed a project view, and a second table with the same three columns would be the same mechanism written twice with two places to get the RLS right. It gains a view column, and the two selections are independent, which is the point of the column -- a crew briefing belongs OUT of a printed public programme and firmly IN the budget. The key had to widen: with the old (project_id, event_id) key, excluding an event from the budget after excluding it from the agenda would collide and the route's on conflict do nothing would swallow it silently, a tick that appears to work and changes nothing. The table is not renamed, and that is a deployment decision rather than a naming preference -- the Worker deployed an hour earlier SELECTs the old name. It does not do its own arithmetic. miniBudgetMath already knows the three things that make a budget total correct here, every one a way to be quietly wrong by a large number: a line contributes line_total * share_pct because a cost shared with an event space is half each; a NULL share means the ONLY owner, which is 100 and not 0; and cost and income are totalled SEPARATELY and never netted. And it says what it could not count -- totalOf returns an unpriced count beside the amount, the money equivalent of the combined agenda's unscheduled section, because a total that quietly omits the lines nobody has priced is a number somebody will take to a client. What currency is this, and sometimes the honest answer is “cannot say”: line_total_base is in the tenant's base currency and on the demo workspace that is NULL, so the label falls back to the currency the budgets actually use when they all agree and refuses to name one when they do not. Money is behind budgets.view_financial, and the route selects no amount columns at all for a caller without it -- not fetched-then-stripped, because a masked figure that reaches the Worker is one careless log line from being written down. The paged rules are now shared on .print-sheet: the margin defect fixed that morning existed in TWO places because the second was a copy of the first. Verified on a fixture carrying costs AND income, a line with no amount and two events: 7 250 000 + 1 800 000 = 9 050 000 cost against 6 900 000 income, the unpriced line flagged and excluded, and unticking an event from the BUDGET left the AGENDA's selection untouched. Applied to local, dev and production. *Evidence: 1 migration.*
W326Two layout classes that were never rowson productionThe owner, of the three controls on the event screen: “the three buttons are not aligned.” The root cause is fifty call sites wide. .row is the flex container; .row-end and .row-tight were written as MODIFIERS, one adjusting alignment and the other the gap, and each assumed .row was beside it. Measured: 32 places carry row-tight alone and 18 carry row-end alone, against 6 and 13 that pair them properly. Alone they did NOTHING -- gap and align-items are inert on a box that is not a flex container -- so fifty rows in this product were laying their children out as inline boxes on a text baseline. Mostly that passed unnoticed: JSX separators put a space where the gap should have been, and most rows hold controls of one height. It stopped passing where W322 put an icon-only button beside two labelled ones: an inline-flex box's baseline is its first line of TEXT, and a button containing only an SVG has none, so its baseline is its bottom edge and it sat visibly higher than its neighbours. The bug was not in the new button; it was in a class that had never worked and had never been asked to. A class called row-tight is a row, so the selector is widened to say so, which fixes all fifty at once. Verified by measuring rather than looking: the three controls' vertical centres now agree exactly (172, 172, 172) where the row was previously inline, and /projects, /venues, /agenda, /events/detail and /settings/forms were swept for the one real risk of turning a box into a flex container -- every row is display: flex, none overflows, and no page scrolls sideways. *Evidence: 1 design doc.*
W327The currency inherits from the project, and the same gap one route overon productionThe owner, 2026-09-07: “Amikor létrehozok egy új rendezvényt, nem húzza át automatikusan a pénznemet a gazda projektből.” Why it was never wired: the two columns have DIFFERENT NAMES. project.base_currency and event.currency describe the same thing, so nothing that copies same-named fields caught it and nobody wrote it by hand. Measured before fixing: project.base_currency was read by NOTHING in the product -- stored and ignored since it was added. The cascade is the shape /events/new already used for the time zone two lines below, minus the venue, because a venue's country decides what TIME it is, not what MONEY is: caller, else the project, else the tenant. It can still resolve to null, and null is honest. A regression almost shipped, caught by testing the path I had not asked about: app.budget.currency is NOT NULL with a DEFAULT of 'HUF' while app.event.currency is nullable with none, and passing an explicit null DEFEATS a column default -- the default applies only when the column is ABSENT from the INSERT -- so the obvious version turned “no currency anywhere” into a failed insert and a 503 on the whole event-creation path. 14 events locally and 28 in production carry no currency and the demo workspace has no base currency, so that was most of them. The column is now omitted when there is nothing to put in it, leaving the column default as the single authority. Then the same gap one route over: the splitter inherits the mother's time zone and carries a paragraph explaining why it follows the mother rather than the venue, every word of which is true of the currency, and the currency was simply not in the column list. Swept for the same shapes, with the negatives reported honestly: 11 columns stored and read by nothing, of which the numbering triple is DESIGNED and not a defect (D-533/536/539 allocate at ISSUE, which events never reach) and venue.embedding_model and embedded_at are written by the AI worker rather than the API. On containment guards, only agenda_item and agenda_segment carry a time range inside an event and W324 closed the segment the day before; every other event timestamp is a lifecycle stamp -- revoked_at, opened_at, issued_at -- which must NOT be constrained to the event window, because a ticket can be revoked after the event. 610 API tests, 301 web tests, the full 98-suite battery with w63 and w295 red and both pre-existing, and the backtick guard caught a backtick in a SQL comment on its first real outing. *Evidence: 1 design doc.*
W328The venue’s address reaches the event map, and its place id reaches bothon productionThe owner, of an event held at a directory venue Google itself supplied: “You say this venue doesn't have an address, which is weird, because when we were selecting it from the Google API, it should come with an address attached to it.” The address was never lost. app.venue held address_line, city, region, postal_code, country and a place_id for that building the whole time; /events/detail returned the venue NAME and nothing else, so the dashboard handed VenueMap a one-field stub and VenueMap correctly reported that what it had been given carried no address. The component was right about its input; its input was a stub. Measured on production before the fix: of the 9 events at a directory venue, 9 had an address available and 9 were showing “this venue has no address” -- the map on the event dashboard had never once worked for a directory venue, only for an event carrying its own place_id, which is the other and rarer half of W299. And the event's own place_id could not have rescued it: picking a venue deliberately CLEARS it in VenueOrPlacePicker, because an event holding both says it is in two places at once, so on all 9 the prop was null by design. The venue's own place_id is the right fallback -- it names the same building -- and 5 of the 9 carry one; a place id beats address text, because the Embed API resolves an id exactly where an address is a search that can land somewhere else. The same omission one route over, one degree milder: /venues/detail does send the address, so the venue page's map has always drawn, but it sent no place_id, so that map was a text search for a building whose exact id we already had. Two controls: an event whose venue genuinely has no address STILL shows the message and draws no iframe, so the branch is intact and the data path is what changed; and tenant B asking for tenant A's event gets 404 and zero occurrences of the address, the fields riding the same RLS-scoped LEFT JOIN as the name that was already there. Battery 157/159, the two reds w63 and w295 both pre- existing; tsc clean both packages, 305 web tests, vite build, repo guards. *Evidence: 1 design doc.*
W329Eight import roles were offered and discarded in silenceon productionFound by sweeping for W328's shape -- a control that looks finished and whose data never arrives -- not by being reported. The agenda importer's mapping offers twenty- one roles and the commit handler reads thirteen: starts_at, ends_at, duration, audience, link, currency, contact_email and contact_phone were listed in the dropdown, sent in the mapping, and read by nothing. Worse than an unused dropdown entry, because sheetProfile.ts assigns most of them BY CONTENT: a column that is over 60 per cent email addresses becomes contact_email automatically, with “the values are email addresses” shown to the reader -- so the importer told the person it had recognised the speakers' emails, answered 201, and dropped them. Reproduced before it was believed: a two-row sheet with all eight mapped committed one item named “Opening” whose internal_notes was NULL. Eight columns, no error, no trace. They now land on the item's notes, each under its own label, where when already lands -- not parsed into real timestamps, deliberately, because the handler already explains that an imported time carries no year and no zone and that a wrong instant is worse than a visible string, and a parser does not belong in a bug fix. And the drift cannot come back: api/test/agenda-import-roles.test.ts asserts every member of the Role union is named by the commit handler, a source-text test on purpose, because the defect was never a mishandled value but a role the handler NEVER MENTIONS -- so it fails when a twenty-second role is added rather than when somebody happens to import a file that uses it. Forced red three ways and both vacuous-pass guards fired. *Evidence: 1 design doc.*
W330Two complete write paths with no control anywhereon productionSwept every route the Worker serves against every path the web calls: 459 against 243. POST /venues/propose/withdraw and POST /venues/invite/revoke are both fully built -- audited, permission-gated, a written refusal for every case -- and NOTHING CALLED EITHER. This project has shipped that exact defect three times before, in W19, W21 and W26. The withdraw route's own comment names the dead end it was built to close: “a tenant that can offer and cannot take back is a dead end on the screen and a 409 in the suite.” It closed the suite's half; the screen's half was never built, so the dead end survived in the place the comment named. A tenant could offer its venue to the shared directory and never change its mind. The venue page could not have offered the control anyway -- it had no way to know an offer was pending -- so /venues/detail now returns proposal_pending in the same shape as claim_pending beside it, plus may_propose. The invitations block was a COUNT, “2 live links on this venue” and nothing else, so an access link sent to the wrong address stayed live until it expired; it is now a list, each row naming whose link it is with a Revoke button, joined from contacts which is already loaded. Caught on the way, and it is the same defect as the slice one field over: may_propose first landed on /venues, the LIST route, and tsc was happy -- nothing types a Worker's JSON against the screen that reads it -- and only the end-to-end probe found it as may_propose=undefined beside a proposal_pending that had correctly gone true. Still open, reported not fixed: 24 more routes no screen calls, several legitimately outside the app, the rest needing reading one at a time. *Evidence: 1 design doc.*
W331A table column that gets out of the way when the table is too narrow for iton productionThe owner, 2026-09-07, with a screenshot of the events list: narrow layout, huge overflow, unreasonably high rows. .dt is width: 100% with automatic layout, so a table too wide for its wrapper does not overflow -- it SQUEEZES, and the squeeze lands on whichever column can give: on this list the nine-item counts strip, which wraps to nine lines while the column doing the damage is mostly past the right edge and unreadable anyway. Measured on the list at four wrapper widths before anything was written: 1264 gives a 79px row over 3 lines, 1184 gives 102px over 4, 1104 gives 124px over 5, and 944 gives 212px over 9. Four lines is still a table somebody can scan -- the name column already wraps to three on a long Hungarian title -- and five is where it stops being one, so the cut is at 1160 with room either side; pushing it to 1240 would hide the column from every 1440 laptop. The capability is in the shared table, and it is careful about three things. Measured against the WRAPPER, not the window, because the rail and nav panel take 286px and the panel collapses at 900px, so one window width gives the table two different widths. Null until measured, because a 0 would mean “narrower than everything” on the first render and flash the wrong table on every load. And it takes more room to come back than it took to go: dropping a column shortens the page, a shorter page can lose its scrollbar, and on Windows or Linux that hands about 15px back -- hide, widen, show, lengthen, narrow, hide, every frame. Measured on this Mac the gap is 0px, because macOS draws overlay scrollbars, so the loop would have shipped working for the author and flickering for everybody else. 24px of hysteresis clears any scrollbar. And only a column whose content is reachable elsewhere may declare it -- fair for these nine numbers, which are all tiles one click away, not fair for the venue, the code or the dates, and the prop's documentation says so. Harness note: ResizeObserver NEVER FIRES in this project's browser harness, and the viewport emulation fires no window resize event either, so a first reading that looked like a bug in this code was the harness being blind. *Evidence: 1 design doc.*
W333An event director may create an eventon productionThe owner, 2026-09-07: *"They should be able to create events and projects. Otherwise, the system is useless."* Asked which roles, he confirmed this one by name. ★★ THE SHAPE OF THE MISTAKE: Event director is a platform TEMPLATE role holding 133 permissions including projects.create, and it did not hold events.create -- so the role named after events was the one role that could not make one. A seed omission of exactly D-633's kind, closed by GRANT rather than by exemption. *Evidence: 1 migration.*
W335A seat may be scoped to a project, not only to eventson productionThe owner, 2026-09-07: *"please create a project-level sharing to other tenants... I want to give full access to someone else to my specific project."* WHAT ALREADY EXISTED AND WHY IT WAS NOT ENOUGH: membership_event_scope is an allow-list of EVENTS a seat may reach, so a scoped membership can do less, never differently. A project is the tier above the event, and granting one project meant enumerating its events and re-enumerating them every time one was added. *Evidence: 1 migration.*
Phase 11 - Doors for the engines
W342The accreditation terms tables get a write check of their ownon production★★ WRITTEN BEFORE THE FIRST WRITER, WHICH IS THE ONLY GOOD TIME. accred has 24 tables, zero rows on production, and exactly one route referencing it. W342 is the first slice to write to any of them, so this was the last moment the policies could be tightened without a backfill and without breaking a caller. A write check added after the first writer is a migration plus an argument with whatever is already stored. *Evidence: 1 migration.*
W343A brand kit that belongs to an eventon productionThe proposition: *"A proprietary Brand Kit module lets event organisers white-label the entire system per event."* ★★ AND THE PRODUCT ALREADY HAD BRAND TABLES THAT CANNOT DO THAT. Measured 2026-09-07: app.brand_asset, app.brand_colour and app.brand_font are all TENANT-scoped, so they describe the agency and not the event it is running. Per-event white-labelling needs a per-event row, which is what this adds. Four CSS custom properties on a wrapper then re-skin every component inside it -- the design system paying for itself. *Evidence: 1 migration.*
W344The crew newsfeedon productionThe proposition names it: *"Accreditation, Guest List, Advancing, Catering, H&S Inductions and crew Newsfeeds and bulk emailing."* Measured 2026-09-07: no table, no route and no screen anywhere carried the word. ★★ AND IT IS NOT app.chat WITH A FLAG. Messaging already exists -- threads, participants, mentions, an inbox, read pointers -- and reaching for it here would have made a broadcast into a conversation everybody may reply to. One voice, many readers, is a different shape. *Evidence: 1 migration.*
W345The accreditation matrix: categories, bands, time windows, and the cells betweencompleteThe first door onto the half of the access-control problem D-923 kept. **NO MIGRATION: the 24 accred tables, the nine accreditation.* permissions, the grants to app_rw and forced RLS with both USING and WITH CHECK have all existed since W209 to W215 and were MEASURED here rather than assumed -- the slice is five routes and one screen, which is what a door is. THE MATRIX IS A LIST OF CELLS AND NOT A GRID OF CHECKBOXES, and the constraint is why: category_entitlement_uniq is over (category, band, WINDOW) with NULLS NOT DISTINCT, so one category may hold one band during build-up AND during show days. A checkbox holds one bit and would have shown the second grant as the first. The grid was also unshippable on a phone: .dt-wrap is overflow: hidden, so bands past the fold would have been CLIPPED rather than reachable. The delete spells is not distinct from**, because = null matches nothing and would have made revoking a whole-event cell a silent no-op -- proved by sabotage, not by reading. Verified: 21 HTTP assertions end to end against the local Worker each paired with a deliberately invalid control, 10 SQL assertions with two negative controls, the is not distinct from one forced red before being believed; 315 web tests, design parity, vite build, tsc clean both packages; the screen driven in a browser at 1280 and 375 and a category saved through it. *Evidence: acceptance suite.*
W346The pass: issued in a category, frozen with the bands that category held that daycompleteWhat W345's matrix was configured FOR, and again no migration -- accred.pass and accred.pass_zone have existed since W209. THE ENTITLEMENTS ARE FROZEN ONTO THE PASS AT ISSUE, which W209 argues in its own words: editing a category default must not silently re-scope every pass already printed and in somebody's pocket. The gate will read pass_zone and never the matrix. The suite widens the category AFTER issuing and proves the printed pass did not move, while one issued a moment later gets both bands. THE QR TOKEN IS RETURNED ONCE AND STORED NOWHERE: D-730's posture, the same app.grant_token takes, so the column holds a SHA-256 and a database dump hands nobody a working pass. The assertion is not that a token came back but that the raw string appears NOWHERE in the row, plus a second computing the digest independently -- added because reading the hash off a screenshot produced the wrong conclusion once. A PASS STARTS pre_valid, NEVER ACTIVE, or accred.profile.validation_required would be decorative. The five transitions are the ROUTE's and not the schema's, said so in the code: pass_status_known constrains seven values and says nothing about order, and revoked, expired and reissued are terminal because accred.reissue is how somebody gets access back. Two tabs, not two rows: the matrix and the pass list are the same person on the same afternoon, and the counter with a queue is accred.centre, which has no screen yet. Verified: 28 assertions, sabotage forced four red before believing them; battery 157/160, 0 FAILED, 0 did not run. *Evidence: acceptance suite.*
W347The crew link, one row per person on the roster and no accountcompleteThe owner, 2026-09-08: *"Create the Crew / guest portal."* The third of the three portals, and the only one whose audience is named on the EVENT itself rather than being a company on the other side of a contract. ★★ participation_kind IS ALREADY EXACTLY workforce, guest, so one portal serves both. A stagehand reads their own call times and accepts the H&S wording with no account at all. The read records nothing: mail clients prefetch, and a GET that wrote would sign for every crew member on delivery. *Evidence: 1 migration, acceptance suite.*
W349The entry policy: single-use, multi-pass, and whether they may returncompleteThe owner, 2026-09-08, asked directly and answered directly: *"the user has to be able to define any scenarios for the event by badge type, accreditation type, ticket type, crew type."* So the policy is a subject and a rank rather than a flag: app.entry_policy_rank() puts an accreditation category above an attendee category above a ticket source above the event's own default, and the most specific match wins. Re-entry defaults to allowed and is overridable at any of those levels. The deny vocabulary went from twelve grounds to fourteen. *Evidence: 1 migration, acceptance suite.*
W350The cloakroom: one scan deposits or returnscomplete★★ THE DESIGN IS NOT INVENTED HERE. It was read out of the owner's own Bubble app in July and is recorded as E-350 and E-351. Nothing on record reserves a slot, something on record gives it back, and the attendant never picks a mode -- the scan knows which it is. Occupancy is counted from the items and never stored on the rail, so the number cannot drift from the things it counts. *Evidence: 1 migration, acceptance suite.*
W351Vouchers: a drink, a meal, a tote bag, and one scan that spends onecompleteThe owner, 2026-09-08, asked for the other uses of the QR code, *"setting up vouchers"* first. ★★ WHAT THE BUBBLE CAPTURE GIVES AND WHAT IT DOES NOT: E-350 workflow 15 writes a COUNT onto the attendee record, which cannot answer *what was spent, when, by whom*. The balance is counted from an append-only log and never stored, and re-read inside the transaction that writes, one statement from the insert, so two tills each showing "1 left" cannot both pour. *Evidence: 1 migration, acceptance suite.*
W352Swapping details by scanning a badge, and only with consentcompleteThe owner, 2026-09-08, asked for badge-to-badge contact sharing and was asked directly what should be shared. The answer was unambiguous: "Only after an explicit opt-in." So consent is a GATE IN FRONT of the exchange rather than a setting on it, in three separate answers -- name, email, phone -- because a single "share my details" hands a phone number to somebody who meant to swap a name. Every scan is logged whether or not anything was shared, because *who has been reading my badge* is a question only a log written both ways can answer. *Evidence: 1 migration, acceptance suite.*
W353Setting a cloakroom up: rails, hangers, shelves, and missing ticketscompleteThe owner, 2026-09-08: *"the cloak system admin screen to set up a cloakroom by rails, hangers, shelves, missing tickets, numbering, etc."* W350 built the counter and gave it ONE kind of place and ONE reason not to use one; three of the five words in that instruction were not in the schema. Adds kinds -- hanger, shelf, rack, locker -- a stub prefix so A-101 and B-101 are different hangers, and three states that keep *a broken hanger* and *a lost stub* apart. *Evidence: 1 migration, acceptance suite.*
W354The newsfeed reaches the crewcompleteW344 built the feed and it had never been readable by the people it is written for, for two measured reasons. app.newsfeed_read.membership_id is NOT NULL with a foreign key to app.membership, and a crew member has no membership -- that being the entire point of a portal that knows somebody by a token. A receipt may now name a participant instead, exactly one of the two. ★★ AND THE COUNT WAS WRONG IN THE FLATTERING DIRECTION: audience_size was count(distinct person_id) and a roster row may be a placeholder with none, so 12 rows with 8 placeholders reported an audience of 2 -- a feed nobody could read, reporting that everybody had. *Evidence: 1 migration, acceptance suite.*
W355A delivery phase can finally be filledcompleteW170 shipped app.delivery_phase_task, app.delivery_phase_progress() and POST /events/phases/tasks, and W227 shipped the panel that renders the answer. Nothing ever connected them. So a phase could be defined and a progress bar drawn, and there was no way to put a task into the phase the bar was measuring -- the panel could only ever have read zero. An API-only slice: no table of its own, and the gap was a missing caller rather than a missing column. *Evidence: acceptance suite.*
W356A ticket sent to the wrong person can be taken backcompleteW293 shipped POST /guests/invites/revoke-ticket and nothing ever called it. The reason was one column deep: /guests/invites did not carry the ticket's state, so no screen could see what to offer a control for. That is the shape this ledger keeps recording -- a complete write path with no door -- and here the door was missing because the read next to it was one field short. *Evidence: acceptance suite.*
W357A provenance key that outlived its value, and the gate it was quietly closingcompleteReported as one venue row and it was eleven times that, because the report scanned app.venue and app.venue alone. Scanning all THREE tables that carry a per-field provenance jsonb -- and DISCOVERING their keys rather than listing them -- found 8,570 stale claims on local and 8,592 on production, in two fingerprints that are two different bugs: company.place_id at 8,530 and venue_space.area_sqm are imported, an import that stamped every field it ATTEMPTED rather than every field it FILLED; company.trading_name, .country and .vat_number at six rows each are owner, which is the reported defect on a table nobody had looked at. The harm is not untidiness. app.provenance_may_write compares ranks, owner is 60 and an ABSENT key is 0, so a stale owner claim on an EMPTY column permanently refuses the importer (20), ai_approved (40) and ai_direct (10) -- the field looks empty, refuses to be filled, and nothing names the reason. The fix is a trigger and not a route change, and that is the whole decision. POST /venues/update was driven, not read: city: null on a venue carrying {"city":"owner"} answered 200 and left the claim standing. But this Worker writes venue provenance in exactly ONE place and never writes owner at all -- Estate does, stamping Object.fromEntries(cols.map((c) => [c, 'owner'])), keyed on the column TOUCHED rather than the value WRITTEN, which is the fingerprint of every owner row found. Two products write these tables; a repair in one repository's route would have left the other free to carry on. The trigger names no column -- it reads to_jsonb(new) -- and leaves a key naming no column alone, because a claim about a field that does not exist is a different defect and collapsing the two would hide it. 17 assertions, and the sabotage reddens six of them across all three tables including the route reproduction, while every positive half stays green. One assertion went red for being WRONG and was repointed: it asked whether a refilled field was fillable, and an owner claim on a HELD value is a legitimate claim the importer should be refused. w63 assertion 1a, the invariant that caught this, was not relaxed -- it is unchanged and still passes 17/0. No production data touched: the 8,592 rows heal when something next writes them, and sweeping them is an owner decision recorded as its own question. *Evidence: 1 migration, acceptance suite.*
W358The invoice that could never be itemisedcompleteA ROUTE WITH NO CONTROL IS NOT A FEATURE, AND FOUR OF THEM WERE SITTING IN A COMMENT. ProcurementScreen.tsx opens by naming the sixteen routes it uses. Five were never called by anything, and the list had been read as a description ever since. The cost is not tidiness: POST /budgets/invoices/lines/save is the ONLY route in the Worker that inserts into app.invoice_line, so an invoice could be raised, approved and marked PAID and never itemised. 565 local invoices carried 7 lines between them, all seven written by suites; production had 0 lines on 0 orders and 0 invoices. Four are now wired -- add and remove an invoice line, correct an order header, correct an invoice header -- and the fifth stays out behind a written reason. AND AN ACCEPTANCE SUITE CANNOT CATCH THIS, WHICH WAS MEASURED RATHER THAN ASSUMED: disconnecting the Add-line control left w358-acceptance.mjs green at 15 of 15 while web/test/procurement-manifest.test.ts went red in both directions. That test reads the comment and the code from the SAME file, so neither can drift from the other, and its own first version had the exact blind spot it exists to remove -- a template prefix /budgets/purchase-orders/${what} matched every route beneath it and reported two unwired routes as wired. An interpolation fills ONE segment; the rule now says so. W19, W21, W26 and W330 each shipped a write path nothing could reach, and this is the first check that would have caught any of them. *Evidence: acceptance suite.*
W360The holidays that spoke English in HungarycompleteHUNGARIAN PUBLIC HOLIDAYS, SHOWN TO HUNGARIAN USERS IN ENGLISH, under a heading reading *Magyarország*: 1956 Revolution Memorial Day, All Saints' Day, Saint Nicholas Day. The cause is one token. Google's calendar ids are language-prefixed names -- en.hungarian, not hu -- and api/src/holidays.ts hard-coded en. on all five countries, in a file whose own comment says the prefix is a language. GET /holidays now takes locale, the parameter five other routes already take, defaulting to en so no existing caller changes; the dashboard passes localeParam(). On screen: *1956-os forradalom ünnepe*, *Mindenszentek*. AND THE INTERESTING PART IS THE FALLBACK. All ten (language, country) pairs the change can produce were fetched: every one answers 200 with an identical event count, but only hu.hungarian is really Hungarian -- hu.german, hu.uk, hu.usa and hu.austrian return their English twin's content. Google localises a country's calendar into its own languages and serves English otherwise, silently, with a 200. That is worse than a 404 and it is exactly what the file's existing comment does NOT warn about, so section 3 of the suite asserts the fallback is harmless rather than pretending it cannot happen. The same slice translated the dashboard greeting, which was Good morning! in English because ${partOfDay}! is a template and a template is not a t() literal. *Evidence: acceptance suite.*
W361The counted noun, and the index that printed the databasecompleteTHE GUEST LIST SAID 0 vendégrekords. {t("guest record")}{n === 1 ? "" : "s"} -- the English plural bolted on AFTER the lookup, making a Hungarian non-word. No suffix could ever have been right: Hungarian does not pluralise a counted noun at all, *5 vendégrekord*, so the decision belongs inside t() and both English forms map to one Hungarian word. Twelve sites moved. AND THE PRODUCT'S MAIN INDEX PRINTED THE DATABASE. /events rendered <Badge>{r.status}</Badge> -- the raw enum -- on every row, beside Pre-event, Not scheduled and Not set; <em>{label}</em> rendered all nine count columns with no t() anywhere near them, and three of the nine had no Hungarian at all while the gate read green, because a bare string inside a TUPLE is not a shape it collects. Rewriting the array as label: puts them in its input set. ONE WORD WAS SERVING TWO GRAMMARS: assets was *eszközök*, right for a column header and wrong after a numeral -- and it was the odd one out already, since spaces, crew, guests, tasks and budgets were all singular in Hungarian. The vocabularies were READ from pg_enum, not invented: status is planned, started, ended, cancelled, phase is pre_event, live, post_event. 22 SITES ARE DELIBERATELY NOT FIXED HERE -- whole English template literals that never reach t() -- because each needs a Hungarian sentence rather than a lookup, and they are listed rather than half-done. *Evidence: acceptance suite.*
W365The setup wizard speaks Hungarian, including its own contentcompleteTHE FIRST SCREEN A NEW EVENT MANAGER MEETS ASKED ITS QUESTIONS IN ENGLISH, under Hungarian chrome. All 263 strings it renders are now translated: 14 questions, 5 question groups, 25 moments, 131 build actions, 49 measures, 25 *decides* lines, 5 withheld reasons and 4 phase headings. DELIVERED BY W290'S UNIT-VOCABULARY PATTERN, NOT BY A SCHEMA CHANGE: the generated English is checked in at web/src/system/wizard-vocabulary.ts, hu.ts translates it like any other key because the dictionary is keyed on the English source, the screen calls the translator on what the API returns, and api/test/w365-acceptance.mjs holds the checked-in list against the four seed tables in BOTH directions. THE i18n GATE WOULD OTHERWISE HAVE READ ALL 263 AS ORPHANS and demanded their deletion -- the same hole nav, units and the server messages already fill, arrived at a fourth time and by far the largest. AND A HOMOGRAPH COLLIDED, WHICH AN ENGLISH-KEYED DICTIONARY CANNOT HOLD: Registration already meant *Nyilvántartási szám*, a company registration number. The imprecise side was made precise -- CompaniesScreen now asks for Registration number, which is what its Hungarian always said -- and the wizard got the word. Scoped to the wizard, not the playbooks: the other 132 English rows belong to /events/playbooks, which W362 measured as reachable from no screen, so translating it would be work with no reader. CORRECTED 2026-09-09, THE SAME NIGHT: the sentence before this one is FALSE. W362 measured no such thing. /events/playbooks was NOT among its 43 unreached routes, because MarketingPlanScreen.tsx calls it by name. The claim was carried from FINDING-2026-09-07-unreached-routes.md, which predates that screen, and repeated without being checked against a sweep run two hours earlier in the same session. The playbook content is user-visible at /events/marketing-plan: 154 distinct strings, 7,352 characters. W366 translates them. Doing the wizard first was right for a different reason -- one slice at a time -- and the reason recorded was not that. ★ The wording is the building session's, not the owner's, and it is one contiguous block in hu.ts that can be rewritten line by line without touching code. *Evidence: acceptance suite.*
W368Who may enter a zone, and two gates that had been red on maincompleteA ZONE WITH NO RULE ADMITS ANY VALID TICKET, WHICH IS CORRECT, AND NOTHING SHOWED IT. app.access_zone_rule is what turns a zone from a counter into a boundary and it had no writer, so a backstage door admitted general admission invisibly. Found by running a real event through the door end to end -- the GA ticket walked into Backstage twice and the scan engine was RIGHT both times: first the zone had no rule, then a ticket_source rule named the event's ONE internal source, which every minted batch went into. New POST /checkin/zones/rules, the rules returned on the zones read, a list column reading "Anyone with a valid ticket" in warning tone, and an optional source_id on /tickets/generate so crew passes can have a source of their own. RENUMBERED FROM W345, WHICH WAS ALREADY TAKEN by the accreditation matrix (3563af0) -- landing it under that id would have made the ledger say two things under one number. IT SHIPPED WITH TWO GATES RED ON main, BOTH INHERITED AND BOTH FIXED HERE. holidays.test.ts asserted /^en\./ against a map W360 had deliberately changed to bare names, so it went red the night W360 landed and nobody saw it; it now asserts what holidayCalendar() produces, which is the value that reaches Google. And verify-demo-bridge-dev-only.sh had been red on every push since W324, whose two @media print blocks named .demo-banner beside .rail and .topbar -- putting the string back into the always-shipped stylesheet, which is the ONE thing src/system/demo-banner.css exists to prevent, as its own header records. CSS is not tree-shaken. A GUARD NOBODY READS IS THE SAME AS NO GUARD, and this one had been saying so for eleven days. *Evidence: acceptance suite.*
W370The schedule that could be read and never set, and a checker that read its own commentscompleteA UNIT'S PREVENTIVE SCHEDULE WAS RENDERED, THE ROUTE TO WRITE ONE WAS SERVED, AND THERE WAS NOTHING BETWEEN THEM. So app.maintenance_schedule could only ever be empty, next_due_on was always null, and the overdue list was empty for every unit -- a feature that read as working and reported nothing. Read off the running app before the fix: the warehouse table's *Következő szerviz* column said Nincs ütemezve on every row. ★★ AND THE FILE'S OWN HEADER HAD CLAIMED THE CONTROL SINCE W29.2 -- POST .../maintenance/schedule Unit drawer, \Set schedule\`. A comment describing an INTENDED wiring is indistinguishable from one describing a real one, which is why the wiring is now a row in route-reachability.test.ts that fails when it stops being true. TWO OF THE THIRTEEN UNREACHED ROUTES ARE NOT GAPS, measured rather than assumed: /assets/inventory/detail already returns movements and evidence and the drawer renders both, and GET /guests already answers with the same three keys /guests/categories does. Wiring any of them would have added a second way to fetch rows already on screen. FOUR RUNTIME-BUILT ENGLISH SENTENCES -- ${n} move(s), ${n} record(s), ${n} item(s), every ${n} days -- rendering under Hungarian headings, invisible to W364's check because it reads for a plural SUFFIX and these carry (s) and a bare days. Found by reading the running screen, which is the lesson W366 recorded and which held again. ★★ AND THE REACHABILITY CHECKER WAS READING ITS OWN COMMENTS: documenting a route as a duplicate, in backticks, marked it REACHED. i18n.test.ts took that root-cause fix on 2026-09-09 after eating five comments; the sibling was left out and had been blind by luck. It strips comments now -- and that immediately produced a FALSE finding, which was the useful part: /quotes/units read as unreached and is fetched on every render as ${base}/units. The checker now resolves a base-assembled path too, scoped to one file, because the first version paired unrelated files and wrongly cleared /guests/categories`. *Evidence: acceptance suite.*
W372Advancing, and the crew member who could not answer a formcompleteTHE ONE MODULE ON THE BASE PLATE WITH NOTHING SHIPPED AGAINST IT SINCE 7 SEPTEMBER. Advancing is the forms engine given an event, an addressee and a chase — and it adds no form machinery: app.form, form_version, form_field, form_response and form_answer already build, version, publish and collect answers with per-field withholding. Two tables bind them, modelled on app.cfp_call because that shape is already working in production. ★★ AND THE ADDRESSEE IS A ROSTER ROW, WHICH ALREADY HAS A DOOR — W347 gave every event_participant a crew-portal link, so there is no new credential, no new portal and no new grant_type to invent. ★★ THE HALF THAT MAKES IT POSSIBLE AT ALL, AND IT WAS NOT OBVIOUS: A CREW MEMBER COULD NOT ANSWER A FORM. form_response carries a RESTRICTIVE policy admitting respondent_user_id = app.current_user_id(), and the crew portal runs with userId: null — so the disjunct is null = null, which is NULL and not true. A restrictive policy can only narrow, and this one had narrowed advancing out of existence before it was written. Same cause as W354's newsfeed_read.membership_id: a credential model that assumes an account, meeting a portal built on there being none. The tempting repair was OR event_participant_id is not null — which works, and is the *protected by accident* shape W369 measured in app.ticket_batch the day before. The identity goes into the SESSION instead: app.current_participant() mirrors app.current_user_id() line for line as a sixth GUC. A DEFECT IN SHIPPED CODE FELL OUT OF THE SUITE: the required-field check read value_text is not null, and an empty string is not null — so a required field submitted as "" satisfied it and the response was accepted with nothing in it. The same line was in /portal/form/answers, live since W151. *Evidence: 1 migration, acceptance suite.*
W374The chase that could not leave the buildingcompleteW372 shipped /advancing/chase saying in its own header that it RECORDS a chase and does not SEND one, because sending is app.notification. The stated reason turned out to be wrong: notification.recipient_user_id references app.app_user and /notifications/send refuses anybody without an app.membership, while an advancing addressee is an event_participant -- the roster row W347 gave a portal to *because that person has no account*. Measured on production first: 59 roster rows, 18 with a guest_email, 0 with a person or a user, 0 rows in app.crew_invite, and 41 of 59 with no address at all, now recorded as no_address rather than passed over. The order is send, then persist: the crew token is a SHA-256 that cannot be recovered, so every chase mints a fresh link and minting revokes the previous one -- revoking first would let a provider refusal destroy a working credential and deliver nothing. workforce.edit is required beside forms.edit because the chase now mints a credential, and it was asked of production rather than invented: all five roles holding one hold the other. *Evidence: 1 migration, acceptance suite.*
W377The Hungarian that was already in the databasecompleteapp.product_label had existed, populated with 17 Hungarian rows, and NOTHING read it -- while /inventory rendered the English product.short_description under Hungarian column headings. The translation was done and the door was missing. It was the only one of seven: six sibling *_label tables are wired and this one had zero references, which is what turns a lone unused table into a finding. ★★★ And the slice nearly shipped an erasure: once the list returned the label, the catalogue drawer -- which fills its inputs from that read and saves them straight back into app.product -- opened pre-filled with the TRANSLATION in the editor for the English SOURCE, so Save would have destroyed the original while looking inert. Caught by driving the screen. The route returns the source alongside, the drawer edits that, and a notice says why. The public directory was deliberately reverted: localising it answered 503, permission denied, because app_public has no grant on that table where its wired siblings do. *Evidence: acceptance suite.*
W378The category the catalogue could not setcomplete★★ RENDERED TWICE, ACCEPTED BY BOTH WRITE ROUTES, AND SET BY NOTHING. Measured before a line was written: 0 of 19 products on production carried a feature_category_id and 3 of 42 on local, while InventoryScreen rendered feature_category_name at lines 277 and 486 and ProductDrawer held exactly four fields, none of them the category. W370's shape in a third file -- a value displayed, a route to write it served, and nothing between them -- so the column could only ever have been blank. THE PICKER IS NOT NEW AND NEITHER IS THE QUERY: useCategoryPicker already took a base and listFeatureCategories now serves both /quotes/categories and /assets/products/categories. ★★ READING IT OFF /quotes WOULD HAVE COST NOTHING TODAY AND BEEN THE WRONG COUPLING. Passing "/quotes" from the warehouse screen works on the first try -- quotes is internal in authz/surface.ts and that route carries no permission gate at all -- but the catalogue would then read its vocabulary through a surface it has no other business on, and the day somebody adds session.require(tx, "quotes.view") there, an entirely reasonable thing to do to a quotes route, the warehouse picker goes blank for anyone without a quoting permission with nothing in either file to say why. The new route is gated on inventory.view, which is what /assets/products itself requires. ★★ AND THE ASSERTION THE SLICE RESTS ON IS THE PAIRING, NOT THE SETTING. Proving a category can be set is the easy half. The drawer sends feature_category_id only when somebody picked or cleared, because the route reads an ABSENT key as leave-it and an explicit null as clear-it -- its own categoryTouched. Break that in either direction and renaming a product silently unfiles it, an edit that destroys a value nobody is looking at. Sabotage 3, rewriting the update the obvious way as body.feature_category_id ?? null, reds 4a alone. ★ EVERY ASSERTION COMPARES AGAINST THE CATEGORY ACTUALLY CHOSEN, because !== null passes against a route that files everything under the first row it finds; and the product is CREATED THROUGH THE ROUTE rather than seeded, because a SQL fixture read back proves the SELECT works and says nothing about the write path -- precisely the half that was missing. ★★★ DRIVING IT FOUND A DEFECT NO SUITE COULD, FOR THE FIFTH TIME. Opened in Hungarian, the picker's second step read *Choose a sub-category* in an otherwise Hungarian drawer: the prompt was assembled at runtime as ` Choose a ${STEP_LABELS[i].toLowerCase()} , and a string that is never a literal cannot be reported missing by a checker that collects t("literal") -- so web reported a complete dictionary over a control asking in English. W370's runtime-built strings and W375's spanning matcher, a third time. The English was wrong anyway: *Choose a item*. Both halves are written out per step now in the shared component, so the quote screens take the repair too. THE ROUTE IS CLASS D, NOT BACKLOG: its URL is assembled from a base the screen passes into a hook in another file, exactly like the two portal rows, and the composition rule could have been satisfied by writing the leaf template into the screen -- code shaped to quieten a checker rather than to do anything. *Evidence: api/test/w378-acceptance.mjs 17/0/0, forced red by three sabotages moving DISTINCT sets -- ignoring ?parent reds 1e alone, dropping the gate reds 2a alone, ?? null reds 4a alone; the Class D row proved load-bearing by deleting it, which reds the main reachability assertion naming this exact route. Battery 182 suites, and the three that were red on the first pass were all documented self-poisoning rather than regressions: w10, w29 and w29-1 on a W2-retired grant token, and w25 on its OWN erasure -- that suite prints *NOT A REGRESSION ... a previous run of this suite succeeded* and cannot restore what it erased because W25 revokes DELETE on app.guest_invite_send; after the two fixture reloads the harness itself names, all four are green and w25 is 119/0. api 622/622; web 349/349; typecheck, i18n-wrap, design-parity. On production: api version 79f1102a serving 100%, web cb6be108, and the served index-wct2cd9n.js carries all five new Hungarian strings under a Cache-Control: no-cache HEADER, with *Choose a item* at 0. ★ A 401 on api.event.clinic was measured to prove NOTHING -- /assets/products/categories, a route that does not exist, and an unmapped surface all answer 401 unauthorized identically -- so the deploy is evidenced by the serving version id and the pre-auth crew surface, which routes: /crew?token=<invalid> answers 404 this link is not valid any more. Driven at 1400x1000 in Hungarian: JELLEMZOKATEGORIA blank on every row before, the drawer on *Kordon és tömegirányítás* reading *Nincs kategóriába sorolva.*, drilled Equipment and Furniture Rental to Crowd Control devices with the state line following each pick, saved, and the row then read Crowd Control devices -- confirmed in the database as a level-2 category, local 3 of 42 to 4 of 42. No migration. D-955.* AND THE OWNER THEN ASKED FOR THE CATALOGUE TO BE FILLED, so it was, later the same day: 18 of 19 filed on production, in 07-delivery/w378-catalogue-feature-categories-2026-09-09.sql. The slice itself still set none, and the distinction matters -- app.feature_category drives amount_unit_dim and the quote-line picker, so a filing changes which units are offered when that product is quoted, which is a business consequence and was the owner's to authorise. The nineteenth was test data and the owner then removed it: This is a new product / ladnkjnda, created 2026-07-29, had sat in the live catalogue for six weeks. It was left unfiled rather than given a category, because filing it would have dignified a row that should not exist, and deleting production data is the owner's call. It is gone as of 2026-09-09 by 07-delivery/w378-remove-test-product-2026-09-09.sql, which captures the whole row and its exact reversal insert first. ★★ A PRODUCT DELETE IS NOT ONE DELETE: five foreign keys point at app.product doing THREE different things -- two CASCADE, one RESTRICT, two SET NULL -- and one of them, estate.venue_feature, reaches out of app into another product's schema. All five were measured at zero for that row and the file re-measures them at apply time and refuses rather than taking anything with it. Both guards were forced red on local first, on a name matching two rows and on a product with six inventory items. Production is 18 products, all 18 filed. ★★ EVERY CATEGORY NAME WAS CHECKED TO RESOLVE TO EXACTLY ONE ROW before the file was written and again at apply time, because a name in a 3 150-row DAG is a label and not a key. ★★ AND RUNNING IT ON LOCAL FIRST IS WHAT CAUGHT THE REAL DEFECT: matching on name alone is TENANT-BLIND, and local carries the same seed catalogue for two tenants, so the first version reported UPDATE 33 against eighteen names and filed another tenant's catalogue silently -- the only visible sign being a notice reading 34 of 18. Production is single-tenant with unique product names, measured, so it was always safe THERE and was not safe as a file. A tenant guard now refuses when it cannot tell whose catalogue it is looking at, and it was forced red on local before production was touched. Left open and recorded: FINDING-2026-09-09-feature-category-has-no-label-table.md -- filling the column made it visible that app.feature_category is the one taxonomy with **no *_label` sibling**, so 18 of 19 rows now read an English category under a Hungarian heading. *Evidence: acceptance suite.*
W379The board that omitted a shipped slice in silencecompleteA LEDGER WHOSE ONE JOB IS TO SAY WHAT IS BUILT WENT FROM W374 TO W377 WITH NOTHING ON THE PAGE TO SAY WHY. Eight slices were named in this script and absent from every table it writes, two of them -- W375 and W376 -- shipped to production the previous evening under D-952 and D-953, and a third, W297, the error boundary that fixed the black screen the owner photographed. THE PRINCIPLE WAS ALREADY WRITTEN DOWN HERE AND ENFORCED FOR ONLY ONE OF THE TWO WAYS A ROW CAN VANISH. A slice outside every PHASES range REFUSES TO GENERATE, over a comment reading *a ledger that omits a shipped slice in silence is worse than one that refuses to build*; a slice with no evidence file only printed to stderr, over a comment reaching the same conclusion in its own words. stderr is read by whoever RUNS the generator and this document is read by whoever OPENS it, and the second reader is the one asking the question the file exists to answer. REFUSING WOULD HAVE BEEN THE WRONG SYMMETRY and the existing comment says why: the row genuinely has no evidence and no edit here can conjure any, since W93 lives in ../event-clinic-app/web which nothing here scans, so refusal would make the board unobtainable over a condition nobody can clear. Disclosure, not refusal. ★★ AND THE DISCLOSURE HAD TO CARRY THE SHIPPED CLAIM OR IT WOULD LIE IN THE WORST DIRECTION: naming a slice under a heading reading *What this ledger does not cover* and stopping there says NOT BUILT, which is exactly the wrongness WEB_ONLY_SHIPPED exists to correct. Membership of that set is this script's own record that a slice is deployed, so it is emitted per slice -- six entries carry no claim and two do, because if every entry carried it the sentence would carry no information. *Evidence: 07-delivery/test_build_ledger_reconcile.py, 12 assertions to 17, running the real collect(), reconcile() and main() against a tmpdir corpus. The harness could not see the document at all -- run() returned three values and the tmpdir was destroyed before the caller could read anything -- so it now reads OUT INSIDE the tmpdir and returns it fourth. W375 is the fixture slice number deliberately, being a real WEB_ONLY_SHIPPED member, so the claim branch is exercised without repointing that set. Forced red by three sabotages moving DISTINCT sets: the block never emitting, which is the pre-fix state exactly, reds 4 while the stderr case stays green, and that is what proves the new assertions measure the page rather than the warning; the claim emitted unconditionally reds *a slice with no such record does NOT* alone; the section emitted unconditionally reds the control alone. ★★ THE THIRD IS THE ONE WORTH KEEPING -- a disclosure that is always present discloses nothing and would read as eight permanent holes on a board that has none, and it is the only case a positive control could never have found. The generator was restored from a scratchpad copy after each sabotage and the restore confirmed by shasum -a 256 -c, because this worktree is shared and a sabotage left behind breaks the board for everybody. D-956.* Left open and named in the delivery document: the ## Status line counts 307 slices while 315 are named here, and W375 and W376 were deliberately NOT given delivery documents to move them into the table, because the reader's question is answered correctly without them and their substance is already at length in D-952 and D-953. *Evidence: 1 design doc.*
W380The send no harness could make succeedcompleteW374'S OWN SUITE NAMED THIS AND REFUSED TO SMUGGLE IT IN: *to measure this branch a test seam would have to be added to the one file where mail leaves the building; that is a separate slice with its own blast radius*. This is that slice. Measured first: three files in api/src posted to a hardcoded api.brevo.com, W374 stood at 21 passed and 1 SKIPPED, and the skipped branch is the one that MUTATES -- the count moves, the credential the person already holds is destroyed, and a replacement is minted. ★★ THE LOOPBACK RESTRICTION IS THE WHOLE DESIGN, NOT A PRECAUTION. Every Brevo request carries api-key: <BREVO_API_KEY> in a header, so a base URL able to name any host is not a convenience but a CREDENTIAL EXFILTRATION PRIMITIVE: anything that could set that var on a production Worker would be handed the live key on the next send, to a server of its choosing, with no error and nothing in the app to show for it. Restricting it to 127.0.0.0/8, localhost and ::1 means a Worker carrying the var cannot send the key off the machine. ★ AND IT REFUSES RATHER THAN FALLING BACK, because quietly ignoring a bad value and posting to the real Brevo would have an environment sending REAL MAIL while its operator believes it is pointed at a stub -- the same direction NOTIFY_REDIRECT_TO fails in. Resolved at FACTORY time, so it surfaces as the MailerNotConfigured every caller already separates from a retryable send failure. ★ THE THIRD CALL SITE WAS INCLUDED THOUGH NOTHING WAS BLOCKED ON IT: index.ts fetches inbound attachments with the same key, and mailer.ts has claimed since W12 that *everything Brevo specific lives below this line and nowhere else in the codebase*. With a third hardcoded host in the tree that sentence was decoration; grep -rn "api.brevo.com" src/ now returns brevo-base.ts alone. It cost two lines because the surrounding loop already records a failure per attachment instead of throwing. ★★ THE STUB HAD TO BE ABLE TO REFUSE, AND THAT IS WHAT UNCOVERED A HIDDEN DEPENDENCY: W374 sections 2 to 6 all measure a send that FAILED, and they got their failure from a deliberately invalid key reaching the real api.brevo.com over the internet -- verified by curl, 401 -- so six assertions in an acceptance suite rode on a live third-party round trip, and on a machine with no network the mailer throws instead and records a different string. The stub refuses by default, which makes them hermetic, and section 7 flips it to accept. *Evidence: api/test/brevo-base.test.ts 10 assertions; the obfuscation cases are load-bearing, since http://localhost.evil.example and http://evil.example#127.0.0.1 each CONTAIN a loopback spelling and are not one, which is what tells a hostname comparison from a string comparison. Forced red three ways moving DISTINCT sets: writing the check as override.includes("localhost") reds 3, falling back instead of refusing reds 3, dropping the path check reds 1. w374-acceptance.mjs 21/0/1 to 28/0/0, section 7 built on Bela, whose link section 2c had already proved SURVIVED an earlier chase, so 7e is a before-and-after on THE SAME CREDENTIAL. Forced red three ways: freezing chase_count reds 7d alone; revoking without minting reds 7f alone WHILE 7e STILL PASSES, which is the point, since destroying a credential and delivering no replacement is a distinct defect from not revoking; and making the seam ignore the override returns the suite to exactly 21/0/1, proving the SKIP fallback is real rather than decorative. Battery 181 passed, 0 FAILED, 0 did not run, 0 unmeasured, 5021 assertions, exit 0 -- the one remaining skip is w31's documented SQL-layer pair. api unit tests 632/632, up from 622. tsc clean. On production: version 150defa1 serving 100%, and the pre-auth crew surface routes -- /crew?token=<invalid> answers 404 *this link is not valid any more* while a route that does not exist answers 401, which is exactly why a 401 proves nothing here. BREVO_BASE_URL appears nowhere in wrangler.jsonc and nowhere in the deploy's own printed var list, so production carries no override and behaves as before. D-957.* ★★ AND IT CAUGHT A REGRESSION THE BATTERY HAD HIDDEN BEHIND A SKIP. w253 records a reason per failed attachment and asserts it names one of *the two causes this environment can actually produce*; the seam introduced a third, a connection refusal, which that assertion correctly refused. It was invisible in the battery because w253 skips itself entirely without INBOUND_EMAIL_SECRET in the RUNNER process. The fix was to serve it the same stub rather than widen the pattern to accept anything -- w253 is 30/0/0 -- and running the battery WITH the secret exported took it from 4935 assertions to 5021. *Evidence: 1 design doc.*
W381The guest-list send that succeeds, and the message id nobody had assertedcompleteW380 RECORDED THIS AS UNBLOCKED AND NOT DONE. THIS IS THE DONE. w293 section 5 covers the bulk guest-list send and every one of its branches measures a send that FAILED. Its 5d is the giveaway: rows.every(r => r.outcome === "sent" || (r.failure_text ?? "").length > 0) -- the left half has never been true, and the right half asks only that a failure carries text, so it has passed identically over three quite different things: a provider 401, a host that could not be reached, and, before W380, a live round trip to the real Brevo over the internet. ★★★ THE SUCCESS BRANCH WRITES A COLUMN NOTHING HAD EVER LOOKED AT AND IT IS THE ONE THAT MATTERS: provider_message_id is the only handle anybody has when a guest says the invitation never arrived. Section 5's own query already selects it. If the mailer stopped returning it, or the route stopped storing it, nothing anywhere would have noticed. *Evidence: w293-acceptance.mjs 36 to 41, section 7 flipping W380's stub to accept. 7b is a positive control ON THE STUB, because 7a alone passes against a route that reports a send it never made. Forced red three ways moving DISTINCT sets: storing null instead of result.messageId reds 7c alone; writing the failure reason unconditionally reds 7d alone; making the seam ignore the override returns the suite to exactly 36/0/1, its pre-slice shape. Battery 181 passed, 0 FAILED, 0 did not run, 0 unmeasured, 5026 assertions. tsc clean. NO DEPLOY AND NONE NEEDED: the only changed file is a test, so production runs the same bytes it ran after W380, version 150defa1. D-958.* ★★ TAKEN WITH W380 THIS IS ONE FINDING AT TWO REMOVES. A guard that can only observe ONE outcome passes over any number of different causes for it, and the loose assertion is invisible precisely because it is green. The measurement that exposed both was the same question: what would this assertion still accept if the thing under it changed? A disjunct whose left half is unreachable reads as covering both branches and covers one. *Evidence: 1 design doc.*
W382The pass that could be issued and never scannedcomplete★★★ accred.pass_decision IS A FINISHED DECISION ENGINE ON PRODUCTION WITH NO CALLER ANYWHERE IN THE WORKER. Measured before a line was written: 24 tables in accred, every one RLS-protected and every one 0 rows; 8 of them with zero references in api/src; 0 callers of a function returning nine deny grounds, twelve once pass_ || status is expanded, and STABLE so it decides without recording. accred.scan_event is append-only and carries a BEFORE INSERT trigger writing one crowd.count_event per ALLOWED scan -- the whole of D-923's argument, built and never fired. W346 could ISSUE a pass and nothing could SCAN it. ★★ AND THE TICKET DOOR COULD NOT DO IT EITHER: app.resolve_ticket selects from app.event_ticket and nothing else, measured on production, so a pass at /checkin/scan has always been an unknown code. D-923's join is at the LEDGER, where both paths feed crowd.count_event, and never at the lookup. ★★★ THE EVENT ON A DOOR COMES FROM THE BAND AND NEVER FROM THE BODY, which is FINDING-2026-09-08 applied rather than re-learned: the policy constrains the new row's OWN tenant_id and the foreign key is checked with row security BYPASSED, so an honest tenant with a foreign access_zone_id is exactly the write that slips through. The insert selects THROUGH the RLS-scoped band. ★★ AND THE CROWD PASSAGE IS CHECKED AGAINST THE EVENT, WHICH IS NOT A TENANCY CHECK: a door wired to another EVENT's passage would feed that event's fire-safety figure with people who never entered it, inside one tenant, where no policy is looking -- and an unresolvable passage REFUSES rather than writing the door with a silent null, which would leave the crowd figure quietly short by everybody who walked through. ★★ THE REFUSAL VOCABULARY IS ASSERTED IN THREE LAYERS because the sibling map for the ticket door SHIPPED INVENTED, six keys that could never fire and six real grounds with no sentence: web asserts the sentence map against api/src/accred/passReasons.ts in both directions, and the acceptance suite asserts that union against WHAT POSTGRES ACTUALLY RETURNS by driving every ground until it fires -- the middle layer is what earns the first, since the real authority is a plpgsql body in the spine repository a web suite cannot read. ★ TWO SCREENS AND NOT ONE, because the nav model's own rule is that a tab needs the SAME PERMISSION and Contributor holds accreditation.scan but not accreditation.manage: the doors are a tab for the planner, the scan is a row for the marshal, which is the Gates/Door split the model already made. *Evidence: api/test/w382-acceptance.mjs 28/0/0, forced red by four sabotages moving DISTINCT sets -- taking the event from the body reds the cross-tenant group; not recording a refusal reds 4b and 8d; recording a refusal as allow so the trigger counts it reds 8c, the assertion a fire officer relies on; dropping the passage check reds 2d and 2e alone. Battery 183 suites, 182 passed, 0 FAILED, 0 did not run, 0 unmeasured, 5054 assertions, exit 0; api 632/632; web 353/353; tsc clean. On production: api 0bd3e969 at 100%, web 01bf98ed, the pre-auth crew surface answering 404 while a route that does not exist answers 401, and the served index-C1fldt2I.js carrying *Kártyakapu* and *Beléptetőpontok* under a Cache-Control: no-cache HEADER with a never-written control string at 0. No migration: every table already existed. D-959.* ★★ AND IT FIXED A SUITE THAT FAILS ONE DAY A MONTH: w226 set next_issue_on = current_date - 40, which lands on the first of last month whenever today is the 10th or 11th -- the period its own fixture has already billed -- so the resumed run is correctly skipped and 5b reads as a broken generator. Measured on 2026-09-10 giving 2026-08-01 against monthsAgo(1) of 2026-08-01; the same suite passed hours earlier on 2026-09-09. ★ AND IT NEARLY SHIPPED A CALL TO A ROUTE THAT DOES NOT EXIST: the first doors tab fetched /crowd/passages, which the Worker never served, and nothing would have caught it -- the reachability checker measures routes no screen calls and never a screen calling a route that is not there. Recorded as the next slice. *Evidence: acceptance suite, 1 design doc.*
W383A screen may not call a route the Worker does not servecomplete★★★ route-reachability.test.ts ASKS WHICH SERVED ROUTES NO SCREEN CALLS, AND NOTHING ASKED THE REVERSE. Ten tests, all in one direction; 0 anywhere measuring screens to routes, against 431 apiGet/apiPost call sites and 330 distinct static prefixes. A screen calling a path the Worker never serves gets a 404 and renders as an EMPTY LIST or an error toast, which reads as a data problem rather than a typo, and nothing in the type system can see it because both sides are strings. ★★ IT IS NOT HYPOTHETICAL: W382's doors tab was written calling /crowd/passages, which does not exist -- the passages come back from /crowd/zones -- and it was caught BY HAND an hour before it would have shipped as a dropdown that was always empty. ★★ THE RULE IS THE STATIC PREFIX, everything before the first ${ or ?, and matching the whole literal is what a naive scanner gets wrong: a template can NEST, AssetsScreen really calls /memberships/pickable with a conditional query built from an inner template, and a quote-balancing regex truncates that into a path existing nowhere and reports a defect that is not there. ★ THREE CALLS BUILD THEIR WHOLE PATH FROM A BASE and have no static prefix; the test COUNTS them and fails if that number grows, rather than reporting a clean sweep it did not make. *Evidence: web/test/screen-calls-a-real-route.test.ts, 5 assertions, 0 unresolved on the current tree. Forced red two ways: planting /crowd/passages back into the doors tab reports the path AND the file; making served() permissive reds the negative control, which is how this guard would otherwise go quietly green over anything. Two further controls ship with it -- the call list must exceed 300 and the served list 400, so a renamed helper cannot produce a clean sweep over nothing, and three real call shapes are asserted to still resolve, guarding the opposite failure of a matcher so strict it reports everything. web 358/358; battery unaffected and re-run anyway at 183 suites, 182 passed, 0 FAILED, 0 unmeasured, 5054 assertions. Test-only: nothing to deploy. D-960.* The two checkers were NOT merged: they read the same two trees and ask opposite questions, and one file answering both would be 500 lines whose failure message has to say which direction it is talking about. *Evidence: 1 design doc.*
W384The accreditation request, and the decision that is not this route'scomplete★★★ THE DESIGN WAS ALREADY WRITTEN DOWN, IN THE MIGRATION. accred.request was one of the seven lifecycle tables with NO WRITER anywhere in the Worker, measured the same night. The obvious build is a state machine over its ten statuses, and W209 refused that in advance in a comment above the table: *the lifecycle runs on the Approval Engine rather than on a state machine of its own. M-23 already models steps, approvers, due dates and decisions.* approval_id is the pointer that says so, and app.approval with its four tables, five routes and an approver's own queue is built. So this slice builds a request that can be REGISTERED and SUBMITTED and nothing that decides one -- a second decision path would be a second answer to *has this been decided*, with no approver, no due date and no record of who decided, which is D-923's refusal about occupancy in a third place. ★★ THE EVENT IS PINNED TO THE CATEGORY'S, never taken from the body: accred.request is one of the 36 whose policy constrains the new row's own tenant_id and never the event, and the foreign key is checked with row security BYPASSED. W346 pins the pass this way and W382 pinned the door; this is the third, and the finding written hours earlier predicted it. ★★★ AND THE QUEUE RETURNS WHETHER A DOCUMENT IS ATTACHED, NEVER THE POINTER. photo_file_id and identity_document_file_id are Article 9 and 10 material; accreditation.view_identity_document is held by Admin and Tenant owner alone while the queue is gated on accreditation.view, which five roles hold -- so returning the file id would route that material past the narrower permission without anybody deciding to. The assertion checks the response object's own KEYS, not just its values. *Evidence: api/test/w384-acceptance.mjs 23/0/0, forced red by three sabotages moving DISTINCT sets: adding approve and reject as ordinary moves reds the whole decision group 5a-5d; taking the event from the body reds 2a, 2b and 3d; returning the document pointers reds 3e alone. Battery 184 suites, 183 passed, 0 FAILED, 0 did not run, 0 unmeasured, 5077 assertions; api 632/632; web 358/358; tsc clean. On production: api a6ef6798 at 100%, web 4d0f15d9, the pre-auth crew surface answering 404 against a nonexistent route's 401, and the served index-CXJcJ_lx.js carrying *Igénylések* and *Döntésre küld* under a no-cache HEADER with a never-written control at 0. No migration. D-961.* ★★★ AND A SABOTAGE THAT FAILED TO FAIL CLOSED A GAP IN THIS SUITE. The first draft of the decision assertions tried approve while the request was still invited and asked only for a 400 -- so the sabotage adding approve from requested was refused ON STATE GROUNDS, returned 400, and the suite stayed green over the exact defect it exists to catch. It is driven from requested now, the one state where that move would be legal, and asserts the refusal names an UNKNOWN ACTION rather than a wrong state: a negative assertion has to name the rule, or any refusal satisfies it. Left open: the photo and identity document, because request_set_erase_after is switched off until the owner sets a retention period and attaching documents first would create material with no erasure date; screening; and the submission does not yet CREATE the approval row, which needs a per-event approver list nothing holds. One judgement recorded for the owner: an approval's approval_type is constrained to six values and accreditation is closest to protocol. *Evidence: acceptance suite, 1 design doc.*
W385The request goes for a decision, and the decision is never copied backon production★★★ THE OUTCOME IS NEVER MIRRORED, AND THAT IS THE SLICE. W384 built the accreditation request and stopped at the edge of the decision because W209 put the lifecycle on the Approval Engine. POST /accreditation/requests/submit raises an app.approval over the request with the approvers the caller picks, links approval_id and stamps submitted_at -- the same engine and the same call RequirementsScreen already makes, so whoever is chosen sees it in THEIR OWN /approvals/mine. accred.request.status keeps the APPLICANT track and the approval keeps the DECISION; the queue joins and reads it every time it is shown. Copying it is the obvious build and it is two records of one decision, which disagree eventually. Measured before deciding: nothing in this Worker reads request.status except its own route, so there is nothing that needs a copy. ★★ AND IT NEEDED A MIGRATION FOUND BY RUNNING RATHER THAN BY READING: app.approval.subject_type is NOT a CHECK constraint, it is a case inside the trigger function app.approval_subject_exists(), so a query over pg_constraint reported the column as free text -- which is what the first measurement concluded, and EC456 at runtime is what settled it. One arm added, following the two that added crowd_alert and pass_design, applied to local, dev and production. ★★★ THE PROBE ASSERTS EC457 AND NOT SUCCESS, which is the whole reason it is the right probe: a MISSING arm falls through to else and raises EC456, and an arm that does not COUNT leaves n null so the insert SUCCEEDS with an approval pointing at nothing. Only an arm that exists and counts produces EC457. ★★ AND THE ROUTE ANSWERED A REFUSAL AS 503 retry:true in its first draft -- the class closed on five routes on 9 September, back within a day, now wrapped in asRefusal exactly as /approvals/new wraps the same engine. *Evidence: w384-acceptance.mjs 23 to 31, forced red two ways moving DISTINCT sets -- mirroring the decision into status reds 7e and 7h, taking approvers on the caller's word reds 7b, 7c and 7d. The migration probe forced red by removing the arm, reporting *Expected EC457 ... got EC456*, and the ledger probe forced red the same way on local reporting MISSING. Battery 184 suites, 183 passed, 0 FAILED, 0 unmeasured, 5085 assertions; api 632/632; web 358/358; tsc clean; migration ledger 419 probes, exit 0. On production: api d8c513ac, web 7d2d18ba, crew surface 404 against a nonexistent route's 401, and index-UMdmGwlH.js carrying *Jóváhagyók* under a no-cache HEADER with a control at 0. D-962.* ★★★ A PROCESS FAILURE WORTH MORE THAN THE SLICE. A sabotage was restored, the restore was VERIFIED BY CHECKSUM, and the file was found carrying that sabotage several steps later. Everything measured in between was measured against sabotaged code, including a battery run reported as a failure and a conclusion written down as *confirmed: a foreign approver is accepted, a real defect in my route*. It was not. A checksum verified at the moment of restore says nothing about the file at the moment of the gate: re-check by CONTENT, grepping for the sabotage's own marker, immediately before the run that matters. *Evidence: 1 migration, 1 design doc.*
W386The pass door, on a phonecompleteW382 gave the pass a door any hardware reader can drive. This is the same door held at arm's length, and it adds no route: it calls /accreditation/scan, which W382 built and whose suite covers every one of the engine's twelve grounds. ★★ NOTHING HERE IS A SECOND DECODER: useQrCamera holds the one copy of the camera, the iPhone jsQR fallback, the torch and the same-code debounce, which W352 moved into a hook precisely so a fix lands once -- camera code being exactly what no harness here can cover. This is a LAYOUT over that hook, sharing scan-* with the ticket phone door because they are the same object held in the dark. ★★★ AND IT IS A SEPARATE SCREEN FROM THE TICKET PHONE DOOR, for D-923's reason unchanged: app.resolve_ticket cannot see a pass and never could, so a marshal holding a phone has to know which door they opened -- one surface answering both would be one lookup answering both. Separate from the pass DESK door too, for W336's reason: a phone and a reader on a table are one person choosing a tool, and the choice belongs in the navigation. *Evidence: web 358/358, tsc clean, and screen-calls-a-real-route.test.ts -- W383, written hours earlier -- confirms the one path it calls is served. On production: web 723ee06d, the served index-DsHydgRV.js carrying *Kártyakapu telefonon* and *Beolvasás indítása* under a no-cache HEADER with a never-written control at 0. D-963.* Deliberately no new unit test and that is not a gap: the decision is asserted in w382-acceptance.mjs against the real database and the refusal words in pass-reasons.test.ts in both directions against the engine's own union, so a test here would restate two existing guards. The scanning stage cannot be driven in this harness -- it needs a camera -- which is the ticket phone door's documented limitation and the reason the decoder has one copy. *Evidence: 1 design doc.*
W387The pass that was lost, and the one that replaced itcomplete★★★ THE ENGINE WAS COMPLETE AND ITS ONLY MENTION IN THE WORKER WAS A COMMENT. W346 wrote *the way to give somebody access again is accred.reissue, which records that the second card replaces the first* -- and grepping that table name returned exactly that sentence and nothing else. Meanwhile the database held the table, both its vocabularies, the reissued status value, and accred.reissue_revokes_the_old_pass, a trigger setting the old pass to reissued in ONE STATEMENT so there is no window in which both cards work -- whose own comment says status is read live by accred.pass_decision(), which had no caller at all until W382, four hours earlier. ★★★ THE REPLACEMENT COPIES THE OLD PASS'S FROZEN BANDS AND NEVER RE-DERIVES THEM FROM THE CATEGORY, which is the whole care of the slice and the thing the obvious implementation gets wrong. W346 freezes pass_zone AT ISSUE so widening a category tomorrow cannot widen plastic already in a pocket, and re-deriving undoes that SILENTLY IN EITHER DIRECTION: a category narrowed since issue quietly demotes somebody who only lost their card, one widened since hands them access nobody granted. Each band keeps its own source, so an override is not laundered into a category default by being reissued. ★ AND THE SCREEN ALREADY TOLD PEOPLE TO DO IT BY HAND: the revoke panel said *revoke this one and issue a replacement*, a workaround that loses both the frozen bands and the record of which card replaced which. *Evidence: api/test/w387-acceptance.mjs 18/0/0. Section 1 is a positive control -- the old card opens a REAL DOOR before the reissue -- without which every assertion below passes against a card that never worked; 2c then shows it REFUSED at that same door with ground pass_reissued, which is the only thing that shows the revocation means anything and is measurable only because W382 gave pass_decision a caller. Section 3 narrows the category between issue and reissue, so a route that re-derived would hand back one band instead of two and look entirely reasonable doing it. Forced red two ways moving DISTINCT sets: re-deriving from the category reds 3b, 3c and 3d; making revoked and reissued non-terminal reds 4a and 4b. Restores verified BY CONTENT scoped to the route, because the naive markers appear in W346's issue path too. Battery 185 suites, 184 passed, 0 FAILED, 0 unmeasured, 5103 assertions; api 632/632; web 358/358; tsc clean. On production: api 97d3961b, web c3e83b98, crew surface 404 against a nonexistent route's 401, and index-lHmkAQsg.js carrying *Kártya cseréje* under a no-cache HEADER with a control at 0. No migration. D-964.* ★★ A FIXTURE CONTROL CAUGHT A FIXTURE, NOT A ROUTE. 4b read as a route defect on the first run -- a revoked pass appeared reissuable -- and the revoke had SILENTLY FAILED, because that route requires a reason and the fixture had not supplied one, so 4b was reissuing a live pass. One assertion on the fixture named it immediately. Left open: new_pass_id is nullable and this route always fills it, so the two-step flow a big accreditation centre uses -- declare the loss now, print the card later -- stays available and unbuilt. *Evidence: acceptance suite, 1 design doc.*
W388The vest that adds a band, and the one handed backcomplete★★★ A SENTENCE THAT HAD BEEN TRUE AND UNREACHABLE IN TWO WAYS AT ONCE. accred.pass_decision's own comment has said since W211 that *the band may be held by the pass OR by a live accessory stacked on it, and from here the two are indistinguishable, which is the point* -- D-743. It calls accred.pass_effective_zones, the pass's FROZEN bands UNION every live accessory that grants one. Both existed. And accred.supplementary had 0 references in api/src, accred.pass_accessory 0, both tables 0 rows on production, and pass_decision itself had no caller at all until W382: nothing could issue an accessory, and nothing asked the question one would have answered. ★★★ THE PROOF IS A ROUND TRIP AT A REAL DOOR, NOT A ROW COUNT. Section 2 refuses a pass at a grid gate on zone_not_on_pass, issues a vest granting that band, watches THE SAME PASS ADMITTED with nothing about the pass changed, hands the vest back, and watches the same pass REFUSED AGAIN on the next scan -- a sequence no route that merely writes rows can satisfy. D-741 and D-742 stay in the function and are not re-implemented here: only issued counts and it is read live, so a bib handed back shuts the grid at the next scan rather than at some sync, and the union joins through pass_id, so a pooled row grants nobody anything. ★★ THREE SEPARATE WAYS TO POINT AT ANOTHER TENANT'S ROW -- a band, a time window and a category are each an FK checked with RLS BYPASSED while the policy constrains only this row's own tenant -- all three resolved through an RLS-scoped read pinned to the event. The quota and the category restriction are the DATABASE'S: supplementary_quota_holds raises EC471 and refuses over quota, and both surface through asRefusal in the database's own words rather than as a 503 of ours. The list counts live rows only, because a count of everything ever issued says a quota is spent twice over while half of it is back on the shelf. *Evidence: api/test/w388-acceptance.mjs 17/0/0. 2a is a positive control -- the pass is refused at the gate BEFORE anything is issued, without which every assertion below passes against a pass that could already get in. Forced red two ways moving DISTINCT sets: not marking a returned accessory returned reds 2d, 2e and 3a; dropping the event pin reds 5a and 5b alone. Restores verified BY CONTENT. Battery 186 suites, 185 passed, 0 FAILED, 0 unmeasured, 5120 assertions; api 632/632; web 358/358; tsc clean. On production: api d558dda9, web 0fd1375f, crew surface 404 against a nonexistent route's 401, and index-Be5WsmzX.js carrying *Kiadópult* and *Felszerelés* under a no-cache HEADER with a control at 0. No migration. D-965.* ★★ AND W383 EARNED ITS KEEP SIXTEEN HOURS AFTER IT WAS WRITTEN. The store counter was written calling GET /accreditation/accessories, a route that did not exist yet, and the guard reported it by path and by file on the first run -- exactly as it had reported the /crowd/passages that prompted it. The route was built rather than the call left to 404 quietly. An upgrade is refused without a time window in words before the database raises, because an unbounded upgrade is a permanent promotion, which is a category change and not an item of kit. *Evidence: acceptance suite, 1 design doc.*
W389The desk where the plastic is handed overcomplete★★★ A DOOR THAT LOCKED FROM BOTH SIDES, AND NO KEY ANYWHERE. accred.profile is the event's accreditation policy and it is read by FIVE database functions -- pass_decision, pass_requires_approved_design, pass_requires_consent, pass_validation_required and request_set_erase_after -- and written by NONE, with 0 rows on production. The fourth is a trap: an event whose validation_required is on may mint a pass ONLY as pre_valid (EC473); accred.pass_decision then refuses that pass at EVERY door on not_validated; and leaving pre_valid raises EC474 unless an accred.validation row with result = activated ALREADY EXISTS -- and accred.validation had 0 references in the Worker. So an event that switched validation on would have had every pass on it PERMANENTLY DEAD with no route out anywhere in the platform. This slice builds the exit; the switch gets its route later, which is the right order. ★★★ AND DRIVING THE OLD WAY OUT FIRST FOUND A LIVE DEFECT. Assertion 2b tries /accreditation/passes/status with activate -- the only other route that moves a pass -- and it answered 503 service temporarily unavailable, retry: true: EC474, a deliberate permanent refusal, reaching the operator as *the service is down, try again*. It never would have worked. The exact class D-962 closed on five routes the day before, sitting in a route three days old, found only because the suite tried the old path before building the new one. Fixed here with asRefusal. ★★ THE RECORD AND THE ACTIVATION ARE ONE TRANSACTION IN THAT ORDER, because EC474 reads the table on the UPDATE and two calls would let a desk leave a pass holding an activated validation and a pre_valid status -- a contradiction nothing else in the system would notice. ★★ THE VALIDATOR IS THE CALLER AND NEVER THE BODY: this table exists to record who checked whose identity, and a desk that can name somebody else as the checker is not a record, it is a rumour. *Evidence: api/test/w389-acceptance.mjs 32/0/0. Positive controls: 0b proves the trigger is live on this database at all, 2a proves the pass is refused at its own band's door before anything is issued, and 6b is the isolating one -- an identical event with NO profile row admits its pre_valid pass, so 2a's refusal came from the switch and not from the status. Forced red three ways moving DISTINCT sets: recording without moving the pass reds 2e and 2f; taking the validator from the body reds 4b; dropping the event pin reds 6c and 6d. Battery 187 suites, 186 passed, 0 FAILED, 0 unmeasured, 5152 assertions; api 632/632; web 358/358; tsc and build clean. Both screens opened against the local Worker before shipping, with GET /accreditation/centres answering 200 from the tab itself. On production: api c2dcb203, web 7ec3227c, crew surface 404 against a nonexistent route's 401, and index-DooN0X_G.js carrying *Validáló pult*, *Akkreditációs központok*, *Kártya átadása* and *Csapatszálloda pultja* under a no-cache HEADER with a control at 0. No migration. D-966.* ★★★ A SABOTAGE THAT FAILED TO FAIL, AND WHAT IT EXPOSED. Dropping and event_id = ... from the pass lookup -- the cross-tenant shape this project has met four times -- left the suite ENTIRELY GREEN. Section 5 presents tenant B's real pass and asserts a refusal, and it passes either way, because row security has already filtered that row out of the SELECT: section 5 was measuring RLS, not the route. The pin's actual job is the case NO POLICY IS LOOKING AT -- our own pass, from our own OTHER event -- and 6c and 6d are the only assertions in the file that fail when it is gone. ★★ A MIGRATION WRITTEN, APPLIED NOWHERE, AND DELETED. The first draft added an append-only trigger to accred.validation, reasoning from role_table_grants: app_rw holds UPDATE and DELETE, so the record looked rewritable. It was already append-only on all three databases -- validation_append_only since W211, beside the identical triggers on accred.scan_event and accred.terms_acceptance. A grant says nothing about a trigger. Section 7 is what should have been run first, so it runs every time now. ★★ AND W346 NAMED THE ROW THREE NIGHTS EARLY: the comment on the accreditation surface has said since then that *the counter with a queue is accred.centre, which has no screen yet -- and THAT is the surface that will deserve a row of its own, with its own operator.* It has one, and the argument did not need making twice. Centres is the SIXTH tab, which is the convention's cap exactly, and the next slice wanting a seventh has to make W27.3's argument rather than add one. *Evidence: acceptance suite, 1 design doc.*
W390The event's accreditation policycomplete★★★ FOUR OF EIGHT COLUMNS DO SOMETHING, AND THE SPLIT WAS MEASURED. accred.profile carries thirteen columns and had no route. Over pg_proc.prosrc on production: validation_required is read by pass_decision and pass_validation_required; design_approval_required by pass_requires_approved_design (EC475); required_consents by pass_requires_consent (EC470); personal_data_retention_days by request_set_erase_after (D-739). screening_required, data_residency_region, authority_export_enabled and authority_owner are read by NOTHING, and are not on the screen and not accepted by the route: a switch that changes nothing is worse than an absent one, because somebody sets it and believes it. ★★★ AND A FIFTH IS HELD BACK, WHICH IS W389'S RULE APPLIED A SECOND TIME. design_approval_required IS read by an engine, so by the rule above it belongs -- and accred.pass_design has 0 writers in the Worker and 0 rows on production, so has_approved_design is false for every category on every event, and turning it on would raise EC475 on EVERY pass issue for that event with nothing anywhere able to approve a design. The same trap validation_required was. When a policy switch and the thing that satisfies it are both unbuilt, build the SATISFIER first: shipping the switch first arms something nobody can disarm, and it always looks like the smaller slice. Both remaining switches have an exit -- W389's desk, and /accreditation/consent since W342 -- and section 5 asserts the third is NOT writable, so nobody restores it by accident. ★★★ A COLUMN DEFAULT THAT HAD TO BE DEFEATED. personal_data_retention_days defaults to 90. D-739 is *no period, no stamp, no erasure, switched off until the owner sets it and visibly so*, and it holds today ONLY because no profile row exists on any event: the moment one is created for an unrelated reason, the default arms a 90-day erasure of Article 9 material on a date nobody chose. The insert names the column and writes NULL. 2c reads the COLUMN rather than the response, because a route can report null while the database holds 90. ★★★ AN ARRAY THAT READS BACK AS A STRING, FOUND BY OPENING THE SCREEN. The panel said consent was required on an event whose list was empty: on workerd postgres.js runs with fetch_types off and a text[] arrives as the STRING '{}' -- length 2 -- and the checkbox handler SPREADS that value, which spreads a string into characters. Every assertion in the suite passed while this was true, because they all read the column through the owner connection in Node, where the array decodes correctly. This codebase had the WRITE direction of that trap written down since the artist profile; the read direction produces plausible wrong behaviour instead of an error. Both projections go through to_jsonb now, and two assertions read the ROUTE'S RESPONSE. *Evidence: api/test/w390-acceptance.mjs 27/0/0, forced red four ways moving DISTINCT sets: the retention default reds 2c and 2d; making design_approval_required writable reds 5a; clobbering an absent field reds 4e; projecting the array raw reds 2c-2 and 4b-2. Controls: 2a asserts the 90-day default still EXISTS, 5b asserts accred.pass_design is still empty so 5a's reasoning still holds, 4d shows the same pass call succeeding once both consents are recorded, 1b shows the GET creates no row. Battery 188 suites, 187 passed, 0 FAILED, 0 unmeasured, 5179 assertions; api 632/632; web 358/358; tsc and build clean. The panel was DRIVEN IN A BROWSER before shipping, which is how the array defect was found at all. On production: api 3841594d, web 0b87d8b2, crew surface 404 against a nonexistent route's 401, and index-CC1l-YJu.js carrying four Hungarian strings under a no-cache HEADER with a control at 0. No migration. D-967.* ★★ THREE ROUTES FIXED THAT THIS SLICE DID NOT WRITE, AND 28 MORE MEASURED. POST /accreditation/passes answered every database refusal as 503 retry: true -- including EC470, whose message NAMES the missing consent form and whose own comment says *this fires at a production desk with a queue behind it, and the operator has to know which form is missing*. The route threw that sentence away and said the service was down. /accreditation/passes/reissue had the same gap. 28 write routes across api/src touched a table with a coded raising trigger and had no asRefusal handler; this slice fixed one of them, leaving 27, listed by route and table in FINDING-2026-09-10-the-27-routes-that-answer-a-refusal-as-503.md. A guard on the event-write-anchoring.test.ts pattern is worth more than 27 edits, because this class reappeared within a DAY of D-962 closing it on five routes. ★ THE VOCABULARY MOVED INSTEAD OF BEING COPIED: CONSENT_TYPES now lives in web/src/system/consentTypes.ts, one list for two screens, with accred-consent-types.test.ts repointed at it -- two copies of a vocabulary drift, which is the defect that guard exists to prevent, one level up. ★ AND THE PANEL IS ON THE MATRIX, NOT A SEVENTH TAB, on the precedent set six days earlier by W349's entry policy living inside CheckinGatesScreen. *Evidence: acceptance suite, 1 design doc.*
W391A refusal is not a 503complete★★★ D-962 CLOSED THIS CLASS ON FIVE ROUTES ON 9 SEPTEMBER AND IT WAS FOUND THREE MORE TIMES THE NEXT DAY, on routes written days earlier, by two slices that were not looking for it -- W389 driving the old way out of pre_valid, and W390 asserting that EC470 named the missing consent form. A route calls an engine, the engine raises a deliberate PERMANENT refusal, and with no try/catch the caller is told {"error": "service temporarily unavailable", "retry": true}. The operator is told to try again and it never will work. EC470's message names the missing form ON PURPOSE -- its own comment says *this fires at a production desk with a queue behind it, and the operator has to know which form is missing* -- so the one sentence in the exchange written to be read by a person is the one thrown away. ★★ A GUARD, NOT TWENTY-SEVEN EDITS. Fixing instances leaves the next new route free to repeat it, which is exactly what happened inside a day; this project has recorded four times that *remember to edit the other file* is not a mechanism. Same shape and same argument as event-write-anchoring.test.ts. Measured on production: 26 tables in app, accred and crowd carry a trigger raising an EC-coded refusal; 45 non-GET handlers write one of them, 18 call asRefusal and 27 do not. All 27 are listed, each naming THE CODES IT WOULD SWALLOW -- app.file raises EC589, app.form_answer EC490 to EC494 -- so the list is a debt register rather than a list of things somebody decided were fine. Append-only guards are deliberately OUT: app.forbid_mutation raises without a code, a correct route never reaches it, and swallowing that as a refusal would hide a different bug. *Evidence: four assertions -- unlisted routes fail; a row that outlives its reason fails, so wrapping a route means DELETING its row; the count is a ceiling of 27 so the debt cannot grow; and a positive control. Forced red two ways: unwrapping /accreditation/passes reds THREE of the four; blinding the table list reds ONLY the positive control, which is the whole reason it is there -- everything else passes vacuously on an empty detector, the false green this project keeps finding, arriving on schedule. It also asserts BY NAME that the three routes wrapped on 10 September read as handled, so the detector is measured against a change somebody actually made. api 636/636, tsc clean. Test-only: no route, no screen, no migration. D-968.* ★ WHAT IT CANNOT DO, stated the way its sibling states its own limit: a listed route is not proven broken, an unlisted one is not proven right, and only driving a route settles either. What it does is stop the list growing silently. *Evidence: 1 design doc.*
W392The code in the SQLSTATEcomplete★★★ THE HELPER WAS HALF-BLIND, AND THE SCHEMA HAD ANTICIPATED IT. asRefusal turns a database refusal into its own sentence instead of a 503, and it matched an EC code IN THE MESSAGE TEXT. Five functions put their code in the SQLSTATE instead -- assert_known_time_zone, assert_budget_line_currency, assert_project_status_lifecycle, event_move_refuses_orphans and venue_type_keeps_primary -- with using errcode = 'EC388', which is a legal five-character SQLSTATE and the MORE careful convention, because the code survives somebody rewording the message. They fell past the helper even on routes that called it. And event_move_refuses_orphans carries a comment three lines above its raise saying a plain raise exception *would surface to the user as "something went wrong" for what is really an answerable question* -- it chose a custom SQLSTATE precisely to avoid that, and the Worker did it anyway. The author anticipated the failure mode, took the one step available, and the layer above read only the other field. ★★★ AND THE SUITE MEASURED THE EXPOSURE DOWN. Five routes driven against four rules, and FOUR of the five refuse at the ROUTE, in words, before the trigger can fire: /events/update checks pg_timezone_names itself, /projects/new checks the lifecycle itself, and both membership routes look a role up with retired_at is null. One did not: /events/new -- and its shape is the thing to carry, because it is the CREATE path whose own UPDATE sibling validates the same field. A create path has no existing row to validate against, so the guard tends to be written on the update path and not on the create. W391's guard lists routes by the tables they WRITE and says of itself that it *cannot say a listed route is broken in practice*; this is that sentence being true. The list is a place to look, not a defect register. *Evidence: api/test/w392-acceptance.mjs 11/0/0, forced red two ways moving DISTINCT sets: blinding asRefusal to the SQLSTATE reds 1b, 1c and 1d; removing the route-level time-zone guard on /events/update reds 1f alone -- and that second one is the interesting sabotage, because it makes the trigger the ONLY thing refusing and 1f fails only because it asserts a 400 without a code rather than merely "not a 503". An assertion that only ruled out 503 would have gone green on the 409 and told nobody the route guard had been deleted. Controls: 1a the same route succeeding with a real time zone, 1e nothing written, 2c /projects/new succeeding with a lifecycle-correct status, 2e no membership created. Battery 189 suites, 188 passed, 0 FAILED, 0 unmeasured, 5190 assertions; api 636/636; tsc clean. On production: api c23e9242, crew surface 404 against a nonexistent route's 401. No SPA change, so no bundle string to probe -- and the refusal path itself cannot be probed on production without a credential, which is stated rather than dressed up: the evidence is the suite driving the real Worker at this commit. No migration. D-969.* ★★ TWO ASSERTIONS PASSED FOR THE WRONG REASON BEFORE THEY WERE FIXED. The first draft asserted status !== 503 for the retired role and got a 400 *no such role in this workspace* -- a refusal on entirely other grounds, before the trigger -- and the same for /projects/new, which answered a route-level 400 in the trigger's exact wording. Both green, both measuring nothing. Each now asserts WHICH LAYER refused. ★★ SEVEN ROUTES WRAPPED AND THEIR ROWS DELETED from W391's register, which is what that register's second assertion exists to force -- the guard failed on the first run after the wrapping and named all seven. /memberships/invite already had a try/catch, handling EC282 by hand and rethrowing everything else, so it read as handled to a person and was not: the argument for a guard that reads asRefusal rather than catch. ★ THE THREE INVOICE ROUTES ARE RECLASSIFIED UNREACHABLE: app.invoice's codes come from webhook_on_transition, whose messages are about the WIRING being wrong. Those cannot fire on a correct trigger, a 500 is the right answer to a mis-wired one, and swallowing it as a refusal would hide a real fault. Register ceiling 27 to 20. *Evidence: acceptance suite, 1 design doc.*
W393The pass design, and the switch it unblockscomplete★★★ CLOSING THE LOOP W390 HAD TO LEAVE OPEN. W390 refused to offer accred.profile.design_approval_required because accred.pass_design had 0 writers and 0 rows, so accred.has_approved_design was false for every category on every event and turning the switch on would have raised EC475 on EVERY pass issue for the event with nothing anywhere able to approve a design. That was W389's rule applied a second time -- build the satisfier before the switch -- and this is the satisfier. Section 4 is the loop closing: turn it on, watch a pass refused with EC475, approve a design, watch the same call succeed. ★★★ THE STAMP IS A SYNC AND NOT AN APPROVE VERB. has_approved_design reads approved_at, so the outcome HAS to reach that column -- which looks like the mirroring W385 refused, and the difference is where the value comes from. /accreditation/designs/stamp approves nothing: one statement reads app.approval.state and sets approved_at when it says approved, clearing it otherwise. So an approval withdrawn or rejected since takes the stamp away on the next call, the route is idempotent, and no route anywhere can stamp a design that was never approved -- which is what pass_design_approved_names_its_run was already reaching for by refusing an approved_at with no approval_id. The schema insisted the stamp NAME its run; this makes it DERIVED from it. ★★★ AND EDITING A DESIGN CLEARS ITS APPROVAL. A design somebody approved and somebody else then re-drew is not the thing that was approved, and leaving the stamp would let new artwork through on an old decision, silently, at the printer. ★★ THE STAMP AND THE APPROVAL CAN DISAGREE AND THE LIST SAYS SO: out_of_step is computed in SQL, because a design approved last week whose approval was rejected today is still opening doors until somebody syncs it. *Evidence: api/test/w393-acceptance.mjs 23/0/0, forced red three ways moving DISTINCT sets: ignoring the approval's state reds 3a and 3d; keeping the stamp through an edit reds 3e and 4b; dropping the event pin reds 1f and 1g. Battery 190 suites, 189 passed, 0 FAILED, 0 unmeasured, 5213 assertions; api 636/636; web 358/358; tsc and build clean. The designs panel was DRIVEN IN A BROWSER and created a design through the real route. On production: api fce720ee, web 8442103c, crew surface 404 against a nonexistent route's 401, and index-CuOgPWt6.js carrying four Hungarian strings under a no-cache HEADER with a control at 0. No migration. D-970.* ★★★ A SABOTAGE THAT FAILED TO FAIL, FOR THE THIRD TIME. Dropping and event_id = ... from the category lookup left the suite GREEN: tenant B's category is refused whether or not the route pins the event, because row security has already filtered it out of the SELECT. 1f and 1g drive our own category from our own OTHER event -- the case no policy anywhere is looking at -- and they are the only assertions that fail when the pin is gone. W389 found this on accred.validation, W392 found the same shape in its status assertions, and here it is again: every event-pin assertion in this codebase needs the same-tenant sibling, and the cross-tenant one measures row security and should say so. ★★ AND W390'S OWN SECTION 5 WENT RED ON THE FIRST BATTERY AFTERWARDS, EXACTLY AS BUILT. It asserted the switch was NOT writable -- true when W390 shipped, false the moment this slice landed. It now asserts the new truth PLUS the condition that made the change legitimate: that a route which can create a pass design exists. An assertion about a deliberate absence is worth writing precisely because it fails when the absence ends. ★ creative AND NOT protocol: W385 raises a protocol approval over a request because that is a decision about a person; a pass design is ARTWORK, and approval_type_check has a value for exactly that. W213 put pass_design in app.approval_subject_exists in August, so the engine has expected this for weeks. *Evidence: acceptance suite, 1 design doc.*
W394The refusal a browser could not read, and the net under the registercomplete★★★ THE SAME CLASS THREE TIMES IN ONE SLICE: A DELIBERATE REFUSAL WHOSE SENTENCE NEVER REACHED THE PERSON IT WAS WRITTEN FOR. (1) /crew/advancing/answer answered EC491 to EC494 as 503 retry:true. It is a PRE-AUTH surface -- somebody with no account, holding a link, answering a rider -- and CrewPortalScreen.tsx says in its own comment that it declines to restate those rules *because the triggers own them*. It could not have been fixed at the route as written: asRefusal was declared FOUR THOUSAND LINES BELOW the pre-auth routes, and a const above its declaration is a ReferenceError rather than a fallback, so the routes facing OUTSIDE the organisation were the only ones structurally unable to answer a refusal in words. Moved to module scope. (2) The EventNotYoursError clause in the outermost catch RETURNED its Response and so skipped the header loop three lines below it, so the one answer in this Worker with a sentence written to be read by a person was the one answer a BROWSER COULD NOT READ -- the SPA got a CORS failure and showed *Failed to fetch* where the Worker had said *this event is shared with you, so you can read it but not write to it*. Measured with curl before the change: no access-control-allow-origin on that 403, present on every other answer. The wrapper's own comment says, in capitals, that catching there is what makes every answer carry CORS *including the ones it gives badly*; a later slice added an early return past it. (3) THE NET. That same catch now calls asRefusal before its dbDown test. An EC raise IS a PostgresError, so every uncaught coded refusal was reported as a transient outage. D-962 closed that on five routes on 9 September, it reappeared twice within a day, and W391 measured 27 more -- one clause at the choke point covers every route this Worker will ever have, including the pre-auth ones. *Evidence: api/test/w394-acceptance.mjs 17/0/0, forced red four ways moving DISTINCT sets: blinding the net reds 3e and 3f only; reverting the CORS fix to an early return reds 3b alone; blinding both layers reds 1b, 1c, 1d, 1e, 3e and 3f. Battery 191 suites, 190 passed, 0 FAILED, 0 did not run, 0 unmeasured, 5230 assertions, receipt at commit 8143cd8 with a clean tree; api 637/637; tsc clean. On production: api 0f6f0900 (was fce720ee), the pre-auth crew surface answering *this link is not valid any more* against a deliberately invalid path answering *not found* with the path echoed -- both 404, so the STATUS proves nothing and the BODIES are the evidence. No SPA change and no new bundle string, so the probe establishes that the code deployed and the suite establishes that the refusal reads correctly. No migration. D-971, D-972, D-973.* ★★★ AND THE GUARD THAT WAS COUNTING THIS HAD TWO BLIND SPOTS, WITH THE SAME ROUTE FALLING THROUGH BOTH ENDS OF ONE. W391's detector started a block only on a line carrying url.pathname === AND req.method === together, and forty-eight route guards in index.ts do not have that shape. /crew/advancing/answer checks no method at all, so it was never a block start: its writes to app.form_answer were attributed to /venue-invite/save twelve hundred lines earlier, which does not touch a form table. The register carried a row for a route that could not be refused and no row for the pre-auth one that was, and the count still read 27, then 20. ★★ AND RAISING IS NOW MEASURED PER OPERATION, from pg_trigger.tgtype, which cuts both ways with one column: app.venue_venue_type's only coded trigger is BEFORE DELETE and the detector had never scanned a delete from, so /venues/type-proposals/decide was invisible; app.agenda_item's is UPDATE-only, so /agenda/items/new and /agenda/import/commit were listed for a refusal an INSERT cannot reach. Three routes off, three on, and a much more honest 20. ★★ A SABOTAGE THAT REDDENED NOTHING, AND IT IS A FINDING RATHER THAN A RECEIPT. Unwrapping /crew/advancing/answer with the net intact changes nothing a test can see -- section 1 measures the NET, not the route's own catch. The wrap is kept for a reason no assertion can state: reaching the net logs unhandled: through console.error, and on a pre-auth form a wrong answer is an ORDINARY EVENT, not a fault. Written into the suite header rather than papered over, because an assertion that passes for a reason other than the one in its name is the trap this project has now recorded five times. ★ THE ONE GENUINELY REACHABLE GROUP LEFT is app.budget_line/EC386, and it is reachable for a reason no route can see: the trigger fires on EVERY insert and update, so a budget whose currency was changed after its lines existed refuses every later touch of any line. 3e and 3f drive exactly that. Register ceiling 20 to 19, and every row now carries what was measured and when. *Evidence: acceptance suite, 1 design doc.*
W395The sanction that stops the passcomplete★★★ THE TABLE SAID IT MADE A RULE ENFORCEABLE AND NOTHING READ IT. w210 introduces accred.sanction saying it is *"the record that makes anti-touting and the non-transferable rule enforceable RATHER THAN MERELY PRINTED ON THE BACK OF THE CARD"*. Measured on production 2026-09-10: 0 triggers, 0 functions, 0 policies, 0 views, 0 routes and 0 rows. A confiscation recorded against a pass left it active and the next scan at any gate said allow. The sentence was not true and the table had no way of making it true. ★★★ AND THE ENGINE WAS ALREADY FINISHED, FOR THE SIXTH TIME THIS WEEK. accred.pass_decision denies on pass_' || status and reads the row LIVE at every scan, so a sanction needed no new deny ground, no new column and no new lookup -- it needed to reach status. ★★ AND THE PRECEDENT WAS FIVE SECTIONS ABOVE IT IN THE SAME FILE: accred.reissue_revokes_the_old_pass, whose comment gives the reason for a trigger over route code -- *"ONE STATEMENT, so there is no window in which both cards work"*. Grep the comments for the table you are about to build. ★★ IT REFUSES RATHER THAN SILENTLY DOING NOTHING, AND THAT IS THE DESIGN. The obvious form is an UPDATE whose WHERE matches nothing when the target is wrong: the sanction is written, no pass is revoked, and the caller gets 201 -- a record saying a credential was confiscated over one that still opens doors. EC476 instead, for a pass not on this event and for a pass with nothing left to take. *Evidence: api/test/w395-acceptance.mjs 32/0/0, forced red three ways moving DISTINCT sets: dropping the event pin reds 3b, 3c and 3d while 3a stays green; removing the accreditation.revoke require reds 4c, 4d and 4e; widening the revoking actions to include warning reds 1b, 1c, 1d, 2g, 4d and 5e. Battery 192 suites, 191 passed, 0 FAILED, 0 did not run, 0 unmeasured, 5262 assertions, receipt at commit bbf1750 with a clean tree; api 637/637; web 358/358; tsc and build clean. The panel was DRIVEN IN A BROWSER and revoked a real pass through the real route, leaving revoked_reason = 'confiscation (touting, serious)'. 1 migration on all three databases; the ledger runs 420 probes, every migration present on dev and prod. On production: api 74c34865, web 69eaaa56, the pre-auth crew surface answering 404 against a nonexistent route's 401, and index-Dvntmg2H.js carrying four new Hungarian strings under a no-cache HEADER with a control at 0. D-974, D-975.* ★★★ THE EVENT PIN, AND THE SIBLING THAT IS THE ONLY REAL TEST OF IT -- FOURTH TIME IN FIVE SLICES. The cross-tenant assertion passes whether or not the trigger pins the event, because row security already filtered that row out of the SELECT. 3b, 3c and 3d drive our own pass from our own OTHER event and are the only assertions that fail when the pin is gone; 3a keeps the cross-tenant case and SAYS IN ITS TEXT that it measures row security. ★★ AND THE MIGRATION'S OWN PROBE CLAIMED THE PIN AND DID NOT MEASURE IT. Its notice read *"so the trigger exists AND pins the event"*; with the pin deleted the probe still passed, because a pass id that cannot exist is refused by the not-found branch either way. Found by the sabotage, in the session that wrote the claim, and the notice now says what it measures. ★★★ RECORDING AN INCIDENT IS NOT THE SAME PERMISSION AS TAKING A CARD. The route requires accreditation.revoke as well as accreditation.manage when the action confiscates or expels -- otherwise it is a side door around the code /accreditation/passes/status has required since W382, and anybody who may record a warning could take a credential away by picking a different word from the same dropdown. The caller had to be BUILT, and that is a measurement in itself: no role in this database splits the two, so the gate could never have been driven with a fixture caller and the second require() would have been a line nobody tested. Section 4 clones the Event director template with one explicit deny. ★★ AN ASSERTION FOUND PASSING FOR THE WRONG REASON ON ITS OWN FIRST RUN. 2f accepted either pass_revoked OR no_such_access_point, and pass_decision tests the access point FIRST and returns -- so on any database with no access point it went green without the engine reaching the status check. It builds its own zone and gate now, asserts pass_revoked exactly, and 2g is the control. ★★ A RESTORE FAILED SILENTLY AND WAS CAUGHT BY GREPPING THE MARKER. The third sabotage's restore ran with a stale working directory -- a cd inside a compound command -- and reported FileNotFoundError in the middle of a long output while the migration kept the sabotage. W385's lesson held: restore by CONTENT, not by checksum, and re-check immediately before the run that matters. ★ FUTURE BANS ARE RECORDED AND NOT ENFORCED, DELIBERATELY. Enforcing one means refusing a person a pass at a LATER event -- a cross-event rule with a duration, a scope and an owner, none of which is decided. W389's rule: when a switch and its satisfier are both unbuilt, build the satisfier first. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W396The approved request becomes its passcomplete★★★ THE COUNTING FUNCTION COULD NOT COUNT ANYTHING THIS PRODUCT ISSUES. The handover called accred.quota_used finished and correct with no caller, and commissioned accred.allocation built WITH its enforcement. It counts live passes through accred.pass.request_id, and measured on 2026-09-11 no line of the Worker had ever set that column: the issue route took a person and a category, the reissue route did not carry it, and the request lifecycle W384 and W385 built ended at a decision nothing could act on. A quota enforced on top would have been a limit nothing ever consumed, and it would have LOOKED enforced. The count had to move first; W397 is the quota. POST /accreditation/passes accepts request_id -- the pass takes person, category and event FROM the request, read through row security and pinned to the event in the body, and only an approved decision grants one, read from app.approval and never copied. One migration: pass_request_same_event (a composite key), pass_one_live_per_request (quota_used's own definition of live), accred.request_awaiting_pass(), and W387's reissue trigger carries the request to the replacement AFTER retiring the old card, so no statement ever holds two live passes on one request. ★★ THE SIDE DOOR, CLOSED BEFORE THE QUOTA EXISTS. The direct path refuses a person whose approved request is waiting, and the desk offers them only through the request, organisation named. ★ OVERRIDES ARE REFUSED, NOT APPLIED: nothing writes accred.request_override. ★★ AND A W387 DEFECT ON THE SAME SCREEN: Replace threw away the replacement card's one-time code, of which only a hash is kept, so every replacement made from the Passes screen was plastic nobody could print a working code on. *Evidence: api/test/w396-acceptance.mjs 32/0/0, forced red by eight route and three database sabotages each reddening its own set -- the request pin alone reds 4b while the category select still pins (defence in depth, measured), the queue-lookup revert reds 6h alone, the key reds 4d alone, the carry reds 5b-5d. Two assertions were found unable to fail and fixed first: 6f passed under the old person-based lookup, so 6h was added; 4d used a live pass, so a leak at 4c let the index answer before the key. Battery 193 suites, 192 passed, 0 FAILED, 0 did not run, 0 unmeasured, 5294 assertions, receipt at 8c7e0bf with a clean tree; api 637/637; web 358/358; tsc and design-parity clean; CI green on both workflows. Driven in a browser: issue from the request, the one-time code, the queue reading Kiallitva, Replace showing the new code. 1 migration on all three databases, 421 probes, the new probe forced red by removing the carry while a comment spelled the exact phrase it matches. On production: api fc1bb3b6 (was 74c34865), the pre-auth crew surface answering its own sentence against an invalid path echoed as not found; web c7ab0d0d, index-BrUV88Ac.js and the AccreditationScreen chunk BYTE-IDENTICAL to the local build under a no-cache HEADER, four new Hungarian strings present, a control at 0. D-976, D-977.* *Evidence: 1 migration, acceptance suite, 1 design doc.*
W397The quota an organisation is held tocomplete★★★ TWO NUMBERS WERE RECORDED AND CONSULTED BY NOTHING. accred.allocation.quota_allocated had no writer at all, and accred.category.quota_default -- *the default per-organisation quota* -- had been WRITABLE AND ON SCREEN as *Quota per organisation* since W345, its hint promising *"zero means none until somebody allocates them"*, with nothing anywhere reading it. W396 made the count real; this holds it against a limit. The limit is the organisation's own allocation, else the category's default, else none, and a trigger on the pass refuses one becoming live against a request once it is full -- EC485 naming the organisation, the category and both numbers, as W211's EC472 does. One count, two ways in: accred.quota_used_by(event, company, category) is the rule and quota_used delegates to it, so the screen and the refusal cannot disagree. GET/POST /accreditation/allocations, an *Organisation quotas* panel listing every organisation including those held to the default, an organisation search over the 35 000-row directory, and a Passes desk that disables a waiting request whose quota is full. ★★★ FOUR THINGS FOUND BY BUILDING IT. (1) A replacement at a quota lowered BELOW use was refused -- ordering alone holds at a FULL quota, which is exactly what the migration's own probe tested and passed, and at an over one a holder with a lost card could not be given a new one while the panel promised a lowered quota revokes nothing. Found by the suite's 3a; a replacement is exempted by name now and the reissue trigger carries a request only from a card it actually retired. (2) FIVE DESKS TOOK ONE PLACE, FIVE TIMES, TWICE: without the row lock every concurrent issue counted before any committed. With for no key update exactly one of five succeeds. (3) The reissue route accepted an expired card, which W346's own comment calls terminal -- an expired card has given its place back, so replacing it would take a new one under another name. (4) ★★★ A FOREIGN-KEY VIOLATION WAS THE 503 LIE A FIFTH TIME: POST /accreditation/requests naming an organisation that does not exist answered *service temporarily unavailable, retry: true*, measured on the real Worker. Three routes pre-count their dependents *because the database would refuse anyway -- as a 23503 nobody can read*; the class is closed at the choke point now, 23503 and 23001, with only the constraint's NAME travelling. *Evidence: api/test/w397-acceptance.mjs 38/0/0, forced red by six route and seven database sabotages each reddening its own set -- the lock's twice, the exemption reds 3a alone, the key 6g alone, the 23503 branch 7a alone. The anchoring guard event-write-anchoring.test.ts caught the first draft of the allocation route for writing beside a caller-supplied event id with no ownedEvent. The design-parity gate was run BEFORE the push this time and the new panel's exemption carries a frame measured on the running app. Battery 194 suites, 192 passed, 0 did not run, 5331 assertions, 1 SKIPPED; w251 FAILED on both runs and is the machine, not the code -- its semantic searches give Workers AI 2 500 ms and fall back to substring, one directory call logged at 2 653 ms under a load average of 11 to 32 from Spotlight indexing, a DIFFERENT assertion failed each run, and the suite passes 17/0 solo against the same Worker at the same commit. api 637/637, web 358/358, tsc, parity and build clean; CI green. 1 migration on local, dev and production, 422 probes on both remote databases. Driven in a browser end to end: the panel listed the organisation held to the default, the search found four organisations for *Event Equip*, a quota was set then lowered through the same upsert, and the desk showed *1/1 · betelt a keret* disabled. On production: api 7909a725 (was fc1bb3b6), web 4ac4a3ca, index-BLtSlzyK.js and AccreditationScreen-DI9tBLOI.js byte-identical to the local build under a no-cache HEADER, five new Hungarian strings and the code markers present, a control at 0. D-978, D-979, D-980.* *Evidence: 1 migration, acceptance suite, 1 design doc.*
W398A pass design's stamp follows its approvalcomplete★★★ THE STAMP FOLLOWED A BUTTON, AND W393 HAD LEFT IT AS A DECISION BECAUSE IT TOUCHES A SHARED ENGINE. W393's own closing note: *a trigger on app.approval could keep every design in step without anybody pressing anything -- that is the durable fix and it touches a shared engine, so it is a decision rather than a tidy-up.* Until somebody pressed *Read the decision* an approved design unlocked nothing (EC475), and -- the half that matters -- a design whose approval had been WITHDRAWN went on unlocking it. It is an arm in app.approval_closes_subject, the engine's own subject-closing trigger function, beside the agenda item's (W258) and the asset's (W261): not a second trigger on the busiest table in the engine. ★★ IT SITS ABOVE THAT FUNCTION'S EARLY RETURN, which is the one real change to its shape: the function returns at once for any state but approved, rejected and changes_requested -- right for an agenda item, wrong for a stamp, which must be taken back when an approval is withdrawn or reopened. ★★ AND ONLY THE DESIGN'S CURRENT APPROVAL STAMPS IT (and d.approval_id = new.id), the care the asset arm already takes. The approver recorded is the one whose decision concluded it, read from the engine's own decision log rather than whoever pressed a button. ★★★ AND THE DEFECT UNDERNEATH: A RE-DRAW KEPT ITS APPROVAL. W393's edit branch cleared approved_at above a comment reading *a design somebody approved and somebody else then re-drew is not the thing that was approved*, and left approval_id pointing at an approval still approved -- so pressing the button put the OLD decision on the NEW artwork, one press from what the comment forbids. The re-draw clears the approval now. ★★★ AND W393'S OWN SUITE HAD ASSERTED THAT AS THE EXPECTED BEHAVIOUR: its 4c re-approved the superseded round and required the stamp route to stamp the re-drawn artwork. It asserts the new truth now -- the superseded round cannot stamp it, the design's own new round does with no /stamp call, a rejection takes the stamp away by itself, and a stamp written OUTSIDE the engine is still reported out of step, which is the one thing that column can now mean. *Evidence: api/test/w398-acceptance.mjs 24/0/0, every decision driven through /approvals/decide, the route an approver uses. Forced red five ways, each its own set: the arm removed reds 1e/1f/1h/4a/4i; moving the arm BELOW the early return reds 2a/2b/2c, the withdrawn case; the pin dropped reds 4h alone; the approver subselect reds 1f alone; the re-draw keeping its approval reds 4c-4h, and 4d/4e ARE the defect reproducing. Section 5 is the regression control on the shared engine: an agenda item's approval still closes its subject. w393 rewritten to 26/0. Battery 195 suites, 194 passed, 0 FAILED, 0 did not run, 0 with no assertions, 5360 assertions, 1 declared skip, receipt at a9f4ded, clean tree -- the first fully clean battery of the three slices, after a harness leak was found and closed. api 637/637, web 358/358, tsc, parity and build clean; CI green. 1 migration on local, dev and production, 423 probes, the arm and the two arms it was copied from all matched as code with comments stripped. On production: api a0b81bb5 (was 7909a725), the pre-auth crew surface answering its own sentence against an invalid path echoed as not found. No web change. D-981, D-982.* ★★ AND THE HARNESS LEAK THAT SURFACED IN THE SAME BATTERY: w22 had created an event through /events/new on every run since W333 and never removed it. Tenant A reached EXACTLY 100 events -- the limit /events itself applies -- and the row that fell off the end was another suite's fixture, so shared-event-write went red on an assertion about tenancy with the cause three suites away. 83 leaked events removed, w22 deletes and ASSERTS its deletion now, and /events truncates at 100 and says nothing, where /companies and /contacts fetch one row over the page and return may_be_incomplete. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W399The two oldest lists say how much of themselves they showcompleteGET /events AND GET /projects WROTE limit 100 AND RETURNED NOTHING ABOUT IT. The note beside COMPANY_PAGE in this Worker reads *trap 2 of this build plan is a capped list a screen believes is complete*, and thirty-eight list routes fetch one row over the page and answer may_be_incomplete. M-01 and M-02 -- the first two entries in the navigation, the lists every other screen picks an event or a container from -- were not among them. ★★★ IT WAS NEVER A ONE-SCREEN DEFECT: SEVEN CALLERS READ GET /events AND FIVE DRAW A CHOOSER -- the events index, EventGate (the landing eight event-scoped areas fall back to), useEventChoices, the layouts screen and the merge screen -- so the 101st event could not be opened, could not be a parent, could not be given a layout and could not be merged into, with nothing anywhere saying a row had been dropped. ★★ AND THE BROWSER HELD A COPY OF THE SERVER'S NUMBER: EVENT_PAGE_LIMIT and PROJECT_PAGE_LIMIT mirrored two literals across a network boundary and guessed the flag as length >= limit, which claims a longer list whenever the total is EXACTLY the page size. Both are deleted. The flag alone would have been half a fix -- a notice telling a reader that rows are missing on a screen with no way to reach one -- so both routes take a server-side q on name and code, and the two indexes and EventGate gain the search field their own W126 comments predicted. *Evidence: api/test/w399-acceptance.mjs 26/0, forced red by nine sabotages. Three things the sabotage run found: a search assertion written as *the result contains the row* stayed green with the WHERE clause deleted, so it asserts the list NARROWS; removing the page slice made the suite throw rather than report; and the coalesce on the nullable code prevents nothing, because TRUE OR NULL is TRUE -- the route comment claiming otherwise is corrected and the assertion relabelled a positive control. The bulk fixture lives in tenant B so a crashed run cannot push another suite's fixture off a page, which is the leak the commissioning handover had just spent a night on. Battery 196 suites, 195 passed, 0 FAILED, 5386 assertions, receipt at e49ec64. On production: api 874c1de1, web 7d4b3fe3, the two code-split chunks byte-identical under a no-cache header. D-983.* *Evidence: acceptance suite, 1 design doc.*
W400The override an approver writescompleteaccred.request_override HAS EXISTED SINCE W209 WITH NOTHING WRITING TO IT, under its own comment reading *An approver may raise or lower one person's access within their authority*. Measured 2026-09-12: zero rows on local, dev and production. W396 had made the issue path REFUSE a request carrying one rather than flatten it to the category default -- a refusal nobody could reach, which was the point, and which this removes. ★★★ THE TABLE COULD HOLD A BAND FROM ANOTHER EVENT: its zone and window keys referenced their tables by id alone, and row security constrains the new row's own tenant while a foreign key is checked with row security bypassed, so an honest tenant with their own band from event B was the write that slipped through. FINDING-2026-09-08 in a fourth place. One migration adds event_id, three composite keys and a NULLS NOT DISTINCT unique index, so a foreign band, a foreign window and a cell that is both added and removed are all unrepresentable for every writer rather than only for the route that remembers to check. ★★★ THE RESOLUTION RULE IS THE SLICE: additions first, removals LAST, and a removal naming no window takes the band at EVERY window. The unique index makes one cell's contradiction impossible, but a remove of a band and an add of that band DURING A WINDOW are two different cells and both can exist -- so the order is the rule, and it is the fail-safe direction: an approver who says *not this band* must not be left with it open during load-in. ★★★ AND THE PERMISSION IS accreditation.manage, NOT accreditation.issue: a desk that hands out cards must not be able to widen what a card opens and then hand it out. All five seed roles holding issue also hold manage, so the choice changes nothing today -- which is exactly when it has to be right, because the model exists so a narrow desk role can be cut later. The suite BUILDS that role, an Operations lead cloned with a deny, and measures it with effective_permissions(). *Evidence: api/test/w400-acceptance.mjs 38/0, forced red by seven route and two database sabotages each reddening its own set, the database ones arming their restore first and running it in a finally. 4c inserts a foreign band by direct SQL and asserts 23503 from the KEY. W396's own section 7 asserted the refusal this slice removes, and the battery is what caught the pair -- rewritten to the claim that outlived it, that an override is never IGNORED. 1 migration on all three databases. D-984, D-985, D-986.* *Evidence: 1 migration, acceptance suite, 1 design doc.*
W401A run sheet on its own clock, and a question about food on an anthemcompleteTWO OWNER REPORTS ON 2026-09-12, AND FIVE MORE FOUND BY READING THE SAME SCREEN. ★★★ THE COMBINED RUN SHEET PRINTED ITS TIMES IN THE CONNECTION'S ZONE AND ITS DAYS IN THE EVENT'S, from two lines of one query. The route resolved the day per event and sent the instant as ::text, which renders in the session's TimeZone; the screen sliced the clock out of that string under a comment arguing -- correctly -- that the BROWSER's zone would be wrong. Every word of that argument was true and the answer was not. Measured: local Postgres is Europe/Budapest and Neon is GMT, so the defect was invisible on a developer's machine and two hours wide in production, which is why it shipped; at a boundary the two halves of one row contradicted each other outright, a 00:30 item keeping its correct local date and printing 22:30. The zone travels per row now and both screens format through one function in dateParts, at 24 hours, because the same programme rendered *07:00 PM* on one and *19:30* on another. ★★★ AND THE SERVICE-STYLE QUESTION WAS ASKED BESIDE A NATIONAL ANTHEM: W321 put *How food is served* on every agenda line, and it is NULL on all 172 production agenda items, never once answered since it shipped five days earlier. A catering kind makes it answerable and the ROUTE clears a style off every other kind, because a control the browser hides is a courtesy and a style stored invisibly under a session is a value no screen can show or correct. ★★ FIVE MORE IN THE SAME SWEEP: the status and platform vocabularies were English in the drawer and Hungarian in the table beside it -- one map, translated on the row and raw in the control that sets it, invisible to the i18n gate in both spellings; two Fold summaries were English when filled and Hungarian when empty; the Forgatokonyv tab printed the three dashes the owner called visual litter on 2026-09-05, fixed then in the tab beside it; and dayOf returned a bare English *Unscheduled*. ★★★ AND ONE FOUND BY BREAKING IT: AN ENGLISH SENTENCE IN A SQL COMMENT CHANGED WHAT AN INTERPOLATION COMPILED TO. postgres.js picks the builder for an interpolated list by scanning the preceding SQL TEXT -- comments included -- for the keyword at the LARGEST index. A comment containing *renders in* pulled the in match earlier than the last real as in the column list, so the array was built as IDENTIFIERS and the query failed with a syntax error naming a uuid. The route answered 503 for every project with an event until the prose moved out of the template. *Evidence: api/test/w401-acceptance.mjs 17/0, forced red by four sabotages each reddening its own set; 1c drives a project spanning Budapest and London and asserts the same instant reads 19:30 and 18:30 through the rows' own zones while the sliced string reads identically for both, and 1d is written to hold whatever zone the connection is in. 1 migration on all three databases. Driven in a browser end to end. D-987, D-988, D-989.* *Evidence: 1 migration, acceptance suite, 1 design doc.*
W402/health names its buildcompleteFIVE SLICES IN A ROW ENDED WITH A DEPLOY NOBODY COULD PROVE WAS THE RIGHT BUILD. Every handover since W398 closed on the same sentence -- *the version id changed and the pre-auth surface still answers* -- which proves the routing table of SOME build is live and not which one. wrangler deploy exiting 0 says the upload was accepted, and this project has a recorded deploy that exited NON-zero while the Worker and its routes went live regardless. ★★★ AND ON api.event.clinic A 401 PROVES NOTHING, BECAUSE AUTH RUNS BEFORE ROUTING: an unrouted path, a typo and a deploy that never landed are one answer, so the only usable probe was a pre-auth surface and the only thing a pre-auth surface said was {"ok":true}. The web half has been byte-comparable per code-split chunk since W260. /health names a short commit hash now, -dirty when the api/ tree was modified and unknown when nothing told the Worker, injected by npm run deploy with --var BUILD_SHA: -- confirmed to MERGE with the config's own vars on a --dry-run first, because a flag that replaced the block would have silently dropped ALLOWED_ORIGIN, PURGE_DRY_RUN and SMS_LIVE from production. ★★★ THE DISCLOSURE QUESTION IS THE WHOLE OF THE SLICE AND IT IS ENFORCED IN THE CODE THAT DISCLOSES, not in the script that fills it: a branch name leaks what is being worked on, this repository's branches are named after customers and defects, half its commit messages name the hole they close, and BUILD_SHA is an ordinary var one edit away from carrying git describe --all. src/build-id.ts checks the shape on the way out and publishes unknown for anything that is not a short hash. ★★ THE -dirty MARKER IS THE LIKELIER MISTAKE: nobody deploys the wrong commit on purpose and everybody has deployed an uncommitted fix, and it counts UNTRACKED files because a new source file nothing has committed is bundled the moment something imports it. *Evidence: api/test/w402-acceptance.mjs 15/0 -- sections 1 to 3 against the live Worker, section 4 running the deploy script's own shell expression inside a scratch git repository made deliberately dirty -- and api/test/build-id.test.ts 8 tests driving the branch that REFUSES, which no acceptance suite can reach because a running Worker carries exactly one BUILD_SHA. Forced red by seven sabotages each reddening its own set; removing the guard entirely reds six of eight units and TWO STILL PASS, because a valid hash is valid either way. ★★★ AN ASSERTION WENT GREEN ON A BUILD ID THAT WAS ALWAYS DIRTY: the first section 4 interpolated the expression into printf '%s' "...", sh does not nest double quotes, every run errored with test: too many arguments, the || echo -dirty branch fired unconditionally, and the INCLUSION assertion passed on it -- caught by 4d, the EXCLUSION one, a clean tree does NOT carry the marker. W399's lesson one slice later. ★★ And a sabotage caught a VACUOUS assertion: 1c compared json.build on both sides of a junk bearer token and stayed green on undefined === undefined when the field was deleted. It compares the raw bodies now. Battery 199 suites, 198 passed, 0 FAILED, 0 did not run, 5457 assertions. ★★★ AND THE PROOF IS THE SLICE ITSELF: production answers build: 253d9ae, which IS the deployed commit, against three invalid controls -- an unhandled path under the pre-auth surface answers 404, /healthz answers the 401 that proves nothing, and the previous commit 0c4bb5d is nowhere in the answer. The first deploy in this repository that can be told apart from the one before it. D-990.* *Evidence: acceptance suite, 1 design doc.*
W403The English in an attribute passes through the translatorcomplete**THE 2026-09-12 HANDOVER, ITEM 3, AND THE OWNER'S FIRST NAMED ITEM ON 2026-09-13: *Start with W403, the English the i18n gate cannot see*. Every i18n check read text nodes and translator arguments, and a prop is neither, so <EventGate area=...>, <Field hint=...> and <Badge status=...> rendered English inside screens that translate everything around them. All fifteen measured on 2026-09-12 were still there. ★★ THE HANDOVER'S GATE, MEASURED BEFORE IT WAS WRITTEN, WAS WRONG IN BOTH DIRECTIONS. Rule (a) as written matched 32 props, because 23 viewBox digit strings carry whitespace, and requiring two words of letters brought it to the 8 real hits. Rule (b)'s dictionary condition hid status='Pending' on both venue screens, because nothing had translated Pending yet, and without the condition it finds 9, all real. A ternary between two Capitalised literals in a prop found 4 more, all real: Draft and Published on both venue badges, and two English aria-labels in ClientQuotesScreen. 21 in all. ★★ TRANSLATING THE AREA ALONE WOULD HAVE BROKEN THE HUNGARIAN.** EventGate glued the area between fragments, which puts *a* before *ellenőrzőlisták* and a singular verb after *Fellépők*. Each notice is one sentence with an {area} hole now, and the Hungarian puts the area after a colon. *Evidence: the gate red against a clean copy of HEAD with 8 prose and 13 label hits, green after; 4 of 4 sabotages in scratch copies each reddening only their own rule, and a control 13 of 13. Browser, local, Hungarian, an empty workspace provisioned through POST /me/workspace: Stáb and Ellenőrzőlisták read as whole sentences. web 429/429, typecheck and build clean. Production: the dictionary chunk hu-DGqlJcW3.js then hu-asrwNvd_.js, byte-identical to the build, the old fragment key 1 then 0, the new keys 0 then 1, an invalid control 0 and 0, and ClientQuotesScreen-BsgzzVFS.js byte-identical. Commit cabce90, D-1068.* Left open and measured: 48 template literals in props, about 40 of them real, which is W443, and 35 bare English fallbacks such as ?? 'Not set'. *Evidence: 1 design doc.*
W404An approved budget is frozencompleteTHE OWNER ASKED WHETHER A BUDGET LINE FOLLOWS ITS ASSET, AND THE ANSWER TO HIS FOLLOW-UP WAS A RULE THIS PRODUCT DID NOT HAVE. 2026-09-12: *It should not change automatically, because there may already be an accepted budget that we cannot modify.* ★★★ REPRODUCED BEFORE A LINE WAS WRITTEN: against the local Worker, on a budget whose state is approved, POST /budgets/lines/new answered 200, /update moved an amount from 2 to 999 and /remove deleted the line. ALL THREE. app.budget.state has carried approved and closed since W27, budgets.approve has been a separate permission at financial scope since D-403, and nothing on the line write path ever asked what state the budget was in. ★★ AND ONLY HALF THE APPROVAL WAS GUARDED: entering the approved state needed budgets.approve and LEAVING it needed only budgets.edit, so the freeze would have cost one extra request -- set the state back, edit the lines, and leave the budget un-approved, which is a worse end state than the edit it was meant to prevent. ★★★ IN THE DATABASE AND NOT IN THE SEVEN ROUTES that write these two tables, on W394's argument. ★★★ AND IT FREEZES THE WHOLE ROW EXCEPT THE FILING, WHICH IS THE DESIGN RATHER THAN A SHORTCUT: freezing *the money columns* needs a hand-kept list of them and would leave a money column added by a later slice FREE BY DEFAULT, so it is inverted and an unknown column is frozen by default. One real caller needs the exemption today -- POST /events/split clears event_space_id on lines whose room has left, a referential repair that changes no figure. ★★★ AND app.budget_transaction IS DELIBERATELY OUTSIDE THE FREEZE, which is the owner's other sentence: *we may need to pay the supplier, the workforce, or the performer a time-based fee ... the master event budget and the supplier payouts may differ.* Freezing the PLAN while the ACTUALS accrue is what makes that difference exist at all. *Evidence: api/test/w404-acceptance.mjs 29/0, forced red by six sabotages each reddening its own set. ★★ TWO SABOTAGES FOUND DEFECTS IN THE SUITE RATHER THAN IN THE CODE: an assertion reading *the line is still there* compared a TOTAL and passed on a coincidence -- one line added and this one removed -- and an exemption about frozen budgets was being asked on an ACTIVE one, where neither branch can fire. ★★ A THIRD FOUND THE OWNER TRIGGER UNMEASURED: /budgets/lines/owners/set writes both tables in one transaction, so dropping its trigger left the suite at 25 of 25 and the trigger could have been deleted with nothing noticing. ★★ AND AN APPEND-ONLY LEDGER MADE THE SUITE'S OWN FIXTURE PERMANENT: driving the actuals route left a row app.forbid_mutation refuses to delete, and creating a planner cascaded an UPDATE onto the append-only app.audit_event -- a person cannot be erased out from under the record of what they did. Both repaired by borrowing rather than minting: the seed Event director, and an insert inside a rolled-back transaction. 1 migration on all three databases; the post-flight BUILDS an approved budget inside a rolled-back transaction and drives the refusal, because production has none to aim at -- measured at 39 budgets, 12 active holding all 82 lines, 27 draft holding none, ZERO approved and ZERO closed, so nothing editable today stops being editable. Battery 200 suites, 199 passed, 0 FAILED, 0 did not run, 5486 assertions, re-run at this commit after the first run showed w276 red on another session's uncommitted ArtablaScreen.tsx. ★★★ THE WORKER HALF WAS COMMITTED AND DELIBERATELY NOT DEPLOYED BY THIS SESSION: npm run deploy bundles the WORKING TREE, and that tree held another session's unfinished W405 route. Only the W404 hunk of api/src/index.ts was committed, staged with git apply --cached on a single hunk rather than git add. ★★★ AND W402 PROVED ITSELF ON THIS SLICE WITHIN THE HOUR: the peer session committed W405 as 4b51909 on top of W404 and deployed, /health on production went from 253d9ae to 4b51909, and git show 4b51909:api/src/index.ts contains W404, D-991 -- so the Worker half IS live, and the argument that it is live is a two-step chain that could not have been made the day before. D-991, D-992, D-993.* *Evidence: 1 migration, acceptance suite, 1 design doc.*
W406Attach a cost from the record's own sidecompletePRODUCTION HELD 82 BUDGET LINES AND NOT ONE NAMED AN OWNER. app.budget_line_owner was EMPTY, so the Mini Budget panel on every asset, performer and crew row was blank and every line on the Budget screen read *Nobody yet* -- and from a screen, a feature nothing has ever been attached to is indistinguishable from a feature that does not work. The machinery was complete on both sides and had a door on ONE: /budgets/lines/new has always taken owner_type and owner_id, /budgets/lines/for-owner has always read them back, GET /budgets?event_id= has always listed the candidates, and the only surface that could send them was the Budget screen. A record could be TOLD what it costs and could never SAY it. Web only: no migration, no new route, no changed route. ★★★ D-432 IS KEPT STRUCTURALLY RATHER THAN BY A RULE: the panel writes amount and is never passed the owner's own quantity, so prefilling would take a NEW PROP threaded from the screen -- a visible edit in a diff -- rather than a default changed from empty. A rule against prefilling can be broken by accident; this cannot. ★★★ AND THE FROZEN BUDGET IS LISTED AND DISABLED, NOT FILTERED AWAY, carrying its state, because a reader whose only budget is approved must be told why rather than shown an empty select. The FIRST ATTACHABLE budget is preselected, not the first row, because the route orders is_baseline desc and a frozen baseline would leave a disabled option selected under a button that cannot work. ★ The list is a DENY-list where W404's is an ALLOW-list and the asymmetry is the argument: W404 inverts because there the bad branch is money moving unnoticed, and here the bad branch is a budget DISAPPEARING with no explanation while the other branch is EC600, whose message names the state and says what to do. ★★ THE UNITS ARE PICKED AND NOT TYPED. app.budget_line.amount_unit carries no foreign key -- measured, unlike app.asset.quantity_unit, which does -- so free text is legal and is also how pc, pcs, piece and darab end up in one column. W290 checked the whole 72-code vocabulary into the bundle with a Hungarian label and a test, and nothing on the budget path had ever read it. *Evidence: api/test/w406-acceptance.mjs 24/0, forced red by five sabotages each reddening its own set -- the owner join row dropped, state out of the budget projection, a null unit_price no longer counting as a money write, the budget amount written back onto the asset which is the D-432 defect itself, and /assets/detail dropping event_id. Three more against isFrozenBudget in web/test. ★★★ TWO DEFECTS FOUND BY DRIVING IT IN A BROWSER, NEITHER VISIBLE TO ANY STATIC CHECK: load() set loading, which makes the panel return a bare paragraph, so the re-read after an attach tore down the writer mid-use and the reader was returned to a button -- a refresh must not blank what it is refreshing. And the panel had TWO return statements, putting the writer at two tree positions, so React would have unmounted it across the empty-to-populated transition anyway once the first cause was fixed. The first diagnosis was the second one and it was the WRONG root cause. ★★★ AND THE SUITE BORROWED THE WRONG IDENTITY OUT OF A COMMENT: W404's note that Event director does not hold budgets.approve was read as *no money rights*, and effective_permissions says it holds budgets.view_financial and budgets.view_margin as well -- so the section entirely about a cost-blind caller was asked three times of somebody who was not one, and its negative passed the write it was built to see refused. W271 had built the role with the right shape in July. The precondition asserts the PERMISSIONS now, not the role id. Deployed and probed: /health answered d4d4014, the previous commit 4b51909 was nowhere in the answer, and the served SPA bundle carries the slice's own Hungarian string while an invented control string is absent. D-994, D-995.* *Evidence: acceptance suite, 1 design doc.*
W407The actual, marked and displayed, never appliedcompleteTHE OWNER, 2026-09-12, IN TWO MESSAGES. *when time is changing during the event or during the production phase, we have to mark it and display it. It should not change automatically, because there may already be an accepted budget that we cannot modify ... the master event budget and the supplier payouts may differ. We must make this differentiation visible and clearly marked for the user.* And, later the same day and mid-build: *not just the time can change, but also the amount can be different in production.* ★★★ THE SECOND MESSAGE IS LOAD-BEARING RATHER THAN A DETAIL, because line_total is GENERATED as round(amount * time_qty * unit_price, 2): a payable following the actual HOURS while keeping the planned QUANTITY is wrong the moment four are booked and three arrive, which is D-432's own worked example. Both dimensions are actual or the figure means nothing. ★★★ A TABLE AND NOT TWO COLUMNS, AND THE CHOICE IS FORCED BY W404 RATHER THAN PREFERRED. That morning's freeze compares the WHOLE budget_line row against a nine-name filing allowlist, so any column added later is frozen by default -- deliberately, so a money column added by a later slice is not free by default. An actual_amount column there would therefore answer EC600 exactly on the approved budget the owner's first sentence is about. W404 had already named the alternative: app.budget_transaction sits outside the freeze so the plan can freeze while the actuals accrue. This is the second member of that family, and the migration REFUSES TO APPLY if the freeze is ever attached to it -- an omission is not a guarantee, and a later sweep that attached it would delete the requirement with every test still green. ★★ AND NOT app.budget_transaction EITHER, MEASURED: it carries direction, amount, currency and occurred_on and has no quantity and no time, so it can say EUR 400 was paid and cannot say three radios were booked for two days and two turned up for three. ★★★ NULL IS NOT MEASURED AND IS NEVER ZERO. app.participation_shift holds a planned start and end and NO ACTUAL CLOCK, so a crew line is genuinely unmeasured until app.checkin_event says otherwise. Both columns are nullable with no default, app.budget_line_payable falls back to the PLANNED figure PER DIMENSION so *we know the hours and not the headcount* is expressible, and a measured ZERO is a different state from an unmeasured one. The screen renders an unmeasured dimension as the words *not measured*, never as a dash, because a dash under a heading reading ACTUAL reads as nought. ★★ RECORDING IS budgets.record_actual AND NOT budgets.edit -- reporting what happened is the finance act -- and the recorder is mounted BOTH on the Mini Budget panel and in the Budget screen's line drawer, the second being REACHABILITY rather than convenience: the panel only exists on a record a line names as an owner, and production holds 82 lines of which none does. payable is money and is ABSENT for a caller without the cost right, named in masked_fields, while the QUANTITIES stay visible because a quantity is not money and over-masking would delete the comparison the owner asked to see. *Evidence: api/test/w407-acceptance.mjs 38/0, forced red by NINE sabotages each reddening its own set: the payable following only the time (5), falling back to zero instead of the plan (5), the freeze attached to the actuals table (2), the actual APPLIED to the plan (6), payable dropped from masked_fields (1), payable not stripped for a cost-blind caller (1), budgets.edit instead of budgets.record_actual (3), the RLS with check weakened to its own tenant_id only (1), and a coalesce merge instead of a whole-statement replace (4). ★ THE FOURTH IS WORTH READING TWICE: applying the actual to the plan reds six, and three of those because W404 then REFUSES the write -- the forbidden behaviour is not merely wrong, it is refused by the slice before it. The migration's own guard was forced red separately. ★★★ AND THE SUITE FOUND A REAL DEFECT BEFORE THE BROWSER DID: the first migration used a bare numeric where budget_line.amount is numeric(14,3), so the plan came back as 4.000 and the actual beside it as 3 -- one comparison in two formats. Repaired with an ALTER as well as a corrected CREATE, because create table if not exists does nothing to a database that already took the first version. ★★ AND THE LINE DRAWER HELD A SNAPSHOT OF ITS ROW, so recording an actual wrote it to the database, reloaded the list, and the drawer went on saying *Nothing marked yet*; it derives from the list now, which fixes every future refresh. 1 migration on all three databases; the production post-flight ran seven MUSTs with the payable-equals-line_total control over 94 REAL PRODUCTION LINES rather than an empty set. Deployed and probed: /health answered a0a1464, c7fd4c9 was nowhere in the answer, git show a0a1464 contains the route while git show c7fd4c9 does not, and the served bundle carries the slice's own Hungarian. D-996, D-997, D-998, D-999.* *Evidence: 1 migration, acceptance suite, 1 design doc.*
W409A sign-up becomes a workspacecomplete★★★ WHAT THE DELIVERY DOCUMENT DOES NOT SAY, MEASURED 2026-09-13 BY A SESSION THAT BUILT NEITHER THIS SLICE NOR THE REST OF THIS ROW. The slice's own commit, 4689028 in the app repo, is titled *BUILT, NOT YET SABOTAGE-VERIFIED* and opens *DO NOT DEPLOY THIS UNTIL THE SABOTAGES HAVE RUN*. Its suite passed once, 24/0 against a local Worker. Its one sabotage attempt crashed with the machine, and that commit says none of it is evidence. No sabotage run is recorded since: no later commit touches w409-acceptance.mjs, the delivery document is unchanged since it was committed, and the acceptance receipt predates the slice. ★★★ BOTH HALVES ARE ON PRODUCTION ANYWAY. api.event.clinic/health answered build 6acbbca, whose history contains 4689028 and whose api/src/index.ts still routes POST /me/workspace. The served web bundle carries WorkspaceCreateScreen and *Create your workspace* in both languages, across 126 chunks in which an invented control string appears in none. It went out no later than W412's deploy of 37aea42, which already contained it. 4689028 is on no remote branch, and origin/main is 28 commits behind the build production runs, so CI on the remote has never seen it. D-1001 to D-1004 are cited in the code, the suite and the delivery document, and decision-log.md holds no row for any of them. NOT MY SLICE. THIS ROW WAS ADDED BY THE W420 SESSION BECAUSE THE GENERATOR HAD REFUSED SINCE W409 SHIPPED, and a refusing generator freezes BUILD-LEDGER.md and the status page it feeds for every session, not just its author. Every sentence below is taken from w409-a-sign-up-becomes-a-workspace.md itself. None of it is a summary of intent and none of it is mine to claim; the slice's author should overwrite this row without ceremony. Measured on production before a line was written: 7 app_user rows, 4 active memberships, 3 people who had signed in and had no workspace, 0 app.invitation rows, and 0 lines of the Worker that insert a tenant. Two of the three are real addresses. The Sign up button worked and led to an empty room: driven as a membership-less caller, /events, /projects and /companies all answered *no active account for this user* with the full navigation rendered around them. ★★★ WHY AN ELEVATED PATH, AND WHY NOT A NEW ONE. app.tenant's WITH CHECK is app.is_platform_admin() and the table FORCES row security, so app_rw, the role every request connects as, cannot insert a tenant under any session -- the identity chicken-and-egg in its purest form, where the caller has no tenant and creating one is the thing being asked for. withService already IS that path: it sets the admin flag for the transaction and guards every statement against SERVICE_TABLES, which ALREADY contained app.app_user, app.membership and app.tenant. Nothing was widened. ★★ A security definer function was the obvious alternative and was refused: it would have worked, and it would also have been a new privileged object in a codebase that already has a narrower, reviewed mechanism expressing exactly this scope. ★★ app.audit_event is NOT on that allowlist and was not added to it -- the audit row is written afterwards, in a SECOND transaction, inside withTenant as the workspace that now exists, and is deliberately not allowed to fail the request, because the workspace exists by then and refusing to report success would leave the caller believing nothing happened. ★★ THE LOCK IS THE RULE, NOT THE CHECK BEFORE IT: select ... for update on the caller's own app_user row serialises simultaneous submissions, which is W397's lesson after five accreditation desks took one place five times over, and the suite drives five concurrent requests. ★★ One workspace per person is a PRODUCT rule rather than a constraint, because a supplier legitimately holds active memberships in several tenants, so *at most one active membership* is false as a schema rule; a second attempt is refused BY NAME, naming the workspace they already belong to. ★ Template roles are referenced, not cloned -- cloning nineteen roles per workspace would turn a permission fix into a migration over every tenant. ★ The slug is made unique by the DATABASE, not predicted by a SELECT, because two strangers may pick the same company name on the same morning and the row lock covers a user rather than a slug; and a name of only punctuation slugs to the empty string, which would breach NOT NULL rather than uniqueness, so it falls back. *Evidence: acceptance suite, 1 design doc.*
W411A blank price says why, and whether it blocks the tenderon production**THE OWNER, 2026-09-12: *get a figure onto each, or mark why it cannot have one yet*. Measured on production before a line was written: of 87 price-table lines on the Oktober 23 project, 25 carry no unit price and 6 more carry an explicit zero. The 6 ARE donations and already said so via W133 gift_in_kind; the 25 cost money nobody has quoted. ★★★ AND 14 OF THE 25 ARE CORRECTLY BLANK, WHICH IS THE SLICE.** The 2026-08-20 technical description schedules NO quantities for the operational layer: line 531 lists medical cover, guarding, barriers and evacuation as the winning bidder's general duties under the Operational Command, and line 76 gives a RATIO rather than a count -- one drinking-water point per 5000 participants, with a capacity plan as a deliverable. So writing *24 mobile WCs* onto those rows would have been the DEFECT and not the fix: it narrows a performance requirement into a shopping list and fabricates a specification the source deliberately declines to give. The handover's own *24 mobile WCs for 8-10 thousand* is a scale analogy and says so. The headline becomes 11 blockers, not 25 blanks. ★★★ NOT A FIFTH zero_cost_reason, WHICH WAS THE CHEAP MOVE AND IS WRONG. All four of W133's values mean the line COSTS NOTHING and its consumers read the column as *free, and here is why*; a to_be_priced member would have made 25 unpriced TENDER lines read as donated in a document a bidder answers with a price. A sibling table instead, four codes derived FROM the 25 rows and each naming whose move it is. W133's guard is NOT narrowed -- it tolerates a null line_total on purpose so a half-typed line is not refused, and BudgetScreen offers its control on exactly that condition, so tightening it would have turned a working screen into a 23514 two days before a tender. W411 refuses the CONTRADICTION instead: no line may be both donated and not-yet-quoted. ★★ blocks_tender IS A COLUMN, NOT A RULE IN THE ROUTE, because the route, the screen and artabla-export.py must not disagree about which blank is acceptable, and two copies of a rule is one copy that goes stale. ★★★ THE ARITHMETIC WAS BROKEN AND NOTHING COULD HAVE SHOWN IT: line_total is generated as amount * time_qty * unit_price and 24 of the 25 had BOTH factors null, so a bidder's price would still have produced NULL. The QR line is sharpest -- 400 db stored, time_qty null, so 400 x NULL x price = NULL. The 14 lump-sum lines now hold amount 1, time_qty 1 and the unit *egy osszegu*, which moves no total today and lets one exist once quoted. ★★★ app.artabla() CAME INTO THE SPINE WITH THIS SLICE, AND THAT IS A REPAIR. It was live on production and defined NOWHERE in the migration chain -- its only home a per-event working folder, patched by a second file that changed its return type -- so migration-ledger.sh could see it in neither direction. The same run added the missing W404 probe, which had left the ledger exiting 1 with zero migrations absent. The ledger now exits 0 for the first time: 427 probes, every migration present. *Evidence: 1 migration, 6 structural self-assertions forced red by 7 sabotages, and probe16.sql 8/0 forced red by 3 more. NO ACCEPTANCE SUITE, WHICH IS A GAP AND IS DECLARED AS ONE: the route change is three pass-through fields and two counts, verified against production rather than a harness. ★★ TWO SABOTAGES WENT RED FOR THE WRONG REASON AND HAD TO BE REBUILT -- one died on a duplicate key at the INSERT, one on a SQL syntax error, so assertions 1 and 6 were never actually driven. A red from the wrong check is a green in disguise. ★★ THE REFUSALS ARE DRIVEN IN THE APPLY SCRIPT, NOT THE MIGRATION, on its own project, event and three lines inside a transaction that is ALWAYS rolled back -- W133 tested its guard by mutating a REAL budget line and restoring it, which on production is a fixture laundered through live data. The fixture budget is active ON PURPOSE, because an approved one would have W404 refuse every write and the probe would pass for a constraint never tested. Each refusal names its SQLSTATE. ★ The code column stays RAW and the sentinel lives in the LABEL, so the column stays filterable. ★★ AND THE FIRST JUSTIFICATION FOR IT WAS WRONG, WHICH IS THE MORE USEFUL FINDING. It claimed coalesce(line_code, 'KBT-DONT') makes that code a display-only default no row stores. Measured: all 11 lines store it EXPLICITLY and the project holds zero null line_codes, so the coalesce fires for nothing. The zero rows that prompted the claim came from the measuring query filtering on b.project_id ALONE, missing the event_id half of the union -- the SAME omission that had returned 12 of 87 lines minutes earlier. A real trap was found, half-learned, and then a plausible mechanism was reached for instead of the WHERE clause being re-read. A mechanism that WOULD explain an observation is not evidence that it did. The rule stands on its own merits. ★★ AND THE APPLY SCRIPT WOULD NOT PARSE BECAUSE OF ONE APOSTROPHE IN PROSE: macOS ships bash 3.2, which counts single quotes inside a quoted heredoc nested in a double-quoted command substitution, so *W133's deliberate tolerance* in a SQL comment broke the file -- and the reported line number named neither the apostrophe nor anything near it. ★★★ DEPLOYED TWICE, AND THE FIRST WAS WRONG. Another session was mid-build on W412 in the same checkout, sharing api/src/index.ts and hu.ts, so this slice was isolated into its own worktree -- and both step1-build.sh and step2-deploy.sh hard-code cd ~/Documents/event-clinic-app/web, so the isolation was defeated and the MAIN tree's bundle shipped while the API came from the worktree. That left production incoherent on a user-facing claim: the served screen said an invitation had been emailed and the deployed Worker had no mailer. Repaired by rebasing onto W412 once it committed and deploying BOTH halves from one commit. Probed: /health answered 0845b04, the served hu chunk is real JS at 441167 bytes rather than an SPA fallback, and carries both slices' Hungarian while three near-miss control strings are absent. ★ A nonexistent route answers 401 exactly as the real one does, so the BUILD_SHA is the only evidence a route shipped. D-1010, D-1011, D-1012, D-1013.* *Evidence: 1 migration.*
W412The invitation that was never sentcompleteTHE ROUTE HAS BEEN CALLED "invite" SINCE W282 AND HAS NEVER SENT ANYTHING. Read end to end on 2026-09-12 it calls app.seat_person, inserts app.membership with invited_at = activated_at = now(), replaces the scope and answers 201. No mailer call, no token, no app.invitation row -- behind a button reading *Invite somebody* and a toast reading *Seat given*. Measured the same day: app.invitation held ZERO rows on production AND on dev, so nobody has ever been invited through this product by either mechanism; an Event director seat was granted at 13:28 UTC and the person heard nothing. No migration, no database change, no new route. ★★★ THE SEAT WAS NEVER BROKEN, WHICH IS WHY THE MAIL MAY CARRY NO TOKEN. auth/identity.ts:113 matches a signing-in caller to a pre-provisioned app_user on lower(email), so the door was already open and a token would have been a second way through it. That is the OPPOSITE of the two builders either side of it: venue-invite-message.ts and advancing-chase-message.ts both put a credential in a mail and both justify it identically -- their recipient has no account BY CONSTRUCTION and there is no tokenless link to send. app.invitation is left untouched. ★★ AND NOT buildNotificationNotice, WHICH FITS ON THE TWO TESTS THAT DISQUALIFIED THE ADVANCING CHASE FROM IT -- its recipient must be internal and ours is, its link must be tokenless and ours is. It fails its FIRST rule instead: it renders app.notification.title and .body verbatim so the mail and the inbox row cannot disagree, and there is no notification row here. So a sixth builder. ★★★ THE OUTCOME IS RECORDED IN app.audit_event AND NOT IN app.notification_delivery, AND THE CHOICE IS MEASURED RATHER THAN PREFERRED: a delivery row needs a notification_id, app.notification.target_type is gated by app.chat_target_type_allowed(), and that function admits fifteen values with no membership among them. The usual home would take a migration widening a vocabulary in order to produce an inbox row for somebody with no account to read it in until the invitation is already spent. app.audit_event asks only tenant_id = app.current_tenant(), carries no vocabulary constraint, and already held a membership row on production. ★★ BOTH OUTCOMES ARE RECORDED, NOT ONLY THE FAILURE -- a log holding failures alone cannot tell *the mail went* from *this code never ran*, which is the ambiguity this route spent three weeks in -- and the address is NOT copied into the detail, because that table refuses DELETE by trigger and a copy would outlive every erasure path reaching app.app_user. ★★★ A REFUSED SEND DOES NOT COST THE SEAT, WHICH INVERTS /advancing/chase. That route builds its mailer FIRST and answers 503 for a missing key, which is right where nothing has happened yet; here the access is already live, and a 503 would say the invitation failed while the seat works -- so the owner grants it again, and again. MailerNotConfigured is an OUTCOME here. ★ THE LINK IS THE APP ORIGIN AND NOT /sign-in, WHICH DOES NOT EXIST: the wall is <SignedOut> at every route, so /sign-in would have looked correct through TWO unrelated fallbacks -- the SPA rewrite serving index.html with a 200, then path="*" redirecting to / -- and curl, a browser click and an assertion would all have been green. *Evidence: api/test/w412-acceptance.mjs 27/0, forced red by six sabotages each reddening its own set: the slice reverted (14, including the headline), the workspace dropped from the mail (2a alone), a token added to the link (3a+3b), a refused send answering 503 (4a,4b,5a,5d,6c), only failures recorded (5b+5d), the address copied into the audit detail (5c alone). ★★★ TWO DEFECTS IN THE SUITE WERE FOUND BY THE SABOTAGES RATHER THAN BY THE CODE. The first draft SKIPPED its own headline assertion when the slice was reverted -- *the stub saw nothing* has two causes and it collapsed them -- and a suite of skips exits 0, so the assertion the whole slice exists for could have been deleted with nothing noticing. And sabotage 1 CRASHED the suite rather than failing it, hiding twelve assertions behind a null read. ★★ AND THE APPEND-ONLY TABLE MADE THE SUITE'S OWN FIXTURE PERMANENT: the first run scored 26 green and crashed in the teardown on 23001, which is W409's finding met a second time and W404's a third. The sweep suspends user triggers in one transaction, superuser-only and therefore impossible on production. The spine verifier's five assertions have each been watched go red against a poisoned row, a widened chat_target_type_allowed and a removed catalogue entry. ★★★ DEPLOYED FROM A CLEAN TREE, BECAUSE THE WORKING TREE WAS NOT THIS SLICE'S. Another session was mid-build on W411 in the same checkout, sharing api/src/index.ts and hu.ts. W404 hit this two slices earlier and committed partial hunks; this went a step further and materialised the commit with git archive into a scratch directory, symlinked node_modules and deployed both halves from there, so the deployed API and bundle are the commit and nothing else. Probed: /health answered 37aea42, ea166f5 was nowhere in the answer, git show 37aea42 contains buildSeatInviteNotice while git show ea166f5 does not, and the served chunks carry this slice's strings in both languages while an invented control string AND W411's Hungarian are both absent -- the second being the proof that the clean tree is what shipped. D-1014, D-1015, D-1016, D-1017.* *Evidence: acceptance suite, 1 design doc.*
W413Every contact the project holdson production**THE OWNER, 2026-09-12: *Collect all the suppliers, workforce, people and stakeholders' contacts from the events of a project and create a combined list, just as you do with agenda and budget.* One function, app.contact_rollup_project(project_id, membership), a UNION over five populations, each row carrying the permission codes it needs in requires_view and requires_contact so the ROUTE filters per population instead of guessing a single gate for all five. ★★★ THE ANCHORED SURVEY FOUND FOUR TABLES AND THE ANSWER WAS FIVE.** It missed app.event_participant.guest_email, which is where WORKFORCE addresses actually live, so the screen would have shipped with its workforce section permanently empty -- and an empty section reads as *this project has no crew*, not as *this query is wrong*. A column survey anchored on the names you expect finds the tables you expected. ★★★ THE SCOPE GATE IS THE EVENT, NOT THE PROJECT, AND THAT WAS MEASURED. app.event carries one policy and it is TENANT-ONLY, with no scope in it, so where e.project_id = ... alone would have rolled up every event in the tenant into a project the caller may not reach. The function gates on app.membership_may_reach_event(p_membership, e.id), the same predicate gate 4b applies to a record. ★★ app.company HAS NO name COLUMN, and the rehearsal on a fixture caught it before a deploy did. api/src/index.ts:845 already records the identical mistake in its own comment: *typechecks, passes every unit test, answers 503 on every request*. The second time a codebase makes a mistake it has written down is the argument for rehearsing reads, not for reading harder. ★ Rows are returned through session.strip(), which OMITS a withheld key rather than nulling it, because a null falsely says *exists and is empty* (D-193). ★★ A GAP IS DECLARED RATHER THAN PAPERED OVER: there is no app.field_policy row for event_participant.guest_email, so field masking has nothing to say about the one column this slice newly exposes. Named here and in the owner-action list. *Evidence: 1 migration.*
W415A seat cannot change in silencecompleteTHE QUESTION COULD NOT BE ANSWERED WHEN IT WAS ASKED. On 2026-09-12 the owner asked why his account looked limited to one project, and *what was it before* was UNANSWERABLE FROM THIS DATABASE: fourteen statements in api/src/index.ts write app.membership and its scope tables and not one of them recorded anything. It was settled by branching the production Neon branch at a point in time -- a fine tool and NOT an audit trail, because it expires with the retention window, it cannot say WHO, and nobody reaches for it until something has already gone wrong. Every other consequential write in this product leaves a row; changing what a person may reach did not. ★★★ A TRIGGER, AND THE USUAL ARGUMENT IS NOT THE STRONGEST ONE. D-991's *a rule that must be remembered at each call site is one the next slice forgets* applies, but the decisive fact is that ESTATE WRITES app.membership FROM ANOTHER REPOSITORY -- a Worker-side audit could not see those statements at all, and half a trail is worse than none because it reads as whole. ★★★ INVOKER AND NOT SECURITY DEFINER, WHICH IS THE OPPOSITE OF THE REFLEX AND IS MEASURED RATHER THAN ASSUMED. app.audit_event FORCES row security and its insert policy is tenant_id = app.current_tenant() or app.is_platform_admin(); every writer already satisfies one arm -- withTenant sets the tenant, withService (the W409 sign-up path, which seats an owner in a tenant the session is not in yet) and withMaintenance set the admin flag, and a migration runs as neondb_owner, whose BYPASSRLS beats FORCE ROW LEVEL SECURITY where ownership alone does not. So a definer buys nothing and adds an elevated object to police. And the failure mode is the point: a future path satisfying neither arm does not skip the row quietly, its membership write FAILS. ★★ BOTH SIDES ARE RECORDED, AND NOTHING IS RECORDED WHEN NO ACCESS CHANGED. Either side alone answers *what is it now*, which the membership row already says; only the pair answers the question that could not be answered. And W412's invite is an UPSERT that touches the row on every re-invitation, so without the comparison the append-only table fills with rows saying nothing happened. ★ The role NAME travels with its id so the trail reads without a database, and the person is identified by id and never by address, because app.audit_event refuses DELETE and a copied address would outlive every erasure path reaching app.app_user. DELETE is not covered: W282 settled that revoking is a status change, and covering it would entangle this with tenant deletion, which W409 already records as impossible for the same append-only reason. *Evidence: api/test/w415-acceptance.mjs 19/0, forced red by four sabotages each reddening its own set -- the trigger dropped (10), the no-change comparison removed (3b alone), only the new side recorded (2b+4b), and the row's tenant compared against the session's (6a, 6b, 6c and nothing else). ★★★ THE FOURTH IS WORTH READING TWICE: that comparison is the obvious, careful-looking way to write this trigger, and it PASSES 16 OF 19 ASSERTIONS WHILE BREAKING EVERY NEW WORKSPACE. Without section 6 the suite would have been green on it. ★★ AND SECTION 5 WAS MEASURING NOTHING ON ITS FIRST RUN: it drove a direct write on the harness's own connection, which is a SUPERUSER with BYPASSRLS, so it proved the trigger fires and proved nothing about whether the audit insert survives row security -- the actual claim the invoker decision rests on. It runs under set local role app_rw now, the shape Estate writes in. ★ THE POST-FLIGHT'S OWN DRIVER WAS WRONG AND PRODUCTION CAUGHT IT: app.membership carries a COMPOSITE foreign key (role_id, role_class), so changing the id alone is refused by membership_role_class_fk and the trigger is never reached; every route already writes both columns, which is why nothing else had met it. The migration probe was forced to report ABSENT in three directions before being trusted, and its markers are quote-free with old.role_class::text appearing ONLY in the from-side. 1 migration on all three databases; the production post-flight drove a REAL seat inside a rolled-back transaction, leaving 6 memberships and 0 audit rows. No Worker change and no deploy. ★★ STILL SILENT: THE SCOPE TABLES, which both write routes replace wholesale, so *when did my scope become Oktober 23* remains unanswerable -- the exact shape of the question that started this. Named rather than built, because a wholesale replacement of N rows is a different record from a field changing value. D-1051.* *Evidence: 1 migration, acceptance suite, 1 design doc.*
W420The marketing plan can be written by handon production**THE OWNER, 2026-09-12: *A marketing tervet szabadon kell tudni letrehozni, moditani, az elemeit valtoztatni, torolni.* ★★★ A USER-AUTHORED LINE WAS UNREPRESENTABLE, AND THE SCHEMA SAID SO IN THREE PLACES AT ONCE. Measured on production first: app.marketing_plan held ZERO rows and app.marketing_plan_row ZERO, so the feature had never produced anything and could not have produced anything a planner wrote. (1) playbook_phase_id NOT NULL against a 44-row template table, so every row HAD to be one of those 44. (2) UNIQUE on (plan_id, playbook_phase_id) capped a plan at 44 rows and refused a 45th idea. (3) NO TITLE COLUMN AT ALL -- a row borrowed its words from app.playbook_phase, so nothing could be renamed and a row pointing at no phase had nothing to call itself. ★★★ AND THE UNIQUE CONSTRAINT NEEDED NO CHANGE, which was the one thing here worth measuring rather than reasoning about. Making the column nullable LOOKS like it breaks that constraint and does not: the index carries indnullsnotdistinct = FALSE, read out of pg_index, so NULLs are DISTINCT and (plan, NULL) repeats freely while one TEMPLATE phase per plan still holds. The pre-flight REFUSES TO RUN if that is ever not true, because it is a property of the INDEX and not of the schema as written, and the post-flight drives BOTH halves -- two hand-written rows coexisting, and the same phase refused twice with 23505 -- since either assertion alone passes with the constraint in the wrong shape. ★★★ THE MIGRATION ALONE WAS NOT THE FEATURE. readMarketingPlan joined app.playbook_phase with an INNER JOIN, so a hand-written row, which by definition has no phase, would have been silently dropped from the READ: the write succeeds, the list comes back without the new line, and the planner cannot tell a refused write from a dropped read. One word: left. Measured on production after the deploy, and the PAIR is what makes it evidence: over two identical hand-written rows the LEFT JOIN returns 2 and the old INNER JOIN returns 0. Asserting only that the left join returns 2 would ALSO have passed before the fix on any plan whose rows all carry phases, which is every generated plan. ★★ WHAT IS DELIBERATELY NOT RELAXED: playbook_code, track, seq and reason stay NOT NULL and a hand-written row carries real sentinels, because the screen GROUPS BY playbook_code and track -- nulls there would key a group on the string null and sort the row last as an unexpected track, so a line added to a track would vanish from the timeline being read. None of the four carries a foreign key, measured, so a value is free to invent. ★★ THE EDITOR EDITS WHAT THE LINE OWNS. A generated row's wording lives on app.playbook_phase, shared reference data for every plan on every event, so offering it here would let a planner correcting one event's timeline rewrite the framework under all of them. A generated line edits its dates, its note and whether it is in the plan; only a hand-written line edits its title, and the drawer SAYS so rather than showing no title box and reading as a bug. ★ title and notes travel as themselves and are NOT coalesced into phase: W411 paid for that shape already, since a display default in a field the editor reads back is a field that cannot be edited. ★ stage_order is RECOMPUTED rather than patched, because marketing_plan_row_date_shape refuses an undated row that has none, so clearing both dates would otherwise leave the row in the single shape the CHECK forbids. ★ A current plan is never replaced silently -- asking for a blank plan answers 409 and names the plan that exists, because regenerating is explicitly that act and asking for a blank one is not. ★ generated_at loses its NOT NULL so a hand-authored plan can say it was never generated; there is no origin column**, which would be derivable from that one and could then disagree with it. *Evidence: 1 migration, 5 structural self-assertions each forced red by its own sabotage, in both manifests; migration-ledger.sh prod exit 0 at 430 probes; apply-2026-09-12-w420-production.sh with 4 driven post-flight cases on a rolled-back fixture. ★ The driven assertions were MOVED OUT of the migration after the first draft: they build a fixture and the file ends in COMMIT, so a passing run would have left that fixture on production -- and spine-rerunnable loads every migration into an EMPTY database where no app.tenant row exists for them to use at all. ★ Two of the five sabotages first went red with Postgres's own message rather than the assertion's, which passes for ANY refusal; both name the assertion now. web 362/362, api 643/645 with the two pre-existing refusal-debt failures unchanged at 20 against 19 and naming the same 7 routes, checked rather than assumed since this slice adds five write routes. ★★★ A PRODUCTION PROBE, BECAUSE THE 401 PROBE PROVED NOTHING AND THE CONTROLS SHOWED IT: auth runs before routing, so an invented sibling route, an unmapped surface and a genuinely deployed route all answer 401 identically. What stands instead is /health reporting b03195a, that commit's own source, the served bundle (8 positives present, 5 invented controls absent, and a nonexistent chunk returning 200 with text/html, which is what makes the greps mean anything), and every handler statement executed against the live schema inside two rolled-back transactions, with an undefined_column control to prove the probe could still go red at the moment it reported green. ★★★ AND THIS SLICE AUTHORED THE DEFECT ITS OWN PHASE IS NAMED AFTER, found by OPENING THE SCREEN AND LOOKING after every probe above was green. Add a line here lives in a TRACK HEADER and the screen groups by playbook_code.track, so the group list is EMPTY at zero rows: the plan created by this slice's own *Start an empty plan instead* rendered with exactly ONE control on it, Delete this plan. The route answered 200 to curl the whole time and nothing on the page could reach it. web was 362/362 over it because every test and every probe here measures a route or a constraint and none of them opens the screen. Fixed, then driven end to end in a browser against a real database: empty plan, untitled line refused with the button DISABLED and zero rows written, titled line added, half-dated range rejected by a message that NAMES the rule, range completed, line renamed, line deleted with the empty-plan notice returning rather than a dead end, plan deleted. Dates round-trip exactly, so the day-early trap is absent on this path. ★★ A SECOND DEFECT, OLDER THAN THIS SLICE: all five track headings shipped ENGLISH on a Hungarian screen, absent from hu.ts and unwrapped, because test/i18n.test.ts greps for t("literal") and a heading arriving as t(runtimeValue) out of a Record is invisible to it -- a string a checker cannot see is a string it cannot report missing. Repaired by FORMATTING, since the same test collects a bare string alone on a line, and forced red by deleting one Hungarian entry to prove the green is real. D-1030, D-1031, D-1032, D-1033, D-1034.* *Evidence: 1 migration.*
W421One bin instead of a standing red slabcompleteTHE OWNER, 2026-09-12, THREE REPORTS ON ONE DRAWER AND ONE ON EVERY DRAWER. ★★★ TWO OF THE THREE HAD A CAUSE THE CSS MADE LOOK IMPOSSIBLE, so all of it was MEASURED IN A BROWSER before anything changed. *"Input fields are not aligned in one line"* -- they were not, and align-items: start was ALREADY SET, which is what made it look like it could not be happening. Measured: the three fields carried marginTop 0px, 16px and 16px, labels at 571/587/587 and inputs at 603/619/619, so the FIRST field sat 16px higher than its neighbours. .field + .field adds 16px so fields STACKED in a column get their rhythm; in a grid the gap already does that, so the margin is pure leakage -- and it lands on every child EXCEPT the first, which is exactly why the row goes CROOKED rather than merely low. .field-row has carried the cancellation since it was written and .form-grid was simply never given one. After: all three at marginTop 0, labels 556, inputs 588. ★★ *"the drawer is not wide enough"* -- 1240px, and the number was measured rather than chosen: .mb-sheet wanted 1139 of track and got 1005 inside the 1080 panel, short by 134, the twelve columns summing to exactly 1139 and the panel losing 74 to its own padding, so the floor is 1213. After: 1165 wanted, 1165 given, scrollbar gone. The overflow scroll is KEPT underneath, because below about a 1330 viewport max-width takes over and no width here can fit it. ★ *"make the units visible"* -- two questions, both answered: WHAT UNIT by a suffix carrying the line's own amount_unit and time_unit, as TEXT and not a select, since an actual recorded in another unit is not comparable; and WHAT AGAINST by the planned figure in the hint, which the panel above only showed once something had ALREADY been recorded. ★★★ AND THE DELETE SECTIONS: 44 PLACES WORE THE DESTRUCTIVE SKIN AND ONLY 14 WERE DELETIONS. The other 30 are .danger-zone used correctly as a coloured panel for something else -- copy this token now, a document approval workflow, taking a warehouse unit out of service, marking a purchase order paid, revoking access -- so a blanket sweep would have put a DELETE BIN ON "MARK PAID". DeleteControl is a new control the 14 adopt and the rest keep what they were always right to use. ★★ AN INLINE DISCLOSURE AND NOT A FLOATING POPOVER, measured too: .drawer-body is overflow-y: auto and CSS computes the other axis of a scroll container to auto as well, so a panel hanging off a bin near the FOOT of a drawer would be clipped by the container it sits in -- and these controls sit at the end of the body. *Evidence: web 362/362 after the i18n suite caught 10 Hungarian entries orphaned by the English that went with the Folds, all 10 removed; tsc clean; served CSS and the DeleteControl chunk verified against invented controls. ★★★ THREE BUGS FOUND WHILE CONVERTING, none of which any suite would have caught. MicrositeScreen's delete sits inside a <form> and Button sets no default type, so a bare confirm button is type=submit and "Keep it" would have SAVED THE RECORD. MicrositeScreen also carried useEffect(() => setConfirmDelete(false), [draft.id]), one line that is the only thing between a persistent drawer and the WRONG DELETION -- arm the bin on one record, open the next, and the confirmation survives with the new record's handler behind it; recordKey is therefore a REQUIRED prop, and it was forced red by sabotage: with the reset removed, line two opens already armed from line one. And focus lands on KEEP IT rather than the delete, because the confirmation appears under the keyboard focus ring at the moment it opens. ★ Two conversions GAIN a confirmation they never had -- the accreditation doors and kit tabs were one button in a red panel. ★ EventDetailScreen is deliberately NOT converted: openDanger() FETCHES what the deletion would take, 58 cascading tables and 16 that keep their rows and lose the link, and no generic control can show that -- W311 built it from this owner's own words, *egy kuka ikon mogott*; only its outer red panel stood open around a collapsed button and that wrapper is plain now. FormBuilderScreen is not converted either, its delete already being a confirm-only drawer. D-1035, D-1036, D-1037.* *Evidence: 1 design doc.*
W422A count steps by one, not by a thousandthcompleteTHE OWNER, 2026-09-12: *the amount at the event, when I click, should not be fractional numbers but whole numbers. When I click it from 1, it should step to 2. When I click it from 5, it should step to 6 instead of 1.000 to 1.001.* Reproduced on the real control first: two arrow presses from 1 landed on 1.002, because step=0.001 had been copied from the measurement fields three folds below, where a gram and ten square centimetres really are the right increment. A COUNT IS NOT A MEASUREMENT. ★★★ AND THE OBVIOUS FIX IS WRONG, WHICH THE BROWSER ITSELF SAYS. step is not only the spinner increment, it is a VALIDITY rule: measured on this very input with 2.5 in it, step=1 reports stepMismatch true and step=any reports false. The unit picker beside this box offers 72 codes with m, kg and m2 among them, so 12.5 metres of truss is a real quantity on a real event, and step=1 would have marked it invalid for the sake of tidying the arrows -- trading a visible annoyance for an invisible refusal. step=any measured: 1 steps to 2, 5 steps to 6, two presses down give 4, and 2.5 steps to 3.5, a whole unit added and the fraction kept. ★★★ THE FIRST SURVEY WAS A LINE-SCOPED GREP AND THEREFORE NOT A SURVEY: it required type=number and step= on ONE LINE, so every multi-line JSX input was invisible, and ClientQuotesScreen's recurring-invoice form wraps. The served bundle is what caught it -- the post-deploy check PREDICTED step:0.001 would be 0 in that chunk and measured 2. A check that names the number it expects can fail; *the fix is present* would have passed with two of the three counts still wrong. ★ A second trap in the same family: counting step=any across src/ returned 4 for 3 real inputs, because the new comments quote the attribute; the built bundle has no comments, which is the other reason the served count is the honest one. ★★ SponsorsScreen ent-qty WAS ALREADY step=1 AND IS RIGHT THAT WAY, which is the distinction worth keeping: an entitlement has NO UNIT PICKER, so integers are the only sensible value and the stepMismatch that would be a trap on the asset quantity is a feature there. The difference is the unit picker, not the word *quantity*. Measurements (0.001 on m2 and kg), money (0.01 on four price fields) and percentages (0.01) were all left alone, because none of them is this bug. *Evidence: web 362/362, tsc clean, every figure measured by driving the real control in a browser against a real database. The served chunks were checked against PREDICTED COUNTS rather than presence -- AssetsScreen any/0.001/0.01 = 1/2/1 and ClientQuotesScreen = 2/1/4, with two invented steps at 0 -- and all eight matched. ★ One inconsistency RAISED rather than quietly changed, and then decided the same day: ri-lvat stepped by 0.001 while vat_pct, the same field in the same file, stepped by 0.01. A percentage is legitimately fractional so neither was broken -- a question about precision rather than a defect, which is why it was reported instead of fixed. The owner answered within the hour, *Make the VAT steps consistent too. Use 0.01*, so both now read 0.01 and the file carries no step:0.001 at all, the three counts having moved to any. D-1039. D-1038.* *Evidence: 1 design doc.*
W423The six fields an event could not changecompleteTHE OWNER, 2026-09-12, on the event editor flyout, and then again while the measurement was still running: *tell me, then, how can I edit these fields? Where is the editor for all of this?* ★★★ THERE WAS NO EDITOR, AND THAT IS THE ANSWER. He named six fields; two of them, time_zone and attendance_expected, were already editable in the flyout's own When fold and always had been. The other four had NO WRITE PATH ANYWHERE ON PRODUCTION, measured rather than argued: host_company_id was READ by two list projections and written by nothing at all, with 0 of 30 production events carrying one; phase was written by nothing in the Worker and neither trigger on app.event touches it, so all 30 sat on the column default pre_event for ever; status had one writer, POST /events/status, which opens if (env.DEMO_AUTH !== "true") return 404 and is therefore a fixture for the w2 and w3 suites and a 404 on production; and currency was settable at INSERT only, 4 of 30. ★★★ AND THE FORM CARRIED A SENTENCE EXPLAINING A RULE THE PRODUCT DID NOT IMPLEMENT -- *Status and phase are not edited here: they move with the event itself.* Nothing moved them. That is worse than a missing field, because it tells the reader the gap is deliberate and stops them reporting it; the owner reported it anyway. Deleted. ★★ THE TWO LIFECYCLE VOCABULARIES ARE VALIDATED AGAINST pg_enum, not against a list in the route, which is the posture the time zone check beside them already takes against pg_timezone_names: the write path validates against the very table the column is defined by, so it cannot accept a value the column would refuse, and the refusal NAMES the alternatives. ★ Currency is uppercased rather than trusted, because character(3) PADS a short value silently instead of refusing it -- "hu" would store as "hu " and never match "HUF" anywhere. ★ phase and status are NOT NULL, so settable and never clearable, and the payload builder NEVER sends an empty one: "" means the form was never given one, which is exactly the create path. ★★ TWO DEFECTS FOUND ON THE WAY, OF ONE FAMILY. EventDetail declared host_company_name and NOT host_company_id, so the screen could not read a value it was already being sent -- the sentence that interface ALREADY CARRIES about project_id five lines down, the second time that exact defect has been found on one type. And OrganisationField rendered an empty search box and a select of SEARCH RESULTS ONLY, so a record that already had an organisation showed no sign of it: it had only ever been used to SET a value, never to EDIT one, and D-290's rule that a picker shows its current value or is disabled predated it being pointed at a stored column. *Evidence: driven end to end against a real database -- all four written in one request and confirmed in SQL, EVERY REFUSAL FORCED and each naming the rule and the alternatives (non-member phase, non-member status, null phase, two-letter currency, currency with a digit, non-uuid host, uuid absent from the directory), a positive control still accepted and the stored row undamaged, then the same four changed THROUGH THE FLYOUT and confirmed in SQL again, with the create drawer showing Host and currency and correctly NOT showing Status and phase. Served bundle 12/12 with two invented controls absent and the deleted hint confirmed gone in both languages. web 362/362, api 643/645 at the unchanged refusal baseline naming no events route. No migration: all six columns existed. ★ ONE PRE-EXISTING TRAP FOUND AND NOT FIXED, reported instead: /events/update re-validates the event's interval against its programme on EVERY update, so an event whose sessions already fall outside its dates cannot have ANY field changed -- including its currency. The first fixture tried refused the whole request for that reason. That is W131's containment guard working as built, and whether a currency edit should be blocked by a misdated session is a product question. D-1040, D-1041, D-1042.* *Evidence: 1 design doc.*
W424An edit that touches no date stops pretending to move the eventcompleteTWO OWNER REPORTS, 2026-09-12. *Yes, fix the internal check too. It shouldn't block unrelated edits.* ★★★ THE TRIGGER WAS RIGHT AND THE ROUTE WAS MANUFACTURING THE MOVE. app.event_move_refuses_orphans opens by returning early unless the dates actually changed, so a currency-only edit should never have reached its check. It did, because /events/update rewrote BOTH date columns on EVERY update from resolveInterval, which hands back the STORED value when the body omitted one -- and postgres.js hands a timestamptz to JavaScript as a Date, which holds MILLISECONDS. Measured on the event that refused: 2026-09-06 12:00:48.655986+02, a microsecond tail of 986 that cannot survive a Date, so the write put .655000 back and the trigger correctly saw a different instant. THE PRECISION IS LOST AT READ TIME, so no care in JavaScript could restore it -- the column must simply not be written. The fix is starts_at = starts_at when the caller sent no dates, which is not a tautology but a write that touches nothing, and it makes the SET honest: every other column there already meant *absent leaves the stored value alone* and these two only pretended to. ★★ BOTH HALVES DRIVEN, because either alone is worthless: the refused currency edit now answers 200 with the timestamp byte-identical and the tail intact, AND a real move that would orphan a session is STILL refused with the same sentence and the event does not move. Breaking the guard would have been far worse than the bug. ★★ AND SECTIONS NOW COME BACK IN TIME ORDER: they were ordered by line_order then name, and measured on the owner's own event ALL THREE CARRY line_order 0, so the tie broke alphabetically and the 11:05 section rendered THIRD, after the one starting at 16:00. The fallback to the section's earliest item is not optional -- only 5 of 27 production sections carry a starts_at of their own, so ordering on that column alone would have pushed the other 22 to the end in one silent reshuffle, a worse answer than the one replaced. A section is placed by when it ACTUALLY happens, and one with no time anywhere keeps its manual position, last. *Evidence: verified THROUGH THE ROUTE and not in SQL alone -- three probe sections whose times deliberately invert their line_order came back EARLY(order 1), LATE(order 0), UNTIMED, where the old rule gave LATE, EARLY, UNTIMED; probes removed and the fixture restored. On the owner's own event the rows now come back 09:00, 11:05, 16:00. ★ One self-inflicted break: the first version of the date comment carried TWELVE BACKTICKS in prose inside a SQL template literal, which ends it. verify-no-backticks-in-sql.sh scans only dash-dash lines and its own header admits a backtick in a block comment would pass it and still break the build; tsc caught it. api 643/645 at the unchanged refusal baseline, web 362/362, tsc clean, no migration, no web change. Deployed and /health answers c06c95c. D-1043, D-1044.* *Evidence: 1 design doc.*
W425A session can hold sessions, and a section can describe itselfon productionTWO OWNER REQUESTS IN ONE MESSAGE, 2026-09-12: *sub-agenda line items (sessions) within an agenda line item*, and *give Sections the same details, descriptions, etc. what we have at the Session level*. Measured first: app.agenda_item carries 34 columns and nine are descriptive; app.agenda_segment carried TEN and not one was descriptive -- a section had a name, a clock and nothing to say for itself. And an item had no parent: agenda_segment_id is the SECTION it sits in and anchor_item_id is W149's TIMING relationship, and neither is containment. ★★★ ONE LEVEL, ENFORCED RATHER THAN INTENDED, because three views render this programme and an n-level list needs a collapse model, a depth cap for the calendar and a decision about what a grandchild means in a room column -- none of which was asked for -- and because depth crossed with W149's anchoring is where the cycles live. ★★★ THE RULE HAS TWO HALVES AND BOTH ARE NEEDED: refusing a child whose parent is already a child is the obvious one, and the other is that an item which ALREADY HOLDS CHILDREN must be refused a parent, or the second level arrives by the other door. Cases 2 and 3 drive them separately because either alone passes against a trigger written half way. ★★ NO TIME INHERITANCE IN THE DATABASE: a sub-item carries its own times or none, and one with none is shown inside its parent's block BY THE SCREEN -- writing the parent's times onto the child would be a stored derivation that goes stale the moment the parent moves, which is what W411 paid for in a different column. ★ The section's nine are the SAME nine under the SAME names, asserted, and internal_notes is masked by a ROW in app.field_policy rather than by a copy of the rule. ★★ ONE COMPONENT, MOUNTED TWICE: the item drawer's three folds became DescriptiveFields and both drawers mount it, because two copies of the same JSX are two drawers that ask the same nine questions differently within a month -- one gains a hint, one forgets the mask. ★★ AND THE RENDERER EMITS AN UNPARENTED CHILD RATHER THAN DROPPING IT: the database enforces the same AGENDA and deliberately not the same SECTION, but ItemList renders one section at a time, so a child whose parent sits elsewhere would be filtered out of every list and VANISH. A row that exists and is drawn nowhere is the worst outcome available. *Evidence: 6 structural assertions each forced red by its own break and each naming itself. ★★ The first sabotage run found TWO OF MY OWN ERRORS rather than the migration's: sabotage 4 killed the file on a Postgres FK error BEFORE reaching its assertion, and sabotage 6 tripped assertion 5 because the restore re-ran a migration whose if not exists (conname=...) guard SKIPS a wrong-shaped FK instead of repairing it -- correct of the migration, wrong of the restore. Production pre-flight 27 sections and 162 items, post-flight identical, 9/9 columns, mask row, cascading FK and armed trigger, and SIX DRIVEN CASES on production's own data inside a transaction that always rolls back, leaving 0 probe rows. Ledger 431 probes, exit 0. ★ The probe kind was trigger first, which the ledger does not know -- its dispatch ends *) blocked unknown probe kind, and BLOCKED means nothing was measured -- so it probes the FUNCTION. Driven end to end in a browser: a section created with all nine and read back with its folds counting 4 of 5, 3 of 3 and Written; a sub-session rendered indented under its parent with its own badge; a grandchild refused THROUGH THE ROUTE as EC425/409 rather than a 503. Served bundle 11/11. web 362/362, api 643/645 at the unchanged refusal baseline. ★ Declared and not fixed: drag-reordering across a nesting changes a row's position but not its parent. D-1045, D-1046, D-1047.* *Evidence: 1 migration, 1 design doc.*
W426A session says who and what is on it, from its own sidecomplete**THE OWNER, 2026-09-12: *I want to add related performers, assets, workforce, etc. with 2-way connection to those data.* ★★★ THE TWO-WAY CONNECTION WAS NOT BUILT HERE -- IT ALREADY EXISTED, AND THAT IS THE WHOLE DESIGN. Every link this panel writes is a row in a join table the OTHER side already writes: app.performer_assignment from /performers/assign, app.participation_agenda_item from /workforce/assign, app.asset_agenda_item from /assets/allocate. So this slice adds NO WRITE ROUTE AT ALL: the drawer calls those six existing routes, which is what makes the connection two-way BY CONSTRUCTION rather than by synchronisation. A session-side editor with its own tables would be a second store of one fact, and two stores of one fact disagree within a week -- W406 made exactly this call for money, where the Mini Budget attaches a cost from the record's own side and writes the same app.budget_line the Budget screen writes. WHAT WAS MISSING WAS NEVER THE WRITE. It was the READ from this side, and the door -- three complete write paths reachable from one direction only, which is this phase's recurring finding at its third scale. ONE NEW READ, GET /agenda/items/links, fetched when the drawer OPENS rather than loaded with the agenda, which is MiniBudget's own rule: a panel that fetches on mount must not render behind a closed drawer or it costs one request per record on every list. ★★ mayDo AND NOT require FOR THE THREE**, following what GET /agenda already does for its performers column: a caller without assets.view still gets the session's performers, the array comes back empty and the FLAG says why, because empty and *you may not look* are different facts and rendering them identically states something false. Each section is gated separately because the three permissions are separate. ★ The choices are scoped to the EVENT and not the tenant -- a performer who is not on this event cannot be put on one of its sessions, and offering them would be offering a refusal -- and they are capped at 300 and SAY SO, because trap 2 is a capped list a screen believes is complete. ★★ A SECOND-LEVEL DRAWER AND NOT TABS, and the owner offered all three shapes: this is the one the product already has, W108's wide panel built on this owner's own argument that a record's attached detail needs room the record drawer does not have, opening OVER the session so it is still behind you when it closes. Tabs inside a 460px panel would give three lists a third of the width each. ★ Offered only once the session EXISTS, because every link needs an agenda_item_id to point at. ★ One writer for all six routes, and it RE-READS rather than patching the local copy, because a link can be refused by a rule this panel does not know. *Evidence: driven end to end against a real database -- the panel opened with three sections and real candidates, a crew member and an asset were added THROUGH IT, and the rows were then read back OUT OF THE TABLES THE OTHER SCREENS USE, which is the two-way claim measured rather than asserted. Both were removed again through the panel, the database returned to 0 and 0, and the performer nobody touched survived. Route controls: non-uuid 400, unknown id 404. Served bundle 10/10, including that the panel calls the four EXISTING write routes and that no invented link route or second store appears. web 362/362 after the i18n suite named 13 missing strings a few at a time, api 643/645 at the unchanged refusal baseline, tsc clean, no migration. ★ Declared and not built: the owner's *etc.* -- budget lines also carry an agenda_item_id, and documents, checklists and risks do not. And the panel edits the LINK and not its detail: a performer's call time and a crew member's coverage role stay on their own screens, because editing them here would mean writing columns those screens own, which is the distinction the design rests on. D-1048, D-1049.* *Evidence: 1 design doc.*
W427The programme list says how much of each session to showcomplete**THE OWNER, 2026-09-12: *Here please show the short an long description of each session in detailed view. In compact view, hide them, and display only the title.* A Compact / Detailed pair beside the existing List / Calendar / By-room switch, DEFAULTING TO COMPACT because a density control that changes the screen on first sight has already made a decision for the reader -- compact is what the screen did yesterday. ★★ NOT TWO NEW COLUMNS, and both reasons were measured rather than assumed**: a long description is written only for the sessions that need one, so the column would be empty on most rows; and the run sheet is ALREADY scrolled horizontally at this width, so two more columns would push the descriptions off the right edge of the very view meant to show more. They render UNDER THE TITLE in the name cell instead. ★ The toggle appears on the LIST view only: calendar blocks and room-grid cells are sized by TIME and have nowhere to grow, so a density control there would either do nothing or break the geometry. ★ long_description keeps white-space pre-wrap, because a description written in paragraphs was written in paragraphs on purpose. *Evidence: driven in a browser on a session carrying both -- compactHadShort false, compactHadLong false, detailedHasShort true, detailedHasLong true. ★★ THE TWO NEGATIVES ARE THE HALF THAT MATTERS: a toggle that ADDS text is easy to believe, and a toggle that HIDES it is the one that silently does nothing. Served bundle from app.event.clinic with the AgendaScreen chunk BYTE-IDENTICAL to the build. web 362/362, tsc clean, no migration and no route change. ★ Sections carry the same nine descriptive fields since W425 and this toggle does not expand them -- declared, not built. D-1052.* *Evidence: 1 design doc.*
W428The link panel can create the record it is about to linkcomplete**THE OWNER, 2026-09-12: *Adj lehetoseget itt ne csak adatok osszekapcsolasara, hanem uj adatok letrehozasara is.* W426 built the panel that says who and what is on a session and it could only link what the event already had, so a performer who turns up on the day meant two screens for one act with the session lost in between. Each section now carries a name field and a Create and add button chaining the routes the other side already owns: /performers/new into /performers/assign, /workforce/new into /workforce/assign, /assets/create into /assets/allocate. STILL NO NEW WRITE ROUTE, so the two-way connection stays two-way by construction, which is D-1048 unchanged. ★★★ TWO REQUESTS AND NOT A TRANSACTION, AND THE MESSAGE SAYS SO.** There is no server-side create-and-link and this slice deliberately did not add one, so the second call can be refused after the first has already happened -- a frozen agenda, a clash, a rule this panel does not know. That case gets its OWN SENTENCE saying the record EXISTS ON THE EVENT and is simply not attached here. Reporting it as *that could not be created* would be false and would send the reader to create the same person A SECOND TIME, which is how a crew list ends up with two of somebody. A third sentence covers the rarer shape where the create succeeded and its id could not be read back. ★★ A TRANSACTIONAL ROUTE WOULD BE A FOURTH WRITER for tables three routes already own, and would have to re-implement each of their validation rules or skip them: the failure it prevents is rare, recoverable by one more click, and VISIBLE, which is the trade the honest message makes explicit rather than hides. ★ Only a NAME is collected, because the full form here would be its third copy. *Evidence: driven through the panel in a browser against a real database -- a performer created with Create and add appeared in the linked list with no error notice, and was then read back OUT OF app.performer_assignment joined to app.agenda_item, 1 assignment on the session it was created from. Probe rows removed and the fixture restored. Served bundle: three create fields and all three create routes, one each. web 362/362, tsc clean, 6 Hungarian entries. ★ The *etc.* from W426 is still the *etc.* -- budget lines carry an agenda_item_id and have no section here. D-1053.* *Evidence: 1 design doc.*
W429The play button opens on the event's days, and a run can go back to zerocomplete**THE OWNER, 2026-09-12: *A play gomb csak a rendezveny napjan jelenjen meg. Amennyiben megy mar a program, legyen lehetoseg megallitani, vagy visszaallitani nullara is a futast, mintha megsem kezdodott volna el a program.* Two rules, in one function both live surfaces now ask. ★★ START IS OFFERED ON THE EVENT'S OWN CALENDAR DAYS, because a run sheet a month out carried a play button on every line and pressing one stamps a real actual_starts_at that the Actual column, the live badge and every later question about when the show began all believe. COMPARED BY CALENDAR DAY IN THE EVENT'S ZONE, NOT BY INSTANT: on the day means FROM MIDNIGHT, so the caller running a sound check at 09:00 for an 18:00 concert is on the event's day. instantToLocalInput with withTime false is the one place in this app that reads an instant as a wall-clock day in a named zone, and it returns YYYY-MM-DD, which sorts lexicographically in date order -- so the bounds are two string comparisons and no Date arithmetic is involved. AN OPEN END BOUNDS NOTHING, W105's rule for these same two columns as picker bounds, so an undated event KEEPS its play button rather than silently losing it. ★★★ AND END AND CLEAR NEVER CONSULT THE CALENDAR, WHICH IS THE SAFETY PROPERTY. Gating them on the same date looks symmetric, reads as tidy, and would leave a show that crossed midnight RUNNING FOREVER with no control on the screen that could end it. ★★ THE RESET IS NOT A SECOND WASTE BIN: back to zero is a state reset and not a removal, and DeleteControl's own header already counted 44 places wearing the destructive skin of which ONLY 14 WERE DELETIONS. A row whose first bin deletes the session is not the place to put a second one that erases a clock. It is not confirmed either, on the same authority -- the Run of Show tab has shipped an unconfirmed Clear since W23, every call writes an audit row, and the control appears only when there IS something to reset. ★ HIDDEN, NOT DISABLED**: the owner's word is *jelenjen meg*, and a greyed-out Start still reads as a button somebody is failing to press. ★ The route needed nothing: POST /agenda/items/live has taken start, end and clear since W23, and clear was reachable from the Run of Show tab ONLY -- a complete write path reachable from one direction, this phase's recurring finding for the third slice running. *Evidence: 12 unit cases, 8 of 8 sabotages red for their own named reason. ★★ THE HARNESS FOUND MY OWN TEST RATHER THAN THE RULE: case 8 claimed to separate the event's zone from UTC and asserted something TRUE UNDER BOTH READINGS, so replacing the zone with UTC left all twelve green. A test that cannot fail for the reason it names is worse than no test, because it is counted as coverage. Rewritten around a bound that actually crosses midnight in UTC -- a Tokyo all-nighter finishing at 08:00 is 23:00 on the TWELFTH. Driven on BOTH surfaces in a browser: Start to End plus Clear to Start with the database read back NULL and NULL; the event window moved into the past removed the play button AND NOTHING ELSE, pencil bin lock and grip surviving one each; and a session left running four days after its event ended still offered both controls. Fixture restored to the microsecond. Served bundle 19/19 with five invented controls absent and the AgendaScreen chunk byte-identical to the build. web 374/374, tsc clean. ★ Declared and not built: the window is the EVENT'S and not the session's, so a multi-day event offers start on all its days; and an event whose recorded end is earlier than its real one loses the play button for unstarted sessions, since a grace period is a number nobody asked for. D-1054, D-1055.* *Evidence: 1 design doc.*
W430A session carries its own money, and a cost's lens follows its ownercomplete**THE OWNER, 2026-09-12: *Build all these 2-way connections on each drawer-editor for all data type the same way.* ★★★ THE MEASUREMENT CHANGED THE SLICE. Opening each drawer rather than reading the code found that the three types he named ALREADY HAD IT: the performer's SESSIONS fold with Assign, Move and Take off, the asset's ALLOCATED TO SESSIONS, the crew member's ON THE PROGRAMME. W426 was the missing half and it shipped, and a second panel on those drawers would have been a second editor for one fact**, which D-1048 refuses. So the survey went to the SCHEMA -- every foreign key into app.agenda_item, nine across six tables -- and the one link with a door on one side only was app.budget_line, the owner's own *etc.* from W426, declared unbuilt twice. MEASURED ON PRODUCTION: 0 budget lines owned by a session against 95 in total. MiniBudgetOwnerType has listed agenda_item since W108, the API validates it, and AgendaScreen imported MiniBudget ZERO times -- a complete write path with a door on one side only, the fourth slice running. ★★★ AND MOUNTING THE PANEL ALONE WOULD HAVE SHIPPED A SILENT DEFECT. app.budget_line records what a cost belongs to TWICE: owner_type/owner_id plus app.budget_line_owner say WHO OWNS IT with shares, while event_space_id/engagement_id/agenda_item_id say WHAT IT IS FOR and are what the Budget screen's lenses GROUP on. MiniBudget writes only the first, so a cost attached from a session would have SUCCEEDED, SAID SO and shown in the session's own panel while the Budget screen filed it under *Unassigned* for ever. Both halves correct; only the pair wrong. THE CONTROL IS WHAT MAKES IT A FINDING: feature_category_id is set on 83 of 95 production lines and event_id on 94, so the lens mechanism WORKS, while the other three are ZERO. ★★ Fixed AT THE ROUTE and for ALL THREE lens-bearing types rather than in the panel and only for this one, because a guard where every caller routes through is a smaller diff than a guard in each caller. It fires only where the two columns are the same fact spelled twice -- a line owned by a PERFORMER is untouched, since *for which session* is then a separate question -- and an explicit null still means clear. ★ AND THE COUNT WAS A STRING: count(*) is a bigint and postgres.js returns it as text, so the fold read *0 lines* where it should have read *No cost attached* because "0" is truthy. Every non-zero count would have rendered right BY LUCK; the only revealing case is the one a new session is always in. Found by opening the drawer. *Evidence: driven end to end against a real database and then WITH THE DERIVATION DISABLED as the control -- the identical action through the identical UI left agenda_item_id NULL, the defect demonstrated rather than argued. 7 new api tests, 5 of 5 sabotages red for their own named reason including a negative control that an owner type with NO lens column derives nothing, without which the suite would pass equally against a blanket copy-into-every-column loop. Production post-flight inside a transaction that always rolls back: 0 lines on a session before, the old behaviour rendering *Unassigned*, the new rendering a real session name from the owner's own event, 0 after the rollback. Served bundle 7/7 keyed on ownerType agenda_item appearing EXACTLY TWICE with every other owner type at zero. /health 6acbbca. web 374/374, api 650/652 at the unchanged baseline. ★ One self-inflicted break: a backtick in a SQL template comment, the second this phase; the guard DOES catch it, measured both ways, and the gap was running tsc first. D-1056, D-1057.* *Evidence: 1 design doc.*
W431Both agenda tabs draw a session's controls the same waycomplete**THE OWNER, 2026-09-13, on the run of show: *now the edit and delete icons are disappeared, the play and stop icons became buttons with text. I did not ask for this.* ★★★ MEASURED BEFORE ANYTHING WAS CHANGED, because the words described a regression and the history described something else. Production served the W430 build byte for byte; the greyed End and Clear in the screenshot came from a browser tab running JavaScript from before the W429 deploy, since the served code hides those controls; the run of show had drawn Start, End and Clear as text buttons since W22, and at none of the 35 commits in the screen's history did its rows carry an edit or a delete. Nothing disappeared -- but two tabs drew one session's controls two ways, and a reader moving between them sees something break. ★★ W429 IS WHERE IT SHOULD HAVE BEEN CAUGHT**: it unified the RULE behind the two tabs and left them DRAWING it differently. SessionActions now draws both, lifted unchanged out of the Programme list. ★ Edit from the run of show opens the agenda read's copy of the row, because only that read carries W430's budget_line_count. ★ *Clear* rendered in Hungarian as *Törlés*, which means delete, on a button that resets a clock; the reset icon carries its own label, and the orphaned Start and End entries were removed. *Evidence: 1 design doc.*
W432An event space carries its own Mini budgetcomplete**THE OWNER'S STANDING REQUEST, 2026-09-12: *Build all these 2-way connections on each drawer-editor for all data type the same way.* ★★★ IT STARTED BY FINDING THIS SESSION'S OWN ERROR. D-1056 said a session was the last of the six owner types the Mini budget declares to get a screen; measured across every mount, four of the six had one, and event_space and engagement had none. The claim came from the drawers that had been opened, not from the mounts. ★★ WHY THE SPACE AND NOT THE ENGAGEMENT, MEASURED ON PRODUCTION: 31 event spaces with a real editor and 0 costs, and the Budget screen's Space lens -- writable since W430 -- with nothing to fill it; against 1 engagement and no route or screen that creates or edits one. ★★ THE TEST IS THE DURABLE PART: mini-budget-owner-coverage.test.ts enumerates the declared owner types and fails if one has neither a screen nor a stated reason, and again if a reason outlives its fact. ★ W430's own lens test stayed green with the derivation disabled behind a false guard; it is anchored on the whole guard now. ★ SHIPPED IN TWO HALVES**: the web on its own deploy, and the /events/detail line count inside another session's deploy of main at 133a8b7, after that session's migration reached production -- held rather than shipped ahead of it, because the Worker bundles the checkout. *Evidence: 1 design doc.*
W433A seat's reach cannot change in silence eithercompleteW415 MADE A ROLE CHANGE LEAVE A ROW AND LEFT THE SCOPE SILENT, AND THE SCOPE WAS THE QUESTION. On 2026-09-12 the owner asked *when did my scope become Oktober 23.* and after W415 it was still unanswerable. Owner, 2026-09-13: *do the scope tables too*. ★★★ THE AUDIT COULD NOT BE ADDED UNTIL THE ROUTES CHANGED. /memberships/invite and /memberships/update deleted every scope row and inserted the requested set again, so EVERY save -- including one that changed nothing -- passed through a moment with no scope rows, and a seat with no scope rows reaches everything. A correct trigger fed those routes writes a false widening on every click into a table nothing can delete from. writeSeatScope writes the DIFFERENCE, every addition for BOTH tables before any removal; table by table is not enough, because a save moving a seat from an event scope to a project scope still passes through the false moment. ★★★ scoped_after IS THE GATE'S OWN app.membership_is_scoped(), NOT A RULE ABOUT THE EMPTIED TABLE -- measured in a rolled-back prototype before the file existed: removing a seat's last EVENT while a PROJECT remains must read still scoped, and the obvious rule writes a widening that never happened. ★★ Statement-level triggers with transition tables, one row per membership per statement, carrying what was added or removed and the whole resulting set, so every row is a snapshot as well as a delta. ★★ A DEFECT FOUND BY REORDERING: /memberships/update changed the role and replaced the events before refusing an unknown project, and returning from the withTenant callback COMMITS -- a 400 kept all of it. Both scopes are validated before anything is written now. ★★★ AND ONE RECORDED RATHER THAN FIXED: both scope tables reference their parent ON DELETE CASCADE, so deleting the only event or project in a seat's scope silently widens that seat to everything its role permits. /events/access refuses to remove a last scope row by hand; the cascade never asks. On production two ACTIVE seats are one deletion from that, both scoped to Oktober 23.; W433 makes the widening a row with scoped_after false. *Evidence: api/test/w433-acceptance.mjs 25/0, forced red by five sabotages each reddening its own set -- the triggers dropped (12), the old delete-then-reinsert routes (2b, 3b, 3d, 3e), a difference written table by table (3d and 3e alone), scoped_after from the emptied table (3c and 3d alone), and the validation moved back after the write (6b, 6c, 6d, with 7a and 7b red as collateral of the half-committed save). Regressions w282 29/0, w288 50/0, w392 11/0, w412 32/0, w415 19/0, shared-event-write 181/0. The probe forced ABSENT three ways. ★★ The dev post-flight SKIPPED on its first run -- no dev seat held a project scope -- and a skip is not a pass, so the driver builds its own baseline inside the rolled-back transaction and asserts a sixth check. ★★★ DEPLOY ORDER IS THE SLICE: the route shipped first as main 133a8b7, from a clean git archive behind four gates -- the commit's markers, production's build an ancestor so nobody was rolled back, no pending migration but W433, a clean typecheck -- with /health answering 133a8b7, 6acbbca absent, and writeSeatScope 0 in the old build and 1 in the new; only then the triggers, and the production post-flight drove a real seat through six checks and left 0 rows. ★★ Two gates stopped the deploy before it shipped anything, both correctly: zsh reads $SHA:a as a path modifier and handed git a nonexistent revision, and a W432-free check tripped on another session's legitimately committed W432 lines. ★★ Renumbered from W432 after finding that number in another session's uncommitted hunks in the same file; that session's backup-copy restore then reverted this slice's comment renames, so 8f89761 committed them as W432 and 133a8b7 corrects them. D-1060.* *Evidence: 1 migration, acceptance suite, 1 design doc.*
W434A performer's day has three times: arrival, green room, performanceon production**THE OWNER, 2026-09-13: the performance time takes the session's start as its default, arrival means arrival at the venue, and there is a green room time -- *ez a call time?* ★★ THE ANSWER SHAPED THE COLUMNS.** A call time is when someone must report, which is what the Hungarian label already said, so call_time_at keeps that meaning and gets no default; being ready in the green room is a later call and gets the new green_room_at. D-628's fifteen minutes -- *a default like 15 minutes before the start of the performance* -- describe readiness, so they move to the green room. Measured first: 0 of 4 production assignments carried a time. ★★ NEITHER DEFAULT IS STORED: an empty time means the rule applies and the screen draws its answer, so a rescheduled session takes its performers' default times with it. ★ One server rule, readyTimesError, in the order the times happen; an arrival is held to a green room time only when one was set. ★★★ AND A ZONE DEFECT THE SLICE WOULD HAVE MADE WORSE: the performer editor showed stored times as UTC wall-clock and parsed typed ones in the browser's zone -- the pair the agenda fixed on 2026-08-06, still live here. Measured in a Budapest browser on a London event, three open-and-save trips moved 09:14Z to 07:14Z, 05:14Z and 03:14Z. ★ An empty arrival field says Not set instead of DateField's format example, which the owner had read as a date somebody set. *Evidence: 1 migration, 1 design doc.*
W435The branding form lays its fields out in the form gridcomplete**THE OWNER, 2026-09-13, on the Approval screen's branding tab: *kérem javítsd a formázást, a sorok kezdődjenek azonos magasságban.* The five branding fields sat in .field-grid, which W348 made the dt/dd list layout: a 132px first column, and the stacking margin of .field + .field reaching every field after the first. So the first row's second field started 16px below the first. W348 moved six screens to .form-grid and this one was not among them, and nothing tested the split. ★★ THE FIX IS A CLASS NAME AND THE TEST IS THE SLICE. form-grid-holds-forms.test.ts finds every element carrying field-grid and fails if a form control sits INSIDE it. Inside and not near, because PortalQuotes keeps a real dt/dd list with a price input just after it. It went red on the unchanged screen naming MicrositeScreen. ★ THE FIRST TWO PRODUCTION PROBES WERE BLIND.** They asked for MicrositeScreen-*.js, Vite had put the screen into the chunk named after AssetProductionBoard, and so the probe fetched the single-page fallback, 16048 bytes with a 200, and every count read zero. *Evidence: 1 design doc.*
W436A performer chip stays inside its column and says a name oncecomplete**THE OWNER, 2026-09-13, on the programme: *a zöld chip ne lógjon át a következő oszlopba, és ne takarja ki a következő oszlop szövegét.* Measured before the change at 1440px: a chip 415px wide in a 208px cell, ending 219px past it over the Room column. .badge is nowrap, which is right for a status and wrong for a name. ★★ WRAPPING IS OPT-IN**, so *In progress* still never breaks in two: Badge gains wrap, the programme's performer chip uses it, and its dot keeps to the first line. ★ AND THE CHIP STOPPED SAYING A NAME TWICE. The owner's run sheet read *Nemzeti Filharmonikus Zenekar — Nemzeti Filharmonikus Zenekar*, a title equal to the performer's own name. Equal means equal after trimming, spacing and case, and a title that merely contains the name is information and stays. *Evidence: 1 design doc.*
W437A space has its own page, with its programme, people, assets and moneycomplete**THE OWNER, 2026-09-13, on the Spaces tab: *add tab menus with associated assets, performers ... the filtered list of the associated assets and the associated workforce and performers ... the sum of how much that space costs, how many people associated to work there ... space dimensions summary, etc. Be creative.* A space's name opens its page at ?tab=spaces&space=<id>, with Overview, Programme, Performers, Workforce, Assets and Costs in ?view=, because the event page's bar owns ?tab= and a second bar writing it would throw the reader off Spaces. ★★ WHAT HERE MEANS IS DECIDED ONCE. GET /spaces/detail reads the space's tree once and every section filters on it: a session by its own room or else its agenda's, a performer based here or booked into such a session, crew covering such a session or staffing a desk here and never a guest, an asset placed here or used in such a session. ★★ MONEY COUNTS ONCE, AT THE SHARE OWNED HERE**, or whole when only the budget's Space lens names the space, and amounts are stripped at gate 5 while the lines still show. ★ The overview says what is unknown: floor per guest, capacity use, asset floor coverage, power, weight, setup time and the build window, each sum telling how many of its inputs were recorded. *Evidence: acceptance suite, 1 design doc.*
W438A space shows its access zones, who may reach it and who came incomplete**THE OWNER, 2026-09-13: *what access zones are associated with that space, how many people have access to it, how many people checked in into that area*. ★★ THE LINK TABLE HAD NO WRITER. app.access_zone_space shipped with W296 and held 0 rows on local and production, so no screen could tie a zone to a room. POST /checkin/zones/spaces sets or clears one link, both ends checked on the event first, and the gates screen's zone drawer and the space page's Access view tick it from either end. ★★ ONE PREDICATE. Who may enter a zone was decided inline in the scan engine; it moved unchanged into app.access_zone_admits, which the engine and the count both call, and a source test fails if a copy returns. ★★ EVERY ZONE AROUND A SPACE, NOT ANY**: a wing inside the venue and backstage is reached only by a ticket both admit, and a space no ticketed zone covers is unguarded. The view also carries check-ins at the space, people inside, average stay and the doors standing there. ★ The seventh view exposed the tab bar placing its ink by offsetLeft, which sat under the first tab while a More-menu tab was open. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W439A shift happens somewhere, and a space tracks its cleaningcomplete**THE OWNER, 2026-09-13: *cleaning shifts tracker*. ★★ A SHIFT HAD NO PLACE. app.participation_shift carried who, when, the role and a state, so a cleaning shift could be booked and never be said to be in the hall it cleans. w439 adds event_space_id, optional because most shifts are for the whole event, and ON DELETE SET NULL so a removed room keeps its people booked. The shift routes refuse a room from another event and keep a place an update does not mention; the shift board, a person's shifts and both shift forms carry it. ★★ THE TRACKER** on the space page's Shifts view lists the crew and team shifts worked in the space and the spaces inside it, marks cleaning by role code, and says who is cleaning now, the next cleaning shift, the hours booked, the share of the programme with a cleaner on and the gaps with none, merging overlaps so two cleaners cover an hour once. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W440A sign-up refusal is handled at its own routecomplete**THE 2026-09-13 HANDOVER, ITEM 2, AND THE OWNER'S SECOND NAMED ITEM: *the 503 that POST /me/workspace returns on a coded refusal, which holds the API battery at 663 of 665*. ★★★ IT WAS NOT A 503, MEASURED BEFORE A LINE WAS WRITTEN. Driven on local with the Tenant owner template role retired for one call, the route answered 409 with EC441's own sentence and left no tenant behind, because W394 put asRefusal in the Worker's outermost catch on 2026-09-10. The 503 came from the failure message of refusal-handling.test.ts, which W394 made untrue and nobody reworded, and the handover repeated it. What the missing handler did cost: the outermost catch logged a refusal the schema raises on purpose as unhandled: /me/workspace, and the detector held the API battery red, where the next real failure would have hidden. One .catch on the withService call ends both. ★★★ THE SABOTAGE FOUND A HOLE IN THE DETECTOR.** It decided a route was handled with block.includes('asRefusal'), and the comment above the new catch names the helper, so with the catch deleted and the comment kept the file stayed 5 of 5 green. It reads only lines that are not comments now, which moved no verdict among 47 write sites on HEAD or on the tree, and a control test holds both directions. *Evidence: the detector red at HEAD naming the route, green on the fix, S1 green against the old detector and red against the corrected one, every run on a scratch copy of index.ts. Drive after the fix: the same 409, and 0 unhandled log lines where the same call before the fix added 1, control 201, both provisioned workspaces and users deleted afterwards. API 666/666 from 663/665. Commit 39c263b, D-1069.* ON PRODUCTION: /health moved from 1b3f588 to bc0a3fd, Worker version d2c6c7eb, the handler marker is in bc0a3fd and absent from 1b3f588, and an invalid build string is absent. *Evidence: 1 design doc.*
W441A project offers its cost and income centres to all its eventscomplete**THE OWNER, 2026-09-13: *Please make the cost centers and income centers available accross all the events through a project. Also, save the center names at the tenant level and make them available as templates or recommendations from past events*. ★★★ THE NAMES WERE ALREADY SAVED AT THE WORKSPACE AND NOTHING COULD USE THEM, MEASURED BEFORE A LINE WAS WRITTEN. app.cost_centre and app.profit_centre carry tenant_id and a unique code, but no route returned or wrote a line's centre, no project could name one, and production held 0 centres and 0 filed lines. The single-column keys on app.budget_line accepted another workspace's centre, because Postgres checks a foreign key with row security bypassed, so the migration made them composite and added budget_line_centre_matches_direction before any route opened the columns. app.project_centre names the centres a project offers. GET /budgets/centres answers by project or by budget with each centre's place in the project and its use on lines and in projects, which is the recommendation, and POST /projects/centres/copy takes a past project's set as the template. The project page adds, creates and withdraws a centre, and the line drawer offers the project's centres first. ★★ RENAMING A RETIRED CENTRE BROUGHT IT BACK.** The update wrote is_active ?? true. Once that was fixed its audit event still did, found while narrowing sabotage S5, and check 9b with sabotage S7 now hold it. *Evidence: a migration with 7 assertions, suite 33 of 33, 7 sabotages each red on its own check with index.ts matching its clean fingerprint after each, API 666/666, web 429/429, battery 208 passed and 0 failed at 5312719, and a drive on local through the project page and the line drawer. Commit 5312719, D-1071.* ON PRODUCTION: the migration was applied before D-1072 with its three refusals driven by name, and the ledger read 436 of 436. /health moved from bc0a3fd to 5312719, version 47c9b9bc serves 100%, the marker counts 5 in the commit and 0 in the build it replaced, and the control 5312718 is absent. The web's ProjectDetailScreen and BudgetScreen chunks and the Hungarian dictionary carry W441's strings after the deploy and did not before, and the control string is absent. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W442The battery measures againcompleteFOUND WHILE VERIFYING W440: THE FIRST BATTERY SINCE a0a1464 READ 189 PASSED, 7 FAILED AND 12 DID NOT RUN, AND NONE OF IT CAME FROM W403 OR W440. ★★★ ONE SUITE LEAKED A TENANT ON EVERY RUN, AND FIFTEEN SUITES PICKED IT. w409 provisions a workspace whose name has no letters and swept only names like W409SUITE, and its SWEEP assertion counted the same way, so it passed while leaking. Five such tenants had built up on local, their ids sorted before the demo tenants, and every fixture taking order by id limit 1 from app.tenant found no event. The sweep now takes the letterless name and every tenant id the route handed back, and the assertion counts both. ★★ THREE SUITES OUTLIVED WHAT THEY TESTED. W416 removed the setup wizard, so w40 is retired and w241 stops driving the wizard route. W423 moved statusLabel and phaseLabel into EventFields.tsx, so w361 reads that file. ★★ A TRIGGER WROTE A NEWER ROW THAN THE SUITE'S OWN. W415's membership trigger writes membership.seated when w228 seats its mask caller, so 4c2 compares with the tenant's newest audit id, read from the database before the call, which the GET does not change. *Evidence: the sweep reduced to names again fails SWEEP with 1 survive; the audit export flipped to ascending fails 8 assertions including 4c2, reversed by undoing the change with a clean fingerprint; the first attempt at that sabotage did not land and its own landing check said so. The battery at bc0a3fd: 207 passed, 0 FAILED, 0 did not run, 0 no assertions, 1 SKIPPED, 5 641 assertions. Commit bc0a3fd, D-1070.* Left open: the 15 fixtures still take the first tenant by id, and w27-2 leaks profit centres, 308 on local. *Evidence: 1 design doc.*
W443The English in a template literal passes through the translatorcomplete**NAMED BY W403 ON 2026-09-13: *template literals in props, 48 on the whole tree and about 40 real*. ★★★ MEASURED AGAIN, 44 OF 356 TEMPLATE LITERALS IN PROPS CARRIED WORDS TO A READER, IN 28 FILES THAT ALREADY TRANSLATE. A Hungarian screen reader heard Remove before a tag's name, and a Hungarian user was asked Delete …?. The gate reports a template literal in a prop whose text outside its placeholders holds a word, counting the braces inside a placeholder. It names the structural props it leaves alone rather than the ones it reads, so a prop nobody has classified fails loudly. Each site is a translated sentence with a named placeholder filled by .replace(), with 40 Hungarian entries, an English plural split into two sentences, and permission codes joined with a comma. ★★ THE BRACE COUNTING WAS UNTESTED UNTIL THE CONTROL HELD A WORD-FREE PLACEHOLDER WITH BRACES**, because the control's first brace case was a sentence the rule reports either way. *Evidence: the gate red on the tree before the fix listing 44 sites, with W403's tests and its own control green, 3 sabotages each failing where they had to, web 431/431, type check and build clean. Commit 8b891a3, D-1074.* ON PRODUCTION: the page loads index-BKa8Yjq0.js, and hu-CguJeT0j.js and GuestsScreen-BA1idYIz.js carry the new sentences after the deploy and did not before, with the control string absent. The API is unchanged since fc29110. *Evidence: 1 design doc.*
W444The price table names a line's cost centrecomplete**THE OWNER, 2026-09-13, ON THE OCTOBER 23 PROJECT: *Also, please identify all these names in the budget lines and associate these cost centres to each line*. ★★★ D-1072 FILED 116 OF THE PRICE TABLE'S 315 LIVE LINES, AND NOTHING THE OWNER READS SHOWED IT. W441 showed a line's centre in the line drawer, one line at a time, and app.artabla() returned 19 columns with no centre. A return type cannot be widened in place, so the migration drops and creates the function from W411's own body, measured byte for byte against production, with the code and name of the cost centre as its last two columns. The route passes them on and the price table draws the centre under the item. ★★ THE FIRST CUT HAD TWO CHECKS THAT COULD NOT FAIL.** The apply script compared a boolean joined into text with 'f', and it reads 'false'. The grant assertion used has_function_privilege, which PUBLIC's default execute answers for app_rw, and a default privilege on schema app grants app_rw execute on every new function anyway, so the first S5 applied cleanly. The assertion reads the ACL now, and S5 revokes after the create. *Evidence: a migration with 5 assertions, an apply script that fingerprints every project's price table over W411's 19 columns before and after, a local rehearsal from the exact W411 state, suite 11 of 11, 11 sabotages each failing where they had to, API 666/666, web 429/429, and battery 209 passed and 0 failed at fc29110. Commit fc29110, D-1073.* ON PRODUCTION: the fingerprint f0b50135 over 315 rows identical before and after, grants identical, 116 of 315 October 23 lines named a cost centre, and the ledger at 437 of 437. /health moved from 5312719 to fc29110, version 57c654da serves 100%, the marker counts 1 in the commit and 0 in the build it replaced, and the control fc29111 is absent. The ArtablaScreen chunk carries koltsegkozpont_kod after the deploy and did not before, and the control string is absent. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W445The English in a fallback passes through the translatorcomplete**RECORDED BY W443 ON 2026-09-13: *bare English fallbacks, such as ?? Not set, that no rule reads*. ★★★ MEASURED ACROSS LINE BREAKS, 50 FALLBACKS HANDED THE SCREEN ENGLISH, IN FILES THAT ALREADY TRANSLATE. A line-by-line reading found 41: it missed the 11 written with || and a fallback VenuesScreen writes across a line break. The gate reads a ?? or || followed, across whitespace and line breaks, by an English sentence or a Capitalised label, and leaves lowercase single words alone, because 94 fallbacks are codes in that shape. Each fallback is translated, five that sat inside an English sentence took the whole sentence with a placeholder, and 26 Hungarian entries were added. ★★ ONE FALLBACK WAS A KEY.** BudgetScreen keyed a line group with no value as the English word and sorted on it, so the key is a named constant and the heading translates it where it is drawn. *Evidence: the gate red on the tree before the fix listing 50 sites with every other gate green, 3 sabotages each failing where they had to, web 433/433, type check and build clean. Commit 5a84de8, D-1075.* ON PRODUCTION: the page loads index-S05bxrw5.js, and AssetsScreen-CGYzXsbX.js and hu-DYAHZ8lX.js carry the new sentences after the deploy and did not before, with the control string absent. The API is unchanged since fc29110. *Evidence: 1 design doc.*
W446A date or a time is written in the screen's languagecomplete**THE 2026-09-13 HANDOVER, ITEM 3: *WorkforceScreen shows times in 12-hour form*. ★★★ THE BROWSER DECIDED THE CLOCK, ON 60 CALLS.** The Workforce screen's formatter passed undefined as its locale, so an English-language browser wrote Sep 13, 2026, 6:26 PM over a Hungarian screen that means 2026. szept. 13. 18:26, and 59 date and time formatting calls in 31 files, plus one in dateParts.ts, did the same. localeTag() beside currentLocale() answers hu-HU or en-GB, en-GB because the product's clocks are 24-hour, and the 59 calls pass it. clockIn() names en-GB instead of importing the translator, because only digits come out of it. The gate reads every .ts and .tsx file, not only the files that translate, because a date needs no English in it to be written the wrong way. *Evidence: the gate red on the tree before the fix listing 60 calls with every earlier gate green, 3 sabotages each failing where they had to, web 435/435, type check and build clean, and the three locales printed by Node for the same instant. Commit 9a4566e, D-1076.* ON PRODUCTION: the page loads index-DFII1SUj.js, which carries the helper after the deploy and did not before, with the control string absent. The API is unchanged since fc29110. *Evidence: 1 design doc.*
W447The English in a ternary passes through the translatorcomplete**RECORDED BY W445 ON 2026-09-13: *a then-branch that is an English literal, 103 of them, about 98 real*. ★★★ 98 BRANCHES IN 45 FILES, AND 16 OF THEIR SENTENCES WERE BUILT FROM PIECES. Notices after a save, Your session has expired on twelve screens, labels and loading placeholders all came from a ternary. The gate reads a then-branch that is an English sentence or a Capitalised label, across line breaks, and leaves class names and JSX fragments alone. It does not read else-branches, because after a colon a literal looks like an object property. One script made 25 hand edits, translating 16 sentences built from a template or with + whole, with placeholders. It then wrapped 128 literals, the 98 then-branches and the 30 else-branches beside them, and wrote nothing until all 121 new Hungarian entries existed. ★★ A TRANSLATED WORD CAN BE A KEY**, so every new sentence was searched for in a comparison afterwards. The one match compares a lookup's state code, not the translated word. *Evidence: the gate red on the tree before the fix listing 98 branches with every earlier gate green, 3 sabotages each failing where they had to, web 439/439, type check and build clean. Commit b3d61e2, D-1078.* ON PRODUCTION: the page loads index-DZ79IODB.js, and EventDetailScreen-ClfFHI22.js and hu-DYTX87tY.js carry the new sentences after the deploy and did not before, with the control string absent. The API is unchanged since fc29110. *Evidence: 1 design doc.*
W448A number is written in the screen's languagecomplete**RECORDED BY W446 ON 2026-09-13: *numbers in the wrong locale or none, money() writing 12,000.00 HUF on a Hungarian screen among them*. ★★★ 49 CALLS, WHERE W446 ESTIMATED ABOUT 33. Its pattern skipped every receiver ending in a parenthesis, such as Number(x).toLocaleString(). money() on the budget screen pinned en-GB for every reader, the price table pinned hu-HU for every reader, 40 bare toLocaleString() calls followed the browser, and four Intl.NumberFormat calls took no locale. 48 formatter calls take localeTag(), money() in miniBudgetMath.ts takes a required locale so the module stays free of React, and its 15 callers pass it. The gate reports a bare toLocaleString(), an Intl.NumberFormat with no locale, and a locale written into a formatter outside dateParts.ts and plainHandover.ts, each allowed with its reason. ★★ THE TYPE CHECK FOUND WHAT THE REWRITE COULD NOT SEE.** MiniBudget handed money to LineActual as a prop, which is not a call, so it now hands over a function that supplies the locale. *Evidence: the gate red on the tree before the fix listing 49 calls with W446's gate green, 3 sabotages each failing where they had to, web 437/437, build clean, and Node's output for the same figures in both locales. Commit 89ebb4b, D-1077.* ON PRODUCTION: the page loads index-eCHyNRuI.js and serves BudgetScreen-3BOcfU4n.js, which pins no en-GB, after the deploy and did not before, with the control string absent. The API is unchanged since fc29110. *Evidence: 1 design doc.*
W449The English in a template literal passes through the translatorcomplete**RECORDED BY W447 ON 2026-09-13: *English in a template literal outside a prop, such as Showing … of … and % of capacity on the crowd monitor*. ★★★ 171 SITES IN 61 FILES, FOUND BY PARSING AND NOT BY A PATTERN. The TypeScript parser read 1350 template literals under web/src. 721 with a word sat in files that translate, and 189 of those were apiGet paths. The gate reports a template whose text outside its placeholders holds a word, and sets aside paths, queries, keys, file names, classes, CSS values and units Hungarian writes the same way, each by its shape. 51 of the 171 were JSX children, 45 object properties such as a notice's text, and 27 props that W443's one-line rule could not read because the template sat in a ternary or ran over two lines. One script made 165 edits, one translated sentence with named placeholders per site. It translated the plain strings joined to those sentences with them, gave every spliced code its own sentence, and wrote nothing until all 168 new Hungarian entries existed and every placeholder survived. ★★ THE FIX FOUND A HOLE IN A CHECKER. W363's line matcher read the new key {shown} of {total} types as a stranded of, because W375 had stripped a translator's argument for the spanning matcher only. It strips it now, with a control in both directions. ★ ONE TITLE WAS HUNGARIAN FOR EVERY READER**, a logo title beside a Hungarian dictionary key, and it is an English key with its Hungarian now. *Evidence: the gate red on the tree before the fix with 171 sites, after a first run that also counted two detail: keys, 4 of 4 sabotages failing where they had to with the fingerprint restored, the web battery at 442 of 442, and the type check clean. D-1085.* ON PRODUCTION: the page loads index-CLAO-g_V.js, and the Dashboard and Hungarian chunks carry the new key and sentences after the deploy and did not before, with an invalid control absent and a dropped key gone. /health unchanged at fc29110. *Evidence: 1 design doc.*
W450A row cannot name another workspace's eventcomplete**RECORDED 2026-09-09 FOR ONE TABLE, AND CARRIED IN EVERY HANDOVER SINCE: *app.ticket_batch's policy is weaker than its three siblings' … Today it is protected by accident*. ★★★ IT WAS FIFTEEN TABLES, AND LOCAL ALREADY HELD A ROW THE KEY SHOULD HAVE REFUSED. Of the 47 tables in app that carry both event_id and tenant_id, fifteen had a single-column key to app.event and a policy that checks only the workspace, and Postgres checks a foreign key with row security bypassed. A refused wrong_event scan from 2026-09-05, written before the event-ownership sweep, named Tenant B's gala under workspace A, and nothing in the database objected. Each of the fifteen keys became (event_id, tenant_id) against app.event (id, tenant_id), keeping the delete action measured on the old key, with the three set-null keys nulling event_id alone so the row keeps its workspace. ★★ THE FIRST DRAFT OF THE PAIRING ASSERTION COMPARED COLUMN SETS, AND A SWAPPED PAIR PASSES THAT.** Postgres creates a key pairing event_id with tenant_id without a word when no row contradicts it, and most of the fifteen tables are empty on production. S3 swapped access_zone's pair, and the assertion now reads the pairs by position. *Evidence: a migration with 4 assertions, a control drive accepted on 9 tables before the migration and refused by each table's own key after, 3 of 3 sabotaged copies refused with the keys unchanged, and the battery at 209 passed and 0 failed with the keys in place. D-1079.* ON PRODUCTION: 0 foreign rows before, 15 single-column keys became 15 composite, the rolled-back drive refused by engagement_event_same_tenant with the other 14 tables empty, the ledger at 438 of 438, the key present and a control name absent, 0 keys paired the wrong way round, and /health unchanged at fc29110. *Evidence: 1 migration, 1 design doc.*
W451A row cannot name another workspace's membercomplete**RECORDED BY W450 ON 2026-09-13: *the same shape beyond the event … app.membership with 21*. ★★★ IT WAS 33 KEYS IN THREE SCHEMAS, AND TWO ROUTES CHECKED NOTHING. Every key to app.membership, in app, accred and crowd, was single-column, and app.membership had no unique (id, tenant_id) index to key against. The accreditation routes resolve an approver through app.membership, whose policy admits app.is_platform_admin(), so an operator's session found Tenant B's member there. /approvals/new and /approvals/submit put the approver ids from the request body straight into app.approval_step. Each key became (<column>, tenant_id) against app.membership (id, tenant_id), keeping its delete action, with the 19 set-null keys nulling the member alone. ★★ THE FIRST BATTERY WITH THE KEYS FOUND A SUITE WRITING THE ROW THEY REFUSE.** w158's fixture took its approver from select id from app.membership limit 1, with no tenant and no order, and drew Tenant B's member for a Tenant A budget. The key refused it, and f476258 reads the fixture's own tenant. ★ The production probe expected 17 set-null keys and counted 19. The migration's header and the document had miscounted the list, and the per-key assertion had been right. *Evidence: a migration with 3 assertions, a control drive accepted on 15 columns and an operator session accepted before the migration and both refused by their own keys after, 3 of 3 sabotaged copies refused with the state unchanged, the battery at 209 passed, 0 failed and 0 did not run at f476258, and /approvals/new with a foreign approver answered 400 naming the key. D-1080.* ON PRODUCTION: 0 foreign rows before, 33 single-column keys became 33 composite, the rolled-back drive refused on the 5 tables with a row, the ledger at 439 of 439, the key and the index present and their controls absent, 0 keys paired the wrong way round, and /health unchanged at fc29110. *Evidence: 1 migration, 1 design doc.*
W452Every key to an event names the event's workspacecomplete**RECORDED BY W450 ON 2026-09-13: *The 31 tables whose event is checked by a policy alone. A policy guards app_rw, not the owner role, where a composite key would guard both*. ★★★ IT WAS 56 KEYS IN THREE SCHEMAS, AND EVERY POLICY ON THEM LET AN OPERATOR THROUGH. After W450, 56 single-column keys to app.event were left on tables with a NOT NULL tenant_id: 34 in app, 16 in accred and 6 in crowd, app.event.parent_event_id among them. A policy named the event column on 33, and on all 56 every write policy names app.is_platform_admin(), so an operator's session passed the check whatever event it named. Venue sharing widens reading only, so no reference to another workspace's event is legitimate. Each key became (<column>, tenant_id) against app.event (id, tenant_id), 51 keeping cascade and 5 set null with the event column nulled alone. ★★ THE FIRST BATTERY WITH THE KEYS FOUND A SUITE WRITING THE ROW THEY REFUSE, FOR THE SECOND SLICE RUNNING.** w150's fixture took its tenant from the first wizard run and its event from the first event, and paired Tenant A with Tenant B's event. The key refused it, and a68fe15 reads the tenant from the event. *Evidence: a migration with 3 assertions, a control drive accepted on 33 columns and an operator session accepted before the migration and both refused by their own keys after, a tenant session refused by its policy throughout, 3 of 3 sabotaged copies refused with the keys unchanged, and the battery at 209 passed, 0 failed and 0 did not run at a68fe15. D-1081.* ON PRODUCTION: 0 foreign rows before, 56 single-column keys became 56 composite, the rolled-back drive refused on the 14 columns with a row, the ledger at 440 of 440, the key present and its control absent, delete actions counted at cascade 51 and set null 5, and /health unchanged at fc29110. *Evidence: 1 migration, 1 design doc.*
W453A row cannot name another workspace's rowcomplete**RECORDED BY W452 ON 2026-09-13: *The rest of the class … 264 single-column keys between two different tenant tables remain*. ★★★ 252 KEYS TO 83 TABLES, AND 77 OF THOSE TABLES HAD NOTHING TO KEY AGAINST. Measured on local and production alike: 277 single-column keys between workspace tables in app, accred and crowd. 252 run between two tables with a NOT NULL tenant_id and name the other's id. 25 cannot hold a composite key: 22 name a shared directory whose tenant_id may be NULL (app.venue 11, app.role 5, app.event_role_reference 3, app.document_template 2, app.document_template_line 1), one sits on such a table, and two name crowd.count_event by seq. The migration makes a unique (id, tenant_id) index on each of the 77 referenced tables without one and replaces each key by (<column>, tenant_id) against (id, tenant_id), keeping its delete action, with names cut to 51 bytes before _same_tenant because one would have been 64. ★★ A MIGRATION RUNS ONCE, SO THE SLICE ALSO SHIPS A SUITE THAT READS THE CATALOGUE. w453-acceptance.mjs fails if a single-column key between two workspace tables comes back, names the 25 left on purpose exactly, and drives the owner and an operator's session into the key. Before the migration it failed exactly 1a, 4b and 5a. ★★ THE APPLY SCRIPT HAD TWO FAULTS, BOTH FOUND ON LOCAL.** A parenthesis stopped the first real apply in its read-only pre-flight. And its drive counted an update that raised nothing as accepted: app.message_freeze_on_edit puts chat_id back, so the update named nothing and the key was never asked. The drive now counts an acceptance only when a row then crosses workspaces. *Evidence: a migration with 3 assertions, the suite red on exactly its three checks before and 13 of 13 after, a control drive accepted on 95 columns and an operator session accepted before the migration and both refused by their own keys after, 3 of 3 sabotaged copies refused with the state unchanged, and the battery at 210 passed, 0 failed and 0 did not run at 961cc45. D-1082.* ON PRODUCTION: 0 crossing rows before, 252 single-column keys became 252 composite, 83 of 83 referenced tables indexed, the ledger at 441 of 441, and a probe with controls. The drive could not run, because production holds one workspace, and the apply script exited 1 for it as written. /health unchanged at fc29110. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W454A row names a shared row or one of its own workspacecomplete**RECORDED BY W453 ON 2026-09-13: *The 25 keys left single-column. A reference to a shared directory needs a guard that allows a shared row or a row of the same workspace, which a key cannot express*. ★★★ A KEY CANNOT SAY SHARED OR MINE, SO A DEFINER SAYS IT. 23 of the 25 name, or sit on, one of the five tables whose tenant_id may be NULL: app.venue 11, app.role 5, app.event_role_reference 3, app.document_template 2, app.document_template_line 1, and app.document_template.number_series_id. A (column, tenant_id) key would refuse every reference to a shared row, and on local 117 invoices and 121 purchase orders name a shared venue. Each key gets an after trigger calling app.refuse_another_workspaces_private_row(), which reads the named row, refuses EC497 when it belongs to another workspace, and lets a shared row or a row of the same workspace through. A shared row may name only a shared row. The other 2, the count_seq keys to crowd.count_event, became composite on a new unique (seq, tenant_id) index. ★★ THE GUARD HAS TO SEE WHAT IT REFUSES. A tenant session cannot see another workspace's private venue, and before W454 its event naming that venue was accepted all the same, because the key checks with row security bypassed. As an invoker the guard would find nothing there and let the row through. With the guard altered to an invoker, the suite failed exactly 2b and 5b. ★ A DISABLED TRIGGER IS NOT A GUARD.** The catalogue check reads tgenabled, and with the event guard disabled the suite failed exactly 1a, 3b, 5a and 5b. *Evidence: a migration with 4 assertions, a control drive accepted on all five shapes and both app_rw sessions accepted before the migration, 4 of 4 sabotaged copies refused with the state unchanged, the three foreign references and both sessions refused with EC497 after while rows naming their own venue or a shared one were accepted, w454-acceptance.mjs 14 of 14 after two forced reds, and the battery at 211 passed, 0 failed and 0 did not run at bfbda18. D-1084.* ON PRODUCTION: 0 rows named another workspace's private row before, 23 guards, 2 composite count keys and the index after, the apply script's rolled-back drive refused an event naming a throwaway workspace's private venue and a shared role cloned from its private role with EC497 and accepted an event naming a shared venue, the ledger at 442 of 442, and a probe with controls. The app_rw sessions could not be driven there, because the owner may not set role app_rw. /health unchanged at fc29110. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W455A text node that starts with a number passes through the translatorcompleteRECORDED BY W449 ON 2026-09-14: the warehouse tab's service interval, bare JSX text that none of the matchers reads. ★★ W363'S MATCHER STARTS AT A LETTER, SO A LABEL THAT STARTS WITH A NUMBER WAS READ BY NOTHING. A scan of every file that translates found 32 text nodes that hold a word and do not start with a letter. 22 were TypeScript that only looks like JSX, such as a comparison before a JSX return, and the other 10 were option labels in 4 files: 7, 30 and 90 days on the warehouse tab, 90 days on the calendar feeds, 30, 90 and 180 days on the API tokens, and 2, 10 and 40 venues or more on the media review. The gate is its own matcher, because letting W363's start at a digit would read that TypeScript as prose. Each label is translated with its number as the placeholder, and 2 Hungarian entries were added. *Evidence: the gate red on the tree before the fix with the 10 labels, 3 of 3 sabotages failing where they had to with the fingerprint restored, the web battery at 444 of 444, and the type check clean. D-1086.* ON PRODUCTION: the page loads index-Caq5nYp3.js, the media review chunk carries the key three times and the bare label no more, and the Hungarian chunk carries {n} vagy több helyszín, with an invalid control absent. /health unchanged at fc29110. *Evidence: 1 design doc.*
W456An approver is an active member of this workspacecomplete**RECORDED BY W451 ON 2026-09-13: *A refusal that names the approver*. ★★★ FOUR ROUTES WROTE APPROVAL STEPS FROM IDS A CALLER SENT, AND NONE CHECKED THEM AGAINST THE WORKSPACE BY ITS ID. /approvals/new and /approvals/submit checked nothing, and the two accreditation routes looked the ids up under row security, which shows an operator every workspace's members. W451's key refused a foreign member in every case, with a sentence that never said the approver was the problem. Driven against the old routes, a suspended member of the caller's own workspace was accepted and its approval left behind, and a step with no approver was a 500. One check, approversOutsideWorkspace, now asks app.membership for the caller's workspace and an active status before any write, and one coded refusal, approval.approvers_not_members, names the ids it refused and is translated through the web's catalogue. ★★ THE SABOTAGE THAT MATTERS IS S2.** With the workspace filter removed, so that row security decides, the suite failed exactly the two operator's sessions while every tenant session still passed. *Evidence: w456-acceptance.mjs failed 9 of 14 on the old routes and passes 14 of 14, w384 and w393 each failed their new operator check on the old routes and pass, 3 of 3 sabotages failed where they had to with the fingerprint restored, the web battery at 444 of 444, and the battery at 212 passed, 0 failed and 0 did not run at 805f7cf. D-1087.* ON PRODUCTION: /health names 805f7cf, the web loads index-DQo2Ecup.js and its Hungarian chunk carries the new sentence after the deploy and did not before, with an invalid control absent. The refusal itself was not driven there, because it needs a signed-in session on a production workspace. *Evidence: acceptance suite, 1 design doc.*
W457A translation runs when the screen renders, not when its module loadscompleteFOUND WHILE W456 SHIPPED, in W449's record of the companies screen's view labels, translated once when the module loads. ★★★ 161 TRANSLATIONS IN 24 LABEL TABLES RAN ONCE, WHEN THEIR MODULE LOADED, AND NEVER AGAIN. The provider remounts every screen when the language changes, but a module is evaluated once, so a table built at load kept the first answer the dictionary gave until the page reloaded. i18n.test.ts named the shape in a comment and no rule refused it. The gate parses every file and reports a call to its translator that is not inside a function. A codemod driven by the TypeScript language service made each of the 24 declarations a function and each of its 56 references a call, resolving references through the service, so a name several files declare changed only where it resolves. The keys stay literal, so no Hungarian entry changed. ★ SIX TESTS IN TWO FILES READ TWO OF THOSE TABLES FROM SOURCE, the check-in and pass refusal vocabularies, and only the full web battery found them. They read the function form now. *Evidence: the gate red on the tree before the fix with 161 calls, 3 of 3 sabotages failing where they had to with the fingerprint restored, the web battery at 446 of 446, and the type check clean. D-1088.* ON PRODUCTION: the page loads index-DPbEhWT4.js, and the agenda import chunk still carries its keys, with an invalid control absent. The language switch itself was not driven in a browser. /health unchanged at 805f7cf. *Evidence: 1 design doc.*
W458Each reader chooses the columns of a budget's linescomplete**THE OWNER, 2026-09-14, with a screenshot of the budget's lines: *give the opportunity to the user to define which columns they want to see on this page*. ★★ THE SCREEN ALREADY HELD THE QUANTITIES AND NO COLUMN DREW THEM. /budgets/lines serves amount, amount_unit, time_qty, time_unit and unit_price, and the lines table drew ten other columns. It gains the five, the unit price masked by the total's branch, and an Oszlopok control that shows or hides every named column but the line's name, remembered in the browser. The choice is stored as what was hidden together with the columns the table had, so a column added later takes its own default. The screenshot's four English codes, the direction, the budget's state and mode, and the grouping options, are translated. ★★ THE ACCEPTANCE BATTERY FOUND WHAT THE WEB CHECKS DID NOT. w276 runs web/i18n-wrap.mjs, a CI step that neither the gate files nor the web battery run, and it read the grouping labels, renamed to label for the dictionary gate, as bare English. The follow-up translates them inside a function, which every translation check accepts. ★ S1 PASSED ON ITS FIRST RUN**, because the function that records a choice refuses to hide the line's name itself, and no test gave the reading function a stored choice that did. That case was added and S1 failed on it. *Evidence: the logic's tests and the gates at 62 of 62, 3 of 3 sabotages failing where they had to with the fingerprint restored, the web battery at 450 of 450, two columns hidden and remembered across a reload in a local browser, the bare-English checker at 0 after the follow-up, and the battery at 212 passed, 0 failed and 0 did not run at b4596f9. D-1089.* ON PRODUCTION: after the follow-up the page loads index-ClghozUy.js, the Hungarian chunk carries Oszlopok and Nincs csoportosítás, which it did not before W458, the budget screen's chunk carries the storage key, and an invalid control is absent. /health unchanged at 805f7cf. *Evidence: 1 design doc.*
W459A budget can be archived, and a budget nothing names can be deletedcomplete**THE OWNER, 2026-09-14, on the opera gala's budget: *give me an option to hide or delete a previous budget, because, for example, on this opera event, there is an ancient budget which was never used*, and *where can I rename a budget?* ★★ NOTHING COULD HIDE OR DELETE A BUDGET, AND THE RENAME WAS ALREADY THERE. The Edit budget drawer's first field is the name, which /budgets/update has taken since W27. On production the gala carries a draft of 2026-09-06 that nothing names beside an active budget with 17 lines, and 27 of 45 budgets have no line. app.budget gains archived_at. /budgets/update archives and restores, and GET /budgets leaves an archived budget out unless include_archived=1, so the dashboard and the mini budget's attach list stop offering it while the budget screen shows it on request with a badge. POST /budgets/delete locks the budget, counts its lines, approvals, option groups, projections, orders, invoices and recurring invoices, and refuses with budget.in_use while any exists, and an approved or closed budget with budget.delete_frozen. The drawer where a budget is renamed carries both acts. ★★ A KEY HOLDS SOME ORDERS AND NOT OTHERS. purchase_order_anchor_ck refuses an order that names nothing, and 1807 of local's 1809 orders naming a budget name nothing else, so without the count the database refused their budget's delete with a raw 23514. An order that also names its event would have silently lost its budget, and only the count stands in the way. S2's first run showed it, and the suite now carries both kinds. ★ A WEB SABOTAGE FOUND NO CHECK OF THE CODES AGAINST THE CATALOGUE**, so check 5a reads the codes the delete actually sent and finds each there. *Evidence: w459-acceptance.mjs 32 checks, 6 API sabotages each red exactly where required with the file restored, 3 web sabotages, the web battery at 450 of 450, the flow driven in a local browser, and the battery at 213 passed, 0 failed and 0 did not run at ec2633a. D-1090.* ON PRODUCTION: the column applied with a rolled-back write and an invalid control, the migration ledger complete at 443 probes, /health names ec2633a, and the budget chunk and the Hungarian chunk carry the new request and label after the deploy and did not before, with the control absent. Neither act was driven on production, and the unused gala budget is the owner's to delete. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W460A budget keeps a ledger of its own changes, and nobody can edit itcomplete**THE OWNER, 2026-09-14: *an uneditable ledger of the budget where we are recording each and every budget modification, which can be filtered line by line*. ★★★ NOTHING RECORDED WHAT A LINE WAS. The audit row of an updated line holds its budget's id and nothing more, and eight statements in the Worker write lines. The database writes the ledger now: app.record_budget_change, a definer, runs after every write to a budget or a line and records who, when, every field that changed from and to, and a line's figure before and after with the difference and its effect. app.forbid_mutation refuses every edit, the Worker's role may only read, row security carries a read policy only, and no key ties a row to the budget, the line or the person, so the history outlives all three. Every existing budget and line starts with a baseline row. GET /budgets/changes filters by line, person, effect, field, kind and date with the whole budget's running totals, masks the money like the line does, and refuses the money filters to a reader without the money right. The budget screen's Változásnapló tab draws it, and a line's drawer opens its history. ★★ LOOKING AT THE TAB FOUND SIX DEFECTS THE SUITES COULD NOT SEE**, the last older than this slice: the tab bar's More button named an open seventh tab without the translator, so every surface with a seventh tab read English there. tabs-translate.test.ts reads every name the bar draws, and its first form passed on the defect, because the defect's own line already translated *More*. *Evidence: w460-acceptance.mjs 35 checks, 9 sabotages of the route and the database each red where required with the file and the database restored, 2 web and 2 tab-bar sabotages, the web battery at 451 of 451, the tab driven in a local browser, and the battery at 214 passed, 0 failed and 0 did not run at 527a2a7. D-1091.* ON PRODUCTION: the ledger applied with a baseline for every budget and line, a rolled-back rename recorded and its edit refused, the migration ledger complete at 444 probes, /health names 527a2a7, and the web's budget chunk and Hungarian chunk carry the route and the tab after the deploy and did not before, with the control absent. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W461A budget can be kept as a snapshot, with its reason, that never changescomplete**THE OWNER, 2026-09-14: *a button which creates a snapshot in time of the budget and duplicates it, so we can have a snapshot which cannot be edited anymore but applies as proof of history in time*, with the reason recorded. ★★★ A SNAPSHOT IS A BUDGET, NOT A DOCUMENT. The one frozen copy in the schema was a jsonb document of a projection a client was shown, and the only freeze was W404's approved state, which budgets.approve can leave. app.take_budget_snapshot copies the budget, its option groups, its lines and who bears them into a new budget, reading the columns from the catalogue so a later column is copied too, records the reason and seals the copy, so the snapshot opens in the same screen as the budget it was taken from. app.budget_snapshot_is_frozen refuses with EC498 every change but archiving, on the budget and on its lines, owners, option groups, actuals and transactions, and lets a line's filing move as on an approved budget. POST /budgets/snapshot requires the reason, /budgets/delete refuses a snapshot, and the budget screen draws Pillanatkép készítése with a required reason, a badge, a notice with the time and reason, and no control that writes. ★★ THE SUITE CAUGHT THE FREEZE ANSWERING 42703 IN PLACE OF EC498 on the three tables with no budget_id, because a PL/pgSQL condition resolves every field of NEW when it is planned. It reads through to_jsonb now. ★ CHECK 5A COUNTED THE CODES INSTEAD OF NAMING THEM**, found while drafting the sabotages, and names them now. *Evidence: w461-acceptance.mjs 30 checks, 9 sabotages of the routes and the database each red where required with the file and the database restored, 2 web-facing sabotages, the web battery at 451 of 451, a snapshot taken and read in a local browser, and the battery at 215 passed, 0 failed and 0 did not run at 47320a8. D-1092.* ON PRODUCTION: the migration applied with a rolled-back snapshot of a real budget copying every line and refusing its edit, the migration ledger complete at 445 probes, /health names 47320a8 and not the build before it, and the web's budget chunk and Hungarian chunk carry the route and the button after the deploy and did not before, with the control absent. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W462A database refusal reads in the reader's languagecomplete**RECORDED BY THE 2026-09-14 HANDOVER AS ITS FIRST ITEM: *a database refusal reads in English on the budget screen*. ★★★ THE CODE WAS ALREADY ON THE WIRE. asRefusal answers a raised ECnnn with that code and the database's English, and renderServerMessage looks the code up, so a catalogue sentence keyed by the code translates the refusal with no change to the Worker. The catalogue's namespace test refused every key without a dot, and it now exempts exactly EC and three digits, which no route's own code can take. ★★ ONE SENTENCE ANSWERS EVERY RAISE OF A CODE, SO EC498 WAS SPLIT. It meant four things, and app.take_budget_snapshot's three refusals moved to EC499, which is not catalogued. The migration asserts which function raises each code, and w462-acceptance.mjs reads every raise of each catalogued code from the database. ★ THE FIRST SABOTAGE RUN MISSED FOUR REQUIREMENTS.** W319's paired control is red whenever a catalogue sentence lacks Hungarian, and the suite crashed on a route that sent no code, which it now reads as no sentence. *Evidence: w462-acceptance.mjs 18 checks, red 10 of 18 first, 9 sabotages exactly as required in the suite and the web tests with the files and the database restored, the refusal read in Hungarian and in English in a local browser, the web battery at 455 of 455, and the battery at 216 passed, 0 failed and 0 did not run at f52ab77. D-1093.* ON PRODUCTION: the migration applied with a rolled-back drive of the copy and the freeze, the migration ledger complete at 446 probes, /health names f52ab77 and not the build before it, and the web's scripts carry both sentences and their Hungarian after the deploy and not before, with the control absent and W461's sentences present both times. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W463MiniBudget does not offer a snapshot as a budget to write tocomplete**FOUND WHILE W462 MEASURED WHICH SCREENS REACH THE DATABASE'S REFUSALS: *MiniBudget disables an approved or a closed budget and does not know a snapshot*. ★★ A SNAPSHOT KEEPS ITS SOURCE'S STATE.** W461 seals a snapshot with the state of the budget it was taken from, so a snapshot of an active budget reads as active, and isFrozenBudget read the state alone. MiniBudget's attach form therefore offered a snapshot, preselected it when it came first, and met EC498 on save. isFrozenBudget now returns true for a budget with snapshot_taken_at whatever its state, the form's budget type carries the column, and a snapshot's disabled option names itself as a snapshot. A projection that omits the column still offers the budget, as W406 chose for a missing state. *Evidence: mini-budget-math.test.ts red on the old predicate and green after, 1 sabotage exactly as required with the file restored, the list measured through the local API, the web battery at 456 of 456, and the battery at 216 passed, 0 failed and 0 did not run at ef04cb0. D-1094.* ON PRODUCTION: /health names ef04cb0 and not the build before it, and the script carrying MiniBudget's attach form reads snapshot_taken_at after the deploy and did not before, with the control absent and W462's sentence present both times. *Evidence: 1 design doc.*
W464A fixture names the workspace it usescomplete**THE 2026-09-14 HANDOVER'S SECOND ITEM, *fixtures that pick an arbitrary row*, RE-MEASURED THE SAME DAY: 159 unpinned single-row lookups in 78 suites, 72 of them able to answer the wrong row. ★★★ 52 OF THEM PICKED THE WORKSPACE ITSELF, in 22 suites, by asking app.tenant for its first row, which answers whichever workspace sorts first. W442 had seen five leaked workspaces sort ahead of the demo ones. A decoy workspace that sorts first and holds nothing made 15 of the 22 suites fail on demand, 14 of them crashing on a row the empty workspace does not hold. Every pick now names Demo Agency A, or Demo Agency B for the three unordered picks that answered it, so no suite changes what it measures. w464-acceptance.mjs refuses a workspace read with a limit and no where in any suite, proves its detector on nine shapes, and runs three of the broken suites with the decoy present. ★★ THE DECOY FOUND WHAT THE SCAN DID NOT.** w234 read the first two workspaces for its cross-workspace check, which a limit 1 rule passed, and crashed again with the decoy present. The detector reads any limit now, and w234 names both. *Evidence: w464-acceptance.mjs red with 52 picks and the three decoy suites, 9 of 9 after, all 22 suites green with the decoy present, 4 sabotages exactly as required with the files restored, and the battery at 217 passed, 0 failed and 0 did not run at ea4c50d. D-1095.* ON PRODUCTION: nothing production runs changed. /health names ea4c50d and not the build before it, and the web's entry script is the one it was. *Evidence: acceptance suite, 1 design doc.*
W465A fixture names the row it usescompleteTHE REST OF THE 2026-09-14 HANDOVER'S SECOND ITEM, AFTER W464 NAMED THE WORKSPACE: 22 single-row lookups on tables that carry tenant_id, in 19 suites, still named no row. ★★★ W256 COULD DESTROY A TRANSLATION. Its hidden venue was the first unpublished one with an overview, and it upserted a Hungarian overview onto it and deleted its marker's rows after, so a venue with a real translation would have lost it. A stand-in translation put there made the unchanged suite overwrite and delete it. Its upsert now updates only a row carrying its own marker. Every other pick names the row it answered, four of them Demo Agency B's, and w13 names a fixture space where it had answered the owner's real October 23 space. ★ THE BATTERY FOUND W164 COUNTING ANOTHER SUITE'S FIELD BY ITS LABEL, a field w192 had left when a broken launch kept it from its database, and w164 counts its own key prefix now. ★★ THE SCAN HAD TO TELL A PICK FROM A LIST. Its first draft flagged nine role reads that collect every role holding a permission, and missed a workspace named only inside a subquery and a constant after a negative match. w465-acceptance.mjs removes subqueries before looking for a pin, counts a constant only after = or in, lists w288's deliberate pick of another workspace's asset with its reason, and fails a stale or growing list. *Evidence: w465-acceptance.mjs red with 22 picks and the stand-in destroyed, 8 of 8 after, 5 sabotages exactly as required with the files and translations restored, and the battery at 218 passed, 0 failed and 0 did not run at 4686d3d. D-1096.* ON PRODUCTION: nothing production runs changed. /health names 4686d3d and not the build before it, and the web's entry script is the one it was. *Evidence: acceptance suite, 1 design doc.*
W466A stored state, status or kind is shown in wordscomplete**THE 2026-09-14 HANDOVER'S FIRST ITEM, *codes shown as stored on the quotes, companies and warehouse screens*, MEASURED THE SAME DAY: 31 places printed a stored state, status or kind as JSX text, beside 2 render functions, 11 template strings and 4 calls of a pretty() that only replaced an underscore. ★★★ ONE TABLE PER VOCABULARY, NAMED BY WHAT DEFINES IT. web/src/system/storedCodes.ts holds 13 tables, each named by the check constraint or the enum that defines its codes, and codeLabel prints a code through its table. stored-codes.test.ts reads each definition from the spine, the latest one for a redefined constraint, and refuses a code with no words and words for a refused code. ★★ W361'S WORDS HAD NOT REACHED THREE OTHER PLACES. The event page, the project page and the shell's event bar printed an event's status as stored, and statusLabel and phaseLabel read their words from the same module now. ★ THE FIRST BATTERY FOUND W361 READING WHERE THE WORDS HAD BEEN.** Its suite read the switch statements W466 replaced, and reads the two event tables now. The scan names the 14 places still printing another vocabulary, and the test fails when one of them stops, so the list only shrinks. *Evidence: stored-codes.test.ts red 5 of 8 before the module with 16 places named, 8 of 8 after, 6 sabotages exactly as required with the files restored, w361 red 4 at the first commit and 9 of 9 after with 3 sabotages as required, w251's paid search fixture failing once and passing alone, the screens read in Hungarian in a local browser, the web battery at 464 of 464, and the battery at 218 passed, 0 failed and 0 did not run at 77b5361. D-1097.* ON PRODUCTION: /health names 77b5361 and not the build before it, and the web's scripts carry the words and their Hungarian after the deploy and not before, with the control absent and W462's sentence present both times. *Evidence: 1 design doc.*
W467Both venue type pickers load againcomplete**FOUND WHILE THE 2026-09-14 HANDOVER'S SECOND ITEM WAS MEASURED: the suite for *venue types have no Hungarian anywhere* asked the venue-types route for 200 rows, as both pickers do, and was refused. ★★★ NEITHER VENUE TYPE PICKER COULD LOAD ITS LIST. The single picker on the venue form and the venue invitation page, and the multi-select under it, both asked for 200 rows, and the route has refused any limit above 100 with a 400 since e8feb9d on 2026-08-14. Measured on production the same day. On the local venue form the type list held one option. fetchVenueTypes pages at the route's most and follows next_offset, and the multi picker loads through it, so the two cannot ask for different pages again. ★ A SABOTAGE REQUIREMENT WAS WRONG, AND THE ROUTE SAID WHY.** A limit that is not a number is answered with the default page, so an unresolved page size is caught by the suite's resolution and limit checks and not by the request. *Evidence: w467-acceptance.mjs 9 checks, red 5 of 9 against the old pickers, 4 sabotages exactly as required with the files restored, the venue form listing 67 types in a local browser after listing none, the web battery at 464 of 464, five batteries failing only the paid search suites w250 and w251, whose local inference sometimes never answered, measured by a sampler and by timed requests with a longer wait that was not kept, and the battery at 219 passed, 0 failed and 0 did not run at 13b8cc0. D-1098.* ON PRODUCTION: /health names 13b8cc0 and not the build before it, no script production serves asks for 200 rows after the deploy and both old requests were there before, the paging is served, and the route still answers 100 with 67 types and refuses 200. *Evidence: acceptance suite, 1 design doc.*
W468A venue type reads in the reader's languagecomplete**THE 2026-09-14 HANDOVER'S SECOND ITEM, *venue types have no Hungarian anywhere*, MEASURED THE SAME DAY: every label in app.venue_type_label was English apart from one en-US label, production answered a Hungarian reader in English, neither picker asked for a locale, and six lines on four screens printed a type code with its underscores turned into spaces. ★★★ 67 HUNGARIAN LABELS, AND THE SCREENS READ THEM. The w468 migration inserts one label per type and refuses a type left without one and a label that copies the English. Both pickers ask for the reader's locale, and useVenueTypeLabel feeds the venue list, its card subtitle, the venue page and the proposals queue. ★★ THE ROUTE'S NOTE IS MEASURED. It named en and en-US as the only locales with values, and reads the table now. ★ A MIGRATION PROBE COULD NOT SURVIVE A NEW LANGUAGE.** W31.9's probe counted conference_centre's labels in every locale, so the first Hungarian label made W31.9 read missing, and it counts W31.9's own two labels now. *Evidence: w468-acceptance.mjs 14 checks, red 9 of 14 before, 14 of 14 after, 6 sabotages exactly as required with the files and the labels restored, the venue list and page read in Hungarian in a local browser, the web battery at 464 of 464, the API unit tests at 666 of 666, and the battery at 220 passed, 0 failed and 0 did not run at 5f854d5. D-1099.* ON PRODUCTION: the migration applied through its apply script with 67 of 67 types resolving to Hungarian and the English unchanged, the migration ledger complete, /health names 5f854d5 and not the build before it, the route answers hu in Hungarian and en in English with the measured note, and the web's scripts carry the new Hungarian after the deploy and not before. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W469An accreditation request's approval can be rejectedcompleteFOUND 2026-09-14 WHILE SURVEYING THE REFUSAL CODES THE API CAN RETURN: every rejection or change request of an accreditation request's approval was refused with EC466, because W385 made the request approvable in app.approval_subject_exists and did not name it in app.reopen_approval_subject. ★★★ THE BRANCH CHANGES NOTHING, AS W385 DECIDED. The approval concludes and the request keeps its status. ★★ THE CLASS IS CHECKED, NOT ONLY THE CASE. The migration's assertion, its apply script and w469-acceptance.mjs each compare the two functions' case lists, so the next approvable type without a reopen behaviour fails before its first rejection. ★ A SABOTAGE FOUND ALL THREE READERS BLIND TO A DIGIT. They matched [a-z_]+ and read any quoted name now. *Evidence: w469-acceptance.mjs 11 checks, red 7 of 11 against the function before W469, 11 of 11 after, 3 sabotages exactly as required with both functions restored, and the battery at 222 passed, 0 failed and 0 did not run at acff723, W470's commit, after a first run at 1f79fed failed w460 6h across local midnight, which W470 repaired. D-1100.* ON PRODUCTION: the migration applied through its apply script with no approvable type left unnamed and no approval or decision written, the function's hash equal to local's and different before, the migration ledger complete, and /health names acff723 and not the build before it. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W470A budget ledger's days are the reader's dayscompleteFOUND 2026-09-15, WHEN THE BATTERY FOR W469 RAN ACROSS LOCAL MIDNIGHT AND w460 6h FAILED: GET /budgets/changes read since and until as midnight in the database connection's zone, Europe/Budapest on local and GMT on Neon, so on production a Budapest reader's since today dropped every change made between midnight and two. ★★★ A DAY NEEDS ITS ZONE. The route refuses since or until without tz, refuses a zone Postgres does not know, and reads each day as midnight in the named zone. ★★ THE TAB SENDS THE BROWSER'S ZONE, the one its When column prints in. ★ THE SUITE CHOOSES A ZONE THAT CAN MAKE IT WRONG, on another date than the connection's, so its check fails at any hour. *Evidence: w470-acceptance.mjs 9 checks, red 5 of 9 with W470's diff reversed, 9 of 9 after, 4 more sabotages exactly as required with both files restored, w460 corrected to ask for UTC's days, the web battery at 464 of 464, the API unit tests at 666 of 666, and the battery at 222 passed, 0 failed and 0 did not run at acff723. D-1101.* ON PRODUCTION: /health names acff723 and not the build before it, and the web's scripts carry the tab sending its zone after the deploy and not before. *Evidence: acceptance suite, 1 design doc.*
W471A crowd report's days are the event's dayscompleteFOUND 2026-09-15 AMONG THE CASTS OF A PARAMETER TO ::date THAT W470 LEFT UNAUDITED: crowd.daily_summary cut each day at midnight in the database connection's zone, and GET /crowd/report started its window at UTC midnight, because postgres.js sends a string bound to a timestamptz through a JavaScript Date. On production a Budapest event's night counted toward the day before. ★★★ A CROWD ZONE'S DAY IS ITS EVENT'S. The function reads the event's time zone and cuts each day from midnight to the next midnight there, and the route reads a bare date or a time without an offset as the event's wall clock and names the zone in the report. ★★ A DAY ENDS AT THE NEXT MIDNIGHT, so the day a clock changes is 23 or 25 hours long. ★ THE FIRST RED PROVED NOTHING. The suite's first fixture was moved an hour by the same serializer, and now reads its instants back. *Evidence: w471-acceptance.mjs 11 checks, red 5 of 11 before W471 and again with W176's function and W471's route diff reversed, 11 of 11 after, 4 more sabotages exactly as required with the route and the function restored, the API unit tests at 666 of 666, and the battery at 223 passed, 0 failed and 0 did not run at 7b03177. D-1102.* ON PRODUCTION: the migration applied through its apply script with the function cutting in the event's zone after and the connection's before, its hash equal to local's, the migration ledger complete, and /health names 7b03177 and not the build before it. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W472A stored code in a field named for it is shown in wordscompleteW466'S SCAN READ ONLY A FIELD NAMED EXACTLY state, status, kind OR publication_state, AND 8 PLACES ON 7 SCREENS PRINTED A STORED CODE IT COULD NOT SEE, an approval's state, a document line's, an event's status, a role class, a webhook consumer's kind and a shift's state, measured at 7b03177. ★★★ FIVE TABLES AND 8 CALLS. Each vocabulary gets a table named by its constraint or enum, and every place prints through codeLabel. ★★ THE SCAN SEES ANY FIELD NAMED FOR ONE, with or without a fallback, so the next such field fails the test on the day it is printed. ★ A CONSTRAINT NAMED _check IS READ AS ONE. The test's reader took approval_state_check for an enum and found nothing. *Evidence: stored-codes.test.ts red on the screens test naming the 8 places before, 22 of 22 after, 6 sabotages exactly as required with the four files restored, the web battery at 465 of 465, and the battery at 223 passed, 0 failed and 0 did not run at d934f9e. D-1103.* ON PRODUCTION: /health names d934f9e and not the build before it, and the web's scripts carry the role class table and the Hungarian for internal, sibling and changes requested after the deploy and not before. *Evidence: 1 design doc.*
W473A refused event move reads in the reader's languagecompleteTHE SURVEY OF REFUSALS THAT REACH A HUNGARIAN READER IN ENGLISH, AND W462'S OWN NOT-DONE LIST: /events/update caught EC387 and answered 400 with the database's English and no code, so the web's catalogue could never answer a refused event move. ★★★ THE CODE TRAVELS WITH THE SENTENCE. The route sends code EC387 beside the English, and the catalogue has its sentence and Hungarian. ★★ THE CODE OPENS THE DATABASE'S SENTENCE, so W462's rule, that every raise of a catalogued code says one thing, reads EC387's raise and covers it. ★ EC386 IS LEFT, raised four times with four sentences, for a split of its own. *Evidence: w473-acceptance.mjs 9 checks, red 5 of 9 before and again with W131's function and W473's diff reversed, 9 of 9 after, 5 more sabotages exactly as required with everything restored, w462 at 20 of 20, the web battery at 465 of 465, and the battery at 224 passed, 0 failed and 0 did not run at 65df798. D-1104.* ON PRODUCTION: the migration applied through its apply script with the code opening the sentence after and not before, the function's hash equal to local's, the migration ledger complete, /health names 65df798 and not the build before it, and the web's scripts carry EC387's sentence and its Hungarian after the deploy and not before. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W474A budget line's currency refusal reads in the reader's languagecompleteW462'S NOT-DONE LIST AND W473 LEFT EC386, WHICH app.assert_budget_line_currency RAISED FOUR WAYS, SO NO CATALOGUE SENTENCE COULD ANSWER IT AND THE BUDGET LINE ROUTES ANSWERED IT IN ENGLISH WITH NO CODE. ★★★ ONE CODE, ONE MEANING. EC386 keeps the foreign-currency default it was numbered for, and EC500, EC501 and EC502 take the other three, each opening its own sentence. ★★ THE CODE TRAVELS, from the budget line routes beside the English, and the catalogue answers EC386, EC500 and EC501 in Hungarian. ★ THE DICTIONARY WENT BACK AFTER THE DRIVER with no local writer, and was repaired from the saved diff, re-checked for a minute and committed. *Evidence: w474-acceptance.mjs 12 checks, red 7 of 12 before and again with W136's function and W474's diff reversed, 12 of 12 after, 6 more sabotages exactly as required with everything restored, one of them a code made to mean two things, w462 at 26 of 26, w394 at 17 of 17, the web battery at 465 of 465, and the battery at 225 passed, 0 failed and 0 did not run at fb1c5a6. D-1105.* ON PRODUCTION: the migration applied through its apply script with each code once after and EC386 four ways before, the function's hash equal to local's, the migration ledger complete, /health names fb1c5a6 and not the build before it, and the web's scripts carry the three sentences and their Hungarian after the deploy and not before. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W475An approval over a removed record is withdrawncompleteW469'S NOT-DONE LIST: AN APPROVAL OUTLIVED ITS RECORD. app.approval NAMES ITS SUBJECT WITH NO KEY, SO REMOVING A PROGRAMME ITEM, TASK, ASSET, BUDGET, CHECKLIST OR EVENT LEFT ITS OPEN APPROVAL IN ITS APPROVERS' QUEUE. ★★★ A DELETE TRIGGER ON EVERY SUBJECT TABLE WITHDRAWS THE OPEN APPROVALS OVER THE REMOVED ROWS, cascades included, and leaves a decided round as it was decided. ★★ EACH WITHDRAWAL IS AUDITED, with what it was over and who removed the record. ★ THE TABLES ARE THE SUBJECT CHECK'S OWN LIST, read by the migration's assertion and by the suite, so a subject type added without the trigger turns both red. *Evidence: w475-acceptance.mjs 10 checks, red 6 of 10 before and again with the triggers and function dropped, 10 of 10 after, 6 more sabotages exactly as required with everything restored, and the battery at 226 passed, 0 failed and 0 did not run at 6f37f3a. D-1106.* ON PRODUCTION: the migration applied through its apply script with no trigger before and all 11 after, 0 open approval(s) over a removed record withdrawn by its repair and none left, the function's hash equal to local's, the migration ledger complete, and /health names 6f37f3a and not the build before it. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W476A refused event move names the item's start in the event's zonecompleteW473'S NOT-DONE LIST: THE REFUSAL OF AN EVENT MOVE NAMED THE STRANDED ITEM'S START ON THE DATABASE SESSION'S CLOCK, BUDAPEST ON LOCAL AND GMT ON PRODUCTION, AND NAMED NO CLOCK. ★★★ THE START IS PRINTED ON THE EVENT'S OWN CLOCK, AND THE ZONE IS NAMED, so the English reads the same on any connection. ★★ A MOVE INTO ANOTHER ZONE USES THE CLOCK IT IS SAVED WITH. ★ A ZONE POSTGRESQL DOES NOT KNOW PRINTS UTC, because this trigger fires before the zone check, so the move is still refused with EC387. *Evidence: w476-acceptance.mjs 8 checks, red 5 of 8 before and again with W473's function, 8 of 8 after, 4 more sabotages exactly as required with everything restored, and the battery at 227 passed, 0 failed and 0 did not run at 82a2dc4. D-1107.* ON PRODUCTION: the migration applied through its apply script with the session's clock before and the event's after, EC387 still raised once, the function's hash equal to local's, the migration ledger complete, and /health names 82a2dc4 and not the build before it. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W477An event that owns records nothing else holds is refused in wordscompleteFOUND WHILE SCOUTING W475: AN EVENT THAT OWNED A CHECKLIST, DOCUMENT, INVOICE OR PURCHASE ORDER NOTHING ELSE HELD COULD NOT BE DELETED. THE KEYS SET THE EVENT TO NULL, EACH RECORD'S OWN CHECK REFUSED A ROW BELONGING TO NOTHING, AND THE ROUTE ANSWERED WITH POSTGRES'S SENTENCE AFTER ITS PREVIEW HAD LISTED THE RECORDS AS KEPT. ★★★ THE ROUTE COUNTS THEM FIRST AND REFUSES IN WORDS, before anything goes, with event.delete_would_orphan and the counts, and the dry run says whether the event may be deleted. ★★ THE PREVIEW NAMES THEM IN THE READER'S LANGUAGE AND KEEPS THE BUTTON DISABLED. ★ THE PREVIEW'S LABELS ARE ENGLISH KEYS, where they were Hungarian tuples the dictionary test could not see. *Evidence: w477-acceptance.mjs 8 checks, red 5 of 8 before and again with W477's diff reversed, 8 of 8 after, 6 more sabotages exactly as required with everything restored, the API unit tests at 666, the web battery at 465, and the battery at 228 passed, 0 failed and 0 did not run at b2ae877. D-1108.* ON PRODUCTION: /health names b2ae877 and not the build before it, and the web's scripts carry the refusal's sentence, its Hungarian and the English labels after the deploy and not before, and the Hungarian tuples before and not after. *Evidence: acceptance suite, 1 design doc.*
W478An event's deletion says which open approvals it withdrawscompleteW477'S NOT-DONE LIST: SINCE W475 AN EVENT'S DELETION WITHDRAWS EVERY OPEN APPROVAL OVER A RECORD IT TAKES WITH IT, AND ITS DRY RUN COUNTED NONE OF THEM, SO A READER DELETING AN EVENT WAS NOT TOLD WHOSE APPROVAL QUEUES WOULD CHANGE. ★★★ THE DRY RUN COUNTS THEM, over every approval subject table the deletion's cascading keys reach, quotes through engagements and crowd alerts through zones among them. ★★ THE PREVIEW NAMES THE COUNT IN THE READER'S LANGUAGE. ★ THE SUITE READS THE KEYS AGAIN, so a path added later that the route does not count turns it red. *Evidence: w478-acceptance.mjs 8 checks, red 4 of 8 before and again with W478's diff reversed, 8 of 8 after, 5 more sabotages exactly as required with everything restored, the API unit tests at 666, the web battery at 465, and the battery at 229 passed, 0 failed and 0 did not run at 1021330. D-1109.* ON PRODUCTION: /health names 1021330 and not the build before it, and the web's scripts carry the notice's words with their Hungarian and render the count after the deploy and not before. *Evidence: acceptance suite, 1 design doc.*
W479A crowd report reads its days as they are and its hours on the event's clockcompleteW471'S NOT-DONE LIST: THE CROWD REPORT PRINTED A BARE DAY THROUGH new Date, MIDNIGHT UTC SHOWN IN THE BROWSER'S ZONE, SO A READER WEST OF UTC READ 1 OCTOBER AS 30 SEPTEMBER, AND READ THE HEATMAP'S HOURS ON THE BROWSER'S CLOCK. ★★★ EVERY DAY PRINTS AS THE DAY IT IS, through one helper that formats midnight UTC in UTC. ★★ EVERY HOUR READS ON THE EVENT'S CLOCK, the zone /crowd/report has sent since W471. ★ THE DEFAULT WINDOW IS THE READER'S OWN DAYS, where it was UTC days. *Evidence: web/test/w479-crowd-report-clock.test.ts, red on 2 of 4 before and again with W479's diff reversed, 4 of 4 after, 6 more sabotages exactly as required with the files restored, the web battery at 469, and the battery at 229 passed, 0 failed and 0 did not run at 0ac1b89. D-1110.* ON PRODUCTION: /health names 0ac1b89 and not the build before it, and the crowd report's chunk reads no hour or day on the browser's clock and reads the event's zone after the deploy, where before it read the browser's clock five times and the event's zone never. *Evidence: 1 design doc.*
W480A date column's day is read on the reader's calendar as that daycompleteW479'S NOT-DONE LIST: THE API SENDS A SQL DATE AS MIDNIGHT UTC, AND THE DASHBOARD, THE SPONSORS SCREEN AND THE CLIENT QUOTES PARSED IT AGAIN AS AN INSTANT, SO FOR A READER WEST OF UTC A PROJECT STARTING ON 1 OCTOBER SAT ON 30 SEPTEMBER AND EVERY SUCH DAY PRINTED A DAY EARLY. ★★★ A DATE COLUMN'S DAY IS LOCAL MIDNIGHT OF THAT DAY, built from its parts by one helper, so the calendar places it and every screen prints it as the day the column holds. ★★ EVERY FORMAT THE SCREENS HAD IS KEPT. ★ THE AUDIT OF ALL 58 DATE COLUMNS FOUND NO OTHER SCREEN parsing one as an instant. *Evidence: web/test/w480-date-columns-read-on-the-readers-calendar.test.ts, red on 3 of 4 before and again with W480's diff reversed, 4 of 4 after, 5 more sabotages exactly as required with the files restored, the web battery at 473, and the battery at 229 passed, 0 failed and 0 did not run at 01be0b3. D-1111.* ON PRODUCTION: /health names 01be0b3 and not the build before it, and the three screens' chunks parse no date column as an instant after the deploy, where they parsed one 7 times before, and a script builds the day from its parts. *Evidence: 1 design doc.*
W481A crowd report's hours and days are cut on the event's clockcompleteW471'S NOT-DONE LIST: THE CROWD REPORT'S CURVE STEPPED ITS DAYS ON THE DATABASE SESSION'S CALENDAR AND ITS HEATMAP CUT THE SESSION'S HOURS, SO A NEW YORK EVENT WAS SAMPLED AT 23:00 ONCE ITS CLOCK WENT BACK AND ENTRANCES IN KOLKATA FELL IN HOURS THE EVENT DOES NOT HAVE. ★★★ THE CURVE STEPS ON THE EVENT'S CALENDAR, so every daily sample is the event's midnight. ★★ THE HEATMAP CUTS THE EVENT'S HOURS, whatever the session's zone and the event's offset. ★ A STEP SHORTER THAN A DAY STAYS A REAL STEP, so the event's 25-hour day keeps 101 quarter hours. *Evidence: w481-acceptance.mjs 14 checks, red 6 of 14 before and again with W176's functions, 14 of 14 after, 5 more sabotages exactly as required with everything restored, and the battery at 230 passed, 0 failed and 0 did not run at 515eaab. D-1112.* ON PRODUCTION: the migration applied through its apply script on PostgreSQL 18, with neither function on the event's clock before and both after, their hash equal to local's, the migration ledger complete, and /health names 515eaab and not the build before it. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W482An erasure date is counted on the event's calendarcompleteFOUND BY W481'S AUDIT: A CHECK-IN'S AND AN ACCREDITATION REQUEST'S ERASURE DATE COUNTED FROM THE EVENT'S END ON THE DATABASE SESSION'S CALENDAR, SO ON PRODUCTION A BUDAPEST EVENT THAT ENDED AFTER ITS MIDNIGHT COUNTED FROM THE DAY BEFORE AND A NEW YORK EVENT THAT ENDED IN THE EVENING FROM THE DAY AFTER, AND A WEEKLY PURGE TURNS SUCH A DAY INTO A WEEK WHENEVER IT CROSSES A SUNDAY. ★★★ THE LAST DAY IS THE EVENT'S, read in its own zone, and so is today for an event with no end. ★★ NOTHING ELSE MOVES, not a date the caller sets, not D-739's no stamp without a period, not the 90-day default, and not a row already stamped. ★ THE PURGES ARE UNCHANGED, because W215's footprint probe forbids them to read the event. *Evidence: w482-acceptance.mjs 11 checks, red 8 of 11 before and again with W175's and W210's functions, 11 of 11 after, 6 more sabotages exactly as required with everything restored, and the battery at 231 passed, 0 failed and 0 did not run at d005f91. D-1113.* ON PRODUCTION: the migration applied through its apply script with neither function on the event's calendar before and both after, their hash equal to local's, the migration ledger complete, and /health names d005f91 and not the build before it. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W483A quote's last day is counted on the event's calendarcompleteFOUND BY W481'S AUDIT: WHETHER A SENT QUOTE COULD STILL BE ACCEPTED, AND WHEN THE WEEKLY JOB EXPIRED IT, WAS JUDGED ON THE DATABASE SESSION'S CALENDAR, SO ON PRODUCTION A BUDAPEST QUOTE STAYED ACCEPTABLE AFTER ITS LAST DAY HAD ENDED THERE AND A NEW YORK QUOTE WAS REFUSED WHILE ITS LAST DAY WAS STILL RUNNING. ★★★ A QUOTE'S LAST DAY IS ITS EVENT'S, read in the event's zone. ★★ THE LAST DAY ITSELF IS STILL ACCEPTABLE, and a quote with no last day stays live. ★ W219'S SUITE STATES ITS DATES ON THE EVENT'S CALENDAR TOO, because on the harness session's they fail from 22:00 to 24:00 UTC. *Evidence: w483-acceptance.mjs 7 checks, red 4 of 7 before and again with W219's function, 7 of 7 after, 4 more sabotages exactly as required with everything restored, W219 8 of 8, and the battery at 232 passed, 0 failed and 0 did not run at 4e42881. D-1114.* ON PRODUCTION: the migration applied through its apply script with the session's calendar before and the event's after, the function's hash equal to local's, the migration ledger complete, and /health names 4e42881 and not the build before it. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W484The hint beside a quote's last day says what happens on itcompleteW483'S NOT-DONE LIST: THE FORM THAT CREATES A QUOTE SAID NOTHING EXPIRES A QUOTE, BESIDE THE VERY DATE THAT HAS EXPIRED QUOTES SINCE W219. ★★★ THE HINT SAYS WHAT THE DATE DOES, that the quote can be accepted until the end of that day in the event's time zone and shows as expired within a week. ★★ THE HUNGARIAN SAYS THE SAME, and the untrue sentence's translation is gone, because the i18n test treats an unused one as an orphan. *Evidence: web/test/w484-quote-last-day-hint.test.ts, red on 2 of 3 before and again with W484's diff reversed, 3 of 3 after, 3 more sabotages exactly as required with the files restored, the web battery at 476, and the battery at 232 passed, 0 failed and 0 did not run at 9b22c81. D-1115.* ON PRODUCTION: /health names 9b22c81 and not the build before it, and the client quotes chunk and the Hungarian catalogue carry the new sentence and not the untrue one after the deploy, where before they carried the untrue one and not the new. *Evidence: 1 design doc.*
W485A day the web takes for today is the reader's own daycompleteTHE BUDGET ACTUALS FORM AND THE TWO PROJECT PRINTOUTS TOOK TODAY AS THE UTC DAY OF NOW, SO A BUDAPEST READER RECORDING A COST JUST AFTER MIDNIGHT GOT YESTERDAY AND A LOS ANGELES READER PRINTING IN THE EVENING GOT TOMORROW. ★★★ TODAY IS THE READER'S OWN DAY, read on the browser's calendar by one helper in the date module. ★★ W479'S PRIVATE HELPER IS THE SHARED ONE, so the crowd report and the three new readers cannot drift apart. ★ THE TWO UTC GETTERS LEFT ARE RIGHT, plain clock arithmetic on strings with no zone. *Evidence: web/test/w485-today-is-the-readers-own-day.test.ts, red on 3 of 4 before and again with W485's diff reversed, 4 of 4 after, 4 more sabotages exactly as required with the files restored, the web battery at 480, and the battery at 232 passed, 0 failed and 0 did not run at 70f2c55. D-1117.* ON PRODUCTION: /health names 70f2c55 and not the build before it, and no live script takes today from a UTC ISO string after the deploy, where three did before, one in each of the three chunks. *Evidence: 1 design doc.*
W486Every feature category has Hungarian words, and its English name stayscompleteTHE OWNER'S REQUEST OF 2026-09-15: EVERY FEATURE CATEGORY IN HUNGARIAN, LATER IN OTHER LANGUAGES, WITH THE ENGLISH ORIGINALS KEPT. NOTHING IN THE SCHEMA COULD HOLD A CATEGORY'S NAME IN ANOTHER LANGUAGE. ★★★ EVERY CATEGORY HAS A HUNGARIAN NAME IN APP.FEATURE_CATEGORY_LABEL, keyed on the category and the locale, so another language is rows. ★★ ENGLISH STAYS THE NAME, and the table refuses a label in an English locale. ★ JOINED ON THE ENGLISH NAME, because a category's id differs between local and production. *Evidence: w486-acceptance.mjs 6 checks, red 5 of 6 before and again with the table dropped, 6 of 6 after, 5 more sabotages exactly as required with everything restored, and the battery at 233 passed, 0 failed and 0 did not run at 69da582. D-1118, Q-561.* ON PRODUCTION: the migration applied through its apply script with 3,150 labels, one for every production category, and the labels hash equal to the migration's list over those names, the migration ledger complete, and /health names 69da582 and not the build before it. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W487The public directory answers a category in the reader's language, and the whole tree at oncecompleteTHE SECOND SLICE OF THE OWNER'S REQUEST OF 2026-09-15. W486 GAVE EVERY CATEGORY HUNGARIAN WORDS, AND THE PUBLIC DIRECTORY STILL ANSWERED EVERY READER IN ENGLISH, SEARCHED THE ENGLISH NAME ALONE, SORTED IN BYTE ORDER AND COULD ONLY BE READ 100 CATEGORIES AT A TIME. ★★★ A CATEGORY ANSWERS IN THE READER'S LANGUAGE WITH ITS ENGLISH NAME BESIDE IT, and a caller that asks for nothing gets English. ★★ THE WHOLE TREE AT ONCE, with the published suppliers on each branch counted once each. ★ HUNGARIAN IN HUNGARIAN ORDER, and a Hungarian word finds its category. *Evidence: w487-acceptance.mjs 14 checks, red 11 of 14 before and again with the diff reversed, 14 of 14 after, 6 more sabotages exactly as required with everything restored, the type check clean, W30's suite passing, and the battery at 234 passed, 0 failed and 0 did not run at d2e29e7. D-1119.* ON PRODUCTION: the tree answers production's 3,150 categories, 3092 of them in Hungarian, the level-1 categories answer in Hungarian with name_en and in English for en, and /health names d2e29e7 and not the build before it. *Evidence: acceptance suite, 1 design doc.*
W488The supplier category tree behind a shared link, in English and HungariancompleteTHE LAST SLICE OF THE OWNER'S REQUEST OF 2026-09-15, AND THE OWNER'S WORD MID-SLICE: THE TREE IS PUBLIC THROUGH A SHARED LINK AND NOT PUBLISHED ON THE MARKETING WEBSITE. ★★★ THE SHARED LINK DRAWS THE LIVE TREE IN ENGLISH AT / AND IN HUNGARIAN AT /HU/, with the English name one hover away. ★★ THE SWITCHER CARRIES THE READER'S PLACE BOTH WAYS, measured in a browser on production after a fix the first browser reading called for. ★ SEARCH ENGINES ARE ASKED TO STAY OUT, and the website pages first built were reset before they shipped. *Evidence: 1 design doc.*
W489The app's category picker reads in the reader's languagecompleteAFTER THE OWNER'S REQUEST OF 2026-09-15. W486 TO W488 PUT THE HUNGARIAN NAMES IN THE PUBLIC DIRECTORY AND BEHIND A SHARED LINK, WHILE THE APP'S OWN CATEGORY PICKER STILL ANSWERED EVERY READER IN ENGLISH AND THE WEB NEVER ASKED IN THE READER'S LANGUAGE. ★★★ THE PICKER ANSWERS IN THE READER'S LANGUAGE WITH THE ENGLISH NAME BESIDE IT, on all five routes that read it. ★★ THE WEB PICKER ASKS IN THE READER'S LANGUAGE. ★ HUNGARIAN IN HUNGARIAN ORDER, and a caller that names no language reads English as before. *Evidence: w489-acceptance.mjs 8 checks, red 5 of 8 before and again with the diff reversed, 8 of 8 after, 5 more sabotages exactly as required with everything restored, both type checks clean, the web battery and the i18n-wrap check passing, the W378, W29-1, W2 and W10 suites passing, and the battery at 235 passed, 0 failed and 0 did not run at 49217f8. D-1121.* ON PRODUCTION: /health names 49217f8 and not the build before it, and one live script asks for the picker's categories in the reader's language where one asked the old way before. *Evidence: acceptance suite, 1 design doc.*
W490A category named beside a record reads in the reader's languagecompleteAFTER THE OWNER'S REQUEST OF 2026-09-15. W489 PUT THE HUNGARIAN NAMES INTO THE CATEGORY PICKER, AND THE QUOTE LINE, BUDGET LINE, ASSET AND PRODUCT SCREENS THAT DRILL WITH IT WENT ON NAMING THE CHOSEN CATEGORY IN ENGLISH RIGHT BESIDE IT. ★★★ A CATEGORY NAMED BESIDE A RECORD READS IN THE READER'S LANGUAGE, on the eight reads behind those screens. ★★ THE WEB ASKS FOR THOSE READS IN THE READER'S LANGUAGE. ★ ONE HELPER, BESIDE THE PRODUCT LABELS, and a caller that names no language reads English as before. *Evidence: w490-acceptance.mjs 12 checks, red 9 of 12 before and again with the diff reversed, 12 of 12 after, 5 more sabotages exactly as required with everything restored, both type checks clean, the web battery and the i18n-wrap check passing, and the battery at 236 passed, 0 failed and 0 did not run at 28271c1. D-1122.* ON PRODUCTION: /health names 28271c1 and not the build before it, and the live web makes the six reads in the reader's language. *Evidence: acceptance suite, 1 design doc.*
W491The asset visibility editor and the supplier portal name a category in the reader's languagecompleteAFTER THE OWNER'S REQUEST OF 2026-09-15. W490 NAMED A RECORD'S CATEGORY IN THE READER'S LANGUAGE, AND THE ASSET VISIBILITY EDITOR AND THE SUPPLIER PORTAL STILL ANSWERED ENGLISH, THE ROOTS IN ENGLISH ORDER, AND A HUNGARIAN WORD FOUND NOTHING. ★★★ THE VISIBILITY RULES AND THE PORTAL'S ASSETS NAME THEIR CATEGORY IN THE READER'S LANGUAGE. ★★ THE RULE EDITOR'S ROOTS COME IN HUNGARIAN ORDER, AND A HUNGARIAN WORD FINDS ITS CATEGORY. ★ THE WEB ASKS FOR THOSE FOUR READS IN THE READER'S LANGUAGE, and a caller that names no language reads English as before. *Evidence: w491-acceptance.mjs 8 checks, red 5 of 8 before and again with the diff reversed, 8 of 8 after, 5 more sabotages exactly as required with everything restored, both type checks clean, the web battery and the i18n-wrap check passing, and the battery at 237 passed, 0 failed and 0 did not run at e3e4b51. D-1123.* ON PRODUCTION: /health names e3e4b51 and not the build before it, and the live web makes the four reads in the reader's language. *Evidence: acceptance suite, 1 design doc.*
W492The budget rollup names a category bucket in the reader's languagecompleteAFTER THE OWNER'S REQUEST OF 2026-09-15. W490 NAMED A BUDGET LINE'S CATEGORY IN THE READER'S LANGUAGE, AND THE ROLLUP TAB ON THE SAME SCREEN WENT ON LABELLING EACH CATEGORY BUCKET IN ENGLISH. ★★★ A CATEGORY BUCKET IS NAMED IN THE READER'S LANGUAGE. ★★ UNASSIGNED AND EVERY OTHER LENS KEEP THE ROLLUP FUNCTION'S LABEL, and no database function changed. ★ THE ROLLUP TAB ASKS IN THE READER'S LANGUAGE, and a caller that names no language reads English as before. *Evidence: w492-acceptance.mjs 5 checks, red 2 of 5 before and again with the diff reversed, 5 of 5 after, 3 more sabotages exactly as required with everything restored, both type checks clean, the web battery and the i18n-wrap check passing, and the battery at 238 passed, 0 failed and 0 did not run at 3112cd9. D-1124.* ON PRODUCTION: /health names 3112cd9 and not the build before it, and the live web makes the Rollup tab's read in the reader's language. *Evidence: acceptance suite, 1 design doc.*
W493The services list and the marketplace name a category in the reader's languagecompleteAFTER THE OWNER'S REQUEST OF 2026-09-15. W490 TO W492 NAMED CATEGORIES IN THE READER'S LANGUAGE, AND THE SERVICES LIST AND THE MARKETPLACE STILL ANSWERED ENGLISH, WITH NO LOCAL ROW TO SHOW IT. ★★★ A SERVICE'S AND A MARKETPLACE PRODUCT'S CATEGORY IS NAMED IN THE READER'S LANGUAGE. ★★ THE SUITE FILES AND SWEEPS ITS OWN ROWS, so the claim does not rest on data the local database happens to hold. ★ THE SERVICES SCREEN ASKS IN THE READER'S LANGUAGE, and a caller that names no language reads English as before. *Evidence: w493-acceptance.mjs 5 checks, red 3 of 5 before and again with the diff reversed, 5 of 5 after, 3 more sabotages exactly as required with everything restored and every filed row swept, both type checks clean, the web battery and the i18n-wrap check passing, and the battery at 239 passed, 0 failed and 0 did not run at 002f39d. D-1125.* ON PRODUCTION: /health names 002f39d and not the build before it, and the live web makes the services screen's read in the reader's language. *Evidence: acceptance suite, 1 design doc.*
W494The public directory's products name a category in the reader's languagecompleteAFTER THE OWNER'S REQUEST OF 2026-09-15. /directory/v1/products WAS THE LAST READ IN THE FILE STILL SELECTING fc.name AS A CATEGORY'S NAME, AND ITS category FILTER MATCHED THE ENGLISH NAME ALONE. ★★★ A PUBLIC PRODUCT'S CATEGORY IS NAMED IN THE READER'S LANGUAGE, resolving the tag, then its base language, then the English column, with category_name_en on every row and locale_requested on the answer. ★★ THE FILTER MATCHES EVERY LABEL AND THE DISPLAY SHOWS ONE, so a Hungarian reader can type the only word they have and a consumer that stored English names keeps its key. ★ AN EXPLICIT ?locale ONLY, NEVER Accept-Language, because this is the no-login surface whose answers are cached by query text, which is what W487 decided for the directory. *Evidence: w494-acceptance.mjs 8 checks, red 4 of 8 before with all four controls green, 8 of 8 after, six sabotages each red exactly as declared with every file restored byte for byte and every filed row swept, both type checks clean, and the battery at 237 passed, 0 failed and 0 did not run at e958a54. D-1126.* ON PRODUCTION: /health names e958a54 and 0 of five reads name the build before it, locale_requested resolves hu, hu-HU, en and the junk tag zzzz to en, and the discovery document lists locale. NO ROW-LEVEL PROOF IS AVAILABLE THERE: production carries 18 products and NONE published, so this endpoint has answered 0 items since it existed and still does. Publishing one to make a probe read prettily would change what the public directory serves. *Evidence: acceptance suite, 1 design doc.*
W495An owner's budget lines come in Hungarian ordercompleteAFTER THE OWNER'S REQUEST OF 2026-09-15. W490 PUT THE HUNGARIAN WORDS ON /budgets/lines/for-owner AND LEFT THE ORDER ON THE ENGLISH NAME, BECAUSE NO OWNER IN TENANT A HAD LINES IN TWO CATEGORIES AND NOTHING COULD TELL. ★★★ A HUNGARIAN READER'S LINES ARE SORTED AS HUNGARIAN, under hu-x-icu, measured present on local and on production before it shipped. Every other language keeps the order it had, and a line with no category stays last. ★★ ITS FINDING IS ABOUT THE TESTING: THE FIRST FIXTURE COULD NOT SEE THE COLLATION. Four categories that separate Hungarian order from the English order and from byte order, and NOT from this database's own en_US.UTF-8, so the sabotage that removed the collation changed nothing and the check had been passing without it doing any work. The digraph pairs, cs after cu and zs after zu, made the difference visible. ★ THE CATEGORY IS RESOLVED ONCE, IN A LATERAL, and read three times, because categoryNamed is a correlated subquery. *Evidence: w495-acceptance.mjs 8 checks, red 3 of 8 before with all five controls green, 8 of 8 after, five sabotages each red exactly as declared. Battery at fc8e182: 241 passed, 0 failed, 0 did not run, 0 measured nothing, 6084 assertions, with w31's standing skip the only section not passing. D-1127.* ON PRODUCTION: /health names fc8e182 and 0 of five reads name the build before it, and the shipped ORDER BY was run on the production database itself, answering Hungarian order for hu and English order for en, against an invalid control where an absent collation errors. THE ROUTE NEEDS A LOGIN AND ITS OBVIOUS PROBE DOES NOT DISCRIMINATE: an uncredentialled request answers 401 and so does a path that does not exist. NO OWNER THERE HAS LINES IN TWO CATEGORIES YET, 117 owners and 460 categorised lines measured, so the change is correct, shipped and not yet observable. *Evidence: acceptance suite, 1 design doc.*
W496Every notification type has Hungarian wordson productionTHE HANDOUT OF 2026-09-16 NAMED THIS AS THE FIRST OF THREE LABEL GAPS: 13 NOTIFICATION TYPES, 13 LABELS, ZERO HUNGARIAN, ON LOCAL AND ON PRODUCTION BOTH. ★★★ EVERY NOTIFICATION TYPE HAS HUNGARIAN WORDS, 13 rows beside the 13 English ones, and no code: both reads that resolve coalesce(l_exact, l_base, l_en) — the notifications list and the preferences matrix — were already wired and had nothing to find. ★★ THE ASSERTION THAT CARRIES THE SLICE IS ABOUT PLACEHOLDERS, NOT WORDS. title_template holds {actor} and {target}, Hungarian word order moves them, and a translation that drops one renders a sentence with a hole in it. The placeholder SET is compared and not the sequence, and a negative control that REORDERS them stays green — a check that went red there would have forbidden the exact thing translating into Hungarian requires. ★ title_template IS READ BY NO ROUTE AT ALL, zero occurrences measured; it is NOT NULL, so it was written as real Hungarian rather than filler, and that it is unread is recorded rather than skipped. *Evidence: control red naming all 13, four sabotages each red on its own assertion, the reorder control green with the row read back inside the transaction. Battery: 241 passed, 0 failed, 0 did not run, 0 measured nothing, 6084 assertions. Ledger 458 probes, present on prod. D-1129.* ★★ ITS FINDING IS A FALSE GREEN STANDING BESIDE A TRUE RED. The post-flight was written into a QUOTED heredoc and carried \$1, so the backslash survived into the file and printf was handed the literal string $1 as a locale. The coalesce fell through to English for every locale, which made the Hungarian arm go correctly red BY ACCIDENT and the English arm go green for entirely the wrong reason. Only the failing one was visible. After the fix each of the four arms was shown to move only for its own cause. ON PRODUCTION: 0 Hungarian before and 13 after, the English hash identical either side, hu and hu-HU resolving 13 of 13 to Hungarian and en and de 13 of 13 to English, and the junk locale zz resolving to English 13 and to Hungarian 0. *Evidence: 1 migration, 1 design doc.*
W497A reachable asset spec field has Hungarian words, and the twelve without are the twelve nobody is shownon productionTHE HANDOUT NAMED THIS AS THE SECOND LABEL GAP: 102 ROWS, 90 HUNGARIAN LABELS, 12 MISSING. THE COUNT IS RIGHT AND THE CONCLUSION IS WRONG. ★★★ THIS SLICE TRANSLATES NOTHING. Production falls into exactly two groups and no others: 90 not suppressed and all with Hungarian, 12 suppressed and all without; local 180 and 24 on the same rule. The one read of this table filters where not suppressed after picking one row per code, so a suppressed row reaches no reader in any language, and the 12 without words are precisely those 12. ★★ TRANSLATING THEM WOULD HAVE MADE A DATA DEFECT LOOK FINISHED. All 12 sit on ONE product, "Bespoke software", at line_order 999 with label equal to code — ballast_kg, wind_rating_kmh, fire_class, rigging_load_kg and nine more. Physical rigging attributes on a software product, every one with a properly labelled common twin, every one already suppressed by somebody who was right. Hungarian words would have turned twelve VISIBLY wrong rows into twelve INVISIBLY wrong ones and made a count read 102 of 102. ★ WHAT SHIPPED IS THE RULE ITSELF, in two places read at different moments: a comment on app.asset_spec_field.suppressed for whoever runs \d+ next and counts the same twelve, and a probe in the manifest that is true only when the comment is present AND no reachable field lacks Hungarian — so it proves the migration landed and the invariant holds in one expression, on the ledger run every session makes at Step 0b. NOT A TRIGGER: there is nothing to refuse on either target, and the upgrade path is written into the file. *Evidence: control green; un-suppressing an untranslated field red; deleting a label from a reachable field red; and a negative control — the same field un-suppressed AND translated — green, which is the point, because the check fires on the omission and not on making a field reachable. Ledger 459 probes, present on prod. D-1130.* ON PRODUCTION: no comment before, applied exit 0, the two groups re-measuring 90 and 12, the probe expression built exactly as migration-ledger.sh builds it answering true, and an invalid control turning it false inside a rolled-back transaction with production still holding 12 suppressed afterwards. THE TWELVE ROWS ARE STILL ON THE WRONG PRODUCT, suppressed and harmless. Removing them is a deletion of production data, one of the four things that need the owner, so it is recorded and not done. *Evidence: 1 migration, 1 design doc.*
W498The last glossary entries have Hungarian words, and the reader reaches themcompleteTHE HANDOUT NAMED IT: 12 PUBLIC TERMS AND 14 DEFINITIONS WITH NO HUNGARIAN WORDS, AND MEASURE WHAT THE READER CAN REACH BEFORE WRITING A WORD. ★★★ THE WORDS WERE MISSING AND SO WERE TWO OF THE THREE DOORS. All 26 rows are W320's professional tips and all are reachable, unlike W497's twelve: the Hungarian szótár search for canape answered four English tips on production. But the dashboard's glossary card sent no locale to a directory that reads only an explicit one, so it was English for all 2 846 terms that already had Hungarian words, and the tip popover sent none either and followed the browser instead of the screen. Both now send localeParam(), as the help drawer always did, and a web survey fails on any glossary, tip or help read that does not. ★★ EVERY FIGURE IS CHECKED. The tips are numbers, 7-8 pieces a head, 55 per cent, 0.19 litres, and the migration compares each Hungarian paragraph's digit runs with its English twin's as a multiset, so the Hungarian decimal comma passes and a changed, dropped or added figure does not. ★ W38.26'S COMPLETE HAD GONE FALSE. Its whole-glossary count read 12 on both targets after W320 and would fail a replay, so this migration's own assertions are scoped to the rows it writes, and the whole-glossary rule is a ledger probe instead. *Evidence: migration assertions forced red eight ways on local, each on its own assertion, a decimal-point negative control green; the web survey red once per door; w498-acceptance 13 of 13 with four data and code cuts each red on exactly the checks named. Battery: 243 passed, 0 failed, 0 did not run, 0 measured nothing, 6076 assertions. Ledger 461 probes, present on prod. D-1132.* ON PRODUCTION: untranslated 12/14 to 0/0 with every other row hashed identical, the probe false inside a rolled-back deletion, the public glossary Hungarian for hu and English for en and de, the szótár page driven in a browser, and both URL templates in the served bundle carrying the locale where the previous chunks did not. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W499Anything in an event can be archived, with a reason, and brought backcompleteTHE OWNER, 2026-09-16: DECLINED IDEAS COME BACK TO THE SURFACE, BECAUSE AN EVENT HAS MANY STAKEHOLDERS, AND THE PICTURE OF THE EVENT HAS TO STAY TRUE. ★★★ ELEVEN KINDS ARCHIVE WITH A REQUIRED REASON AND RESTORE EXACTLY, because archiving is purely additive: three columns, no status touched. Ten list reads leave archived rows out; detail reads deliberately do not, or restore would have nowhere to live. One page, *Archived and declined*, shows why each thing was put away, for an event or a WHOLE PROJECT, because October 23 is 29 events and a project-wide budget. The October 23 price table shows why: 107 of its 109 cancelled lines explain the cancellation in a description sentence and 103 name how to restore it, because there was nowhere else to put a reason. ★★ THE BATTERY CAUGHT TWO REAL DEFECTS. The workforce roster died on a filter over a column the migration had given the wrong table, which a type check cannot see inside SQL; and the picture skipped gate 4, W241 finding the same hole it found in /events/detail. The write routes had it too. ★ A PROJECT READER IS NOT THEREBY A READER OF EVERY EVENT IN IT: each event goes through the real gate 4 and its rows drop only when gate 4 refuses. Measuring caught two more: files attach through file_attachment, which the first resolver never read, and W459's budget restore answered 23514 on a reasoned archive until it cleared all three columns. venue_space is deliberately NOT archivable: it has no tenant and no event, and archiving shared directory data would change what other tenants see. D-1131. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W500An archived budget line is counted nowherecompleteW499 TOOK AN ARCHIVED LINE OUT OF THE LINES TAB AND NOTHING ELSE, AND D-1133 MADE IT VISIBLE: EIGHT OCTOBER 23 BUDGETS WHOSE LINES TAB AND ROLLUP TAB DISAGREED BY 905 121 229,92 FT. ★★★ AN ARCHIVED LINE LEAVES EVERY READER THAT TOTALS A BUDGET: the rollup, its margins, the commitment view and the price table in the database, and the combined project budget, a space's costs and an owner's mini budget in the API. ★★ THE OPTION RULE IS DELIBERATELY UNCHANGED. An archived option still wins its group, because letting it step aside would put the Parliament light painting's live 60 000 000 Ft alternative into a public tender's price table the moment its cancelled winner was archived. That is Q-564, not a filter's side effect. ★★ A FILTER CAN BE WRITTEN AND DO NOTHING: the assertion counting the filter in each function's source passed on (archived_at is null or true), and only the assertion that archives a real line and measures the rollup caught it. ★ IT WAS BUILT BEFORE IT WAS ASKED FOR, AND THE PERMISSION LAYER REFUSED TO COMMIT IT, while the same session's confirmed work committed and deployed. It waited, whole, as a proposal until the owner said ship it. *Evidence: migration assertions forced red seven ways, two of them inert filters; w500-acceptance 14 of 14 with eight cuts each red on exactly the checks named; the apply script rewritten to check every reader against a recomputation from the base tables, rehearsed on local with three lines archived (mismatches 10, 1, 2 and 1 before, none after, 56 budgets and 4 projects unchanged) and failed by a negative control whose price-table filter was inert. D-1134.* ON PRODUCTION: rollup mismatches 40 and commitment 8 before, none on any reader after, the 37 budgets and 1 project without an archived line identical, 0 of 45 budgets whose lines tab and rollup tab disagree, from 8, the price table unmoved at 370 lines and 1 619 130 996,06 Ft, the probe false with the commitment view put back unfiltered inside a rolled-back transaction, and the API on c96831d. File 25 then archived the last two cancelled lines with W500 live and the VAROS rollup fell by exactly their 796 500 000 Ft. *Evidence: 1 migration, acceptance suite, 1 design doc.*
Phase 12 - The words the product stores
W501A system admin edits every translation, and every change is keptcompleteTHE OWNER, 2026-09-17, Q-568: TRANSLATION MATCHING AND EDITING SCREENS FOR THE SYSTEM ADMIN, OVER ALL NINE TABLES THAT HOLD TRANSLATIONS. ★★★ ONE SHAPE FOR NINE TABLES: api/src/translations.ts describes each as a query returning key, sort, context, source and translation, so filters, search, counts and the save work alike, and a tenth table is one entry. ★★ ROW SECURITY ALREADY ADMITTED A PLATFORM ADMIN TO ALL NINE, measured before a line was written, so no permission was designed; five /admin/translations routes answer 404 to anybody else. ★★ THE HISTORY IS APPEND-ONLY FOR app_rw, because schema app's default privileges grant everything and the screen whose edits it records must not rewrite it. ★ A venue overview translates from its detected original, never into it, and costs W255's detector once per venue, about 5 s a page under row security; an expression index measured no faster, because the detector is not leakproof. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W502Q-561's review happens in the translations screencompleteTHE OWNER, 2026-09-17, Q-561 AND Q-568: THE DOUBTS AND THE TIPS ONLY, REVIEWED INSIDE THE SYSTEM ADMIN'S TRANSLATIONS SCREEN, ONE BY ONE. ★★★ 181 ITEMS FROM FOUR SOURCES, GENERATED FROM FILES ON DISK: the translators' 114 doubts, each found by its category's unique English name, W498's 26 tip texts, W496's 13 notification types and W499's 28 archive words, each with why it is queued, marked reviewed with who, when and a note. ★★ ROW SECURITY HID A MISSING ROUTE GATE: with the gate removed a non-operator still got 404, from the queue's policy, so the suite requires the gate's own sentence. ★ THE APP'S OWN WORDING IS REVIEWED BY NOTE, listed read-only beside its Hungarian, because it lives in the web bundle's dictionary. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W503The consistency view over every table at oncecompleteTHE OWNER, 2026-09-17, Q-568 ANSWER 2: FIND ONE WORDING TRANSLATED SEVERAL WAYS, AND ONE TRANSLATION STANDING FOR SEVERAL WORDINGS, ACROSS ALL NINE TABLES. ★★★ MEASURED ON PRODUCTION FIRST: 32 wordings translated more than one way and 77 translations standing for more than one wording, out of 9 219 translated wordings, so the answer fits one page and no queue was built. ★★ ONE QUERY BUILT FROM THE TABLES' OWN DEFINITIONS, the same descriptions the side-by-side view reads, compared trimmed and case-folded, list fields left out. ★★ VENUE OVERVIEWS ARE READ FROM THEIR TRANSLATIONS, because reading them the usual way costs 4.3 s of W255's language detector, for rows production does not have yet, and the suite times the answer to hold that. ★ A CORRECTION IS AN ORDINARY SAVE, one row or a whole group, so the rules and the history are W501's. *Evidence: acceptance suite, 1 design doc.*
W504A machine suggestion an admin acceptscompleteTHE OWNER, 2026-09-17, Q-568 ANSWER 7: FILL THE EMPTY ROWS WITH SUGGESTIONS, EACH TAKING EFFECT ONLY WHEN AN ADMIN ACCEPTS IT, WITH QUALITY MEASURED ON A SAMPLE FIRST. ★★★ THE MEASUREMENT CAME FIRST, on 46 wordings the product already holds in Hungarian: gpt-oss-120b matched 11 to 15 word for word and came close on 3 to 8, ahead of llama-4-scout, llama-3.3-70b, m2m100 and mistral-small, and the same model moves by three or four between runs, so the table is a rank. ★★ A REASONING MODEL ANSWERS NOTHING WHEN ITS TOKENS RUN OUT: five of 46 came back empty at 400 tokens, none at 1 500. ★★ CONTEXT MUST BE FENCED OR THE MODEL TRANSLATES IT, and a prompt that names a placeholder as an example gets that placeholder back in the answer. ★★ ACCEPTING ONCE DELETED THE RECORD IT WAS ABOUT TO KEEP, because the discard of a person's own save lived in the shared write; the suite caught it. ★ Nothing suggested reaches a reader until an operator accepts it, and accepting is the ordinary save, with its history. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W505The overview language is stored, not detected on every readcompleteW501 LEFT IT MEASURED: A PAGE OF VENUE OVERVIEWS COST 4 865 ms AND ITS COUNT 4 013 ms, ALL OF IT W255'S DETECTOR OVER ALL 45 013 VENUES. ★★★ STORED ON THE ROW, KEPT BY A TRIGGER, with a partial index: the same page now takes 53 ms and the count 17 ms, measured as app_rw under row security. ★★ AN INDEX ALONE COULD NOT HELP: the detector is immutable but not leakproof, so row security keeps it out of an index condition. ★★ A STORED GENERATED COLUMN WAS REJECTED ON ITS LOCK, 27 s of ACCESS EXCLUSIVE on the table the public directory reads, against an ordinary column whose backfill takes row locks and blocks no reader. ★ A TIMING CHECK IS THE ONLY WITNESS: put the detector back and every answer is still right, so the suite bounds the page at a second and the sabotage that restores W501's exact query reddens that check alone. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W506The translations screens refuse in the reader's languagecompleteW501 LEFT IT OPEN: A REFUSAL FROM THESE SCREENS ARRIVED IN ENGLISH, INSIDE A SCREEN THAT IS HUNGARIAN DOWN TO ITS BUTTONS. ★★★ THE GUARD NAMED ONE FILE AND THE CODES LIVED IN ANOTHER: server-message-codes.test.ts scanned index.ts and the planners, W506's ten codes are in translations.ts, and all ten would have shipped unguarded. It now reads every module of the package, and the sabotage proves the old scan was blind. ★★ A FIELD NAME IS A WORD, NOT DATA: it travels as a message ref and the catalogue words it, which is D-920's rule rather than a new mechanism. ★★ THE UNIT TESTS THAT PINNED THE OLD REFUSAL SHAPE WENT RED, which is a contract announcing itself, and they now pin the code as well. ★ The English sentence stays beside every code, so a caller that never heard of the catalogue still reads something true. *Evidence: acceptance suite, 1 design doc.*
W507Every view reads as the caller, including the last onecompleteAN AUDIT OF PRODUCTION FOUND THE ONE VIEW OF 21 WITHOUT security_invoker, AND EVERY VIEW HERE IS OWNED BY A ROLE WITH BYPASSRLS. ★★★ REPRODUCED WITH A POSITIVE CONTROL, on local, rolled back: as another workspace the base table answered 0 and the view answered 1; with the option set 0 and 0; as the owner 1 and 1, so the test was not satisfied by an empty database. ★★ THE FEATURE WORKS EITHER WAY, which is why nobody noticed: with the view put back, the isolation checks go red and the feature check stays green. ★★ THE MIGRATION SWEEPS ALL FOUR SCHEMAS, so the next view created without the option fails rather than ships, and assertion 2 proves that sweep can fail. ★ Nothing was leaking: the route requires events.view and production has one workspace. This closed the layer the other twenty views already had. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W508The machine never overwrites a personcompleteW504 WROTE THE RULE INTO A COMMENT AND DID NOT KEEP IT: A FIELD THAT ALREADY HAS A TRANSLATION IS SOMEBODY'S WORK. ★★★ REPRODUCED BEFORE IT WAS FIXED: an operator asks for suggestions, types one translation by hand while the model is thinking, accepts the page, and their “EMBERI FORDÍTÁS” becomes the model's “GÉPI JAVASLAT”. The staleness check compared only the English. ★★ ACCEPTING NOW TAKES ONLY THE EMPTY FIELDS, discards a suggestion the person has answered, and refuses a row with nothing left to take, in the reader's language. ★★ MY FIRST TWO SABOTAGES REDDENED NOTHING, because with one suggested field the row is refused before the apply loop: the suite needed a row with one field written and one empty, which is the only shape that reaches it. ★ One guard cannot be reached over HTTP and says so rather than resting on a green suite. *Evidence: acceptance suite, 1 design doc.*
W509The help drawer stops naming one computercompleteAN AUDIT OF PRODUCTION FOUND THE IN-PRODUCT HELP TELLING EVERY SIGNED-IN READER TO RUN A SCRIPT AT /Users/<somebody>/Documents, IN BOTH LANGUAGES. ★★★ IT REACHED ANY CUSTOMER WHO OPENED HELP ON SETTINGS: the route asks for nothing but a workspace and the button is in both shells. Untrue for every reader, and it named the author's account. ★★ THE SEED NOW USES THE REPOSITORY-RELATIVE FORM another help topic had already chosen, and W274's suite refuses any seeded paragraph naming a path under /Users or /home, with the corpus size as its positive control. ★ A FIX IN THE REPOSITORY IS NOT A FIX ON PRODUCTION: the seed had been corrected hours earlier and production still served the old paragraph until the migration was re-run there. *Evidence: 1 design doc.*
W510A company does not carry another business's Google placecompleteAUDIT FINDING 2: 1 028 GOOGLE PLACE IDS HELD BY 2 370 COMPANIES, AND THEY ARE NOT DUPLICATES. Continental Caterers and Ulisess Catering share a place, so do two dance schools, and one id carries 13 wedding planners. ★★ THE OWNER CHOSE: CLEAR THEM, KEEP THEM, NO UNIQUE INDEX, so two companies at one place stays possible. ★★★ THE MIGRATION WAS NOT A TRANSACTION WHEN FIRST WRITTEN: a sabotage cleared all 24 825 local place ids and committed before the assertions ran, with 2 370 archived. It opens begin; now, as 282 of the 457 spine files already did. ★★ A LINE THAT DID NOTHING: provenance - 'place_id' was dead because W357's trigger already does it, and the assertion that remained now guards that trigger. ★★★ W58.16 AND W58.17's GUARDS CAUGHT IT AND WERE NOT BENT: their count floors now count what the import reached, held or archived, using the union idiom the w45 probes already use under D-650. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W511A provenance claim names a value that is therecompleteAUDIT FINDING 3, AND D-934 HAD ALREADY SAID WHAT IT COSTS: a stale imported claim on an empty column ranks 20 and refuses every ai_direct write, which ranks 10, so the field looks empty and refuses to be filled. 8 593 on production. ★★★ THE 8 530 ARE PROVABLY W281's RESIDUE: the stale ids on production and the NO RESULT ids in w58-14 to w58-17 are one set, zero in either direction. W357's trigger landed four days after W281 and wrote the sweep down as the owner's. ★★ THE FILE IMPLEMENTS NOTHING: three no-op assignments, and W357's own triggers rewrite the maps, so there is one definition of stale. updated_at not bumped, because two other products read these tables. ★★★ THE CONTROL HAD TO BE AN EQUALITY: a sabotage emptying the whole map left 297 175 live claims, above any floor worth writing. *Evidence: 1 migration, 1 design doc.*
W512A confirmed category is a category the company holdscompleteAUDIT FINDING 4, FIRST HALF: W81 ACCEPTED 1 803 CATEGORIES AND WROTE NONE OF THEM. W81 inserts only for adds, which is right for what confirms means, and for 1 803 proposals confirms was wrong: existing empty, the name a real category, no alias, 1 793 at high confidence. 131 companies held no category and all 131 were hidden by W54's bar. ★★★ INSERTED AS ai_direct, THEN W80 RE-RUN IN THE SAME TRANSACTION, because W80's standing probe forbids a company clearing the bar and staying hidden: 122 became public. ★★★ A TOTAL CANNOT SEE A TAKE-DOWN: a sabotage took a fixture company private while 122 were published, the total still grew, and it exited 0 and committed. The check now compares the published SET, in the file and in the apply script, and the fixture was found by set comparison and restored. *Evidence: 1 migration, 1 design doc.*
W513A disputed category goes where a better one stayscompleteAUDIT FINDING 4, SECOND HALF: 951 CONTRADICTIONS W81 HELD FOR A PERSON, AND THEY MEAN THE OPPOSITE OF WHAT THEIR SHAPE SUGGESTS. existing is empty on all 951 and proposed names the category the company HOLDS that the evidence disputes: Borsa Italiana, Italy's stock exchange, filed under Event Management. So accepting one REMOVES a category. ★★★ THE OWNER CHOSE: REMOVE WHERE ANOTHER CATEGORY STAYS, 566 companies, and leave the 248 that would be left public under no category at all. ★★ THE PROPOSAL ROW IS THE ARCHIVE: no table added, the decision recorded before the category is deleted, and only imported rows eligible so the undo is exact. ★★ THE HEADER SAID RE-RUNNABLE BEFORE IT WAS: a second run compared 566 lifetime decisions with 0 removals and failed, and a || true in my own test hid it. Scoped to decided_at = now(), and every sabotage re-run against the final file. *Evidence: 1 migration, 1 design doc.*
W514Every fact the venue API serves has a confidencecompleteAUDIT FINDING 5: 29 236 ADDRESSES, 25 920 COUNTRIES AND 23 634 CITIES REACHED FOUR PRODUCTS WITH NO CONFIDENCE AT ALL. ★★★ THE FUNCTION ALREADY SAID SO AND WAS NEVER ASKED: provenance_confidence returns inferred for an absent source, its else branch commented ai_direct, unknown, absent, and the route only called it over keys that existed. ★★ BACKFILLING WAS MEASURED AND REFUSED: 2 238 addresses equal the Bubble export, 12 656 equal it once its HTML is stripped, about 14 300 came from elsewhere. ★★ THE OWNER CHOSE TO FAIL CLOSED: the key set is now the provenance keys and every served fact with a value, one function, and an imported overview still reads published_spec as the regression control. *Evidence: acceptance suite, 1 design doc.*
W515A reader can see that a venue is closedcompleteAUDIT FINDING 6: W64.0 KEPT 270 TEMPORARILY CLOSED VENUES PUBLISHED SO THEY COULD STILL BE FOUND, AND NOTHING TOLD A READER THEY WERE CLOSED. operating_status was collected on 25 436 venues and served on no route, while the detail served a confidence for it. ★★★ SERVED ON THE DETAIL AND ON THE LIST VELA RENDERS FROM, per Q-513, as Google's own value or null. ★★★ A LINE NOTHING COULD TEST: every status has provenance on local and production, so W514's new fact-list entry was unreachable. The suite plants the case on one venue and restores it in a finally, and the sabotage deleting the entry reddens that check alone. *Evidence: acceptance suite, 1 design doc.*
W516A company's sectors are the roots above its categoriescompleteQ-525, OPEN SINCE 2026-08-31, ANSWERED: sectors WAS NULL FOR ALL 27 406 PUBLISHED COMPANIES. ★★★ THE OWNER CHOSE THE 19 CATEGORY ROOTS, stored and kept current by three triggers so every reader, not only the API, gets one answer. ★★★ THREE OF MY OWN DEFECTS, ALL BEFORE PRODUCTION: a text[] reads back on workerd as the string '{a,b}', caught by asserting the route's response; the full recompute was per row, 20.6 s changing nothing, after a set-based 136 ms had justified it; and the suite's selection took 317 s. ★★ THE API SHIPPED BEFORE THE MIGRATION so production never served a string. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W517An event carries the currency its money is incompleteAUDIT FINDING 10's TWO PROVABLE ITEMS. 24 events held money only in HUF and carried no currency, the audit's 11 being those with lines; BQ was the one country of 250 with no flag. ★★ BY PROPERTY, NEVER OVERWRITING, NEVER CHOOSING BETWEEN TWO, with seeded fixtures exempt. *Evidence: 1 migration, 1 design doc.*
W518The nru schema has row security of its owncompleteTHE AUDIT'S CONTAINED ITEM: 789 NAMED PEOPLE WITH NO ROW SECURITY, SAFE ONLY BECAUSE NOTHING HELD A GRANT. ★★★ THE OBVIOUS FIX WOULD HAVE BROKEN ANOTHER PRODUCT: the NRU planner's Worker connects as nru_mirror, which does not bypass row security, and a local simulation showed it reading 0 rows under the naive fix. ★★ ONE POLICY PER TABLE FOR nru_mirror ALONE: it keeps its access, and a mis-granted role reads nothing where it read everything before. ★★★ PROVEN ON PRODUCTION BY THE PLANNER ITSELF: its first sync afterwards wrote all nine tables in full. *Evidence: 1 migration, 1 design doc.*
W519A save carries only what changed, and refuses a clashcompleteTHE CODE REVIEW'S OPEN FINDING: THE TRANSLATIONS SCREEN SENT EVERY FIELD AS IT LAST LOADED THEM, SO TWO TABS SILENTLY UNDID EACH OTHER. ★★★ THE OWNER CHOSE: ONLY WHAT CHANGED, REFUSE A CLASH. The screen sends edited fields with their starting values, saveRow keeps the rest as stored now and answers translations.save_clash, 409, in Hungarian. ★★★ THE DEPLOY ORDER WAS A DATA-LO★★ QUESTION: the new screen against the old API would have cleared every untouched field, so the API went first. ★★ A 200 PROVES NOTHING ON THIS HOST: an invented chunk name answers 200 with the app shell, so the served screen chunk was read for its content. *Evidence: acceptance suite, 1 design doc.*
W520An archived option never wins its groupcompleteONE OF THE THREE UNAPPROVED FIXES: THE OPTIONS TAB MARKED FOUR CANCELLED, ARCHIVED OPTIONS AS IN THE BUDGET. ★★★ THE FIRST VERSION REVERSED AN OWNER DECISION AND W500's GUARD CAUGHT IT: letting the next live option win is exactly the promotion D-1134 refused, and W500's check 4a went red. ★★ AN ARCHIVED LINE IS NEVER ITSELF ACTIVE, AND AN ARCHIVED LEADER STILL PROMOTES NO RIVAL, so the badge stops lying and no total can move. ★ A sabotage of my own escaped its transaction by sitting before begin, and every later one asserts it sits inside. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W521A budget line holding real money is not archivedcompleteTHE SECOND UNAPPROVED FIX: W500 COUNTS AN ARCHIVED LINE IN NO TOTAL, SO ARCHIVING A LINE WITH REAL MONEY WOULD HIDE IT. ★★ A TRIGGER REFUSES IT, EC389, for an issued purchase order, a live invoice or any actual; restoring is never refused. ★★ THE ROUTE NEEDED NOTHING: the Worker's outermost catch already passes every coded refusal through asRefusal, and the sabotage removing a first-draft catch stayed green. ★★ W462's REGISTRY CAUGHT THE NEW CODE and it was registered by reading its one raise. Proven on production in rolled-back transactions. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W522A snapshot, or an archived budget, counts in no project totalcompleteTHE THIRD UNAPPROVED FIX, AND IT WAS LARGER THAN DESCRIBED: A SNAPSHOT KEPT ITS EVENT AND PROJECT, SO THE TENDER'S PRICE TABLE COUNTED ITS LINES TWICE. One snapshot took a local price table from 87 rows to 99 and by 520 000 000 Ft. ★★★ ONE DEFINITION, SIX READS: the price table, four queries of the combined project budget, a space's costs and an owner's mini budget all ask budget_counts_in_totals. ★★ artabla CHANGED IN PLACE BEHIND AN md5 GUARD, because production's body is draft 22's Hungarian one. ★★ THE MIGRATION SHIPPED FIRST, since the API calls a function it creates. Proven on production, rolled back: a snapshot of the largest October 23 budget left the tender at 405 rows. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W523A notification reads in the reader's own languagecompleteD-580 WAS DECIDED ON 2026-08-01 AND UNBUILT FOR SIX WEEKS: BOTH NOTIFICATION READS RESOLVED A CONSTANT en, SO W496'S HUNGARIAN WORDS WERE NEVER SHOWN. No setting said which language a user reads. Owner 2026-09-18, Q-559: the user's own language. ★★★ ONE LIST, ONE RESOLVER: preference language allows en and hu, and app.reader_locale() resolves the reader's own, then the workspace's default, then en, as the caller. ★★ THE WEB SAVES A SWITCH BEFORE THE REMOUNT, because LocaleProvider remounts the tree on a switch, and adopts a saved language once per person. A guessed language is never saved, since a saved one outranks the workspace's default. ★★ TWO OF MY OWN DEFECTS CAUGHT: the settings answer left the language out, and a Worker allow-list duplicated the database's, found when its sabotage stayed green. ★ THE MIGRATION SHIPPED FIRST. Proven on production, rolled back: a saved hu names the task notification *Rád osztott feladat*, and de is refused. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W524The Dashboard is the first rail icon at every levelcompleteTHE OWNER'S SCREENSHOT: INSIDE AN EVENT THE DASHBOARD SAT SEVENTH OF ELEVEN, AND OPENED THE WORKSPACE DASHBOARD UNDER AN UNRELATED EVENT. ★★★ HOME LEADS BOTH RAILS, then the event's families, then the other global areas, and from inside an event Home leaves the event. D-1174, chosen from two readings drawn as rails. ★★ THE SUITE CAUGHT MY OWN REGRESSION: an unknown route inside an event fell back to the rail's first icon, which had become a global group. ★ D-620'S COMMENT CORRECTED: a rail click never left the event. Proven by the nav tests, five sabotages each reddening its own assertion, a browser walk with Directory as the control, and the served entry byte-identical to the build. *Evidence: 1 design doc.*
W525A workspace's owner and Admins edit its name and defaultscompleteNOBODY BUT THE PLATFORM COULD CHANGE A WORKSPACE AFTER CREATING IT, SO ITS CURRENCY, LANGUAGE, TIME ZONE, VAT AND MARKUP KEPT THEIR COLUMN DEFAULTS FOR LIFE. Owner 2026-09-18, Q-560: its owner and Admins. ★★★ WHICH COLUMNS, BY GRANT AND NOT BY TRIGGER: app_rw may write the name, the five defaults and updated_at, never the slug, status or region. ★★ THE PERMISSION RESOLVES THROUGH app.effective_permissions, so the template chain and a deny count, which user_holds_permission ignores. ★★ FOUND ON THE WAY: a value too long for its column answered 503 retry in every route, a 400 now; and a sabotage committed, so every app_rw transaction in the suite rolls back. ★ 6d SAYS IT MEASURES ROW SECURITY, membership_iso, not W525's own clause. Driven on production, rolled back, with the Project manager as the invalid control. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W526A ticket type, and a cap nobody can sell pastcompleteTHE FIRST TICKETING SLICE, AND THE INVARIANT M-34 IS BUILT ON: NOBODY CAN SELL PAST A CAP, UNDER CONCURRENCY. ★★★ 25 TRANSACTIONS ON 25 CONNECTIONS RACE FOR A CAP OF 5, AND EXACTLY 5 WIN, all 20 losers refused with EC391. With the trigger's row lock removed, 15 won. ★★ FOUR TABLES, NOT SIX, D-1175: the credential will be the door's event_ticket, and a hold is an allocation's state with an expiry read, never swept. ★★ ANY REQUEST THAT SETS A CAP NEEDS tickets.cap, creating a ticket type included. ★ AN EVENT OF ANOTHER WORKSPACE WAS AN EMPTY LIST, now not found. Driven on production, rolled back: 3 of 5 sold at a cap of 3. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W527A buyer holds tickets without an accountcompleteTHE FIRST THING A STRANGER CAN DO: SEE WHAT IS ON SALE AND HOLD IT FOR TEN MINUTES, WITH NO ACCOUNT. ★★★ A STRANGER REACHES THREE DEFINERS AND NOTHING ELSE, through withShop, whose guard refuses any other table or function. ★★ ALL OR NOTHING: a hold that cannot be met in full holds none and writes no order. One browser holds little: ten tickets, three open orders an event. ★★ THE FULL BATTERY FOUND WHAT THE SLICE'S SUITE COULD NOT: W241's survey refused the offer route, whose verdict is now recorded. ★ A KILLED SABOTAGE RUN LEFT A FIXTURE CANCELLED and the next restored the damage, so the suite checks its fixture now. Production answered 401 before and 200 after, with EC394 from the definer itself. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W528Only a payment the provider confirms makes an order paidcompleteM-34's I2, AND THE OWNER CHOSE STRIPE THE SAME DAY, D-1176. ★★★ A SIGNATURE IS CHECKED OVER THE RAW BODY BEFORE ANYTHING IS READ, Stripe's scheme, with a five-minute replay window, and only then can an order become paid, idempotently, with the sum checked. ★★★ M-34'S DESIGNED BRANCH, BUILT: paid after the hold expired and the stock was sold is refunded, never overbooked. ★★ A TEST PROVIDER PRODUCTION REFUSES, proven there: the route answers 503 on production. ★ A SABOTAGE STAYED GREEN and found the suite read the order's expiry, not the tickets' holds. Stripe itself is unproven until the owner's keys exist. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W529A paid order becomes tickets, and each holder names themselvescompleteM-34's I3 AND THE OWNER'S D-1170: ONE TICKET PER PERSON, ON THE DOOR THAT EXISTS, AND THE PERSON IT REACHES TYPES THEIR OWN NAME. ★★★ THE PROVIDER'S VERIFIED WORD ISSUES THE TICKETS, idempotently, as W296 door tickets from an internal source, D-1175. ★★ THE DOOR REFUSES AN UNNAMED TICKET THAT MUST BE NAMED, a new ground, and its ledger could not record it until this slice's suite found the 23514. ★★ THE FULL BATTERY FOUND WHAT THE SUITE COULD NOT: W453 refused a single-column key between two workspace tables. ★ A SABOTAGE MASKED ANOTHER through orphaned tickets, so both suites sweep by source now. ★★ W529.2, THE BUYER'S PAGE END TO END, in both languages and with no account, walked in a browser from the offer to a named ticket admitted at the door. THE WALK FOUND WHAT NO SUITE HELD: the local gateway paid the order and then crashed handing the buyer back, because Response.redirect() makes immutable headers, D-1177. ★★ W529.4, THE BUYER IS MAILED EVERY TICKET, once, from the call that issues, in the language they bought in, every attempt written down and a refused mail leaving the order paid, D-1180. *Evidence: 2 migrations, acceptance suite, 3 design docs.*
W530A door sale and a ticket given away, from the same poolcompleteM-34 SECTION 7: ONE POOL, EVERY CHANNEL. ★★★ A DOOR SALE AND A GIFT ARE ORDERS PAID AT ONCE, ON CHANNELS OF THEIR OWN, so W526's cap counts them with the shop's holds, proven by one cap of 5 filled from all three and refused past it whichever way it was asked. ★★ EVERY SALE NAMES WHO MADE IT, I4, in the same transaction. ★★ THE WALK FOUND A CRASH IN THE SHARED FIELD: a list of one child passed Children.count and failed Children.only. ★ W462'S READER STOPPED AT A DOUBLED SQL APOSTROPHE. ★★ W530.2, A COUPON, a percentage or an amount off, through a channel of its own, its uses counted from live orders under a lock, so eight buyers racing for three uses got exactly three, D-1179. *Evidence: 2 migrations, acceptance suite, 2 design docs.*
W531A link and a QR per channel, and what each one soldcompleteB-210: ATTRIBUTION RIDES ON THE CHANNEL EVERY ALLOCATION ALREADY CARRIES. ★★★ A LINK IS AN ONLINE CHANNEL OF ITS OWN, and a buyer who came by it holds on it, the order and every ticket, so what each link sold is one grouping over rows that exist. ★★ A COUPON IS ITS OWN CAMPAIGN, AND A STALE LINK NEVER STOPS A SALE. ★ REVENUE BY SOURCE IS FINANCIAL, tickets.view_financial, D-1181. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W533The owner's live view, read from the sales and the door themselvescompleteB-212: THE DOOR'S LEDGER, NOT A NEW AGGREGATE. ★★★ ADMITTED IS A SOLD TICKET WITH AN ALLOWED ENTRY, and inside now is each person's last allowed scan at the event's door, because the front door allows a second entry and entries less exits would count that person twice, D-1182. ★★ THE SALES SCREEN OPENS WITH WHAT IS TRUE RIGHT NOW and refreshes itself while in front, revenue only for a reader who may see money. *Evidence: acceptance suite, 1 design doc.*
W534Each ticket type carries its refund rule, and a refund revokescompleteTHE OWNER'S D-1168 AS A SETTING. ★★★ ONE NUMBER PER TICKET TYPE, empty for no refund, 0 until the event starts, N until N hours before, so a lawyer's answer changes a value, D-1183. ★★ THE BUYER REFUNDS THE WHOLE ORDER OR NONE OF IT, never once a ticket was admitted, the provider first, then the order refunded, the pool given its tickets back and every ticket revoked, which the door refuses. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W537An event's seat map is its seat stockcompleteD-1185: THE SEAT MAP THE OWNER ALREADY HAD BECOMES WHAT THE SHOP SELLS. ★★★ A DRAWN SEAT IS A ROW NAMING ITS PRICE BAND: the bridge sends every seat the designer's own geometry produced, 5,326 on the demo bowl and all 5,326 stored, and the map sets each band's cap, D-1186. ★★ A HELD OR SOLD SEAT STAYS AS IT WAS SOLD, and a seat is held once because an index says so. ★ The sabotage that wrote renamed seats in one step stayed green until a reversed row proved the two-step write. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W538The organiser opens the seat designer from the eventcompleteD-1185: THE DESIGNER EVENT.CLINIC ALREADY HAD, REACHED FROM THE EVENT AT LAST. ★★★ THE PALETTE IS THE EVENT'S TICKET TYPES: a band made in the designer arrives as a draft ticket type, an edited band changes its ticket type, and a cap the map sets is refused by hand, D-1187. ★★ THE BROWSER WALK FOUND A PRICE EDIT CHANGING A COLOUR, the editor offering only its own swatches, and the fix kept it. ★ The Seating screen promised a template never brings its prices, and was corrected to say what it does bring. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W539The buyer chooses seats on the mapcompleteD-1185: THE HALL, AS A BUYER READS IT. ★★★ TWO PUBLIC READS: each section outlined along its own seats, with its bands and free count, and then one section's seats, free, taken or not for sale, so a page never carries fifteen thousand circles, D-1188. ★★ THE HOLD TAKES A SEAT, at its band's price, and a seat somebody took first is refused in words. ★★ AN EXPIRED HOLD GIVES ITS SEAT UP TO THE BUYER WHO ASKS FOR IT, which opened a race W528 never had: a payment arriving to find its hold released refunds the order whole rather than issuing a ticket that is gone. ★ Three sabotages were refused by the migration's own self-assertions before the suite could see them, and were run again in shapes that pass them. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W540The best seats are chosen for the buyercompleteD-1185: NOBODY BUYING FOUR TICKETS WANTS TO CLICK FOUR CIRCLES. ★★★ THE PROMISE IS N SEATS TOGETHER, NOT N FREE SEATS, so the picker looks for a RUN of adjacent free seats in one row of one price band, ranked by the lowest row and then by the run nearest that row's own middle. Cheapest is the same ranking with price in front of it, D-1190. ★★ THE PICKER ONLY ANSWERS AND app.shop_hold IS NOT TOUCHED: the page shows the seats and their prices and then holds them as ordinary seats, so the seat a buyer is shown is the seat they are charged for. A first draft put the pick inside the hold, and reproducing that function to do it produced a copy differing from the live one in twelve places. ★ A HIDDEN BAND IS NEVER CHOSEN FOR ANYBODY, by band, by section, or by being the cheapest thing in the hall. ★ Two of twelve sabotages passed the first campaign and were its whole value: a row middle measured over the survivors, and a run straddling two prices. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W541The seat travels with the ticketcompleteD-1185: A BUYER WHO CHOSE A SEAT CAN NOW FIND IT. W539 sold the seat and W529 issued the ticket, and between them the order page, the mail, the ticket page and the door each named the price band alone. ★★★ THE SEAT IS A KEY INSIDE AN ANSWER THAT ALREADY EXISTS, so shop_order and shop_mail_content keep their shape, and the ticket page gets a companion, app.shop_ticket_seat, beside app.shop_ticket rather than a seventh column Postgres cannot add in place without breaking the next run of W529, D-1189. ★★ THE STANDING TICKET IS THE CONTROL IN EVERY CHECK, and two of the fifteen sabotages hand that standing ticket a seat, because a control asserting nothing is there passes equally well when the feature was never built. ★ A REFUNDED SEAT GOES BACK ON SALE BY ITSELF: a refund leaves the partial index that makes a seat exclusive, so no code frees it. That was true before this slice and nothing proved it, and a check now buys, refunds and reads the section twice. ★ The suite's own finally called process.exit before a thrown fixture could surface, so a suite that built nothing printed 0 passed, 0 failed and read as green. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W542The two feescompleteD-1184 AND D-1191: TWO FEES ON TOP OF THE TICKET PRICE, BELONGING TO DIFFERENT PEOPLE. ★★★ A FEE IS FROZEN ON THE ORDER, computed at the hold and written down, so raising a fee tomorrow cannot rewrite what somebody paid yesterday and a refund returns a number that was actually charged, D-1192. ★★ THE ORGANISER CANNOT CHANGE EVENT.CLINIC'S FEE, AND NOTHING WAS WRITTEN TO MAKE THAT TRUE: W525 grants the workspace COLUMN BY COLUMN, so a column never granted is refused by Postgres itself. The suite asserts the grant, and its sabotage is the grant, given away and taken back. ★ A REFUND RETURNS THE TICKETS AND KEEPS THE FEES, so Stripe is asked for an amount rather than the whole payment, and the buyer's confirmation says the amount that actually comes back, which it did not before. ★ One sabotage is invisible to the acceptance suite by construction, because its refunds go through the test provider which never sees an amount, so what Stripe is asked for is measured on the unit suite instead. *Evidence: 1 migration, acceptance suite, 1 design doc.*
Phase 13 - The showcalling ecosystem
W600The show, the department and the cuecompleteE-450 AND E-451, ANSWERING Q-077, WHICH HAD BEEN OPEN SINCE 2026-07-27 AND CALLED THE RUN OF SHOW THE HIGHEST-LEVERAGE EMPTY BOX IN THE PRODUCT. ★★★ FOUR OF THE FIVE LEVELS ALREADY EXISTED, SO FOUR ARE NOT REBUILT: event, agenda, agenda_segment and agenda_item carry EVENT, SHOW, SEGMENT and MOMENT, and agenda_item already holds E-271's Flex, Anchor and Sticky timing. The cue is the one missing level, D-1193. ★★ app.event_moment IS A SHIPPED REFERENCE TABLE AND NOT A SHOW MOMENT, so a new table named after it would have collided with production. ★ THREE INVARIANTS ARE THE DATABASE'S: a moment from another running order EC520, a cue waiting for itself EC521, and a circle of dependencies EC522, which matters because W607's readiness engine walks those edges. ★ W453 caught a single-column key to the moment that would have let a cue point at another workspace's, and the repair is explicit because create table if not exists never changes an existing table's keys. ★ Of 18 sabotages the most valuable opened the surface to every external caller: the first version of that check asserted a 403, and a surface open to everyone also answers 403. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W601The Showbook StudiocompleteD-078 IS OWNER-CONFIRMED AND ASKS FOR A MATRIX, NOT A LIST: moments down, departments across, cues in the cells, and each user selecting their own columns. E-450 section F says the same in another vocabulary, D-1196. ★★★ THE RUNNING ORDER IS READ AND NEVER COPIED, so a rename in the Agenda arrives here, which is the check every other check would pass without. ★★ THE COLUMN CHOICE IS NOT A PREFERENCE VALUE, AND THE DATABASE IS WHAT SAID SO: user_preference's (key, value) is a foreign key into an enumerated set, and a selection over a workspace's own extensible departments is not one. No rows means NEVER CHOSE, not chose nothing. ★ THE WARNING ONLY STRUCTURE CAN SEE: a cue that waits for another cue happening AFTER it. Both are valid, both may be approved, and the show still cannot run. ★ The choice is guarded twice, by the query and by the policy, so no behavioural check can see a single break and one check's subject is the policy itself. *Evidence: 1 migration, 1 design doc.*
W602The showbook as a documentcompleteE-451 SECTION 9'S FOURTEEN FIELDS ARE A LAYOUT OVER THE CUE MODEL, NOT A SECOND MODEL, because seven of the fourteen are DEPARTMENTS W600 already stores, D-1197. ★★★ ELAPSED TIME IS COMPUTED AND NEVER STORED: a stored number would be wrong the moment somebody changed a duration in the Agenda. ★★ PRINTED AS A REAL PDF WITH HEADLESS CHROME, WHICH IS HOW THIS STYLESHEET'S OWN DEFECTS WERE FOUND TWICE BEFORE, and it found a third: the Studio headed a column LIGHTING and the document headed the same column VILÁGÍTÁS, one department with two names. The stored name wins in both, and the seed now writes it in the workspace's own language. ★ A DEPARTMENT COLUMN SAYS WHAT ITS CUE IS, NOT WHAT THE CALLER SAYS: printing the number and the GO words gave LX 1 MEHET LX 1, because a GO line contains its own cue number. ★ A sabotage that wired the change marker to always true passed its own check, which only ever looked at a moment it had just edited. *Evidence: 1 design doc.*
W603Versions and freezingcompleteTHE OWNER'S D-1195 MADE REAL, AND E-450'S ACCEPTANCE CRITERION 5. ★★★ A FROZEN VERSION HOLDS THE CUES, NOT A POINTER TO THEM: a version that pointed at the live rows would let a planning edit change what a showcaller is looking at MID-SHOW, which is the one thing freezing exists to prevent, D-1198. ★★ THE DATABASE REFUSES INSERT, UPDATE AND DELETE ON A FROZEN SHOW'S CUES, and the only escape is a definer that takes the caller's right as an argument, W537's EC516 shape. A self-assertion refuses any second function that opens it. ★ FREEZING IS THE PRODUCER'S AND CHANGING A FROZEN SHOW IS THE SHOWCALLER'S, so show.call is NARROWER than show.edit and a self-assertion refuses it reaching more roles: authoring a cue and changing one mid-show are different jobs. ★ W602'S TWO PLACEHOLDERS NOW MEAN SOMETHING: a draft prints no version rather than calling itself version 1, and a moment is CHANGED when a cue differs from what the version froze. Three W602 checks encoded the old behaviour and were rewritten to the new contract rather than deleted. ★ A frozen show can still be DELETED, which the suite's own teardown found first. *Evidence: 1 migration, 1 design doc.*
W604Rehearsal ControlcompleteE-450'S ACCEPTANCE CRITERION 4, AND THE WORD THAT MATTERS IN IT IS TRANSITIVE, D-1199. ★★★ IF CAMERA WAITS ON AUDIO AND AUDIO WAITS ON LIGHTING, A CHANGE TO LIGHTING REACHES CAMERA, two hops away, and the answer names the chain. A trace that joined the dependency table once would report audio, say camera was fine, and camera would never be told: E-451 scenario S06 exactly. ★★ THE TRACE IS DERIVED ON EVERY READ AND NOTHING STORES IT, because a list written down when a change was captured goes stale the moment somebody adds a dependency, and a self-assertion refuses any column that would hold one. A sabotage proves it: an edge added AFTER a change was recorded changes that change's answer. ★★ IT IS THE ONLY FUNCTION IN THIS FAMILY THAT IS NOT A DEFINER, because a definer bypasses forced row security and this one only reads. ★ A CHANGE'S STATUS IS NOT ITS CUE'S test_status: a cue can be TESTED and then CHANGED, everybody believes it works because it worked before the change, and one field cannot hold that. ★ A TEST OBJECTIVE IS NOT AN app.task, AND THE DATABASE IS WHAT SAID SO: task.status is app.work_order_status and carries no failed, so a task cannot tell not-yet-tried from tried-and-broken. ★ THE TRACE OVER-REPORTS ON PURPOSE, archived moments included, because the two errors are not symmetric. ★★ THE FILE WAS RE-RUNNABLE AND NOT FRESH-APPLICABLE, AND PRODUCTION FOUND IT: the key-repair block was written beside the table it reads about and ran before two of the tables it repairs existed, so it applied cleanly TWICE on a database that already had them and failed on the first fresh one. ★ W453 FOUND FOUR KEYS THAT DID NOT NAME THE WORKSPACE, so a rehearsal attendee could point at another workspace's rehearsal: row security hides that on a READ and this is a WRITE rule. Two shipped tables gained the unique (id, tenant_id) anchor a composite key needs, which can never fail because id is already their primary key. ★ The route-reachability guard found a half-built feature for the second slice running: update and remove were served with no control. *Evidence: 1 migration, 1 design doc.*
W605A high-impact change cannot close silentlycompleteE-450'S ACCEPTANCE CRITERION 5, AND THE WORD THAT DOES THE WORK IS SILENTLY, D-1200. ★★★ D-1195 IS UNTOUCHED AND A CHECK ASSERTS IT: on a frozen show the showcaller's live change still rewrites the cue, because a gate standing between a failure and its recovery would be a worse defect than the one it prevents. What is refused is CLOSING a high-impact change while nobody approved it, EC528, and while a department it reaches has not been told, EC529. ★★★ WHO MUST BE TOLD IS RE-DERIVED FROM W604'S TRACE AT THE MOMENT OF CLOSING: acknowledge everybody, close it, reopen it, add one dependency, and the newly reached department puts it back in the way. A stored list would have closed on yesterday's answer. ★★ NO SECOND APPROVAL ENGINE: rehearsal_change joins app.approval's subject vocabulary and the four generic routes, the three modes, the decision log and the audit trail arrive with it. The validator's body was SPLICED from the live definition, W539's lesson, and a self-assertion names all twelve subject types. ★★ approval_closes_subject IS NOT TOUCHED: the gate reads the approval and the approval never writes the change, because two directions of coupling is how a rejected round silently reopens something it never owned. ★ BOTH BOUNDS HAVE CONTROLS: the same change on an unfrozen show closes freely, and so does a low-impact one on a frozen show, because a gate that fires on everything is noise and noise gets switched off. ★★★ AND THEN SWEPT, W605.1, ON THE OWNER'S INSTRUCTION, BECAUSE ASSERTION 10'S DEFECT WAS THE WHOLE SPINE'S: _ IS A WILDCARD IN LIKE, SO A TEST FOR AN IDENTIFIER PASSED ON PROSE THAT LOOKED LIKE IT. 279 tests rewritten across 84 migrations, the ledger's probes and 8 suites, a substring test to position() and a real pattern escaped, and W605's own probe was one of them. Every rewrite was forced red by removing the identifier and leaving a look-alike, and the OLD form stayed green in 146 assertions, 57 probes and 4 suite checks. ★★ Four names also lived inside longer ones, budget_line inside budget_line_owner_type_allowed and three inside purchase_order_line_total() and invoice_line_total(), and now take word boundaries. ★★ AND A COMMENT CAN SATISFY ONE TOO, which the W610 session raised: five function-body checks stayed green with the code gone and a comment naming the identifier, and now match comment-stripped text. ★ A STANDING GUARD NOW REFUSES A NEW ONE, verify-like-underscore.py in both repositories' pre-commit hooks and repo-guards workflows. D-1206, Q-577. *Evidence: 1 migration, 2 design docs.*
W606Live Show ModecompleteTHE OWNER'S D-1195 MADE INTO AN ASYMMETRY THE DATABASE ENFORCES, D-1201. ★★★ ANY DEPARTMENT MAY HOLD AND ONLY THE SHOWCALLER RESUMES: app.show_hold_call is the ONE function in this family that takes no right at all, and a self-assertion refuses it ever growing one, because the person who sees the problem is hardly ever the showcaller. A viewer holds the show in the suite and then cannot release it, EC531. ★★★ A HELD SHOW CALLS NOTHING, EC532, except a cancel, because taking something off the stack during a hold is what recovery looks like. ★★ NOTHING WRITES A CUE'S STATUS: W603's EC525 refuses that anyway, so the live state lives beside the frozen version in an append-only call log, and running the same show again needs no reset. ★★ THE SCREEN READS THE LIVE CUES, WHICH IS SAFE PRECISELY BECAUSE THE SHOW IS FROZEN -- they can change only through the showcaller's own path, so an operator sees a correction at once. Snapshotting the version would have cut the showcaller off from the crew mid-show. ★ NO show_run TABLE: show.status already carried live and complete. ★ THE LOG IS NOT THE SHIPPED forbid_mutation, AND THE SUITE'S OWN TEARDOWN SAID SO: a blanket refusal would leave every show that had ever been called undeletable for ever, so the rule is W603's -- refuse the edit, allow the cascade -- and check 10m asserts it. ★ Live Show Mode is its OWN SURFACE and not a fourth tab, because a tab sits one mis-tap from the planning matrix at the moment a caller must not reach it, and it needed a new icon: every mark in the set was already held. *Evidence: 1 migration, 1 design doc.*
W607The readiness enginecompleteSECTION L IS ONE SENTENCE AND IT DECIDED EVERYTHING, D-1202: *do not create a single opaque score that hides critical exceptions, and a critical unresolved item must remain visible even if the aggregate status looks healthy.* ★★★ THE ENGINE RETURNS NO AGGREGATE AT ALL, one row per category with its own state and its own EVIDENCE, and three self-assertions refuse a score arriving as a column, as a second function or as a stored table. The one word a screen shows IS the worst category's own state, so it cannot read healthy while one is critical. ★★★ A CATEGORY IT CANNOT MEASURE SAYS SO: contingency reads nothing, because E-451's emergency checklist is a DOCUMENT and incidents arrive with W608, so it answers not_measured and names why. Scoring it READY would be the opaque aggregate hiding a gap inside its own output. ★★ IT READS THREE STATUSES THIS PROGRAMME KEPT APART, a cue's test_status, a change's and an objective's, because one of them alone cannot tell a cue nobody tested from a cue tested BEFORE a change nobody rehearsed. ★★ THE DEPENDENCY CATEGORY COMPARES A TUPLE AND NOT A row_number(): cues made through the route share a sort_order, and a tie made the verdict arbitrary between runs. ★ Readiness is a PLANNING tab and never appears in Live Show Mode. *Evidence: 1 migration, 1 design doc.*
W608Post-show review and incidentscompleteE-450 SECTION S IN ITS OWN WORDS, D-1203: *link post-show actions to the EVENT'S TASK SYSTEM*. ★★★ AN ACTION IS AN app.task AND THIS SLICE OWNS NO TO-DO LIST, pointed at its lesson through W19's own target columns, and a self-assertion refuses any table here that looks like a second one. Removing a lesson does NOT remove its task, because somebody may still have to do it. ★★★ JOINING THE TASK VOCABULARY TOOK THREE PLACES AND THE DATABASE FOUND IT: task_target_type_allowed refused the insert and the route answered 503, and the function, the Worker's permission map and the Worker's label switch all had to learn the word. W605's lesson, two slices later. ★★ AN INCIDENT IS RAISED BY ANYBODY WHO CAN SEE THE SHOW AND RESOLVED BY THE SHOWCALLER, which is D-1195's asymmetry applied to what goes wrong rather than to what stops. ★★ W607'S contingency IS NARROWED AND NOT CLOSED: it reads unresolved incidents now, and with nothing open it still answers not_measured, because nothing being wrong is not the same as being prepared. ★ A sabotage found the file could not REPAIR a damaged table, which is a different property from being re-runnable on an intact one. ★★★ AND THEN HARDENED, W608.1, ON THE OWNER'S INSTRUCTION. Measured on a gala-sized show first: 300 cues, 240 edges, show_change_impact 4 ms and show_readiness 15 ms, so performance is not a problem and nothing changed for speed. Three defects the measuring found. THE SHOWBOOK AND THE READINESS ENGINE DISAGREED ABOUT THE SAME QUESTION: the change marker compares SEVEN cue fields and the version category compared TWO, so changing only a cue's standby text printed CHANGED on one surface and ready on the other. They live in different runtimes, so check 13a IS the shared definition. A SHOW NOBODY STARTED COULD BE CALLED, EC536, allowing rehearsal because a dress rehearsal is called. A FINISHED SHOW COULD BE STARTED AGAIN, EC537. And a raise named one code while raising another, which asRefusal would have reported as the wrong rule: a new assertion now checks all 22 refusals in the family. D-1204. *Evidence: 2 migrations, 2 design docs.*
W609The Academy spinecompleteTHE OWNER'S D-1194 MADE STRUCTURAL, D-1205. ★★★ app.academy_progress HAS NO tenant_id, the one place this whole programme crosses the tenancy boundary and deliberately so: a person trains once and carries the record across every workspace they work for, so a freelance showcaller with three agencies does not train three times. A self-assertion shouts if anybody adds the column back, because every other table here is tenant scoped and the next hand will read the omission as a bug. Check 14d drives it: the same learner reads their progress from a DIFFERENT workspace. ★★ THE CURRICULUM IS THE PLATFORM'S AND A WORKSPACE MAY ADD ITS OWN: one policy READS a null tenant and WRITES only the caller's, so a course is a product rather than a template a customer rewrites, and it needs no trigger at all. ★★ THE CURRICULUM IS SEED DATA, NOT PROSE: four levels, fifteen modules, ten principles, 73 outcomes and 46 practicals, PARSED from the programme document rather than retyped, every count asserted against E-451. ★★ Freelancer IS CLASSED EXTERNAL AND IS D-1194'S OWN EXAMPLE OF A LEARNER, so the grant is role_class = internal OR that one name, and a control asserts no other external template has it. A rule from role_class alone would have excluded the only person the decision is about. ★ ON CONFLICT DO NOTHING does not stop a BEFORE INSERT trigger firing, found by sabotage, so the seed uses where not exists. ★ Whether the Academy earns an eighth rail destination is Q-576, not a slice's to decide. *Evidence: 1 migration, 1 design doc.*
W610Knowledge checks and exercisescompleteTHE PROGRAMME NAMED app.form AND IT DOES NOT FIT, D-1211, measured rather than assumed. All five form tables are tenant_id NOT NULL and a learner's attempt has NO tenant under D-1194; form_response is UNIQUE PER RESPONDENT and a knowledge check is RETAKEN; and a form has no correct answer, no score and no threshold. Any ONE is fatal. Part 2 named it on the SAME DAY D-1194 made a learner portable, and a self-assertion now refuses the Academy reaching into the form engine at all. ★★★ **E-451 SECTION 12 IS A THRESHOLD *AND* AN OVERRIDE, AND THE OVERRIDE WINS: check 15c scores EXACTLY the same 75% as 15b and fails, because the one question it got wrong was marked critical. ★★★ THE RIGHT ANSWER IS NEVER SERVED BEFORE GRADING, AND IS WITHHELD AGAIN DURING A RETAKE — the second half was a real defect 15d found, since E-451 allows retakes and a learner reading the answers between them would score 100 every time. ★★ D-1208, THE OWNER'S: a workspace sees the LEVELS a learner holds and never the scores, so no policy on the three learner tables may name a tenant, asserted at build time and driven by 15j. ★ NO QUESTIONS ARE SEEDED: E-451 contains no question bank, and inventing one would put words in the owner's syllabus, so the engine ships empty and E-451's 46 practical exercises become loggable instead. ★ A CASCADE IS NOT AN EDIT — the append-only trigger's first draft made a question un-withdrawable and a learner un-deletable. ★ THE SEVENTH SOURCE-TEXT GUARD DEFEATED BY TEXT**: raise exception contains the word except, so counting the substring found five in a function with two EXCEPT clauses. *Evidence: 1 migration, 1 design doc.*
W611The competency model and the passportcompleteTHE LINE BETWEEN TWO TABLES IS THE OWNER'S D-1208, D-1212, and it is the whole slice. ★★★ academy_assessment AND ITS PER-AREA SCORES ARE THE WORKING RECORD and no workspace may ever read them. ★★★ academy_certification IS THE PASSPORT and a workspace reads it for its own ACTIVE members, because a level is the fact a certificate publishes. A policy cannot hide a column, so the split IS the enforcement: putting the score on the certification row would have handed every agency every score the moment the passport became readable, and no policy could have taken it back. Driven as behaviour, not asserted: a colleague reads 1 level, 0 assessments, 0 area scores, 0 evidence. ★★★ ONLY EVENT.CLINIC AWARDS, D-1207, and there is deliberately NO PERMISSION CODE for it: a code is a thing a workspace can be granted, so the rule is app.is_platform_admin(), EC544, with the certification's WITH CHECK naming the platform and nothing else. ★★ E-451 SECTION 12 IS A WEIGHTED RUBRIC, seven areas summing to 100, parsed from the programme, with the sum asserted: a rubric that does not total 100 produces a score that is not a percentage and nothing downstream would notice. ★★ THE CRITICAL-FAILURE OVERRIDE IS A CHECK CONSTRAINT HERE, not a line of function, because the verdict is STORED and W610 proved a grader can be edited: the same weighted 90% certifies a clean learner and certifies nobody with a critical failure. ★ A FAILED RETAKE NEVER STRIPS A LEVEL ALREADY HELD. ★ RECERTIFICATION: expires_at IS NULLABLE AND NO PERIOD IS INVENTED, Q-579. ★ A SEED THAT ONLY INSERTS CANNOT REPAIR AN EDITED CURRICULUM, found by sabotage. *Evidence: 1 migration, 1 design doc.*
W612The Scenario LabcompleteE-451'S TWELVE SCENARIOS, S01 TO S12, PARSED FROM THE PROGRAMME, each with its injection and its expected competency in E-451's own words. ★★★ E-450 SECTION J'S LAST SENTENCE IS WHY THIS SLICE DEPENDS ON W611: *evidence captured against competencies*. A debriefed run writes app.academy_evidence, and the competency is CHOSEN at debrief rather than derived, because E-451's expected-competency column is a SENTENCE about behaviour and not one of the eleven names, so a mapping would be a guess wearing a foreign key. ★★★ AND THAT JOIN FOUND A WRONG RULE IN W611, ONE DAY OLD: the evidence table was platform-write-only, so a learner ending their own run got a 503. D-1213 widens it as narrowly as it can be -- their own subject, scenario_run only, an ENDED run only -- and check 17j drives all four edges including the one that matters, evidence about another person. ★★ NO TIMING WINDOW IS INVENTED: E-450 asks for injection at *unpredictable but controlled* points, E-451 gives a time for one of the twelve and it is relative to a cue the table cannot see, so both columns are null and the trainer fires it. Asserted over the SEED'S SOURCE and not the data, so a trainer setting one is not forbidden by the check. ★★ NO BRANCH TABLE WAS INVENTED, asserted: E-450 names branching decisions and E-451 contains not one branch, so the shape would have been guessed curriculum. ★★ A CHECK CONSTRAINT MUST NOT BE HALF-MADE OF A FOREIGN KEY THAT SOMEBODY ELSE'S DELETE CAN NULL: the override constraint first coupled its note to trainer_user_id, which carries on delete set null, so deleting a trainer's account would have broken a row nobody touched. overridden_at is the marker now. ★ THE REPLAY IS THE DECISION LIST, in order of at_seconds, and there is no second shape for it. *Evidence: 1 migration, 1 design doc.*
W613The Organisation Academycomplete"ASSIGN TRAINING TO TEAMS" AND app.team IS NOT THE TEAM, D-1214, measured before anything was designed. app.team.event_id is NOT NULL so a team DIES WITH ITS EVENT, and app.team_membership joins event_participant_id rather than an account -- so a requirement hung off it would expire the day the event did and could not reach the accounts that hold the training. The durable grouping a workspace has is the ROLE. ★★★ AND THIS IS THE ONLY ACADEMY TABLE THAT IS TENANT SCOPED, deliberately: a curriculum is the platform's and a learner is portable, but a REQUIREMENT is one workspace's own rule about its own people and must not follow a freelancer to the next agency. Check 18f drives exactly that, with the control that the workspace which made the rule still sees it. ★★★ E-450 SECTION L, APPLIED TO TRAINING AND ASSERTED: no count, no sum, no avg, no group by, no percentage, because a dashboard reading "78% trained" hides the one showcaller whose certification lapsed. One row per person per requirement with the reason beside it, and four states distinguished: missing, expired, revoked and ready -- a withdrawn certification and one never earned are different facts. ★★★ IT JOINS THE PASSPORT AND NOTHING ELSE, D-1208, asserted over the function's comment-stripped source with word boundaries because the header names every forbidden table in prose. ★ EC553 refuses a level from a course this workspace cannot see; EC554 refuses a role nobody here holds, because a rule with no holders can never be met or broken and reads as compliance. *Evidence: 1 migration, 1 design doc.*
W614The Knowledge AssistantcompleteIT IS A READ, AND THAT IS THE WHOLE OF E-450 SECTION K, D-1215. Section K says it twice, *AI must assist, not silently control the show* and *never silently alter approved live cues*. app.show_advice is stable, has no INSERT, UPDATE or DELETE anywhere in it, and is served by a GET with no POST beside it, so it CANNOT alter an approved live cue rather than merely promising not to -- a property instead of a promise, and checkable. Check 19b drives all three. ★★★ AND IT IS NOT A LANGUAGE MODEL: every finding returns the rule it applied AND the E-451 line it quotes, joined live from the twelve-phrase phrasebook of section 8, so a showcaller can disagree with the document rather than with an opinion. Five rules, each traceable to a line: a standby must say what is being stood by, a GO releases something previously prepared, a standby nobody releases leaves a department waiting, Next is not a substitute for standby in E-451's own words, and a cue that waits on one happening later never fires. ★★ NO LIST OF AMBIGUOUS WORDS IS INVENTED: M05 says avoid ambiguous commands and E-451 gives no list, so every rule is a STRUCTURAL test and an assertion refuses a hardcoded list appearing. ★★ IT IS NOT A SECOND READINESS ENGINE and an assertion refuses it reading test_status, because W607 owns that question and two places answering it would eventually answer differently. ★★★ A GLOSSARY NAME IS DATA, NEVER A PATTERN: dozens of the 2866 term names carry regex metacharacters and one unescaped name took the WHOLE advice read down with invalid regular expression. *Evidence: 1 migration, 1 design doc.*
W615AnalyticscompleteFOUR OF SECTION O'S TEN REPORTS CANNOT BE SERVED TO A WORKSPACE, D-1216. Section O names ten in one sentence and was written before the owner answered Q-578: read against D-1208 they split cleanly, because training completion, competency coverage, assessment performance and scenario failure patterns each read a table a workspace may not see. ★★★ SO THERE ARE TWO FUNCTIONS, NOT ONE WITH A FLAG. app.show_analytics() is the workspace's six including certification STATUS, which counts levels and nothing else; app.academy_analytics() is event.clinic's four and refuses everybody else with EC555 as its first act. A single function branching on a permission would be one edit away from serving a score to an agency. ★★★ E-450 SECTION L GOVERNS THE SHAPE: a named report, a named subject, the subject's ID so a reader can open what was counted, one number and a sentence. No composite, no index, no health score, asserted on the return shape and forced red by a sabotage that adds a total to the ROUTE, which is where one would most naturally be added for a screen. ★★ NO TABLE STORES THE NUMBERS, because analytics with a table of its own is a second truth that ages. ★★ Competency coverage names how much evidence is SELF-RECORDED, which is D-1213's cost made visible rather than totalled away. ★ One rule has no behavioural check and the record says so. *Evidence: 1 migration, 1 design doc.*
W616The show-control emit layercompleteA WORKER CANNOT SEND AN OSC PACKET, MEASURED AGAINST CLOUDFLARE'S OWN DOCUMENTATION, D-1217. The runtime offers connect() for TCP and has NO UDP API at all, and its troubleshooting page names "Cloudflare IPs, localhost, and private network IPs" as disallowed destinations. OSC is UDP and a lighting desk is on the venue LAN. The Workers VPC path over a Tunnel is still plaintext TCP only. ★★★ SO event.clinic SHIPS THE QUEUE AND THE PROTOCOL, NOT THE PACKET: osc, companion and resolume are claimed by a BRIDGE on the venue network. ★★★ AND THE FIRST DRAFT BUILT A SECOND HTTP LANE BEFORE MEASURING -- app.webhook_subscription, app.webhook_delivery and app.emit_webhook already sign, retry and log, and runWebhookDeliveries already runs on the minute cron -- so HTTP is a subscription to a NEW EVENT CODE, cue.called, joined to the eleven already in the vocabulary. W608's lesson eight slices later. ★★ A GO NEVER WAITS ON A NETWORK: the call enqueues, so a failed delivery cannot roll back the fact that a cue was called. ★★ THE QUEUE IS THE EVIDENCE and a failure must say why. ★★ A CLAIM IS A LEASE with skip locked and a re-claimable timeout, so a dead bridge does not take the queue with it. ★★ EC556 REFUSES A URL AND NAMES THE WEBHOOK ENGINE, because a message that only says no leaves somebody to invent a second lane -- and its ORDER is the point, since the general shape check would have answered truthfully and uselessly first. ★★★ A WRONG CONSTRAINT WEARING THE RIGHT NAME IS NOT A REPAIRED ONE, W608's defect in a third form, found by this slice's own campaign. ★ NO BRIDGE IS SHIPPED AND NO PACKET HAS REACHED HARDWARE, Q-580. *Evidence: 1 migration, 1 design doc.*
W617The presenter display and its coloured signalscompleteA DISPLAY IS A MACHINE WITH NO ACCOUNT, SO IT CARRIES ITS OWN CREDENTIAL, D-1218, W347's crew link a third time: a 25-character code stored only as a SHA-256 hash, a CHECK that refuses anything else in that column, a compulsory expiry bounded to thirty days, revocation that EC560 makes final, and one show named for life. ★★★ IT DOES NOT READ THROUGH THE PUBLIC LANE, AND THAT WAS MEASURED: DB_PUBLIC is a Hyperdrive configuration with its query cache on, 60 seconds by default per Cloudflare's own documentation, and a presenter told STOP a minute late has not been told. So the code is resolved on the elevated path touching app.show_display alone and everything else is read as the issuing workspace under row security on the cache-disabled binding. ★★★ ONE DEFINITION OF THE MOMENT ON STAGE: app.agenda_current_item, read by Live Show Mode and the display alike, walked through every state by 22c. ★★ A SIGNAL IS TIMEKEEPING, agenda.run_live, EC558, one at a time, and never rewritten, EC559. ★★ THE DISPLAY COUNTS ON THE SERVER'S CLOCK, keeps a wake lock, and rounds time left UP so 0:00 means stop. ★ TWO LAYOUT DEFECTS WERE FOUND BY LOOKING, not by any check, and fixed. *Evidence: 2 migrations, 2 design docs.*
W618The delay engine and the live programmecompleteWHAT THE PUBLIC MAY READ OF A PROGRAMME WAS ALREADY DECIDED, D-1219: the newest approved snapshot of each moment, W258 and W259's rule for the October 23 website handover, now shared as approvedItemSnapshots rather than copied. The live programme adds TIME, which no approver can sign off in advance. ★★★ MEASURED FIRST: on production no item is published and none has an approved snapshot, so a page filtering on published would have been empty for a reason nobody chose, and the Agenda screen now says how many moments will actually show before anything is published. Q-581 asks whether accepted should count. ★★★ THE DELAY ENGINE IS ARITHMETIC IN THE API, api/src/agenda/projection.ts, tested case by case at a fixed clock: flex moves with the stage, an ANCHOR DOES NOT MOVE and reports how far the run reaches into it, sticky follows its anchor, and nothing not started is projected into the past. ★★★ THE PROJECTION RUNS BEFORE THE LIST IS CUT, because a technical changeover takes stage time. ★★ ONE LIVE LINK PER EVENT, W617's pattern, EC561, cache-disabled, writing nothing. ★ TIMES IN THE EVENT'S OWN ZONE. *Evidence: 1 migration, 1 design doc.*
W619The telepromptercompleteA TELEPROMPTER IS W617'S DISPLAY WITH A SCRIPT ON IT, D-1220: a display link gains a KIND, fixed for life by EC560, and a prompter link's read carries the script of the moment on stage through app.agenda_current_item, while a presenter link never carries one. ★★★ A SCRIPT IS THE PRESENTER'S WORDS, NOT THE CREW'S INSTRUCTIONS, so it is outside W603's frozen version, but W603's reason still holds: once frozen it is the showcaller's alone, EC563, decided by the database at the moment of the write because only the database knows the freeze then. ★★ THE SCROLL IS LOCAL, driven by the prompter machine's own keys, because a round trip between the key and the text is a stutter the presenter reads. A new moment returns to the top and waits, a correction replaces the words in place. ★★ THE STUDIO SAYS WHETHER A SCRIPT FITS ITS MOMENT, unrounded. ★ A CLICKER RIDES THE SPEED and a key with no code still works, both found by using it. *Evidence: 1 migration, 1 design doc.*
W620Audience questionscompleteA GUEST ASKS THROUGH THEIR OWN TICKET, D-1221, so Q-501's anonymous public write stays the owner's and untouched: app.ask_guest_question derives the event and the guest from the code, as W294's vote does, needs the event's question box open, refuses a revoked ticket, and holds a ticket to three waiting questions under a lock on its row. ★★★ IT TAKES THE CACHE-DISABLED DOOR, withGuestDoor, two functions and nothing else, because a write made by a SELECT is cacheable by protocol. ★★★ A MODERATOR NEVER SEES WHO ASKED, and EC564 keeps a question's words and moves it forward only. ★★ THE STAGE ALREADY HAD A WAY IN, a W617 signal kept to the live timekeeper by EC558, so a question is not given a second one. The privacy wording is drafted for the owner, not published. *Evidence: 1 migration, 1 design doc.*
W621The vote and the cachecompleteMEASURED ON PRODUCTION, THE VOTE IS CORRECT AS BUILT, D-1222: with a guest, a ticket and a two-option poll planted in the production workspace, a ticket page reread one second after a rename showed the new name even after three warm-up reads, and five votes alternating a second apart each reached the database. So W294's vote stays on the public lane, and D-1221's staleness argument, a hypothesis read from Cloudflare's documentation, is amended rather than acted on. ★★ THE GUEST'S DOOR STAYS FOR ITS GUARANTEE, a fresh read whatever Hyperdrive's parser decides, and its guard now has unit tests that name what it must refuse. The cause of the reads not being cached, the transaction or the functions' volatility, is not isolated and is named as such. *Evidence: 1 design doc.*
W622The showcalling refusals speak HungariancompleteONE SENTENCE ANSWERS EVERY RAISE OF A CODE, D-1223, W462's rule applied and not relaxed: of the thirty-six refusals W600 to W616 raise, the twenty-four that one function raises and that mean one thing are catalogued with their Hungarian, and the twelve that two or three functions raise, or that mean several things, stay in English with their code until W623 gives each one raiser. ★★ THE SENTENCES DROP WHAT THE DATABASE FILLS IN, because asRefusal sends no parameters, and the screens already show the approval state, the departments still to be told and the show's state. Measured in a browser: the Studio's freeze refusal reads in Hungarian with no code. *Evidence: 1 design doc.*
W623Each showcalling refusal means one thingcompleteEVERY REFUSAL FROM EC520 TO EC568 IS RAISED BY ONE FUNCTION AND MEANS ONE THING, D-1224: ten one-raiser functions in W617.1's shape for the codes several functions raised, and four new codes, EC565 to EC568, for the second meanings that had shared one, so all sixteen join the catalogue in Hungarian. ★★ EACH CODE KEEPS THE MEANING ITS OWN SLICE GAVE IT. ★★ EIGHTEEN WHOLE FUNCTIONS REWRITTEN SAFELY: built by script from the bodies in the database with every edit asserted to land once, and production's eighteen compared with them before it was applied. ★ AN ACADEMY REFUSAL IS BROUGHT INTO VIEW, found by using it. *Evidence: 1 migration, 1 design doc.*
W624A refusal is seen where it appearscompleteA NOTICE AT THE TOP OF A LONG PAGE IS BROUGHT INTO VIEW WHEN A MESSAGE APPEARS, D-1225. Measured before the fix with the page scrolled to the button: the Academy's refusal 1,139 pixels above the viewport, the Studio's 317, and Live Show Mode's GO refusal 341 while the caller looked at the button. useRevealed is a hook each screen opts into, not a change to the shared notice, because a notice that scrolled on every render would jump the page while somebody types. ★ W622's own browser check had found its notice and never asked whether it was on screen. *Evidence: 1 design doc.*
W625Every screen has its framecompleteTHE DESIGN GUARD HAD A HOLE, AND THREE SCREENS HAD NO FRAME, D-1233: its pattern read page-card as a frame, so the showcalling Studio, Live Show Mode and the Academy passed while sitting flush against the chrome, measured at 0 pixels. They carry one frame above every branch now, the public programme uses the system's portal-page, and the guard takes the token only. ★★ THE GUARD RUNS IN npm test, after its own notes recorded three earlier times it shipped red unread. Found by the first push to GitHub in ten days, and GitHub's build-check is green again. *Evidence: 1 design doc.*
W626The spine builds from empty againcompleteGITHUB'S FROM-EMPTY BUILD HAD BEEN RED SINCE 2026-09-02, D-1235, so twenty days of slices went in with nothing rebuilding the chain. It failed before the build even started: eight files were in no manifest list, seven from the 2026-09-17 audit and one, W407, a real migration on production that nothing loaded. ★★★ THREE THINGS PRODUCTION HOLDS THAT NO FILE EVER MADE are captured from production byte for byte: app.feature_category_root, which the Worker has called since 2026-07-29, the unique index event_ticket_id_tenant that W529's key references, and security_invoker on four views that W507 requires of every view. A fourth, production's pre-W522 app.artabla, is captured so a build reaches W522 with the body W522 was written against. ★★ TWENTY-ONE FILES FAILED THE REPLAY, IN FOUR FAMILIES: an assertion pinning a count of a schema later slices legitimately grow (W29, W152, W234, W229, W347, W31.9, W209, W502, each now naming what its own file made); a fixture naming any workspace's row where W451 and W453 now refuse one (W258, W27.2); a probe deleting scratch rows W460 made permanent or writing the NO RESULT sentinel W281 forbade (W27, W27.1, and the four W58 imports, regenerated from their generator with the other nineteen outputs byte-identical); and W475's assertion, which required a function and a trigger set that only agree outside a replay. ★★★ TWO NEGATIVE CONTROLS HAD NEVER RUN IN CI AND COULD NOT HAVE: W319's read its fixture as an owner the job does not use and refused every time, and W372's held all seven checks while reading as a file that asserted nothing, because the job counts notices beginning ok:. AND W319'S CONTROL COULD NOT REPORT ITS OWN FAILURES, eight messages built as array concatenation raising 'Array value must start with {' instead of naming the broken check, which is how its seven sabotages of 2026-09-07 each went red. ★★★ W450'S FORWARD-LOOKING CLASS GUARD FOUND THE ONE REAL GAP: W529.4's app.ticket_order_mail held an event and a workspace with no key pairing them, and w626-2 adds it, proved on local to accept a correct row and refuse a cross-workspace one. *Evidence: 3 migrations, 1 design doc. ON PRODUCTION: production read first, the key absent and the function, the price table and the four views already matching, so two files had to be no-ops and were, both function bodies byte-identical by md5 before and after; all three applied twice with every self-assertion passing; the ledger runs 521 probes and reports every migration present on prod and on local. The same control could not run on production, which holds no ticket order at all, and a skip is not a pass. D-1234, D-1235.* ★★ AND THE CHECK ITSELF STILL CANNOT START: every GitHub run in both repositories now ends in startup_failure in seconds, including the app commit that was green at 15:16 the same day, with the workflows unchanged and GitHub reporting all systems operational. That is an account-level block and it is the owner's to clear, written up as OWNER-ACTION-04. *Evidence: 3 migrations, 1 design doc.*
W627A question goes thirty days after its event endscompleteTHE OWNER'S D-1229, BUILT. W620 made the question box and left the retention open, and an open retention on words the public wrote is the kind of thing that stays open for ever. ★★★ THE RULE IS A TRIGGER AND NOT A JOB'S PRIVILEGE, D-1236. The obvious shape was a flag the scheduled job sets and the delete trigger trusts, and it was refused: a flag is an authority, anything that can set it can delete a question asked an hour ago. EC564 now lets a delete through when the question is past app.audience_question_keep_until(event) and asks nothing about who is deleting, so a question one day past its event is removable by NOBODY, the purge included, and the sabotage proves it: a purge rewritten to ignore the date is REFUSED BY THE TRIGGER instead of emptying the table. ★★ AN EVENT WITH NO DATES KEEPS ITS QUESTIONS. The keep-until date is null where the event has neither date, and no comparison makes null true. A draft event losing a guest's words because nobody filled in a field is the worse error. ★★ THE PURGE IS DECLARED ON THE WORKER'S MAINTENANCE PATH WITH THE TWO TABLES IT REACHES, and the migration re-derives that footprint from the function's own body on every build, W215's check applied to a third function, refusing dynamic SQL it could not read. One allowlist entry was refused and the refusal was right: a test claimed the keep-until function for the maintenance path, which never sends it. ★ THE MODERATOR IS TOLD, AND TOLD THE TRUTH: the queue carries the date from the database's own rule rather than a number typed into the screen, in English and Hungarian. *Evidence: 1 migration, 1 design doc. ON PRODUCTION: 0 questions before, the migration applied twice with six self-assertions and the footprint check green each time, and the nine-control file run against production itself where all nine held, rolled back, leaving no control event, ticket source or audit row. C5 is measured by privilege there and says so, because Neon refuses set role app_rw to its owner. API 42e647ab, answering build 0fec46c which is this slice's commit, with its three schedules live; web e7818da1, 146 of 150 assets byte-identical and the served Hungarian chunk carrying the new sentence against a control at 0; the ledgers at 522 probes, every migration present. D-1229, D-1236.* *Evidence: 1 migration, 1 design doc.*
W628The fee event.clinic charges, on a screen of its owncompleteTHE OWNER'S D-1231, BUILT, D-1237. W542 already put the two amounts on the workspace and both are ZERO on every production workspace, before this slice and after it, so nothing here charges anybody anything. What it adds is who may change them, a record of every change with who and when, and the owner's condition. ★★★ NONE OF THE THREE RULES IS IN THE SCREEN OR THE ROUTE. EC569 has one raiser, app.require_platform_staff(), which all three staff functions perform first, so a future route that forgot could still not read a fee. EC570 is a TRIGGER on app.tenant, so no fee above zero can be set before somebody states that the published terms of sale name the fees, by migration, by script or at a psql prompt. EC571 answers a workspace id that does not exist, so a wrong id never reads as a rights problem. ★★ ONLY A RAISE IS GATED: turning a fee off is always possible in one move, because a confirmation standing between somebody and stopping a charge would be the wrong way round. ★★★ EC571 EXISTS BECAUSE THE FILE'S OWN ASSERTION REFUSED THE FIRST DRAFT, where a missing workspace also raised EC569, which is W462's rule enforced against its author. The same check then caught a COMMENT naming EC570 inside another function, which reads exactly like a second raiser. ★★ AND THE SUITE FOUND THAT A WORKSPACE WITH FEE HISTORY COULD NOT BE DELETED: the history is append-only and the key cascades, so the generic guard refused the cascade too. A delete is now let through when the workspace is already gone, W620's shape, and refused otherwise. *Evidence: 1 migration, acceptance suite, 1 design doc. Eleven controls against the functions, SIX SABOTAGES SIX RED, and eleven checks over the routes on a workspace the suite makes and removes. ON PRODUCTION: neither table existed and no workspace had a non-zero fee; the migration applied twice with seven self-assertions each time; the eleven-control file then ran against production itself and held, leaving the statement unmade, 0 fee changes and 0 non-zero fees. D-1231, D-1237.* *Evidence: 1 migration, acceptance suite, 1 design doc.*
W629A form opens to strangers through an emailed linkcompleteTHE FIRST HALF OF THE OWNER'S D-1230, D-1238. A producer opens a call, a public page takes an email address, a personal link arrives by mail, and it answers the form once. ★★★ NOTHING IS ANONYMOUS AND THE PAGE SAYS SO BEFORE THE ADDRESS IS TYPED: the answer belongs to the address the link went to, which is why this half needs no new privacy wording while W632's fully open door waits for one. ★★ THE LINK IS A CREDENTIAL ROW, W31.5's and W347's shape: one address, one call, the sha-256 of a token the database never sees, a fourteen-day expiry and the moment it was spent. The Worker mints and hashes it, so the raw string never rides in a query. ★★ A THIRD RESPONDENT, AND THE ONE-RESPONDENT RULE BECAME A COUNT: app.form_response carried an account and a crew roster row, and a third that did not widen that check would have let one response be a stranger AND an account. ★★★ FOUR CODES, EACH WITH ONE RAISER, AND THE FIRST DRAFT GOT EC572 WRONG: two functions raised it and the file called that one meaning in two places. The W462 suite refuses that by construction, because a catalogued code's raises are read back from the database and compared with its one sentence. ★★ A FOURTH DOOR RATHER THAN A WIDER GUEST DOOR, the maintenance scope's own argument: it reaches four functions and no tables at all. ★★ AND THREE THINGS THE BUILD REFUSED TO LET ME DO: a call whose form asks for a FILE cannot be opened this way, a stranger uploading into a workspace's storage being W632's question; the reachability check refused the slice while the route existed and no screen mentioned it, so the switch now lives in the form builder beside the URL it hands out; and gen_random_bytes is pgcrypto's, which neither this database nor production has. ★ A BLANKET CATCH HID A REAL DEFECT behind a polite 404 saying the form was closed, and now catches only the refusal that means 'this link opens nothing'. *Evidence: 1 migration, acceptance suite, 1 design doc. Ten checks over the real door with NO HEADERS AT ALL, three sabotages red, and the suite's own 9a could not fail in its first draft, asserting status 200 twice through a ternary. ON PRODUCTION: no link table and 0 calls before, the migration applied twice with five self-assertions each time, and 0 calls open and 0 links after, so the door is inert there until somebody makes a call and opens it. D-1230, D-1238.* *Evidence: 1 migration, acceptance suite, 1 design doc.*
W630A machine key opens the bridge lanecompleteTHE FIRST HALF OF THE OWNER'S D-1227, D-1239. W616 built the queue and shipped it with one way in, a signed-in session holding show.call. A laptop on a venue network has no session, and giving one a person's account is how a shared password ends up written on a flight case. ★★★ THE KEY IS THE FOURTH CREDENTIAL TABLE IN THIS SCHEMA and follows the same rules: only a sha-256 is stored, the expiry is NOT NULL, revoking is a column rather than a delete, EC577 refuses renaming a key onto another desk, and EC576 answers missing, revoked and expired identically so a stolen key learns nothing. It holds NO permission codes. ★★ A FIFTH DOOR WRAPPER, pinned to the workspace the key resolved to and reaching W616's claim and settle and nothing else, never a second lane. ★★★ AND event.clinic SHIPS THE BRIDGE ITSELF, at bridge/bridge.mjs, with NO DEPENDENCIES: it runs on a machine nobody patches on a show day and OSC 1.0's wire format is four rules. It never retries a packet, because UDP has no delivery to retry and a second GO half a second later is worse than none. A key that is not live stops it with one line rather than knocking for a week. *Evidence: 1 migration, acceptance suite, 1 design doc. NINE CHECKS, FIVE SABOTAGES, and the bridge run AS A PROGRAM against a UDP socket standing in for the desk, asserting the packet's literal bytes. ★★★ ONE SABOTAGE FOUND A CHECK THAT COULD NOT FAIL: 4a compared the received packet with the same encoder under test, so with the padding removed 1a went red and 4a stayed green. It asserts literal bytes now and the same sabotage reds both. ON PRODUCTION: no key table before, the migration applied twice with six self-assertions each time, 0 keys and 0 emissions after, and the lane answering EC576's own sentence to a made-up key. D-1227, D-1239.* *Evidence: 1 migration, acceptance suite, 1 design doc.*
W631The Companion module, and one lane both programs sharecompleteTHE SECOND HALF OF THE OWNER'S D-1227, D-1241. W630 shipped the machine key, the lane and event.clinic's own bridge program. This is the Bitfocus Companion module, for a venue that already runs Companion. ★★★ IT HOLDS NO LANE LOGIC AT ALL, AND THAT IS THE WHOLE SLICE. Claim, send and settle moved out of bridge.mjs into bridge/lane.mjs and BOTH programs import it. A second copy would be a SECOND DELIVERY PATH, which W616 refused for HTTP and W630 refused for the claim, and two copies do not stay the same: the day one learns a 401 is fatal and the other does not, one venue keeps knocking with a revoked key for a week. ★★ runOnce TAKES ITS fetch AND ITS send AS ARGUMENTS, which is not a testing convenience bolted on: it is why one function is driven by a raw UDP socket in the bridge and by Companion's UDPHelper in the module. ★★ COMPANION'S SOCKET, event.clinic's BYTES. Companion owns the process lifecycle so it owns the socket, but the packet is still built by osc.mjs, the encoder W630 proved byte for byte. Two encoders for one lane would be two programs that could disagree about what a GO looks like on the wire. ★ A REVOKED KEY STOPS THE POLLING and reads as an authentication failure rather than connecting forever; a missing key reads as bad config, because the two send a technician to different places. ★★★ THE EXTRACTION FOUND A DEFECT W630 SHIPPED: the success settle sat inside the same try as the send, so a network hiccup while settling a cue that HAD FIRED settled it as FAILED. A show report saying a cue did not fire when it did is the worse error, because somebody fires it again. *Evidence: no migration, acceptance suite, the Companion package, 1 design doc. Sixteen checks in three sections. Section 1 drives the shared lane with NO network, NO desk and NO Worker. Section 2 asserts an ABSENCE, that neither program names the claim or settle route, with a negative control proving the shared lane does name both. Section 3 uses COMPANION'S OWN MANIFEST VALIDATOR rather than a hand-written list of required fields, which would go stale silently. Check 1f fails against the old settle ordering, and W630's own suite still passes after the extraction. NOT PROVEN: no Companion instance runs in these tests, so that Companion loads the module is unproven by construction, as a real desk was for W630. D-1227, D-1241.* *Evidence: acceptance suite, 1 design doc.*
W632A form opens to everyonecompleteTHE SECOND HALF OF THE OWNER'S D-1230, D-1242, and Q-501 named its price before anybody built it: an abuse control, a decision on identity, a lawful basis and a declared surface. ★★★ THE IDENTITY ANSWER IS A REFUSAL TO PRETEND. ONE ANSWER PER PERSON CANNOT BE ENFORCED on a form with no account and no link, and the product says so. The response row carries NONE of its three respondent columns, which its own check already permits, and the three obvious alternatives were refused BY NAME: a cookie means nothing to a script, an IP address hashed or otherwise is personal data about a stranger who was told the form is anonymous, and a fingerprint is the same objection with worse manners. ★★★ SO THE ABUSE CONTROL IS ABOUT THE FORM, NOT THE PERSON, and needs no new personal data: a cap the producer sets, and sixty answers a minute the product sets, both counted from rows that already exist. ★★ THE RATE IS READ BEFORE THE CAP. Read the other way round, sixty scripted answers fill a capped form and turn a refusal that lasts a minute into one that lasts forever for everybody honest. C12 checks the ORDER, not the result. ★★ BOTH DOORS SAVE THROUGH ONE FUNCTION, and the open door joined withFormDoor rather than taking a fifth wrapper, because a scope is a sentence about what a path may touch and this path's sentence is unchanged. ★★★ AND IT FOUND TWO DEFECTS THAT BELONGED TO EARLIER SLICES: EC492 carried TWO MEANINGS, the answer-length trigger's and W629's unknown-key refusal, unnoticed because EC492 was never catalogued, so the unknown key is EC582 now; and the first draft ordered fields by a column called position when it is display_order. *Evidence: 1 migration, acceptance suite, 1 design doc, 1 legal draft. Fifteen spine controls FORCED RED IN EIGHT DIRECTIONS and seventeen route checks driven WITH NO HEADERS AT ALL, forced red in three. 4b reads the stored row back and requires all three identity columns null; 4c requires no set-cookie; 2c is the negative control that stops 2a passing for a page that always renders the form. THE PRIVACY NOTICE IS DRAFTED IN HUNGARIAN AND NOT PUBLISHED, as the standing agreement requires, with three questions that are the owner's and their lawyer's. D-1230, D-1242.* *Evidence: 2 migrations, acceptance suite, 2 design docs.*
W633A workspace says what kind of organisation it iscompleteTHE OWNER'S OWN QUESTION, ANSWERED AND BUILT THE SAME DAY, D-1240, and it unblocks Q-583 and the whole W640 CRM block. A workspace now declares at signup whether it is an event agency or an independent producer. ★★★ THE MARKUP COLUMNS ALREADY EXISTED. SEVEN OF THEM, on tenant, budget, project, budget_line, invoice_line, purchase_order_line and quote, with ZERO non-zero values on production. So the slice builds nothing and decides who may put a number in a field that is already there. ★★★ TWO DEFAULTS ON PURPOSE: add column carries agency, which stamps the workspaces that existed before anybody was asked and would otherwise silently lose surfaces they already had; the default then becomes independent_producer, so a future insert that forgets FAILS CLOSED, the same asymmetry the surface gate is built on. ★★ ONE TRIGGER FUNCTION, SEVEN ATTACHMENTS, reading its column names out of TG_ARGV, so an eighth markup column costs one line rather than one more copy that drifts. ★★ IT REFUSES A MARKUP BEING SET, NEVER ONE ALREADY THERE, so a change of kind keeps old rows and does not quietly break editing. ★ EC578 IS NOT A PERMISSION REFUSAL and is forbidden by the control from sounding like one, because the fix is a setting and not a grant. ★★ SIGNUP REQUIRES AN ANSWER although the column has a safe default: a caller who omits it has skipped the question, and writing a value for them would record an answer nobody gave. ★★ EVERY SWITCH IS RECORDED, on the owner's clarification that the mode is freely switchable: a markup that outlives a switch back has no other explanation. *Evidence: 1 migration, acceptance suite, web nav model, API, web. Eleven spine controls FORCED RED IN SEVEN DIRECTIONS and seventeen route checks in four. TWO DEFECTS THE SABOTAGES FOUND AND REVIEW DID NOT: app.tenant carries a COLUMN-LEVEL update allowlist, so without an explicit grant the kind would have been declarable once and never changeable, which the control caught on its first run; and check 1a passed for the wrong reason, because the check constraint's NAME contains the field name, so a refusal by the database read as a refusal by the route. ON PRODUCTION: 1 workspace and 0 markups before, the migration applied twice with six self-assertions each time. D-1240, Q-583 answered, Q-588 raised.* *Evidence: 1 migration, acceptance suite, 1 design doc.*
W634A practice note in the dead timecompleteTHE OWNER'S OWN IDEA, MEASURED BEFORE IT WAS BUILT, D-1243. A list of seventeen things event people say and do not do, asked to appear while a screen loads and on the dashboard. ★★★ THE TWO SYSTEMS THAT LOOK LIKE THE RIGHT HOME ARE BOTH WRONG. W274's help and W320's tips are PULL, reached by somebody who asked; this is PUSH, arriving unasked. And a glossary holds VOCABULARY while these are IMPERATIVES: 'Start on time' defines nothing, and a glossary containing it stops being a glossary. ★★★ ONE LINE COVERS 139 SCREENS. Every screen is routeLazy and AppShell has ONE Suspense boundary they all resolve against, so 'while a screen loads' and 'when switching screens' are the same moment. The alternative was 89 files holding their own copy of the word Loading. ★★★ TWO DECISIONS ABOUT TIME, NOT CONTENT, DECIDE WHETHER IT READS AS CONSIDERED. A chunk arrives in about 200ms and text shown that briefly is FLICKER, which makes an app feel slower, so nothing is drawn for 400ms and a fast navigation stays clean. And a note that changes on every switch is never finished, so the current one is a pure function of the clock, floor(now / 90s) % count: every mount agrees without shared state, it advances with no timer, and it walks the whole list. ★★ PAGE HEADERS WERE MEASURED AND REFUSED: PageHead is on 58 of 139 screens, so a note there would appear on some pages and not others for reasons no reader could work out. ★ EVERY NOTE CREDITS ITS SOURCE and the migration asserts it is never empty, because a product that reprints somebody's words with no name on them is doing something it should not. *Evidence: 1 migration, acceptance suite, 1 design doc. Nine spine controls FORCED RED IN SIX DIRECTIONS and ten route checks in three, plus a browser walk at 1280 and 375 in Hungarian. C1 READS WITH NO WORKSPACE SET AT ALL, because a read policy carrying a workspace test would pass every ordinary test and then show nothing in the one place the feature is for. A DEFECT THE SABOTAGES FOUND: check 4a could not fail, since the identity layer refuses a headerless caller before the route is reached, so it now uses a signed-in person who belongs to nowhere. NOT BUILT: five of the seventeen are checkable against a workspace's own data, and a note shown BECAUSE the data says it applies is a different and better product. D-1243.* *Evidence: 1 migration, acceptance suite, 1 design doc.*
W640The client's one pagecompleteTHE FIRST SLICE OF THE AGENCY CRM BLOCK, D-1244, which W633 unblocked by giving the product a switch that means agency. The owner's CRM list called it 'a single-page client view' and the measuring called it 'every part exists, the screen does not'. ★★★ NOT ONE INVOICE IN THE SYSTEM HAS A PAID DATE, measured across local and production: the largest client on local has 498 invoices, none settled; production has 35 192 companies and ZERO with a project, invoice or quote. So 'nobody knows yet' is not an edge case for the figure this page exists for, it is what EVERY client looks like today, and 0 days late about a client nobody has been paid by is the most dangerous sentence the page could produce. known is a field, the averages are NULL rather than zero, and the screen draws an em dash and says why. ★★ THE OVERDUE COUNT IS SHOWN ANYWAY, because it is true whether or not anything settled: the page reads 'how they pay: —' beside 'one invoice, 42 days past its date'. ★★★ INVOKER RIGHTS, NOT SECURITY DEFINER, AND THAT IS THE SECURITY DESIGN. app.company is SHARED, so two agencies invoice the same client; as a definer the function would hand either of them the other's payment history. Controls put two and then three workspaces on one company, and the suite does it again over the route with two real sessions. ★★ THE LIST IS CAPPED AND THE FIGURE IS NOT. Fifty rows shown, the figure computed over all 498, and the heading says which. The suite's fixture puts BOTH settled invoices outside the fifty most recent, so a figure taken from the shown rows reports unknown and three checks go red. ★ A CONTROL TESTED SOMETHING IMPOSSIBLE: C4's first draft tried to build a payable invoice naming a client and the table refused it, so the direction filter excludes nothing and the comment claiming otherwise was corrected. *Evidence: 1 migration, acceptance suite, 1 design doc. Nine spine controls FORCED RED IN FIVE DIRECTIONS and thirteen route checks in two, plus a browser walk in Hungarian. ALSO FOUND: the Hungarian for 'People' maps to 'Fő', heads as in a headcount, right on attendance and wrong over named contacts, which the i18n suite cannot see because it checks presence and not fitness. Q-590: a client nobody published cannot be opened, and production holds 7 786 of those. D-1244.* *Evidence: 2 migrations, acceptance suite, 2 design docs.*
W641A budget becomes a proposalcompleteTHE OWNER'S COSTING-TO-PROPOSAL, D-1245, and measuring corrected where it lands. ★★★ THE DOCUMENT SYSTEM LOOKS LIKE THE RIGHT HOME AND IS NOT: its lines hold name, details and instructions and its headers hold review and approval dates, which is a method statement rather than a priced offer, and its 501 templates are demo fixtures with ZERO on production. client_quote_line.section already means 'grouped as the client sees it'. ★★★ THE FIRST DRAFT MISPRICED AND THE CONTROL'S SHAPE IS WHAT CAUGHT IT. budget_line.line_total is a GENERATED column, round(amount * time_qty * unit_price, 2); reading amount alone quoted a five-day line as one day, and one priced line on local already carries a time_qty of 3. C2 compares every quoted line against the DATABASE'S OWN generated total plus its markup, to the penny, so a control written with hand-computed expectations would have been written to match the bug. ★★★ AND IT REFUSES TO PRICE A DISCOUNT NOBODY HAS DEFINED. discount_given_pct is stored on every budget line and NOTHING in this product computes with it. Applying it invents a rule; ignoring it charges a client a discount they were promised. So the build stops, EC583, and Q-591 asks for the rule. ★★ A PDF CANNOT RENDER HUNGARIAN WITHOUT AN EMBEDDED FONT. Base-14 PDF fonts use WinAnsi, which has no code point for ő or ű, the two letters unique to Hungarian: 'Ügyfél' encodes and 'Bővítés' throws. DejaVu Sans is carried, OFL-1.1, subset on embed, and the suite OPENS THE FINISHED PDF AND READS ITS TEXT BACK, requiring both characters. ★ THE REACHABILITY GUARD REFUSED THE SLICE while the PDF route was served and no screen mentioned it, and a plain link would have answered 401 because it carries no Authorization header. *Evidence: 1 migration, acceptance suite, 1 design doc. Ten spine controls FORCED RED IN SIX DIRECTIONS and ten route checks. A HAZARD IN THE TOOLING, RECORDED: the sabotage harness left a sabotaged function in the database and the next build produced proposals with no section headings until noticed. NOT BUILT: the client-facing share link, which needs its own credential table on W629's pattern. D-1245, Q-591.* *Evidence: 4 migrations, acceptance suite, 4 design docs.*
W642A workspace sets its own targetscompleteTHE OWNER WIDENED THE QUESTION AND THE SLICE FOLLOWED, D-1246. Asked which figure 'plan against actual' should compare they chose billed, and then: 'let the tenant set their goals and targets for their own company and measure against it'. So it is not a plan field, it is five metrics a workspace aims at over month, quarter or year: billed, collected, events, quoted and won. ★★★ BILLED IS NET, AND THAT IS THE WHOLE RISK. VAT is not revenue, and the GROSS function sits next to the net one in the same schema, so the mistake is one character wide and every figure would still look plausible. The control's fixture uses 27 percent VAT, where net is 65000 and gross is 82550, which is the one value that tells them apart, and a self-assertion reads the function's own source for the gross name. ★★★ AND AN AGENCY INVOICES ON THE LAST DAY OF THE MONTH. A window written with < drops exactly those, quietly, every month forever. C8 puts an invoice on the last day and requires it to count. ★★ TWO BUGS THE CONTROLS CAUGHT ON THEIR FIRST RUN: a CHECK constraint that evaluates to NULL PASSES, so a money target with no currency went straight in; and a constraint declared inside create table if not exists can only ever be right the first time, so the correction never arrived on a re-run. ★★ AND TWO DESIGN MISTAKES CORRECTED: EC585 refused a second target for a period, which the unique index would have raised first so the code could never fire, and which was wrong as a product because correcting a target is not a mistake; and C1 could not see the trigger it tested, because the setting function normalises the date itself, so C1c inserts directly. ★ READING AND SETTING ARE GATED DIFFERENTLY: a target nobody can see steers nobody, while choosing the number is a statement about the company. *Evidence: 1 migration, acceptance suite, 1 design doc. Eleven spine controls FORCED RED IN FIVE DIRECTIONS and eleven route checks, one comparing the actual against the same sum worked out a different way. MARGIN IS DELIBERATELY ABSENT: the rollup is per BUDGET and a budget carries no period, so 'margin in September' has no definition and inventing one would put an unreproducible number on a dashboard. D-1246, Q-592.* *Evidence: 1 migration, acceptance suite, 1 design doc.*
W643A person cannot be in two placescompleteTHE FIRST PRODUCER FOR THE ALERT PATH THE OWNER CHOSE, in-app plus a daily digest, D-1250. ★★★ SAME DAY IS NOT OVERLAP AND FLATTENING THEM WOULD RUIN IT. A crew member loading out at one venue in the morning and on a show at another that evening is a NORMAL WEEK; two shifts that genuinely overlap are impossible. An alert system that calls both an error is ignored inside a fortnight, and then ignored on the day it is right, so kind separates them. ★★ THE PAIR IS ORDERED, shift_a < shift_b, which is what makes one clash one row whichever way the scan found it. ★★ OVERLAP IS HALF-OPEN, so a shift ending at 15:00 and one starting at 15:00 do not clash: back-to-back is a handover. ★ CLASHES RESOLVE, THEY ARE NOT DELETED, because *flagged and then fixed* is a different fact from *never flagged*. ★★★ AND IT COULD FIRE ON NOTHING. Measured on production 2026-09-24, after the fact: zero participation_shift rows, and zero of 56 participants carry either identity this rule matches on. W643.3 answers the second half. *Evidence: 4 migrations, acceptance suite, 4 design docs.*
W644A margin erodes while you can still fix itcompleteTHE SECOND PRODUCER, D-1253, and the owner chose the event's date recalculated rather than a frozen quote. ★★★ THE SLICE DESIGNED A REFUSAL FOR A SOLVED PROBLEM AND HAD TO BE REWRITTEN. It planned to refuse a budget whose foreign lines carry no exchange rate. Measured: all 27 null line_total_base rows are SAME-CURRENCY, and assert_budget_line_currency (EC386) already guarantees a foreign line carries a real rate. The refusal would have fired on nothing and hidden that the conversion was the actual job. ★★ THE RATE IS APPLIED TO THE CLIENT PRICE TOO, not only to plan and actual, or a foreign-currency job reports a margin that is arithmetic on two different currencies. ★★ THE GATE IS budgets.view_margin, NOT budgets.edit, and the sabotage that swapped them went UNCAUGHT until the fixture grew a role that separates the two. A permission test on a fixture where both permissions travel together tests nothing. ★★ app.function_code CAME IN WITH THIS SLICE: three assertions had fired on their own explanatory COMMENTS rather than on code, so the function strips comments before matching. ★ Measured on production 2026-09-24: 45 budgets, all comparable, 16 with a client price, and zero lines carrying actuals, so erosion is null for every one and this producer cannot fire yet either. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W645A certificate lapses before anybody noticescompleteTHE THIRD PRODUCER, D-1254. ★★★ A TYPE THAT HAD BEEN CATALOGUED AND NEVER RAISED. document_expiring had both channels and both labels long before this slice and app.notification held ZERO of them. It passed w12, w32 and w523 the whole time, because those suites check a type is WELL-FORMED and not that anything uses it. ★★★ CONFIDENTIAL MEANS DESCRIBED, NEVER NAMED. This slice puts document names into a notification title and an email subject, so a confidential document is announced by its TYPE. app.document_public_label is the single place that decides and both paths read it, which one sabotage proved by reddening checks on the notice and the mail at once. ★★ IT NAMED TWO STATES THAT DO NOT EXIST, void and archived; the real states are draft, in_review, approved, rejected, superseded. An exclusion list of names the column cannot hold excludes nothing. ★★ AND IT DELIBERATELY DOES NOT REQUIRE approved, which was the tempting reading. ★★★ THE FIGURE THAT JUSTIFIED THAT WAS LOCAL AND WAS REPORTED AS PRODUCTION. The record says *measured on local* and is correct; the summary given to the owner repeated 1030 draft and 3 in_review inside a paragraph about production. Production holds ZERO documents, so this watcher cannot fire there at all. *Evidence: 2 migrations, acceptance suite, 2 design docs.*
W646The annual heatmapcompleteA YEAR AT A GLANCE, SO AUTUMN IS PLANNED IN SPRING, the roadmap's own sentence, and listed as depending on nothing new. ★★ NO MIGRATION, AND THAT IS THE DECISION. A database function earns its place when more than one caller needs the same rule; nothing else reads *how busy is this day*, and withTenant already scopes the read. One route, one panel, and the .heat classes CrowdReportScreen had already solved. ★★★ MEASURED BEFORE A LINE WAS WRITTEN: 30 events on production and FIFTEEN carry no start date. A heatmap that paints the fourteen it can place and says nothing about the rest is worse than none, because it looks authoritative and contradicts the list underneath it, so undated is part of the contract and renders ABOVE the busiest-day line. Every dated event is a single day inside one October fortnight, so the grid today shows one dark patch and eleven empty months, which is honest and not yet useful. ★★ THE SERIES IS CLAMPED TO THE YEAR, NOT FILTERED AFTER IT: one row with a ten-year span would expand to 3650 before anything narrowed it. ★★ AN END BEFORE ITS START STILL PLACES THE START DAY, because generate_series with a stop before its start yields NOTHING, which is a disappearance and not an error. ★★ A CANCELLED EVENT DARKENS NO DAY, the one way this screen could make somebody turn down work they could have taken. ★★★ AND ONE CHECK WAS GREEN FOR THE WRONG REASON. 4a drove an external caller, required 403, and kept passing when the sabotage DELETED the route's permission gate: an external caller is refused by the SURFACE check long before any gate runs, so the gate could have been removed unnoticed. A negative test proves a refusal happened and says nothing about WHICH thing refused. Repaired with an internal role carrying events.view at state deny, plus two checks pinning each control to the layer it claims. The role measurement behind it was wrong too: app.role_permission holds neither candidate's events.view and the difference is role_class, since the permission is record_scoped and an internal role passes with no row at all. *Evidence: acceptance suite (26 checks) forced red six ways, 1 delivery record. NO MIGRATION AND NO NEGATIVE CONTROL SQL, declared rather than omitted.* *Evidence: acceptance suite, 1 design doc.*
W655Erasing a workspacecompleteTHE OWNER'S ANSWER TO Q-595, IN TWO HALVES GIVEN A DAY APART: anonymise now, plus a clock, and the clock is EIGHT YEARS, the Hungarian accounting retention period. D-1255 and D-1266. ★★★ THE AUDIT LOG IS APPEND-ONLY AND THE CLOCK HAS TO DELETE FROM IT. app.forbid_mutation is ONE trigger function shared by TEN tables, so weakening it opens all ten. It gains one exemption with FOUR conditions that must all hold: DELETE and never UPDATE, app.audit_event and nothing else, a transaction-local flag only the purge sets, and the row actually past the period. Each is driven as a REFUSAL by the suite rather than read out of the source, and the trigger is the THIRD layer: app_rw holds no DELETE grant and no policy admits one. ★★★ A PERSON WHO WORKS ELSEWHERE KEEPS THEIR LOGIN. app.app_user carries no tenant_id, so a user is overwritten only when they hold no membership in any other tenant. Six users on production, none in more than one workspace, so the guard protects nobody today and is the whole difference the first day somebody joins a second. ★★★ THREE THINGS ELEVEN STRUCTURAL ASSERTIONS COULD NOT FIND, because every one of them reads the function's SOURCE and none of them RUNS it. Eight of the columns set to null are NOT NULL, so the anonymiser would have raised on the first workspace holding an invitation or a notification send, and app_user.email is UNIQUE so its placeholder carries the row id. Four columns named for personal data are BOOLEANS recording whether an address was shared, not what it was, so overwriting them destroys a consent record and erases nothing. And assertion 11 matched name = null inside holder_name = null, naming 108 tables, the same word-boundary error as W640.1's company form made an hour earlier. ★★★ AND CHECK 4d WAS GREEN THREE TIMES FOR THREE DIFFERENT WRONG REASONS: the row was too recent so the DATE test refused it, then the fixture was built inside the try with an invalid verb so the INSERT threw into the same catch. A negative check whose setup can throw into its catch is not measuring what it names. ★★ 4f ONLY REDDENS WHEN BOTH LAYERS GO, which is the finding rather than the gap: a purge that forgets the period is still stopped by the trigger. *Evidence: 1 migration, acceptance suite (17 checks, every one inside a rolled-back transaction because this suite erases a workspace), 1 delivery record, 1 owner action. Eleven self-assertions forced red eleven ways and eight sabotages against the suite. NO ROUTE, DELIBERATELY: a button that irreversibly erases a workspace is not an affordance anybody should have. THE TENANT ROW IS NEVER DELETED and cannot be for eight years, since seven foreign keys besides the audit log refuse it. D-1255, D-1266, OWNER-ACTION-05.* *Evidence: 1 migration, acceptance suite, 1 design doc.*
W656The morning mail shows only what you may seecompleteTHE IN-APP MARGIN NOTICE IS GATED ON budgets.view_margin AND THE MORNING MAIL WAS NOT GATED AT ALL, so W644 withheld the erosion figure from a coordinator on screen and then posted it to them. Of the two copies, the mail is the one that leaves the building. B-216, D-1270, and the owner chose *One mail, only what you may see* and *A setting of its own, plus per section*. ★★★ THE SAME BUG BLACKED PEOPLE OUT. The recipient list was workforce.edit alone, so a finance lead holding budgets.view_margin and nothing else got NO MAIL AT ALL though every alert in it was theirs. The leak and the blackout are one bug from two sides. ★★★ THE ONE-ARGUMENT digest_items IS DROPPED, NOT REPLACED. A new argument list OVERLOADS, so the ungated version would have survived beside its own fix. Dropping it broke FIVE suites with 42883, and that is the best evidence for it: left in place, every one of them would have gone on passing AGAINST the ungated function and the leak would have survived inside the tests that cover the digest. ★★★ FOUR THINGS WERE TRUE BY ACCIDENT. Assertion 1, written to catch exactly that leftover, could never fire: pg_get_function_identity_arguments returns the parameter NAMES. Assertion 8 was satisfied by HALF a fix, because digest_due calls digest_items twice and position() finds the first and stops. [].every(p) is true, so when a sabotage emptied the notice list four checks written for that failure all passed on it. And the suite's own clock lied: a timestamp without a zone is read in the session's zone, this machine runs Europe/Budapest, so 09:00 reached digest_due as 07:00 UTC and NOBODY was due, which made check 2c green because an empty answer looks exactly like a correct refusal. ★★ A MISSING CHANNEL ROW IS SILENT: notification_channel_enabled ends in coalesce(bool_or(...), false), so creating daily_digest without its email row would have stopped every digest in the product with no error anywhere. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W657Nothing ever looked for an alertcompleteTHE THREE ALERT DETECTORS WERE ON NO SCHEDULE AT ALL. app.detect_schedule_clashes, app.detect_margin_alerts and app.detect_compliance_alerts appeared NOWHERE in scheduled(). Ten jobs hung off the three crons and not one was a detector. Their only call sites were three manual POST routes behind a permission check, while the daily digest ran hourly over the tables they fill and faithfully reported that all was well. ★★★ IT IS NOT THE FINDING OF 2026-09-24, which measured that the source tables hold no qualifying data on production. That is a data problem use will fix. This one meant that with PERFECT data nothing would ever have noticed. ★★★ FOUR SLICES SHIPPED OVER IT, because every one of their suites calls the detector itself, which is the right way to test a detector and cannot distinguish hourly from never. W632.1 shipped the same shape three days earlier from the other side, a scheduled function missing from MAINTENANCE_FUNCTIONS. The generalisation is D-1271: a scheduled path is tested by driving the SCHEDULE, not the function. ★★ NOT CHAINED TO THE DIGEST: they share the hourly tick, so the picture is at most an hour old, and chaining would make a detector failure cost a workspace its whole morning mail. ★★ A GUESSED KEY WOULD HAVE UNDER-REPORTED FOR EVER: the log first read q_open, which does not exist, and a missing key reads as 0 with no error. *Evidence: NO MIGRATION, declared rather than omitted. Acceptance suite (6 checks, every one through /__scheduled), API. Three sabotages, all red, including a function missing from MAINTENANCE_FUNCTIONS. The hourly branch is unproven ON PRODUCTION, where /__scheduled does not exist: it rests there on the trigger being registered and the handler being live, both measured. D-1271.* *Evidence: acceptance suite, 1 design doc.*
W658A company may ask to be removedcomplete27406 COMPANIES ARE PUBLISHED AND UNTIL NOW NONE COULD ASK TO LEAVE. D-1268: they stay published AND a removal route is built, because a legitimate interest that cannot be objected to is not one. ★★★ THE MARKER IS A COLUMN, NOT A NEW publication_state VALUE. company_read admits publication_state <> 'not_published', so a removed value would be ADMITTED by it: the company that asked to be taken out would become MORE readable, and every existing reader would have kept working while doing the opposite of what was asked. ★★★ AND THE GUARANTEE IS A TRIGGER, NOT A FILTER PER PUBLISHER. The sweep updates exactly where publication_state = 'not_published', which is where a removed company sits, so a filter there holds only until the next publisher forgets. The sweep is filtered TOO, and that is not redundancy: D-1264 recorded the publication-bar trigger ABORTING A WHOLE SWEEP on one bad row. ★★★ THE TRIGGER WAS BROKEN ON ITS ONLY PATH. Its RAISE referenced old.name and app.company has no name column. PL/pgSQL evaluates a RAISE's arguments only when it FIRES, so the wrong line sat where nothing would see it, and assertion 3 saw nothing because it only checked the trigger was ATTACHED, which is W655's failure repeated. Assertion 3b now fires it and demands the error code. ★★★ FOUR MORE DEFECTS WERE IN THE TESTS. The suite never exercised the sweep at all, because company_is_referenced asks four tables and the fixtures were in none of them. One careless global replace widened a list appearing twice with different meanings, publishing the sweep's own positive control and destroying the anchor the next edit needed, which then silently did nothing. Check 3a removed the negative control it compared against. And app.audit_event refused the suite's cleanup, which is W655's append-only guarantee working. ★★ THE ROUTE ANSWERS IDENTICALLY WHETHER THE COMPANY EXISTED OR NOT, because a 404 would turn an anonymous endpoint into an oracle for which ids are in the directory. The oracle sabotage had to be done at the ROUTE: the database one could never reach a caller, since the route discards the function's return. *Evidence: 1 migration, acceptance suite (13 checks), API. Eight self-assertions, one of which FIRES the trigger, and six sabotages all red. Q-602 IS OPEN: as shipped, anyone who knows a company id can delist it. THE PUBLIC NOTICE IS UNPUBLISHED, drafted in both languages in OWNER-ACTION-06. D-1268, D-1272.* *Evidence: 1 migration, acceptance suite, 1 design doc.*
W659One answer to whether you hold a permissioncomplete**THE PRODUCT HAD THREE RESOLVERS FOR *DOES THIS PERSON HOLD THIS PERMISSION* AND TWO OF THEM WERE WRONG IN THE SAME TWO WAYS. Measured on production 2026-09-27: app.effective_permissions walks the template chain and reads state, while app.members_holding and app.user_holds_permission joined app.role_permission on the code alone and did neither. ★★★ SO EACH WAS BLIND IN BOTH DIRECTIONS AT ONCE: a role explicitly DENIED a permission was returned as HOLDING it, and a role that INHERITS one from its template was returned as NOT holding it, ignoring D-187's nearest-wins rule. ★★★ WHAT IT GATED, STATED PRECISELY. user_holds_permission appears only in WITH CHECK clauses, on app.file for privacy levels and on app.product for inventory.publish, so this was a WRITE ESCALATION AND NOT A READ LEAK: a role denied inventory.publish could publish anyway. members_holding decides who FOUR notification functions tell, so a denied role was on every one of those lists. ★★ LATENT TODAY AND THAT WAS NOT A REASON TO WAIT: 22 roles, one with a template, ZERO active members on a cloned role, and none of the three deny rows is a permission either caller consults. It opens the first time a role is customised. ★★★ AND B-217'S OWN PROPOSED FIX WOULD HAVE FIXED HALF OF ONE BUG IN ONE OF THE TWO PLACES. It asked for and rp.state = 'allow' in one shared function. Both callers route through the one real resolver instead, and assertion 9 refuses the build if either joins app.role_permission directly again. ★★★ THE MEASUREMENT THAT COULD HAVE MADE IT A CATASTROPHE**: the resolver anchors its recursion on app.role, which has FORCED row security, so a policy hiding that row would return an empty chain and deny EVERYBODY. The policy admits is_template, which is what makes the chain walkable, and assertion 5 drives the inherited case rather than trusting the reasoning. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W660A permission carries the product it belongs tocompleteD-1256 SAYS PLANS MUST BIND AND A QUARTER OF THE CATALOGUE IS NOT THIS PRODUCT'S TO PRICE. Measured on production 2026-09-27: 210 permission codes, four plans, and exactly ONE code mapped to a plan feature, so every workspace reaches every capability whatever it pays. **56 of the 210 are estate.*, Property Management, a separate product in its own repository sharing this database, and 56 plus event.clinic's 154 is the whole catalogue. ★★★ THE OWNER FIRST CHOSE TO MOVE THE CODES OUT AND CHANGED HIS ANSWER WHEN THE MEASUREMENT ARRIVED, BECAUSE THE COST STATED IN THE QUESTION WAS WRONG. The estate schema already exists with 66 tables, so that half was free, but Property Management uses the WHOLE shared permission system: 346 app.role_permission rows grant estate codes, held by a foreign key to app.permission, and 72 row-security policies in the estate schema resolve through it. A move meant duplicating that system or dropping that foreign key, and the second weakens a real integrity constraint to express a product boundary. ★★★ SO THE GUARANTEE IS DECLARATIVE AND THERE IS NO TRIGGER. app.permission gains product and a unique on (code, product); app.permission_feature gains product pinned to event_clinic by a CHECK, and its single-column foreign key is REPLACED by a composite one. An estate code cannot be mapped because no row exists at that code with product event_clinic, and naming a different product on the row is refused by the CHECK. Nothing has to remember the rule. All 346 grants, the key and all 72 policies are untouched. ★★ AND THE THIRD SABOTAGE FOUND A DEFECT IN THE SUITE RATHER THAN THE CODE.** Unstamping every estate code reddened NOTHING: the fixture lookup destructured an empty result, threw, and landed in the outer catch as a SUITE ERROR. Non-zero, so not silent, but it read as a crash and no named check pointed at the cause. Check 0a now fails with the actual sentence. *Evidence: 1 migration, acceptance suite (7 checks). Seven self-assertions, THREE OF THEM DRIVEN rather than read, because an assertion on pg_constraint proves a constraint exists and says nothing about what it refuses. Six sabotages, all red, three against the migration and three against the suite. The suite exists for a different reason than the assertions: the migration ran ONCE and this runs on every battery, and the slice is about what a FUTURE session cannot do. Unblocks D-1256, which will write 154 of these rows. Q-601, D-1281.* *Evidence: 1 migration, acceptance suite, 1 design doc.*
W661A free trial shows the product, and then stopscompleteTHE PLAN NAMED FREE TRIAL ENTITLED LESS THAN EVERY PAID PLAN. Measured on production 2026-09-27: four plans, fifteen plan_entitlement rows, and free_trial holds NONE of them. The gate refuses any MAPPED permission whose feature the plan does not entitle, so the moment D-1256 maps 154 codes a trial loses every one and becomes the most restricted plan in the product. Harmless today, because the one tenant is on enterprise, and it would have hit the first person who ever signed up. ★★★ AND THE HALF THAT WAS NOT IN THE QUESTION: renews_at WAS READ BY NOTHING. Not one database function, not one line of the Worker. A plan whose clock had run out went on entitling everything it ever had, so the owner's answer, *everything Professional gets, limited by time*, was UNBUILDABLE AS STATED: it would have handed every trial Professional for ever. ★★ SCOPED TO THE TRIAL DELIBERATELY. A lapsed PAID plan is NOT cut off, because that is a dunning question with a grace period in it and cutting a paying customer off at midnight is not a default to ship quietly, B-219. Check 2c is the negative control that pins the scope and a sabotage widening the clock to all plans reddens exactly it. ★★ AND limit_value IS STILL DECORATIVE, B-220: Team says 10 events and Professional 100, and nothing counts events against either, because the Worker's gate builds a Set of feature keys and drops the number. ★★★ THE SUITE DEFECT, AND IT IS THE SECOND TIME IN ONE DAY. A sabotage deleting every trial entitlement reddened NOTHING: check 1c destructured a row the sabotage had removed, the throw landed in the outer catch as a SUITE ERROR, and the run exited non-zero with no named check against it. W660's suite had the IDENTICAL defect an hour earlier, found by the identical sabotage. The shape that hid it both times is const [{ x }] = await tx. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W662A public form can hide a question it has no business askingcompleteshow_when EXISTED SINCE W629, WAS VALIDATED BY A TRIGGER, AND NEVER REACHED THE PAGE. app.public_form_link_state projected key, label, field_type, required and options and DROPPED it, so every question was shown to every stranger and the conditional governed nothing. ★★★ WHY IT WAS NOT COSMETIC: the October 23 intake asks a private individual for szuletesi hely, szuletesi ido and anyja neve, the three most sensitive fields on the owner's 198-row list, needed from nobody else. Without the conditional they were put in front of every company and every production agency too. ★★ HIDDEN IN THE MARKUP, NOT BY A SCRIPT, because a question hidden after first paint is a question that was briefly asked, and the submit collector skips any control inside a hidden fieldset so a value the person never saw cannot be sent. ★★ THE FULLY OPEN DOOR WAS LEFT ALONE ON PURPOSE, B-223, and W663 closed it the following night when the preview reached it. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W663Read the form without giving anything, and the emailed link opens the formcompleteTWO DEFECTS ON THE ONLY PATH A PERFORMER WALKS, AND THE OWNER FOUND THE SECOND HIMSELF WITHIN THE HOUR. The mailed link was built from appOrigin, which is where the REACT APP lives, and the answer page is rendered by the WORKER. The app has no /form/answer route, so it served its sign-in shell and Clerk bounced every performer who asked for a link. Measured on production 2026-09-27: app.event.clinic answered 200 with the application and 0 questions, api.event.clinic answered the form with 35. ★★★ AND FIXING IT FIXED NOTHING ALREADY SENT. The owner opened a link from his inbox and hit the wall again, and those links live fourteen days with no way to recall them. W663.1 is a redirect on the static asset Worker, verified against his own link: 302, then 200 with 35 questions. A redirect and not a route in the SPA, because rendering it there would be a second copy of the form and bouncing in JavaScript paints the application shell first, which is the flash a reader reads as being asked to log in again. ★★★ THE PREVIEW TAKES NOTHING AND KEEPS NOTHING: no address, no token, no row, no post. It exposes nothing the public code did not already expose, since anyone holding it can request a link to their own address, and a call that is not published still answers 404. ★★ AND IT REACHED A PROJECTION THAT WAS ALREADY BROKEN, B-223: the open door's projection named f.id with no options and no show_when, so it rendered controls named after a column the answer function does not know and a single_choice with no choices. Fixed first rather than built upon, and 3a now requires the two doors to AGREE rather than each to look sane. ★★★ TWO HARNESS DEFECTS IN PASSING: process.exit inside the catch SKIPS the finally, so a crashed run left a live public link behind and the NEXT run failed with EC575 two checks later. W662 was copied from the same template and had it too. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W664The answers can leave the buildingcompleteTHE OWNER ASKED FOR AN EXPORT AND A LINK A LAWYER CAN OPEN WITHOUT LOGGING IN, AND NEITHER EXISTED. Measured 2026-09-27: no CSV anywhere in web or api, and the only doors needing no account were the two form ANSWERING pages. ★★★ ONE BUILDER, TWO DOORS. app.form_answers_sheet is INVOKER, so a signed-in export is pinned to its workspace by row security, while the same function inside the link's DEFINER runs over a form version read off the LINK ROW. Two spreadsheets of one form is how the two come to disagree about what a performer said, and one of them is what somebody drafts a contract from. ★★★ A SEVENTH SCOPE WRAPPER on W641.1's argument that a scope is a sentence: the form door's is a stranger ANSWERING one form, this one is a named professional READING every answer to one, and the data flows the other way. It cannot mint or withdraw, because a path that could do both needs only one mistake. ★★★ THE LINK CARRIES TAX NUMBERS AND BANK ACCOUNTS, so it is labelled, dated one to ninety days, revocable by a column rather than a delete, and counted. Missing, withdrawn and expired all answer EC659, the same code, so a stale link tells its holder nothing, and it is READ not SPENT because a lawyer opens it again on Monday. ★★★ THE FORMULA GUARD EARNED ITS KEEP ON LIVE DATA: a cell beginning =, +, -, @, tab or carriage return is EXECUTED by Excel on open, and on production 15 real cells were defused, all +36 phone numbers Excel would otherwise render as #NAME?. ★★★ AND 1h EXISTS BECAUSE A SABOTAGE PROVED 1g DID NOT MEASURE WHAT IT CLAIMED: making the sheet a DEFINER, the exact mistake that hands one workspace another's tax numbers, left 1g GREEN because the route reads the form's name first and that lookup is also under row security. Defence in depth is good news and a check crediting the wrong layer is not. ★★ THREE DEFECTS FOUND IN PASSING, NONE IN THE NEW CODE: the producer's own on-screen link was built from window.location.origin, the THIRD place that mistake was found in one night; the byte order mark was an INVISIBLE U+FEFF character rather than an escape, so a sabotage aimed at it could not find the line; and Content-Disposition carried a raw accented filename, which RFC 6266 does not allow. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W665One draft contract per performercompleteTHE OWNER ASKED FOR A BUTTON THAT GENERATES A CONTRACT FOR EACH PERFORMER. What it produces is a SZERZODESTERVEZET, a draft, and the heading says so in both languages. It carries every fact the party supplied in the order a contract states them, so nobody retypes a tax number or a bank account out of a spreadsheet into a contract, which is where that class of mistake is made. ★★★ IT DOES NOT CARRY THE OPERATIVE CLAUSES AND DOES NOT PRETEND TO. Section 4 is a checklist for the lawyer acting. Writing binding terms is not this product's job and getting them nearly right would be worse than leaving them out, because a nearly right clause gets signed. ★★ EVERY FIELD APPEARS EXACTLY ONCE: the three sections are chosen by a heuristic over key and type because app.form_field has no column saying where a question belongs in a contract, so everything unmatched falls through rather than being dropped, and the suite counts fields in against fields out. ★★★ THREE CHECKS WERE REWRITTEN BECAUSE A SABOTAGE PROVED THEM HOLLOW. 2d read the rider's last words out of the document and passed with the wrapping deliberately removed, because text drawn past the right edge is STILL IN THE STREAM, just positioned where no printer puts it; it is now a differential over drawn cursor moves with a MEASURED threshold, 3 when it wraps and 0 when it does not. 1c, 2e and 3a were only ever measured in ENGLISH because the suite sent no accept-language, so three sabotages of the Hungarian wording ran green, and the Hungarian document is the one this product exists to produce. And 2e counted the workspace name anywhere and stayed green with the parties section blanked, because the signature lines supply the count on their own. ★★ AND pdf-text.mjs DECODES BOLD TEXT UNRELIABLY: a bold glyph resolved through the regular subset's map comes out as garbage, so any suite assertion about a heading is standing on sand. ★★ .drawer-section IS A HEADING STYLE, NOT A CONTAINER, and seven screens nest content inside it: a respondent's event rendered as KOSSUTH LAJOS TER PROGRAMJAI and the forty word warning before handing over a set of bank accounts came out in full capitals at eleven pixels. Three repaired here, four are a finding. *Evidence: acceptance suite, 1 design doc.*
W666A file answer is not an empty oneon productionapp.form_answer STORES A FILE IN THREE COLUMNS AND W664'S SHEET READ TWO OF THEM. An answer to a file field carries file_id and file_display_name, and value_text and value are both null, so the key was absent from the answers object and both the spreadsheet a lawyer opens and the draft contract rendered it as nincs megadva, NOT GIVEN. ★★★ A RIDER THAT WAS UPLOADED, READING AS A RIDER THAT WAS NEVER SENT, which is the worst shape a defect can take in this deliverable: the contract is drafted from the sheet, and a term the performer supplied is silently not in it. ★★ LATENT, AND THAT WAS NOT A REASON TO WAIT. Measured on production 2026-09-28: the October 23 form has no file field and there are ZERO file answers in the whole database. It opens the first time a producer adds an upload to a form, which is the obvious next thing to do with a rider. ★★ THE NAME, NOT THE ID: a lawyer reading rider-2026.pdf learns a rider exists and can ask for it, and where no name was recorded the id is the honest fallback because SOMETHING was uploaded. ★★★ AND 2q GUARDS IT AGAINST ITS OWN LOAD ORDER. W664's migration declares this function with create or replace and W666 patches that body from its shipped source, so re-running W664 alone would put the old projection back. The spine loads in slice order so W666 lands last, and 2q and 2r in the W664 suite are what say so out loud if it ever does not. *Evidence: 1 migration, 1 design doc.*
W667The other side of the contract: what the producer suppliescompleteTHE OWNER ASKED FOR FORMS FOR THE PERFORMERS AND THE PRODUCERS, AND ONLY THE PERFORMER HALF SHIPPED. The reason given that night was honest at the time: the 198-row list was on disk only as block headings and row numbers, and inventing question text for a state ceremony's contract data unattended is not a thing to do. It was RECOVERABLE. His message is in the session transcript and the table came out of it in full, 198 rows, none missing. ★★★ TWO FORMS, AND THE SPLIT IS NOT TIDINESS, IT IS HOW THE DATA BEHAVES. The event form differs for each of the eleven events, because who provides the sound at the Opera is not who provides it on the Kossuth ter. The terms form is the same for all twenty four contracts, because cancellation deadlines, force majeure, governing law and the notice address are the project's policy: asked per event they would be typed eleven times, and ELEVEN INCONSISTENT CANCELLATION CLAUSES ACROSS ONE STATE CEREMONY is precisely the failure this exercise exists to avoid. ★★★ FOUR OF HIS ROWS ARE THE SAME QUESTION TWICE, because his list is assembled from two source documents: rows 39 to 41 name the call time, the stage time and the availability window and rows 114, 116 and 118 name them again in English, and rows 93 and 167 are one question about entry. Asked twice, a form gets two different answers and the contract carries the wrong one. Each is asked once, both row numbers recorded beside it, and 76 rows become 72 fields. ★★ THESE ARE FACTS, NOT CLAUSES, which is W665's boundary holding: minimum ertesitesi ido 5 nap is a parameter the lawyer needs, and the sentence that turns it into a term is the lawyer's to write. ★★★ TWO CHECKS WERE REWRITTEN BECAUSE A SABOTAGE PROVED THEM HOLLOW. Assertion 6 counted publications with the code OCT23T, so a second publication of the same policy under any other code sailed past the one assertion built to catch it. And 2c counted how many fields carry the key call_time, which CAN ONLY EVER ANSWER ONE because app.form_field is unique on (form_version_id, key) and the database refuses the second; it now compares the WORDING, which is the risk the schema does not cover. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W668A date question is a date, and a file question says it cannot take a filecompleteTHE PUBLIC PAGE HANDLED FIVE OF THE EIGHT DECLARED FIELD TYPES. text falls to the default correctly. date and file had no case at all and fell there too. ★★★ A DATE RENDERED AS A FREE TEXT BOX, ON EVERY PUBLISHED FORM. Measured on production 2026-09-28: three date fields, one of them Szuletesi ido on the live performer intake, which is one of the three most sensitive questions on the owner's whole list. ★★★ AND THE DATABASE WOULD THEN HAVE REFUSED WHAT THEY TYPED. form_answer_matches_its_field casts the answer to date and raises EC493 when it cannot, so a performer writing "1985. marcius 4." got a refusal in a language nobody could act on, for a question the page had invited them to answer in free text. A native date input hands over YYYY-MM-DD, which casts cleanly, and costs no library: the design system's own rule, a native platform feature over a picker. ★★ FOUND BY COUNTING, NOT BY READING: the producer event form declares two date fields and the page reported ZERO date inputs, which was the whole clue, and it came from looking at the rendered page rather than at the switch. ★★ A FILE FIELD HAS NO UPLOAD PATH THROUGH THIS DOOR AT ALL: app.answer_public_form takes a jsonb array of key and value and a file answer lives in form_answer.file_id, which nothing on this path can write, so rendered as the default it was a text box that swallowed a filename and stored it nowhere. It says so now and carries no data-key, and the wording matches the owner's own decision of riders by email. *Evidence: 1 design doc.*
W669A link can open a draft that already existscompleteTHE OWNER ASKED HOW TO SEND A FORM OUT WITH THE DATA WE ALREADY HAVE IN IT, AND THREE SEPARATE THINGS STOPPED HIM. The link state returned the questions and NO answers, form_public_link had no column pointing at a response, and answer_public_form ALWAYS INSERTED a new sheet. ★★★ THE THIRD IS THE WORST: even a prefilled page would have handed the producer a SECOND sheet beside the one it was showing, so he would hold two versions of one performer with no way to tell which was current. ★★★ AND IT IS NOT A SMALL THING TO ASK OF PEOPLE. Twenty four data sheets are on production, loaded from the performers' own PDFs and riders, every one still a draft and none arrived through a link. Sending them an empty thirty four question form is asking them to type what we already have. ★★★ THE STRANGER'S DOOR CANNOT POINT A LINK AT A DRAFT, AND THAT IS THE WHOLE SECURITY DESIGN. request_public_form_link is reached by anybody who types an address, so a response parameter there would let a stranger read somebody else's tax number and bank account. share_form_draft is separate, INVOKER, and behind a session. ★★ A PERFORMER MUST BE ABLE TO DELETE A VALUE WE HAD WRONG: save_form_answers inserted, so a second pass over one field would have breached its unique key. It upserts now and an EMPTY answer DELETES the row, because refine that can only ever add is not refining. ★★★ AND THE FIRST VERSION OF THE KEY WAS A BUG THE FIRST RUN CAUGHT. on delete set null on a COMPOSITE key nulls BOTH columns and tenant_id is NOT NULL, so deleting any draft that had a link raised a not-null violation. Not a corner: D-1257's retention purge deletes responses ON A CLOCK, so it would have begun failing by itself months later on a form nobody was watching. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W670A form asks in stepscompleteTHE OWNER ASKED FOR NICER FORMS WITH A LOGIC DRIVEN, MULTI PAGE REVEAL. Thirty four questions on one page is a form people abandon, and his own note when the performer intake was designed said the 198-row list sent as one page returns nothing. It is now 34, the producer event form 42 and the terms form 30. ★★★ THE COLUMN IS THE WHOLE MIGRATION: app.form_field gains section and the page groups by it, and a form with NO sections renders exactly as it does today, one page, which is every other form in this product. No second column for the order either, because a section's place is the lowest display_order of the questions in it, so the two can never disagree. ★★ PER OUR CSS, COPIED RATHER THAN APPROXIMATED: this page is served to somebody with no account so it cannot import the app's stylesheet, and the tokens are lifted verbatim from tokens.generated.css, BOTH themes, accent --teal-600 #3150af which is BLUE. ★★★ AND THE PAGE STILL WORKS WITH NO JAVASCRIPT, held by three checks: steps are visible by default and hidden only once the script marks them, the rail and the bar are hidden in the markup, and the submit button is NOT hidden. A display:none default would have shown a stranger a title and nothing else. ★★★ A STEP WHOSE EVERY QUESTION IS GATED OFF IS SKIPPED ENTIRELY, computed from the gates rather than declared, and THE STEPS STAY IN THE DOM so the collector still sees every answer and W662's gates go on governing which fieldsets count. Validation is per step, because driving this form by hand the last press answered with EC496 and a list of NINE missing fields spread over the whole page. ★★★ FOUR DEFECTS FOUND BY LOOKING AT IT: the icons rendered as SOLID BLACK SHAPES because a use clones into a shadow tree an ordinary selector cannot reach; the form said EIGHT steps where the database declares seven, because grouping by a RUN of equal names split the party section whose three individual questions sit after the fee; every heading showed the Hungarian half to an English reader; and the select caret is a data URI whose colour cannot be a token. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W671An answer is checked on the path people usecomplete★★★ EVERY STRING VALIDATION IN THIS PRODUCT WAS DEAD ON THE PUBLIC FORM PATH, AND ONE LINE EXPLAINS IT. app.form_answer_matches_its_field opens with if new.value is null then return new, and app.save_form_answers stored a string answer in value_text with value LEFT NULL. So for every text, long_text, date and single_choice answer arriving through a public form, the trigger returned before it checked anything. ★★★ MEASURED BY DRIVING THE FUNCTION, NOT BY READING IT. All three were stored without complaint: a single_choice answered NOT AN OFFERED CHOICE, a long_text of 25 000 characters against a 20 000 cap, and a date answered not a date at all. ★★ AND W632's SUITE ASSERTED THE OPPOSITE, TRUTHFULLY. Its check that a choice never offered is refused passes through /portal/form/answers, which hands the value to value and therefore never meets the early return. One path was tested and the other was the one people use. ★★ WHY IT HAD NOT BITTEN, AND WHY THAT IS NOT REASSURING: the page offers a select and a native date input, so an honest browser produces valid data and the 219 production answers are clean. But both public routes take JSON from anybody holding a link, so the rules were enforced by the FORM rather than by the database, which is the thing this product's own design says never to rely on. ★★ THE FIX IS THAT value IS ALWAYS SET, with value_text kept beside it so no reader changes, and a date NORMALISED to ISO on the way in because 2026.10.23. casts happily and would have been stored exactly as typed, leaving two performers holding two formats of one day. *Evidence: 1 migration whose backfill VALIDATES the 198 rows it touches, so anything the old path let through stops it; api/test/w671-acceptance.mjs 9/0, run over HTTP on /form/answer/<token> and asserting the EC494, EC492 and EC493 CODES, which only the trigger raises, so a route validating in TypeScript could not produce them. Forced red two ways: reinstating the null value reds 1a, 1b, 1c, 1d, 2b and 2c; disabling the date normalisation reds 2b alone. ★★ A CHECK WAS CAUGHT PASSING VACUOUSLY: 1d, none of the three was written, went green while the payload shape was wrong and NOTHING was being written, and the positive control is what found it. On production: 198 rows backfilled, assertions passed, 0 answers left with a null value, and inside a ROLLED BACK transaction an impossible date is refused with EC493 while 2026.10.23. stores as 2026-10-23. D-1291.* *Evidence: 1 migration, acceptance suite, 1 design doc.*
W672The section editorcompleteW670 SHIPPED THE STEPS AND RECORDED THAT THE BUILDER COULD NOT SET ONE, so the three October forms were grouped BY A MIGRATION and any form a producer made afterwards was one long page with no way to change it. A column only a migration can write is not a producer's choice. /forms/version returns each question's section, /forms/fields/new accepts one, and /forms/fields/section moves a question between steps or out of them. The builder gains a Step column whose new-step sentinel is a NUL-prefixed value, which cannot collide with a step somebody actually names. ★★★ IT WORKS ON A VERSION PEOPLE HAVE ALREADY ANSWERED, DELIBERATELY. W151 freezes such a version and the performer intake has 24 answers on production, so an editor that obeyed the freeze could not touch the one form that needed it. W670 exempted section on the argument the trigger already makes for display_order, and 3b is the control: the LABEL of that same answered version is still refused with EC430, so 3a cannot pass on a freeze that has simply been switched off. *Evidence: NO MIGRATION, declared rather than omitted; the column is W670's. api/test/w672-acceptance.mjs 12/0, forced red four ways each reddening its own set. 5a is the end to end: setting a step through the route makes the public page render steps, and 5b is that ungrouping puts it back to one page, because a column nothing reads is not a feature. ★★ TWO DEFECTS THE SUITE COULD NOT SEE, FOUND BY DRIVING IT IN A BROWSER and fixed in W672.1: the step select and the new-step input carried NO ACCESSIBLE NAME, against D-491 and against this screen's own retention select, so 34 comboboxes announced nothing; and the column gets about 120px, where a step called Proba es hangproba / Rehearsal and soundcheck is not telling apart from A produkcio. ★★ AND PortalForm.tsx WAS TEXT-BOXING TWO FIELD TYPES. It ends in a fallback input, so multi_choice and file rendered as text boxes rather than failing to build. Not cosmetic: a multi_choice answered in a text box produces a STRING where the trigger requires an array, so after W671 made that trigger reachable on every path the answer is refused outright, and a file field collects a FILE NAME and stores it as though it were the file. multi_choice is a checkbox group now and file says plainly that it is answered on the form's own link. web/test/portal-form-answers-every-field-type.test.ts reads the API's own TYPES constant, so a ninth field type fails there instead of silently becoming a text box. Driven at 754px, 1280px and 375px with no horizontal overflow at any of them, and moving a question in the browser persisted to the database. ★ THE BATTERY CARRIED ONE RED, w501, AND IT IS A HARNESS STALL NOT A REGRESSION: the same API code passed it in the battery an hour earlier and it passes 3 of 3 in isolation, at 2.0 s against the 13.5 s it took when it failed. Third instance, recorded. D-1292.* *Evidence: acceptance suite, 1 design doc.*
W673A failing suite gets to say whycompleteTHE BATTERY SUMMARY PRINTED THE CHECK'S NAME AND THREW AWAY ITS REASON. Every suite in this tree writes the status it got, the value it read and the control that should have stopped it into a line under the failure, and the runner's excerpt was lines.filter(l => /^\s*FAIL\s/.test(l)), which keeps the name and drops the reason underneath it. ★★ THE SKIP BRANCH FOUR LINES ABOVE IT HAD ALWAYS DONE IT RIGHT, taking the line beneath the skip. So a skip explained itself in the summary and a failure did not, which is backwards: a skip costs a fixture and a failure costs a ship. ★★★ MEASURED COST, THREE INSTANCES: w19 at 10 223 ms against a normal 3 894, w27 at 302 619 ms with a HeadersTimeoutError, and w501 at 13 512 ms against a normal 2 228. Each failed once inside a battery and passed on a rerun, and each cost a rerun to tell a product regression from a harness stall, because the summary said only the check's name. ★★ THE END OF A REASON IS NOT AN INDENTATION RULE: a detail that interpolates a JSON body puts its closing brace at COLUMN 0, so the rule is the next result line, the totals line, a blank line or six lines, whichever comes first. The fallback for a suite that DIED before printing any check is kept and tested, because a throw at import prints no FAIL line and those are exactly the failures that would go silent. *Evidence: NO MIGRATION and no production surface, declared rather than omitted: this is harness only. api/test/w673-acceptance.mjs 13/0, needing neither a Worker nor a database because failureExcerpt is pure, as classifySuiteOutput and w235 are. Five sabotages each reddening its own set, and 5a and 5b are the slice: four of them break a pure function, but the defect was one line in the RUNNER that discarded that function's whole purpose, so a suite testing only the helper would have stayed green while the summary printed exactly what it printed before. ★★ AND THE FIFTH SABOTAGE WOULD NOT LAND, TWICE: through shell, then Python, then a JavaScript regex literal, the backslash-s it wrote was not the one the check looks for. It reported NOT MEASURED rather than a pass, which is the only reason it was noticed, and was rewritten in pure Python with a raw string and an assertion that the old line landed verbatim. Same class as the silent str.replace() no-ops this project keeps rediscovering: a sabotage must assert it changed something. Driven end to end through the real runner against a deliberately failing fixture suite, which now prints its reason and stops before the next check. Item 3 of the finding, an explicit fetch timeout with a named failure, is deliberately NOT in this slice, because nothing should be built on a guess about what stalled. This makes a stall legible, not less likely. D-1293.* *Evidence: acceptance suite, 1 design doc.*
W674A question movescompletedisplay_order COULD BE SET WHEN A QUESTION WAS CREATED AND NEVER AGAIN. The builder rendered it as a read-only number and no route in the API updated it, so a producer who added a question in the wrong place had to DELETE IT AND MAKE IT AGAIN, and on a version somebody had answered could not even do that. ★★ THAT IS WIDER THAN THE LIMITATION W672 RECORDED, which said steps could not be reordered. No question could be reordered at all. POST /forms/fields/move renumbers the version 1..n and swaps the question with the nearest one carrying the SAME section, and the builder gains an arrow pair whose ends are the ends OF THE STEP, not of the form. ★★ NO MIGRATION, AND THAT IS THE DECISION: withTenant already runs inside inTransaction with the tenant GUCs set, so the read, the renumber and the swap are atomic and row security applies to all three. A SQL function would have been a migration for nothing. ★★ IT RENUMBERS BEFORE IT SWAPS because two questions sharing a display_order, or a form built with gaps, would make the swap write the value it already had, ANSWER 200 AND CHANGE NOTHING, which is the worst of both. ★★★ NOTHING SETS ANY COLUMN BUT display_order: app.form_field_version_is_frozen compares the row minus display_order and section, so a statement changing anything else would be refused with EC430 on exactly the form this exists for, the performer intake with 24 answers. *Evidence: NO MIGRATION, declared rather than omitted. api/test/w674-acceptance.mjs 13/0, five sabotages each reddening its own set. The freeze sabotage PROVES the claim rather than asserting it: changing required alongside the order reds the answered case and leaves the UNANSWERED one green. ★★★ AND THE FIRST VERSION OF THAT SABOTAGE LOOKED LIKE PROOF AND WAS NOT. It bumped updated_at and reddened FOUR checks including the unanswered case, which the freeze cannot touch. app.form_field has NO updated_at column, so it failed on a missing column rather than on the freeze, and the route's comment claiming the freeze would catch it was wrong twice over. Only asking WHY each check failed told the two apart. A sabotage that reddens the right checks for the wrong reason is a false proof. 7a is the end to end and has two halves: the public page asks in the order the database holds AND that order is not the one it started with, because an order that never changed would satisfy the first half by doing nothing. Driven in a browser on the 34-question performer intake across its 7 steps: 34 up and 34 down controls each carrying the question's own label as its accessible name per D-491, the ends of each step disabled, a move landed, the reverse restored it, and all seven steps kept their order and their counts. The old 10/20/30 spacing became 1/2/3, which is a wanted side effect of the renumber. A STEP STILL CANNOT BE MOVED AS A BLOCK, which is the remaining half of W672's note. D-1294.* *Evidence: acceptance suite, 1 design doc.*
W675A stall says so, and says what was opencompleteW673 DEFERRED THIS ON PURPOSE, WAITING FOR ONE DIAGNOSED STALL RATHER THAN A GUESS, AND BATTERY 4 SUPPLIED IT. w541 sat at 269 078 ms against a normal 785 ms and reported 9 ok. Probed WHILE IT WAS STILL STALLED: the local Worker answered /health in 64 ms, Postgres answered in 0.29 s, and the database had no non-idle backend at all. ★★★ THAT MEASUREMENT KILLED THE FIX THE FINDING ASKED FOR. A fetch timeout would have turned that PASS into a RED, over a request that did eventually return and was correct. And w541 makes 3 fetch calls against 30 database calls, so a wrapper around fetch alone would most likely have watched the wrong thing and reported nothing. ★★ SO IT REPORTS AND NEVER FAILS. harness-stall-watch.mjs rides into every suite on --import, so no suite has to remember it and a new suite CANNOT BE WRITTEN WITHOUT IT. The runner counts the lines, appends STALLED to the tag, lists them in the summary and records stalled_suites in the receipt, and the state is never computed from them: a suite that stalls and passes is a pass. ★★ IT NAMES THE OPEN SOCKETS because a socket to 8787 and a socket to 5432 stall identically and need opposite fixes, and a duration cannot tell them apart. No socket at all is also an answer and says so. It says OPEN SOCKETS and not "waiting on": a keep-alive connection stays open between requests, so a named peer is where the process COULD be blocked and not proof that it is, and overstating it would send the next reader after the wrong socket. *Evidence: NO MIGRATION and no production surface, declared rather than omitted: harness only. api/test/w675-acceptance.mjs 10/0, six sabotages each reddening its own set. Section 2 runs the watcher in REAL CHILD PROCESSES rather than reading its source, because firing and naming a peer are behaviour. Driven end to end through the real runner: a stalling fixture reports 1 ok, STALLED and is counted as a pass, with the stall listed separately. ★★★ THREE DEFECTS FOUND WHILE BUILDING IT, ALL BY MEASURING. The TICK was fixed at five seconds while only the threshold was configurable, so every test child sat four seconds and never got a tick, which reads exactly like a broken watcher. A check sliced classifySuiteOutput up to the NEXT EXPORT and swept in W673's comment block, which discusses stalls, so it failed for a reason unrelated to the code it is about. ★★★ AND 1b WAS VACUOUS: its child slept 100 ms against a 150 ms tick, so it exited before the watcher ever looked and stayed silent WHETHER THE THRESHOLD WORKED OR NOT. Removing the threshold altogether did not red it, which is how that was found. A negative control that the sabotage cannot red is not a control. Also measured: node -e is CommonJS, so without --input-type=module the top level await is ignored, the child exits immediately and the watcher never reaches its first tick, silently and with exit code 0. It makes a stall legible, not less likely. D-1295.* *Evidence: acceptance suite, 1 design doc.*
W676A suite is slow against its own normalcompleteW675 NAMED THIS IN ITS OWN SLICE NOTE AND THIS IS THE FIX. w501's stall took 13 512 ms IN TOTAL, less than w252 takes when it is perfectly healthy, so no single number catches w501 without flagging w252 on every battery. At 20 000 ms W675 flagged two healthy suites every run; at 60 000 ms it was quiet and blind to everything under a minute. Both are the same mistake. Each suite keeps its last seven durations and its threshold is max(8000 ms, 4 x median), passed per suite as HARNESS_STALL_MS when the runner spawns it, so W675's watcher fires against the right number and still names the open sockets. ★★★ VALIDATED AGAINST SIX BATTERIES AND 1 926 SUITE RUNS BEFORE IT WAS WRITTEN: flags w541 at 343x and w501 at 5.3x, quiet on w19 at 2.6x and on w252's WORST HEALTHY RUN at 1.1x. Two flags in 1 926, and both of them are the known stalls. ★★ w19 STAYING QUIET IS THE POINT: it was filed as a stall for a day on a duration the variance measurement later put inside ordinary noise, where the median run to run ratio is 1.14x and the worst seen is 2.5x. *Evidence: NO MIGRATION and no production surface, declared rather than omitted: harness only. api/test/w676-acceptance.mjs 20/0, seven sabotages each reddening its own set. Four decisions each with its reason: the MEDIAN not the mean so one stall cannot become the new normal; seven samples so a stall must happen four times before it hides the next; an 8 000 ms floor because a ratio alone is hysterical on a suite that runs in 59 ms; and only a run that COMPLETED NORMALLY teaches the baseline, because a failed suite may have exited on its first assertion and would drag the median down until the next healthy run looked like a stall. The file is local and untracked like the receipt, since a laptop's timings are not a fact about the code. ★★★ THE END TO END FOUND A DEFECT IN W675 THAT NOTHING ELSE COULD HAVE. The watcher POLLED every 5 000 ms, which silently rounded every threshold UP to the next multiple of it: an 8 000 ms threshold became an effective 10 000 and a fixture running 9 065 ms against it reported NOTHING AT ALL. Invisible at W675's flat 60 000 ms, which is already a multiple of 5 000, and visible only once thresholds are per suite and arbitrary. It arms a timeout ON the threshold now and fires at 8 002 ms for 8 000. A slice that only ever ran with round numbers could not have found this. ★★★ AND TWO OF ITS OWN CHECKS WERE VACUOUS, BOTH FOUND BY THE SABOTAGE. 2c asserted w252 stays quiet at 24 421 ms, a figure that sits BELOW its own median, so no ratio however hysterical could ever have flagged it and dropping the ratio to 1 did not red it; it uses w252's worst healthy run now. 3d tested junk filtering through a median, which shrugs junk off anyway, so keeping the junk changed nothing; it asks the question as a COUNT now. Third and fourth instances this week: a negative control the sabotage cannot red is not a control. D-1296.* *Evidence: acceptance suite, 1 design doc.*
W677A plan element knows when it is therecompleteB-225, COMMISSIONED BY THE OWNER 2026-09-28. B-041 versions a plan against a BUDGET version and nothing versions it against TIME, so the plan could not answer what is physically standing at 05:00 on build day minus two. ★★★ MEASURED FIRST, AND TWO THINGS THE BACKLOG ROW ASSUMED TURNED OUT DIFFERENT. It says the window derives from the asset's delivery slots in B-064: B-064 IS NOT BUILT, there is no dock and no slot on this database, and the real source is app.asset.builds_at and strikes_at, set on 1 of 31 assets. It says a named phase, build, live, breakdown: app.delivery_phase ALREADY holds exactly that, per event with starts_on, ends_on and line_order, so this references it rather than inventing a second vocabulary, which would have been the D-028 defect by another route. ★★★ THE ANTI-DRIFT RULE IS ENFORCED BY THE SCHEMA, NOT BY REVIEW. B-225 says a second stored copy of a delivery time must be rejected IN REVIEW. Review is a person remembering and a CHECK constraint is not, so a window naming a phase or an asset must leave starts_at and ends_at NULL: there is nowhere to put a copy and the times resolve at read time. ★★ 4a IS THE SLICE AND 4b IS WHY IT MEANS ANYTHING. 4a moves the ASSET's build time and asks the plan again WITHOUT touching the window, so a stored copy shows up as an answer that did not move. 4b is the control that the schema refuses a derived window carrying its own times; without it, 4a proves only that nobody has written the copy YET. *Evidence: 1 migration with 8 self-assertions, and dropping the anti-drift constraint reds five of them. api/test/w677-acceptance.mjs 15/0, four sabotages each reddening its own set, the sharpest being that replacing the asset derivation with a stored copy reds 4a ALONE. Three decisions with their reasons: an element with NO window is ALWAYS standing, because every plan on this database has none today and the alternative empties all of them on the day this ships, which is W670's argument for a form with no sections; the element is a doc_item_id and not a row, following layout_unit, because floor_layout.doc is in its own migration's words the editor's own state exactly as it serialises it; and the window is HALF OPEN, so a thing that strikes at 18:00 is not there at 18:00. Two traps the checks exist for: a phase carries DATES, so its end is the start of the next day and reading ends_on as midnight drops the last day of every phase silently; and the view is security_invoker, or it reads past the row security of all three tables it joins. ★★ FOUR DEFECTS FOUND WHILE BUILDING IT, EVERY ONE BY MEASURING: a grant to a read only role that does not exist here, floor_layout.venue_id being NOT NULL, app.asset using nickname and not name, and venue_scope_tenant_pair tying scope_class to tenant_id. A fifth was self inflicted: a guard asserting a role name was gone matched its own explanatory comment and refused to write the fix the comment described. NO UI YET, declared rather than omitted: two routes and no control on a screen. The time scrubber and control-room playback B-225 also names are the next slice, and this project has shipped a reachable-from-nowhere write path three times. D-1297.* *Evidence: 1 migration, acceptance suite, 1 design doc.*
W678The time scrubbercompleteW677 SHIPPED THE SPINE AND RECORDED THAT IT HAD NO UI. This is the other half of B-225. The layout detail gains a scrubber, a typed instant beside it because a slider cannot hit 05:00 exactly and 05:00 is the question the owner actually asks, and a per element window editor. ★★★ THREE SIGNALS, AND NONE OF THEM IS COLOUR (D-491): an element not on site at the scrubbed instant goes DASHED, goes TRANSLUCENT, and SAYS SO IN ITS OWN LABEL. A plan read on a projector, in sunlight, or by somebody who does not see the fill must still be readable, and faint grey is the state that survives none of those. ★★ THE SPAN IS DERIVED FROM THE PLAN, not fixed: a slider hard coded to a week renders a four hour plan as one pixel. When nothing is windowed there is nothing to scrub and the panel says so, rather than offering a control that moves and changes nothing, which is the state of every plan on this database today. ★★★ THE ABSENT SET IS BUILT FROM THE WINDOWED ELEMENTS ALONE, which is W677's rule that an element with no window is ALWAYS on site. Building it from every item in the document would make an unwindowed plan VANISH the moment somebody touched the slider. *Evidence: NO MIGRATION, declared rather than omitted; the spine is W677's. /layouts/standing now also returns the delivery phases of the layout's event, so a window can NAME a phase rather than carry dates, and a venue template has no event so the option is simply absent rather than present and broken. ★★ A RANGE INPUT IGNORES CSS UNTIL appearance:none, trap 6 of the design system, and this is the product's first. The thumb is styled TWICE because WebKit and Firefox name the pseudo element differently and a selector list containing one the engine does not know is DISCARDED WHOLE, and the focus ring goes on the CONTROL because a ring on a pseudo element is clipped by the track on at least one engine. ★★ THE i18n GUARDS CAUGHT TWO THINGS: a bare English word stranded between two translated fragments, which is how the setup wizard came to say 'Csoport 1 of 5' and is now one key with named slots so the translator chooses the order, and 22 new strings with no Hungarian. Driven in a browser END TO END: the editor offered two sources on a template with no event, setting a window made the scrubber appear where there had been none, the stage read 'Stage (nincs a helyszínen)' and DASHED at 19:42 on the 19th while three unwindowed items stayed solid, and solid again at 11:57 on the 22nd. No horizontal overflow at 375px. NO PLAYBACK AND NO ASSET PICKER, both declared: playback needs the run-of-show clock the showcalling slices own, and an asset picker over 234 production assets needs a search control of its own. D-1298.* *Evidence: 1 design doc.*
W679A plan is shared with one outside partycomplete★★★ THE BACKLOG ROW NAMES THE WRONG PRIMITIVE AND THE DATABASE SAYS SO. B-229 asks for this on app.grant_token rather than a new mechanism. grant_token.membership_id is NOT NULL: every grant token belongs to a person with a record in the workspace, and the quote_submission tokens on this database all point at a supplier contact's membership. A share to one outside party WITH NO LOGIN has no membership to point at, and the column cannot be widened without changing what a grant token means for the five types already using it. ★★★ AND THE PRODUCT ALREADY SOLVED THIS ONCE. W664 shipped app.form_share_link for exactly this shape, a lawyer with no account sent a link, as a table of its own. app.plan_share_link is that table's TWIN, down to the constraint names, because the sentence is the same one with a different object. So B-229 is followed IN SUBSTANCE AND NOT IN LETTER: no new MECHANISM, a second instance of the one the product has. The grant_token route was built first and undone. ★★ A SEVENTH DOOR WRAPPER, AND THE SENTENCE IS THE TEST, which is W641.1's rule: this one is a STAKEHOLDER READING one PLAN that was shared with them, a different reader, object and verb from the lawyer reading form answers, so it does not join the shared form door. One function, NO TABLES and no minting, because a path that could both mint and read would only need one mistake. ★★ ONE REFUSAL FOR EVERY WAY A LINK CAN BE DEAD: revoked, expired and never minted all answer EC662, because telling them apart tells a stranger holding a guessed token WHICH GUESS WAS CLOSER. The SQLSTATE is the code itself, as W664 did, so the route tells a dead link from a real fault without matching on message text. *Evidence: 1 migration with 8 self-assertions. api/test/w679-acceptance.mjs 20/0, four sabotages. THE ACCESS LOG RECORDS WHEN AND HOW OFTEN AND NEVER WHO: W632 refused to store an IP, hashed or otherwise, as personal data about a stranger, and being sent a link does not weaken that; 3b reads the table's columns back and fails if one appears that names a reader, and a refused open does not move the count or it would measure guesses rather than reads. W677's windows travel RESOLVED, so a stakeholder never learns which asset the times derive from. ★★★ A SABOTAGE EDITED THE WRONG CODE AND ITS ASSERTION PASSED. The anchor for 5b appears TWICE in index.ts and matched the FORMS share route instead of the LAYOUTS one. assert old in s passed, the edit landed, and the suite stayed green because nothing it tests had moved, which read as a check failing to catch a real defect. A cousin of the silent str.replace no-op and worse: the no-op changes nothing, this changes something else and reports success. AN ANCHOR THAT CAN MATCH TWICE NEEDS ITS COUNT ASSERTED, NOT ITS PRESENCE. NO LAYER FILTER, NO COMMENTS, NO PDF, all declared: B-081's overlay layers are not built and a filter over a thing that does not exist is a column nobody can set; B-229's (b), (c) and (d) are separate slices. Driven in a browser end to end: the panel minted a labelled link with a 30 day expiry, the database holds a 64 character hash, the link is shown ONCE with the warning that it cannot be shown again, and opening it with NO SESSION returned 200 with no-store and noindex, the plan's name, the words read only and the element, and moved the access log to one open. D-1299.* *Evidence: 1 migration, acceptance suite, 1 design doc.*
W680A plan an authority will accept on papercomplete★★★ THERE IS NO NORTH ARROW, AND THAT IS THE MEASURED ANSWER RATHER THAN AN OMISSION. B-229 asks for one. Nothing in this product records which way a plan faces: no north, rotation or bearing column on app.floor_layout, no orientation anywhere in doc, and underlay_calibration NULL ON EVERY ROW. An arrow drawn without that data points in an arbitrary direction. On a page a producer glances at, a wrong arrow is untidy. On a page handed to a fire service for a safety case it is a false statement about where the exits face, and it would be believed BECAUSE the sheet looks official. The title block says in words that the orientation is not recorded, and the arrow arrives with the data that would make it true. 3a requires the sentence and 3b requires NO arrow, because a sheet carrying both would pass 3a alone. ★★ THE SCALE BAR IS REAL, because the coordinates are millimetres and it is drawn from the SAME TRANSFORM as the plan, so the two cannot disagree. One scale for both axes: fitting each separately stretches the plan, and a stretched plan with a scale bar measures correctly in one direction only. A4 and A3, both landscape, because a site plan is wider than it is tall. The same sheet comes through the SHARE LINK, so an authority that was sent one does not have to ask for the paper, and a WITHDRAWN link yields no paper either or the whole withdrawal is undone. *Evidence: NO MIGRATION, declared rather than omitted. api/test/w680-acceptance.mjs 15/0, three sabotages each reddening its own check. ★★★ THE SUITE WAS WRONG THREE TIMES BEFORE THE PRODUCT WAS RIGHT ONCE, and every one was a way of reading a PDF that CANNOT FAIL HONESTLY. A content stream is FlateDecode compressed, so searching the raw bytes for a sentence the sheet draws finds nothing whether it is there or not, and the first draft reported FIVE FAILURES AGAINST A CORRECT PDF. pdf-lib writes text as HEX, <5374616765> Tj is 'Stage', so inflating alone was not enough. And then one passed for the wrong reason: the scale bar check matched a PDF MOVETO OPERATOR, the letter m after a coordinate, and passed on a sheet with no bar at all, which only a sabotage could have caught because it was already green. Decoded WORDS and raw STRUCTURE are kept apart now, and that separation is the fix rather than a tidiness. NO ANSI SIZES, NO COMMENTS, both declared, and the legend truncates with a count rather than running off the page. D-1300.* *Evidence: acceptance suite, 1 design doc.*
W681A plan can be placed on the groundcompleteEVERY PLAN MODEL IN THIS PRODUCT WAS VENUE-CENTRIC and none of them could place an object on real ground, which is exactly the owner's event class: an embankment, a public square, a road race, a city-wide footprint. ★★★ BUILT WITHOUT PostGIS, AND THAT IS A MEASUREMENT RATHER THAN A PREFERENCE. Production is Neon on PostgreSQL 18.6 and offers PostGIS 3.6.4. The LOCAL dev server is PostgreSQL 16.14 and CANNOT LOAD IT: Homebrew's postgis is built for postgresql@17 and @18 only and pg16's extension directory has no postgis.control. Every slice here ships on a green battery run against that local database, so create extension postgis is red locally and could never ship, whatever production can do. The earlier B-224 note said no host change was needed; that was measured on production only and is corrected in place. ★★ SO GEOMETRY IS GeoJSON IN jsonb, WHICH IS REVERSIBLE AND THAT IS WHY. ST_GeomFromGeoJSON converts a column of these into real geometry in one statement with no loss. What is given up is spatial QUERYING, and B-226's usable area and B-228's clash check are the slices that will force the upgrade decision. B-224 does not, which is why this one could be built at all. ★★★ THE SAME 40 METRE POLYGON IS ACCEPTED ON A DRAWING AND REFUSED ON THE GROUND, because 40000 is forty metres in millimetres and a longitude off the end of the earth in degrees, and nothing else in the product can tell the two apart. The check is a TRIGGER and not a CHECK constraint, because the rule spans two tables: whether a position must be on the earth depends on the PLAN's crs and a CHECK may not read another row. *Evidence: 1 migration with 8 self-assertions, and dropping its crs awareness reds two of them. api/test/w681-acceptance.mjs 14/0, three sabotages each reddening its own set. ★★ AN HONEST LIMIT ASSERTED RATHER THAN HIDDEN: a Budapest pair written backwards is TWO VALID NUMBERS and passes, because GeoJSON is lon then lat and almost every map UI shows lat then lon, so the commonest mistake here is an order swap that lands in Somalia. Only a swap pushing latitude past 90 is catchable. Check 5a asserts that the swap PASSES, so nobody later reads the validation as cleverer than it is. ★★★ THE SUITE FOUND A REAL DEFECT. The import refused a bad shape MID-LOOP by RETURNING a Response from inside the transaction callback. Returning does not throw, so the transaction COMMITTED the shapes it had already inserted: the route answered 400 and left half an import on the plan, which looks finished and is not. The whole batch is validated before anything is written now, which is simpler than relying on rollback semantics. NO BASE MAP (tiles are a paid service and a commercial decision under B-043), no KML, GPX or DXF yet though the source vocabulary already holds them, no composition of a venue plan inside a city plan, and no UI, all declared rather than omitted. D-1301.* *Evidence: 1 migration, acceptance suite, 1 design doc.*
W682PostGIS arrives, and an area that means somethingcompleteTHE MIGRATION W681 SAID WOULD BE NEEDED, AND IT IS HERE BECAUSE THE MACHINE CHANGED RATHER THAN BECAUSE ANYONE CHANGED THEIR MIND. W681 shipped GeoJSON in jsonb and wrote down exactly why: production offered PostGIS 3.6.4 and the LOCAL server was PostgreSQL 16.14, which cannot load it because Homebrew builds postgis for postgresql@17 and @18 only. A migration opening with create extension postgis was red locally, so it could never ship whatever production could do. The owner upgraded every product to 18.6 on 2026-09-29 and the two now match, measured on both. ★★★ THE EXTENSION IS NOT THE POINT. THE UNIT IS. ST_Area returns a number in whatever space the coordinates live in and this product holds plans in TWO. Measured on real PostGIS, the SAME 40 m by 20 m hall: plan_local in millimetres gives 800000000, wgs84 in degrees gives 0.00000008 SQUARE DEGREES, and the same shape cast to geography gives 670.3 m2. A capacity calculation handed the raw number would propose a crowd of eight hundred million on a drawing, or nobody at all on the ground, and neither looks like an error. So the slice hands out SQUARE METRES and refuses a space it does not recognise with EC666, per B-039: loud, never silent. ★★ THE SRID IS NOT DECORATION: geography ASSUMES 4326 when the SRID is unset, so a millimetre drawing would be read as degrees and silently measure near zero. plan_local gets SRID 0, an unknown cartesian space, which is honest because the drawing is not anywhere on the earth. ★★ NOTHING WAS CONVERTED AND NO TABLE WAS ALTERED, which is W681's promise being kept: the geometry is still jsonb and the functions READ it, so the reversibility is being SPENT rather than discarded. Assertion 8 and check 5a both exist to catch a future session rewriting the column for no gain. *Evidence: 1 migration with 8 self-assertions, api/test/w682-acceptance.mjs 9/0, four sabotages each reddening its own checks.* ★★★ 4a IS THE ONE THAT PROTECTS EVERY WORKSPACE: a view without security_invoker runs as its OWNER so the underlying row security never applies, which is the classic way one tenant reads another's rows and what W507 refuses. 4a reads as app_rw with one workspace set and asserts the other's hall is absent; turning the option off turns it red. ★★ ASSERTION 5 TOOK TWO SABOTAGES AND THAT IS THE POINT: disabling only the crs check leaves plan_geom returning NULL and the null guard raises the same EC666, so it still passed. It is written against the OUTCOME, that an unrecognised space never yields a number, not against one code path. A sabotage removing one of two guards proves nothing when the second produces the same outcome. NO SPATIAL INDEX (a functional index would need the crs, which lives on the parent table, and there are zero feature rows on either database), no generated geometry column (possible, since all three functions are immutable, but it would make PostGIS a hard dependency of the table), and no capacity arithmetic, which is B-226 proper and needs the reference value table first, because the density and flow figures are a professional judgement the owner supplies and an assistant must not source. D-1302. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W683A safety value names its source and its datecompleteTHIS MIGRATION SHIPS NO VALUES. NOT ONE, AND THAT IS THE SLICE. The backlog's own words are the specification and the second sentence is a limit on what an assistant may do: the reference values are "a professional judgement and must be supplied or confirmed by the owner, not sourced by an assistant". A crowd density of four people per square metre is the difference between a busy concert and Hillsborough, and it is not a number to be inferred from a search result by something that cannot be held responsible for it. The table is EMPTY on purpose and check 1a asserts it stays that way, so a later session seeding a plausible density turns it red. ★★★ THE LOUD RULE IS BUILT BEFORE THE ARITHMETIC, per B-039. app.safety_reference() RAISES EC667 when nothing is in force rather than falling back, because a default is a hardcoded safety value wearing a disguise and would be the one number in this system nobody ever chose, chosen by the software at the moment a real event needed a real answer. ★★ DATED, SO LAST YEAR'S SAFETY CASE REPRODUCES: the date is a PARAMETER and not now(), and two versions of one value may not be in force at once. Enforced by an EXCLUSION CONSTRAINT over a daterange rather than a trigger, because the conflict is an OVERLAP and not an equality so no unique index can say it, and a trigger loses the race between two concurrent writers. The nil uuid stands in for the platform scope, since NULL = NULL is unknown and the constraint would otherwise never fire on a platform row. ★★★ A WORKSPACE CANNOT SILENTLY OVERWRITE A PLATFORM VALUE, WHICH IS NOT THE SAME AS CANNOT: a Hungarian authority may require a figure the platform default does not carry and refusing that outright would make the product WRONG RATHER THAN SAFE. What is refused is doing it silently, so a departing workspace figure must name what it departs from and say why, or EC668. A trigger and not a CHECK for W681's reason, the rule spans two rows. A value with no source is an opinion: source_standard is NOT NULL, and unit is tied to kind by a CHECK because a density in persons per metre per minute is not a density. *Evidence: 1 migration with 10 self-assertions, api/test/w683-acceptance.mjs 11/0, five migration sabotages and four product sabotages each reddening its own checks, including a wide-open read policy caught by 5d.* ★★ THE SUITE CAUGHT A VACUOUS CHECK OF ITS OWN: 5b was written as check(..., true, ...) and passed whatever happened. It now asserts the declared override was accepted AND that the row landed, and dropping the trigger reds it alongside 5a. A check that cannot fail is worse than no check, because it reports a covered rule nobody is testing. NO VALUES, no route, two kinds only (standing_density and flow_rate, the two B-226 names, a small closed code-bearing set which is D-015's own carve-out since the calculator switches on it), and no sign-off model, which is about a calculated capacity rather than a reference figure and belongs with the arithmetic. D-1303. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W684A zone knows what its floor iscompleteB-226'S OWN SENTENCE MADE REAL: "adding a stage shrinks the capacity without anyone retyping it". ★★★ MEASURED BEFORE DESIGNING, AND THE SUBTRACTION HAD NOTHING TO SUBTRACT AND NOTHING TO SUBTRACT IT FROM: crowd.zone.capacity is an INTEGER SOMEBODY TYPES, crowd.zone has NO GEOMETRY at all, B-041's Element model is NOT BUILT (no element or structure table exists, measured 2026-09-29), and app.layout_feature.kind is GEOMETRIC rather than semantic so nothing could tell a stage from the zone it stands in, both being 'area'. This slice supplies both, on the shapes that exist TODAY rather than on the element model that does not, and when B-041 phase 2 arrives an Element gains a footprint feature and the arithmetic is unchanged. ★★ role SAYS WHAT A SHAPE MEANS and must agree with kind: an exit is a WIDTH so it is a line, a floor is an EXTENT so it is an area, a point is neither. The default is annotation, because an obstruction default would silently shrink every capacity in the product and a boundary default would silently invent zones. ★★★ 2c IS THE ONE A CAREFUL IMPLEMENTATION GETS WRONG: two structures that OVERLAP, subtracted one at a time, remove their shared area TWICE, so an 800 m2 hall with two overlapping 100 m2 stages reports 600 where it has 650. The safety case is CONSERVATIVE BY ACCIDENT and nobody notices, because the number looks plausible. ST_Union before subtracting is why it is right, and the sabotage is the realistic wrong implementation rather than a strawman. ST_Difference already discards the part lying outside, so a stage standing NEXT TO a zone does not shrink it. ★★ zone_floor RETURNS THE PARTS AND NOT JUST THE ANSWER, because a safety case shows its working and a bare number nobody can decompose is a number nobody can check. An undrawn zone raises EC669, since a zero reads as a venue with no room left and a NULL as no limit. ★★★ ONE DEFINITION OF THE UNIT CONVERSION: the usable area is computed on a DERIVED geometry that was never GeoJSON, so writing W682's millimetre conversion a second time was the obvious path. Two copies of a unit conversion is how two safety numbers come to disagree. app.geom_area_m2 is extracted and plan_area_m2 rewritten to call it; assertion 9 reads the function body and reds if it stops. *Evidence: 1 migration with 10 self-assertions, api/test/w684-acceptance.mjs 11/0, six migration sabotages and two product sabotages.* ★★★ TWO SABOTAGES MISFIRED FIRST AND BOTH WERE WORTH MORE THAN THE SLICE. A SABOTAGE THAT CHANGES TEXT IS NOT A SABOTAGE, IT HAS TO CHANGE BEHAVIOUR. add column if not exists ... default 'obstruction' was a NO-OP because the column already existed, so the statement was skipped entirely and the default stayed annotation. Chasing that down exposed worse: assertion 1 counted ROWS, and this database holds ZERO layout_feature rows, so the check could never have failed. It now asserts the column DEFAULT, which is checkable on an empty table. AND THE MIGRATION WAS NOT RE-RUNNABLE, FOR THE SECOND TIME IN ONE SESSION: dropping a unique index while a foreign key references it raises, exactly as W683 did an hour earlier. A migration that only applies to a fresh database is a migration that works once, so both drop the dependent key first and both are applied TWICE before being believed. NO CAPACITY YET (turning square metres into people needs W683's density, which the owner supplies), no route or screen, and crowd.zone.capacity is UNTOUCHED, because a derived capacity that silently replaced a signed-off one would be the exact failure B-226 is written to prevent. D-1304. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W685A capacity that was calculatedcompleteB-226'S THREE CALCULATIONS, on the floor W684 derives and the figures W683 refuses to invent: standing capacity as usable area x density, exit capacity as clear width x flow rate, evacuation time as population / total flow. Worked through on a 40 by 20 hall with a 4 m gate: 800 m2 at 2.007 per m2 is 1605 people, 4 m at 80 per m per minute is 320 a minute, and 1600 people through it is 5 minutes. ★★★ THE PRODUCT STILL SHIPS NO SAFETY FIGURE: every function reads app.safety_reference() and all three REFUSE with EC667 on a fresh database, which is the intended behaviour and not a gap. ★★ IT FLOORS AND NEVER ROUNDS, because rounding 1605.6 to 1606 admits one person the arithmetic did not make room for, and evacuation time takes the CEILING for the mirror reason. ★★★ THE FIXTURE USED 2.0 FIRST AND A SABOTAGE PROVED IT BLIND: at exactly 2.0 the answer is 1600 and floor() and round() AGREE, so swapping one for the other changed nothing and the check stayed green. The density is 2.007 now precisely so the two disagree. A fixture that cannot distinguish the right answer from the wrong one is not a fixture, which is W495's lesson in a different column. ★★★ THE LOUD RULE, AND WHERE IT COULD HONESTLY BE PUT. B-226 asks that a cap exceeding the safe capacity raise at the moment it is typed. MEASURED: app.ticket_type.cap exists and NOTHING LINKS A TICKET TYPE TO A CROWD ZONE, so which zone a ticket admits to is not a fact this database holds and inventing it would be guessing at a commercial model. That link is an OWNER DECISION. What can be checked today is crowd.zone.capacity, the integer somebody types, and EC670 names the floor, the density and the standard it came from. The trigger fires on boundary_feature_id and density_qualifier too, so a number that became unsafe when the zone was assessed or redrawn is caught THEN rather than standing because it was typed earlier. ★★★ AND IT REFUSES ONLY WHAT IT CAN DISPROVE: an undrawn, unassessed or unfigured zone has no derived capacity, so blocking it would make the product UNUSABLE before the owner has loaded anything, while accepting in SILENCE is the failure B-226 exists to prevent. A warning says the check could not be made, and the sabotage that removes ONLY the warning, keeping the refusal, reds check 4d on its own, which is how that check is known not to be riding on 4a. ★★ UNKNOWN IS NOT ZERO: a zone with no exit drawn raises EC671, because zero exits is a plan nobody finished and reporting it as a sealed room is a very different claim. Assertion 5b exists because a sabotage showed assertion 4 could not see it, 4 always having an exit drawn. *Evidence: 1 migration with 10 self-assertions, api/test/w685-acceptance.mjs 13/0, five migration sabotages and three product sabotages.* NO SIGN-OFF MODEL HERE (app.approval already exists with a polymorphic subject, so D-001's named safety officer is a row and a route rather than a mechanism), one flow rate per zone rather than per exit (a level exit and a stairway differ and layout_feature has no column to say which), no route or screen, and crowd.zone.capacity still means what it meant, because a derived capacity that silently replaced a signed-off one would be the exact failure this slice is written to prevent. D-1305. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W686The lowest limit is the one that bindscompleteB-233, AND IT CORRECTS W685 WHICH SHIPPED EARLIER THE SAME DAY. ★★★ THE DEFECT WAS NOT THAT W685 WAS INCOMPLETE, IT WAS OVER-PERMISSIVE, and it was MEASURED before a line was written: a 40 by 20 hall with ONE 1 m door holds 1600 by area and clears 640 in eight minutes, and W685 ACCEPTED a typed capacity of 1600 on it. Over-permissive by 960 people. A room that holds 1600 and empties 640 is not a room for 1600. The owner's market review states the governing rule that operational capacity is the MINIMUM of area, entry, exit, route, licensed and authority limits, and says why: it *"avoids presenting area multiplied by density as a complete safety determination"*. Check 2a is a REGRESSION TEST against W685 and putting the trigger back to its old behaviour reds it. ★★★ IT NAMES THE LIMIT THAT BINDS, AND THAT MATTERS AS MUCH AS THE NUMBER: a planner told only the number cannot act on it, a planner told "the exits" widens a gate. ★★ A CLEARANCE TARGET IS A SAFETY FIGURE LIKE ANY OTHER: people per minute is not a capacity until it meets a time the zone must clear in, so clearance_target in minutes joins standing_density and flow_rate in W683's table, dated, owner-supplied, and this slice invents none. ★★ LICENSED AND AUTHORITY LIMITS ARE ENTERED AND NEVER CALCULATED, on two independent references in the review: Metis states planning capacity is not legal occupancy and OnePlan advises verification against the authority having jurisdiction. A licence of 500 binds below a derived 640. The safety factor W685 had no concept of sits between raw and planning capacity, and a factor above 1 is refused because it INCREASES the capacity. ★★★ AND THE ANSWER CARRIES WHAT IT DID NOT CHECK. Six limits are named and this slice can assess three, entry being B-234 and route B-239, neither built. limits_not_assessed is returned AND carried in the EC670 refusal so the person typing sees it, because a capacity computed from three limits of six is a different claim from one computed from six, and a number that hides which is worse than no number. *Evidence: 1 migration with 9 self-assertions, api/test/w686-acceptance.mjs 11/0, six migration sabotages and two product sabotages, one of them the whole of W685's behaviour restored.* ★ text[] || 'literal' RAISES, malformed array literal, because an untyped literal appended to a text[] is resolved AS an array. Seven sites carry an explicit ::text now. This was already a written-down lesson from an earlier slice, which is the argument for writing such things down. ONE FLOW RATE PER ZONE STILL (B-235), no calculation sheet (B-236, the artefact a safety advisory group actually reads), and entry and route named as unassessed in every answer rather than quietly left out. D-1306. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W687A way through says what kind it iscompleteB-235, WHICH W685 NAMED IN ITS OWN LIMITATIONS, AND IT UNBLOCKS B-234. zone_exit_capacity resolved ONE flow rate from the ZONE and charged it to every opening, so a 4 m stairway and a 4 m level door were the same object. Measured: a door at 80 per m per min carries 320 and a stairway at 60 carries 240, so 560, and W685 reported 640. That is not a rounding difference, it is a stairway credited with a door's capacity, and the error is in the unsafe direction. ★★★ THE DESIGN TURN: THE FIGURE'S UNIT DECIDES THE ARITHMETIC. persons_per_m_per_min multiplies the drawn width, persons_per_unit_per_min multiplies a unit count. So a form is not a branch in code, it is the QUALIFIER of a reference figure, and adding a turnstile, an airlock or a pass gate is a ROW THE OWNER WRITES rather than a migration. That is D-015's rule applied to arithmetic rather than to a dropdown. Six turnstiles at 25 each add 150 a minute whatever they are drawn as, where treating the figure as per-metre would add 50 from their 2 m of drawn width. ★★ circulation_form IS DELIBERATELY NOT A CHECK CONSTRAINT, because a closed list needs a migration every time a venue has an opening nobody thought of, which is the failure D-015 exists to prevent. What makes an open vocabulary safe is that a form with no figure REFUSES BY NAME, so a typo costs a loud EC667 naming the form rather than a silent throughput. A per-unit figure with no count refuses too, because guessing one understates a bank of six SIXFOLD and in the unsafe direction again. ★ DIRECTION IS ON THE OBJECT, entry, exit or both, because B-234's entry lanes are these same openings read the other way. ★ A NAMING COMPROMISE STATED RATHER THAN HIDDEN: role = 'exit' keeps its name and now means an opening people pass through, since renaming it would touch two shipped migrations and two suites to buy a better word. *Evidence: 1 migration with 9 self-assertions, api/test/w687-acceptance.mjs 9/0, four migration sabotages and one product sabotage.* NOTHING THAT WORKED STOPS WORKING: an opening with no form falls back to the zone's qualifier, which is exactly W685's behaviour, check 6a asserts it, and W682 through W686 all still pass unchanged, which matters because this slice REPLACED a shipped function rather than adding one. flow_rate on the answer is now a RESULT and not an input, the effective blended rate across a mixed set of openings, with flow_source naming the forms added up. No service time for a security lane, which is a rate per person rather than a flow and belongs with B-234's queueing, and still no figure shipped, including the new per-unit shape. D-1307. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W688The queue before the doors opencompleteB-234, AND IT CLOSES THE FIRST OF THE TWO GAPS W686 HAS ANNOUNCED IN EVERY ANSWER SINCE THIS MORNING. W685 shipped the BACK half of B-226's second level, exit capacity and evacuation time, and nothing modelled people ARRIVING. The review names the failure quoting OnePlan, that guessing queue times builds bottlenecks into the plan before the first person arrives, and its Ticket Fairy benchmark checks whether entry throughput becomes the LIMITING FACTOR. ★★★ THE ARRIVAL CURVE IS THE SLICE, NOT THE RATE. A single number cannot produce a queue. People arrive BEFORE the doors open, so a queue already exists at minute zero and that is the queue that hurts. An arrival offset may therefore be NEGATIVE and nobody is admitted before minute 0: ten minutes early, 600 waiting and 0 admitted, clearing at minute 5 with a longest wait of 6 minutes. A model that could not express that would say every event starts calm, and the sabotage that admits people early reds three checks at once. ★★★ AND THE GATE CAN NOW BIND: a floor holding 1600, exits clearing 2560, and a TWO MINUTE admission window at 100 a minute gives an operational 200 bound by entry. That is the fourth of B-233's six limits, and W686's answers no longer say entry is unbuilt. ★★ ONE LOOP OVER THE OPENINGS, NOT TWO: entry and exit are the SAME objects read in opposite directions, so zone_way_capacity is the one implementation and both named functions call it. Two copies would drift the day one learns about a new form and the other does not, and assertion 9 reads both bodies, with a sabotage that genuinely inlines the loop turning it red. ★★ THE WORD IS people_waiting, because crowd.queue_observation already uses it for a queue somebody COUNTED on the day, and the planned and measured numbers must be comparable. THE LIMIT STANDS WITHOUT A CURVE, since how fast the doors work does not depend on knowing when people turn up, and the QUEUE is then reported as unknown rather than as zero. *Evidence: 1 migration with 10 self-assertions, api/test/w688-acceptance.mjs 11/0, five migration sabotages and one product sabotage.* ★ TWO OF MY OWN SABOTAGES MISFIRED FIRST, AGAIN: one anchored on guessed indentation, and one replaced the entry function with a wrapper that STILL called the shared loop, so assertion 9 correctly did not fire because nothing had been copied. A sabotage that changes text is not a sabotage, and that is three sessions running where saying so caught something. ROUTE CAPACITY, B-239, is the one limit of six still unbuilt and still named in every answer, no service time for a security lane, one arrival curve per zone with a second for a peak or degraded case belonging to B-238, and still no safety figure shipped. D-1308. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W689The calculation sheetcompleteB-236, AND THE ARTEFACT A SAFETY ADVISORY GROUP ACTUALLY READS. W682 to W688 return their parts on purpose and every figure names its source standard and effective date, and nothing assembled any of it. A safety advisory group does not read a function signature. One document now carries gross area, every excluded footprint BY NAME, net usable area, the density with its standard and clause and date, raw capacity, the safety factor, every competing limit and which binds, and who signed it off. A count tells nobody WHAT was subtracted from the floor, so the sabotage that turns the stage into "an obstruction" reds 1a. ★★★ THE SHEET DOES NOT HIDE WHAT IT COULD NOT WORK OUT: a failed section carries its own unavailable reason instead of being omitted, because a sheet with no exit section reads as a zone with no exits while one that says why it is missing reads as a plan nobody finished. Those are different documents and only one is honest. ★★★ AND IT STATES ITS OWN LIMIT, IN ITSELF. The FIGURES are versioned so a past date reproduces them, proved by a density of 2.0 in force to July and 1.5 after giving two different sheets for June and August each naming its standard. The PLAN is not versioned, so moving a structure changes what a past sheet says about area, that is B-041 and it is not built, and the sheet says so IN ITSELF rather than in a document nobody exports alongside it. ★★ THE SIGN-OFF IS NOT A NEW MECHANISM: app.approval already carries approval_type = 'safety' with approvers, modes, a decision log and an audit trail, so D-001's named safety officer is ONE BRANCH in app.approval_subject_exists. AND W475'S GUARD IMMEDIATELY DEMANDED THE REST, because its suite PARSES that function's source to derive every subject type and requires each to carry an approval_subject_removed_t trigger naming it. Adding crowd_zone without one would leave a safety approval open in somebody's queue over a zone that is gone. *Evidence: 1 migration with 9 self-assertions, api/test/w689-acceptance.mjs 13/0, five migration sabotages, the trigger one reddening W475 as well as its own checks.* NO EXPORT FORMAT (the sheet is jsonb and B-229 already owns print-grade export, so this belongs beside it rather than growing a second renderer), no route or screen, one sheet per zone since a site-wide total is a different document with a different failure mode where zones overlap and share exits, and route capacity B-239 is still the one limit of six unassessed, named as such in every result. D-1309. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W690A route is only as wide as its narrowest linkcompleteB-239'S FIRST PIECE, AND IT CLOSES B-233'S SIXTH AND LAST LIMIT. B-239's own sentence is the slice: "the limiting gate is rarely the one with the smallest number on it". W687 gave every opening its own flow and nothing said what an opening empties INTO. A 4 m gate at 80 per m per min carries 320, discharging down a 2 m stair it carries 160, and every function shipped before this one would have reported 320 and been wrong by a factor of two in the unsafe direction. ★★★ AND NAMING THE LINK IS THE POINT: route_capacity returns the narrowest flow AND the name of the opening that produced it, because a planner told "160 people" cannot act on it while a planner told "the north stair" widens a stair. The sabotage that replaces the name with "a link" reds 1b on its own. ★★★ AND IT REFUSES TO ADD ROUTES THAT SHARE A LINK. Two routes through one corridor do not carry twice that corridor, and adding them double-counts EXACTLY as W684's overlapping structures did, in the same unsafe direction. Answering a shared network correctly is a MAXIMUM-FLOW problem, which is the rest of B-239, so EC675 refuses and NAMES the shared link rather than returning a number that looks right. ★★ B-233'S SIX LIMITS ARE ALL ASSESSABLE NOW: area from W684 and W685, exit from W685 and W687, entry from W688, route from this slice, licensed and authority entered. Every answer has ended "route (B-239 not built)" since W686 that morning and does not any more, and check 3b shows it BINDING: one route at 160 a minute for 8 minutes caps a hall holding 1600 at 1280. *Evidence: 1 migration with 10 self-assertions, api/test/w690-acceptance.mjs 11/0, six migration sabotages and one product sabotage.* ★ THE THIRD RE-RUNNABILITY BUG OF THE NIGHT AND THE LAST: dropping a unique index while a foreign key references it raises, which W683 hit, W684 hit, and a re-run caught here. A migration that only applies to a fresh database is a migration that works once, and the only thing that finds it is applying TWICE. Every migration in this family is now applied twice before it is believed, which is why all three were caught before shipping; this one applies three times. B-239 IS NOT FINISHED and this is its first piece: the level-3 network itself remains, zones joined to each other, demand over time, route LOADS rather than capacities, density warnings and predicted occupancy. The shared-link refusal is the honest marker of where that begins. No automatic route discovery, because which way people actually go is an operational judgement a plan does not contain. D-1310. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W691What happens when one gate failscompleteB-238, WHOSE OWN SENTENCE IS THE SLICE: "a plan that only works when everything works is not a plan, and the question a safety advisory group asks is what happens when one gate fails". A zone had ONE capacity and nothing could express a closed gate. Baseline two 4 m gates give 5120 over eight minutes with an operational 1600 bound by area; shut gate B and the exits carry 2560; halve every rate and it is 2560 again. ★★★ THE SAME CALCULATION, PARAMETERISED, NOT A SECOND ONE. A scenario threads p_scenario through the EIGHT functions that read openings and changes only WHICH count and AT WHAT RATE, because if a scenario ran different code the difference between two rows would be unattributable and the comparison would compare two programs rather than two situations. Check 4a asserts a scenario closing nothing gives the baseline EXACTLY, and a sabotage making the scenario path differ by one percent reds it. THE SIGNATURES ARE DROPPED, NOT OVERLOADED: create or replace with a new trailing argument makes an OVERLOAD and leaves the old body reachable, and two versions of a safety calculation, one silently ignoring the scenario, is worse than no scenario at all. ★★★ THE ACCEPTANCE SUITE FOUND A REAL SAFETY ERROR IN THE PRODUCT. Check 5b expected a scenario shutting every gate to be unassessable and got operational 1600 bound by AREA. The floor still holding 1600 is irrelevant: a zone nobody can leave has a safe capacity of nobody, and reporting the area figure there is the most dangerous answer this family could give. It took a check that DISAGREED with the code to find it. Which forced a distinction missing all along: no exit DRAWN is UNKNOWN, a plan nobody finished, so it raises EC671; every exit SHUT by a scenario is ZERO, a known state meaning nobody may be in there. Those are different facts and must not share an answer, so zone_way_capacity counts drawn openings separately from open ones and the total-failure row reads 0 bound by exit beside a baseline of 1600 bound by area. That one line is what a safety advisory group is asking for: the same zone, the same calculation, one gate state apart. *Evidence: 1 migration with 10 self-assertions, api/test/w691-acceptance.mjs 13/0, seven migration sabotages and one product sabotage.* ★ A shut gate is not counted as a gate, since counting it would report the same number of exits with less capacity and read as crowding rather than as a closure. A multiplier above 1 is refused, because a scenario making a gate faster than its standard is not a safety case. ★ TWO SABOTAGES MISFIRED, BOTH BY CHANGING TEXT AND NOT BEHAVIOUR: one removed a drop on a database where the old signature was already gone, the other removed the threading entirely so every answer became the baseline and the baseline comparison passed. Fourth session running that this caught something. ONE ARRIVAL CURVE PER ZONE still, scenarios are per zone since a site-wide one needs B-239's network, and no route closures beyond the openings. D-1311. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W692A band boundary is a safety claimcompleteB-237'S DATA HALF, AND THE COLOURS ARE THE SMALL HALF. Its own line is the design: "the thresholds are reference values under D-015 like every other safety figure, not colours chosen by a designer, because a band boundary on a heatmap is a safety claim". Somebody deciding amber begins at 3 people per square metre has made a judgement with a source, a date and consequences, and putting that in a stylesheet hides it from the one group that must review it. A band is a density_band reference figure whose qualifier is its name and whose value is where it begins, and app.density_bands IS the legend, reading the same rows the banding reads so a heatmap and its key cannot disagree. ★★★ AND DENSITY DEPENDS ON NO TYPED NUMBER AT ALL. crowd.zone_status bands on PERCENTAGE of crowd.zone.capacity, an integer somebody types. This is measured occupancy over the usable area W684 DERIVES: no capacity, no safety factor, no licence. Four people per square metre is four people per square metre whatever anybody typed, and check 5a asserts both readings survive because neither replaces the other. Against the USABLE floor and not the gross one, since a hall of 800 m2 with a 100 m2 stage is 700 m2 of standing room. ★★★ A READING IN NO BAND REFUSES: if the lowest band begins at 2 and a zone reads 0.4, a silent no-band renders as no colour, and on a heatmap a missing colour and a missing reading look identical, so a quiet zone would be indistinguishable from a dead sensor. EC677 forces the band set to reach zero. ★★ A BAND MAY BEGIN AT ZERO AND W683'S RULE FORBADE IT: value > 0 was written for densities and rates where zero is meaningless, and a band boundary of zero is not meaningless, it is the point. *Evidence: 1 migration with 9 self-assertions, api/test/w692-acceptance.mjs 11/0, five migration sabotages and one product sabotage.* ★ THE FIXTURE COULD NOT SEE IT, FOR THE THIRD TIME TONIGHT: assertion 4 first ran on a hall with NO obstruction, so gross and usable were both 800 and the sabotage swapping them changed nothing. A fixture that cannot distinguish the right answer from the wrong one is not a fixture, which is now W685's density, W684's row count and this. NO COLOURS (which colour renders which band belongs with the screen), no flow overlay yet though flow per opening exists since W687, and the overlay is a moment rather than a film. D-1312. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W693The gate that counts and the gate that carriescompleteB-239'S NEXT PIECE, AND MEASURING FIRST FOUND THAT THE PRODUCT DESCRIBES EVERY GATE TWICE. crowd.passage is where people are COUNTED, sitting between two zones with a plan point; app.layout_feature role='exit' is where they FLOW, carrying a form, a direction and a figure since W687. They are the same physical gate and nothing said so, so the product could measure 480 a minute through a gate it calculates at 320 and never notice. ★★★ THAT COMPARISON IS THE FIRST REAL NETWORK INSIGHT, and it is what B-239 means by a bottleneck ranking: not a ranking of modelled numbers, which is arithmetic anybody can do, but a ranking of where the model and the day DISAGREE. A gate that cannot be compared keeps its place carrying its reason, because a gate missing from a bottleneck ranking is a gate nobody checks. ★★★ AND A RATIO ABOVE ONE IS NOT A VERDICT OF UNSAFE. It says the model and the world disagree, and which is wrong is a judgement a person makes: the gate may be overloaded, its form may be recorded wrong, or its flow figure may be too low for this crowd. All three are worth knowing and none is unsafe on its own, so the vocabulary is descriptive, under_model at_model over_model, and assertion 5 fails if it ever reads unsafe or dangerous. ★ THE NETWORK EDGES WERE ALREADY THERE: inner_zone_id and outer_zone_id have carried them since the counting model was built, so B-239's zones-joined-by-gates needed NO NEW TABLE, only zone_neighbours reading the counting model from the capacity side. ★★ EVERY COUNT THIS SLICE WRITES IS ROLLED BACK: crowd.count_event is APPEND-ONLY and its passage key is ON DELETE RESTRICT, so a count cannot be deleted and a passage that has one cannot be deleted either, and a test that wrote one would leave it for ever and block its own sweep. The migration uses a plpgsql exception block, which IS a subtransaction, and raises at the end to undo; the suite uses sql.begin() and a throw. The constraint respected rather than worked around, and it is the right constraint: a count somebody took on the day is not a test fixture. *Evidence: 1 migration with 9 self-assertions, api/test/w693-acceptance.mjs 10/0, six migration sabotages and one product sabotage.* ★ ONE SABOTAGE MISFIRED FOR THE FAMILIAR REASON: it set feature_id from a subquery returning NULL, wrote NULL over NULL and changed nothing. Rewritten to create a feature first it reds assertion 9 on all six passages. Fifth session running that this caught something. NOTHING IS LINKED AUTOMATICALLY, because which gate a passage IS is a statement about a real venue, and no predicted occupancy or route loads yet. D-1313. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W694The figures the calculations are waiting forcomplete★★★ TWELVE SLICES OF CROWD CALCULATION SHIPPED, W682 TO W693, AND NOT ONE COULD RUN. W683 shipped app.safety_reference_value deliberately EMPTY, because a standing density of four people per square metre is a professional judgement with a signature behind it and never something software infers. That was right, and it left every calculation refusing with EC667 and no control anywhere in the product for anybody to supply a figure. Measured before building: a grep for safety_reference across api/src returned nothing. ★★★ AND A LIST OF THE REGISTER WOULD NOT HAVE BEEN THE CONTROL. The calculators ask by (kind, qualifier) where the qualifier is FREE TEXT off crowd.zone or the opening's circulation_form, so somebody typing a figure called standing while their zone names standing_static gets a full register, a screen that looks finished, and EC667 for ever. app.safety_figures_needed therefore reads the DEMAND side, and check 2a asserts a figure nothing asks for does not appear. The control to answer a shortfall sits ON the shortfall row carrying that row's exact kind and qualifier, LOCKED, so the only way to type a qualifier freehand is to deliberately start from blank. ★★ THE OPENING'S OWN FORM IS READ TOO: W687 resolves an exit by circulation_form FIRST and falls back to the zone, so a reading that looked only at zones would report every figure supplied while the opening carrying the crowd had none. Density bands count as supplied only when the lowest reaches ZERO, W692's reason: on a heatmap a missing colour and a missing reading are the same picture. ★★ DISPLACING A FIGURE IS LOUD, NEVER SILENT, B-039, and it is ONE statement because two writes would leave an overlap for W683's exclusion constraint between them: an earlier date is a VERSION that closes the old row on the day the new one starts, the same date is a CORRECTION that updates in place and writes the value it replaced into the note, and both refuse with EC679 unless the caller said so. Nothing is deleted from this register: a figure ENDS, because it has to reproduce last year's safety case. *Evidence: 1 migration with 10 self-assertions, api/test/w694-acceptance.mjs 17/0, six migration sabotages and seven function sabotages.* ★★★ AND IT FOUND A GUARD NOBODY WAS RUNNING. web/test holds four guards built after real incidents — route reachability, i18n both ways, form-grid, and W485's local day — and this battery spawned api/test and typechecked the API only. **MEASURED: route-reachability had been RED since W681 shipped four /layouts/* routes with no screen, and thirty-one green batteries went past it. Same shape as the typecheck note already in that file: a gate that existed, covered the case, and was never invoked. web/test now runs BEFORE the suites with its own HARNESS_SKIP_WEB_GUARDS escape, proved by a sabotage that stops the battery with exit 1, and three of the four guards immediately caught this slice's own screen. W681's four routes are recorded in NO_CONTROL_YET with what they are owed. ★ THREE SABOTAGES MISFIRED IN A NEW WAY: applied by editing the migration, its OWN assertions caught them and rolled the transaction back, so the sabotaged function never reached the database and the suite passed honestly against the good one. A self-asserting migration cannot be used to sabotage a suite; the sabotage has to be a bare create or replace function outside the assertion block that would veto it. Sixth session running that rule has caught something. THE SCREEN SUGGESTS NO VALUE, EVER** — no default, no placeholder, no typical figure in a hint. D-1314. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W695A plan you can put a zone oncomplete★★★ crowd.zone HAD NO WRITE ROUTE AT ALL, AND THAT IS THE SLICE. The control centre, the gate counter, the crowd report and THIRTEEN slices of capacity calculation all read zones, and nothing in the product could create one. Production held 0 zones, 0 passages, 0 layouts and 0 shapes: every zone that has ever existed came from a migration or a seed. ★★★ AND A SHAPE COULD NOT SAY WHAT IT WAS FOR. W681 built /layouts/features, W684 then added role and W687 added the circulation three, and NEITHER WRITE PATH EVER LEARNED ABOUT THEM, so the only way to mark a zone boundary was a psql session and W694's waiting list read 'nothing is waiting' for every workspace, truthfully and uselessly. Ships /layouts/features/classify, /crowd/zones POST, /crowd/zone-capacity and app.event_zone_capacity, which keeps a zone it cannot assess in the list carrying the calculator's own refusal and answers in two stages so a drawn zone with no figure still reports its measured floor beside the refusal. ★★★ THE PRODUCTION PROBE FOUND A SHARPER CONTROL THAN THE ONE I WROTE. It expected an unclassified plan to REFUSE, which would be safe. It does not: app.zone_floor takes the boundary from whatever the zone POINTS AT whatever its role, and subtracts only shapes marked obstruction, so an unclassified plan answers 800 square metres where the truth is 700, a capacity fourteen per cent too high, with no exit limit assessable and no error anywhere. Check 4b asserts it now, and it is why the screen marks an unsaid shape in amber rather than letting it look finished. ★★ THE PERMISSION WAS MEASURED AND THE FIRST CHOICE WAS WRONG: plan.administer is Admin, Tenant owner and Tenant supervisor only, and the three roles it excludes, Event director, Operations lead and Project manager, are the ones who actually build a crowd plan and can ALREADY draw the boundary the zone points at. Zone write is venues.edit; the safety FIGURE keeps plan.administer, a deliberate difference between a workspace-wide standard and this event's setup. *Evidence: 1 migration with 8 self-assertions all inside a rolled-back transaction, api/test/w695-acceptance.mjs 16/0, four migration sabotages and four route sabotages.* ★★ FIVE OF MY ASSERTIONS WERE WRONG BEFORE THE PRODUCT WAS, every one a thing assumed rather than measured: role defaults to 'annotation' not null, zone_floor's first field is boundary_m2 not gross_m2, plan_area_m2 is not a column at all (the measurement lives in W682's layout_feature_measured view), the no-figure refusal is EC672 not EC667, and the cross-workspace probe drove a user with NO ACCOUNT in the other workspace so the AUTH layer refused it and row security was never reached. It drives ub@demo.test now, a Tenant owner in B holding every permission these routes ask for. ★ AND A FAILURE REPORT THAT WOULD ITSELF HAVE FAILED: two warning branches did select x into r over a composite, which assigns the whole row to a uuid field and ERRORS, and they only run when an assertion fails so a passing run never reached them. ★★ FOUR GUARDS CAUGHT THE NEW SURFACE AND ALL FOUR WERE RIGHT: W241, the route was event-anchored and did not hand the event to GATE 4 so it was readable by somebody limited to other events; W275, the crowd family already held six top-level rows which is the cap since W389, so the safety plan is a CHILD of the floor plan rather than a seventh sibling; an icon collision; and the hand-maintained route list in nav-model.test.ts, which is exactly what W280 found when the chain editor sat unreachable from W96. All four fired through the web/test step W694 wired in an hour earlier. IT DRAWS NOTHING, declared: a canvas is a slice of its own, and what was missing was not a way to draw but a way to say what a drawing MEANS. crowd.passage, crowd.egress_route and crowd.scenario still have no write route either, which is the next gap. D-1315. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W696A gate between two zonescomplete★★★ crowd.passage HAD NO INSERT ANYWHERE IN THIS WORKER. Its only write has ever been /crowd/passage-position, which moves a pin on a bitmap, so the GATE COUNTER, shipped long ago and sitting in the navigation, has never had a gate to count, and W693's pressure ranking had nothing to rank. Six crowd tables carry zero insert paths between them: passage, egress_route, egress_step, scenario, scenario_closure and arrival_point. This takes the first. Ships /crowd/passages, /crowd/gates gated at gate 4, and the gates section of W695's safety plan. ★★ A GATE IS CLOSED, NEVER DELETED: a count taken through it is evidence and crowd.count_event is append-only with ON DELETE RESTRICT, so is_active is the honest operation. ★★ THE OUTSIDE WORLD IS A REAL ANSWER: outer_zone_id is nullable and always meant the outside, so a perimeter gate has an inner zone and NOTHING beyond it, which is a different fact from a gate whose other side nobody has named. The model could say it since the counting model was built and nothing could set it. ★★★ AND THE LINK UNLOCKS W693: with the gate tied to the opening W695 let somebody draw and classify, calculated 320, measured 480, ratio 1.50, verdict over_model, and a SCREEN can now set that up. *Evidence: no migration, api/test/w696-acceptance.mjs 12/0, five route sabotages.* ★★★ A NEGATIVE CONTROL EXPIRED, EXACTLY AS IT SAID IT WOULD. screen-calls-a-real-route.test.ts used /crowd/passages as its FICTIONAL route, the one no screen should resolve, and its own message read: either somebody built it, in which case delete this control, or served() has gone permissive. Somebody built it. The control fired, named itself and told the next person which of the two had happened. The replacement is deliberately UNBUILDABLE rather than merely unbuilt, because a plausible-sounding path is a control with an expiry date on it and this one has now expired once. ★ ONE SABOTAGE MISFIRED FOR THE OLDEST REASON: it added a comment inside the insert and left the value list unchanged, so feature_id still landed correctly. Sixth session running that rule has caught something. FIVE CROWD TABLES STILL HAVE NO WRITE ROUTE, so W688's queues, W690's routes and W691's scenarios remain unreachable. D-1316. *Evidence: acceptance suite, 1 design doc.*
W697The setup sheet for one zonecomplete★★★ THE LAST FIVE CROWD TABLES WITH NO WRITE ROUTE: egress_route, egress_step, scenario, scenario_closure and arrival_point carried zero insert paths between them, so W690's narrowest link, W691's what-if and W688's queue could be set up only in a psql session. ★★ EACH WRITE REPLACES ITS WHOLE SET, because a route IS its ordered steps, a scenario IS the gates it shuts and a curve IS its points: editing one step by id would let a route exist in a state its author never chose, halfway between two versions, with the narrowest-link answer computed over it. It is also what makes REORDERING possible at all, since egress_step is unique on (route_id, step_no) and swapping two steps by update collides on the way through. app.zone_setup catches each section separately, because three readings that each RAISE cannot be called from a page one after another. ★★★ A SAFETY MESSAGE THAT STATED SOMETHING FALSE. app.zone_route_limits raised EC675 saying 'no way out of this zone is open' whenever every DRAWN ROUTE was blocked. Those are different facts: a zone may have open exits no route passes through, and then the SAME ANSWER carries an exit limit of 1280 people and the sentence that no way out is open. They contradict and the false one is the alarming one -- a planner reading it in a degraded scenario would reasonably believe the zone was sealed. The corrected refusal names the drawn routes AND says what it is not claiming. The body was read back with pg_get_functiondef and changed in one place, because W689 shipped twice with a hand-rewritten body naming something that did not exist. ★★★ AND THE NUMBER DID NOT MOVE WHILE THE REASON DID. The assertion expected the operational figure to FALL when a door is shut. It does not: the baseline is bound at 1280 by the ROUTE, whose narrowest step is the 2 m door, and shutting the 4 m door leaves the exits passing exactly 1280 in the same window. A comparison showing only the number would read 'no change' across a scenario that blocked the only drawn way out, so B-238's table carries the BINDING and the limits that stopped being assessable, each in its own column. *Evidence: 1 migration with 12 self-assertions inside a rolled-back transaction, api/test/w697-acceptance.mjs 16/0, five migration sabotages and five route sabotages.* ★★ TWO SABOTAGES MISFIRED AND BOTH WERE INFORMATIVE: removing the multiplier guard still answered 400 because the DATABASE's check refuses it and asRefusal maps 23514 to 400, so a check on the STATUS could not tell the route's sentence from a constraint name, and 3b asserts the words now; and removing the per-route catch changed nothing because every route in the fixture could be costed, a gap in the assertions that assertion 12 closes. ★ THE SAME ORDERING TRAP TWICE IN ONE SLICE: a stepless route placed above assertion 11 made zone_route_limits raise on IT first, and then the PRODUCTION PROBE walked into the identical trap and reported two FAILs that were its own. Recorded rather than quietly reordered. AND THE DESIGN PARITY GATE CAUGHT THE DRAWER, rightly: it is not a screen, it renders a Drawer whose body is measured at padding var(--sp-5) on all four sides, and it surfaced only from the battery because the parity script prints no node:test line, the same gap W625 recorded. D-1317. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W698The calculation sheet, on a route at lastcomplete★★★ W689 BUILT THE ARTEFACT A SAFETY ADVISORY GROUP READS AND NOTHING COULD FETCH IT. Measured 2026-09-30: zone_calculation_sheet appeared ZERO times in the Worker, as did event_density_at and density_bands. The whole OUTPUT side of the crowd family was unreachable, exactly as its input side was until W694, and W694 to W698 close both ends. ★★ A GROUP CANNOT CHECK A NUMBER, IT CAN CHECK A NUMBER WITH ITS PROVENANCE, so every figure carries the standard it came from, the paragraph inside it, the date it applied from and whose figure it is; the floor shows what was taken off it and by how much; the arithmetic is stated in words; and the limits NOT assessed and the missing signature are on the face of it. THE SCREEN COMPUTES NOTHING -- not a sum, not a rounding, not a unit -- because a number on a safety document produced by the screen would be a second calculator nobody audited. *Evidence: no migration, api/test/w698-acceptance.mjs 11/0, three route sabotages.* ★★★ TWO THINGS THE FIRST DRAFT GOT WRONG, BOTH BY ASSUMING A KEY NAME. The screen rendered area.boundary_m2 and excluded[].m2 where the sheet carries gross_m2, excluded_m2 and own_m2, and A MISSING KEY RENDERS AS AN EMPTY CELL ON A SAFETY DOCUMENT rather than as an error: the gross area would simply have been blank on a page handed to a fire service and nothing would have objected. Check 2b caught it, a reader might not have, and it is the THIRD assumed name to be wrong in one night after plan_area_m2 and gross_m2 in W695. And it dumped three sections into a <pre> under kv-raw, a class that does not exist, which renders as nothing and reads as a spacing bug, sixteen of which this project shipped once. It walks the object now, so a figure W689 adds later appears instead of being dropped, and the keys show as the DATABASE's own words because inventing English here would put a word on a safety document no translator ever saw. NO PDF, declared: W680 built a plan sheet with pdf-lib and a real scale bar, a calculation sheet deserves the same and is a slice of its own. The page prints, which is not the same thing. ★★★ AND THE SAME MISTAKE A THIRD TIME, ON THE PART THAT MATTERS MOST, AND IT REACHED PRODUCTION. The sheet carries operational_capacity and binding_limit; the screen typed operational and binding, so the capacity figure and the limit that holds it down were both BLANK on a document handed to an authority and nothing objected. Found by walking the whole chain end to end on production and reading the output, not by any check: check 2b had caught the area case and nothing asserted what was IN the result section. TWO FIXES AND THE SECOND IS THE ONE THAT MATTERS: check 2d asserts the values, and the shapes are now typed EXACTLY, with no Record<string, never> and no blanket optionals, so result.binding is a COMPILE ERROR, proved by making it one and reading TS2339, and tsc runs before every suite in the battery. An assertion cannot see a screen reading a key the API never sends. A type can. Fourth assumed key name wrong in one night, shipped an hour after writing a memory entry about exactly this, so the lesson that holds is not read the definition but MAKE THE WRONG NAME FAIL TO COMPILE. THE DENSITY OVERLAY IS STILL UNREACHABLE, at zero references, and that is B-237's rendering half which wants colour on geometry rather than another table. D-1318. *Evidence: acceptance suite, 1 design doc.*
Phase 14 - The safety planning programme
W699A shape nobody classified is not a floorcomplete★★★ THE UNSAFE DIRECTION, MEASURED ON PRODUCTION. app.zone_floor took the boundary from whatever the zone POINTED AT, whatever that shape's role, and subtracted only shapes marked obstruction. So a plan whose shapes were never classified did NOT refuse, which would have been safe: it answered 800 square metres where the truth is 700, a capacity fourteen per cent too high, in the direction that puts more people in, with no exit limit assessable and NO ERROR ANYWHERE. role defaults to 'annotation', so that was every plan in this product until W695 gave anybody a way to classify a shape. ★★★ AND THE TWO HALVES NEED OPPOSITE TREATMENTS, WHICH IS THE PRINCIPLE THIS SLICE ESTABLISHES: REFUSE WHAT IS UNKNOWABLE, SURFACE WHAT IS SUSPICIOUS. An unclassified BOUNDARY is unknowable and now raises EC680, naming the shape and its actual role. An unclassified OBSTRUCTION cannot refuse, because a shape nobody marked may genuinely be decoration and a database that refused every plan with one would refuse almost every real plan, so app.zone_unclassified_shapes NAMES them with their areas, filtered to those actually intersecting the boundary, and app.zone_setup and the setup drawer carry the list. ★★ EC680 IS A DIFFERENT CODE BECAUSE IT IS A DIFFERENT FACT: EC669 means nobody drew the zone, EC680 means the work is there and nobody labelled it, which is one click to fix. W691 paid for not drawing the same distinction between no exit drawn and every exit shut. *Evidence: 1 migration with 9 self-assertions inside a rolled-back transaction, api/test/w699-acceptance.mjs 10/0, four function sabotages.* ★ NOTHING ELSE BREAKS AND THAT IS ARCHITECTURE RATHER THAN LUCK: every caller that shows a zone already catches per section, so a new refusal becomes a sentence on a screen, and thirteen existing crowd suites passed unchanged. Blast radius measured before shipping: 0 of 2 zones on local, 0 on production. The migration's own assertions fired three times during development, twice on a nesting that put the new check inside an if-not-found block where it could never run, and once on the THIRD ordering trap of the session. Safety Planning Programme kit decision OD-22. D-1319. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W700One home for how many peoplecomplete★★★ THE WORST DEFECT IN THE REFERENCE CORPUS IS A NUMBER THAT CONTRADICTS ITSELF INSIDE ONE DOCUMENT. The Safety Planning Programme brief calls it defect class V01: a real 2026 security plan carries THREE DIFFERENT MAXIMUM CROWD FIGURES, and every capacity check, staffing ratio and evacuation time in it depends on which one a reader happens to use. ★★★ TWO FACTS THE CORPUS CONFLATES, AND SEPARATING THEM IS HALF THE FIX: max_simultaneous is how many are inside AT ONCE, which capacity and evacuation read, and total_attendance is how many pass through, which a licence usually states. They are different numbers and THE LARGER ONE IS NOT THE SAFE ONE: one column is how 60 000 across three days becomes 60 000 standing on a field, and a check constraint refuses a total BELOW the simultaneous maximum because that is arithmetically impossible and is exactly what a mistyped column looks like. ★★★ AND THE CONTRADICTION IS CAUGHT ON READ RATHER THAN PREVENTED ON WRITE, DELIBERATELY. An event figure and a day figure are both legitimate and are entered at different moments from different sources; what must never happen is a day quietly exceeding the event it sits inside, and a unique constraint cannot express that. crowd.event_max_simultaneous raises EC682 and NAMES BOTH ROWS AND BOTH BASES, because refusing without naming them sends somebody hunting through a plan. Two figures for the SAME day are a duplicate rather than a conflict and the unique index refuses those outright. ★★ A FIGURE WITH NO BASIS IS AN OPINION, W683's rule applied to the event's own number, so basis is NOT NULL and closed. *Evidence: 1 migration with 9 self-assertions inside a rolled-back transaction, api/test/w700-acceptance.mjs 13/0, four sabotages.* NO FIGURE SHIPS: this is the event's own number and the organiser supplies it. NOTHING WRITES IT YET, declared: the table and its reader exist, no route and no control do, and that is the next slice. Kit decision OD-24. DEC-SP-6. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W701The crowd figure gets a way incomplete★★★ W700 BUILT THE TABLE AND NOTHING COULD WRITE TO IT, which is the exact pattern W694 to W698 spent five slices closing and this project has shipped six times before that: W19, W21, W26, W330, W358 and W681. Creating a fresh one the day after fixing five is the same mistake wearing this week's clothes, so it is closed the same day. /crowd/forecast reads, writes, amends and REMOVES a figure, because a forecast is a claim about the future unlike crowd.count_event which is append-only because somebody counted those people on the day. ★★★ AND CATCHING THE CONTRADICTION IN TYPESCRIPT DOES NOT WORK. THE FIRST DRAFT TRIED. Every route runs inside ONE transaction through withTenant, so a raise POISONS it: the catch runs and the commit fails anyway, and the suite got HTTP 409 where it expected a readable answer. A savepoint would work and no route in this Worker uses one, which makes it the wrong tool to introduce for a display problem, so crowd.event_max_simultaneous_or_reason catches inside plpgsql exactly as app.event_zone_capacity (W695) and app.zone_setup (W697) already do. Assertion 4 proves the transaction is still usable afterwards, which is the entire point. ★★ THE CONTRADICTION IS SHOWN, NOT THROWN: a day holding more people than its event returns 200 with the calculator's own sentence naming both figures, and BOTH ROWS STILL COME BACK, because the planner is the only one who can say which is wrong and hiding them would hide the fix. Two database rules are repeated at the route so the message names the field rather than a constraint name, and check 2b asserts the WORDS, which is W697's lesson. *Evidence: 1 migration with 5 self-assertions inside a rolled-back transaction, api/test/w701-acceptance.mjs 11/0, five route sabotages.* SLOT FIGURES ARE STORABLE AND NOT OFFERED, and NOTHING CONSUMES THE FIGURE YET: the staffing ratio check, the density statement and section S05 are the intended readers and none is built. DEC-SP-8. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W702The twenty-fifth function arrived publicon production★★★ TWO OF 88 SECURITY DEFINER FUNCTIONS IN app ALLOWED PUBLIC TO EXECUTE ON PRODUCTION, app.form_answers_by_link (W664, 27 September) and app.plan_by_share_link (W679, 28 September). On Neon the definer's owner carries rolbypassrls, so a PUBLIC execute grant on one is a complete path around row security for any role holding nothing more than USAGE on the schema. Neither migration revoked PUBLIC on the function it created, so each arrived carrying the Postgres default. THIS IS INSTANCE THREE AND W249 NAMED IT IN ADVANCE: *what a list cannot do is cover the twenty-fifth, and the twenty-fifth is how both known instances started.* ★★★ THE FIRST DIAGNOSIS WAS WRONG AND THE OWNER HAD APPROVED ACTING ON IT. F1 said W249 was unapplied, that applying it would break the form and plan share links, and that the rule therefore needed a named exemption list. Every clause is false. W249 IS APPLIED -- app.user_holds_permission carries an explicit app_public=X grant and no PUBLIC entry, and W249 section 1 is the only thing that issues it. AND REVOKING BREAKS NOTHING, because both functions are reached through env.DB and app_rw holds an explicit app_rw=X/neondb_owner grant on each. THE INFERENCE THAT WAS WRONG: that a caller unauthenticated at the HTTP edge is unauthenticated at the DATABASE. It is not -- the Worker authenticates to Postgres as app_rw on every request and the share-link token is checked INSIDE the definer function. The PUBLIC entry was never a door anyone walked through, it was a default nobody revoked. SO NO EXEMPTION WAS BUILT AND THE RULE KEEPS ZERO EXCEPTIONS; building one would have written a permanent hole into a security guard behind a reason line that read plausibly and was false. ★★★ PROVED BEHAVIOURALLY RATHER THAN BY READING THE ACL: with PUBLIC revoked on local, w664 passed 30/30 and w680 passed 15/15 including 4a, an authority that was sent a link gets the paper too with no account, the exact path F1 claimed would break, and battery 44 came back 345 passed 0 failed across 7891 assertions, identical to the run before the revoke. That green was proved able to go red: with EXECUTE also revoked from app_rw, w680 failed 3 of 15 including 4a. ★★★ WHAT ACTUALLY FAILED IS NOT A MIGRATION. verify-definer-not-public.sh has asked this question correctly since W249 and found both the first time it was pointed at production. IT IS NOT IN THE BATTERY, so it runs when somebody remembers, and FORTY-FOUR GREEN BATTERIES WENT PAST TWO EXPOSED FUNCTIONS OVER THREE DAYS -- the W694 web-guard lesson one layer down, a gate that existed, covered the case and was never invoked. THE DURABLE FIX IS THE SCHEMA GUARD NOW IN run-all-acceptance.mjs, asking the property on every run before the suites, with its own escape HARNESS_SKIP_DEFINER_GUARD deliberately separate from the web guards -- a half-finished screen is a reason to skip a reachability check and is not a reason to stop asking whether the database hands out a way around row security. A sweep at position 702 cannot see a function created by w703. ★★ THE MIGRATION IS A SWEEP AND NOT A LIST OF TWO, for the reason W249 adopted one, and its a2 paired positive is the assertion that would have caught the exemption: had revoking really cut the links off, app_rw would have lost EXECUTE and the file would abort rather than ship. *Evidence: 1 migration with 4 self-assertions, 3 sabotages each firing only its target assertion, 1 manifest probe measuring local 4/4 and production 1/4, 1 battery guard forced red against production's exact state.* NOT DONE: w664 and w679 were not patched to revoke their own functions (w253 is that pattern and it is a good habit, but it is the rule that failed twice this week, so the guard is the control); and the manifest holds no probes for slices 646 to 701, 56 slices, pre-existing and recorded rather than closed. D-1320. *Evidence: 1 migration, 1 design doc.*
W703The register holds the method tooon production★★★ SIX OF THE NINE THINGS THE MASTER BRIEF'S CENTRAL RULE NAMES HAD NOWHERE TO LIVE. The rule is *no safety number without a source, every density, flow rate, walking speed, correction factor, clearance time, staffing ratio, equipment ratio, distance and threshold lives in the safety reference register*. W694 built that register with FIVE kind and unit pairs and they all describe what a CROWD does. Phase 4 is mostly about HOW THE METHOD COMPUTES, and the check constraint refused walking speeds, ground factors, event type factors, counter occupancy, the minimum route width, both halves of the stall width rule, the staffing ratio, equipment coverage areas and every separation distance. A Phase 4 author had two options, hard code them or invent a second register. ★★★ THIS WAS MY DECISION AND NOT THE OWNER'S. Q-SP-6 was raised by the figure dossier written the same night and is unanswered. I took option (a), one widened register, under the standing instruction to decide and record rather than stop. One table means one provenance discipline: source_standard and qualifier are already NOT NULL and length checked, so a walking speed cannot be stored anonymously any more than a density can, where two registers would double that machinery and give a future author two places to put a number. IT SUPPLIES NO FIGURE and production held 0 rows before and after. Assertion 1 matters most in THIS file, because a file that exists to make room for parameters is the one where supplying a plausible 65,95 would feel like finishing the job. ★★ WHAT IS DELIBERATELY NOT A KIND IS THE MORE INTERESTING HALF: escape_width_divisor, the 260 persons per metre the corpus implies, equals flow_rate multiplied by clearance_target and both are already kinds. A derivable quantity stored as a figure is a second copy that can disagree with the two it came from, and a third number for the owner to verify when verifying the two underneath it is the same work and is honest about what rests on what. ★★★ ASSERTION 3 IS THE ONE THAT EARNS THE SLICE. Assertions 1, 2 and 4 all pass against a table with NO KIND RULE AT ALL, because they only ask whether the catalogue round-trips. A widening that admits everything is not a widening, it is a deletion. Sabotage S1 is that exact mistake and a3 is what sees it. ★★ AND W694 ASSERTION 2b IS REPAIRED IN THE SAME SLICE BECAUSE THIS BREAKS IT: it read if n <> 5, a magic constant duplicating the size of the list it was checking, so W694 re-run after this would report 2b FAILED with nothing wrong. It now counts the catalogue. A check that must be edited whenever the thing it measures grows is a check that will one day be edited to match a mistake. *Evidence: 1 migration with 5 self-assertions, 3 sabotages each firing only its target assertions, chain order proved stable by re-running W694 alone (catalogue reverts to 5, 0 warnings) then W703 (restores 15), register holding 0 figures throughout.* NOT DONE: no route and no screen for the ten new kinds, and app.safety_figures_needed() still asks only about the crowd kinds because a zone does not yet demand a walking speed and inventing that demand would be guessing at the calculation before the calculation exists. D-1321. *Evidence: 1 migration, 1 design doc.*
W704An event says what kind of event it iscompleteSafety Planning Programme, Phase 1, first bullet. ★★★ THE EVENT SAFETY PROFILE: seventeen questions whose answers switch what a security plan says, from state or civil event to fenced, staged, near the fireworks and protected persons present. Each answer carries who gave it, because the brief is explicit that the software never decides which legal regime applies. ★★★ OD-01 TAKEN BY THE BUILDER, NOT THE OWNER: the safety schema exists because new tables in app are granted to mise_rw by default privileges, the W172 reason for crowd, and none of the eight Estate objects in kit file 04 section 10 is a profile. ★★ ONE ROW PER ANSWER, so provenance is NOT NULL beside the value. TYPED BY THE DATABASE: safety.profile_field holds each type and closed choice list, a trigger refuses anything else with EC683 naming the accepted values, and the screen reads the same list. ABSENT IS NOT NO. ★★★ GATE 4 ON THE READ AND EVERY WRITE, proved by a record ACL deny with the gate asserted by number. ★★ The first trigger let 2026-02-31 reach a failed cast, a 503 at the route, and check 2d exists for it. ★★ GUARDS WIDENED: w453 and w454 now see safety, and w507, the security_invoker guard on views, never covered crowd and now covers both. *Evidence: 1 migration with 5 assertions and 2 sabotages, suite 16/0 with 5 route sabotages each firing only its own check, web 508/0, battery 343 passed 0 failed, screen driven in a browser at 1280 and 360, apply-2026-09-30-w704-production.sh passing its type-rule control on production with 0 answers after, /health naming e3e5489 and the served screen chunk byte-identical to the build.* NOT DONE: derived and ai_draft provenance are storable and unwritten, no answer history. D-1322. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W705An event gets one safety plan, and the documents follow the profilecompleteSafety Planning Programme, Phase 1, second bullet. ★★★ ONE PLAN PER EVENT IS A UNIQUE CONSTRAINT, NOT A ROUTE CHECK. safety.start_plan inserts with ON CONFLICT DO NOTHING and returns the plan either way. Check 2a fires two starts concurrently and 2b goes round the route to require the table to refuse by name. ★★★ ONLY RULES THE KIT STATES ARE RULES. Five document types are in every corpus plan and always required, the separate fire documentation follows fire_doc_style, and five more are listed with no rule, so they are reported owner_decides and never created, because a guessed rule would put a document in front of an authority that nobody decided was needed (Q-612). An unanswered question makes its document PENDING and names it. ★★ A DOCUMENT THAT STOPS BEING REQUIRED IS KEPT, it may hold work. ★★ The first draft was not re-runnable, it dropped a key another key depends on. The first production apply printed FAIL on a correct control because format('%s', boolean) prints t. *Evidence: 1 migration with 5 assertions and 3 sabotages, suite 13/0 with 3 route sabotages and the constraint dropped, web 508/0, screen driven in a browser, apply-2026-09-30-w705-production.sh passing its control on production with 0 plans after.* NOT DONE: Phase 1's section lists and missing inputs, which need the template registry and OD-12. D-1323. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W706The definer guard looks past appon production★★★ THE BATTERY'S DEFINER GUARD ASKED ONLY ABOUT SCHEMA app, and W704 created safety the same day, so a PUBLIC definer function there would have passed every battery, W702's lesson one schema over. Both guards now sweep every schema except the two system ones and name the schema. ★★ MEASURED BEFORE WIDENING: definer functions live in app, estate, mise and av, none open on local, dev or production, and production carries the estate schema, which the brief calls dev only. ★ The shell guard printed app. in front of every offender and would have read app.safety.x. *Evidence: a planted safety.w706_sabotage() made the battery guard fail naming it and the shell guard exit 1, while the shell guard at the previous commit reported ok over the same database.* THIS ROW READS NOT BUILT AND IS BUILT: the change is to two guards, at app dfb2442, and this generator only counts a migration, an apply script or an acceptance suite as build evidence. D-1324. *Evidence: 1 design doc.*
W707A shape says what object it iscompleteSafety Planning Programme BL-SP-6. ★★★ 81 DRAWABLE OBJECT TYPES from kit file 04 section 2, in eight families, with names in both languages, the geometry each is drawn as, the roles each may carry and a security flag. THE TYPE SAYS WHAT A SHAPE IS, THE ROLE HOW THE CAPACITY FUNCTIONS TREAT IT, and they stay separate. ★★★ ONE RULE FROM BOTH TABLES: EC684 refuses a stage on a line AND a line marked as a fence becoming an exit, because a rule on one table is a rule the other table's route walks round. ★★ OBSTRUCTION IS ALLOWED ON EVERY AREA TYPE because it can only lower a capacity, and no type may bound a zone. ★★ A satellite in safety, not a column, because columns in app reach mise_rw. ★ The classify route drops the old type before the role moves, so a fence becomes a gate and an exit in one save. ★★ PRODUCTION HOLDS NO PLAN, so the apply control builds one inside a rolled-back transaction. *Evidence: 1 migration with 5 assertions and 3 sabotages, suite 12/0 with 4 route sabotages and the layout trigger dropped, web 508/0, driven in a browser, apply-2026-09-30-w707-production.sh reading stage=EC684 fence=accepted exit=EC684 on production.* NOT DONE: figure links, products, staffing templates, property schemas, enforcement of the security flag (OD-15). D-1325. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W708The crowd figure contradiction reaches the screen that asks no daycomplete★★★ W700'S CONTRADICTION COULD NEVER APPEAR ON THE SCREEN BUILT TO SHOW IT. The safety plan screen reads the forecast with no day, the resolver checks a day only when one is named, so its only possible refusal was EC681, *nothing recorded*, which the screen rendered under *These figures disagree*, in English, above an empty table. W701's suite asked with a day set and passed throughout. The route now returns every clashing day computed in SQL, and the screen warns once per day in the reader's language. *Evidence: suite 3/0, two sabotages of the clash rule, w701 still 11/0, web 508/0.* D-1326. *Evidence: acceptance suite, 1 design doc.*
W709The chain builds from empty again, and the ledger sees the last 58 sliceson productionBL-SP-3. ★★★ BOTH SPINE MANIFESTS STOPPED AT w645, so 40 migrations were unledgered and a from-empty build verified nothing about the crowd safety family. 40 load entries and 39 probes added, w667 skipped as content. ★★★ REPRODUCING THE CI JOB LOCALLY FOUND EIGHT DEFECTS: two files loading before what they read, w660 aborting without Estate's codes, and five statements or assertions true at their own position and false after a later slice, including w502, which on a re-run would have recorded two notification types as translated by W496 when they were not. Pass 1 and pass 2 now load clean and the ledger reads 582 of 582 on the empty database. ★★★ THEN PRODUCTION'S LEDGER FOUND W661 NEVER APPLIED, the free trial holding 0 entitlements while the build ledger called W661 complete. Applied, the owner's own Q-609 answer, 7 assertions passing, the one tenant untouched. THIS ROW READS NOT BUILT AND IS BUILT: its changes are to manifests and existing migrations, which this generator does not count. D-1327. *Evidence: 1 design doc.*
W710Third-level items go on top of the page, never in the panelon production★★★ THE OWNER: the panel's rows and icons changed as the reader walked down it. D-623 hid each child until its family was open, 6 rows at rest and 15 on the door. The panel lists top-level surfaces only, the parent stays lit, and the family is a SEGMENTED ROW on top of the page, because twelve family pages also carry a ?tab= bar. ★★ THE CONTROL ROOM IS THREE FAMILIES as the owner asked: ways in, issued and handed over (tickets, accreditation, vouchers, cloakroom), and the crowd. ★★ THE PORTAL-WIDE WALK of all eleven groups found constant rows and one lit entry everywhere except /messaging/quarantine, which lit two, now fixed. *Evidence: four nav-model checks over every group, two sabotages each red, web 512/0, the rendered walk.* D-1328. *Evidence: 1 design doc.*
W711The crowd report reads the open event and follows the design systemon production★★ THE OWNER: the event showed as a code. It was a free-text input holding the UUID, on a screen whose event is already open in the shell. The controls were bare browser inputs in a hand-rolled grid and the page had no rhythm. Now the event comes from the URL, the controls are Field components, and an event with no zones says so and links to the safety plan. NOT SEEN: the report body with data, since no local event has zones and counts. D-1329. *Evidence: 1 design doc.*
W712The plan answers what it showscompleteSafety Planning Programme, the derived provenance W704 stored and nothing wrote. ★★★ A PLACED STAGE ANSWERS has_stage, A CLOSED HIGH FENCE AROUND THE EVENT AREA ANSWERS fenced, per kit files 04 and 06, written by database triggers so no route has to remember. ★★★ THE PLAN ONLY EVER SAYS YES: no stage on a half-drawn plan is not evidence of no stage, so removing the evidence unanswers the question. ★★★ A STATED ANSWER IS NEVER OVERWRITTEN, and the read returns what the plan shows beside it, so the screen shows the disagreement. ★★ Gates, turnstiles and screening lanes close the ring, a cordon ring does not. ★ Withdrawing a stated answer lets the plan answer again. *Evidence: 1 migration with 5 assertions and 4 sabotages, suite 10/0 with 1 route sabotage and 3 triggers dropped, web 512/0, battery 54 350/0, 1280 and 360 in the browser, apply-2026-09-30-w712-production.sh reading stage=derived stated=kept open=unanswered on production, the served chunk byte-identical.* NOT DONE: stale document sections (needs OD-12), no snapping tolerance. D-1330. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W713A zone's report counts its own gatescompleteThe unverified half of the owner's W711 request, the crowd report body seen with data. ★★★ TOTAL ENTRANCES 15 965 OVER A DAILY TABLE SUMMING 14 573 ON THE SAME PAGE: the share and heatmap took the event, so a site's inner sector door counted people already inside, and a sector report showed every gate of the site. ★★★ ONE PERIMETER, the subtree zone_occupancy sums, read by share, heatmap, headline and daily table alike. ★★ A peak above capacity is said, counter drift is not printed as a crowd of minus five, the curve gains days, the heatmap a day row, the caption and percentages the reader's language, and the family row shows the page you are on at 360 px. *Evidence: 1 migration with 4 assertions and 3 sabotages, suite 6/0 with a route sabotage and the old daily rule, web 512/0, seen at 1280 and 360 on a two-evening fixture, apply-2026-09-30-w713-production.sh reading site=160 sector=50 daily=160 on production.* NOT DONE: the report route asks no permission (Q-613). D-1331. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W714An old file re-runs over a wide chaincompleteB-245, BL-SP-9. ★★★ RE-RUNNING w687, w688 OR w690 OVER THE WIDE W691 CHAIN FAILED AS is not unique, measured red for each from the committed file. Eight functions, not the three the handover named. ★★★ w685 AND w686 ALREADY RE-RAN CLEANLY BY DROPPING THE WIDE CHAIN THROUGH drop type ... cascade, and the migration ledger could not see the seven missing wide signatures. The three files now drop the wide signature before each narrow create, D-1332. Chain from w685 re-run 22/0 on local, acceptance w685 to w691 green, production read only and unchanged by design. *Evidence: 1 design doc.*
W715On a phone the page gets the whole widthcompleteThe second item of the 2026-09-30 afternoon handover, and the layout the owner's W710 and W711 requests were about. ★★★ EVERY PAGE WAS 248 PX WIDE AT 360, measured on the dashboard, the event list and the crowd report, because the icon rail and the collapsed panel kept 112 px. ★★ THE COLLAPSED PANEL NAMED ITS ICONS ONLY ON HOVER, and a phone has none. Below 640 px the rail and the labelled panel now open as one drawer from the topbar, the page behind it is inert, and the scrim is a Close button. D-1333. Guard 4/0 with four sabotages red, web 516/0. *Evidence: 1 design doc.*
W716The ledger reads a signaturecompleteB-246. ★★★ A RE-RUN OF w685 SWAPPED THE WIDE CAPACITY CHAIN FOR THE NARROW ONE AND THE LEDGER READ W691 PRESENT, because its only probe was a table and a function probe matched by name. A function probe with an argument list is now checked with to_regprocedure, D-1334, and the same state reads W691 MISSING. ★★★ THE FROM-EMPTY BUILD FAILED TWO w641 ASSERTION FILES, both from 2026-09-23, a C7 still asserting the rule W641.2 replaced and a C10 refused by row security on an empty database. Both repaired, red then green. Nobody saw it because GitHub Actions has started no run since 2026-09-23, Q-614. CI simulation clean on both passes, 389 checks. *Evidence: 1 design doc.*
W717A question can be a tablecompleteB-222, and it unblocks wave 3 of the October 23 contract intake. ★★★ THE FORM ENGINE HAD NOTHING TABLE-SHAPED, so eleven uses by four attributes would have been 44 fields or one over-broad consent. A ninth field type, grid: rows and typed columns in its options, one object per answered row in its answer. ★★★ THE DATABASE REFUSES CONSENT TO A USE THE TABLE NEVER ASKED ABOUT, EC685, because the Szjt. ties consent to named uses. The public page, the portal form and the builder handle it, and exports read it as words. D-1335. Suite 10/0 with three sabotages red, production control dup=refused extra=EC685 good=stored. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W718A question can say how to answer itcompleteWave 3 of the October 23 intake needed it. ★★★ THE APPROVED HELP LINE IS THE LEGAL POINT OF THE CONSENTS TABLE, "a use you leave empty is one you have not consented to", and app.form_field had nowhere to put it. A nullable help column, bounded, frozen with the rest of an answered question. Both public doors hand it to the page, which shows it under the label and ties it to the control with aria-describedby. D-1336. Suite 3/0, sabotaged red, migration assertions red with the doors and freeze sabotaged. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W719A form carries its privacy noticecompleteOWNER-ACTION-08. ★★★ THE CONTRACT INTAKE'S PRIVACY NOTICE HAD NOWHERE TO GO: no column, and neither public door handed the page anything but the questions. A nullable, bounded app.form.privacy_notice, on the form rather than the version and NOT frozen, because waves 1 and 2 are already answered and must show it to everyone who comes back. Both doors hand it to the page, which shows it above the questions on the emailed link, the ask-for-a-link page and the preview. The wording is the owner's and is not loaded: its loader refuses while a placeholder remains. D-1337. Suite 6/0, 4 red with the render removed, migration assertions 4 and 4b red with the doors unpatched. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W720The producer writes the privacy noticecompleteOWNER-ACTION-08. ★★ W719 PUT THE NOTICE ON THE PAGE AND ONLY A SCRIPT COULD WRITE IT. POST /forms/privacy-notice sets or removes it, refusing a missing key and anything outside 40 to 8000 characters with a sentence rather than a 23514, and a foreign form answers 404. The form builder gets an Add notice / Edit notice button per form, the word being the state, and a drawer that shows its own refusal because the page's copy sat behind it. D-1338. Suite 8/0, 4 red with the route storing nothing and the list dropping the column. Walked in the browser: refused, saved, shown on the public page, removed. *Evidence: acceptance suite, 1 design doc.*
W721An answer remembers the notice it was given undercompleteD-1337's ceiling, closed. ★★★ THE NOTICE ON A FORM CAN BE EDITED, AND WHAT A PERSON WAS TOLD MUST NOT MOVE WITH IT. app.form_response.privacy_notice_shown, stamped by a trigger at the one moment every route shares, the transition into submitted, with the form's notice at that moment. A caller cannot supply it, a draft cannot carry it, and once submitted it cannot be changed (EC686). Production had 24 responses started and none submitted, so nothing needed backfilling. D-1339. Suite 4/0 over the performer's answer path, 3 red with the trigger disabled, migration assertions 5 red without it. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W722The organiser reads what the person was toldcomplete★★ W721'S STAMP WAS EVIDENCE NOBODY COULD REACH EXCEPT FROM THE DATABASE. /forms/response carries privacy_notice_shown, and the builder's One response panel shows it in the app's own Fold, as it read when the answer was sent, or a line saying the form showed none. A draft shows nothing rather than implying the current notice. D-1340. Suite 2/0, 1 red with the column nulled in the route. Walked in the browser: the first cut used a bare <details class=card>, which the design system does not skin, and was replaced by Fold. *Evidence: acceptance suite, 1 design doc.*
W723The safety tables live in appon production★★★ Q-611: SCHEMA safety IS GONE. Seven tables, nine functions and every constraint and index now sit in app prefixed safety_, forced by app.plan and app.document already existing. W704, W705, W707 and W712 create there directly; W723 moved production's. The rehearsal went red on a real defect, a trigger branching on tg_table_name = 'feature_object'. D-1343. *Evidence: 1 migration, 1 design doc.*
W724The owner says when each document is requiredcomplete★★ Q-612: NO SAFETY DOCUMENT IS LEFT FOR THE OWNER TO DECIDE. Escape maps, crisis communication and the authority pack always; weather when outdoor over threshold; traffic when leaving by car. Suite 7/0, 0/7 with the old rules back. D-1344. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W725A crowd report is read by whoever may view the eventcomplete★★★ Q-613: /crowd/report REQUIRES events.view ON THE ZONE'S OWN EVENT. The route took event_id and never used it, so the gate reads the zone's event and a crossed pair is 404. External member, gate 4 deny and crossed event all red without the gate. D-1345. *Evidence: acceptance suite, 1 design doc.*
W726Money moves only the way the rules saycomplete★★★ CONTROL PROFILES S0, H1 H2 H3 H4 H7 H8 H11, HELD BY TRIGGERS. The named approver decides in order (EC687, EC688), no approved budget with an open step (EC689), the invoice state machine (EC690), an issued invoice is fixed (EC691), actuals in the change ledger, invoice numbers unique per supplier. Every database refusal now reaches the caller as a 409. Control 14/14, suite 12/0 and 1/11 with the triggers off. D-1354. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W727A spend limit caps the spend, not the capabilitycomplete★★ CONTROL PROFILES S0, H5. No route passed an amount, so any limit banned the whole capability. Now no amount means no spend, and the five money routes pass spendOf, kept so by a source survey. Suite 5/0, 2/3 with the old rule back. D-1355. *Evidence: acceptance suite, 1 design doc.*
W728A project declares its control profilecomplete★★★ CONTROL PROFILES S1. Role, procurement regime and public funding at creation; an undeclared row takes the workspace default, not reviewed; EC692 for a forbidden combination, EC693 for loosening after money is committed, which becomes a two-approver request. Control 12/12, suite 15/0. D-1356. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W729One rule, one definition, a severity per profileon production★★★ CONTROL PROFILES S2. One resolver, 52 rules, a severity per profile key, public funding overlaid. Settings, relaxations and justifications are each two other people acting. Commitments keep their profile. Control 13/13. D-1357. *Evidence: 1 migration, 1 design doc.*
W730The one who asks is not the one who approveson production★★ CONTROL PROFILES S3, R-80. An order is not issued by its creator (EC696); a one-person workspace and a private event log a self-approval instead. Control 6/6. D-1358. *Evidence: 1 migration, 1 design doc.*
W731What was received is what is paidcomplete★★★ CONTROL PROFILES S4. Receipts, the three-way match, R-28 at payment (EC698), the receiver not paying. Control 8/8, suite 8/0. D-1359. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W732The base signalscomplete★★ CONTROL PROFILES S5. One gate for every rule: block refuses (EC699), justify holds the order for two people. Ten rules wired. Its control found that a retry raised a fresh finding and could never be released. Control 12/12. D-1360. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W733Revenue has its own controlson production★★ CONTROL PROFILES S6. R-50 no sale without a revenue agreement, R-51 a coupon over ten percent is held, R-53 complimentary quota. R-52 not built. Control 8/8. D-1361. *Evidence: 1 migration, 1 design doc.*
W734In-house rules by value bandon production★★ CONTROL PROFILES S7. Four platform value bands, unconfirmed and editable by a two-person request: quotes, own estimate, conflict-of-interest declaration before issue. Control 9/9. D-1362. *Evidence: 1 migration, 1 design doc.*
W735The framework and its price tableon production★★ CONTROL PROFILES S8. Frameworks, renewals, price-table versions and call-offs. R-26, R-07, R-15, R-16, R-17 at issue under a call-off, R-06 as a report. Importer loaded 3 393 RKM24 items. Control 8/8. D-1363. *Evidence: 1 migration, 1 design doc.*
W736A certificate is generated from evidencecomplete★★★ CONTROL PROFILES S9. The performance certificate is generated by one function from evidence with an independence level, never filled in. R-21 refuses it without the matrix's level, R-24 holds a same-day one, it certifies the counted quantity. Blind count on a phone. Control 10/10. D-1364. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W737A fee is earned only on what was passed throughcomplete★★★ CONTROL PROFILES S10. Fee derivation per event, base the supplier's price, rate the framework's by item area, R-35 to R-39, fee invoice paid after close. R-39 reproduces the report's chapter 6.4 method. Control 9/9. D-1366. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W738Public money has a grant, a basis and a calendarcomplete★★ CONTROL PROFILES S12 PART ONE. Grant and amendments; R-41 grant cover, R-70 legal basis, R-72 campaign period at an order's issue, R-42 fast amendment. Control 8/8. D-1367. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W739Money passed on is accounted forcomplete★★ CONTROL PROFILES S12 PART TWO. Pass-through ledger, releases against guarantees, Ávr. 100 sample size, recipient risk score with reasons, staff time, policy-change log, itemised export with security_invoker. R-60, R-61, R-62. Control 9/9. D-1368. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W740Every report is named and follows the profileon production★★ CONTROL PROFILES S13. T1 to T7 and P1 to P11 in one function, W615's shape, computed not stored, run as the caller, each report only where the profile switches its layer on. Control 7/7. D-1369. *Evidence: 1 migration, 1 design doc.*
W741A price table is drafted, bid on and awardedon production★★ PRICE TABLES. A version is a catalogue, a supplier's bid or the awarded table, and a draft or final; a draft's items change, a final one's never; a bid prices only the list's items. The NRÜ 2026 tender lists load as drafts. Control 7/7. D-1373. *Evidence: 1 migration.*
W742A framework has partners, periods and limitscomplete★★★ VERSION 3, S8. R-100 to R-124 registered; periods, partners, extensions and increases within KM limits, call-off route and condition, the contractor's confirmation. Control 12/12. D-1375, D-1376. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W743An item keeps its identity across versionson production★★★ VERSION 3, S8. A version-independent price item, the diff between versions, canonical units, KM 9.1.5's reference resolved by sheet name. Measured on the real NRÜ lists. Control 7/7. D-1375. *Evidence: 1 migration, 1 design doc.*
W744A subcontractor is cleared before it workson production★★ VERSION 3, S8. KM 7.12 undertaking and KM 7.11 sanctions check before a subcontractor is active, the frame status, contract contacts with validity. Control 7/7. D-1377. *Evidence: 1 migration, 1 design doc.*
W745A maximum price rests on market researchcomplete★★★ VERSION 3, S17. Campaigns, quotes with sources, figures leaving related respondents out, the method as data, two approvals before a maximum price lands, R-118 screening. Control 9/9. D-1378. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W746A commitment is proven, not promisedcomplete★★ VERSION 3, S16. Commitments with evidence, approved substitutions, the trip log and its computed green share, R-108 and R-109 at the certificate, H3. Special data store waits for Q-630. Control 8/8. D-1379. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W747A fee follows KM 9.1.4on production★★ VERSION 3, S10. Event-specific beside passed-through, prior approval by another person, three quotes or an exception, the 10 percent caps, no fee on a price-table item. Control 8/8. D-1380. *Evidence: 1 migration, 1 design doc.*
W748Measured lines and categories are provenon production★★ VERSION 3, S9. R-107 transport certified on logged km, R-123 category proof against a definition, the electronic invoice format (KM 9.4.4), works and their rights date (KM 14). Control 6/6. D-1381. *Evidence: 1 migration, 1 design doc.*
W749Securities, penalties and advances are ledgerscomplete★★ VERSION 3, S18. KM 9.2 advance and guarantee limits, the delay penalty net of force majeure and capped, the third delay penalty escalates (R-115), late notices (R-117), the insurance and advance watches (R-116, R-120), H4. Control 9/9, route suite 4/4. D-1382. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W750An event has an approved categorycomplete★ VERSION 3, S1. Event categories approved by two other people, an event's category inside its workspace, R-20 and R-21's high-value threshold from the category. Control 6/6, route suite 4/4. D-1383. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W751The H reports follow the profileon production★ VERSION 3, S13. H3, H4, H6 and the new H7 framework timeline in control_reports, H4 for every profile and the rest KBT only. H1, H2 wait on S15 (Q-623), H5 on Q-638. Control 5/5. D-1384. *Evidence: 1 migration, 1 design doc.*
W752The sample is drawn after the reporton production★★ VERSION 2, S9 REMAINDER. MUS sampling: the seal commits to sha256 of its seed, the key lines covering 60 percent are checked in full, a systematic monetary-unit sample of the rest needs independent counts (refused as M4 step 8 where R-20 runs), and the certificate reveals the seed so anyone can recompute the sample. Control 7/7, W736 suite check 6b. D-1386. *Evidence: 1 migration, 1 design doc.*
W753An order is issued from the approved offercomplete★★ VERSION 2, S4 REMAINDER. Offer versions fixed when received, approval by name writes the lines onto the draft order, an order with offers is issued only from its approved version and exactly its lines, R-10 holds a fast order, R-11 records an unnegotiated one, and the negotiation log is append-only. Control 8/8, route suite 4/4. D-1387. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W754Three quotes are three independent bidderson production★ VERSION 2, S11 REMAINDER. A quote names its organisation; the in-house quote count counts each bidder once and none declared related, so three quotes mean three independent bidders. Control 4/4. D-1388. *Evidence: 1 migration, 1 design doc.*
W755A second checker recounts part of the sampleon production★ VERSION 2, S9 REMAINDER. A fifth of the sampled lines, chosen from the seed, need independent counts by two different people before the certificate (M4 step 8). Control 5/5. D-1389. *Evidence: 1 migration, 1 design doc.*
W756A second level reviews a sample of certificatescomplete★ VERSION 2, S9 REMAINDER. High-value certificates drawn from the seed for a review by someone who took no part in the check; findings need a note; a queue of the unreviewed. Control 5/5, route suite 3/3. D-1391. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W757A deficiency is named, and a line can be disputedcomplete★ VERSION 2, S9 REMAINDER. An itemised deficiency notice on receipt from the evidence matrix, R-23 holding the certificate while a gap is open, the acceptance deadline from receipt or, where the contract allows, from completion, and line-by-line partial acceptance with reasons. Control 7/7, route suite 3/3. D-1393. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W758The checker is drawn on the day, and rotatescomplete★ VERSION 2, S9 REMAINDER, THE LAST. A checker pool per event, a random draw per supplier for today only, never last time's checker, and T2's roster flag for high-value events whose pool cannot rotate. Control 7/7, route suite 3/3. D-1395. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W759A price is compared with the marketon production★ VERSION 2, S13 P4 AND R-01, R-02. Order lines priced from a price-table item are compared with the market research median: R-02 at 3 times, R-01 at 1.5 times, at issue and in the preflight; P4 reports every issued line against its reference. D-1398. *Evidence: 1 migration, 1 design doc.*
W760The contractor is told what is missingon production★ VERSION 2, S9 FOLLOW-UP. New deficiencies send the contractor's user one notice per report through the notification inbox and e-mail. Control 3/3. D-1400. *Evidence: 1 migration, 1 design doc.*
W761The in-house value bands are confirmedon productionOwner-confirmed in-house bands as new platform rows; the unconfirmed ones stay as the record. Control 3/3. D-1407. *Evidence: 1 migration, 1 design doc.*
W762An order stays within its approved budgeton production★★ R-41's BUDGET HALF. The event needs a budget in force whose cost covers its issued orders, and an attached budget line may not be overspent; R-41 at its severity. Control 5/5. D-1406, D-1410. *Evidence: 1 migration, 1 design doc.*
W764What the EKR must be told, and by whencomplete★ VERSION 3, S13 H5. Certificates and active subcontractors to publish in the EKR, due by a workspace deadline that has no default, recorded when published. Control 5/5, route suite 3/3. D-1403. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W765A reopened competition is prepared and recordedcomplete★★★ VERSION 3, S15 CORE. The channel decision (R-119), the narrowed table, weighted criteria, offers checked against each partner's framework price (R-100) and the maximum price (R-101), a quantity-weighted evaluation (R-102, R-118) and the award as a call-off contract. Control 8/8. D-1412. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W766Channels and efficiency are reportedon production★★ VERSION 3, S13 H1 AND H2, AND S15'S PAGE. H1 compares each line's best offer with the market and the maximum price; H2 the saving, offers, spread, framework-price match, winners' shares and concentration, naming no bidder. The Procedure page prepares, records, evaluates and awards a reopened competition. Control 5/5, route suite 5/5. D-1414. *Evidence: 1 migration, 1 design doc.*
W767Every event has a sustainability impact surveycomplete★★ OWNER REQUEST OF 2026-10-01. Every event gets its impact survey from a 14-question template (the owner's MotoGP circuit survey, worded for any event) the first time it is opened. The workspace rewords or removes questions until the first answer; fills it in at once; or sends partners a link of 1 to 90 days and 1 to 50 uses, every use adding to one response, and revokes it. Control 7/7, route suite 6/6. D-1416. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W768An ESG figure says where it came fromcomplete★★ M-17 BEGINS, D-103. A figure for an event carries its indicator from a shared library, its value in the indicator's unit, one of six source types, the measured, estimated or declared label, the methodology, the confidence and its submitter. It is reviewed by someone else, locked once reviewed, and a locked figure changes only with a reason, every change kept. The Sustainability page records, reviews, locks and amends. Control 7/7, route suite 5/5. D-1418. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W769A footprint shows its workingcomplete★★ M-17, D-104 AND D-024. A factor set names its source, its date and its version, and once published never changes. An event is counted with a published set, and its footprint lists every figure beside its factor, scope, set and source; a figure with no factor is shown and not counted. No factor value is shipped. Control 5/5, route suite 4/4. D-1419. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W770An event has a sustainability plan, and what was done is keptcomplete★★ M-17, D-024 AND D-104. A workspace's requirements say where they come from (a standard, a client, a tender, itself) and are versioned, never reworded. An event adopts them with a target, an owner and a status, and a missed or dropped target needs a justification. The action log only grows; a correction names what it corrects. Deleting an event takes all of it, and W768's locked figures, with it. Control 6/6, route suite 3/3. D-1420. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W771An ESG report is signed off, and each reader gets their cutcomplete★★ M-17, D-105 AND AR-48. Readiness says what stands between an event and sign-off: an unreviewed figure blocks, and a missing core indicator, a figure below 50 percent confidence and an open finding are flagged. Sign-off locks every figure and keeps its flags. A finding closes only on a corrective action. Section 4's seven readers each get their cut as JSON or CSV, the public one only after sign-off. Control 6/6, route suite 5/5. D-1421. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W772A figure and an action carry their evidencecomplete★ M-17, D-103 AND D-024: EVIDENCE IS A FILE. The Sustainability page uploads a file to the event and names it on a figure or an action; the page and the auditor's trail name it, and opening it goes through the file model's own permission. No migration: W768 and W770's composite keys already refuse another workspace's file. Route suite 2/2. D-1424. *Evidence: acceptance suite, 1 design doc.*
W773Events are compared on their footprintcomplete★ M-17, B-093. The workspace's events side by side from reviewed figures only: kg CO2e with the set each was counted with, per attendee where the attendance is known, waste per attendee, the diverted and renewable shares, and whether each is signed off. A missing figure or attendance is an empty cell, never a zero. Control 4/4, route suite 1/1. D-1425. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W774A supplier declares its figures by linkcomplete★★ M-17, AR-47. A request to a supplier for chosen indicators becomes a form of its own, opened by W767's limited link. The supplier's answers are imported once as declared, unreviewed figures that name the supplier and its method, and someone other than the importer reviews them. Control 5/5, route suite 3/3. D-1426. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W775An event is assessed level by levelcomplete★★ M-17, D-104, FROM THE OWNER'S MOTOGP EVALUATION 2025. A workspace imports a checklist as pasted rows of topic and Level 1 to 4 wording. An event is assessed against it at a target level: each topic's reach (criteria met over its Level 4 criteria) gives Level 1 to 4, and the event PASS PLUS, PASS, PASS MINUS or NO PASS by the sheet's PASS Criteria. The owner's 50-row checklist imports exactly. Control 6/6, route suite 3/3. D-1429. *Evidence: 1 migration, acceptance suite, 1 design doc.*
W776Every area has an ownercomplete★ M-17, D-103: OWNERSHIP IS BY WORKSTREAM. Each area of an event's sustainability (energy, water, waste, transport, catering, materials, accessibility, social, governance) is owned by a member of the workspace or an outside party with a contact, and an area with a core indicator and no owner is flagged before sign-off. Control 4/4, route suite 3/3. D-1430. *Evidence: 1 migration, acceptance suite, 1 design doc.*

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 event-clinic-app/api/test/w<n>-acceptance.mjs 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. 22 of the 49 slices have such a suite. The other 26 are on production with nothing proving them in isolation, and the page says so rather than calling them complete. A migration reaching production is a deployment, not a proof.

Run every gate at once:
cd ~/Documents/event-clinic-app/api && for f in test/w*-acceptance.mjs; do node "$f" || break; done

Run it locally

# 1 · API worker against LOCAL Postgres, with the demo auth bypass
preview_start portal-api-demo        # :8787, wrangler dev --var DEMO_AUTH:true
# 2 · the portal
preview_start portal-web-localpg     # :5173
# measure what the three databases actually carry — never read it from a doc
cd ~/Documents/ec-q341 && ./04-data-architecture/spine/migration-ledger.sh local   # then dev, then prod
# regenerate this ledger after building a slice
cd ~/Documents/ec-q341 && python3 07-delivery/build-ledger.py

Real router paths, so every link below is a genuine deep link. Use port 5173, NOT the 5183 harness entry — the API's ALLOWED_ORIGIN is 5173, so on 5183 every tile silently renders a dash and nothing is logged. Screens need VITE_DEMO_USER set plus the portal-api-demo worker, or the auth gate holds them shut.

Screens with data

Screen
Open at
What is on it
Portal home
landing
http://localhost:5173/
The shell. Its 360px collapse was fixed across all 48 screens on 2026-08-03
Events
W13 · data
http://localhost:5173/events
The event and event space spine
Event wizard
W40 · data
http://localhost:5173/events/wizard
Guided event creation. Its tasks arrive undated — a build action has no date of its own
Marketing plan
W44 · data
http://localhost:5173/events/marketing-plan
A dated marketing calendar generated from an event. Export is per track and opt-in: 44 lines arriving unasked would bury the real work
Projects
W19 · data
http://localhost:5173/projects
app.project, the container above the event
Venues
W21/W42/W45 · data
http://localhost:5173/venues
The venue tree, the bulk import, and venue media now served from R2
Companies
data
http://localhost:5173/companies
Platform-scoped — company has no tenant_id; company_claim is the only link
Contacts
W20 · data
http://localhost:5173/contacts
The person and contact directory
Agenda / run of show
W22 · data
http://localhost:5173/agenda
The three-level tree: agenda, segment, session line item
Workforce
W23 · data
http://localhost:5173/workforce
Eight tables, including the participation join E-438 revealed
Performers
W24 · data
http://localhost:5173/performers
Event-level talent resolving to a directory person under D-008
Guests
W25 · data
http://localhost:5173/guests
Shared attendee record by flag, invitations, RSVP
My tasks / checklists
W26 · data
http://localhost:5173/tasks/mine
Also /checklists. One search field across all ten target types — D-385
Budgets
W27 · data
http://localhost:5173/budgets
The cross-domain hub, wired to the quote lines. Split four ways
Files / documents
W28 · data
http://localhost:5173/files
Also /documents. The D-012 one-file model and the D-016 polymorphic attachment
Inventory / assets / stock
W29 · data
http://localhost:5173/inventory
Also /assets and /stock/where. The three-tier chain: product, inventory item, asset
Messaging & the Inbox
W31 · data
http://localhost:5173/messaging
The record thread and activity log. Split ten ways, W31 through W31.9
Notifications
W32 · data
http://localhost:5173/notifications
Applied to production on the owner's explicit instruction
Quotes
W5–W10 · data
http://localhost:5173/quotes/lines
Also /quotes/new and /quotes/proposed. Quote pricing, authoring and supplier-proposed lines
Procurement
data
http://localhost:5173/procurement
The buy side
Settings / numbering / me
W15/W18 · config
http://localhost:5173/settings
Theme and user preference. Also /numbering and /me
★ Public directory (W30)
LIVE IN PRODUCTION · no login
https://api.event.clinic/directory/v1
Not a screen — the API other products read. Procurevent and the wedding portal consume it. Paged, capped at 100, wildcard CORS, s-maxage=900, served by the app_public role. Cache-bust when verifying a deploy or the edge will show you the old answer
Dashboard roll-up
W33 · partial
http://localhost:5173/
GET /dashboard/summary exists and has its own suite, but that suite calls itself *a correction, not a slice*. The reporting essentials have no implementation