Build state
How the simulator is put together, the rules that must not break, and every ratified design decision.
| Field | Value |
|---|---|
| Version | 3.7 |
| Updated | 2026-08-22 |
| Owner | CJ Oltman |
| Target file | the simulator — 44 screens: 23 stages + 12 proposed + 9 forms |
| Source of truth for lifecycle | chat-lifecycle-stage-simulator.html — Brad’s original, in the same folder, never edited |
| Open items | Open items v2.0 — every blocker and question, by owner. Source of truth. |
| Team handoffs | Handoffs v1.0 — LIVING. What Brad and Jay owe, phrased to be answered without opening the file. |
| Work order | Build order v1.8 — the current ordered build pass. Start there. |
| Call record | Call record v1.2 — the 2026-08-21 review, quoted and attributed |
| Companion doc | Review method v2.2 — context for a chat reviewing a call recording |
| Predecessor doc | BoRo_ChatLifecycle_BuildList_Internal.md v1.3 — superseded by this doc; its §0 is withdrawn |
| Verify with | ./tools/lifecycle/verify-all.sh — runs all three deploy checks. Tooling lives in the repo now, not in a session scratchpad. |
| Verified at handoff | SMOKE PASS 368 + 74 form + 76 dock + 4 arrival + 96 chain + 96 chat + 13 live + 35 walk + 3 all + 7 panel + 35 spec + 7 offsite · master all three identical · token drift clean |
The 368 is the constant. It is Brad’s 23 stages × 2 roles × 2 themes × 2 devices × 2 lanes and it has not moved since the lane axis landed, so a regression is always attributable. The suffixed suites move with the feature set and are expected to. This pass took
form12 → 74 (six new forms, plus a role-identity sweep and a lane check),dock12 → 72 (it stopped comparing two placements and started enforcing one, across every stage at both roles), and addedchat96 for B-5. If a suffix shrinks without a feature being withdrawn, that is the regression.
BLUF
The post-review build list is finished except for two items, both of which are blocked on somebody else. As of 2026-08-22 the file carries 42 screens — Brad’s 23 stages, ten proposed chain stages, and nine form screens — on a cumulative thread with conversation woven between the milestones, gated menus, real photography and a working pairing code.
Done since the 2026-08-21 review: the ship gate (G-1 initiating actions + forms, G-2 real item images), then B-1, B-2, B-4, B-6, B-7, B-8, B-9, B-10, menu gating, both check-in chains, and D-1 built to a reading CJ ratified. This pass (2026-08-22) added: D-3 settled, B-5, and the six remaining forms.
- D-3 is SETTLED — the action dock wins, decided by CJ. The in-thread alternative and the capture-bar toggle that carried both are withdrawn. This is the second reversal on that control and §5 records both.
- B-5 is built, as a presentation-layer
FILLERarray on the same phase spine asEVENTS(). Brad’s master was not edited — see §5 for why that was the choice rather than the old prohibition. The Chat and Timeline tabs now genuinely differ. - All nine forms exist, and
formHtml()was rewritten declaratively on the way. Adding a tenth is a data entry, not a code change.
What is left: two items, neither of them unblocked. Refresh Tap waits on R-4 (MAX_TOTAL_RENTAL_DAYS
is 12 in the shipped constants against 30 in FSD §13.1) and the all-stages view. B-11 waits on D-4. After
those it is decisions, not building.
Open items live in Open items — 27 of them, by owner. Two still block more than
one thing: Q9 (no semantic tokens in boro-shared — the real integration blocker, and unscheduled work
rather than a question anyone can answer) and D-4 + Q3 (the no-show chip, a data-model question only Brad
can settle).
Four things a new session must not do. Do not act on §0 of the build list (§3 has the evidence). Do not
edit STAGES[], SAMPLE_CHAT or ALWAYS without CJ’s say-so and sending Brad the diff (§2 rule 1 — that
rule changed character on 2026-08-21, it did not disappear). Do not trust getComputedStyle across a theme
switch on an element render() did not rebuild (§2). And do not resolve anything tagged D-1…D-7 without CJ —
build the comparison and leave the decision. D-3 is now settled and D-1 is built as a proposal; the rest
still stand.
1 / What the file is
A single self-contained HTML document — a 42-screen clickable simulation of the BoRo rental conversation,
reviewed with Brad Larson and going to a workshop with Jaydan Nguyen. It is a specification, not a
prototype — but it is walkable. As of 2026-08-22 the lifecycle controls navigate: every action on a card
declares go (the stage it produces), form (the form it composes) or inert (no destination here, with the
reason on hover), and the harness fails a button that declares none. A reviewer can click from Interest to a
settled rental in seventeen presses, and the harness asserts they can.
What stays inert, and why it is not a loophole: four buttons have no destination in this document — two
Contact Support, one Message Support, and Request BoRo Recovery — because support and dispatch leave the
rental conversation. A fifth, Decline at the pickup decision, is inert because FSD §10.4 carries that
control and STAGES[] does not. Each names its reason on hover and each is listed in the Styles panel.
Three transitions have no button on purpose. From a confirmed meetup into its check-in, at all three
meetups, the window opens on a clock rather than a tap — so the card says so, TIME_OPENED declares them, and
the capture bar’s Next carries the reviewer across.
Where the 42 comes from: 23 stages in STAGES[] (Brad’s master) + 10 proposed chain stages in
EXTRA_STAGES + 9 form screens in FORMS[]. Only the first 23 are his.
Two layers, kept visually distinct on purpose:
- Capture wrapper —
.sim-bar(top) and.notes/.styles(bottom). Blue-slate tooling chrome. Never follows the product theme. Carries role / stage / device / theme / lane / panel controls; the lane toggle appears only on the stages it affects and carries its own ⓘ explainer. - Product preview —
.preview > .phone > .shell. Real BoRo tokens, light as the base with a[data-theme="dark"]remap scoped to.preview— the same hook the app consumes throughboro-shared.
The Styles panel (header → “Styles”) is the design handoff. It carries a live token map read from computed styles, component geometry, and ~50 flags recording every ratified decision, deviation and open question. Keep it current — it is what the developer reads, and it is where a decision from a call has to land.
2 / Rules that must not break
STAGES[],SAMPLE_CHATandALWAYSare Brad’s master — and as of 2026-08-21 they may be edited with CJ’s authorization. The rule changed character rather than disappearing. It was never really “these bytes are sacred”; it was “Brad must never find a silent change to his lifecycle data.” So the deploy check is now a delta report: it prints a unified diff of any block that differs from his original, ready to hand to him. Default remains derive-in-the-presentation-layer —chipLabel(),rowDates(),FORMS[]all exist because that was the honest way to do those jobs. Edit the master only where the presentation layer cannot do the job truthfully, and send Brad the diff when you do. Slice precisely — fromconst STAGES = [to its closing\n ];, not up toconst SAMPLE_CHAT, because a screening comment sits between them and a naive slice reports a false positive.- Check token drift on every deploy, next to the
STAGES[]hash. The embedded token block is a copy ofboro-ops/brand/boro-tokens.css, and the DS moves. This file sat on v2.8 while the DS shipped v2.9 on 2026-08-20, and the one drifted value was an AA failure across the entire caption layer. Compare the.previewand.preview[data-theme="dark"]blocks against the token file, normalising whitespace and leading zeros —0.10and.10are the same value, and a check that cries wolf gets ignored. - Single self-contained document. Only external requests are fonts: General Sans from Fontshare (product) and IBM Plex Sans + JetBrains Mono from Google (wrapper).
- Where the FSD and a built screen disagree, the FSD governs and the screen is the defect.
- Measure and look — neither substitutes for the other. Contrast from rendered pixels; geometry from
getBoundingClientRect(). Several decisions in this project reversed once measured. And then look anyway: measurement catches a value one step off or a state that never renders, looking catches a semantic contradiction nobody thought to assert, and neither class is reachable from the other (Universal_SOP§3). Three defects here passed the suite and were caught by a screenshot in seconds.
The smoke harness — use it on every change
The tooling is in the repo now. tools/lifecycle/ holds the harness and the three checks; it used to live in a
session scratchpad, which is why a new chat could not run it.
cd "…/App/Chat Lifecycle" && ./tools/lifecycle/verify-all.sh
It injects tools/lifecycle/smoke-harness.js into a throwaway copy and runs headless Chrome — the source file is
never modified, which is the point. A clean run today reads:
SMOKE PASS 368 (184 per lane) + 74 form + 72 dock + 4 arrival + 80 chain + 96 chat
368 is the lifecycle grid — Brad’s 23 stages × 2 roles × 2 themes × 2 devices × 2 lanes. It has been 368 since the lane axis landed and is deliberately held constant, so a regression is always attributable. The suffixed suites are the things that do not belong on that grid: the nine form screens, the settled dock placement, both arrival orders, the ten proposed chain stages, and B-5’s conversation. They move with the feature set — see the note under the header for what moved in the 2026-08-22 pass and why.
Current assertions, all measured rather than asserted from the source:
- every referenced CSS var resolves in both themes
- chips are a 25–50% wash in BOTH themes, the ink is never
--chip-on, and the ratio is measured against the composite (fill over ground), not the fill alone - card rails, discs, the timer bar and the current segment pip are opaque — never a wash
- one visible avatar per message run, sitting on the bubble baseline
- reading column is 760, left-aligned; the composer column matches
- map plate:
xMidYMid slice, the geofence measured circular in rendered pixels, the pin’s bounding-box midpoint on the ring centre and no more than 45% of its diameter, and the pin/ring ink identical across every card tone (the plate carries no status) - card rhythm: every adjacent pair of blocks inside a card is 12px apart
- timer numeral and body text meet AA at the right threshold for their size (3:1 for display, 4.5:1 for body)
- only one live display timer per thread, and only one card offering actions
- badges come from the closed vocabulary, and “Approved” never lands on a card whose title says complete, arrived, returned, active or closed
- date separators never repeat, never lead the thread, never trail it, and never walk backwards in time
- history never shrinks along the canonical spine as the lifecycle advances
- the segment counter is five pips with exactly one current, and never appears on Lane A
- the lane toggle is visible exactly where it changes something — and byte-identical output between lanes everywhere it is hidden
- every menu item that is disabled carries a reason, and no menu leads with the destructive item
- exactly one header chip slot is visible
.action-rowis asserted absent from the DOM, not merely hidden
When it fails, check the assertion before the file. Several failures so far were bad assertions rather than defects — a flat 4.5:1 applied to a 28px numeral where AA is 3:1; a control count that was counting a popover’s contents. But the harness has also caught real bugs that no amount of looking would have: a finished rental still counting down, a plate ring whose lift silently changed, and an exception stage whose phase sat below its own event so its card never rendered at all. Update the assertion when a decision is reversed — several in §5 have been, and an assertion enforcing a superseded rule is worse than none.
Do not trust getComputedStyle on an element whose own attributes did not change while an ancestor’s
data-theme did. The tabs are the case: render() only classList.toggles them, and when the toggle is a
no-op Chrome keeps the cached color even though the paint is correct. A sweeping in-page contrast audit built
on it produced eight false failures — it reported the light tabs at 1.00:1 while the screenshot showed them
near-black and the element’s own --content-primary read #1C1917.
The guard: measure through a probe element you insert yourself (el.appendChild a <span> with
style.color = "var(--token)"), or re-create the subtree, or skip the DOM and do the arithmetic on the token
values — arithmetic is immune and it is what caught the real defect. Elements render() rebuilds via
innerHTML (bubbles, cards, chips, the composer) are fine.
3 / §0 of the build list is WRONG — do not act on it
The build list calls prohibited sample items a blocker, citing FSD §14.3. §14.3 contains no item schedule — it states three governing principles and defers the list to Terms of Service §8.
Checked against the published Terms (v2.5, effective 2026-08-06, boro-landingpage/src/data/legal.ts):
8.4.4 H-4— “Pressure washers — all types, electric and gas-powered, at any pressure rating: PERMITTED”8.4.1 H-1— chainsaws, table saws, wet tile saws, chippers PROHIBITED; miter saws, circular saws, nail guns TIER 2; angle grinders, jackhammers, reciprocating saws, wood routers PERMITTED. A drill appears nowhere in the schedule.8.4.6 H-6— ladders PERMITTED. Nothing in8.4.7 H-7covers a hair styler.
All three sample items are compliant. Acting on §0 would have churned the hero item, the SVGs, all sample copy
and three inbox rows for nothing — and §0.4 would have written a false rule into the file for future passes to
follow. A screening comment is already in place above SAMPLE_CHAT, pointing at Terms §8.4 with the real
prohibited list.
4 / FSD verification already done — do not redo
Read from boro-ops/product/BoRo_MasterFSD.md (mounted; no upload needed).
⚠ These are V-tags, not D-tags — corrected 2026-08-22
Until this version these rows were labelled D-2, D-4, D-5 and D-6, which are four live IDs in a completely different register. In
BUILDandREGISTER, D-1 is the QR model, D-4 is the no-show tone and D-5 is the extension decline window — none of which is what the rows below say. A session told “do not resolve anything tagged D-1 through D-7” and reading this table first would have protected the wrong things and resolved the right ones.The only D-register is Open items §1–§3, mirrored in
BUILD§Do not resolve. These rows are verification results — questions already answered by canon — so they carryV-and nothing here is a decision anybody still owes.
| Verified | Ruling | Source |
|---|---|---|
| V-1 session length | 45 minutes. “exactly one 45-minute session timer. There is no separate 15-minute timer.” The cards saying “30 min window” are the defect | §10.2 |
| V-2 QR model | One code, tied to the booking, displayable only inside the 100-ft geofence, valid only when both devices are inside. Not mutual scan. (The open decision about this is D-1.) | §10.1 |
| V-3 condition capture | Lender captures up to 5 photos + 1 video, timestamped; borrower confirms before taking possession. Built as the capture-condition form, 2026-08-22 |
§10.3 |
| V-4 cancellation pricing | cancellation.ts implements >24h none / 12–24h 50% / <12h 100% of the commitment hold, to the counterparty. Don’t block cancelling — price it in the confirm modal. Built as the confirm-cancel form, 2026-08-22 |
§6.1 + boro-shared/src/cancellation.ts |
| V-5 max term | 30 days both lanes. Lane A (≤$100, no Shield) has no mid-rental events. Lane B ($101–$1,000) is five six-day segments, four Refresh Taps | §13.1, §13.4, §6.2 |
| V-6 double-hold | Disclosure before the tap is mandatory, plain language, on a $500 item the borrower may briefly see $1,000 held | §13.6 |
| V-7 extension increments | Commit up front to +6 / +12 / +18 / +24 days, to a 30-day total; the lender sees the total resulting duration and is not re-asked. §13.2 names no lender response window — that gap is D-5 | §13.2, §13.3 |
| V-8 report-a-problem | Freezes the transaction; fault follows the inspection, not the accusation. Genuine fault: lender bears $25.00 dispatch + $0.75/mile. False report: it flips to the borrower | §10.6 |
Correction to the build list §4.3: it says the return submissions are blind, “neither party sees the other’s.” §11.2 is narrower — “The borrower photographs the item at handback; the images are shown to the lender… Neither party sees the other’s submission.” There are two artifacts: the handback photos are shared, the condition tickets are blind. Building it as “everything hidden” breaks the lender’s ability to see what they are being handed.
Live conflict already logged in the FSD’s own register: MAX_TOTAL_RENTAL_DAYS is 12 in the shipped
constants against 30 in §13.1; MAX_RENTAL_DAYS = 6 is correct only as the Lane B segment ceiling.
5 / Decisions locked — do not re-open
Thirteen labs, all ratified by CJ. The labs are the decision record.
Colour and chips
- Stage chip tone derives from rental status via
TONE_BY_STATUS, never a hand-picked hue.needsViewerAction/amberForoutranks status — whoever owes a decision sees warning. disputed→ error, not info. A state where something has gone wrong must not wear routine blue.- Both themes are a tint (ratified 2026-08-21, Lab 14 — light was solid until then). Light is the hue at 34% over the ground with the label pushed 55% toward black; dark is the same wash with the label lifted 35% toward white. The two percentages differ because they must: luminance is not symmetric about the middle of the range, so the literal mirror of dark measures 3.30–4.29:1 and fails AA on every tone. At 55% the identical wash clears everywhere — worst 5.65:1, better than the 5.0:1 dark tier it mirrors.
- Rails, discs and bars take the opaque
--hue-*set, never the chip fill — a 3px rail in a 34% wash disappears on dark. - Every chip and badge carries a 1px hairline; neutral takes
border-strong. - Card badge uses chip geometry (22 · 3/9 · 11px/600), not standalone Badge’s 28.
- Warning timer numeral:
#C2410Corange-700 on light (5.18:1), bright amber unchanged on dark.--status-warning’s brown reads muddy at display size. Orange-700 as a single value fails dark at 3.38:1 — it is light-only.
Layout
- Reading column 760px, left-aligned; tabs and composer match.
- Desktop and mobile share one shape. Utilities: info visible in the header, ⋮ holds help / report / close.
Composer is
+, field, Send — three controls. - Send is muted on an empty field,
.readyis the brand fill. - No brand header. Built and then removed by decision.
- The action dock is SETTLED — docked — and it carries TWO reversals (D-3, decided by CJ 2026-08-22).
Reversal 1, 2026-08-21: the dock was built and then removed by decision. Reversal 2, 2026-08-22: it
is restored as the standard. Brad asked for it back on the review call — “I don’t want the next step to be
lost in the thing. That’s why I docked it above the message.” CJ’s in-thread alternative was built, carried
in parallel behind a capture-bar toggle so the two could be compared live, and withdrawn on the decision
along with the toggle. CJ’s objection is not refuted and carries forward: the dock “covers up so much of
the chat”, so it stays two buttons and must never grow back into the nine-control
.action-rowdeleted on 2026-08-21. Scope is narrower than it reads: Brad blessed card-borne next steps (“the next step can be like on that card… but to initiate … there needs to be like a button for that”), so only the two initiating actions were ever in question.dockModeis gone;threadHtml()emits nothing andcomposerHtml()always emitsnextStepHtml(). The harness asserts.next-actionsis absent from#threadat every stage and both roles — absent from the DOM, not hidden, the rule.action-rowis held to. Do not restore the in-thread branch, and do not re-remove the dock. - Mobile header is two rows — nav (back · relation centred · ⋮) over an identity band on
surface-basewith a 1px hairline seam, 52px thumbnail, chip holding the right edge, name clamping at two lines. - Lifecycle cards are elevated documents — white ground,
shadow-md, 3px status rail, icon in a filled disc, terms in an inset panel with its own border. - My chat bubble is
surface-inverse, not brand red. Messages group into runs: one avatar and one tail on the last bubble of a run, pointing at each other. - Conversation rows carry a message preview; an unread third conversation exists to show the §07 variant
(
content-primarypreview + 8px trailing dot). - Approve is Primary, Deny is Neutral (Ghost’s label is
content-brand, which would put two reds on one card). - Card blocks are spaced by one 12px gap on the card’s flex column, not per-block top margins.
- A running session is “In process”, amber with a clock — never green/Approved.
- The rental carries the display timer, same as the inspection session, and settles to a line once the thread moves past it. Line weight is reserved for dates that qualify something else.
- Both sheets are drawn open and gated — “+” for lifecycle actions, “⋮” for utilities, disabled items shown with their reason. That is §7 item 7, brought forward.
- Relation toggle full width under the headline, with a working filter beside it.
- Badges are a closed vocabulary — Approved · Completed · Active · Pending · In process · Ready to pair · Under review · Flagged · Denied · Overdue · Expired · Closed · No show · Not returned. Fourteen entries (twelve until 2026-08-22, when the two terminal cards went red and “Closed” stopped being the right word for either). “Approved” is only a proposal that has not yet happened. (Corrected 2026-08-21: this list was missing Ready to pair, which is live on the pairing card. A harness rule regenerated from the short list fails that card.)
- Fact labels and the terms lead-in are bold — the label column carries the contract vocabulary.
- Sample items carry real make-and-model names, re-screened against Terms v2.5 §8.4.
- Card actions run full width — side by side when their own labels fit, stacked when they don’t. The decision is made by the wording, not a breakpoint.
- The thread is cumulative. Every event at or before the current phase renders; only the event at the current phase is live. A completed rental carries 12 cards where it carried 3.
- The chip dictionary —
Check-in · Waiting/You're here/Pair to start, plusInspection · ConcludingandReturn · Scheduled. Labels derive;STAGES[]is untouched. - Initiating actions are full width at the foot of the thread, on three renter screens only. Placement is D-3, under test — Brad’s dock against CJ’s in-thread, Jay breaks the tie.
- Try-on vs inspection derives from the ITEM’s
wornflag, not the stage. - Form screens live in
FORMS[], neverSTAGES[], withform:addressing in the picker. Form and pending card read from onePROPOSALobject so the card is the preview. - Sample items carry real photography from BoRo’s own library, embedded as data URIs, screened by make and model against Terms v2.5 §8.4.
- Cards have three shapes — expanded, folded, flat. The newest is expanded; a meetup card stays expanded until both parties have checked in, because it carries directions somebody is still using; a card with nothing behind the fold renders flat with no chevron.
- A no-show and an unreturned item read red, card and action. The chip half is a named override pending D-4 — see the Styles panel.
- Card tone has four kinds and amber is reserved (2026-08-22, CJ).
wait· info is the default for unfinished — waiting on somebody, or a step in progress with nothing at stake.pend· warning is a closed list: a clock the viewer is standing inside (the two 45-minute sessions, the 9-hour grace window) or money about to move (the authorization disclosure, the take-it-or-not decision).no· error is the only place the hazard triangle appears. An amber chip over a blue card is not the mismatch Brad caught — he caught a green card under an amber chip, two statements that contradict. These do not: the chip says who owes the next move, the card says whether anything is wrong. Chip tone still derives fromTONE_BY_STATUS. - One info affordance per layout (2026-08-22, CJ — closes CR-12). Desktop keeps the ⓘ in the header and drops it from ⋮; mobile drops it from the nav row and keeps it in ⋮. It was duplicated in both layouts. This is the amendment to the mobile header line above.
- Send lights up on input.
.readywas styled from the first build and nothing ever set it — this section described a state the document could not reach. - The thread pins after the composer is built, not before. The old order measured against the previous composer’s height, so the newest card sat clipped exactly when the dock appeared.
- The active tab rule is 3px, strictly heavier than the 1px tab bar it sits on.
- The lifecycle buttons navigate (2026-08-22). Three declarations —
go,form,inert— and navigation must go forward; a control landing the viewer earlier than the card they pressed it on is the cumulative thread eating its own history. One declared exception: denying a pickup routes toschedule-denied, which sits at phase 1.5, so it rewinds past the whole inspection chain. That is a finding for Brad, not a bug in the wiring — hisschedule-deniedserves both the denied inspection and the denied pickup, and a stage has one position on the spine. The rewind is declared on that one button and the harness permits exactly it. - Renting / Lending is live, and so is the filter. Both painted state and did nothing; the sidebar toggle
sets
roleand stays in step with the capture bar’s Renter / Lender pair — one state, two ways to reach it. - Cancelled and Terminated are two filter rows (2026-08-22). It does not settle Q3 / D-4, which is
about what a no-show is called and what tone it wears; both statuses already existed separately. One
inference is flagged:
not_returnedsits in Terminated, a system end rather than a user quit — Brad’s to confirm. - A sheet reads as choices — the label carries its own weight, the reason sits lighter underneath.
- The reference panel is DOCKED RIGHT, full height, with its own scroll (W-23, 2026-08-22 workshop — Brad’s ask: “better suited to be on the right instead of the bottom… make it like scrolling and like bigger”). It was a 42vh bar across the foot. Under 1180px it overlays the preview row rather than pushing — and rather than the viewport, which would cover the capture bar including Next. The second half of W-23, a detached pop-out window, is deliberately not built.
- The panel still defaults to Hidden (2026-08-21) — placement moved, the default did not — the product preview is what a reviewer opens this file to look at, not the capture chrome.
Built in polish batch 2, 2026-08-21 — listed here because these are the ratified decisions they implement
- Timers: two weights, not four — a clock you are standing in gets the display numeral and a bar; a date you are not gets a line. Format by magnitude: date over a week, days inside a week, minutes inside a day, seconds only in the last hour.
- Date separators in the thread.
- Map plate:
preserveAspectRatio="xMidYMid slice"so the geofence stays circular at any width, and no venue label — the card body already names it. Revised 2026-08-21: the pin is centred in the ring and ~40% smaller (~36% of ring diameter), reversing “anchored at the tip, ring raised 9px” — the ring is a 100-foot zone, not a point. The plate carries no status: pin and geofence were taking the card’s tone, so one venue was green on one card and amber on the next. Composition is constrained by the crop — only about y 26–106 of the 132-tall viewBox survives at desktop widths. - Segment counter on the rental-active card for Lane B (five pips, current one amber) rather than five
duplicate stages, with a Lane A / Lane B toggle in the capture bar. Refined 2026-08-21: the toggle
appears only from phase 9 (the pickup proposal) and
activeLane()forces Lane A below that, so hidden also means inert. It carries its own definition on hover and keyboard focus.
6 / What is built
The batch log below is kept for its reasoning. The short version: full DS restyle on
boro-tokens.cssv2.9 / DS v2.21; a cumulative thread whose cards fold to one line behind the newest; conversation woven between the milestones, so the Chat and Timeline tabs differ; timers in two weights; a map plate; date separators; the chip dictionary and a closed fourteen-word badge vocabulary; gated ⋮ / “+” / filter menus that open the forms they name; real item photography and team headshots; two initiating actions, docked, and nine form screens; a Lane A/B model with accruing segments; a symmetric pairing code; and both check-in chains as proposed stages. Forty-two screens.
6b / How it got there, batch by batch
Full DS restyle: tokens (boro-tokens.css v2.9 / DS v2.21), General Sans with Inter fallback, §07 components, §04 mobile
band framing, focus rings, aria labels, the Styles panel. Plus batch 1 of the call notes:
- Dark wash chips + the opaque
--hue-*split - 760 left-aligned reading column
- Bubble runs
- The tone mismatch Brad caught — the GPS card was
lifeCard("ok", …), a green card under an amber chip. Nowpend, titled Check-in · One arrived - “Schedule a meetup” in the composer (the renter has two legal paths; the button opens a chooser)
- The Terms v2.5 §8.4 screening comment
Plus batch 2 of the call notes (2026-08-21):
- Timers, both weights, with one magnitude formatter. Display timers on the inspection session and the late grace window; line timers on the meetup cards, the rental-active card, the extension and the review window.
SESSION_MINUTES = 45. Both meetup cards said “30 minutes”; FSD §10.2 says exactly one 45-minute session. §2 rule 3 applies. The field is now labelled Session, the FSD’s own name.STAGES[]untouched, so Brad’s gap note still carries the live question.- Map plate on the five location cards,
xMidYMid slice. (Pin geometry was revised in batch 4 — see below.) - Date separators, emitted only where the day changes.
- Segment counter and a Lane A / Lane B toggle in the capture bar.
rowDates()substitutes the Lane B range only wherestage.datesis the Lane A rental range, so the try-on’s own dates andSTAGES[]are safe. - A
latecard — it previously wore a red chip over a green “Return meetup” card and nothing else. Carries the 9-hour grace countdown (FSD §12.1) and the two resolution paths (§12.3, §12.4). New copy, needs Brad. .action-rowdeleted, not hidden.composerState()kept; the “+” now carries its list indata-menu.- Card foot: the last block in a card clears 16px off the bottom edge. It was sitting flush.
And batch 3 (2026-08-21), all ratified by CJ:
- Light chips became a tint, matching dark’s construction with a deeper ink shift. See §5.
- The chip dictionary, three renames across five stages, derived through
chipLabel(). - Full-width card actions.
- A cumulative thread. The single biggest change in the file: milestones are now
EVENTSon one timeline and a stage is a position on it. Adding a stage means giving it a phase, not editing a chain ofif (stage.id === …). Card counts along the spine: 1 · 1 · 2 · 2 · 3 · 4 · 5 · 6 · 7 · 7 · 9 · 10 · 10 · 11 · 12 · 12.
Batch 4 — a review round, and where several earlier decisions were reversed:
- Map pin centred in the geofence and ~40% smaller, reversing “anchored at the tip, ring raised 9px”. The ring is a 100-foot zone, not a point. The offset is derived, not eyeballed.
- The plate carries no status colour. Pin and ring were taking the card’s tone, so one venue was green on one card and amber on the next.
- The map redrawn to read as a place — road casings at two widths, a diagonal, parkland, water. Composition is constrained by the crop: only about y 26–106 of the 132-tall viewBox survives at desktop widths.
- One 12px gap between every card block, replacing per-block top margins. The facts panel had none, so it sat flush against whatever preceded it.
- “Check-in · One arrived” is live-only — once both have arrived it is the same check-in in an earlier state, not history, and the “Both arrived” card carries both timestamps.
- The inspection card is “In process” — amber, clock glyph — where it was green with an “Approved” badge on a session still running.
- Short button labels — Complete & Rent Item, Finish & Return Item.
- An entry point for extensions.
extension-requestedexisted as a stage with no way to reach it, and Brad’s ownrenterCanlists both “Request 1-day extension” and “Return early” — neither had a control. - Real make-and-model sample items, re-screened against Terms v2.5 §8.4.
Batch 5:
- Badge vocabulary closed. “Approved” was on eleven of twelve cards in a finished thread, including a check-in, a return meetup and the rental itself.
- Bolding — the fact label column, the
Commitment hold:lead-in, and the display-timer labels.
Batch 6:
- The rental carries the display timer, same as the inspection session — it is the longest clock the borrower is standing in. It settles to a line once the thread moves past it.
- Sidebar restructured — relation toggle full width under the headline, with a working filter beside it (All · Needs your action · In process · Completed · Cancelled & terminated). It replaces the old “show completed & cancelled” checkbox, which could only do one of those five things.
- ⋮, “+” and the filter all drawn open and gated, disabled items showing their reason. That is §7 item 7.
- On desktop the chip moved to the relation line under the product name; mobile keeps the right edge.
Batch 7 — 2026-08-22, this pass:
- D-3 SETTLED: the dock wins.
dockMode, the#dock-grouptoggle and the in-thread branch are all gone; the harness stopped comparing placements and started enforcing one across every stage at both roles. Two reversals recorded as reversals — see §5. - B-5 · conversation between the milestones. A
FILLERarray keyed byaton the same phase spineEVENTS()uses; messages and cards merge into one sorted stream. Three of the four items carry their own copy; the lens stays empty on purpose as the document’s empty-thread sample, and the harness asserts it. The Timeline tab renders milestones only, which is what finally makes the two tabs differ. SAMPLE_CHATwas not edited — and that was the choice, not the old prohibition. See §5 and the Styles panel: making his drill-specific five item-keyed changes the shape of his data rather than adding to it. Master check still reports all three blocks identical.- The conversation row previews the thread it is showing. It read the last
SAMPLE_CHATentry unconditionally, so after B-5 it said “EXACTLY.” while the thread ended four days later. Preview and timestamp both derive now. - The six remaining forms, and
formHtml()rewritten declaratively on the way. See §7 item 4. - A defect a screenshot caught and the harness had passed: the cancellation form’s two buttons both read Cancel and meant opposite things. Fixed, and a rule now fails any form whose two actions start on the same word.
368 combinations pass (184 per lane). STAGES[] byte-identical at 26,286 characters. Token drift clean.
7 / What is left, in order
THE POST-REVIEW BUILD LIST IS FINISHED. Every item on it is built. What follows is kept as the record of what each one was and where it landed; the only thing still waiting is B-11, on D-4 + Q3.
Polish batch 2— done 2026-08-21, see §6.- Chip dictionary — the three safe renames are done. A real
Latechip and an extension chip stay open and only Brad can settle them; his own gap notes rule both out.chipLabel()makes each one line. The two check-in chains— DONE 2026-08-22 as proposed stages inEXTRA_STAGES. Register R-7 is Brad’s adoption.Form stages— ALL NINE DONE, andformHtml()rewritten declaratively so a tenth is a data entry.Refresh Tap— DONE 2026-08-22, unblocked by R-4 settling at 30 days.refresh-tap-dueandrefresh-tap-failed, both inEXTRA_STAGES, both Lane B only — Lane A has no Shield to refresh, soactiveLane()forces B and the toggle hides. §13.6’s double-hold disclosure sits on the same screen as the button because the FSD says before the tap and calls it not optional; §13.4’s ordering is stated as the safety property it is; §13.5’s “the rental does not cancel” is the whole message of the declined screen. Four of those are asserted by name.All-stages view— DONE 2026-08-22, and built last on purpose: it inherits whatever the thread does, so every earlier decision shows up in it without the view knowing about any of them. Every screen rendered side by side as a real thread, pinned to the bottom of its own history, proposed stages marked.Menu gating— done 2026-08-21, extended 2026-08-22 so three items open the form they name.- B-11 · no-show reads as a consequence, not a filing — the one item still blocked, on D-4 + Q3.
8 / Still open — needs Brad
Superseded as the working list by Open items v1.6, which consolidates these with BUILD’s D-tags and the call notes’ conflicts into one register, keyed by owner and by what each blocks. IDs are preserved on both sides. What follows is kept for its evidence and reasoning; the register is what you work from and what goes in front of Brad or Jay.
- The seven
gap:notes in his own file: Cancelled vs Terminated on no-show; close-while-accepted; the 45-vs-30 minute field; missing chips; composer behaviour in the review window and in a dispute; hold amount on the pending card; the GPS one-arrived copy swap. - Repo/branch currency, Figma link and edit access, CSS-variables vs Tailwind for the handoff, and whether the
full DS token set lands in
boro-shared. boro-shared/tailwind/boro-theme.csscarries no semantic tokens — only the brand ramp, two surface steps, spacing and radii. There is currently nothing in the app to build this against. This is the real integration blocker.RentalStageChip.tsxfails WCAG AA on five of twelve states, worst 2.00:1 on the chip meaning a lender owes a decision. Live in the app today, independent of this file.
Waiting on CJ, not Brad
- The line timer treatment. Lab 15 round 2 recommends icon + sentence (“Meetup starts in 1 day”). Round 1’s pill was rejected as underwhelming — a chip means state everywhere else in this document, and a countdown is not a state. This is the one unratified lab.
- Past segment pips drawn in success, where the ratified decision named only “current one amber”.
- Whether the Lane A / Lane B toggle earns its place before Refresh Tap gives Lane B more to differ on. It currently changes the rental term, the facts row and the segment counter, and nothing else.
- The badge spread on a long thread — a finished rental shows a column of Completed badges. Raised, not settled.
Settled since — kept visible so they are not re-opened
- D-3, the dock placement — SETTLED 2026-08-22 by CJ: docked. See §5 for both reversals and their reasons. Jay no longer chooses this; the toggle that would have let him compare the two live is withdrawn with it. Worth saying at the workshop rather than letting him find it, since the register named D-3 as the decision the workshop existed to make.
Added by the 2026-08-21 review — see BUILD §Do not resolve
- The QR model (D-1). (Built as a proposal 2026-08-22 to CJ’s ratified reading; Brad and Jay still react.) Three live positions on one mechanic: FSD §10.1 (one booking code, geofence-gated, valid only with both devices inside), Brad on the call (“they should both have a QR code or scan the other person’s… whoever wants to do it” — his second call running asking for mutual scan), and a lender-side rendered code. It is the substrate for both check-in chains, so a wrong guess gets built on twice.
- The blue own-bubble (D-2). Reverses
surface-inverse, hedged three times, and there is no blue in the token set — a new semantic token and a ninth entry on §9, where none of the eight are applied. Also unresolved: CJ argued the counterparty’s bubble should carry the emphasis; the point was never answered. - No-show tone (D-4). Brad wants red, not grey. Tone derives from
TONE_BY_STATUSandterminatedis neutral, so this needs either a platform-wide move or a new status — the same question as Cancelled vs Terminated. Settle both together. - Extension decline window (D-5). CJ referenced “back within that one day”; FSD §13.2 names no lender response window. Either it exists somewhere unread or it is an assumption that will get built.
- “Make these thicker — the active one” (D-7). CJ, unanswered on the call. Unclear whether it means the segment pips (which would fold into the Q15 pip question) or the display timer’s progress bar.
Three things from batch 2–4 that want Brad’s eye specifically
- The
latecard is new, and BoRo Recovery is not in hisrenterCanlist — it comes from FSD §12.4. SESSION_MINUTESmoved 30 → 45 per FSD §10.2; his gap note still carries the question.- The meetup cards no longer print a weekday — his data calls the same date Sat on the try-on and Tue on the pickup, which was invisible until the thread became cumulative and put both in one view.
9 / Design System amendments this work has accumulated
The register is Design-system amendments. Twelve, none
applied. Doc first, then regenerate boro-tokens.css, boro-typography.css and the Tailwind preset
together — never one alone.
This section used to hold the list, and so did two other places. They disagreed: this one numbered eight,
R-5said ten and named.badge.infowhich this one had missed, and this one named the component specs — the largest item — whichR-5had missed. The union is twelve. Nobody was wrong on purpose; each list was written in a different pass and none of them was the one place. It is a dataset now, and this section points at it.
The one rule that survives here, because it was learned here: --content-tertiary sat on the list until
someone looked, and the DS had already fixed it at v2.21 with the stone-550 half-step — the list was simply
a version behind. Check the DS changelog before proposing an amendment.
10 / Lab index — the decision record
11 / Working pattern that produced good results
- Propose options in a lab before implementing anything visual. Build the options on real tokens, measure contrast and geometry live in the page, recommend, then let CJ pick. Several recommendations reversed once measured — the Soft chip ceiling, the orange numeral on dark, the surface-lift row on dark.
- Render and look.
./_tools/shot.sh '<js state>'renders any screen to a PNG without touching the source — then crop withsipsand read it. Bugs that pass a smoke test are visible in a render, and this session had two of them: a form whose heading was scrolled off by the webfont callback, and a merge branch that never fired because a renderer readwindow.stage. - Name your own mistakes plainly and fix them. Several defects in this project were in the labs, not the
product — a
.previewclass collision, a.m spanrule catching chips, a nested.specresetting dark tokens. - Check the DS changelog before proposing a DS amendment.
--content-tertiarywas re-derived from rendered pixels as a “new” AA finding; the DS had already fixed it the day before with astone-550half-step. The file was simply a token version behind. Anything holding a copy of the tokens drifts. - Reversals are normal — record them as reversals. Four ratified decisions were overturned in one day (light chips solid → tint, pin anchored → centred, plate tone → monochrome, rental line → display timer). Each is written into the Styles panel as a reversal with its reason, so nobody “fixes” it back.
- Build the assertion with the decision. Every new component in this session shipped with a harness rule. That is what caught the finished rental still counting down, and the exception stage whose card never rendered because its phase sat below its own event.
- BLUF every response, detail below a
---.
12 / Changelog
v3.7 — 2026-08-22. W-18 and W-2’s interim copy. Off-site testing is built as an axis on the pickup decision — Decline disabled with its reason rather than removed, because §10.4 disables it and a gated control shows its reason. The card carries the locked sentence the rule depends on: the trigger is the ACT of taking the item, not the passage of time — a rewrite that loses it turns the rule into an arbitrary timeout, so the harness fails the card if it stops saying it. W-2: both terminal cards were pricing a case §6.1 does not define, and were incoherent on their own terms — “neither side… forfeited to the other party” has no other party. They now state the event and name what is and is not settled. The rule is still owed; this is a holding position.
v3.6 — 2026-08-22. The agreed, unambiguous batch off the workshop, and a new living document.
Handoffs is the targeted pass R-6 has been asking for since 2026-08-21 — the
things waiting on Brad and Jay, phrased so they can be answered without opening the file, with an answered
section that keeps closed items visible. W-22 gives every stage a Building it block — the governing FSD
sections, what the screen has to do, and a Watch out line — which is the clearest thing Brad asked for in
either call; it lives in SPEC_NOTES in the presentation layer, in its own block, because why: is his copy
in his master and this is ours. W-20 (Lane A never reads “None”), W-15 (the capture is mandatory and
has a control), W-10 (the badge column lines up — a folded card’s chevron had been pushing its badge 30px
inboard of an expanded card’s).
W-20 exposed an assertion that was passing on a keyword rather than on a fact — N-3’s folded-card money rule was exempting anything that said “Shield”, so a lane threshold that had always been there failed the moment the wording changed. Tightened to what N-3 actually prohibits.
v3.5 — 2026-08-22. The 2026-08-22 Jay workshop, and the first item off it. W-23 built — the reference
panel is docked right, full height, with its own scroll and a close control inside it; under 1180px it overlays
the preview row rather than the viewport, which the first version did and which covered Next on the capture
bar. The pop-out window half of W-23 is deliberately not built and is flagged. §5’s panel line is amended:
placement moved, the Hidden default did not. The workshop’s 27 W- items are in REGISTER v2.0 §5b with the
evidence in Workshop record. C6 closed; three canon conflicts opened (W-1
against FSD §10.2, W-2 against a case §6.1 does not define, W-3 against LOCKED §8). A recurring measurement
trap became a shared harness helper — noTransitions(), for reading a computed style through a CSS transition.
v3.4 — 2026-08-22. The post-review build list is finished. Refresh Tap (§7 item 5) and the all-stages
view (§7 item 6) both landed, so nothing on the list is outstanding and B-11 is the only item still waiting
on anybody. Refresh Tap is two Lane-B-only proposed stages carrying §13.6’s mandatory disclosure, §13.4’s
ordering property and §13.5’s the rental does not cancel. The contact sheet renders all 44 screens as real
threads. Two findings and one latent bug came out of the pair: R-11 (a Lane B term has four taps and
the spine has one position for the rental — the tap is built as an interruption, not a step); a tile could not
be a <button> because the threads inside carry buttons, and the harness passed the mangled DOM while a
render showed it instantly; and activeLane() had existed since the toggle landed with only rowDates()
calling it, so “hidden also means inert” was true of exactly one thing.
v3.3 — 2026-08-22. Six changes from CJ, and R-4 settled at 30 days, which unblocks Refresh Tap and
leaves no build item waiting on anybody. Card tone gained a fourth kind so amber could be reserved for a real
clock or real money, and the hazard triangle for a real error. CR-12 closed — one info affordance per
layout; it had been duplicated in both, not just on desktop. Send lights up on input, the thread pins after
the composer exists, the active tab rule is 3px, and the sidebar filter split earlier in the day gained its
missing .badge.info tone. Three of these were things this panel already claimed were true — .ready,
the pin, and the info rule — which is the pattern worth noting: a Styles entry is a claim, and until there is
an assertion behind it, it is only a claim.
v3.2 — 2026-08-22. The document became walkable. Card actions navigate (go / form / inert, forward
only); the end-to-end walk is asserted — seventeen presses, Interest to settled — with the try-on branch
asserted separately. Renting / Lending and the filter are live controls rather than painted state. Cancelled
and Terminated split into two filter rows; menu labels carry their own weight. Four findings came out of the
wiring and are recorded rather than papered over: schedule-denied is one stage doing two jobs (Brad);
Decline at pickup exists in FSD §10.4 and not in STAGES[]; "I'm Here" carried a marker nothing read, and
the harness had been asserting the marker rather than the behaviour; and the cancelled sample row became
unreachable when its bucket was split under it.
v3.1 — 2026-08-22. This pass: D-3 settled, B-5 built, the six remaining forms built, and five defects in these documents fixed.
- Header — version, date, screen count (42, with the arithmetic shown), companion versions corrected to
REGISTERv1.6 andBUILDv1.8, new verification line, and a note on which baseline numbers are constant and which are expected to move. - §1 and §6 — the file is 42 screens, not 23 and not 33. “33 screens — 23 stages, ten proposed, and three form screens” did not add up in either direction; the arithmetic is now written out.
- §4 — the D-tags in this section were a collision and are now V-tags. They were labelled D-2/D-4/D-5/D-6, which are four live IDs in a different register meaning different things. A session told not to resolve D-1…D-7 and reading §4 first would have protected the wrong things. Two rows added (V-7 extension increments, V-8 report-a-problem) from the FSD reading this pass needed.
- §5 — the dock rewritten from reversed and under test to settled, with both reversals recorded.
- §7 — item 4 corrected (it said two forms were done when three were, and all nine are now); items 5 and 8 marked blocked on R-4 and D-4; item 7 extended.
- §8 — a Settled since block, so D-3 is not re-opened.
- §13 — starter prompt corrected: it still said 33 screens and still told the next session the dock was undecided.
v3.0 — 2026-08-22. Shipped without a changelog entry; reconstructed here from §6b and §7 for traceability rather than left blank. The check-in chains as ten proposed stages, B-4’s collapsible past cards and merged completion summary, D-1 built as a proposal, and the register’s first consolidation.
v2.1 — 2026-08-21. Folded in the 2026-08-21 review call. Nothing was removed; four changes and two additions:
- Header now points at Build order as the work order and Call record as the call record. The predecessor build list is at v1.3 and superseded, not v1.1.
- §5 — the action dock is a reversal under test, not a settled removal. The old line would have had a future session restore the removal Brad asked to undo.
- §5 — the badge vocabulary was missing
Ready to pair, which is live on the pairing card. Twelve entries, not eleven. A harness rule generated from the short list fails that card. - §7 — ordering superseded by BUILD’s ship gate. G-1 and G-2 come first; two of the eight forms pull forward.
- §8 — five decisions added from the call (D-1, D-2, D-4, D-5, D-7 in BUILD’s numbering).
v2.0 — 2026-08-21. Six batches of build, the DS restyle, cumulative thread and gated menus.
13 / Starting the next chat
Paste this, then let it read.
Read these four documents before doing anything, in this order:
1. .../App/Chat Lifecycle/HANDOFF_ChatLifecycleSimulator.md (build state)
2. .../App/Chat Lifecycle/REGISTER_OpenItems_ChatLifecycle.md (open blockers, by owner)
3. .../App/Chat Lifecycle/BUILD_ChatLifecycle_PostReview.md (the ordered work list)
4. .../App/Chat Lifecycle/NOTES_CallReview_ChatLifecycle.md (the 2026-08-21 call, quoted)
The target file is in that same folder: chat-lifecycle-stage-simulator-v2.html.
It is a 42-screen clickable specification of the BoRo rental conversation - not a
prototype - reviewed with Brad Larson and shown to Jaydan Nguyen.
Start by running ./tools/lifecycle/verify-all.sh and reporting the three baselines, so any
regression from this pass is attributable to it. Then give me your plan before
you edit anything.
Constraints, all of which the docs expand on:
- STAGES[], SAMPLE_CHAT and ALWAYS are Brad's master. They MAY be edited with my
say-so, but the change goes to Brad with the diff the master check prints.
Default to deriving in the presentation layer - chipLabel(), rowDates(),
FORMS[] and EXTRA_STAGES all exist because that was the honest way.
- Check token drift against boro-ops/brand/boro-tokens.css on every deploy.
- The document stays self-contained apart from the two font hosts, so images
embed as data URIs.
- Where the FSD and a built screen disagree on transaction behaviour, the FSD
governs and the screen is the defect.
- Item eligibility is screened against Terms of Service v2.5 section 8.4 by make
and model. FSD section 14 sets tiers and defers to it. No summary of either is
screening authority.
- Measure AND look. Neither substitutes for the other - the harness has passed
things a screenshot caught immediately, and the eye has missed values only the
harness could hold.
- Do not resolve anything tagged D-1 through D-7. Build the comparison, leave the
decision, and say in your summary which ones you hit. D-3 is settled (docked)
and D-1 is built as a proposal; the rest still stand. The only D-register is
REGISTER §1-3 and BUILD's "Do not resolve" table - HANDOFF §4 carries V-tags,
which are verification results, not decisions anybody owes.
- Every decision lands in the Styles panel, and a reversal is recorded AS a
reversal with its reason. Build the assertion alongside the decision.
What to pick up first, unless CJ says otherwise: the all-stages view (§7 item 6) — it is the last
unblocked build item in the file. Everything else that is left is waiting on somebody: Refresh Tap needs
R-4 (MAX_TOTAL_RENTAL_DAYS is 12 in the shipped constants against 30 in FSD §13.1) and B-11 needs D-4 +
Q3, which are one question and settle together.
Then it is decisions, not building. The register’s §7 names who owes what.
Two traps this project has already fallen into, both in §2 and worth re-reading before touching anything:
getComputedStyle returning stale values across a theme switch, and an element id silently becoming a global
so a renderer referenced window.stage instead of its own parameter. Neither threw; both passed the harness.