The Time Axis

An earlier page (18-where-time-lives, archived) compared three places the time axis could live and picked its own tab, arranged as a dated spine. This is that spine. Almost every record on it already ships — the phases, the two dates, the draws, the change orders, the inspections — filed across Budget, Plan and Maintenance where a binder reader will never look. The two things that do not ship are the two the paper binder is best at.

The placement was superseded. The design was not.

What 18 concluded, and this page was drawn to: the time axis gets a tab of its own, taking over the shipped Build destination, and that tab is a dated spine.

What superseded it. A blind comparison (23-which-navigation, archived) settled it, and the winner is drawn in this deck on Now and The house: three doors — Now · The house · Ask. There is no tab to take over. Nor is time a lens: 21 drew a fourth by when lens on its own frame 7 and cut it, because every one of its group headings turned out to be a sentence the other three lenses already print on a sub-line. The lenses are three — by room · by material · by who — and what time does in the architecture that stands is order the Now door.

Where this spine went. It is a phase view rather than a record view, which is why it did not fit the lens row and does not have to. It is now the sixth row of the binder, "The build so far", under The house, beside Money, Papers & permits, People, Care and Plans & drawings. That is FICTION.md's tab table, and it is 21's own wording: Plan becomes the binder's sixth row.

What that changed on this page. The name, which is now the pinned one and no longer the short form. And every annotation that argued this was a takeover of a shipped tab. No frame changed. No date, no arithmetic, no gap, no verdict about what is built — a spine, a two-column phase matrix at three widths, the delays named ahead and the five visit windows are the same drawings whichever door they are behind. 21 says as much in its own words: mockup 19's dated spine is not superseded at all.

1 · The build so far
Where you are, on a rail — and the phase you are in is the first thing on it. The three that closed collapse into one node above it, because a spine's whole advantage over a tracker is that it can give the current phase more room than the finished ones, and a first draft that spent 707px on three past phases before reaching the open one gave it less. Opened, it holds the rooms working in it, the inspection that gates the next payment and the three things that usually go wrong here. No bar, no percentage and no "phase 3 of 5".
Already in the code

The five phases and their order are canonical_phases with sort_order, served by GET /api/phases. Both phase titles here are real strings: Into the finishes is phaseFinishCloseout and Finishes & Close-Out is phaseLabelFinishCloseout, two registers of the same five in app_en.arb. The inspection is inspections, the draw is project_draws, and both have four live routes.

Nothing stores this yet

The screen. Not the records under it: the four rooms inside the open phase are derivable rather than stored — decisions.phase and decision_zones both ship, so the join needs no migration — but the dates beside each room are invented, because decisions.due_date is written only by a builder, by hand.

Where this screen sits, and where it used to

This annotation used to argue placement, and the argument is spent. It said the screen was missing but not the tab, because HcShell ships a fourth destination labeled Build (navBuildMobile, hc_scaffold.dart:165-166) going to /builder and BuilderScreen, and that the retired deck's mockup 18 closing note therefore had to argue a takeover. That shipped tab is still there and still goes to the builder-connection surface — what has gone is the reason to want it. The navigation is three doors, and this spine is the binder's sixth row, The build so far, under The house. Mockup 21 dissolves the Build destination on its own terms: the builder becomes a contact and a thread.

And the collapsed node is not free either

Rolling three closed phases into one line reads as an ordinary disclosure control and is not: it needs to know each phase closed, and a completed phase is exactly what nothing in the app records. Expanding it would need the same two dates frame 2 is asking for. The collapse is drawn because it is the right shape on a phone, not because it is the cheap half.

The join that does not exist

The draw and the inspection are drawn together because the lender releases on the inspection, and nothing in the schema joins them. project_draws carries a free-text category and no phase and no inspection id. inspections carries a phase. So the ledger and the schedule use two different vocabularies and cannot be asked the same question.

Node states, and what they are not. Filled is a phase that ended, the ring is where you are, hollow is ahead. There is no track that fills and no denominator anywhere on the frame — which is the correction, because the Plan screen's own timeline draws both.

2 · Target and actual
The paper binder's most-used column, and the one with no home in the schema. Two headed columns per phase rather than a table, so the two dates never have to be told apart by position. This is the narrow arrangement; the same data is a real four-column table on the desktop canvas below.
Already in the code

The phase target column is stored only when someone supplies an actual project target. Track L does not create canonical phase-duration columns or seed a second phase catalog; it derives decision due dates from the latest actual phase, authored workstream duration, and lead-time inputs, then marks those dates with due_date_source='derived'.

Nothing stores this yet

The actual column was the sharpest gap on the page, and Track L closes it. PUT /projects/:p/phases/:ph writes project_phase_status.target_date, actual_date, and slip_cause, stamps updated_at for sync, updates projects.current_phase_id from the latest actual phase, and recomputes derived decision due dates in the same transaction.

And no target, per phase

There is still no separate due or planned column on project_phase_status. The stored phase fields are target, actual, and slip cause; decision-level due dates are materialized on decisions.due_date only when the system can derive them, and source-null explicit dates are preserved.

Which makes both columns of this frame new storage, not new UI — two dates on a phase, and a way for the person who knows to write them.

3 · What usually goes wrong here
The paper names the likely delays in advance on every stage page, before they happen and while there is still something to do about them. Three for the phase you are in, one line of why, and one line of what you can actually do. Nothing in HouseChalk does this, and it is the cheapest good idea in the source material.
Nothing stores this yet

All of it, and there is nothing close. Grepping every migration for delay, risk and milestone finds one column: decision_library.delay_consequences, which is static library copy hanging off a decision and says what happens if you are slow to choose. That is a different thing from what usually goes long in a phase. There is no delays table, no risk register and no per-phase advisory of any kind.

The shape of the work

A library table keyed on the five phase slugs, three to five rows each, seeded once and read on every project. Roughly the shape library_maintenance_tasks already has, which holds 53 hand-authored rows and is seeded from data/library/maintenance.json. So the pattern exists and only the content is missing.

Which is the argument for doing it early. This is content before it is engineering, and it is content nobody else writes because nobody else is willing to say out loud that the cabinet clock starts on the signature. The table is cheap; the fifteen sentences in it are the product.

One line on the frame is not library content and could not be: yours are drawn and priced, sign the change order. That reads this project's own project_change_orders row. A per-phase advisory is only worth the screen if the generic line can be followed by the specific one, and that is a join a library table cannot do on its own.

4 · Worth being there
For a homeowner building at a distance this is the highest-stakes logistics call in the build, and the paper answers it on six separate pages. Five moments justify standing in the house. Exactly one of them justifies a flight if you only take one, and in this build it has already happened.
Nothing stores this yet

All of it, and nothing in the schema is named for it. No site_visits, no visit_windows, no visit_date — no table, column or index anywhere carries the word. Grepping the migrations for visit does return two hits, but both are prose in a comment (0022:7 revisit, 0072:10 on every visit), so the honest sentence is "no storage", not "no occurrences". There is also no calendar screen: the app has one date-picker widget (hc_date_picker.dart) called from three places across two screens, and no timeline-of-events surface at all.

And the photos have nowhere to go

The capture exists. The record does not — and an earlier version of this annotation had that backwards. A homeowner can take a progress photo without a message thread: SelfManagedCaptureCard offers note, voice and photo on the Build tab (builder_screen.dart:157, wired at :381, :443 and :459), and the last two are the no-builder and invite-pending states, which have no thread behind them at all. The zone workspace reaches camera capture too, and that path does attach the photo to a room. What is missing is narrower and still fatal to this frame: no gallery, no chronological view, no way to ask what is behind a given wall, and no FAB on the homeowner shell — so the thirty-four pre-drywall photos can be taken and never found again.

The one part that is nearly free

Which visit windows exist and roughly when they fall is derivable from the phase the project is in, the same way the delays are. What is not derivable is whether the homeowner took one, which is a row per project.

The distinction this used to draw is now a word

Thursday and frame 5 said two different things by wearing two different surfaces: a plain .card for a thing that happens, and .card-absent — the flat quiet surface mockup 16 introduced — for a thing the app cannot do. That convention ran across six pages and it is gone as of 2026-09-06, because .card-absent is a --surface-container fill at the card radius — the region’s ground wearing a card’s shape — and a region is a region of the screen, not a thing on it. Boxed at the card radius, an aside takes the same footprint as the options above it and reads as one nobody picked; the design package records itself making that exact correction to the callout, and its own absent tone is the outline rung, which is not for a sentence either. Both readings say the same thing. The meaning is not lost, because it was never in the box. Frame 5 still says it still has nowhere to write down how long in its own words, which is the sentence that was doing the work; the surface was only agreeing with it. What the app cannot do here is unchanged and still belongs in this annotation: nothing schedules the day, nothing asks for the four photos, and ask for four photos is what a homeowner does today because a message to the builder is the only path that works.

5 · What moves on a date
Draws and change orders live inside the Budget screen today, and inspections inside Maintenance. All three are dated records, so on the time axis they are one story: money is released by an inspection, and a change order moves the money and cannot say whether it moves the date.
Already in the code

Every row. project_draws carries draw_number, description, category, status, draw_date, receipt_url, notes and amount, with four routes. project_change_orders carries title, change_type, status, requested_date and impact_amount, plus decision_id, state, co_number and a whole signature flow from migrations 0076 and 0078. inspections carries inspection_type, type_label, scheduled_at, phase, status, inspector_name and outcome_notes, with four routes.

Nothing stores this yet

The schedule magnitude, and it is the frame's whole argument. Be precise about which half is missing, because the coarse version of this claim is wrong: change_type really does accept schedule_change alongside cost_change and scope_change (change_orders.go:361), and the signature flow deliberately requires agreement on a $0 schedule_change because time is a cost. So a change order can be about the schedule. What it cannot do is measure it: the only impact columns are impact_amount and impact_amount_cents, both money, with no days column, no revised-date column and nothing that touches a phase. This pushes you a week is a sentence the record cannot hold, which leaves a $0 schedule change looking free. On the Budget screen that is invisible. On a time axis it is the point.

And the gate is not modeled

Draw 5 releasing on the June inspection is drawn and not stored. project_draws has a free-text category and no phase, no inspection id and no dependency of any kind. draw_date is text and amount is a float in dollars rather than cents. So the lender's milestones and the app's five phases are two vocabularies with nothing joining them.

Two smaller things, both still true. The dates on the draw spine cannot be entered. PaymentDraw.drawDate exists, budget_repository.dart sends draw_date when it is set, and the column is there — but _save() in draw_sheet.dart:143 submits amount, description, status, category and notes and no date at all, so nothing ever sets it. And change orders now carry two parallel status columns, the legacy status and the newer state from 0076, so a screen reading the wrong one will disagree with the signature flow.

Frame 6 — the matrix, on a wide screen — is not drawn at this width, because the whole point of it is that it only exists at one. Widen past 840 to see it. The same five phases and the same two date columns are immediately below as frame 7, which is the arrangement they take down here.

6 · The matrix, on a wide screen
Above 840 the phase matrix stops being a stack of cards and becomes what it actually is: four columns, one row per phase, read down a column rather than across a card. The spine keeps its place beside it, because the table answers how did we do and the spine answers where am I, and those are still two questions.
The build so far · The Hollis House
The build so far

Monday 17 May 2027 · into the finishes

1 Mar - 10 Aug 2026Just getting started
10 Aug - 20 Nov 2026Structure is going up
20 Nov 2026 - 9 Feb 2027Inside the walls
Since 9 Feb 2027 · you are here Into the finishes Four rooms have work in this phase.
From 21 Jul 2027Living & upkeep
Target and actual
PhaseTargetActualRan long by
Just getting started 1 Mar - 10 Aug 2026 1 Mar - 10 Aug 2026 —
Structure is going up 10 Aug - 30 Oct 2026 10 Aug - 20 Nov 2026 21 days
Inside the walls 30 Oct 2026 - 19 Jan 2027 20 Nov 2026 - 9 Feb 2027 —
Into the finishes 19 Jan - 30 Jun 2027 9 Feb 2027 - running — so far
Living & upkeep Ongoing — —
The house, targeted
30 June 2027
Now expected
21 July 2027
Why
A truss package quoted in weeks and delivered in months, last August.
Already in the code

The wide chrome, all of it. Above a 520pt container HcNavMenu switches metric set on its own and keeps its persistent utilities in the header — which here is the avatar alone, because this product has no inbox and so no dock in the deck carries a bell. Mockup 15 measures the open panel at 356pt. Drawn collapsed here, because the matrix is what this frame is about.

Nothing stores this yet

The Actual column, exactly as on frame 2. Widening the screen does not change what the schema holds; it only makes the empty column obvious, because on a desktop table a blank column is a blank column.

The fourth column is the one thing here the phone cannot carry, and what it holds decides what the table says. It is Ran long by — each phase's own overrun — not the calendar's distance from target, which reads 21, 21, 21 down three rows and looks like three delays. Read this column down and there is a single number in it. That is the whole finding, and it only becomes a glance in a table; on frame 2 it takes a sentence per card and a summary under them.

7 · The same matrix, at 600
Where the table gives up. Below 840 the four columns become one card per phase carrying two headed date columns and the overrun as a sentence — the arrangement frame 2 uses on a phone, at the width it first has to appear. Nothing is dropped and nothing is scaled: Phase becomes the card's title, Target and Actual become two columns inside it, and Ran long by becomes prose. This is also the arrangement the whole page falls back to below 840, because frame 6 is gated there rather than allowed to squeeze.
Target and actual

Just getting started

Target
1 Mar - 10 Aug 2026
Actual
1 Mar - 10 Aug 2026

Closed on the day. Plans, permit and the lender all landed inside the window.

Structure is going up

Target
10 Aug - 30 Oct 2026
Actual
10 Aug - 20 Nov 2026

Twenty-one days longer than its window. This is the one, and it is the only one. The truss package was quoted in weeks and delivered in months.

Inside the walls

Target
30 Oct 2026 - 19 Jan 2027
Actual
20 Nov 2026 - 9 Feb 2027

Started three weeks late and took exactly its own length. Rough-in came in on time.

Into the finishes

Target
19 Jan - 30 Jun 2027
Expected
9 Feb - 21 Jul 2027

Three weeks late in and, so far, three weeks late out. The cabinet lead time decides whether it holds.

Living & upkeep has no date arithmetic and that is correct. If a decision has no actual phase anchor, no primary workstream, no workstream duration, or no lead-time input, Track L leaves the derived date empty. A table has to draw that as a blank row; a stack can say why.

Why this is a separate drawing

Because a four-column table at 600 is four columns at 150 each, and two of those columns hold a date range like 30 Oct 2026 - 19 Jan 2027. Narrowing produces four wrapped columns of ragged type and a header row nobody can align to. The arrangement has to change, not the scale, and the switch has to be drawn to be believed.

The sentence under the run is the case that only shows up when you draw both. In a table, Living & upkeep is a row with three em-dashes in it, which looks like missing data. Under a stack it can be a sentence explaining that the blank is correct — the migration really does leave both duration columns NULL for that phase on purpose. Same data, and only one of the two arrangements can be honest about it without a footnote.

8 · When you are your own GC
Q145 gives the trade-visit writes two writers, and only one of them had a screen. This is the other: a homeowner with no builder on the house, coordinating trades herself. It is not 21 frame 10 in green.
A different axis, which is why it is a different screen

41's axis is where a homeowner meets a visit her builder booked: she reads it, and the useful question is whether it is worth standing in the house for. A person who is her own GC is not reading somebody's schedule, she is keeping one, and the useful question is who has not called back. Drawing 21 frame 10 in emerald would have answered the first question with the second screen.

The list comes first and the act is under it

The builder reaches this from a job, one visit at a time, and lands on a form. She has no job screen and no dispatcher, so what she needs on opening is the whole week in one card. The write is a single cta below it, and the fields behind it are 21 frame 10's, unchanged: the same table, the same free-text trade, the same calendar day.

And the arm can close under her

Q145's condition is while the project has no connected builder, so this is a capability that can end without her doing anything. The callout is the whole of what the product says about that, in advance and once. A screen that let her keep writing and then refused at the boundary would be the same fact delivered as a failure.

Recorded as owed on the day the builder half was drawn

X3 and X4 served both writers and 21 drew one of them. A tick covering two routes and one of their two writers is how the other half stops being counted, so the row went into OWED_WORK.md the same afternoon and this frame is what closes it.

9 · Before the ground is broken
Q151 ruled this page holds every trade visit. Nothing gates a visit on the project's state, so a site survey booked before the first phase begins is searchable, routes here, and until now arrived at a sentence saying there was nothing to show.
The refusal is kept and the emptiness is not

The pre-build copy is a deliberate refusal to draw a dated spine before there is one, and a phase band here would contradict it. So the callout survives word for word and the visits go under it, in the two bands they already have on frame 1. What changes is only that the page stops claiming there is nothing when there is.

Two site visits that are not construction, and that is the point

A survey and a soils boring are exactly the visits that happen before a phase exists, and both are decided against: the geotech reads next to the foundation type. A homeowner who searches for geotech lands here, and the version of this page without this section is the one that told her it had not started yet.

Found in the adversarial pass on Q151's own PR

That review verified the banding and did not verify the state it bands in. readTimelineTradeVisits runs whatever the state and the projection carries them the whole time; only the screen dropped them. It is worth saying plainly, because a ruling checked against the state it was written for is the shape of gap that survives its own review.

What this row closes, and what it does not

The audit's finding was that the time material is already built and shipped and filed where a binder reader will never look. That is mostly right, and this page is the mostly. Four of the paper binder's twenty-three capabilities land here as routing and layout rather than as new capability — the whole-build start and target dates, the key dates (which in this app are the inspections), the draw schedule and the construction milestones. Four do not, and the count matters because an earlier draft of this paragraph put the phase tracker in the first list while three other places on this page correctly said it was unbuilt.

The two ideas worth taking from the paper, stated plainly

Naming a delay before it happens converts a shock into a briefing. A homeowner who has read cabinets are made to order, six to ten weeks, and the clock starts on your signature in April is a different person in June from one who has not. The information is free, it is true on almost every build, and it is the difference between a product that reports and a product that coaches. It also does the thing the whole set is trying to do: it lowers anxiety by being specific rather than by being reassuring.

Telling a remote homeowner which moment is worth a flight is the highest-stakes logistics call in the build, and nobody makes it for them. Five moments justify standing in the house and exactly one justifies rearranging your life around it. Getting that one right is worth more to a homeowner three time zones away than any other single sentence in the app, and the answer is not a preference — it is pre-drywall, every time, because it is the only day the inside of the wall is a photograph rather than a memory.

Adaptive, not responsive

The phase matrix is drawn three times because it is the one shape in this set that cannot be scaled. As a table it wants four columns; as a card it wants two. Frame 6 is the table at a 1120pt container, frame 7 is the stack at 600, and frame 2 is the same stack at 390. Between 6 and 7 the arrangement changes: Phase becomes a title, Target and Actual become two headed columns inside the card, and the fourth column becomes a sentence. No column is dropped and nothing is shrunk to fit.

And the switch is now enforced, which it was not. This page asserted that the matrix is never rendered below 840 and there was no media query anywhere in the set to make it so. At 600 the four columns resolved to 60 / 76 / 74 / 102px, the table overflowed its card by 68px and the canvas clipped the rest — the exact failure the paragraph above says must not happen, happening two screens below the paragraph. Frame 6 is now gated at min-width: 840px and a line takes its place under it. An adaptive claim that is only a sentence is a responsive layout with a good opinion of itself.

Drawing both surfaced something a media query would have hidden. Some rows have no derived date because a real timing input is absent: no actual phase anchor, no primary workstream, no workstream duration, or no lead-time input. In the table that is a row of em-dashes, which reads as missing data. In the stack it can be a sentence saying the blank is correct. One arrangement can be honest about it and the other needs a footnote, and that is the kind of thing you only find by drawing the second one.

The rule this page is correcting

No progress bar and no ratio on a homeowner surface. The Plan screen currently draws both: _TimelineBar at plan_screen.dart:800 is a segmented emerald bar, one pill per phase, and plan_screen.dart:515 renders the string "Phase {current} of {total}". Neither appears anywhere on this page.

And the bar is not only against the rule, it is not evidence. PlanService.segmentsFor fills a segment purely from its position relative to the current phase — everything before is drawn complete. Since nothing outside tests ever writes a project_phase_status row, no completion is ever recorded, so the bar reports an index dressed as a measurement. A spine node states a date and a state and claims nothing else, which is both calmer and more truthful.

Notes on the drawing

Grounded in

Cited as the thing not to do: searching for a "limited window" pattern returns almost nothing but countdown timers — FocusFlight, Pillow and Peanut all put a ticking clock on a closing window, and Deel's onboarding puts a red completion bar and "2/5 steps completed" on a schedule. A pre-drywall walkthrough is a genuinely closing window, which is exactly why it must not be drawn like a discount expiring. Sunlitt's golden hour is the same information with the urgency taken out, and that is the version this product ships.