Skip to content

feat(harness): add the projection, locators, checks, trace, and the first demos - #67

Draft
robfeldmann wants to merge 23 commits into
phase-4/05-scenariosfrom
phase-4/06-projection
Draft

robfeldmann wants to merge 23 commits into
phase-4/05-scenariosfrom
phase-4/06-projection

Conversation

@robfeldmann

@robfeldmann robfeldmann commented Oct 24, 2026 •

Copy link
Copy Markdown
Owner

Slice 6 of plan 036 (docs/plans/phase-4-view-layer-and-developer-tooling.md): the projection, the SEL locators, the invariant rules, expect cost, the trace, shot as text plus grid, and the first per-primitive demos. Stacked on #66 (phase-4/05-scenarios), so this PR targets that branch.

What lands

  • Projection (Sources/TesseraHarness/Projection): the view graph's diagnostics as sorted-key JSON with a version, plus the screen as rows and a style grid. dump prints it.
  • Rules v0 (ids are the 1.0 promise): no-dirty-after-pass, frame-inside-clip, measured-within-proposal, siblings-disjoint, requirements-aggregate. check [RULE...] is a statement; tessera check NAME [SIZE] runs them over one app with no input. A violation names the rule and the node. siblings-disjoint looks through ForEach, if, and view groups to the container that places them, so layers a list builds under a ZStack are layers.
  • Locators: layout= and id= join type=, text= (a substring of the leaf's own cells), and nth=. An empty value is a parse error.
  • expect cost NAME<=N over bodies, bytes, measures, placements, and renders; the bytes come from a new package seam, TerminalSession.bytesDrawn (a running total, differenced per step).
  • Trace v0: one JSONL record per step (event, drew, invalidation reasons, changed nodes and rows, cost). tessera run --trace DIR and TESSERA_TRACE write it; tessera replay FILE --app NAME runs it again and compares everything except bytes. Keys, pastes, resizes, and the terminal's focus changes replay; a mouse event or a terminal reply is refused by name.
  • shot writes NAME.txt and NAME.grid.txt (the catalog's grid dialect). The grid never has more style rows than chars rows, and a screen with more than 62 distinct styles fails the shot as a harness error. NAME.txt now reads a wide grapheme as one character.
  • Demos in DemoSupport, each with a desktop and a mobile scenario that shots both and asserts a frame (most also run check): text (with the archived Unicode samples), stacks (Spacer, Spacer(minLength:), a Spacer in a VStack, priority; absorbs the stack demo), frame-padding, zstack (layers from a ForEach and an if too), flex (two pages: the constraints, then a priority tier, a child with no .flex value, and cross-axis placement), foreach. The corpus is 16 scenarios.
  • Library: ViewGraph.update() discarded a resize's viewport reasons, so a resize redraw reported none; fixed with a test and a line in the Slice 1 spec.
  • Docs: the Developer tooling page, the scenario README, the tessera-devtools skill (0.3.0), mise-tasks/dev/{check,replay,run}, text.md (it says what the Text demo renders until Slice 8), the plan, and the CHANGELOG.

Critic review

Every finding is fixed in a7260a7, each behavioural one with a test: F1 siblings-disjoint through list nodes (crafted projections, live fixtures, the zstack demo and its scenarios; it fails without the fix), F2 the grid row rule, F3 62 styles fail closed, F4 run --pretty warnings, F5 empty arguments, F6 the demo gaps, F7 siblings-disjoint named in the Flex scenarios, F8 CHANGELOG, F9 the trace version first, F10 the help overview, F11 text= is a substring, F12 the ForEachDemo comment. The open question is settled by recording focus events replayably (Discoveries in the plan). The re-check's fix is a7d0b5c: a view list is recognised by its module-qualified type (TesseraCore.ForEach, Swift.Optional, …) and carried as the projection node's isList, so an app's own view named ForEach is not looked through (a live test fails under short-name matching), and the README says a container-less multi-view body is a true positive.

Verify

  • mise run core:test: 959 tests green on Xcode 26.6 and on Xcode 27.1 (838 on the base).
  • swift test --package-path Examples --only-use-versions-from-resolved-file: 39 tests green on both (25 on the base).
  • mise run dev:run Examples/Scenarios: 16 scenarios, 16 passed; two runs give the same bytes, and --repeat 3 agrees.
  • mise run quality:format, quality:lint, and docs:lint green, and the pre-commit hook's lint of Examples/Tests; no Package.resolved in the diff.
  • CI on 6ea1bd5 (run 40087437615, after the critic fixes): all 14 jobs green, and every test leg printed 16 scenarios, 16 passed, 0 failed, Windows included. The head carries the run after the re-check's fix.

For the owner

Decisions and discoveries marked "(owner to weigh)" are in the plan. The ones most worth a look: a bare check fails on errors and only lists frame-inside-clip warnings (and run --pretty prints them); text= matches a substring of the topmost leaf; the grid golden's row rule and its 62-letter limit; live traces record focus events; and the library findings below.

Library findings, each recorded in the plan and left as today's behaviour unless noted:

  • update() dropped a resize's viewport reasons (fixed here).
  • Every step of an app with stacks reports layoutEnvironmentChanged and renderEnvironmentChanged: stacks rebuild their children's EnvironmentValues and reconciliation compares storage identity.
  • The headless VT lacks DEC mode 2027, so ZWJ emoji and regional indicators advance different cells than Tessera's width table.
  • A layoutPriority Spacer starves lower-priority rigid siblings.
  • Flex's min(n) floor cannot compress while Text has min == ideal, fill weights invert when children are oversized, and Flex takes no alignment (a smaller child is always leading or top, though flex.md says "the selected" alignment).
  • measured-within-proposal and frame-inside-clip fire by contract on a fixed frame wider than its parent, on Flex overflow, and on non-wrapping Text; the workspace demo at 40x16 trips them on a collapsed pane (Slice 8).
  • A root @ViewBuilder body is always dirty, so every step draws (Slice 4 found it; the cost counters now show it).

…a draw's bytes

ViewGraph.update() cleared the viewport invalidation reasons that resize had recorded, so a
redraw after a resize reported none. The graph now remembers the change until the next render.

TerminalSession gains a package bytesDrawn: the bytes the last draw wrote, which the harness
reads for its cost counters without a lock or an output tap.
…irst demos

Projection is the view graph's diagnostics as sorted-key JSON plus the screen, with a version.
Five rules v0 read it (no-dirty-after-pass, frame-inside-clip, measured-within-proposal,
siblings-disjoint, requirements-aggregate), through the check statement and tessera check.

The scenario language gains dump, check, expect cost NAME<=N, and the layout= and id= locators.
shot writes NAME.txt and NAME.grid.txt. Every step records a trace line (event, drew, the
invalidation reasons, changed nodes and rows, cost): run --trace DIR and TESSERA_TRACE write it,
and tessera replay runs it again and compares it.

DemoSupport gains demos for Text, stacks, frame and padding, ZStack, Flex, and ForEach, each with
a desktop and a mobile scenario; the stack demo is absorbed into stacks. The corpus is 16
scenarios, every shot also with a grid golden.
…demos

siblings-disjoint looks through ForEach, if, and view groups to the container that places
them, so layers built by a list under a ZStack no longer fail the rule. The grid golden
now keeps an empty chars row for each styled blank row, so it never has more style rows than
chars rows, and a screen with more than 62 distinct styles fails the shot as a harness error
instead of writing a repeated letter. run --pretty prints a bare check's warnings, an empty
path, name, or flag value is a usage error, an empty locator value is a parse error, the
trace version is read before its schema, and a live trace records the terminal's focus
changes in a form replay delivers.

The Flex demo gains a second page (a layoutPriority tier, a child with no flex value,
cross-axis placement), the Stacks demo a Spacer(minLength:) and a Spacer in a VStack, and the
ZStack demo layers from a ForEach and an if. text.md says what the Text demo renders today.
The projection node gains isList, set from the reflected type before it is shortened, so
siblings-disjoint looks through the library's ForEach, if, and view groups and not through an
app's own view that happens to be named ForEach. The README says a container-less multi-view
body is a true positive.
…-4/06-projection

# Conflicts:
#	Examples/Tests/DemoSupportTests/CounterDemoTests.swift
#	Examples/Tests/DemoSupportTests/ScenarioCorpusTests.swift
#	Sources/TesseraHarness/Scenarios/Locator.swift
#	Tests/TesseraHarnessTests/HarnessCLIScenarioTests.swift
#	Tests/TesseraHarnessTests/ScenarioRunnerTests.swift
#	Tests/TesseraHarnessTests/ScenarioTests.swift
#	docs/plans/phase-4-view-layer-and-developer-tooling.md
Express the projection, checks, trace, replay, grid, statement, and demo tests as inline snapshots and expectNoDifference where they check structured state, sort the conformance lists of the new types, conform CostBound to CustomStringConvertible, and replace plan and slice references in comments and the changelog.
A trace file's name mirrors its scenario's path, so the scratch directory's path appears there in a second spelling on Windows; redact that one too.
…-4/06-projection

# Conflicts:
#	.agents/skills/tessera-contributing/SKILL.md
#	.agents/skills/tessera/SKILL.md
#	Sources/TesseraHarness/HarnessCLI+Scenarios.swift
#	Sources/TesseraHarness/HarnessCLI.swift
#	Sources/TesseraHarness/HelpText.swift
#	Tests/TesseraHarnessTests/HarnessCLIScenarioTests.swift
#	Tests/TesseraHarnessTests/HarnessCLITests.swift
…swift-argument-parser

check, replay, run --trace, and launch's TESSERA_TRACE move onto the ArgumentParser
declarations the other commands use, and the last hand-rolled argument parsing and help text
are gone. A usage error exits 2 and says what is accepted where it happened; results,
reports, and exit codes are unchanged.
…-4/06-projection

# Conflicts:
#	.agents/skills/tessera-contributing/SKILL.md
#	CHANGELOG.md
#	Sources/TesseraHarness/HarnessCLI+Scenarios.swift
#	Tests/TesseraHarnessTests/HarnessCLIScenarioTests.swift
#	docs/architecture/developer-tooling.md
… and replay completion

Like --repeat, --trace and --app are declared as arrays so validate() can reject a second
one with the message the command line always gave, and the completion tests send the
request the shipped scripts send for check and for replay's --app.
…-4/06-projection

The merge also makes --trace and --app plain single-value options listed in singleValueOptions,
so a repeated one is refused before ArgumentParser parses, as --repeat is.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant