Phases 3-9: import, inventory, 3D, openings, clearance, floors, portability - #1
Merged
Conversation
Directory, package name, app title, and the `app` field written into every .space manifest. Readers never checked that field, so files written by the old name still open.
PDF and image import, the blocking calibration gate, background transform/opacity/lock, and the runtime asset store that finally makes a .space file carry what it references. Three defects found along the way, two of them predating this phase: - `openDocumentFile` returned `.document` and dropped `.assets`, so a space with a background opened, saved and reopened with nothing behind the walls. - Konva refreshes hit geometry on the next draw, not on assignment, so a layer switched on and clicked in the same frame is still deaf: pick Select, click a wall fast enough, nothing happens. The tool is now checked inside the shape handlers, and the mode switch rebuilds the hit graph in a layout effect. - The gate grew taller on mousedown, shifting the stage under the pointer, so a reference drawn as 3000mm committed as 3048mm and every dimension traced afterwards inherited the error. pdfjs is dynamically imported: 437kB of PDF renderer no longer loads for people who draw by hand. Main bundle back to 599kB from 1,036kB.
Both canvas specs redeclared SCALE and ORIGIN with their own copy of the reasoning; when DEFAULT_VIEWPORT moves, one would go stale and fail like a product bug rather than a broken assumption. The history coalescing keys on the label alone, so the case that would break it is a different edit landing between two slider frames. Asserted directly: opacity, rotate, opacity leaves three entries and each undoes separately.
Catalog and placements are separate entities, so "I own 6, 4 are placed, 2 to go" stays expressible. Manual entry in any unit, 28 presets at real published sizes, click-to-place, drag, rotate, wall snap, surface snap, and a validation panel that warns and never blocks. The calibration gate stops being decorative: addPlacement throws PlacementBlockedError carrying the same sentence the panel shows, and the Place button is disabled rather than offered-and-refused. Wall snap seats the footprint's back edge on the wall's near face, not its centre on the centreline — a centred 2000x900 sofa is half inside the wall. Tested as a round trip at eight angles including three off-axis. Two defects fixed in existing code: - PlacementLayer cancelled the event bubble unconditionally, the same pattern fixed in StructureLayer last phase. Unreached only because nothing created placements. - The hit graph also goes stale when a shape first appears, not only when `listening` flips: place an item, click it immediately, and it does not select. drawHit now runs on a change in shape count too. deleteSelection re-seats anything surface-mounted on a deleted placement, so the document never references a host that is gone.
…l path The item form let you choose Wall or Ceiling, and addPlacement then dropped the item on the floor without saying so — the same offered-and-refused pattern the Place button was just fixed for. Both options are disabled until phase 5 gives them somewhere to attach. beginPlacementTransform now refuses a placement whose item is missing. Its snap context would be null, which silently ignores Alt for the whole drag; it was already unreachable because such a placement renders nothing to grab, and this makes that a rule rather than a coincidence. Every placement e2e dropped items into an empty document, so the composition of a real snap context with a drag that re-solves each frame had only synthetic unit coverage. Added a spec that drags a sofa across a room into a wall. The unreadable-file spec now asserts the message rather than a count that was already zero — the only way that assertion could fail was a blocked page, which reads as a mystery timeout rather than a bug.
An opening cuts a wall in elevation, not in plan, so `ExtrudeGeometry` holes
were never going to work. `wallSegments` splits a wall into the solid boxes
that remain instead — flank, sill wall, lintel, flank — which needs no CSG and
gives the 3D view, the walker and the validation panel one geometry to share.
A doorway becomes passable because the only solid above it starts at 2032mm,
with no special case anywhere in traversal.
Phase 5 opens three new ways for a reference to dangle, and each is closed
here rather than tolerated downstream:
- a wall-mounted placement keeps its stored elevation whether or not its wall
exists, so deleting a wall now re-seats it instead of leaving a shelf in
mid-air;
- dragging a wall shorter leaves its openings hanging past the end. That is
reported, not clamped — silently sliding somebody's front door along the
wall to make it fit hides the mistake;
- a ceiling mount resolves to `ceiling − drop − height`, which goes negative
for anything tall enough and sinks the item through the floor. `solidSpan`
will not object, so validation does.
Opening sizes are the real published ones — a 32" x 80" door is 813 x 2032,
not "about 800 x 2000" — for the same reason the furniture presets are.
`LengthField` moves out of `ItemForm` so the properties panel can edit an
opening in any unit, committing on blur rather than per keystroke.
393 unit tests, 50 e2e.
The document has been three-dimensional since phase 1; this is where that stops being a claim. Walls, floors, ceilings and placements extrude from the same geometry the plan view draws and the collision engine tests — there is no second scene graph to keep in sync — and you can walk through the result with the arrow keys. Two decisions carried the phase. **The walk simulation does not live inside three.js.** `useFrame` is the obvious home and the wrong one: it makes walking something that only happens when a WebGL context exists, and makes traversal impossible to test without a GPU. It is a plain rAF loop over pure functions instead, so the camera is a consumer of the walker rather than its owner, the position readout keeps working when the canvas does not, and 31 unit tests walk through doorways with no renderer in sight. The canvas sits behind an error boundary; the HUD and the loop sit outside it. **Step-up is not a key — it is the bottom of the body interval.** A body of [0, 1800] collides with a 5mm rug, because [0,5] and [0,1800] genuinely overlap, and the walker is stopped dead by a carpet. Starting the interval a stride above the floor gives [200, 1800]: the rug passes underneath, the dresser at [0,810] still blocks, the bed frame at [250,600] still blocks, and a doorway lintel at [2032,2438] still lets you through. One number, and every case falls out of it — the same shape of fix as `voidBelowMm`. Space raises the clearance for a deliberate step; C lowers the top so you can duck under a shelf. Arrow keys **turn** rather than strafe, departing from PLAN.md §10.2. The requirement is arrow-key traversal, and with turning bound only to Q/E someone using the arrows alone can never change direction. A/D strafe instead. Wall and ceiling mounts become reachable, which is what the item form was holding those options back for. An item that says it is wall-mounted and is dropped where there is no wall lands on the floor **and says so** — never on a wall the app guessed, because moving that wall would then move the item. Also here: a walker who has been built around can walk back out (every candidate move is blocked, and being frozen is worse than being briefly inside a wall); a ceiling drop that resolves below the floor is clamped to it rather than sunk out of sight; and saved views encode through a pinned key set with a total decode, so a record this version did not write cannot put a NaN into a matrix and blank the screen. three and drei are code-split like pdfjs — 888kB that most sessions never load. Main chunk 661kB / 209kB gzip, up 14kB from phase 4. Deferred and stated rather than claimed: instancing (§10.4's 500-at-60fps is unmeasured; one mesh per solid today) and a contact-normal collision resolver (moves retry per axis, so diagonal walls slide stickily). 463 unit tests, 64 e2e.
Two defects the phase-5 tests could not see. **A drag was silently re-seating mounted items on the floor.** The snap only ever reports a surface mount or a floor mount — a wall snap seats the footprint against the wall but never claims a wall mount — so the floor result means "no host here", not "put this on the floor". Passing it through dropped a wall-mounted TV to the ground the moment it was nudged 5mm along its own wall, with no message. This is the same shadowing bug as the drop path, one layer down; `PlacementTransform.origin` now carries the mount the drag began with, so a host wins, a surface mount that has left its host falls, and a wall or ceiling mount is left alone. Moving a thing is not detaching it. **The 3D view ignored the edit mode.** Clicking a wall in furnish mode selected it and the properties panel offered to delete it — structure the plan view is refusing to let you touch. The layer toggle is a property of the document, not of the renderer. Every space spec entered from plan mode, which is why nothing caught it; the new pair asserts both the refusal and that the click really does reach the scene, so the first is not passing on a raycast that missed. Also: `notice` is cleared by undo, redo and a mode switch, rather than outliving the thing it describes. And the ceiling behaviour is written down — a global toggle rather than camera-aware hiding, because deciding "is the camera inside this room" per frame flickers on every threshold — since the other two departures from PLAN.md were documented and this one's silence read as an oversight. 466 unit tests, 66 e2e.
Five kinds of opening behaved identically once the hole was cut. They behave in four different ways, and the difference is the reason the kinds exist: door hinged, swings into a room → the swept sector must stay clear sliding slides across the wall face → the wall it parks over must stay clear pocket slides into the wall → nothing in the room must stay clear cased no leaf → nothing window fixed glazing → nothing A pocket door needing no room-side clearance is the entire argument for fitting one, so a model that drew it like a slider would be answering the question wrong rather than approximately. The swept sector is computed once and serves three consumers. Its boundary *is* the 2D door symbol — the closed leaf, the arc, the open leaf — so the drawing and the clearance check cannot disagree about where the door goes. It is also the collision polygon and it positions the 3D panel. Details that would be wrong if skipped: - The hinge sits on the *face* the door opens onto, half a wall thickness off the centreline. 57mm on a 114 wall: invisible in a drawing, a systematic bias in every clearance answer. - The sector is polygonised at one vertex per 5°, because a chord cuts inside the true arc and a narrow object at the outer edge falls into the gap. Both the area invariant and the sliver case fail at a coarser step; verified by setting it to 45 and watching them go red. - Clearance is tested against furniture only. A door swinging back against the adjacent wall is how doors are hung, not a fault. - Glazing blocks the walker; an open door does not. Glass is something you cannot walk through, and a leaf drawn open would otherwise narrow its own doorway by however far it happened to have been swung, with no way to push past it. One stored field, read according to the kind. `leafOf` returns a shape named for the leaf: a slider structurally has no angle to read, a pocket door no face. The field is retained across a kind change — door to cased and back is the door you had — so a cased opening may carry a swing nothing reads. Losing a choice the user made is the worse trade. `updateOpening` accepted a `swing` in its patch type and silently dropped it. Hanging the leaf uses flip buttons rather than selects: there is no honest label for the two sides of a wall, and the arc in the drawing is what makes the choice legible. Adding `opening` to the 3D scene's refs would have undone 837fd4a — an opening ref fell through both branches of the layer-toggle guard, so a door leaf could be selected, and deleted, in furnish mode. The guard is now one rule in `modes.ts`, written as "placement, or else structure", so a kind added later locks with the structure it belongs to instead of becoming selectable everywhere. Not claimed: windows do not open. And the 3D gate on a leaf is covered by an exhaustive unit test over `refIsEditable` rather than end to end — in orbit a leaf is a slab a few pixels wide seen edge-on, and walk mode takes no selection clicks at all, so a grid of clicks would test where the camera happens to sit. The first version of that e2e passed with the guard reverted, which is how it was caught. 516 unit tests, 73 e2e, typecheck, lint, build.
The swing angle wrote to the document on every keystroke, and the stored value is clamped to 15°. So typing `135` sent `1`, which was stored as `15`, which redrew the controlled input as "15" — and the next keystroke landed against that. No angle whose first digit falls below the floor could be entered at all. Every keystroke was also its own undo entry. The fix is the pattern already in this file twice — the room name holds its text locally "so a rename is one undo step, not one per keystroke", and `LengthInput` commits on blur or Enter and reverts on Escape. The angle field was the third instance of the same need and the only one that skipped it. `NumberInput` is that contract for a plain number. The e2e could not see it: `fill()` delivers the whole value in a single change event, which is the one input path that works. Rewritten to type the digits, and verified against the old control — it ends at 180 rather than 135. 517 unit tests, 74 e2e.
Two checks that sound alike and are not. A clearance zone asks whether a drawer opens; the walkway probe asks whether a person gets past. They disagree about walls, and that disagreement is the design. Zones do not test against walls. Wall snap seats an item's back edge *on* the wall face, so a `back` zone tested against walls fires on every chair pushed against one — the default outcome of using the snap, not a corner case. That is the rule phase 6 settled for door swings, and the probe is what answers the other question, with walls very much included. Two more that would have been wrong if skipped: - **The step-over threshold belongs to the intruder, not the zone.** A rug in front of a dresser is not a blocked drawer, but lifting the zone's floor to fix that exempts a band of space and hides a 90mm shoe rack sitting in it. What makes the rug irrelevant is that you step over it. Same shape as `voidBelowMm`. - **A zone goes through `toWorld`**, the transform the outline itself uses, so rotation and flipping come out right by construction. Flipping really does move a left-hand drawer to the other side. ## The probe height in PLAN §9.3 was wrong It specified a single ray at 900mm — "hip height, where you actually squeeze past furniture". A standard sofa back is 840mm, so that ray passes straight over the one piece of furniture the spec names and reports a clear walkway through the middle of the couch. A dining table at 760 and a dresser at 810 go the same way; any single height is either low enough to catch table legs or high enough to miss the sofa. So the probe asks what traversal already asks: is anything solid inside `[STEP_OVER_MM, STAND_HEIGHT_MM]`, imported from `walk.ts` rather than restated. The walker's answer about a doorway and the plan's answer about a gap can now never disagree. Found by a test asserting a sofa was an obstruction and getting 4 walls back; PLAN.md records the correction rather than quietly matching the code. The route is editor state, like a measurement — a question asked of the plan, not part of it. Storing the *path* rather than the number is what makes it worth having: move the sofa and the answer follows. It survives a tool change, and the tool stays live in furnish mode, because "can I still get past?" is a question you ask while pushing furniture around. That last one needed the palette's `t !== 'select'` gate widened and the placement layer's `selectable` narrowed to Select, or a click meant for the route selected the sofa instead. Zones draw on the selected item only. A warning that says "the bookcase blocks the drawer pull" is an argument and the hatched rectangle is the evidence — but six dining chairs with pull-out zones would carpet the floor in hatching and say nothing. The panel groups by what you would do about a problem rather than by the issue enum, in one list, keeping `validateFloor`'s blocking-first order rather than forming a second opinion about severity in the component least qualified to have one. Stated rather than claimed: the medial-axis navmesh stays out of scope, so the probe reports the narrowest gap *at a sample* — vertices plus every 100mm — not the true infimum. 555 unit tests, 85 e2e, typecheck, lint, build.
`findClearanceViolations` skipped the zone's owner and anything a stride clears,
but not what was stacked *on* the owner. A lamp surface-mounted on a dresser rides
on it and cannot be in the way of its drawers, whatever its footprint does.
It was quiet only by coincidence. A zone runs from the floor to `heightMm`, whose
default is the host's own height — which is also the elevation a surface-mounted
child resolves to. The two spans meet exactly and `spansOverlap` is strict, so
nothing fired. Set a zone's `heightMm` explicitly, which is the documented reason
the field exists ("a drawer pull at 810mm is indifferent to a shelf at 1500"), and
the coincidence goes: the lamp reports that it blocks the dresser.
Nothing in the shipped preset library triggers it, so this is a latent trap rather
than a live bug — confirmed by writing the test first and watching it fail.
The walk is by chain rather than one level, so a tray on a lamp on a dresser is
still on the dresser, and it terminates on a cycle instead of hanging. Paired with
a test that something stacked on a *different* nearby item still reports, so the
exemption is "it rides on the host", not "it is off the floor".
Also: `narrowestGap` now says it assumes pre-filtered obstructions — it consults no
spans, and `walkwayObstructions` is what filters by band. And a test that the
walkway route does not survive `loadDocument`, which it already did not.
558 unit tests, 85 e2e.
Detection walks the planar graph of wall centrelines and keeps the faces that wind positive. By sign, not by magnitude: a courtyard's outer face is smaller than the room around it, so "discard the biggest" gets it backwards. Walls are split at crossings *and* at T-junctions. The T-junction pass is the one that matters — a partition butting into the middle of a wall has its endpoint on that wall's interior, and without a node there the graph has no branch and the walk hands back the single loop around the outside. Boundaries are centrelines, matching commitRoomRect, so a drawn room and its detected twin compare equal — asserted, along with the ring canonicalisation that makes a second run a genuine no-op rather than a rewrite of every boundary in the document. Detection adds and updates but never deletes. An Area-tool room has no walls by design; removing what detection cannot see would delete a legitimate room on every run. Unmatched rooms are reported and left alone. Matching is by overlap area, so the larger half of a partitioned room keeps its name. Ceiling height is now editable, which is what makes it worth having: the headroom check reads it through ceilingHeightAt, so lowering a room to 1900mm immediately reports the wardrobe that no longer fits. The three mechanisms were each verified by reverting them and watching the right tests go red.
index is the stacking order and the array is insertion order. orderedFloors
is the only thing that sorts, because the two disagree permanently the first
time someone adds a basement: it takes index -1 and is appended.
Switching floors is a silent document write. Undo walks back the edits you
made; having it teleport you between storeys would make the stack unusable.
It still dirties the document, because reopening a house on the floor you
left it is why activeFloorId is in the document at all. The switch also
clears selection, draft and route — pruneSelection would keep every one of
them, since a wall on the floor below still exists.
Moving a placement carries everything standing on it, however deep, or a
surface mount is left naming a host on another floor that findPlacement
resolves into an elevation against the wrong datum. A wall mount is reseated
and reported. Floor deletion does the same repair and refuses the last floor.
The ghost underlay participates in nothing: not the hit graph, not the
counts, not floorBounds and so not zoom-to-fit — which is what keeps the
viewport still across a floor change, the only reason the underlay is worth
having. listening={false} is load-bearing rather than tidy: PlanStage reads
empty canvas as e.target === stage, and that is what clears the selection and
starts a pan. My first test for that could not fail; the replacement was
verified against a listening ghost before the guard went back.
Collision stays on the active floor whatever the 3D toggle says. Floors
default to elevationMm 0, so feeding the stack to the walker would have you
inside the walls of a storey you were only looking at.
PLAN row 2 promised phase 8 would relate rooms to their walls. Detection is
that mechanism, so room-boundary dragging is now dropped for v1 rather than
left dangling — recorded in both rows.
The ghost floor ate clicks. A solid on another floor stopped propagation and then declined to act, so R3F never reached the active-floor wall behind it — looking at a building with every floor shown, the top storey swallowed everything. The guard now runs before the stop. The existing 3D selection tests run at the default single-floor visibility, which is why a green suite said nothing about it. deleteFloor never ran the reset it was written to run. Its recipe moves activeFloorId, which makes the setActiveFloor call afterwards a no-op — so a walkway route drawn on the deleted floor survived and re-answered against the remaining floor's geometry. The reset is now its own store action that whoever changes floors calls. addFloor left activeFloorId dangling after undo: it switched after its own mutation, so the inverse patch removed the floor while the id still named it. activeFloor falls back, which hides it, but the picker matched no option and a save wrote an id that was not in the document. The switch is now inside the recipe, so undo restores both — a bare switch stays out of history, a switch that rides along with an edit does not. movePlacementToFloor skipped the calibration gate addPlacement applies, and walked only the source floor for riders — the same cross-floor surface mount deleteFloor already repairs and tests. Validation is active-floor scoped. That was a distinction without a difference with one floor and is now the difference between "no issues" and "none that I looked for", so the panel names the floor. Each fix has a test that was watched failing against the code it replaces.
Ctrl+S writes the file it wrote last time. The handle is read from `window` at call time, never snapshotted at import — headless Chromium has the API, so a snapshot would leave the download path with no end-to-end coverage and make the in-place path undrivable from a stub. Cancelling the picker is a decision, not a failure: nothing is shown and the document stays dirty. Autosave keeps assets in their own IndexedDB store. Keeping only document.json recovers a space whose background is gone, which is the failure assetMapFor throws to prevent reached by another door; rewriting the raster every 20s is absurd. An asset is immutable once stored, so it is written by id once and every tick after that writes the record alone. Recovery has two offers. §5 covers "newer than the opened file"; the case that matters after a crash is the one it does not describe, where there is no opened file and the record belongs to an id this session has never heard of. A record is deleted when its document is saved, so a surviving one always means unsaved work — without that rule every clean reload nags and the prompt is trained out of you. Thumbnails render offscreen from document geometry, not from the Konva stage, which is not mounted in 3D and would capture the current pan. Also here: drag-and-drop onto the window, which §5 required and no phase had built, and the frozen schema-1 container. That fixture is hand-authored and committed on purpose — one regenerated from current code asserts only that today's writer agrees with today's reader.
schedule() returned early while a timer was pending, so the debounce was armed by the first change after a write and never re-armed. The deadline term in autosaveDelay is lastWriteAt + 20000 - now, and lastWriteAt is always recent at the moment schedule() runs, so the min always picked the quiet period: a two-second throttle wearing a debounce's documentation. Every delay autosaveDelay returned was individually correct, which is why an exhaustive unit test over it stayed green. The behaviours differ only in how many writes a burst produces, so the new test drives mutate every 100ms for 20s and counts them — one at the cap, against nine before. The burst that reaches this is a slider, not a drag: the two-slice store means a wall drag touches the document once, on release.
Four parser tiers — JSON-LD, microdata, OpenGraph, page text — with the dimensions tracked apart from the name and image, because they are the only part that becomes geometry and a page can publish a clean name while leaving the measurements to a paragraph. Scraped numbers do not go through parseLength. It reads a bare number in the document's display unit, which is right for a field with the unit on screen beside it and exactly wrong for a spec table: 84 x 38 x 32 in a millimetre document is an 84mm sofa. Every number here needs its own unit, a QuantitativeValue with no unitCode included. The client decides the endpoint is absent by content type, never response.ok. A static host answers an unknown POST with a 200 carrying index.html; ok is true and the JSON parse then throws inside a catch written for network failures. vite preview is such a host, which is why the degradation test runs against a real absence. The endpoint fetches a URL a stranger supplied from inside the server's network. It resolves before connecting, revalidates every redirect hop, and caps the read as it goes rather than trusting content-length. Fixtures are synthetic and §13 now says so. Building real ones means fetching third-party pages for this repo's convenience and committing someone else's markup. Also fixes an old one this work walked into: the item form prefilled raw millimetres into fields parsed in the display unit, so opening a 2'7" armchair and pressing Save with nothing changed made it 67'6" wide. The existing edit test only renamed, so nothing caught it.
::ffff:7f00:1 is 127.0.0.1 written in hex, and the mapped-address check only recognised the dotted form — which is not the spelling anyone probing the guard would use. Addresses are now expanded to their eight groups and tested, rather than matched by prefix string. The numeric host forms turn out to be handled a layer down: the WHATWG URL parser normalises https://2130706433/ and 0x7f.0.0.1 to a dotted quad before parseTargetUrl sees the hostname. That is load-bearing and invisible, so it is asserted rather than assumed — the literal guard would otherwise have a hole covered only by the DNS step, which a runtime with no resolver skips entirely. Second gap: handleProductLookup had no coverage at all. Everything else tests the parser or the guards, and the e2e suite runs against preview, which has no endpoint by design — so the wrapper both deployments call had never been executed. Now tested, and the dev middleware was run by hand: 400 with a JSON message for a bad URL, 400 for a private address, 405 for a GET.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brings
mainup to the end of PLAN.md's phasing table: 19 commits covering phases 3through 9, plus the review passes each one ended with.
What lands
Decisions worth knowing about
Collision always comes from the active floor, whatever the 3D display toggle says.
Feeding the whole stack to the walker would make traversal depend on a view setting,
and since floors default to
elevationMm: 0a second floor added before its elevationis set would put you inside the walls of a storey you were only looking at.
Room detection keeps planar-graph faces by sign, not by size. A courtyard's outer
face is smaller than the room around it, so "discard the biggest" gets it backwards.
Detection adds and updates but never deletes — an Area-tool room has no walls by
design, and removing what detection cannot see would delete a legitimate room on every
run.
supportsSaveInPlace()readswindowat call time. Headless Chromium has the FileSystem Access API, so a module-load snapshot would leave the download path with no
end-to-end coverage and make the in-place path undrivable from a stub.
Autosave keeps assets in their own IndexedDB store. Document-only would recover a
space whose background is gone; rewriting the raster every tick is absurd. An asset is
immutable once stored, so it is written by id once.
Scraped dimensions do not go through
parseLength. It reads a bare number in thedocument's display unit, which is right for a field with the unit beside it and exactly
wrong for a spec table —
84 x 38 x 32in a millimetre document is an 84mm sofa.Deferred and stated rather than claimed
medial-axis navmesh is out of scope.
fetchwill not pin a socket.Verification
765 unit tests, 124 Playwright e2e, typecheck, lint and production build all green.
Every defect fixed along the way has a test that was watched failing against the code
it replaced — including the worst of them, an item form that turned a 2'7" armchair
into a 67'6" one when opened and saved with nothing changed.