Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
81 commits
Select commit Hold shift + click to select a range
79da704
docs(plans): settle Phase 4 decisions 2, 5, and 8, add the colour-rep…
robfeldmann Oct 23, 2026
ea513af
feat(terminal): add the package seams the headless harness drives
robfeldmann Oct 23, 2026
46f0d65
docs(plans): record Phase 4 Slice 2 evidence, seam decisions, and dis…
robfeldmann Oct 23, 2026
0bb02e7
refactor(terminal): draw in the caller's isolation
robfeldmann Oct 24, 2026
7ef4b92
Merge remote-tracking branch 'origin/main' into tmp-merge-58
robfeldmann Oct 24, 2026
0539b57
Merge origin/docs/phase-4-decisions into phase-4/02-seams
robfeldmann Oct 24, 2026
05df29c
Merge origin/main into phase-4/02-seams
robfeldmann Oct 24, 2026
7d9eb46
Merge origin/main into phase-4/02-seams
robfeldmann Oct 24, 2026
08174d2
fix(terminal): reject a draw that commits after its session closed
robfeldmann Oct 24, 2026
38cc7fd
feat(harness): add TesseraRuntime with Application and TesseraHarness
robfeldmann Oct 24, 2026
53b7a25
chore(examples): retire the Showcase
robfeldmann Oct 24, 2026
e5de1ab
docs(plans): record Slice 3, the runtime, harness, and Showcase retir…
robfeldmann Oct 24, 2026
e05d6e9
Merge origin/phase-4/02-seams into phase-4/03-harness-runtime
robfeldmann Oct 24, 2026
2c28bb6
fix(harness): qualify the isolation docs, guard import Tessera, drop …
robfeldmann Oct 24, 2026
096cec9
docs(harness): state the isolation rule by compiler mode, not by module
robfeldmann Oct 24, 2026
338bd6f
feat(harness): add the application registry, the main-actor driver, a…
robfeldmann Oct 24, 2026
b2ee4f1
docs(plans): record Slice 4, the registry, viewports, driver, and tes…
robfeldmann Oct 24, 2026
e707aa0
docs(plans): record the Windows leg of Slice 4's CI
robfeldmann Oct 24, 2026
40fea7d
fix(harness): address the Slice 4 review nits
robfeldmann Oct 24, 2026
3faa2f4
feat(harness): add the scenario language, runner, and tessera run
robfeldmann Oct 24, 2026
ef4592b
test(harness): read the README table by scalar lines so a CRLF checko…
robfeldmann Oct 24, 2026
5b4dabf
docs(plans): record Slice 5, the scenario language, runner, tessera r…
robfeldmann Oct 24, 2026
4868e84
fix(harness): address the Slice 5 review findings
robfeldmann Oct 24, 2026
c979401
docs(harness): word the scenario finish rule so a later expect finish…
robfeldmann Oct 24, 2026
6781c57
fix(core): keep a resize's viewport reasons through update and count …
robfeldmann Oct 24, 2026
10f8484
feat(harness): add the projection, locators, checks, trace, and the f…
robfeldmann Oct 24, 2026
1cd10ea
Merge remote-tracking branch 'origin/phase-4/05-scenarios' into phase…
robfeldmann Oct 24, 2026
b2c3161
test(harness): bound the cost of a ForEach step in its scenario
robfeldmann Oct 24, 2026
5b8b4c1
docs(plans): record Slice 6, the projection, rules, cost, trace, and …
robfeldmann Oct 24, 2026
a7260a7
fix(harness): address the critic review of the projection, grid, and …
robfeldmann Oct 24, 2026
6ea1bd5
docs(plans): record the critic review fixes for Slice 6
robfeldmann Oct 24, 2026
a7d0b5c
fix(harness): decide a view list from its module-qualified type
robfeldmann Oct 24, 2026
ad44559
docs(plans): record the module-qualified view list fix for Slice 6
robfeldmann Oct 24, 2026
ee30fd7
Merge remote-tracking branch 'origin/main' into phase-4/03-harness-ru…
robfeldmann Oct 24, 2026
8eb1466
fix(harness): address review feedback on the runtime and harness
robfeldmann Oct 24, 2026
d37e358
Merge remote-tracking branch 'origin/phase-4/03-harness-runtime' into…
robfeldmann Oct 24, 2026
86a1cb5
refactor(harness): apply review conventions to the driver and registry
robfeldmann Oct 24, 2026
a4a0183
Merge remote-tracking branch 'origin/phase-4/04-driver' into phase-4/…
robfeldmann Oct 24, 2026
52d4bbc
refactor(harness): apply review conventions to the scenario runner
robfeldmann Oct 24, 2026
79e6297
Merge remote-tracking branch 'origin/phase-4/05-scenarios' into phase…
robfeldmann Oct 24, 2026
4b96bcc
refactor(harness): apply review conventions to the projection and checks
robfeldmann Oct 24, 2026
3041a7b
refactor(harness): spell the scratch directory with forward slashes
robfeldmann Oct 24, 2026
16fa5f3
Merge remote-tracking branch 'origin/phase-4/05-scenarios' into phase…
robfeldmann Oct 24, 2026
3feb5ce
refactor(harness): redact the trace file spelling of the scratch path
robfeldmann Oct 24, 2026
2c463f5
docs(skills): split the devtools skill into a published tessera skill…
robfeldmann Oct 24, 2026
d2f74d5
Merge remote-tracking branch 'origin/phase-4/04-driver' into phase-4/…
robfeldmann Oct 24, 2026
d230a3e
Merge remote-tracking branch 'origin/phase-4/05-scenarios' into phase…
robfeldmann Oct 24, 2026
8a8ccc1
fix(harness): keep tessera help free of repository paths
robfeldmann Oct 24, 2026
4f55e65
Merge remote-tracking branch 'origin/phase-4/05-scenarios' into phase…
robfeldmann Oct 24, 2026
324f760
feat(harness): declare the tessera command line with swift-argument-p…
robfeldmann Oct 24, 2026
4bc3717
feat(dev): explain dev:launch without a name and complete its app names
robfeldmann Oct 24, 2026
361a6e3
Merge remote-tracking branch 'origin/phase-4/04-driver' into phase-4/…
robfeldmann Oct 24, 2026
762e2b6
refactor(harness): declare run, shot, and tree with swift-argument-pa…
robfeldmann Oct 24, 2026
c02a1ab
feat(dev): explain dev:run, dev:shot, and dev:tree without arguments …
robfeldmann Oct 24, 2026
8704a90
Merge remote-tracking branch 'origin/phase-4/05-scenarios' into phase…
robfeldmann Oct 24, 2026
aa28877
refactor(harness): declare check, replay, and the trace options with …
robfeldmann Oct 24, 2026
c61df24
feat(dev): explain dev:check and dev:replay without arguments and com…
robfeldmann Oct 24, 2026
04ef8a5
fix(dev): build tessera once per dev task and run the binary
robfeldmann Oct 24, 2026
9d002c1
Merge remote-tracking branch 'origin/phase-4/04-driver' into phase-4/…
robfeldmann Oct 24, 2026
a6a414a
fix(dev): run dev:help through the shared quiet build
robfeldmann Oct 24, 2026
dd6ac69
fix(harness): keep help with an extra word a usage error and test com…
robfeldmann Oct 24, 2026
ee42d75
fix(dev): exit 2 from a dev task given no argument unless stdout is a…
robfeldmann Oct 24, 2026
e0366bf
Merge remote-tracking branch 'origin/phase-4/04-driver' into phase-4/…
robfeldmann Oct 24, 2026
1b6efc0
fix(harness): refuse a repeated --repeat again and test shot and tree…
robfeldmann Oct 24, 2026
747322a
fix(dev): exit 2 from dev:run, dev:shot, and dev:tree without argumen…
robfeldmann Oct 24, 2026
ec8ffa3
Merge remote-tracking branch 'origin/phase-4/05-scenarios' into phase…
robfeldmann Oct 24, 2026
1c93465
fix(harness): refuse a repeated --trace or --app again and test check…
robfeldmann Oct 24, 2026
61f80c9
fix(dev): exit 2 from dev:check and dev:replay without arguments off …
robfeldmann Oct 24, 2026
233dc43
fix(harness): refuse a repeated single-value option before ArgumentPa…
robfeldmann Oct 24, 2026
0e4a1e5
Merge remote-tracking branch 'origin/phase-4/05-scenarios' into phase…
robfeldmann Oct 24, 2026
b710b88
Merge remote-tracking branch 'origin/main' into phase-4/04-driver
robfeldmann Oct 24, 2026
a3cbacc
chore: drop files the squash merge of #64 re-added
robfeldmann Oct 24, 2026
dac0a8e
Merge remote-tracking branch 'origin/phase-4/04-driver' into phase-4/…
robfeldmann Oct 24, 2026
6b70445
Merge remote-tracking branch 'origin/phase-4/05-scenarios' into phase…
robfeldmann Oct 24, 2026
3b7a7b4
Merge remote-tracking branch 'origin/main' into phase-4/05-scenarios
robfeldmann Oct 24, 2026
7ed0220
Merge commit '3b7a7b4' into phase-4/06-projection
robfeldmann Oct 25, 2026
7e63504
Merge remote-tracking branch 'origin/main' into phase-4/06-projection
robfeldmann Oct 25, 2026
1f9ef5f
docs(plans): add the library-fixes slice and renumber plan 036
robfeldmann Oct 26, 2026
45160b3
docs(plans): refresh plan 036's next step after #59-#66 merged
robfeldmann Oct 26, 2026
a0783de
docs(plans): close the short-name container match in Slice 7
robfeldmann Oct 26, 2026
28d67fa
docs(plans): record the grid golden's style-key ranges decision
robfeldmann Oct 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 21 additions & 13 deletions .agents/skills/tessera-contributing/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,42 @@
---
name: tessera-contributing
description: Use Tessera's own demos, scenario corpus, and developer tools while working in this repository. Use when running `mise run dev:*`, launching, picturing, or snapshot-testing a demo, adding a demo and its scenario, running `Examples/Scenarios`, or fixing a library defect a demo shows. Read with the `tessera` skill, which it extends.
version: 0.2.0
description: Use Tessera's own demos, scenario corpus, and developer tools while working in this repository. Use when running `mise run dev:*`, launching, picturing, checking, tracing, or snapshot-testing a demo, adding a demo and its scenarios, running `Examples/Scenarios`, or fixing a library defect a demo shows. Read with the `tessera` skill, which it extends.
version: 0.3.0
---

# Contributing to Tessera with the developer tools

Everything in the [`tessera` skill](../tessera/SKILL.md) applies here: the `tessera` commands, the scenario grammar, goldens, the exit codes, and the pty recipe. This skill adds what only a Tessera contributor has: the `mise` tasks, the demos, the scenario corpus, and the demo snapshot tests. [Developer tooling](../../../docs/architecture/developer-tooling.md) is the design behind the tools.
Everything in the [`tessera` skill](../tessera/SKILL.md) applies here: the `tessera` commands, the scenario grammar, goldens, checks, traces, the exit codes, and the pty recipe. This skill adds what only a Tessera contributor has: the `mise` tasks, the demos, the scenario corpus, and the demo snapshot tests. [Developer tooling](../../../docs/architecture/developer-tooling.md) is the design behind the tools.

## Commands

| mise | tessera | Does |
| --- | --- | --- |
| `mise run dev:apps` | `tessera apps` | List the registered demos: name, minimum size, viewports. |
| `mise run dev:launch NAME` | `tessera launch NAME` | Run one demo in this terminal until it quits. |
| `mise run dev:launch`, `dev:shot`, or `dev:tree` | `tessera help COMMAND`, then `tessera apps` | With no name, print the command's help and the demos. At a terminal that is the answer and the task exits 0; in a pipe or a script nothing ran and it exits 2, as `tessera` does. `dev:run` with no path prints its help the same way. |
| `mise run dev:run PATH...` | `tessera run PATH...` | Run scenario files or directories of them, with `--record`, `--repeat N`, and `--timing`. |
| `mise run dev:launch NAME` | `tessera launch NAME` | Run one demo in this terminal until it quits. With `TESSERA_TRACE=FILE` it writes the session's trace. |
| `mise run dev:launch`, `dev:shot`, `dev:tree`, `dev:check`, or `dev:replay` | `tessera help COMMAND`, then `tessera apps` | With no name (or, for `dev:replay`, no file), print the command's help and the demos. At a terminal that is the answer and the task exits 0; in a pipe or a script nothing ran and it exits 2, as `tessera` does. `dev:run` with no path prints its help the same way. |
| `mise run dev:run PATH...` | `tessera run PATH...` | Run scenario files or directories of them, with `--record`, `--repeat N`, `--timing`, and `--trace DIR`. |
| `mise run dev:shot NAME [SIZE]` | `tessera shot NAME [SIZE]` | Print one demo's screen as text. |
| `mise run dev:tree NAME [SIZE]` | `tessera tree NAME [SIZE]` | Print one demo's node tree. |
| `mise run dev:check NAME [SIZE]` | `tessera check NAME [SIZE]` | Run every invariant rule over one demo at a size, with no input. |
| `mise run dev:replay FILE --app NAME` | `tessera replay FILE --app NAME` | Run a recorded trace again against a demo and report what differs. |
| `mise run dev:help [COMMAND]` | `tessera help [COMMAND]` | Print the commands, or one command's usage. |

`mise run dev:*` builds the `tessera` executable once, quietly (`swift build --quiet --package-path Examples --product tessera`), and runs the binary it built, so a build that succeeds prints nothing and one that fails still prints its errors; the binary keeps the task's terminal, and `--json` and `--pretty` go through to it. It is the product of `Examples/Sources/TesseraDev`, whose registry is `Demos.registry()` from `Examples/Sources/DemoSupport`, so the apps it lists are Tessera's demos.

With mise's shell completions enabled, `mise run dev:launch <Tab>` offers the demo names, `dev:shot` and `dev:tree` offer them too and then the sizes, and `dev:run` offers `.scn` files and directories. Completion never builds: `scripts/dev-app-names.sh` asks the `Examples/.build/debug/tessera` that an earlier `dev:*` run built, and offers nothing before there is one. Run `mise run dev:apps` once after a fresh checkout, or after registering a demo. To complete `tessera` itself in your shell, see the `tessera` skill.
With mise's shell completions enabled, `mise run dev:launch <Tab>` offers the demo names, `dev:shot`, `dev:tree`, and `dev:check` offer them too and then the sizes, `dev:replay` offers them after `--app`, and `dev:run` offers `.scn` files and directories. Completion never builds: `scripts/dev-app-names.sh` asks the `Examples/.build/debug/tessera` that an earlier `dev:*` run built, and offers nothing before there is one. Run `mise run dev:apps` once after a fresh checkout, or after registering a demo. To complete `tessera` itself in your shell, see the `tessera` skill.

## The loop

The `tessera` skill's loop, through `mise`: `dev:launch NAME` or `dev:shot NAME mobile` to see the demo; `dev:run Examples/Scenarios/NAME-desktop.scn` to read the report; `dev:tree NAME mobile` and a `dump` statement for the nodes and the projection; `dev:run … --trace DIR` for why a step redrew; `dev:check NAME` for the broken invariant. Step 6 is to fix the library, never the demo, and rerun. `dev:run Examples/Scenarios/NAME-desktop.scn --trace /tmp/traces` writes `/tmp/traces/Examples/Scenarios/NAME-desktop.trace.jsonl`, and a live trace is `TESSERA_TRACE=/tmp/live.jsonl mise run dev:launch NAME`, which `dev:replay /tmp/live.jsonl --app NAME` runs again headless.

## Run a demo

`mise run dev:launch counter` opens the counter in your terminal. Each demo quits on `q`, matched by `isQuitKey` in `Examples/Sources/DemoSupport`. An agent without a terminal uses the pty recipe from the `tessera` skill with `mise run dev:launch counter` in place of `swift run tessera launch counter`; `mise run dev:shot` or a scenario is the cheaper way to read a screen.

## The scenario corpus

`Examples/Scenarios` holds Tessera's scenarios, `NAME.scn` with its goldens in `NAME/` beside it. `mise run dev:run Examples/Scenarios --pretty` runs all of it, as CI does on macOS, Linux, and Windows, and `ScenarioCorpusTests` in `Examples/Tests/DemoSupportTests` runs the same files through `ScenarioRunner`. `Examples/Scenarios/README.md` is the grammar's source; the `tessera` skill carries the part an app author needs, so a change to the grammar updates both. A new `shot` needs one run with `--record`, and the golden it writes is reviewed and committed with the scenario. The corpus is the examples to copy: `counter-basics.scn` drives input, `expect screen`, and a `shot`.
`Examples/Scenarios` holds Tessera's scenarios, `NAME.scn` with its goldens (`NAME.txt` and `NAME.grid.txt` for each `shot`) in `NAME/` beside it. A demo has two scenarios, `NAME-desktop.scn` and `NAME-mobile.scn`, each taking a `shot` at its size. `mise run dev:run Examples/Scenarios --pretty` runs all of it, as CI does on macOS, Linux, and Windows, and `ScenarioCorpusTests` in `Examples/Tests/DemoSupportTests` runs the same files through `ScenarioRunner`. `Examples/Scenarios/README.md` is the grammar's source; the `tessera` skill carries the part an app author needs, so a change to the grammar updates both. A new `shot` needs one run with `--record`, and the goldens it writes are reviewed and committed with the scenario. Assert the numbers the catalog doc gives (its sizing tables and allocation fixtures), not the numbers you saw. The corpus is the examples to copy: `counter-basics.scn` drives input, `expect screen`, and a `shot`.

## Snapshot-test a demo

Expand All @@ -46,19 +52,21 @@ func `the counter counts`() async throws {
}
```

`withHeadlessHost` opens a terminal session over a headless device, steps the demo through the same driver as `launch`, and feeds every frame to one Ghostty virtual terminal, so `host.screenRows` is the screen a terminal would show. `@testable import TesseraHarness` reaches it; it is not API for app authors, who run their scenarios through the public `ScenarioRunner`. The viewport names are the ones the `tessera` skill lists.
`withHeadlessHost` opens a terminal session over a headless device, steps the demo through the same driver as `launch`, and feeds every frame to one Ghostty virtual terminal, so `host.screenRows` is the screen a terminal would show. `host.projection` is the projection and `host.records` the trace. `@testable import TesseraHarness` reaches them; they are not API for app authors, who run their scenarios through the public `ScenarioRunner`. The viewport names are the ones the `tessera` skill lists.

## Add a demo

1. Write a `final class` conforming to `Demo` in `Examples/Sources/DemoSupport`: a kebab-case `name`, a `minimum` size, `viewports`, and `fixtures` (empty when there are none). None has a default.
1. Write a `final class` conforming to `Demo` in `Examples/Sources/DemoSupport`: a kebab-case `name`, a `minimum` size, `viewports`, and `fixtures` (empty when there are none). None has a default. Name the class so it does not shadow a library view (`TextDemo`, not `Text`).
2. Handle the quit key in `apply(_:)` with `isQuitKey`, and report it through `isFinished`.
3. Register it with one line in `Demos.registry()`.
4. Add its scenario to `Examples/Scenarios` and a snapshot test beside the counter's.
4. Add its two scenarios to `Examples/Scenarios` and a snapshot test beside the others.

A demo is interactive and shows every style or variant that can differ. It uses only what the library provides.
A demo is interactive, shows every style or variant that can differ, and reads well at 40 columns: no row is wider than the `mobile` viewport. It uses only what the library provides. A root that returns one view from a `@ViewBuilder` body is dirty after every update (the builder wraps it in a non-equatable tuple view), so a demo's draw count says nothing about whether its state changed; a body that returns its root explicitly does skip clean steps.

## Rules

- When a demo shows a library defect, fix the library and add a regression test. Never bend the demo to hide it.
- When a demo shows a library defect, fix the library and add a regression test. Never bend the demo to hide it, and never loosen a scenario until it passes.
- A failing check rule is the library's finding: fix the library and add a regression test. Some rules fire by contract: a fixed `frame(width:)` wider than its parent offered, a `Flex` child wider than its segment, and `Text`, which does not wrap, wider than the width it is offered. A scenario that shows that on purpose names the rules that must hold, such as `check no-dirty-after-pass siblings-disjoint requirements-aggregate`.
- A name registered twice fails every command that reads the registry. Never rely on the later registration winning.
- `--record` writes goldens from what the demo does now; it is for a new `shot` and a reviewed, intended change, never for a failing run you do not understand.
- The projection, the tree text, the trace, and the `expect cost` counts are free to change between releases; the scenario grammar, the rule ids, the exit codes, the report's fields, and the two golden formats are not.
Loading
Loading