diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8141309430..1e4b1c9cd1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -803,10 +803,12 @@ jobs: # The seven Ward Flow browser journeys, and this lane BLOCKS. # # They live in the `chromium-mockups` project, which `test:e2e:pr` excludes with - # `--grep-invert @mockup`, so before this job the only lane that ran them was - # `ui-advisory` below — and that carries `continue-on-error: true`. A broken ward - # journey was therefore executed, reported, and passed anyway. Nothing in the - # repository could go red because of them. + # `--grep-invert @mockup`, so the only other lane running them is `ui-advisory` + # below — and that carries `continue-on-error: true`. A broken ward journey was + # therefore executed, reported, and passed anyway; nothing in the repository could + # go red because of them. Not hypothetical: MERGE 01-03 (2026-09-05) turned five + # ward routes into redirect stubs, these journeys went red, and the drift sat + # unnoticed for a day because no required check could see it. # # They are kept separate from `ui-advisory` rather than by removing its # `continue-on-error`, because that lane also runs `@quarantine` — tests that are @@ -814,31 +816,24 @@ jobs: # None of the seven is quarantined and all seven are tagged `@mockup`; checked # against `tests/flake-ledger.json`, which holds one entry and it is not a ward spec. # - # 🔴 INERT UNTIL SOMEBODY TURNS IT ON, AND THAT IS THE WHOLE SAFETY PROPERTY. + # ENABLED 2026-09-06 by owner decision. Until then this job was gated behind + # `vars.WARD_JOURNEYS_BLOCKING`, a repository variable that was never set, so it + # always skipped. That gate existed for a real reason — an already-red lane made + # blocking stops every UI pull request in the REPOSITORY, a blast radius entirely + # outside Ward Flow — and it was to be lifted only with a green run in hand. That + # run: `npm run test:e2e:ward-journeys` at 42c4dec, `3 skipped, 71 passed`. The + # three skips are the parked morning-page pair (see `tests/ui-ward-morning.spec.ts`) + # and one inconclusive forced-colours probe; none is a failure. # - # These seven have never gated anything, so nothing has kept them passing. A lane - # that is already red, made blocking, stops every UI pull request in the REPOSITORY - # — a blast radius entirely outside Ward Flow. The first draft relied on a comment - # saying "do not merge until they are seen green once", and that was not good - # enough: this reaches `main` inside a ward pull request measured at 1,629 commits - # and 429 files, of which `ci.yml` is one. A reviewer approving 429 files of ward - # work is not making a decision about CI policy; they are not even seeing one. - # - # So merging it does NOTHING. `vars.WARD_JOURNEYS_BLOCKING` is unset by default, the - # job is skipped, and `pr-required` records it as skipped-and-fine. Turning it on is - # one deliberate act in repository settings, at the moment somebody has a green run - # of the seven in front of them. - # - # ⚠️ A rule enforced only by a comment is not enforced. Splitting this onto its own - # branch was considered and is weaker — it still depends on somebody remembering why - # the branch is separate. This removes the dependency on anyone reading anything. + # ⚠️ The `pr-required` aggregate carries the matching `require_success` entry and + # its condition mirrors this `if:` exactly. The two have to move together; a future + # edit to one is an edit to both. ui-ward-journeys: name: Ward Flow browser journeys needs: changes if: > - vars.WARD_JOURNEYS_BLOCKING == 'true' && - github.event_name == 'pull_request' && needs.changes.outputs.ui_changed == 'true' && + (github.event_name == 'pull_request' || github.event_name == 'merge_group') && github.event.pull_request.draft != true runs-on: ubuntu-24.04 timeout-minutes: 30 @@ -1372,7 +1367,6 @@ jobs: UI_FAST_RESULT: ${{ needs.ui-critical-fast.result }} UI_RESULT: ${{ needs.ui-critical.result }} WARD_JOURNEYS_RESULT: ${{ needs.ui-ward-journeys.result }} - WARD_JOURNEYS_BLOCKING: ${{ vars.WARD_JOURNEYS_BLOCKING }} LIGHTHOUSE_RESULT: ${{ needs.lighthouse-budget.result }} DB_RESULT: ${{ needs.db-reset-verify.result }} CARING_CONTACTS_DB_RESULT: ${{ needs.caring-contacts-db.result }} @@ -1488,13 +1482,14 @@ jobs: # The seven Ward Flow browser journeys. Required here as well as blocking in their own # job: without an entry in this aggregate a failing lane is reported and merged past, # which is the same hole `continue-on-error: true` left. Gated to match that job's own - # `if:` exactly — pull_request, ui_changed, not draft — so an out-of-scope run reports + # `if:` exactly — pull_request|merge_group, ui_changed, not draft — so an out-of-scope run reports # skipped-and-fine rather than a false "result was skipped" failure. - # ⚠️ `WARD_JOURNEYS_BLOCKING` FIRST, and it mirrors the job's own `if:` exactly. Without it - # here, the aggregate would demand success from a job the workflow skipped by design, and - # the flag would fail closed on every UI pull request — the opposite of inert. The two - # conditions have to move together; a future edit to one is an edit to both. - if [ "$WARD_JOURNEYS_BLOCKING" = "true" ] && [ "$UI_CHANGED" = "true" ] && [ "$PR_DRAFT" != "true" ] && [ "$EVENT_NAME" = "pull_request" ]; then + # ⚠️ This mirrors the job's own `if:` exactly. The two conditions have to move together; + # a future edit to one is an edit to both. Both carried a leading + # `WARD_JOURNEYS_BLOCKING` term until 2026-09-06, when the lane was enabled and the term + # was dropped from both in one commit — see that job's own comment for the green run it + # was lifted against. + if [ "$UI_CHANGED" = "true" ] && [ "$PR_DRAFT" != "true" ] && { [ "$EVENT_NAME" = "pull_request" ] || [ "$EVENT_NAME" = "merge_group" ]; }; then require_success "ward-flow-journeys" "$WARD_JOURNEYS_RESULT" else require_skipped_or_success "ward-flow-journeys" "$WARD_JOURNEYS_RESULT" diff --git a/docs/outstanding-issues-inbox/443423fd-e6c5-4770-9453-c17ea545bedb.json b/docs/outstanding-issues-inbox/443423fd-e6c5-4770-9453-c17ea545bedb.json new file mode 100644 index 0000000000..ac17f342bf --- /dev/null +++ b/docs/outstanding-issues-inbox/443423fd-e6c5-4770-9453-c17ea545bedb.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "443423fd-e6c5-4770-9453-c17ea545bedb", + "createdOn": "2026-09-09", + "action": "add", + "payload": { + "pri": "P2", + "type": "issue", + "summary": "Ward Flow: the Command mockup emits ten eligibility gates, the engine twelve, and nothing keeps the two lists together", + "detail": "FOUND 2026-09-09. Command's gatesFor() emits allocatable_bed, capability, security, sex_mix, acuity, specialling, authorisation, catchment, prior_decline, freshness. ELIGIBILITY_GATES in ward-eligibility.ts is age, allocatable_bed, authorisation, capacity_freshness, cohort, forensic, legal_status, prior_decline, security, sex_designation, sex_mix, specialling. THE DIFFERENCES: the engine carries age, forensic and legal_status, which the mockup has no equivalent of; it splits sex_designation out as its own gate where the mockup folds designation into sex_mix; and it has no acuity or catchment gate, both of which the mockup shows to the coordinator as first-class checks with their own sentences. 'capability' and 'cohort' are the same check under two names. WHY IT MATTERS: the third-edition standard governs colour, type, wording and behaviour in close detail and says nothing at all about the rule model, so there is no mechanism holding the mockup's clinical logic to the engine's. The two have already drifted in both directions, and the mockup is what a clinician is shown when the design is reviewed. NEXT ACTION: add a short subsection to section 8 naming ELIGIBILITY_GATES as the source of truth for what a gate is, and one line in the definition of done requiring a mockup's gate list to be a subset of it under the engine's own names. Cheap to state, and it stops the next mockup inventing an eleventh gate.", + "source": "Artifact review 2026-09-09 (d821a155 vs ward-eligibility.ts)", + "issueUlid": "01M22T8P4BS6PNK999E9RGZ79W" + } +} diff --git a/docs/outstanding-issues-inbox/50590ddf-5cb2-4e57-b4ca-7925796a99a2.json b/docs/outstanding-issues-inbox/50590ddf-5cb2-4e57-b4ca-7925796a99a2.json new file mode 100644 index 0000000000..f711aba384 --- /dev/null +++ b/docs/outstanding-issues-inbox/50590ddf-5cb2-4e57-b4ca-7925796a99a2.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "50590ddf-5cb2-4e57-b4ca-7925796a99a2", + "createdOn": "2026-09-09", + "action": "add", + "payload": { + "pri": "P1", + "type": "issue", + "summary": "Ward Flow: enforce hospital authorisation as a candidate destination constraint", + "detail": "FOUND 2026-09-09 by review of artifacts d821a155 and e7f7b6f2 against src/components/ward-management/. Both artifacts omit authorisation from ABSOLUTE_GATES, and ward-flow-reducer.ts places it in SUITABILITY_GATES. The canonical destination contract in docs/ward-flow-context.md and docs/ward-management-decisions.md ADR 1 distinguishes lawful detention under Forms 3A/3B in an unauthorised general hospital emergency department from eligibility for an involuntary inpatient destination. A patient's current-location detention must not be presented as unlawful merely because that site is unauthorised. Candidate involuntary inpatient destinations must satisfy the hospital-authorisation requirement; a coordinator's recorded reason cannot waive it. NEXT ACTION: reconcile the reducer and mockup destination gates with that existing contract, keep the current-location and destination cases separate in tests, and document the distinction beside the gate. The separate security/locked-bed classification still needs assessment against the actual destination requirements; it is not resolved by this authorisation finding.", + "source": "Artifact review 2026-09-09 (d821a155, e7f7b6f2, ward-flow-reducer.ts)", + "issueUlid": "01M22T7W66MF6MZ7NDHPMMMANG" + } +} diff --git a/docs/outstanding-issues-inbox/6273682d-423c-4b2f-8b99-00c5ec4e9b15.json b/docs/outstanding-issues-inbox/6273682d-423c-4b2f-8b99-00c5ec4e9b15.json new file mode 100644 index 0000000000..01ef66b89d --- /dev/null +++ b/docs/outstanding-issues-inbox/6273682d-423c-4b2f-8b99-00c5ec4e9b15.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "6273682d-423c-4b2f-8b99-00c5ec4e9b15", + "createdOn": "2026-09-09", + "action": "add", + "payload": { + "pri": "P2", + "type": "issue", + "summary": "ward-flow-reducer.ts contradicts itself on capacity_freshness: its comment calls it a non-overridable world fact, its array lists it as overridable", + "detail": "FOUND 2026-09-09. In src/components/ward-management/ward-flow-reducer.ts, the classification comment at line 440 reads: 'THE WORLD — is there a bed at all? allocatable_bed, capacity_freshness, specialling. Already refused separately, above, and NOT overridable: no reason typed into a form creates a bed.' Forty-seven lines below, SUITABILITY_GATES includes capacity_freshness, carrying its own comment that it was ADDED BY OWNER RULING on 2026-09-02 and is the only gate ever moved out of the world-fact group, on the reasoning that a stale bed count is information rather than a wall and 'I have confirmed the current bed state with the ward directly' is a named person taking responsibility. The array is correct and current; the comment above it was not updated when the ruling landed. WHY IT MATTERS: the two sit in one file and give opposite answers to 'can a recorded reason get past a stale bed count'. The prose comment is the part a reader meets first and is written in the tone of a safety boundary, so it is the one that gets believed. It has already propagated: ledger row #Q6WD1M repeats the stale grouping in its reconstructed detail, which means the error is now in two places. NEXT ACTION: correct line 440 to list capacity_freshness with the suitability gates and note the ruling, then correct the same sentence in #Q6WD1M. A one-line change, but it removes a documented contradiction inside a clinical-safety classification.", + "source": "Artifact review 2026-09-09 (ward-flow-reducer.ts lines 440 and 487)", + "issueUlid": "01M22T8PEHKHGJ127YYBQ6XJQ7" + } +} diff --git a/docs/outstanding-issues-inbox/6fbe61cf-75fc-4c2e-b003-22b51f66a734.json b/docs/outstanding-issues-inbox/6fbe61cf-75fc-4c2e-b003-22b51f66a734.json new file mode 100644 index 0000000000..4eff6ffca1 --- /dev/null +++ b/docs/outstanding-issues-inbox/6fbe61cf-75fc-4c2e-b003-22b51f66a734.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "6fbe61cf-75fc-4c2e-b003-22b51f66a734", + "createdOn": "2026-09-09", + "action": "update", + "payload": { + "id": "#G4YPNE", + "detail": "Unchanged and unchangeable from inside the project: it requires Aboriginal health practitioners. Recorded here as a Ward Flow gate in its own right rather than relying on the Caring Contacts entry (#1S81R8), because Ward Flow is a separate surface — bed allocation, transport and detention-adjacent legal status — with its own cultural safety questions, and an item filed under another feature will not be found by someone preparing Ward Flow for use. HARD GATE BEFORE ANY REAL-PATIENT USE. Not a blocker on further prototype building. CONFIRMED 2026-09-09 across the design surface as well as the code: neither the Command mockup (artifact d821a155) nor the third-edition design system (artifact e7f7b6f2) contains a rule, a field, a gate or a note on Aboriginal cultural safety, in roughly 27,500 lines between them. The standard is otherwise scrupulous about recording what it has decided and what it has deliberately left out, with a whole section for decisions taken and decisions for the owner, which makes the silence read as nothing having been considered rather than as work owned elsewhere. NEXT ACTION UNCHANGED and still outside this team, but one cheap thing helps now: add a line to the standard's decisions-for-the-owner section recording that the review is a known gate owned by Aboriginal health practitioners, so each of the eighteen mockup migrations does not rediscover the gap independently.", + "baseRowFingerprint": "26c3f2f27a4db03535ecb30a81e37bc6cacf4e262dc73d52b8b86a559439e09b" + } +} diff --git a/docs/outstanding-issues-inbox/7b4cbd9f-8736-42c5-baf3-722870b9e4c4.json b/docs/outstanding-issues-inbox/7b4cbd9f-8736-42c5-baf3-722870b9e4c4.json new file mode 100644 index 0000000000..79ba0eac16 --- /dev/null +++ b/docs/outstanding-issues-inbox/7b4cbd9f-8736-42c5-baf3-722870b9e4c4.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "7b4cbd9f-8736-42c5-baf3-722870b9e4c4", + "createdOn": "2026-09-09", + "action": "update", + "payload": { + "id": "#6DT5K7", + "detail": "MEASURED 2026-09-04: the rule is live in 8 files, 4 of them source — escalation/page.tsx, escalation-board.tsx, referrals/referral-destination-options.ts, ward-derivations.ts — plus four planning documents. It is written in capitals with reasoning ('THIS BOARD RECORDS AND SHOWS. IT SUGGESTS NOTHING — no least-bad options, no ranking of wards the patient does not fit, no near-miss computation') and reads exactly like a safety principle, so every session that meets it obeys it. Ward Builder Two reports the owner reversed it in conversation, wanting active patient-to-bed matching with the clinician making the final acceptance. THE REVERSAL IS NOT IN docs/ward-flow/owner-rulings-2026-09-04.md, whose last change predates the conversation, so it exists only in one chat transcript. WHY THIS IS P1: the cost is not the unbuilt feature, it is that the codebase actively instructs the next session to build the opposite, confidently. Also note the regulatory dimension flagged on PR #2597 and left unresolved there: a board that ranks wards for a patient is closer to clinical decision support than one that records, which is the TGA/SaMD classification question. Needs an owner decision, not an integrator's. ADDED 2026-09-09, and it changes the picture: THE REVERSAL IS ALREADY BUILT IN THE MOCKUP. Command (artifact d821a155) presents an explainable shortlist whose shortlistFor() sorts every ward in the network for the selected patient, first by availability class in the fixed order eligible, overridable, declined, no_specialling, no_bed, then by ready beds descending. That is active patient-to-bed matching with a ranked list, presented as the screen's third column, which is the behaviour the owner asked for in conversation and the opposite of what the four source files still instruct. So the codebase and the reference mockup now teach opposite rules, and the mockup is the artifact a clinician is shown at review. THE STANDARD IS SILENT ON IT, which is the part to fix first and is nearly free: section 8.4 is careful that no verdict is ever drawn about a person, and says nothing about whether ordering the wards is a suggestion. One sentence settles it either way. The TGA/SaMD point recorded above applies with more force now that a ranked list exists in a built artifact rather than only in conversation.", + "baseRowFingerprint": "362e8d9e3fda58809cdd33e2a795a00866b0d1b3aec25a5dff820d8fc3cc372b" + } +} diff --git a/docs/outstanding-issues-inbox/86df0514-1263-40ed-a511-c94b44367fef.json b/docs/outstanding-issues-inbox/86df0514-1263-40ed-a511-c94b44367fef.json new file mode 100644 index 0000000000..c6724b195d --- /dev/null +++ b/docs/outstanding-issues-inbox/86df0514-1263-40ed-a511-c94b44367fef.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "86df0514-1263-40ed-a511-c94b44367fef", + "createdOn": "2026-09-09", + "action": "update", + "payload": { + "id": "#BAY1TY", + "detail": "MEASURED 2026-09-04: ward-model.ts declares 'sex: Sex' and no gender field anywhere. ward-eligibility.ts consumes it directly — unit.sexMix[movement.sex] and sexDesignationAccepts(unit.sexDesignation, movement.sex) — so a patient's bed eligibility is computed from that one value. There is currently no way to record that a patient's gender identity differs from their recorded sex, and therefore no way for the board to represent, let alone match on, that distinction. Harmless today (synthetic prototype, no real patients) and expensive later: the data model is the costly thing to change, the matching rule is cheap. Ward Builder Two relayed the owner's view that this goes to a clinician who works in this area rather than being settled internally, and that is right. TREAT AS A GATE BEFORE ANY REAL-PATIENT USE, not as an emergency. ADDED 2026-09-09 by review of the Command mockup (artifact d821a155) and the third-edition standard (artifact e7f7b6f2). The prototype carries the same single field and hardens it: a movement holds sex Male or Female, a ward holds sexDesignation plus sexMix as a two-key object of Female and Male, and the sex_mix gate reads both. MORE THAN THAT, THE PAGE'S OWN RECONCILE CHECK ENFORCES THE BINARY: it fails the ward unless sexMix.Female plus sexMix.Male equals occupied exactly, so a third value cannot be added to the fixture without breaking the screen's consistency line. The design system says nothing about sex or gender anywhere in its ten rules, its wording section or its definition of done, so nothing stops the next of the eighteen mockups copying the binary field forward. TWO SMALL THINGS WORTH DOING BEFORE THE CLINICIAN REVIEW LANDS, neither of which pre-empts it: record in section 8 of the standard that the modelling question is open and owned outside the build, and stop new mockups inheriting the field by copy.", + "baseRowFingerprint": "d90500f16865b5545e5724ce44877e5531512f2a280aa0dec0b0587c0eb53fb2" + } +} diff --git a/docs/outstanding-issues-inbox/bb829d78-5b96-4216-b9bd-e94e76db6bf4.json b/docs/outstanding-issues-inbox/bb829d78-5b96-4216-b9bd-e94e76db6bf4.json new file mode 100644 index 0000000000..3a6cdfb38d --- /dev/null +++ b/docs/outstanding-issues-inbox/bb829d78-5b96-4216-b9bd-e94e76db6bf4.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "bb829d78-5b96-4216-b9bd-e94e76db6bf4", + "createdOn": "2026-09-09", + "action": "add", + "payload": { + "pri": "P2", + "type": "issue", + "summary": "Ward Flow standard states 23 wards across 17 sites, and its own reference build carries 16 across 9", + "detail": "FOUND 2026-09-09. Section 8.3 of the third-edition standard (artifact e7f7b6f2) states as a data rule: 'There are eight emergency departments and Joondalup Health Campus and Peel Health Campus have no inpatient ward in the data. There are twenty-three wards across seventeen sites. A page reads the collection and never carries a hard-coded nine.' MEASURED in artifact d821a155, the reference build the standard points at: UNITS holds 16 wards across 9 sites (ARM, BTY, FRE, FSH, GRY, RGH, RPH, SCGH, SJGM), and EDS holds 8 departments. The department figure is right. The ward and site figures are not. WHY IT MATTERS BEYOND A TYPO: section 6 of the standard renders every one of its component examples live from Command's own stylesheet and data, so the standard asserts a figure its own reference build contradicts on the same page. Anyone building the next mockup takes twenty-three from the prose and finds sixteen in the data. Note the standard's WACHS handling is not the defect: no WA Country ward exists in the data and that absence is deliberately and correctly stated by the 'WA Country states its absences' rule. NEXT ACTION: decide which number is the network's real size, then either extend Command's UNITS to match or restate 8.3 as the real figure with a sentence saying Command carries a subset. Check the same sentence in the repository's own ward-sites.ts before choosing, since a third figure there would make this a three-way disagreement.", + "source": "Artifact review 2026-09-09 (e7f7b6f2 section 8.3 vs d821a155 UNITS)", + "issueUlid": "01M22T8P9KA2TNT9NXNYZ2M1DA" + } +} diff --git a/docs/outstanding-issues-inbox/d62e303c-9d77-4bf1-9f9d-5404375b714d.json b/docs/outstanding-issues-inbox/d62e303c-9d77-4bf1-9f9d-5404375b714d.json new file mode 100644 index 0000000000..9aedb98fb6 --- /dev/null +++ b/docs/outstanding-issues-inbox/d62e303c-9d77-4bf1-9f9d-5404375b714d.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "id": "d62e303c-9d77-4bf1-9f9d-5404375b714d", + "createdOn": "2026-09-09", + "action": "add", + "payload": { + "pri": "P1", + "type": "issue", + "summary": "Ward Flow: the Command mockup and the shipped reducer give opposite answers on a prior decline", + "detail": "FOUND 2026-09-09 by review of artifact d821a155 (Ward Flow Command, third edition) against src/components/ward-management/. THE MOCKUP: prior_decline is one of three ABSOLUTE_GATES, alongside allocatable_bed and specialling. Its fixed sentence, printed on the candidate row, in the disabled override control's tooltip and in the refused-actions register, is 'A recorded reason does not undo a ward's decline.' A declined ward therefore sorts to availability class 'declined' and cannot be chosen at all. THE ENGINE: ward-flow-reducer.ts deliberately places prior_decline in neither the world-fact group nor SUITABILITY_GATES, so eligibilityRefusal never consults it and a re-approach passes with no reason recorded. Its comment gives the reasoning: a ward that declined ninety minutes ago because it had no bed is the commonest thing in bed management, beds free up, and none of the five OVERRIDE_REASONS names a mismatch that exists. It records that enforcing it broke 23 tests across 7 files, almost all re-referrals after a seeded decline. WHY IT MATTERS: this is not a styling difference. The prototype teaches a coordinator that a decline closes that ward to that patient; the engine treats re-approaching as routine and unremarkable. One of the two is wrong and a coordinator trained on the prototype would be wrong in the direction of not ringing a ward that would now say yes. NEXT ACTION: an owner decision, then write it into section 8.4 of the third-edition standard (artifact e7f7b6f2) so the next mockup inherits it, and align whichever side moves. The reducer's own comment already records the question as open.", + "source": "Artifact review 2026-09-09 (d821a155 vs ward-flow-reducer.ts)", + "issueUlid": "01M22T7W1BFJW91M28CY861QTA" + } +} diff --git a/docs/ward-flow/WARD-LEAD-HANDOVER-2026-09-05.md b/docs/ward-flow/WARD-LEAD-HANDOVER-2026-09-05.md index f416513278..7b3a00609d 100644 --- a/docs/ward-flow/WARD-LEAD-HANDOVER-2026-09-05.md +++ b/docs/ward-flow/WARD-LEAD-HANDOVER-2026-09-05.md @@ -232,12 +232,14 @@ provide it for itself). **Neither of the two above should be trusted without run ### This is NOT Ward Verifier's inert CI flag — they are unrelated -Ward Verifier's `vars.WARD_JOURNEYS_BLOCKING` is a **separate matter with nothing in common but the -word "expected"**. It is a repository variable that is unset, which makes the seven browser journeys -non-blocking in CI; it becomes live only when the owner sets it after seeing those journeys green -once, and it can only take effect on `main`. **It produces no red locally and no failing assertion -anywhere.** A red you can run and a flag you cannot are different objects, and conflating them would -let a real failure hide behind "that one's expected". +Ward Verifier's `vars.WARD_JOURNEYS_BLOCKING` was a **separate matter with nothing in common but +the word "expected"**. It was a repository variable, left unset, which made the seven browser +journeys non-blocking in CI. **SUPERSEDED 2026-09-06:** the owner removed the gate outright rather +than setting the variable, after a green run of the seven (`3 skipped, 71 passed`). The journeys now +block on every UI pull request and the variable no longer exists in `ci.yml`. The distinction this +section drew still stands for anything else that is "expected": a red you can run and a flag you +cannot are different objects, and conflating them lets a real failure hide behind "that one's +expected" — which is precisely what happened to these journeys on 2026-09-05. ## 3. Outstanding work, with owners @@ -338,8 +340,9 @@ answers, and this project has been caught by it. raw values untouched and referrals still findable under any spelling. - **Scratch files / `.next`** — approved by name, per file. An approval for two named files does not stretch to a third. -- **Browser tests** — keep them, do not run them. Now behind `vars.WARD_JOURNEYS_BLOCKING`, - inert until deliberately enabled. +- **Browser tests** — keep them, do not run them. Was behind `vars.WARD_JOURNEYS_BLOCKING`, inert + until deliberately enabled. **Superseded 2026-09-06:** the gate is removed and the seven journeys + block on every UI pull request. - **White ward ground** — asked for three times; done in the one shared token. **Rulings I made that are not yet built:** diff --git a/tests/ci-cache-safety.test.ts b/tests/ci-cache-safety.test.ts index fe755d5212..b8a1712aaf 100644 --- a/tests/ci-cache-safety.test.ts +++ b/tests/ci-cache-safety.test.ts @@ -440,12 +440,12 @@ describe.skipIf(process.platform === "win32")("PR required aggregate — cancell DB_RESULT: "skipped", CARING_CONTACTS_DB_RESULT: "success", /* - * 🔴 **ADDED 2026-09-06, AND ITS ABSENCE TURNED ALL TWELVE OF THIS BLOCK'S CASES RED.** The - * `ui-ward-journeys` job and its two aggregate variables were added to `ci.yml` without this - * fixture gaining the matching entry, so `WARD_JOURNEYS_RESULT` reached the extracted script - * as an EMPTY STRING. `record()` treats anything that is not `success`, `skipped` or - * `cancelled` as a failure, so every case — including "passes when every in-scope job - * succeeded" — recorded `ward-flow-journeys result was ` and exited 1. + * 🔴 **THIS ENTRY AND `ci.yml` MOVE TOGETHER, AND ITS ABSENCE ONCE TURNED ALL TWELVE OF THIS + * BLOCK'S CASES RED.** The `ui-ward-journeys` job was added to `ci.yml` without this fixture + * gaining the matching entry, so `WARD_JOURNEYS_RESULT` reached the extracted script as an + * EMPTY STRING. `record()` treats anything that is not `success`, `skipped` or `cancelled` as + * a failure, so every case — including "passes when every in-scope job succeeded" — recorded + * `ward-flow-journeys result was ` and exited 1. * * ⚠️ **AND NOTHING LOCAL COULD HAVE CAUGHT IT: this whole `describe` is `skipIf(win32)`.** It * runs on Linux only, so on this project's development machine it reports as SKIPPED rather @@ -453,19 +453,16 @@ describe.skipIf(process.platform === "win32")("PR required aggregate — cancell * edited alongside a workflow, guarded by a block that cannot run where the workflow is * edited, is the shape to watch for here. * - * `"skipped"` is what GitHub actually sets for a job whose `if:` is false, which is the state - * on every pull request until `WARD_JOURNEYS_BLOCKING` is turned on in repository settings — - * so this fixture now describes the real default rather than an omission. + * `WARD_JOURNEYS_BLOCKING` was removed from both this fixture and `ci.yml` on 2026-09-06 when + * the lane was enabled; it no longer exists in the workflow, so binding it here would test a + * variable the script never reads. * - * ⚠️ **BOTH VARIABLES ARE NEEDED AND THE BLOCKING FLAG FAILS FIRST.** The script runs under - * `set -u`, so the unbound `WARD_JOURNEYS_BLOCKING` aborts it at that line before - * `WARD_JOURNEYS_RESULT` is ever read — which is why every case in the block died, not only - * the ward one. In the real workflow `env:` binds it to `${{ vars.WARD_JOURNEYS_BLOCKING }}`, - * which is the EMPTY STRING when the variable is unset: bound, and not `"true"`. The empty - * string here is therefore the faithful default, not a placeholder — writing `"false"` would - * test a state the repository never actually produces. + * `"skipped"` is the faithful default for THIS fixture specifically, because it sets + * `UI_CHANGED: "false"` — the lane's `if:` is false, and GitHub reports a skipped job. It is + * NOT the default for a UI-changed pull request any more: there the lane runs and must + * succeed, which is why the `UI_CHANGED: "true"` cases below set it explicitly rather than + * inheriting this. */ - WARD_JOURNEYS_BLOCKING: "", WARD_JOURNEYS_RESULT: "skipped", }; @@ -631,6 +628,9 @@ describe.skipIf(process.platform === "win32")("PR required aggregate — cancell UI_CHANGED: "true", UI_FAST_RESULT: "success", UI_RESULT: "cancelled", + // The ward lane runs on every UI pull request now, so it must be green here or this + // case would go red for two reasons and stop isolating the cancellation it is about. + WARD_JOURNEYS_RESULT: "success", }).status, ).not.toBe(0); expect( @@ -638,6 +638,9 @@ describe.skipIf(process.platform === "win32")("PR required aggregate — cancell UI_CHANGED: "true", UI_FAST_RESULT: "cancelled", UI_RESULT: "success", + // The ward lane runs on every UI pull request now, so it must be green here or this + // case would go red for two reasons and stop isolating the cancellation it is about. + WARD_JOURNEYS_RESULT: "success", }).status, ).not.toBe(0); }); @@ -648,6 +651,37 @@ describe.skipIf(process.platform === "win32")("PR required aggregate — cancell expect(workflow).toMatch(/pr-required:[\s\S]*?if: always\(\)/); }); + it("requires the Ward Flow journeys on a UI pull request, and only there", () => { + /* + * ENABLED 2026-09-06. Until then `ui-ward-journeys` was gated behind + * `vars.WARD_JOURNEYS_BLOCKING`, a repository variable nobody ever set, so the lane always + * skipped and this aggregate always took its `require_skipped_or_success` branch. Nothing + * could go red because of a broken ward journey — and on 2026-09-05 several did, unnoticed + * for a day. + * + * These four cases are the proof the lane is genuinely blocking now, which no case asserted + * before: the first two are the states that must FAIL, the last two the out-of-scope states + * that must still pass. Without the first case in particular, removing the job's `if:` and + * this aggregate's condition would look identical to leaving them in. + */ + // `UI_CHANGED: "true"` also puts `production-ui-critical` and `production-ui` in scope, and + // the fixture leaves both skipped — so every in-scope case below carries them green. Without + // that the ward result is not the variable under test and the "in scope and green" case fails + // for an unrelated reason, which is exactly what this test caught while being written. + const uiPr = { UI_CHANGED: "true", UI_FAST_RESULT: "success", UI_RESULT: "success" } as const; + + // In scope and red: the lane failed. + expect(runAggregate({ ...uiPr, WARD_JOURNEYS_RESULT: "failure" }).status).not.toBe(0); + // In scope and absent: a lane that did not run verified nothing, so it cannot pass the PR. + // This is the exact case that passed before the gate was removed. + expect(runAggregate({ ...uiPr, WARD_JOURNEYS_RESULT: "skipped" }).status).not.toBe(0); + // In scope and green — the ward result is the only thing that changed from the case above. + expect(runAggregate({ ...uiPr, WARD_JOURNEYS_RESULT: "success" }).status).toBe(0); + // Out of scope: no UI change, and drafts. Skipped is correct and must not fail the aggregate. + expect(runAggregate({ UI_CHANGED: "false", WARD_JOURNEYS_RESULT: "skipped" }).status).toBe(0); + expect(runAggregate({ ...uiPr, PR_DRAFT: "true", WARD_JOURNEYS_RESULT: "skipped" }).status).toBe(0); + }); + it("never puts a status-check function anywhere but an `if:` condition", () => { /* * GitHub allows success()/failure()/cancelled()/always() ONLY in `if:` conditions. Using diff --git a/tests/ui-ward-search.spec.ts b/tests/ui-ward-search.spec.ts index 0443218ebf..ba99e2a9bf 100644 --- a/tests/ui-ward-search.spec.ts +++ b/tests/ui-ward-search.spec.ts @@ -371,12 +371,14 @@ test.describe("@mockup Ward patient search", () => { * `align-self: stretch` with `min-height: var(--ward-tap)` — on both the input and its sibling, * and the stylesheet's own comment names the absent-declaration defect it closes. * - * ⚠️ **TREAT THIS SPEC'S STATUS AS UNKNOWN RATHER THAN RED OR GREEN, and that is the point worth - * keeping.** It sits in the `chromium-mockups` lane, which the owner ruled is kept but not run, so - * **nothing executes to contradict a claim made here.** A stale "expected to be RED" in an unrun - * lane is worse than in a running one: a reader takes it as a live defect, and there is no red or - * green anywhere to correct them. **Re-read this comment the first time the lane is switched on** - * — the flag is `vars.WARD_JOURNEYS_BLOCKING`, unset by default. + * ⚠️ **THE LANE IS SWITCHED ON NOW, AND THIS SPEC IS GREEN. Re-read done 2026-09-06, which is + * what the previous wording asked for.** This comment used to say to treat the status as UNKNOWN + * rather than red or green, because the `chromium-mockups` lane was kept but not run and so + * nothing executed to contradict a claim made here — a stale "expected to be RED" being worse in + * an unrun lane than in a running one. `ui-ward-journeys` no longer carries its + * `vars.WARD_JOURNEYS_BLOCKING` gate, so this spec runs and blocks on every UI pull request, and + * the run that lifted the gate reported `3 skipped, 71 passed` with this case among the passes. + * The status is observed, not asserted, and CI now corrects anyone who writes otherwise here. * * ⚠️ **AND THE DEFECT ITSELF IS THE STRONGEST ARGUMENT IN THIS REPOSITORY FOR KEEPING THESE * SEVEN SPECS.** jsdom computes no layout, so no offline test could have measured a 36px control;