Jobs

The door b-job never had. Not a dashboard, and not because dashboards are vulgar — a list carrying portfolio metrics becomes the surface he checks instead of Today, and Today's whole claim is that one thing needed him.

1 · The list
Four jobs, which is what six starts a year on twelve-to-eighteen-month builds actually looks like. Each row says where it is and what it is waiting on. Nothing on it is a metric.
No margin column, no revenue, no health dot

Aggregate margin across jobs is real and it is his business. It is not on the way to a job, and putting it here creates a second candidate for the morning screen. The first cut has one, and it is Today.

And no contract type on the rows

It is a fact about the money on the job screen, not an identity for the job. Four labels down a list would teach him to sort by it, which is not a thing anybody does.

2 · The job
The job answering is this one alright before anything else. One open decision, money, what is coming with what late costs, settled.
Read, never written, is on the screen

Not in marketing. It is the promise that makes the rest possible — the sheet is read once so a change order can come back out in his columns — and a builder who has to be told it by a salesperson will not believe it.

Margin is here and can never be inferred from her screen

Contract minus committed is not margin, and nothing on the homeowner's side puts a figure one subtraction away from it. That is the hard line, and it is why Sam sees a shorter version is a link to her screen rather than a claim about it.

How fast they answer is a fact about the job

They answer within a day, most of the time. The other half of the per-item line on Send, and the only other place time-to-decision surfaces. A job that is slow to answer is a fact about that job, the way its contract type is — it changes what he raises and when. There is no median across jobs anywhere in this app, because a number waiting for him every morning is a score, and a score is a thing to manage rather than a thing to know.

Every figure carries its source

From QuickBooks, read this morning. G4 in the small: a number with no provenance is the thing this product exists to replace, and the rule applies to his screens as much as to hers.

3 · One job, and the list is skipped
At one to three homes a year a builder frequently has exactly one live job. The door persists and goes straight through — a list of one is not an empty container, it is a wasted tap.
The door does not disappear

Only the list does. Removing the door would be a second information architecture for the commonest case, and the builder who starts a second job would find his navigation had changed shape underneath him.

And the explanation is inert rather than absent

The same component the permission model uses. It says what will happen when there is a second job, once, quietly, at the bottom — rather than leaving a builder to wonder whether the app has lost the list.

4 · A job that is neither fixed price nor cost plus
Ridgeline is GMP — cost plus with a cap and a savings split. The money loop behind it is not built, and the product still must not refuse the job.
Contract type adapts the money step and gates nothing

Every flow on this job exists and works. What differs is one section, and what is missing there is named rather than hidden — the honest sentence goes in the reader's own terms and no badge says not built. It was drawn in a .card-absent until 2026-09-06 — a --surface-container fill at the card radius, which is a region wearing a card’s shape and is the one thing the region rung exists to prevent. The meaning was never in the box and the sentence still carries all of it.

Four types, and the mix is unmeasured

The only hard data anybody has is a vendor's: 38,000 residential projects, fixed-price around 80% overall and cost-plus reaching only ~46% even above $1M — and it pre-dates the material shock that pushed builders toward cost-plus. The honest read is that fixed-price is probably still the majority, cost-plus is common and rising, and this segment's mix is unknown. Design for four; assume none.

The visit door is a ghost, and the rule was read before it went there

cta's rule is that ghost is for a quiet way out, not for a state-changing commit. This one commits nothing — it opens frame 10, where the commit is — and the job screen keeps its single primary free for whatever the job actually needs answering. A filled button here would say adding a visit is the most important thing on a job that has nothing open, which is the opposite of what frame 2 spends its primary on.

And no cap bar

A guaranteed maximum is an externally enforced ceiling, which is the one case where a denominator is permitted. It is still not drawn as a fill here, because there is no running total to fill it with — a bar over a number the product cannot yet compute would be a fiction about a fiction.

5 · Quotes, and the G4 bug drawn on purpose
A feature with three routes, a capability and a live bug, and zero mentions across the whole spec set until the coverage audit found it. The broken state is above; the fix is below.
The broken state is drawn on purpose

Because a fix with nothing to compare against gets re-broken. Somebody reading only the corrected list has no way to see what the mistake was, and the mistake — making one badge carry three unrelated facts — is the reusable lesson.

Provenance is never dropped on the way to the screen

And it is not decoration here: a quote is how an allowance gets a real number, and it is the input to everything the variance layer then does. $3,400 over means nothing without Reed Millwork quoted $21,400 against an $18,000 allowance on 14 May.

Two of the three are unnamed, on purpose

The fiction has three trade contacts and Reed Millwork is the one that quotes cabinets. A name invented in passing to fill a comparison row is exactly what the arbiter file exists to prevent.

6 · Search, across the jobs
The magnifier has been on the builder's appbar on twenty frames across seven pages and opened nothing. Mockup 11 draws the homeowner's, over one binder; this one is over four jobs at once, which is a different question and gets a different answer.
Open first, then settled, and never ranked

The homeowner's search on mockup 11 groups by what a thing is — a color, then where it appears, then the papers. A builder is not browsing: he is looking for the one thing with his name on it today, so the top group is whatever is still open and the sort inside it is by job. Relevance ranking is what puts a settled decision from April above a quote due on the 29th.

Every row carries its job

Which the homeowner's rows never have to. Cabinet package means two different things on two jobs, and a search result without a job name on it is the fastest way to answer the wrong homeowner. It is in the caption rather than as a chip, because it is provenance.

Why it is on this page

Search is over the jobs, so it sits with them. It is also the answer to a thing mockup 20 leaves open: at four jobs the dock is three doors and a list, and by twenty the list is the wrong instrument — the escape hatch is what makes that survivable, the same way it does for the lenses on mockup 11.

7 · Archive this job
The builder can see the consequence but cannot perform the owner act. The refused control explains whose seat can archive without hiding what archive means.
The refusal still states the consequence

The builder can understand what will happen without being offered a request the server refuses. Typing the house name belongs to deletion; archive is reversible.

Owner seat, not account administrator

The ruling is about seats.role. A builder seat can keep reading an archived job but cannot move it in or out of either list.

8 · Behind Show archived
The default list ends at Show 1 archived. Opening the fold names the job, its archive date, and the way back without confusing archive with completion.
The count is the affordance

Show 1 archived is enough on the default list. The opened state above proves what is behind it and does not make every builder scan old work on every visit.

Finished state unchanged

The archive date is shown beside the job and never substituted for its build status. closed_on and archived_at answer different questions.

9 · The binder is current, and read-only
Opening the folded row does not look like a network failure. The job reads normally; the first writing control explains why it cannot act and offers the way back.
Read-only is a project fact

The banner persists where writing controls would otherwise act. It is not an error overlay, because the binder loaded and the device watermark is current.

Restore is the exception, on the owner side

Ordinary writes stop. The restore route remains available to an owner seat, while this builder view names the way back without pretending it can perform it.

10 · Putting a visit on the job
The way in is frame 4's Coming section, and the way out is the arrow. Two things are required — who is coming and what day — and the third is offered because a visit is usually about something being decided.
Who is coming is free text, and that is the design

trade_visits.trade is text NOT NULL and nothing joins it to contacts. A visit gets written down in the ninety seconds between a phone call and the next thing, and a picker that first demands a contact record turns a note into data entry — which is how it stops being written down at all. The confirmation in frame 12 is a contact, because a claim about who said something has to survive being asked a year later.

A calendar day, and never an instant

scheduled_for is a date. Wednesday is Wednesday whichever side of a timezone he is standing on, and check-date-handling.mjs gates that distinction across the codebase precisely because a date that converts is a date that moves overnight.

The decision field is drawn empty on purpose

decision_id is real and nullable, and Ridgeline has nothing to put in it: FICTION.md holds that Ridgeline and Alder Street are drawn thin on purpose — neither has a room, a palette or a decision set, and neither may be given one in passing. So the empty state is the honest drawing here rather than a shortcut, and the populated one belongs on a job that has decisions.

The consequence names what does not happen

Nothing is sent to Hendricks. That sentence is under the button because the assumption it corrects is the expensive one: a builder who thinks the app told his framer, and did not ring him, has been made worse off by using it.

11 · Correcting it before it has happened
Hendricks moved to Friday. The day changes, and the confirmation below it cannot be filled in yet — not because a rule forbids it, but because there is nothing to confirm.
A constraint drawn as an affordance, not as a refusal

trade_visits_confirmed_implies_happened is a CHECK in 0001_init.sql, and the gateway restates it at the boundary so a violation is a refusal rather than a 500. This frame is the third place it lives and the only one a builder ever meets: the switch cannot be moved, so the sentence it would have refused with never has to be written.

Inert, and never absent

states.css's rule for a capability you lack — visible and inert, never absent, because absence teaches nothing and reads as broken — applies to a control that is not reachable yet for the same reason. Hiding the confirmation until a day appears would leave him wondering where the app put it. This is frame 9's shape, the flat card with the reason in its own line, rather than .tg.lock: that variant means on, and a floor stops you turning it off, which is the opposite fact.

Correcting is not rescheduling

Q146 rules that a correction reaches the whole row in place: the trade, the scheduled day, the decision it is about, the day it happened and the confirmation, all of it, one row. There is no second visit and no superseded copy, which is why the sentence under the button is about identity rather than about saving.

12 · Who said so, and how
The window package came a day late and Ana texted him at 7:12am. Three fields behind one switch, because a time with nobody behind it is a figure without its cause.
One switch over three columns

CHECK (num_nulls(confirmed_at, confirmed_by_contact_id, confirmed_via) IN (0, 3)). The table has held that since 0001_init and its own comment says why: a naked 7:12am with nobody behind it is the shape U1 refuses — a figure without its cause. Three separate optional fields would let a builder produce exactly that and then be told off for it. One switch cannot.

The confirmer is a contact, and it has to be one on this job

confirmed_by_contact_id is a real foreign key, and Ana is a contact rather than a seat — the window supplier does not sign in. The catch the schema cannot hold is that contacts is account-scoped, so the key alone would accept somebody from another of Dave's jobs; the store checks contact_projects instead. That is a membership question, not a foreign key, and it is why the picker offers this job's people rather than his address book.

This is the row frame 4 reads back

Scheduled for one day, happened on another, confirmed by a person who does not have an account. The deck has drawn that row on three pages for a year — here, 20 frame 2 and 26 frame 3 — and this is the first frame that says how it got there. Two dates on one row is also the whole reason X4 is a correction rather than a result: the scheduled day is not overwritten, so a day later than you had it is a fact the job can still state.

13 · No signal, and this one waits
He is standing in the house. A photograph goes in, a checklist tick goes in, and a visit does not — and the screen says which rather than graying out a control and leaving him to guess.
The asymmetry against the checklist tick is deliberate, and the deck ruled it first

26 frame 1's tick files offline and drains on signal; both of these routes are OnlineOnly. That is not an inconsistency to tidy up — it is 26's own rule, written before either route existed: everything he can look at, he can look at; everything that touches somebody else's record waits. A photograph and a tick are observations he is entitled to assert alone. A visit is an arrangement with a second person, and a phone that quietly accepts one has promised something on the framer's behalf.

No retry button, and that is a rule rather than an omission

states.css: an error that can retry itself already has — but not while offline, where a retry button is a button that does nothing, and offering it is worse than the wait. So .say .act points at what is still writable. The photograph is a real answer here: 26 frame 3 files it against the delivery once he is back, which is the same fact reaching the record by the road that is open.

Neutral ground, never a warm tint

No signal is not a fault and it does not take --terracotta. Being offline in a half-built house is the building's doing, and coloring it as an error blames the reader for it. The as-of line is stated once at the top and never per card, for the same reason: a stale screen that says so fourteen times is a screen of warnings.

Inert, with the reason reached for

Drawn in .inert.reached, which is the state after he taps it. At rest it is rung 1 and the sentence is closed; reaching for it opens the one line. Both states exist and a mockup draws the second, because the first is indistinguishable from a screenshot of a working button.

14 · What waiting costs
The figure 20 frame 2 puts on her Now, from the side that produces it. Two numbers is one number: a date it stops being free, and what it costs after. Neither can be saved without the sentence underneath it.
Four fields, because the table has four and none of them is optional

waiting_costs holds free_until, amount_cents, quoted and assumed, all NOT NULL, with waiting_costs_has_its_cause requiring the last two to be non-empty after trimming. So the screen has no optional section: a form that let him save a bare number would be drawing a row the database refuses. The $0 half is not a field, because it is what free means.

The two sentences are the product, and the number is the part that is easy

U1 says a figure never appears without its cause, and this is the only screen where the cause gets written. Everything downstream renders the pair: 20 frame 2 as Quoted / Assumed under the split, 21 frame 4 as the one-line $3,400 if late. Both are reads of what he typed here, and if he types nothing useful they render nothing useful.

Removing it is a claim, so it is a row and not an X

DELETE .../waiting-cost is a guarded delete rather than a patch to null, and API_SURFACE.md says why: a builder taking a cost off is asserting the delay stopped costing, not undoing a typo. It sits at the bottom under its own heading, reachable and unhurried, and the copy names the consequence on her screen rather than the operation on ours.

Reached from the decision, never from a money screen

Zero or one per decision, keyed on decision_id. It is a fact about one thing being undecided, so it is entered where that thing is and does not appear on 23 beside draws and change orders, which are facts about the contract.

15 · Booking an inspection
Frame 5 of 41 puts inspections on the clock and 40 files them under Papers. Both are reads. This is the act, and it is the same shape as putting a visit on the job because it is the same kind of fact: somebody is coming, on a day.
A day is nullable and the label is not

inspections.booked_for is a bare date with no NOT NULL; label has one. That is the right way round and the hint says so out loud: a builder knows he needs a rough electrical long before the county tells him when, and a form that demands the date first is a form he closes. inspections_upcoming indexes (project_id, booked_for) WHERE result IS NULL, so an undated one is simply not upcoming yet.

This was served for eight days before any frame drew it

X1 shipped this route on 2026-08-25 and X2 shipped its correction, and the deck drew neither. It was found on 2026-09-02 while recording the same gap one track item later, which is the honest reason it is here rather than a redesign: a route with no frame behind it is a screen somebody will invent under deadline.

Not a paper, and Papers is where it ends up

add_paper_screen.dart offers permit, certificate and other, and an inspection is deliberately not among them. A permit is a document somebody hands you; an inspection is an appointment that becomes a result. They meet in the binder and they are not entered the same way, which is why this is a job screen and not a fourth paper type.

16 · What the inspector said
It passed, and Ben signed it. Three fields, and they move together: a result with no day and nobody on it is a rumor, and this one releases a draw.
A failure is a result, not a deletion

X2's whole claim is that recording does not replace the scheduled identity: the booked day stays, the row keeps its id, and a re-inspection is a second row. So Failed is a normal option in a normal list rather than a destructive act, and the copy under it says what happens next instead of asking whether he is sure.

Three fields that arrive together, drawn as one screen

result, resulted_on and signed_by are three nullable columns describing one event. They are not gated on each other by a CHECK the way a trade visit's confirmation is, so this is a convention rather than a constraint — which is exactly why it has to be drawn as one screen. Nothing else would stop a result being filed with nobody behind it.

And the consequence names the money

41 frame 5's argument is that money is released by an inspection. The sentence under the button is the only place in his app that says so at the moment it becomes true, which is worth more than the same fact stated correctly on a screen he opens later.

Why the list is not a dashboard

Not puritanism. A list carrying margin, revenue or a health indicator becomes a surface a builder opens instead of Today — and Today's entire claim is that it will tell him when one thing needs him and otherwise leave him alone. Two surfaces competing to be the morning screen is how that claim dies, and the losing one is always the quiet one.

What is not settled

The money loop — estimate to budget to QuickBooks. It is the long-term commercial heart, too heavy for the first cut, and partly off-strategy: the commodity core is not worth rebuilding. What money means is settled; building the loop is later, and it is why frame 4's cap has no running total behind it.

The contract-type mix for this segment, which is a validate-with-real-builders item. Four types are drawable today and none of them is assumed to dominate.