Pilot 6 · SaaS · Supply-chain footprint estimation for one analyst

Report 06
2026

Acreline

It prints one number and then spends the rest of the product refusing to let you read it as a measurement.

Abstract

Acreline is a workbench for one person estimating a manufacturer’s supply-chain carbon footprint from a spreadsheet. Six views carry it from a file to an audit pack: import, column mapping, data quality, a scenario, a comparison, and the export that names its own provenance. 34 public states, all reachable through product controls; 18 QA‑only states that live in Storybook and nowhere else.

The thing worth reading this page for is what happened at step ⑤b — the acceptance check that reads every number on a screen against every other surface naming the same number. That check found a defect in all five earlier projects in this rebuild. Here it found 0 on screen, and 2 in the documents, because a register naming one selector per quantity was written before a single view was described, and the implementation therefore had nowhere to drift to. Gate C passed at round 1, over all 64 renders.

Everything in this project is invented. Harrow Vale Instruments, the Kilnhurst site, Ines Okonkwo, every supplier, every material, the Vale Factor Set and every tonnage, range, version and stamp are fiction written for a prototype. No real company, person, site, published emission factor, factor set, methodology or jurisdiction appears anywhere in the build or on this page. Acreline is not a carbon accounting tool, this page is not advice on how to compute a footprint, and the product itself puts the point better than a disclaimer would: an estimate, not a measurement.

Role
Product design and front-end build
Surface
SaaS desktop · 1440 × 900 primary, with 1280 × 800 and 768 × 1024 measured as different compositions, not as the same one squeezed
Component system
react-aria-components 1.5.027 components and 2 types imported across 8 sites in src/, of which 16 are the machines the spec binds by name
Slice
6 views · 3 edge states · 34 public states · 18 QA‑only · 29 registered quantities
Measured
64 renders · 288 Playwright tests · 52 Storybook stories · 51 axe scans · 132 quantity-agreement rows
Accepted
Gate C passed at round 1 — the second of the six to do so, and the last of the six. One review round, one verdict

Key

§ n
A numbered section of this report.
Q1 … Q29
An identifier from the product’s own single-source register (TASK_FLOW.md §0). The rail beside each section lists the quantities that section is about; the whole register is reproduced at § 15.
PLATE I … XII
A capture from the Gate C state audit, byte-identical to the file the audit wrote against the running build. Not a re-render and not a mock-up.
a mono run
A string read off the build, or a path in this repository.
a dotted rule
A claim on the left, and where it was checked on the right.

Division one

The report — one file, six views, and a number that keeps saying what it is

§ 1 View 1
Data Import
  • Q1
  • Q2
  • Q29

The register was written before the first screen

The acceptance standard this portfolio is built against has a step ⑤b: for every state, list the quantities the screen asserts, and check that every surface naming the same quantity carries the same value — visible text, badges, tab labels, footers, progress positions, and aria-label and live-region text alike. It was added to the standard because a quantity that changes with state, printed on two surfaces, drifted in all four of the first four projects in this rebuild.

Acreline answered it at Gate B instead of at Gate C. Before any view was described, docs/build-pack/acreline/TASK_FLOW.md §0 listed every quantity the product would ever assert — twenty-nine of them — with the one selector that computes it, and SCREEN_SPEC.md §0.12 bound each selector to every surface that would render it, per view. The whole table is at § 15.

Four build rules follow from it, and the fourth is the one worth arguing about:

  1. No surface recomputes a quantity locally. A badge, a footer, a tab label, an aria-label and a live-region announcement all read the same selector.
  2. The noun is part of the number. Q10 counts gaps; Q11 counts the rows behind them. They are never printed as one figure and never in the same unlabelled position.
  3. A derived percentage is derived where it is printed, from the pair printed beside it. Where a denominator is zero the surface writes not comparable in words rather than dividing.
  4. Acreline prints no percentage set that must sum to 100. A four-part split of 1,169 t rounds to 99.9 at one decimal, and the cheapest repair — largest-remainder rounding — turns 6.9409 into 7.0. So the composition rows carry 704 t of 1,284 t, a pair, and no share in per cent appears on that view at all.

Rule 4 is the same move the project makes for geometry: remove the claim rather than manage the drift. It is enforced at runtime — a guard walks all six views and asserts that at most two percentages are painted on any of them, and that every composition row matches N t of N t exactly.

apps/acreline/tests/public-guardrails.spec.ts:395 — "no percentage set that must sum to 100 is printed anywhere"

for (const view of VIEWS) {
  await view.go(page)
  await settle(page)
  const percentages = (await paintedText(page)).match(/-?−?\d+(\.\d+)?\s?%/g) ?? []
  expect(percentages.length, `${view.name}: ${percentages.join(' ')}`).toBeLessThanOrEqual(2)
}
/* And the composition rows carry tonnes and a rule, never a share in %. */
await openScenario(page)
await settle(page)
const shares = await page.locator('.comp-share').allInnerTexts()
expect(shares.every((s) => /^\d[\d,]* t of \d[\d,]* t$/.test(s.trim()))).toBe(true)

View 1 is where the register starts. The file chip states 39,312 B · 11 columns · 248 rows; the requirements panel turns each of the four bounds the product will enforce into a measured value beside it — the format is CSV, the size is 39,312 B against 20 MB, the row count is 248 against 5,000, the header row is Detected; and the preview says Showing the first 8 of 248 rows. Six surfaces name Q1 in this one state, and the agreement check reads all six.

Plate I The file, and the four bounds as measurements state audit capture import-parsed · 2880 × 1800 px, 1440 × 900 at deviceScaleFactor 2 · sha256 ce1a730b8916…
Acreline's Data Import view. A drop zone, a file chip reading hv-suppliers-2025Q4.csv with 39,312 B, 11 columns and 248 rows, a checked box labelled First row names the columns, and a panel headed WHAT THIS FILE MUST BE listing four bounds each with its measured value beside it. On the right, a preview table of eight rows across four visible columns, footed by the line Showing the first 8 of 248 rows.

The requirements panel is the argument of this view. It does not say the file is valid; it prints each rule the product will apply and the value it measured, so a refusal later has already shown its working. On import-rejected the same panel marks the failing bound and states 5,412 beside 5,000 or fewer, and the forward control stays disabled.

§ 2 View 2
Column Mapping
  • Q2
  • Q3
  • Q4
  • Q5
  • Q6

A partition of eleven, and a second number beside it

The validation band is one sentence carrying four labelled numbers: 7 recognised · 3 need a decision · 1 excluded — 7 of 9 fields matched. The first three are a partition of the file’s eleven columns; they must sum to Q2 in every state, and the harness measures that sum on the painted verdict words rather than on the band, twice — on arrival and again when the mapping is complete.

The fourth number is not part of that partition, and the difference is the whole reason the band prints it separately. Q4 counts mapped columns; Q3’s clause counts distinct canonical fields. They agree until one field is claimed by two columns, and the moment they part is exactly the state a mapping screen has to be honest about — which is why the band is built to name the field in the same sentence rather than leave a reader to reconcile two counts.

apps/acreline/src/views/ColumnMapping.tsx:31 — the band, built from §0 selectors

const band =
  `${q.q4} recognised · ${q.q5} need a decision · ${q.q6} excluded` +
  ` — ${q.canonicalMatched} of ${q.q3} fields matched` +
  (q.conflicts.length > 0 ? ` — ${q.conflicts.join(" and ")} claimed by two columns` : "");

Three decisions later the same band reads 9 recognised · 0 need a decision · 2 excluded — 9 of 9 fields matched, and the footer restates it in its own words: 9 of 9 fields matched · 2 columns excluded. Nine plus zero plus two is still eleven. That invariant is what caught the one real ⑤b defect in this project, and it was in the specification rather than in the build — § 12.

A measured disagreement with the build pack, recorded rather than smoothed over

The state inventory says the destination combo box opens “on the four remaining canonical fields”. It offers threeShip mode, Received date and Do not import — because seven of the nine fields are already held on arrival and the product does not offer a field another column holds. The data wins; the count is asserted against 9 − Q4 in the same render, so neither figure can be wrong on its own; and the disagreement is written into the state’s own record instead of being reconciled quietly.

Plate II The partition on arrival, and the matrix it summarises state audit capture map-auto · 2880 × 1800 px · sha256 be3f45f12ebc…
Acreline's Column Mapping view. A band across the top reads 7 recognised, 3 need a decision, 1 excluded — 7 of 9 fields matched. On the left a list of the file's eleven columns with a verdict beside each; on the right a matrix giving each source column, four of its values, the canonical field it becomes and its verdict. The footer reads 3 columns still need a decision and the forward control is disabled.

Every verdict carries a word, never a colour alonematched, needs a decision, excluded — and a runtime guard asserts it on every status marker in the build. The four sample values under each column are there so a decision about a column can be made from the column, not from its name.

§ 3 View 2
Column Mapping
  • Q1
  • Q7

The zero case is written in words

Q7 is the one per-column percentage the product prints, and it is printed as a pair and a percentage together: a matched column reads 248 of 248 rows carry a value (100%) above All values pass validation. The percentage is computed in the expression that renders it, from the pair beside it, so the two cannot disagree.

The interesting case is the empty column. buyer_notes holds nothing, and rather than print a division it prints the same pair with a zero numerator and then replaces the verdict with a sentence:

0 of 248 rows carry a value (0%)

Nothing to validate

src/views/ColumnMapping.tsx:170 · :172 — the ternary is on the VERDICT, not on the figure

All eight sample cells beneath it read no value, so the summary and the sample it summarises sit on one surface and a reader can check one against the other without leaving the row. This is the small version of the rule the whole product is built on: a number is never left to stand alone where the thing it counts can be shown beside it.

Plate III An empty column, summarised and sampled on one surface state audit capture map-coverage-zero · 2880 × 1800 px · sha256 c2adc0615276…
The Column Mapping matrix with the buyer_notes row expanded. It reads Not matched to any field, 0 of 248 rows carry a value (0%), and Nothing to validate, above eight sample cells every one of which reads no value, and two controls: Confirm mapping and Do not import.

The two column-level exits belong to an undecided column, not to a matched one. The inventory attaches them to a state whose column is already recognised; a matched column has no exits to offer, so the build records the disagreement on the state and evidences the exits here, where they are real.

§ 4 View 3
Data Quality
  • Q8
  • Q9
  • Q10
  • Q11
  • Q12
  • Q13

Filtering changes what you see, not what is true

Data Quality opens on a single banner — 201 of 248 rows are ready · 47 need a decision — and a rail with one group per kind of outstanding decision. Selecting a group filters the table to Showing 34 of 47 rows that need a decision. and the group states 3 factor gaps · 34 rows.

The banner does not move. That is deliberate and it is asserted: filtering changed no row’s state, so the figure that counts rows is unchanged, and a product that redrew it would be telling the user that looking at something had altered it. The two numbers in the rail are the ones that move, and they are labelled with different nouns because they count different things — this is register rule 2, and it exists because a shipped product in the reference sweep sets “17 required values are missing” above a list of rows each reading “1 error”.

Resolving the first gap moves both figures in the same render: 219 of 248 rows are ready · 29 need a decision and 2 factor gaps · 16 rows. When everything is decided the banner changes what it counts, to 244 rows included · 4 excluded, and the rail says so in one line rather than emptying silently:

Nothing is waiting on a decision.

Every row is included or excluded. The audit pack records both.

src/views/DataQuality.tsx:110 · :203
Plate IV A group selected: the table filters, the banner does not state audit capture quality-gap-selected · 2880 × 1800 px · sha256 567882788bf8…
Acreline's Data Quality view. A banner reads 201 of 248 rows are ready, 47 need a decision. A left rail headed WHAT NEEDS A DECISION lists three groups — Missing factor with 3 factor gaps and 34 rows selected, Invalid unit with 9 rows, Non-positive quantity with 4 rows — and below them a block headed RATIONALE NOT RECORDED naming Nitrile, Malaysia, 5 rows with a Record the rationale control. The table on the right shows eleven rows, each marked no factor in words, under the line Showing 34 of 47 rows that need a decision.

The rail is also where the export blocker is visible from. The RATIONALE NOT RECORDED block names the same gap, the same origin and the same row count as the blocker on view 6, because both find the object rather than restating a string that happens to agree today.

§ 5 View 3
the fix drawer
  • Q10
  • Q11
  • Q21
  • Q26

A sentence travels with the number

A factor gap is resolved by choosing something to stand in for the missing factor. The drawer offers three, each named and versioned and never valuedGlass, container — EU average · Vale Factor Set v3 and two others — and then asks for one sentence. The field’s placeholder is the part of this product I would keep if I could keep one line:

One sentence. It travels with the number into the audit pack.

src/views/DataQuality.tsx:289 — the rationale field's placeholder

It is not a label telling you what to type. It tells you where what you type goes, which is the only reason a person would take the field seriously. The forward control in the drawer reads Apply to 18 rows and is disabled until the sentence exists — so the number the decision will touch is stated before the decision is taken, on the control that takes it.

No numeric emission factor appears anywhere in the product, in any state. That is a rule of the fiction and it is also a guard: a runtime pass walks all six views and asserts that no decimal with two or more places is painted on any of them, and that the words that would introduce a factor value — per kg, factor =, coefficient — appear nowhere. A prototype that invented plausible emission factors would be a prototype that looked like it knew something, and it does not.

The resolution is then listed with its stamp, so what was decided and when is on screen rather than in a log: Borosilicate glass · Portugal over recorded 14 Apr 2026, 09:58.

Plate V The fix drawer: three named stand-ins, and the sentence state audit capture quality-fix-drawer · 2880 × 1800 px · sha256 9bdefb9cc61b…
The Data Quality view with a 400 px drawer open on the right, headed Borosilicate glass · Portugal and scoped to Borosilicate glass · Portugal · 18 rows. Under CHOOSE A FACTOR TO STAND IN it offers three radio options, each naming a factor and its factor-set version. Below is a field labelled Why this one whose placeholder reads One sentence. It travels with the number into the audit pack, and a disabled control reading Apply to 18 rows beside Cancel. The filtered table stays visible to the left.

The table stays visible while the drawer is open. The drawer takes 400 px and the table keeps the rest with its own filter line, because the decision is about those rows and hiding them to ask about them would be the wrong trade. At 768 the drawer becomes a 400 px docked bottom sheet rather than a side panel — a change forced by a defect the first run found, § 13.

§ 6 View 4
Scenario Builder
  • Q12
  • Q13
  • Q14
  • Q15
  • Q20

An estimate, not a measurement

The total is the largest thing on the view and it never appears without the sentence underneath it:

1,284 t CO₂e

1,130 – 1,438 t at ±12% factor uncertainty

Built from named factors and stated assumptions. An estimate, not a measurement.

src/views/ScenarioBuilder.tsx:46 — and, verbatim, in the block's accessible name at :37

The claim and its qualification are one object. The same sentence is built into the total block’s accessible name, so the qualification is not a visual footnote that a screen reader skips past — it is announced with the figure it qualifies.

Above the total, a panel says what the number is built from before the number is read: 244 rows included, 4 rows excluded, Vale Factor Set v3. Below it, four composition rows, each with an illustration, a tonnage, and — instead of a percentage — 704 t of 1,284 t beside a proportional rule.

That rule is measured, not eyeballed. The build pack lists it as one of the things a reviewer has to open at full size, so the harness makes it mechanical instead: each rule’s width in pixels is asserted equal to t ÷ 1,284 × 356, giving 195.2 / 91.8 / 46.6 / 22.5, and the four are asserted strictly ordered. The four tonnages sum to the total exactly — 704 + 331 + 168 + 81 = 1,284 — and the agreement check reads that sum off the painted rows.

On the right, the flow plane: four origin plates, two freight plates and one destination, every label a row count or a tonnage read from a selector. 62 + 48 + 29 + 105 = 244, the same Q12 the scope panel prints; 178 t + 153 t = 331 t, the transport category exactly. Nothing on the plane is a written constant.

Plate VI The baseline: what it is built from, what makes it up, and where it flows state audit capture scenario-baseline · 2880 × 1800 px · sha256 bbb8cf2d68b4…
Acreline's Scenario Builder. On the left a panel headed WHAT THIS IS BUILT FROM listing 244 rows included, 4 rows excluded and Vale Factor Set v3; below it the figure 1,284 t CO2e, the line 1,130 – 1,438 t at ±12% factor uncertainty, and the sentence Built from named factors and stated assumptions. An estimate, not a measurement. Under WHAT MAKES IT UP, four rows — Purchased goods 704 t, Transport 331 t, Raw materials 168 t, Storage 81 t — each with a small monochrome illustration, a line reading N t of 1,284 t and a proportional rule. On the right a flow plane with four origin plates, two freight plates and the Kilnhurst site, edges labelled with row counts and tonnages.

The four illustrations are the project’s only generated raster besides the ground. Five assets, two candidates each, one selected each, zero regenerations — ten images, and $0.00 spent against a $0.50 cap, because they were made on a free image path that never touches the cost ledger. The ledger records that as the reason rather than leaving an empty file to be mistaken for an unbounded one.

§ 7 View 4
the range
  • Q15
  • Q17

The range says what it is not

The range line is a control. Pressing it opens an explanation in place — not a tooltip, not a help page — and the explanation is two paragraphs, the second of which is the reason this product exists:

Each factor in the set carries its own spread. The range is the total recomputed at the low and high end of every factor at once, which is the widest the arithmetic allows.

It is not a confidence interval and it is not a measurement error. It says nothing about how likely either end is.

src/views/ScenarioBuilder.tsx:79 · :82

Almost every product that prints a range implies a distribution by printing it. This one states the construction, then removes the three readings a person is most likely to bring to it. It also declares its own width — ±12% — from a single constant, printed beside the range on all three surfaces that carry one, so the band and the number it widens cannot part company.

Plate VII The range, explained where it is printed state audit capture scenario-uncertainty · 2880 × 1800 px · sha256 9e1a353e20e9…
The Scenario Builder with the range line underlined as a control and a panel open beneath it headed WHERE THE RANGE COMES FROM, with a Close control. It explains that each factor carries its own spread and that the range is the total recomputed at the low and high end of every factor at once, then states that it is not a confidence interval and not a measurement error, and says nothing about how likely either end is.

The panel replaces the composition rows rather than covering them. Nothing on this view floats except the two dialogs; an explanation that obscured the thing it explains would be making the same mistake as a tooltip.

§ 8 View 5
Scenario Comparison
  • Q16
  • Q17
  • Q18
  • Q19
  • Q21
  • Q22

Two ranges, read against each other

One change — Ship mode changed from Air to Sea on 41 rows. — moves exactly one category. Transport falls from 331 t to 216 t; the other three keep their figures; the total becomes 1,169 t. The comparison then prints the difference twice, and the two printings are not the same kind of thing:

  • −115 t is stored arithmetic: Q16 minus Q14, and the harness recomputes it from the two cells printed beside it.
  • −9.0 % is derived at print time from the same two operands, which is why it moves when they move and the absolute does not (§ 9).

Under them is the sentence I would point at if asked what “interpreting your own numbers without overclaiming” looks like in practice:

The two ranges overlap between 1,130 t and 1,309 t, so the 115 t difference is larger than the gap between them but smaller than the width of either range.

src/views/ScenarioComparison.tsx:126 — the bounds are max(low) and min(high), computed here

It draws the comparison a reader would otherwise draw badly, states the limit of it in the same breath, and stops. It does not say which scenario is better.

Two rows below are worth reading as a pair. Rows included shows 244 against 244 and a difference of 0; Factor set shows the same version in both lanes and a difference of same. Both cells are derived in the expression that renders them — two integers give a signed subtraction, two strings give a word — and 0 is printed rather than left blank, because an absent row and an unchanged row are different facts.

At 768 the lanes stack, and that is measured rather than asserted

768 × 1024 is a different composition, not the same one squeezed: the left rail becomes a 56 px icon rail, the drawers become docked bottom sheets, the target floor goes back to 44 px, and the two comparison lanes stack into one column. The stack is measured — one grid column, every cell at the same x — and asserted in a story of its own.

Plates VIII & IX The comparison at 1440, and the same state at 768 state audit captures compare-default (2880 × 1800) and compare-default@768 (1536 × 2048) · sha256 f7d5eb38762b… and 27b031880045…
Acreline's Scenario Comparison at 1440 by 900. Two lanes, Baseline 2025 Q4 and Kilnhurst consolidation, each headed with 244 rows and Vale Factor Set v3, and a Difference column. Total footprint reads 1,284 t against 1,169 t with −115 t and −9.0% in the difference column. Range at ±12% reads 1,130 – 1,438 t against 1,029 – 1,309 t, and beneath the row a sentence states that the two ranges overlap between 1,130 t and 1,309 t. Rows included reads 244 against 244 with a difference of 0; Factor set reads the same version in both with a difference of same; Assumptions reads 4 against 5 with +1. A chip beside the title reads pinned to v3. The same Scenario Comparison state at 768 by 1024. The left navigation has become a 56 px icon rail. The two lanes are stacked into a single column: each row shows its label, then the baseline value, then the proposal value, then the difference, all at the same left edge. The overlap sentence wraps across three lines.

At 1280 this view was 1,416 px wide inside a 1,281 px frame when it was first run, and the whole difference column was off screen. Nothing reported it: the region scrolls, so the overflow detector correctly found nothing. It was found by measuring the frame’s own rectangles against the geometry the spec states — § 13, defect F22.

§ 9 View 5
edge state 2
  • Q19
  • Q23
  • Q24
  • Q25

A version change prints its consequences first

The analysis is pinned to Vale Factor Set v3. A newer set exists, and the comparison says so in a band beneath its own header: Vale Factor Set v4 was published on 9 April. 2 of the 9 factors used here changed. Every figure keeps its value and its position while the band is up — nothing is greyed, nothing is marked stale, nothing is hidden behind the notice. The band is information, not a modal in disguise.

That count is two lengths rather than two constants: the product holds the list of factors the analysis uses and the list changed in v4, and prints 2 of 9 from them. It never names which two, because it does not know what a reader would do with that.

Choosing to act on it opens the one dialog in the product that is genuinely a decision, and the dialog prints the consequences before the choice:

Baseline 1,284 t → 1,301 t

Proposal 1,169 t → 1,186 t

Difference 115 t → 115 t, −9.0% → −8.8%

Recomputing creates a new revision. The v3 revision is kept.

src/views/ScenarioComparison.tsx:256–264 — every figure computed, none written

The third line is the register earning its keep. The absolute difference does not move and the relative one does, because Q19 is derived at print time from a denominator that moved: 115 ÷ 1,284 = 8.96 % and 115 ÷ 1,301 = 8.84 %, printed as −9.0 % and −8.8 %. A product that stored the percentage would have shown −9.0 % in both columns and been quietly wrong in the one place a person is most likely to quote.

And the last line is the difference between a warning and a decision: nothing is destroyed. The v3 revision stays in a list and stays selectable. The dialog opens with Keep v3 holding focus, not the action; an outside click does not decide anything, because the dismissable behaviour that made a stray click on the scrim silently record a decision was removed — § 13, defect F15.

Plate X The decision, with its consequences already printed state audit capture compare-version-dialog · 2880 × 1800 px · sha256 b208e0e3298b…
A modal dialog over the Scenario Comparison, titled Recompute on Vale Factor Set v4? It lists v3 pinned 6 Apr 2026 14:30 and v4 published 9 Apr 2026 08:00 in a monospaced block, states that the pinned version is used by 1 scenario and 2 saved views, and under WHAT RECOMPUTING WOULD MOVE prints Baseline 1,284 t to 1,301 t, Proposal 1,169 t to 1,186 t, and Difference 115 t to 115 t, −9.0% to −8.8%. Below that: Recomputing creates a new revision. The v3 revision is kept. Two controls, Keep v3 unfilled and Recompute on v4 filled.

The dependents are counted by kind, not summed. 1 scenario and 2 saved views — three things, but never 3, because a scenario and a saved view are not units of the same thing and a person deciding needs to know which is which.

§ 10 View 6
Provenance Export
  • Q12
  • Q13
  • Q26
  • Q27
  • Q28
  • Q29

A refusal that carries its own measurement

An analyst who opens Provenance Export before repairing the analysis gets a refusal, and the refusal is built like a finding rather than like an error. It states the measurement and the threshold together, enumerates what is blocking, says which view owns each blocker, and offers a route to it:

Why can’t I export yet?

Assumptions with a recorded rationale: 4 of 5

On Data Quality — one assumption has no recorded rationale: Nitrile · Malaysia, 5 rows. Go to Data Quality

On Scenario Comparison — this analysis is pinned to v3; v4 was published on 9 April. Go to Scenario Comparison

src/views/ProvenanceExport.tsx:126 · src/derive/quantities.ts:218–247 — the blocker finds its object

4 of 5 is doing the work of a sentence: it is the measurement and the bar in the same breath, so “why not” and “how far off” are one reading. And the blocker text is built by finding the gap — the material, the country and the row count are read from the same data the rail on view 3 reads — so the two surfaces cannot drift into naming different things.

The refusal is announced as well as drawn: the heading and its measurement sit inside one live region, so a screen reader hears the refusal with the number that turns it off. That shape is the repair for a real defect — role="status" had been put on the heading alone, which is not allowed on an h2 and would have announced the refusal without its measurement.

The pack names what it does not contain

With both blockers cleared, the panel lists the audit pack as two columns: Included (6) and Not included (3). The second column is the unusual one. Most products list only what they include:

Not included (3) — the file as uploaded · the 4 excluded rows · the factor library itself

src/views/ProvenanceExport.tsx:149–156 — and the row count is Q13, read rather than repeated

The middle item is a slot rather than a string: it reads the excluded-row count from the register, so the pack’s own description of its hole cannot go stale. And the export control states the scope it will write — Export audit pack · 244 rows — before it is pressed.

Plates XI & XII The refusal, and the pack that names its own exclusions state audit captures prov-export-blocked and prov-export-ready · 2880 × 1800 px each · sha256 b86b87c9eeb5… and dde3ca98a7f7…
Acreline's Provenance Export with the export panel replaced by a blocker block. It is headed Why can't I export yet? with the line Assumptions with a recorded rationale: 4 of 5 beneath it, then two rows: one naming Data Quality and an assumption with no recorded rationale for Nitrile, Malaysia, 5 rows, with a Go to Data Quality control; the other naming Scenario Comparison and the pinned v3 against v4 published on 9 April, with a Go to Scenario Comparison control. Above, a breadcrumb across five stages and a lineage plane of node plates connected by lines. The same view with both blockers cleared. The panel now shows two columns, Included (6) listing the resolved dataset, column-mapping record, factor-resolution log, assumption register, scenario definition and run stamp and digest; and Not included (3) listing the file as uploaded, the 4 excluded rows and the factor library itself. A filled control at the foot reads Export audit pack · 244 rows.

The lineage plane is a group, not a picture. It was first built as role="img" with sixteen buttons inside it — a role whose subtree is presentational, so a screen reader honouring it would never reach a single node. It is now a labelled group whose plates are operable, and the flow diagram on view 4, which has no operable child, stays an image.

§ 11 §17 package
the take
  • 27.80 s

The same analysis again, walked once

One take, no cuts, at the primary viewport. It goes from an empty import to the export panel through the product’s own controls, resolved by role and accessible name using the same drivers the acceptance suite uses. There is no seed, no QA route and no URL parameter in this build, so every frame is a surface an analyst can reach; nothing is stubbed for the camera and no state is written directly.

27.80 s1440 × 900VP8 / WebM25 fps 695 frames2,496,208 B0 audio streams13 caption cues

Where it stops, and what it cannot show

The last beat is the export panel with the pack listed and the control live. It is not pressed: the frame after an export is a stamp, a digest and a Download again control, which is a receipt rather than a decision, and the decisions are what this product is for.

Edge state 3 — the refusal in Plate XI — is not in the take, and the first cut of the captions claimed it was. Reaching Provenance through the forward flow clears both blockers on the way: resolving the Nitrile gap in the drawer records the rationale, and Keep v3 settles the version. Edge 3 is reached by opening Provenance from the left nav before repairing the analysis — a different session, not a later beat of this one. The caption was corrected after the frame inside its own window was extracted from the take before this one and read: it showed the export control live.

Three more captions did not survive their own frames

One said 331 t becomes 216 t over a frame that prints 216 t and not 331 t. One said the two verdicts that changed over a frame showing three. And the first cut’s sweep caption held a 0.957 s window over five separate decisions, which is not a description of anything; it is now two captions, each timed to a frame the take actually holds. Every cue was checked against a frame extracted from the finished file with ffmpeg -accurate_seek -ss <t> -frames:v 1, and the table of thirteen is in PACKAGE.md.

And a note about the frame itself

video.size equals the viewport. Playwright only ever scales a page down to fit the size a config requests, never up, so a take that declares a size larger than its viewport carries the page in one corner and pads the rest with flat grey. Two mobile takes in this portfolio shipped that way and were re-recorded. Measured on this file at 6 s: content bounding box 1440 × 900 in a 1440 × 900 frame, 0.0 % of it padding.

Division two

The examination — what was measured, what was found, and what is still wrong

§ 12 Step ⑤b
44 × 3 = 132
  • Q2
  • Q4
  • Q5
  • Q6

Step ⑤b, and the two defects it found in the documents

reports/acreline/QUANTITY_AGREEMENT.json holds 44 distinct comparisons, measured at three viewports, for 132 rows, and the file contains 132 rows. That precision is deliberate: the previous project’s report implied fifty measurements over a twenty-five-row artifact, and this build changed to a shard-merge so that the file holds what the report says it holds. Across those 132 rows the file records 0 disagreements, over 513 individual surface readings.

Accessible names and live-region text are read as surfaces, because the spec counts them: the table’s aria-label and aria-rowcount, the total block’s accessible name, each composition row’s accessible name, the difference cell’s accessible name, the drawer’s accessible name and the step indicator’s Step N of 3 are all compared against the painted figure.

Both defects were in the build pack. Neither was on a screen.

That is the point of having answered ⑤b at Gate B. With one selector assigned to each quantity before any view existed, the implementation had nowhere to drift to; what was left to catch was the register’s own arithmetic.

⑤b-1 · TASK_FLOW §0, Q4 — a definition that contradicted its own invariant

Q4 was defined as columns.filter(c => c.mapped && c.auto).length with a constant value of 7. The && c.auto freezes Q4 at 7 while Q5 falls, so Q4 + Q5 + Q6 = Q2 — the invariant the same document states — breaks the moment a column is matched by hand: 7 + 1 + 2 = 10 ≠ 11. The band would have asserted a partition of eleven columns that accounted for ten.

The arithmetic table won. It is the one place the register’s own invariant is written out, and the same document’s own tables move Q4 7 → 8 → 9 and state 9 of 9 fields matched at the end of the flow. The build computes cols.filter(c => c.mapped).length; §0 and SCREEN_SPEC.md §2 — which had copied the same clause, ⑤b-2 — were corrected, with the correction recorded rather than the row quietly rewritten.

Two near-misses were examined and kept rather than "fixed":

  • 9 recognised against 9 of 9 fields matched — different quantities, agreeing at the end of the flow and parting the moment one field is claimed twice, which is exactly what the conflict state shows.
  • 0 in one difference cell against same in another — both derived where they are rendered, from different types, and both asserted.

The visible arithmetic, checked rather than taken on trust

  • 704 + 331 + 168 + 81 = 1,284 tthe baseline total exactly, read off the painted rows
  • 704 + 216 + 168 + 81 = 1,169 tonly Transport moves
  • 1,169 − 1,284 = −115 t; 115 ÷ 1,284 = 8.96 % → −9.0 %Q18 stored, Q19 derived at print
  • on v4: 1,301 and 1,186, still −115 t; 115 ÷ 1,301 = 8.84 % → −8.8 %the percentage moves when its own operands move
  • 201 ready + 47 pending = 248; gap 1 is 18 rows → 219 and 29both printed, both measured
  • 18 + 11 + 5 + 9 + 41 = 84, but the union is 66the sum is printed nowhere; per-assumption counts are not additive, and the view says so in words
§ 13 First compile
24 defects
  • F1–F24

The first time it ran, and the twenty-four defects that were there

The design step produced source that compiled. That was the entire extent of what was known about it: it had never been driven. The first time it was, two of the thirty-four public states could not be reached at 1440 and nine could not be reached at 768. Those, and twenty-two more, were repaired before anything was reviewed.

Four of them made public states unreachable, and none of the four is the kind of thing a screenshot shows:

reports/acreline/QA_HARNESS_REPORT.md §3 — F1 to F4
#What was wrongMeasured
F1 The destination combo box’s trigger was a bare chevron with only a colour declared. 6.45 × 20 px — under the 24 the preset requires at every width, and under the 44 this project puts back at 768
F2 Because of F1, one public state could not be reached at all: a press on that trigger did not open the popover. the state timed out after 120 s in the audit
F3 Both drawers shipped with no height, so each took its content height at the foot of a column that was already full — and every state downstream of that press is downstream of a clean quality review. Apply to 18 rows at y 1180 in a 1024 px frame: outside the viewport and unreachable by scrolling. 9 of 34 states unreachable at 768
F4 React Aria puts the checkbox role on a one-pixel-clipped input that the label covers, so every press retried until timeout. 13 × 13 at 280,509. The drivers now press label.a-checkbox, which measures 414 × 58

The rest fall into four groups, and the useful thing about them is how they were found.

  • Six ranks of control were still at desktop height at 768, and controls were being squashed below their declared height by flex-shrink — 4 assumptions measured 114 × 20 against a declared 24, a matrix trigger 168 × 20 in a 56 px row, five breadcrumb items 23 – 111 × 20. None of that is visible in a screenshot. It was found by measuring every control against the preset’s own bar, at all three viewports.
  • Four defects were geometry against the spec’s own arithmetic. The comparison lanes were 1,416 px wide in a 1,281 px frame; a dialog control sat 10 px off the frame at 768 and a breadcrumb chip 48 px off it; the lineage plane declared 1,184 px and, under box-sizing: border-box with its own 1 px rules, left 1,182 for 1,184 of content, clipping the last column and producing 11 overrun findings that were all one pixel pair. It is 1,186 px now and the findings are 0.
  • Eight were accessibility defects that the stricter of the two bars caught. Storybook fails a story on any axe violation, which is stricter than the Playwright suite’s serious-and-critical bar, and it is what found a role that is not allowed on an aside, a status role on a heading, an empty table header and a heading order that jumped h1 to h3. Four scrollable regions no keyboard could reach, an excluded row at 2.9:1 and the lineage plane’s role="img" came from the same sweep.
  • Two were behaviour that looked fine and was not. Both dialogs opened with the container focused rather than the cancel control. And an outside click on the version dialog recorded a decision: the dismissable behaviour fired one of the edge state’s two exits silently, and injected a one-pixel dismiss button ahead of Cancel in the focus order. Both dialogs are now among the two moments the product does not allow to be interrupted.

One more, and it is the one I would keep as the argument for having a fixture at all: repairing F7 introduced F24 — a flex item’s automatic minimum size is its content, so a 64-character header overran its column instead of wrapping — and the fixture written to prove that headers wrap caught it in the same run.

§ 14 Gate C
64 renders
  • 34 / 34

Coverage is driven, not claimed

Every one of the 34 public states is reached by a driver that goes through a control the product exposes, resolved by role and accessible name. This build reads no URL parameter at all — there is no seed, no fixture switch and no QA route — which is proven statically in src/, statically in the built dist/ with a staleness check, and at runtime on all six views at all three viewports. A state the harness can reach is therefore a state a user can reach, without qualification.

The six detectors, over all 64 renders

Re-measured from reports/acreline/STATE_AUDIT.json by walking every state record, not read off its summary
Detector 1440 × 900
34 renders
1280 × 800
13 renders
768 × 1024
17 renders
All 64 Gate
overlap0000gate
overflow0000gate
truncation0000gate
contrast0000gate
touch-target1001gate — accepted, below
contrastIndeterminate0000measured, not waived
dead-space57201895not a gate — human judgement

The JSON’s own summary field counts the primary viewport only. findingTotals reads {"dead-space": 57, "touch-target": 1} — 58 — and a reader quoting it as the project total would be short by 38. The table above is the walk over all three viewports. Thresholds were the SaaS preset’s own, unmodified: target 24 (44 at 768 by the preset’s per-viewport override), dead-space ratio 0.20, overlap 1 px, contrast 4.5 and 3. No threshold was changed, no state excluded, no selector added to any exclusion list and no detector disabled. domDigestAliases is empty — the only project of the six with no DOM-digest collisions and no aliases to declare.

The one touch-target finding, and why it is still in the report

It is F4’s element: React Aria’s own checkbox input, clipped to one pixel inside the label, reported at 13 × 13. The easy handling would have been to add it to the profile’s decorative-selector list, which would remove the finding from the audit. That list is untouched — the three entries it already was.

Instead the measurement was added. tests/target-size.spec.ts measures label.a-checkbox — the thing a person presses, 414 × 58 at 1440 and ≥ 44 at 768 — against the same preset bar at all three viewports, and skips the nested input structurally, by the clip that hides it, rather than by name:

apps/acreline/tests/target-size.spec.ts:88

if (node.closest('label.a-checkbox, label.proxy-option') && node.tagName === 'INPUT') continue

The finding stays visible as a true statement about React Aria’s DOM, beside a true measurement of what a person presses.

Dead space: 95 findings, and not 95 defects

Dead space is reported for human judgement and never gated, because the failure it invites — inventing content to fill a rectangle — is worse than the void. Here the judgement is easy once the findings are grouped, and grouping them is the whole point:

  • 64 of the 95 are one fact about the left nava six-item rail in a full-height column; reported once per render, in every state, at every width, by construction
  • 9 are the issue railas many groups as there are kinds of outstanding decision — three, then fewer, then none
  • 6 the blocked export panel, 5 the preview pane, 4 a comparison regiona fixed 200 px panel holding two blockers; nothing to preview before a file is chosen
  • 3 the fix drawer, 2 the table pane, 1 each for the done panel and the import panela drawer holds one gap's three options and one field

Adding a seventh place to the navigation to fill 530 px is inventing content; so is padding a table to the fold. The metric is noisy on this shape, and a reader who quotes the raw 95 will badly misread the build — which is why it is decomposed here rather than defended.

A correction to the acceptance review, made by re-measuring rather than by trusting it

The Gate C review reports this split as 62 nav findings and 31 on main.content. Walking every finding record in STATE_AUDIT.json gives 64 on nav.leftnav and 31 spread across eight other selectors, none of which is main.content — that selector produces no dead-space finding anywhere in the audit. The review’s judgement is the one this page follows and it is unaffected; its two numbers are not what the file holds, and the harness report’s own class table already says 64. Recorded here rather than reproduced.

Where the states are, and which of them are public

  • 1. Data Import 4
    • import-empty
    • import-parsed
    • import-header-off
    • import-rejected
  • 2. Column Mapping 6
    • map-auto
    • map-column-open
    • map-unmapped-selected
    • map-excluded
    • map-coverage-zero
    • map-complete
  • 3. Data Quality 7
    • quality-all
    • quality-gap-selected
    • quality-fix-drawer
    • quality-gap-resolved
    • quality-unit
    • quality-exclude-dialog
    • quality-clean
  • 4. Scenario Builder 5
    • scenario-baseline
    • scenario-editing
    • scenario-applied
    • scenario-assumptions
    • scenario-uncertainty
  • 5. Scenario Comparison 6
    • compare-default
    • compare-category
    • compare-assumptions
    • compare-version-conflict
    • compare-version-dialog
    • compare-recomputed
  • 6. Provenance Export 6
    • prov-default
    • prov-node-selected
    • prov-row-evidence
    • prov-export-blocked
    • prov-export-ready
    • prov-export-done

4 + 6 + 7 + 5 + 6 + 6 = 34, which the harness asserts against the inventory’s own matrix so the two cannot drift. Eighteen more states are QA-only: they exist in Storybook, are asserted absent from src/ and from the built dist/, and 34 + 18 = 52.

The forward flow, and the three places it is allowed to fail

  1. T1Data Import → Column MappingContinue to mapping — disabled until a file is parsed, and on the empty state it carries the reason Add a file to continue
  2. R1Column Mapping → Data ImportBack to the file — the parse is kept, the mapping decisions are kept, nothing is re-parsed
  3. T2Column Mapping → Data QualityContinue to review — disabled while any column needs a decision, and the footer states the count that blocks it
  4. R2Data Quality → Column MappingBack to mapping — every resolution already recorded is kept, because a resolution is a decision about data rather than about the mapping
  5. T3Data Quality → Scenario BuilderBuild a scenario — disabled while any row needs a decision
  6. T4Scenario Builder → Scenario ComparisonCompare with baseline — the single filled control in the footer, after the change is applied
  7. T5Scenario Comparison → Provenance ExportWhere does this come from — the difference row’s own trailing control
  8. R3Provenance Export → Scenario Comparisonthe Scenario stage of the audit breadcrumb. Every crumb is a control that does something, and a test asserts it — four of them did nothing at first compile
  9. Edge 1a factor gap on view 3exit: Resolve, through the drawer. Decline: Exclude these rows, behind a dialog that names the count. The banner itself has no dismiss control — the one non-dismissible surface in the product
  10. Edge 2a newer factor set published under the analysis, on view 5exit: Recompute on v4. Decline: Keep v3, which closes the band for the session and raises a pin chip. The band is up on arrival, and no seeding is needed to put it there
  11. Edge 3the export refusal on view 6there is no decline and no override. The blockers can be resolved and the export can be abandoned; it cannot be forced

The seven suites, and the two workshops

288 = 96 tests × 3 viewports · re-run at this commit: 288 passed
Suite× 3What it holds
critical-flow30T1 → T5 and back, the refusal-and-reason contract at each step, and the whole sequence with the URL never moving
edge-states36the three edges’ triggers, primary exits and declines; that edge 3 has neither; that no edge is ever the default
quantity-agreement33step ⑤b, static over every string literal containing a digit, and at runtime over the rendered surfaces
public-guardrails66public/QA separation in src/, in dist/ and at runtime; no URL parameter; no percentage set summing to 100; no numeric factor; no colour-only status; no filled destructive control
keyboard42the React Aria machines the spec binds, driven by key rather than by click
target-size30the preset’s bar on every control, plus the geometry no detector measures
a11y51axe-core on 17 surfaces × 3 viewports

axe: 51 scans, 0 violations at every impact — not merely zero serious and critical. Tags wcag2a wcag2aa wcag21a wcag21aa wcag22aa, scope body so React Aria’s portalled modals and popovers are inside it, and no waiver, no disabled rule and no excluded selector. Every one of the 51 run records in AXE_REPORT.json carries an empty violation list; the file holds the runs rather than a summary of them, so that can be checked rather than believed.

Storybook: 52 stories, 52 with a play(), 52 passing — Compare 9, Import 7, Mapping 9, Provenance 9, Quality 10, Scenario 8. Each mounts the shipped application and reaches its state through the product’s own controls; the stories live outside src/, so no public entry point can import one. Their axe bar is set to fail on any violation, which is how four of the accessibility defects in § 13 were found.

Four inventory rows have no product surface, and the stories say so

A parsing state that does not exist because the transition is atomic; a zero-row change the product cannot construct because it offers exactly one change and that change matches 41 rows; and two “dense” states for planes that have no expansion control. Each story asserts the rule the row exists to protect — that the transition is atomic and ships no progress element; that no percentage of any kind is printed on view 4; that every pair of plates at the narrowest viewport clears 8 px; that the lineage plane’s sixteen plates have 12 distinct second lines and 16 distinct full labels, the four repeats told apart by line one. So the build implements 48 of the 52 rows as described and 4 as the rule beneath them, and a reviewer who counts states will find that rather than a smooth 52.

§ 15 Appendix
the register
  • Q1
  • Q29

Appendix — the register, all twenty-nine

This is the table that was written before the first screen was described. The values are the canonical fiction’s; where a value moves through the flow the arrows are the states it moves through. The last column is the reason ⑤b had nothing to find on screen: every surface that names the quantity is a view of the one selector.

docs/build-pack/acreline/TASK_FLOW.md §0 — reproduced, with Q4's corrected selector
#QuantityIn the fictionThe one sourceViews
Q1rows in the dataset248dataset.rows.length1, 2, 3, 6
Q2columns in the file11dataset.columns.length1, 2
Q3canonical fields9CANONICAL_FIELDS.length2
Q4columns recognised7 → 8 → 9columns.filter(c => c.mapped).length2
Q5columns awaiting a decision3 → 1 → 0columns.filter(c => !c.mapped && !c.excluded).length2
Q6columns excluded1 → 2columns.filter(c => c.excluded).length2
Q7row coverage of one columnper columncol.valuesPresent / Q12
Q8rows ready201 → 219 → 244rows.filter(r => r.issues.length === 0).length3
Q9rows needing a decision47 → 29 → 0rows.filter(r => r.issues.length > 0).length3
Q10factor gaps3 → 2 → 0gaps.filter(g => !g.resolved).length3, 6
Q11rows behind the gaps34 → 16 → 0sum(unresolved gaps → g.rows.length)3
Q12rows included244rows.filter(r => r.included).length4, 5, 6
Q13rows excluded4rows.filter(r => r.excluded).length3, 4, 5, 6
Q14baseline total1,284 ttotal(baseline)4, 5, 6
Q15baseline range1,130 – 1,438 tband(Q14, UNCERTAINTY)4, 5
Q16proposal total1,169 ttotal(proposal)5, 6
Q17proposal range1,029 – 1,309 tband(Q16, UNCERTAINTY)5
Q18difference, absolute−115 tQ16 − Q145, 6
Q19difference, relative−9.0 %(Q16 − Q14) / Q14 at print time5
Q20category totals704 / 331 / 168 / 81 trollup(rowsIncluded, byCategory)4, 5, 6
Q21assumptions on a scenariobaseline 4, proposal 5scenario.assumptions.length4, 5, 6
Q22rows touched by at least one assumptionbaseline 43, proposal 66distinct(union(a => a.rows))5, 6
Q23pinned factor-set versionv3analysis.factorSet.version1, 3, 4, 5, 6
Q24current factor-set versionv4factorLibrary.latest.version5, 6
Q25dependents of the pinned version1 scenario, 2 saved viewspins.groupBy(kind)5
Q26assumptions carrying a rationale4 of 5assumptions.filter(a => a.rationale).length6
Q27export blockers2 → 0exportBlockers().length6
Q28items in the export pack6 included, 3 excludedPACK_INCLUDED.length, PACK_EXCLUDED.length6
Q29dataset import stamp14 Apr 2026 09:58dataset.importedAt1, 3, 6

The register is not a document that describes the build; it is a document the build is measured against. src/derive/quantities.ts computes all 29 in one place, and the agreement suite asserts both that it does and that no view file re-derives a register quantity locally with its own filter.

§ 16 Sources
and residuals
  • Pilot 6

Sources, stack, and what is still wrong

On the use of references

Before anything was designed, a preflight against a reference library made 19 real tool calls inside 28 JSON-RPC calls and fetched 292 screens with 0 download failures, which were tiled into boards and studied for structure. No reference application is named on this page and no reference screenshot is published — the study was structural, naming an application would imply a resemblance the build does not claim, and publishing the screenshots is forbidden by the package contract. The image-bearing material is excluded from this repository, and a byte-level scan for base64-shaped runs across every committed report file is part of the receipt.

The defects that sweep found in shipped products are cited in the build pack as the reason for three of the register’s four rules — a set count printed over a paged list, a total printed above rows each restating a different noun, and a difference of a real amount printed above an increase of 0.00 % repeated on every row. Those are the hazards this product is arranged to avoid; they are not accusations, and none of them is named here.

The stack, as installed

  • react-aria-components1.5.0 — resolved, not the range in package.json
  • react · react-dom18.3.1
  • typescript · vite5.5.4 · 5.4.11
  • storybook · @storybook/test-runner · addon-a11y10.5.4 · 0.24.4 · 10.5.4
  • @playwright/test1.62.0
  • axe-core · @axe-core/playwright4.12.1
  • @fontsource/inter · newsreader · jetbrains-mono5.3.0 — the product's three families, self-hosted
  • the buildtsc -b && vite build, 0 errors · 521.21 kB js (152.85 kB gzip), 57.02 kB css, 5 webp, 66 font files

Implementation

The application was implemented in a single Open Design run pinned to one model, whose receipt records the model observed on every one of 2,794 events, all 20 visual reference inputs and all 10 build-pack inputs consumed, 0 unread inputs, and 46 artifact files of which 28 were written or edited by the model. Everything in § 13 happened afterwards, in this repository, to that output.

What is still wrong, and was left

  1. Two composition rules the spec states for 768 are not implemented.

    The comparison’s label rail is not promoted to a row header above each lane’s values; the lanes stack, which is measured, but the promotion is a composition the build does not make. And the issue rail is not the 56 px horizontal strip the spec describes — it stays a bounded vertical block with its own scroll, because a 56 px strip cannot hold the groups, the resolved list with its stamps, and the control that clears an export blocker, and putting a public control out of reach is the defect F3’s repair exists to remove. The reason is good; the deviation is still a deviation, and the spec’s own 756 px arithmetic for that view no longer decomposes as written.

  2. The build pack predicted 7 accepted dead-space findings. The run measured 95.

    Every one is classified and carries a reason, and 64 of them are a single pane the pack never considered. But an estimate that is off by more than a decade was not doing any work, and the honest reading is that the prediction was a guess with a table around it.

  3. Five of the eleven blind-spot rows the pack wrote for itself remain human-only.

    tabular-nums is asserted as a CSS property, not as an aligned column of rendered digits. The interior luminance of a plate over the contour ground was never sampled — the plates are opaque by construction and no indeterminate contrast was reported, but that is an argument, not a measurement. Whether four illustrations read as four subjects, whether the overlap sentence reads as a finding rather than a hedge, and whether 4 of 5 reads as a measurement are not mechanical questions and are not answered.

  4. One press in the harness carries a 60 ms delay.

    A literally-zero-millisecond press — Playwright’s default, and a duration no pointer, finger or switch produces — is swallowed by React Aria’s combo box. The threshold was measured (0 fails; 10, 20, 30 and 50 pass) and the keyboard path is exercised separately with no delay at all. It is still a timing constant in a test, and a reader is entitled to be suspicious of one.

  5. One state is asserted after the fact rather than during.

    The import parse is atomic, so there is no frame in which a parsing state exists; its story asserts the atomicity instead. If a future change made the parse asynchronous, that story would keep passing while the state it stands for went unreviewed.

  6. Motion is never measured.

    Every state is settled before capture, by contract. Six projects in, that is a standing gap in the method rather than a fault in this build — but it is true here too, and the 140 ms and 220 ms transitions this product declares are the one part of it no gate has ever looked at.