feat(layout): one Board's geometry, and the regions enumerated once - #39
Open
pedromvgomes wants to merge 2 commits into
Open
feat(layout): one Board's geometry, and the regions enumerated once#39pedromvgomes wants to merge 2 commits into
pedromvgomes wants to merge 2 commits into
Conversation
|
A Board's Step tree in, flow-map geometry out. Nothing draws. `layout(board)` rather than `layout(doc)`: the canvas draws one Board at a time with a call as a doorway into another, so a function over the document computes four screens to draw one. A Placement is keyed by StepRef. Step ids are Board-local, so a bare id says two Blocks' `ret` share a position. `regionsOf` in @hatua/model is the single enumeration of what a container nests. walkSteps, stepLists and callsOf all read it, and so does the layout — so a region is covered by construction rather than by a fourth private walk with no corpus to disagree with it. That is why this package depends on the model; the model does not depend on it. A Fork's Branches are columns that converge. Every other region — a loop's body, a core.try's body and handler — is stacked under the card that owns it, each under a label band. A handler is not an alternative chosen between: it runs because the body failed, and part of the body has already run. The card height is answered here, from isContainer. Collapse is an input, and the only one that is not a function of the document; a collapsed container's children get no geometry, so the totals describe the map on screen. No Go mirror: a Host runner never lays anything out. Said in the doc comment so it is a decision rather than an omission. `--passWithNoTests` is gone from the package, so an empty suite fails.
The map placed a handler region on any verb; the list drew one only on core.try. A `handler:` on a core.fork is meaningless and no runner reads it, but walkSteps still yields the Steps inside it, so the generic rules report COMPONENT_UNKNOWN against them by name — a diagnostic naming a Step no surface draws is one nobody can select or delete. Refusing to draw a region does not make it absent from the document; it makes it unreachable. So StepList draws a handler wherever the key is, the rule its branches and its body already followed. The verb decides the word over a region, never whether there is one. StepList.test.tsx holds the two surfaces together on a Step carrying all three region keys at once. Also: one name for the node above the first Step, where handoff.md had been saying both "start node" and "root node"; an assertion for what EMPTY_REGIONS is named for; and centre() states the property it keeps rather than a rounding failure its callers make unreachable.
pedromvgomes
force-pushed
the
feat/layout-algorithm
branch
from
August 24, 2026 15:25
4e78084 to
883fed2
Compare
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.
Stacked on #38. Base is
feat/try-and-item.packages/layoutturns a Board's Step tree into geometry. It draws nothing —FlowMap.tsxis still a<Placeholder>and stays one.What it takes, and where the walk lives
layout(board: Board, { collapsed }). Notlayout(doc): ADR-0013 says the canvas draws one Board at a time, with a call as a doorway into another, so a function returning every Board's geometry computes four screens to draw one and needs a second coordinate space to hold them apart.That needs
@hatua/model, which is a dependency edge that did not exist. It creates no cycle — model importsexpressions,schemaanddocument, and must not import this. The edge is cheaper than a fourth traversal, and the argument is not about packaging:This stack has fixed three walks that forgot a region —
walkSteps,stepLists,callsOf. So the regions are now enumerated once, asregionsOf(step)in@hatua/model, and all three read it, and so does the layout. A container's regions are one answer, in one place, pinned by the cross-language rules corpus that already exerciseshandler:.That changes what the coverage test proves, and it is worth being exact about it. "Every Step
walkStepsyields has exactly one Placement" no longer catches a forgotten region, because both readers would forget it together. It catches a Step placed twice, and a Board's Steps placed under the wrong Board. What catches a forgotten region is the corpus and a fixture that carries all four regions — see the mutation results below, where dropping the handler fromregionsOffails five model tests as well as four here.The decisions
Four regions, and where the second one of a
core.trygoes. A Fork's Branches are columns that converge — they are alternatives chosen between, and which one is the reader's question. Every other region is stacked under the card that owns it, in document order: a loop's body, and acore.try's body above its handler. A handler is not an alternative chosen between — it runs because the body failed, and part of the body has already run by then. Columns would make left-to-right mean "later" in the one place on the map where it means nothing else, and would put a third thing on screen that reads as a Fork.What tells the regions apart is a label band above each one —
try/on failure,loop,if/else if/and. Every child region gets one, including a Branch's, so acore.try's second region costs no shape the first did not already have.<StepList>already answers this way with the chip over each region.Card height is answered here.
heightOf(step)returnsnodeHeightWithMetaexactly whenisContainer(step), because the meta row is the container summary. A caller-supplied height would leaveFlowMap.width/.heightas totals over numbers this package never saw, so it could not compute them honestly.The root node is a
Rect, not aPlacement. It names no Step — it is chrome derived fromdoc.triggers[]on the root Board and from the contract inside a Block, which is what keepsremoveStep,walkStepsandunknownComponentsfrom needing a case for it. APlacementwith an optional ref would push "sometimes there is no Step here" into every consumer's type to spare one field here.A Placement is keyed by
StepRef, never a bare id. Two Blocks may each hold aret(#34), andstepKeyis the flat spelling where one is needed.Collapse is an input, and the only one that is not a function of the document. A collapsed container's children get no geometry at all rather than geometry the canvas hides — otherwise every reader of a total (scroll extent, fit-to-screen, a minimap) reads a number that is wrong whenever anything is folded.
No Go mirror, deliberately. Hatua does not execute and a Host runner never lays anything out. Stated in the package doc comment, so it is a decision rather than an omission a
sdk/go/layout.gocould quietly fill in.Verification
--passWithNoTestsis removed frompackages/layout. Confirmed by movinglayout.test.tsaside:turbo run test --filter=@hatua/layoutthen fails withNo test files found, exiting with code 1.49 tests, over six shaped documents asserted non-empty where they are iterated:
walkStepsyields has exactly one Placement, per Board, cross-checked as sorted sets and then as counts — over a fixture with a Fork, a loop, and acore.trywith both regionsretproducealpha/retandbeta/retwidth/heightbranches:,steps:andhandler:at once gets all of them (nothing in the schema or the rules refuses that document, so branching on "is this a Fork" would silently drop two regions)Mutation-tested:
regionsOfdrops the handler region@hatua/layout, 5 failed in@hatua/modelPlacementkeyed by a bare id (board: null)@hatua/layoutpnpm typecheck && pnpm lint && pnpm testgreen (18/18 turbo tasks; the two remaining biome warnings are pre-existing —scope.ts:366andTemplateInput.module.css:164— confirmed by stashing this branch's changes and re-running).go vet ./... && go clean -testcache && go test ./...green.pnpm codegenleaves no drift: 15 generated files before and after, asserted non-zero first, same SHA-256 over the file list both times, andgit statusagrees.Which layer could hold this wrong and stay green?
Asked once, in writing. Four honest answers:
@hatua/layout. Its tests are its only consumer, so a plausible-but-wrong algorithm is invisible until the canvas lands. This is the biggest one and this PR does not close it.LAYOUT's numbers againstdocs/handoff.md§ Flow map geometry — which this PR adds, because the doc comment previously cited a document with no geometry in it. A number changed in one and not the other is green.<StepList>'s own region reading. It computesBoolean(step.branches?.length || step.steps || step.handler)and walks the three keys itself, so it andregionsOfagree by inspection and by nothing else. Left alone here by instruction; named inpackages/react/src/layouts/README.md.regionsOfis TypeScript-only. A fifth region would need Go changed separately, and only a corpus fixture exercising it would catch the omission.Documentation
docs/handoff.mdgains Flow map geometry: every constant, what it measures, what it has to satisfy, the region shapes, the root node and collapse. The package doc comment cites it, and now it says something.CONTEXT.md's Derived Layout entry gains what a Placement is, that layout is per-Board, that collapse is the one non-document input, and that this rule is implemented once rather than twice.packages/react/src/layouts/README.mdgains the region shapes, and the second reading named above.Next in the stack
The canvas that draws these Placements.
Build.tsxmounts<FlowMap />with no props and holdsselectedIdas a barestring, which is Board-blind for the same reasonPlacement.stepIdwas. And nothing anywhere lets a user reach a Block's Board, though ADR-0013 decided that a call is the doorway.