diff --git a/.gitignore b/.gitignore
index 720e1f8..78d3273 100644
--- a/.gitignore
+++ b/.gitignore
@@ -78,3 +78,12 @@ mutants/
# hypothesis property-test cache (local machine paths; never ship)
.hypothesis/
+
+# Raw VHS captures. The tapes write a full-length recording (docs/job-hero.mp4
+# is 2.3 MB of mostly dead air) and only the trimmed cut is worth committing —
+# scripts/trim_hero.sh turns one into the other. These are untracked AND were
+# unignored, so a `git add -A` swept a raw capture into an unrelated test PR
+# once (#102). Commit the *-post.* cut deliberately instead.
+docs/job-hero.mp4
+docs/*-raw.mp4
+docs/*-raw.gif
diff --git a/AGENTS.md b/AGENTS.md
index 8d8218d..321a17d 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -37,6 +37,8 @@ Logic lives in focused packages; `bambu_cli/bambu.py` is a **thin entrypoint** (
| Module / package | Role |
|------------------|------|
+| `bambu.py` | Thin entrypoint: the `plate` console script + a `main` re-export. Nothing else belongs here |
+| `printer.py` | `BambuPrinter` — the transport facade over `protocols/` (FTPS + MQTT); build it via `get_printer()` / `RuntimeContext.printer()` |
| `cli.py` | `main()` dispatch and the **only** module holding `sys.exit`; re-exports `build_parser` from `cliparse` |
| `cliparse.py` | The argparse tree (`build_parser`, `get_global_parser`, `JsonArgumentParser`). Split from `cli.py` so domain code can build a namespace without importing the entrypoint |
| `paths.py` | Filesystem path helpers (`expand_path`, `display_path`, `path_for_message`, `exception_for_message`) shared by CLI and domain |
@@ -126,17 +128,19 @@ Published on PyPI as `platecli`; the installed command is `plate`.
| **Wheel** | Runtime `bambu_cli` package only — no docs, scripts, or tests |
| **Sdist** | Runtime + tests/scripts + **ship docs**: `README.md`, `AGENTS.md`, `SECURITY.md`, `CHANGELOG.md`, `docs/api.md`, `docs/manual.md`, `docs/troubleshooting.md`, `docs/schemas/*` |
-**Repo-only (never in sdist/wheel):** `CONTRIBUTING.md`, `docs/quality-roadmap.md`, `docs/test-backlog.md`, `docs/mutation-baseline.md`, `docs/live-printer-smoke.md`, and local agent notes (not in repo). Enforced by `MANIFEST.in` + `tests/package_contents_smoke.py`.
+**Repo-only (never in sdist/wheel):** `CONTRIBUTING.md`, `docs/quality-roadmap.md`, `docs/test-backlog.md`, `docs/mutation-baseline.md`, `docs/live-printer-smoke.md`, `docs/releasing.md`, `docs/README.md`, `docs/plans/*`, and local agent notes (not in repo). `MANIFEST.in` ships only the files it lists, and `tests/package_contents_smoke.py` additionally asserts the first six are absent from the sdist (`FORBIDDEN_SDIST_FILES`).
## Quality gates (agents)
| Gate | Command / note |
|------|----------------|
| Default tests | `uv run python -m pytest tests/ -q -m "not live"` — never contacts a printer |
-| Coverage (CI) | `--cov-fail-under=83` (CI run `30632442521`, 2026-07-31: Windows 88.09% / Linux 88.51% / macOS 88.33%; A+ target **92%** — see roadmap) |
+| Coverage (CI) | `--cov-fail-under=83` (CI run `31044588411` on `5b08720`, 2026-08-05: Windows 88.8% / Linux 3.9 89.3%, 3.12 89.2%, 3.14 89.2% / macOS 89.1%; A+ target **92%** — see roadmap) |
| Lint | `uvx ruff check bambu_cli` + `uvx ruff format --check bambu_cli` |
| Types | `uvx mypy -p bambu_cli` |
| Security lint | `uvx bandit -c pyproject.toml -r bambu_cli -ll` |
+| Dependency audit | `pip-audit` over the exported lockfile (blocking high+; CI-only step) |
+| Contracts & layers | `python scripts/gen_schemas.py --check` and `python scripts/check_layers.py` — both blocking in the same lint job |
| Smokes (not pytest) | `syntax`, `cli_help`, `ci_workflow`, `python_compat`, `dependency_resolution`, `release_readiness`, `privacy`, `agent_cli` — see [CONTRIBUTING.md](CONTRIBUTING.md). A green pytest says nothing about these. |
| Mutation baseline | `./scripts/run_mutation_baseline.sh` — nightly / `workflow_dispatch` only; [docs/mutation-baseline.md](docs/mutation-baseline.md) |
| Live printer | Opt-in only: `BAMBU_LIVE=1` + real config + `BAMBU_LIVE_SOURCE`. [docs/live-printer-smoke.md](docs/live-printer-smoke.md). Always ask the user before `--confirm` or `BAMBU_LIVE_PRINT_CONFIRM`. |
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 499cdb9..d2fdeb2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -14,6 +14,10 @@ uv sync --extra test # test deps (pytest etc.) live in the "test" extra
Plain `uv sync` installs runtime deps only — the test commands below then fail
with `No module named pytest`. CI installs the same set via `uv pip install '.[test]'`.
+The `test` extra also pulls in `textual`, so the `plate tui` pilot tests run in the
+default suite. The user-facing install is the separate `[tui]` extra
+(`pip install 'platecli[tui]'`); Textual is never a runtime dependency.
+
## Running tests
```bash
@@ -91,8 +95,17 @@ uvx ruff format --check bambu_cli
uvx mypy -p bambu_cli # full package; check_untyped_defs; no residual excludes
uvx bandit -c pyproject.toml -r bambu_cli -ll
# pip-audit is also blocking in CI (dependency high/critical)
+
+# Also blocking in the same CI job, and cheap to run locally:
+python scripts/check_layers.py # import-layer boundaries
+uv run --python 3.12 --with pydantic python scripts/gen_schemas.py --check
```
+The lint job additionally runs three greps (no test-awareness in production code,
+`sys.exit` only in `cli.py`, no `@mockable`) and a `-m "security or contract"`
+pytest pass. `gen_schemas.py` is pinned to 3.12 because it needs 3.10+ to evaluate
+the contracts' `X | None` annotations — the package itself still runs on 3.9.
+
CI pins these tool versions (see `.github/workflows/ci.yml`); running them unpinned locally is fine.
A green `pytest` does **not** mean lint/types/security gates are green.
@@ -106,10 +119,14 @@ Agent/runtime rules: **[AGENTS.md](AGENTS.md)** (ships in sdist).
Threat model: **[SECURITY.md](SECURITY.md)** (ships in sdist).
JSON contracts: **[docs/api.md](docs/api.md)** + **[docs/schemas/](docs/schemas/)** (ship in sdist).
-As of the 2026-07 codebase audit: overall **solid A− / A**. Main gaps to A+ / 1.0 are
-coverage (~82% vs target 92%), domain→`cli` helper extraction, single-sourced TLS pin
-verification, remaining JSON schemas, and a few camera-hardening items documented in
-SECURITY.md.
+As of 2026-08-05 (0.5.0): overall **solid A− / A**. The 2026-07 audit's four
+architecture/contract gaps have since closed — the domain→`cli` helper extraction
+(B.4), the single-sourced TLS pin verification (B.5), the remaining JSON schemas
+(now *generated* from `bambu_cli/contracts/`, one per `--json` subcommand), and the
+camera bind/pin-fallback hardenings. Main gaps to A+ / 1.0 are now coverage
+(89.2% measured on CI's Linux legs, CI floor **83**, target 92) and the camera
+residuals still listed in SECURITY.md. Do not read "A−/A" as "A+" — see the
+scoreboard for what is actually ticked.
## Code conventions
diff --git a/README.md b/README.md
index 8cb770a..9cf0f67 100644
--- a/README.md
+++ b/README.md
@@ -85,6 +85,12 @@ plate setup
plate doctor # optional: verify the connection end to end
```
+
+
+
+
+
+
Now go from a link on the internet to plastic on the bed:
```bash
@@ -107,19 +113,22 @@ It walks you from a model URL (or local file) to a running print without touchin
### Watch the printer while it works
```bash
-pip install 'platecli[tui]'
+pip install 'platecli[tui]' # or: pipx install 'platecli[tui]'
+ # or: uv tool install 'platecli[tui]'
plate tui # or: plate tui --sim to explore it without a printer
```
+Install the extra the same way you installed `plate` — a `pip install` into your
+shell's Python does not reach a `pipx` / `uv tool` environment. Already installed
+without it? `pipx install --force 'platecli[tui]'` (or `pipx inject platecli textual`).
+
`plate tui` is a live view of your printer: state, temperatures, layer and progress, and the AMS trays, on one screen that keeps updating — plus a job monitor that follows a running print to completion. You can start a print from it too, through the same prepare-and-confirm flow the wizard uses, so you never have to leave the screen.
It is a front-end, not new machinery: it slices and builds the `job` request through the same shared code `plate go` runs, so the two cannot drift. Every safety rule holds — a print only ever starts from the confirm dialog, cancelling keeps the sliced file, and leaving the monitor never stops a print. Textual is an optional extra and never a runtime dependency, so `plate go` keeps working with nothing extra installed on SSH, dumb terminals, and with screen readers.
-
-
-
-
-
+
+
+
## Why platecli
@@ -127,7 +136,7 @@ It is a front-end, not new machinery: it slices and builds the `job` request thr
- **Fully local & private** — talks straight to the printer over your LAN; no Bambu cloud account, ever.
- **Deliberate-action gate** — physical commands refuse without `--confirm` (exit `5`), so a typo, a truncated argument list, or a replayed read-only command can't start a print. It is a gate against *accidents*, not an authorization boundary: `plate` cannot tell your `--confirm` from an agent's, so anything you let run `plate` can pass the flag. Sandbox agents accordingly.
- **AI-agent ready** — every command speaks `--json` with published schemas, plus a `--sim` mode for hardware-free automation.
-- **Watch it live** — `plate status --monitor` follows a print with a live progress bar until it finishes.
+- **Watch it live** — `plate status --monitor` follows a print with a live progress bar until it finishes, or run the full-screen `plate tui` (optional `[tui]` extra) for a dashboard you can also start a print from.
- **Fixes itself findable** — `plate doctor` checks network, FTPS, and MQTT health and tells you exactly what's wrong.
- **Hardened where it counts** — TLS certificate pinning, SSRF-guarded downloads, and size-capped ZIP extraction.
@@ -155,7 +164,7 @@ you can put in a shell script or hand to an agent, use this.
## Built for AI agents
-Every command emits machine-readable `--json` output backed by published [JSON Schemas](https://github.com/DLANSAMA/platecli/tree/main/docs/schemas/), `--sim` provides a full fake printer for development without hardware, and the `--confirm` gate means physical actions never happen by accident. See the [user guide](https://github.com/DLANSAMA/platecli/blob/main/docs/manual.md) and [docs/api.md](https://github.com/DLANSAMA/platecli/blob/main/docs/api.md) for the JSON contracts and stability policy.
+Every command emits machine-readable `--json` output backed by published [JSON Schemas](https://github.com/DLANSAMA/platecli/tree/main/docs/schemas/), `--sim` provides a full fake printer for development without hardware, and the `--confirm` gate means physical actions never happen by accident. Two commands are deliberately human-only — the `go` wizard and the `tui` full-screen UI refuse `--json` and a non-TTY stdin with exit `5`; `plate job --confirm` is the machine path that does the same work. See the [user guide](https://github.com/DLANSAMA/platecli/blob/main/docs/manual.md) and [docs/api.md](https://github.com/DLANSAMA/platecli/blob/main/docs/api.md) for the JSON contracts and stability policy.
## Documentation
diff --git a/bambu_cli/tui/styles.tcss b/bambu_cli/tui/styles.tcss
index c2dcd26..ba2267c 100644
--- a/bambu_cli/tui/styles.tcss
+++ b/bambu_cli/tui/styles.tcss
@@ -58,8 +58,15 @@ AmsPanel {
height: 1fr;
}
+/* 62, not 60: the longest radio label is the AMS-detected material
+ ("PLA — easy, rigid, most models (detected in AMS)") at 49 cells, and a
+ RadioButton adds 4 for its toggle and padding = 53. A 60-wide column leaves
+ the RadioSet 54 cells of content — one to spare — so the moment the form is
+ taller than the terminal and this column grows a scrollbar, content drops to
+ 52 and the closing paren is clipped. Two extra columns absorb the scrollbar
+ instead of silently truncating which material was detected. */
#prepare-inputs {
- width: 60;
+ width: 62;
height: 100%;
padding: 0 2 0 0;
}
diff --git a/docs/job-hero.mp4 b/docs/job-hero.mp4
deleted file mode 100644
index 74ddfa2..0000000
Binary files a/docs/job-hero.mp4 and /dev/null differ
diff --git a/docs/manual.md b/docs/manual.md
index d4dc559..5738915 100644
--- a/docs/manual.md
+++ b/docs/manual.md
@@ -163,7 +163,7 @@ Best-match-first, exactly as auto-detection tries them:
| Platform | Binary | `profiles/BBL` |
|---|---|---|
-| Linux | `orca-slicer` / `OrcaSlicer` / `orcaslicer` on `$PATH`, then `/usr/bin/orca-slicer`, `/usr/local/bin/orca-slicer`, `/opt/OrcaSlicer/orca-slicer`, `/var/lib/flatpak/exports/bin/io.github.softfever.OrcaSlicer`, `~/.local/share/flatpak/exports/bin/io.github.softfever.OrcaSlicer`, `~/Applications/OrcaSlicer.AppImage`, `~/tools/OrcaSlicer.AppImage` | `/usr/share/OrcaSlicer/resources/profiles/BBL`, `/opt/OrcaSlicer/resources/profiles/BBL`, `~/tools/squashfs-root/resources/profiles/BBL` |
+| Linux | `orca-slicer` / `OrcaSlicer` / `orcaslicer` on `$PATH`, then `/usr/bin/orca-slicer`, `/usr/local/bin/orca-slicer`, `/opt/OrcaSlicer/orca-slicer`, the Flatpak exports (`{/var/lib,~/.local/share}/flatpak/exports/bin/` for **both** app ids — current `com.orcaslicer.OrcaSlicer` first, legacy `io.github.softfever.OrcaSlicer` after), `~/Applications/OrcaSlicer.AppImage`, `~/tools/OrcaSlicer.AppImage` | `/usr/share/OrcaSlicer/resources/profiles/BBL`, `/opt/OrcaSlicer/resources/profiles/BBL`, the Flatpak app trees (`{/var/lib,~/.local/share}/flatpak/app//current/active/files/share/OrcaSlicer/resources/profiles/BBL`, both app ids — these are inferred, see the note above), `~/tools/squashfs-root/resources/profiles/BBL` |
| macOS | `/Applications/OrcaSlicer.app/Contents/MacOS/OrcaSlicer`, `~/Applications/OrcaSlicer.app/Contents/MacOS/OrcaSlicer` | the matching `.../Contents/Resources/profiles/BBL` |
| Windows | Each of the three directories is probed for `orca-slicer.exe` first (current installer), then `OrcaSlicer.exe` (older builds): `%PROGRAMFILES%\OrcaSlicer\`, `%LOCALAPPDATA%\Programs\OrcaSlicer\`, `%PROGRAMFILES(X86)%\OrcaSlicer\` | the matching `...\OrcaSlicer\resources\profiles\BBL` |
diff --git a/docs/mutation-baseline.md b/docs/mutation-baseline.md
index 50a1ca5..fc2cddc 100644
--- a/docs/mutation-baseline.md
+++ b/docs/mutation-baseline.md
@@ -1,7 +1,7 @@
# Mutation testing baseline (Phase 3)
-**Baseline date:** 2026-07-09 (scores below; re-run before changing the floor)
-**Doc refresh:** 2026-07-17 (scope/floor unchanged)
+**Baseline date:** 2026-07-09 (original widened baseline; retained below for comparison)
+**Current measurement:** 2026-08-04 — **50.7%**, floor raised 40 → **48** (re-run before changing the floor again)
**Tool:** mutmut 3.6.0
**Reproduce:** `./scripts/run_mutation_baseline.sh`
**CI:** `.github/workflows/mutation.yml` — `workflow_dispatch` + nightly `schedule` only
diff --git a/docs/plans/interactive-mode-plan.md b/docs/plans/interactive-mode-plan.md
index c059668..f36e00c 100644
--- a/docs/plans/interactive-mode-plan.md
+++ b/docs/plans/interactive-mode-plan.md
@@ -1,9 +1,14 @@
# Implementation plan: interactive mode (`plate go`)
-**Status:** Draft for implementation — hand-off document for the implementing agent.
-**Prerequisite:** Ship **0.4.0** first (current `main` is `0.4.0.dev0` with an active
-Unreleased changelog). Interactive mode targets **0.5.0** on a stable base. Do not
-start Phase 1 until the 0.4.0 tag exists (see `docs/releasing.md`).
+**Status:** **Implemented — `plate go` shipped in 0.4.0.** Kept as the design record
+and rationale; it is no longer a to-do list, and the "current version" statements
+below are frozen at the time of writing. For current behaviour read
+[the manual](../manual.md#guided-mode-plate-go), not this file.
+
+*(Historical, as written:)* **Prerequisite:** Ship **0.4.0** first (`main` was
+`0.4.0.dev0` with an active Unreleased changelog). Interactive mode targeted
+**0.5.0** on a stable base; do not start Phase 1 until the 0.4.0 tag exists
+(see `docs/releasing.md`).
## 1. Goal
diff --git a/docs/quality-roadmap.md b/docs/quality-roadmap.md
index 899728b..b1f33b0 100644
--- a/docs/quality-roadmap.md
+++ b/docs/quality-roadmap.md
@@ -15,13 +15,13 @@ Historical baseline (do not read as current), from the audit + full
`pytest --cov=bambu_cli` on 2026-07-08: **368 tests**, **78%** line coverage
(1105 / 4973 stmts missed), **130** `sys.exit` sites in `bambu_cli/`, **7**
`@mockable` sites (def + 6 uses), **1** `BambuError` raise in production.
-The "Baseline" column below is that snapshot. **Current measured (2026-07-31,
-`feat/tui-settings-ux`): 1307 passed / 1308 collected, 88.53% branch coverage
-over 7755 statements (local Linux).** Read off the full CI matrix on `feat/tui`
-at `cc6f78c` (this branch has not reached CI yet)
-(CI runs a clean checkout, so its Linux legs sit a little higher):
-Windows 3.14 **88.09%** (still the binding leg), macOS 3.14 88.33%, Linux 3.9
-88.53% / 3.12 88.50% / 3.14 88.51%.
+The "Baseline" column below is that snapshot. **Current measured (2026-08-05, the
+released `0.5.0` commit `5b08720` on `main`): 1419 passed / 1 deselected, 89.1%
+branch coverage over 8120 statements (local Linux, py3.12).** The full CI matrix
+for that same commit (run `31044588411`; CI runs a clean checkout, so its numbers
+differ slightly from a local run — do not reconcile one to the other):
+Windows 3.14 **88.8%** (still the binding leg), macOS 3.14 89.1%, Linux 3.9
+89.3% / 3.12 89.2% / 3.14 89.2%.
| Area | Baseline | Gate to A | Gate to A+ | Primary evidence |
|------|----------|-----------|------------|------------------|
@@ -40,10 +40,13 @@ Windows 3.14 **88.09%** (still the binding leg), macOS 3.14 88.33%, Linux 3.9
## Scoreboard (current)
-Updated **2026-07-31** (TUI phases 1–5 landed; test/coverage numbers re-measured against CI run `30632442521`). Foundational phases
+Updated **2026-08-05** (`plate tui` shipped in **0.5.0**, PRs #97 + #104; test/coverage
+numbers re-measured against CI run `31044588411` on the release commit). Foundational phases
(0/A/B) are done. Phase C **typing is done** (full package + `check_untyped_defs`);
-coverage floor is **83** (target 92). Phase D schemas largely landed but not
-complete for every command. The camera Docker bind default and camera pin
+coverage floor is **83** (target 92). Phase D's schema work is **complete** — every
+`--json` subcommand has a schema, and the schemas are now *generated* from
+`bambu_cli/contracts/` (`scripts/gen_schemas.py --check` is blocking in CI); what
+remains in D is the 1.0 prep itself, not the contracts. The camera Docker bind default and camera pin
soft-fallback hardenings are now **fixed** (loopback-only default bind,
fail-closed on pin mismatch and on `ssl.SSLError` during the handshake when a
pin is configured); see [SECURITY.md](../SECURITY.md) for the remaining
@@ -61,9 +64,9 @@ security is not yet **A+**.
| Correctness / bugs | **A** | dead flags fixed (global `--json` before subcommand); structured errors; purity greps; version single-sourced |
| Typing | **A** | `uvx mypy -p bambu_cli` full package with `check_untyped_defs = true`; no residual excludes |
| Error model | **A** | `sys.exit` only in `cli.py` (errors.py hits are docstrings); domain uses `abort` / `BambuError` |
-| Tests | **A−** | **1406** non-live tests collected / **1405** passing (2026-08-05; the Textual TUI phases 1-5 plus the structural refactor wave: layer-boundary enforcement, the Printables adapter's malformed-payload containment sweep, and round-trip tests proving each generated schema matches what its contract emits); **89.1%** coverage measured 2026-08-05 on Linux; CI floor **83**; per-module floors not enforced |
+| Tests | **A−** | **1420** non-live tests collected / **1419** passing (2026-08-05, `5b08720`; the Textual TUI phases 1-5 plus the structural refactor wave: layer-boundary enforcement, the Printables adapter's malformed-payload containment sweep, and round-trip tests proving each generated schema matches what its contract emits); **89.1%** branch coverage measured the same day on local Linux (89.2% on CI's Linux legs); CI floor **83**; per-module floors not enforced |
| CI / release | **A−** | single pytest path; purity greps; bandit/audit/mypy blocking; **`--cov-fail-under=83`** (A+ target remains 92) |
-| Docs / governance | **A−** | roadmap + backlog + SECURITY + AGENTS aligned (2026-07-24); prior AGENTS mypy-blocklist / backlog ≥98% claims corrected |
+| Docs / governance | **A−** | roadmap + backlog + SECURITY + AGENTS + CONTRIBUTING re-aligned in the 0.5.0 truth pass (2026-08-05); prior AGENTS mypy-blocklist / backlog ≥98% / "schemas incomplete" claims corrected. Not A+: `tests/test_docs_consistency.py` pins the coverage *floor* and the test count, but nothing pins the cited coverage *percentage*, so that number can still rot silently |
| Product polish | **B+** | quality gates in place; still pre-1.0 Beta (version is single-sourced from `pyproject.toml`); coverage ratchet + camera defaults remain for 1.0 A+ |
**Overall:** **solid A− / A** — error model, typing, security controls, architecture
@@ -71,16 +74,17 @@ security is not yet **A+**.
is coverage toward 92 and documented camera hardenings. Tagging `v1.0.0` still requires §5.
**Coverage floor history:** 79 (honest post-Phase-1 gate) → **81** (2026-07-09) → **83** (2026-07-26; bound by the Windows leg at 83.85%, not Linux's 84.10%).
-Measured branch total is **88.35%** on local Linux (2026-07-31), 88.51% on CI's Linux leg; the floor is set
+Measured branch total is **89.1%** on local Linux (2026-08-05, py3.12), 89.2% on CI's Linux legs; the floor is set
at the multi-OS minimum so the matrix does not flake while still denying points
of silent rot vs the old 79 gate.
-**Ratchet headroom (measured 2026-07-31, run `30632442521`):** every leg now sits
-above 88 — Windows 88.09%, macOS 88.33%, Linux 3.9/3.12/3.14 88.53/88.50/88.51% —
-against a gate of 83, so roughly five points of drift can pass unnoticed. Windows
-remains the binding leg, as it has at every ratchet. Raising the gate to **85** is
-supported by this data with ~3 points of margin; **88** is not, because Windows
-clears it by 0.09pt and would flake the matrix. Ratcheting means moving `ci.yml`,
+**Ratchet headroom (measured 2026-08-05, run `31044588411` on `5b08720`):** every
+leg now sits above 88 — Windows 88.8%, macOS 89.1%, Linux 3.9/3.12/3.14
+89.3/89.2/89.2% — against a gate of 83, so roughly six points of drift can pass
+unnoticed. Windows remains the binding leg, as it has at every ratchet. Raising
+the gate to **85** is supported by this data with ~3.8 points of margin; **88**
+now clears Windows by only 0.8pt, which is a thin margin for a matrix that has to
+stay green on every PR. Ratcheting means moving `ci.yml`,
the citations in this file, and `docs/test-backlog.md` together — `tests/test_docs_consistency.py`
and `tests/ci_workflow_smoke.py` both enforce that.
@@ -131,7 +135,7 @@ A+ for *this* project means all of the following are true simultaneously:
| `slicer/` | ~75% | ≥85% | ≥92% |
| `job/` | ~93% | ≥95% | ≥97% (keep) |
| `commands/` | ~80% | ≥90% | ≥95% |
-| `tui/` (Textual front-end, optional extra) | measured 2026-07-31: **13 of 17 modules at 100%** (`app`/`deps`/`entry`/`services`/`settings_model`/`widgets/*`/`screens/dashboard`/`screens/help`); the other four are `screens/settings` 99.0%, `screens/confirm` 97.2%, `screens/prepare` 95.8%, `screens/monitor` 95.7% — **package minimum 95.7%** | ≥85% | ≥92% |
+| `tui/` (Textual front-end, optional extra) | measured 2026-08-05 (local Linux, after the #104 prepare-screen restructure that added `widgets/summary.py`): **14 of 18 modules at 100%** (`app`/`deps`/`entry`/`services`/`settings_model`/`widgets/*`/`screens/dashboard`/`screens/help`); the other four are `screens/settings` 98.4%, `screens/confirm` 97.3%, `screens/prepare` 96.1%, `screens/monitor` 95.8% — **package minimum 95.8%** | ≥85% | ≥92% |
| JSON contract tests | partial (`test_json_contracts.py`) | every command | every command + schema file |
| Property / adversarial tests | few | netsafety + zip + filenames | + redirect/SSRF fuzz |
| Flakes in CI (30 consecutive green main runs) | unknown | 0 known | 0 |
@@ -636,11 +640,11 @@ If **full A+** is the goal, follow phases 0→A→B→C→D in order; skip ahead
| 0 Trust & truth | **done** | local | 2026-07-08 | allow-private-ips, bare except, version single-source |
| A Testing foundation | **done** | local | 2026-07-08 | TLS suite, markers, transport tests, cov~80% |
| B Error model & seams | **done** | #11 | 2026-07-08 | abort/BambuError; sys.exit entry-only; mockable removed. **B.4** paths/jsonio/argutils extract done (domain no longer imports private cli helpers); **B.5** single pin helper done (PR #89) |
-| C Coverage & typing | **in progress** | #18 | 2026-07-09 | full-package mypy + `check_untyped_defs` done; cov ~84% with CI floor **83** (target 92); per-module floors not enforced |
-| D Contracts & 1.0 | **in progress** | local | — | schemas + contract harness + stability policy; remaining agent `--json` schemas land in follow-up PRs |
+| C Coverage & typing | **in progress** | #18 | — | full-package mypy + `check_untyped_defs` done (#18, 2026-07-09); **C.4** hermetic fake OrcaSlicer done; cov 89.2% on CI's Linux legs with CI floor **83** (target 92); per-module floors not enforced, so C.5 is the open item |
+| D Contracts & 1.0 | **in progress** | #101 | — | schemas + contract harness + stability policy done; **schemas are now generated** from `bambu_cli/contracts/` and every `--json` subcommand has one (#101). Open: support matrix (D.3), optional structured logging (D.5), and the 1.0 prep itself (D.6) |
| E Stretch | not started | | | fuzz job, SBOM, dependabot, scheduled live-printer |
| Doc truth pass | **done** | local | 2026-07-24 | versions de-literalized, prerequisites stated, camera guidance corrected, test/coverage numbers re-measured |
-| TUI (`plate tui`) | **done** (phases 1–5) | `feat/tui` | 2026-07-31 | Textual front-end over the shared `interactive/core.py`: dashboard, prepare, confirm modal (only `confirm=True` path), job monitor, help overlay, and advanced slice settings (the named `slice` flags plus a key/bucket/value override editor routed to `--set` / `--set-filament`). Optional `[tui]` extra; pilot-tested headlessly at 80×24; every `tui/` module ≥95.8% (measured 2026-08-01, most at 100%). **2026-08-01:** the "all settings" browser was cut before merge — it inferred each key's editor control from the values the installed profiles happened to hold, a tuned heuristic over OrcaSlicer's vocabulary that no test could catch drifting; see the cut note in [tui-plan.md](plans/tui-plan.md) if it is revisited |
+| TUI (`plate tui`) | **shipped in 0.5.0** | #97, #104 | 2026-08-05 | Textual front-end over the shared `interactive/core.py`: dashboard, prepare, confirm modal (only `confirm=True` path), job monitor, help overlay, and advanced slice settings (the named `slice` flags plus a key/bucket/value override editor routed to `--set` / `--set-filament`). Optional `[tui]` extra; pilot-tested headlessly at 80×24; every `tui/` module ≥95.8% (measured 2026-08-01, most at 100%). **2026-08-01:** the "all settings" browser was cut before merge — it inferred each key's editor control from the values the installed profiles happened to hold, a tuned heuristic over OrcaSlicer's vocabulary that no test could catch drifting; see the cut note in [tui-plan.md](plans/tui-plan.md) if it is revisited |
> **Verified 2026-07-09** against a clean checkout — the "current scoreboard" above
> was corrected the same day. Coverage floor raised 79→**81** (multi-OS minimum:
@@ -663,6 +667,16 @@ If **full A+** is the goal, follow phases 0→A→B→C→D in order; skip ahead
> **Re-verified 2026-07-26**: 759 non-live tests passing; 83.6% branch coverage over
> 5555 statements on Windows (Windows runs slightly below the Linux figure). Retained
> as the dated record; superseded later the same day by the scoreboard above.
+>
+> **Re-verified 2026-08-05** (0.5.0 docs truth pass, commit `5b08720`): 1419 non-live
+> tests passing locally and on every CI leg but Windows (1414 + 5 skipped there);
+> 89.1% branch coverage over 8120 statements on local Linux, 88.8–89.3% across the
+> CI matrix (run `31044588411`). Floor holds at **83**. Corrected in this pass: the
+> claim that Phase D schemas were "not complete for every command" (they are, and
+> are generated), the pre-merge `feat/tui` framing of the TUI (shipped in 0.5.0),
+> and CONTRIBUTING's "coverage ~82% / helper extraction / TLS pin / remaining
+> schemas" gap list, all four of which had closed. Still **not** A+: coverage is
+> 89, not 92; no per-module floors; camera residuals stand.
### mockable count (burn-down)
diff --git a/docs/releasing.md b/docs/releasing.md
index 29a083d..05cfa36 100644
--- a/docs/releasing.md
+++ b/docs/releasing.md
@@ -22,8 +22,8 @@
— **dropping the `.devN` suffix** main carries between releases — and moves the
`CHANGELOG.md` `Unreleased` entries under the new version heading. Wait for the
required checks, then squash-merge. `release.yml` compares the tag to
- `pyproject.toml` exactly, so `v0.4.0` against a `0.4.0.dev0` version fails the
- build.
+ `pyproject.toml` exactly, so a `vX.Y.Z` tag against an `X.Y.Z.dev0` version
+ fails the build.
2. `git tag vX.Y.Z && git push --tags`
3. `release.yml` runs: CI matrix -> build (sdist+wheel, `twine check`, tag/version match)
-> publish to PyPI via trusted publishing (`pypi` environment, `id-token: write`)
@@ -37,7 +37,7 @@
`pip install platecli`.)
5. If the release touched FTPS, gcode confirm, slice validation, or job upload, run the
[live-printer smoke](live-printer-smoke.md) with a printer attached.
-6. **Bump main to the next dev version** (e.g. `0.5.0.dev0`) in a follow-up PR.
+6. **Bump main to the next dev version** (after 0.5.0, that is `0.6.0.dev0`) in a follow-up PR.
Without this, `main` keeps claiming to be the released version: a contributor or
agent running from a source checkout reports `plate X.Y.Z` while executing
unreleased code, and the bug-report template asks for exactly that string. This
diff --git a/docs/test-backlog.md b/docs/test-backlog.md
index 2a4bfea..ce937f7 100644
--- a/docs/test-backlog.md
+++ b/docs/test-backlog.md
@@ -6,17 +6,17 @@
This file is a short **remaining-gaps** list only. Refresh after each phase or audit.
Do not treat historical “≥98% coverage” claims as current — see the snapshot below.
-## Snapshot (2026-07-31)
+## Snapshot (2026-08-05, release commit `5b08720`)
| Metric | Current (honest) | A+ / 1.0 target |
|--------|------------------|-----------------|
-| Non-live tests collected | **1406** collected / **1405** passing (measured 2026-08-05 on Linux; the Textual TUI phases 1-5 plus the structural refactor wave: layer-boundary enforcement, Printables-adapter containment, and generated-schema contract tests) | ≥550 with zero known flakes ✅ size |
-| Line/branch coverage (CI) | **89.1%** Linux measured 2026-08-05; **88.09%** Windows measured 2026-07-31 (not re-measured since); **floor 83** (Windows is the binding leg) | **≥92%** total; optional module floors |
+| Non-live tests collected | **1420** collected / **1419** passing (measured 2026-08-05 on Linux; the Textual TUI phases 1-5 plus the structural refactor wave: layer-boundary enforcement, Printables-adapter containment, and generated-schema contract tests) | ≥550 with zero known flakes ✅ size |
+| Line/branch coverage (CI) | CI run `31044588411`, 2026-08-05: **88.8%** Windows (the binding leg), 89.1% macOS, 89.2–89.3% Linux; **89.1%** measured the same day on local Linux; **floor 83** | **≥92%** total; optional module floors |
| Typing | Full package mypy + `check_untyped_defs` | keep; optional full `strict` later |
| Error model | `sys.exit` only in `cli.py` | keep |
| `@mockable` / test-awareness | **0** (CI greps) | keep |
-| JSON schemas | **26** files under `docs/schemas/` (`tui.json` added with the TUI) | every `--json` command + monitor goldens |
-| Mutation baseline | Pure safety modules; floor **40%** | hermetic Orca stub landed (C.4); re-run `mutmut` on `slicer/output.py` to raise its row |
+| JSON schemas | **26** files under `docs/schemas/`, **generated** from `bambu_cli/contracts/` (`tui.json` added with the TUI); coverage is derived from `build_parser()`, so a new subcommand cannot ship schema-less | monitor goldens; field-level api.md ↔ schema sync |
+| Mutation baseline | Pure safety modules; score **50.7%** measured 2026-08-04, CI floor raised 40 → **48** | the C.4 re-run happened and **disproved** the prediction: `slicer/output.py` stayed at 21.8% while its line coverage went 79.8% → 92.7%. See [mutation-baseline.md](mutation-baseline.md); raising that row needs a production refactor, not more tests |
| Live printer | Documented opt-in harness | manual pre-release (optional scheduled lab) |
| Product version | pre-1.0 Beta (single-sourced from `pyproject.toml`) | **v1.0.0** when roadmap §5 is complete |
@@ -50,7 +50,7 @@ Tracked in [SECURITY.md](../SECURITY.md) known limitations:
|-----|-------|
| Raise CI floor 83 → 85 → 88 → **92** | Residual: mqtt/ftps pin paths, pool recovery, wizard TTY, Orca process |
| Per-module floors (optional) | mqtt / ftps / netsafety / download / camera |
-| ~~Hermetic fake Orca binary~~ | **Done (C.4).** `tests/fakes/orca_stub` + `tests/test_slice_stub_integration.py` run `cmd_slice` end-to-end through the real slicer subprocess (`_run_orcaslicer`/`_finalize_slice`); `slicer/output.py` line coverage 79.8%→~93%. Mutation re-run on that module still pending. |
+| ~~Hermetic fake Orca binary~~ | **Done (C.4).** `tests/fakes/orca_stub` + `tests/test_slice_stub_integration.py` run `cmd_slice` end-to-end through the real slicer subprocess (`_run_orcaslicer`/`_finalize_slice`); `slicer/output.py` line coverage 79.8%→92.7%. The mutation re-run is **done** (2026-08-04): the module's score did not move (21.8%), so the remaining work there is extracting the pure decision logic out of `_finalize_slice`, not more end-to-end tests. |
### P2 — Contracts & agent surface
diff --git a/docs/tui.gif b/docs/tui.gif
new file mode 100644
index 0000000..71e5aa8
Binary files /dev/null and b/docs/tui.gif differ
diff --git a/docs/tui.tape b/docs/tui.tape
new file mode 100644
index 0000000..b6261d7
--- /dev/null
+++ b/docs/tui.tape
@@ -0,0 +1,74 @@
+# TUI recording for the README. Regenerate from the repo root with:
+# vhs docs/tui.tape
+# Requires: vhs, an installed `plate` in .venv/bin WITH the tui extra
+# (uv pip install -e '.[tui]'), and a reachable printer. Recorded against a
+# REAL printer, mid-print: the dashboard is a live view, and an idle printer
+# records as 0% with a static progress row, which undersells it. Start the
+# print first, wait for RUNNING, then run this tape.
+#
+# `plate tui --sim` also records fine and needs no printer, if you ever want a
+# capture that shows populated AMS trays on a machine that has no AMS.
+#
+# There is no dark/light pair for this one, unlike demo-*/doctor-*: Textual
+# paints its own background, so the terminal theme barely shows through. One
+# capture serves both README color schemes.
+#
+# Window must be at least 100 COLUMNS wide: below that the prepare screen
+# deliberately collapses to a single column (see PrepareScreen._apply_layout),
+# which is not the layout we want to show off. vhs Width/Height are PIXELS,
+# not cells — at FontSize 15 Hack a cell is ~10.5x18.6 px, and Margin(36)+
+# Padding(24) eat 120 px of each axis, so 1100 px is only ~93 columns and
+# records the narrow layout. 1400x700 gives ~122x31 — tall enough that the
+# prepare form shows its Prepare/Settings buttons without scrolling. If you change FontSize
+# or Padding, recompute this and re-check the frames.
+#
+# Before committing a re-record: extract frames with ffmpeg and LOOK at them
+# for IP/serial/home-path leaks — do not trust the tape (media lesson,
+# 2026-07-26; the doctor GIFs once shipped a printer IP and a cert
+# fingerprint). --sim avoids real printer data, but the shell prompt and any
+# paths on screen are still yours.
+
+Output docs/tui.gif
+
+Set Shell "bash"
+Set FontFamily "Hack"
+Set FontSize 15
+Set Width 1400
+Set Height 700
+Set Padding 24
+Set Margin 36
+Set MarginFill "#16295e"
+Set BorderRadius 14
+Set WindowBar Colorful
+Set WindowBarSize 44
+Set Theme { "name": "platecli", "background": "#1c1f26", "foreground": "#e6edf3", "cursor": "#5eead4", "black": "#2a2e38", "red": "#ff5f57", "green": "#28c840", "yellow": "#febc2e", "blue": "#58a6ff", "magenta": "#bc8cff", "cyan": "#5eead4", "white": "#e6edf3", "brightBlack": "#8b949e", "brightRed": "#ff7b72", "brightGreen": "#3fb950", "brightYellow": "#d29922", "brightBlue": "#79c0ff", "brightMagenta": "#d2a8ff", "brightCyan": "#5eead4", "brightWhite": "#f5f5f5" }
+
+Hide
+Type "export PATH=$PWD/.venv/bin:$PATH && clear"
+Enter
+Show
+
+Type "plate tui"
+Sleep 500ms
+Enter
+
+# Dashboard: state, temperatures, layer and progress, AMS trays — refreshing
+# on a timer against the real printer.
+Sleep 8s
+
+# The job monitor: follows the running print toward its terminal state.
+Type "m"
+Sleep 7s
+Escape
+Sleep 1s
+
+# The prepare screen — the two-column form the 0.5.0 restructure is about.
+Type "n"
+Sleep 5s
+
+# Back out and quit cleanly so the last frame is a shell prompt, not a
+# half-torn-down alternate screen buffer.
+Escape
+Sleep 1s
+Type "q"
+Sleep 2s
diff --git a/tests/test_tui_prepare.py b/tests/test_tui_prepare.py
index eeb5761..77fa40b 100644
--- a/tests/test_tui_prepare.py
+++ b/tests/test_tui_prepare.py
@@ -612,6 +612,32 @@ async def test_narrow_terminal_stacks_the_columns(tmp_path):
assert material.outer_size.height == len(("PLA", "PETG", "ABS", "TPU")) + 2 # + border
+async def test_detected_material_label_survives_the_form_scrollbar(tmp_path):
+ """A short terminal must not truncate WHICH material was detected.
+
+ The form is taller than a 25-row terminal, so #prepare-inputs grows a
+ vertical scrollbar — and a Textual scrollbar takes real cells rather than
+ overlaying. With the column at 60 that left the RadioSet 54 cells against a
+ 53-cell button, then 52 once the scrollbar appeared, so the label rendered
+ as "…(detected in AMS" with the closing paren shaved off. Found by
+ recording the TUI and looking at the frames; no assertion had caught it.
+ """
+ _install_ready_settings(tmp_path)
+ app = PlateApp(_args(), _deps(ams_detector=lambda args: "PLA"))
+ async with app.run_test(size=(122, 25)) as pilot:
+ await _settle(pilot)
+ screen = await _open_prepare(pilot)
+
+ inputs = screen.query_one("#prepare-inputs")
+ assert inputs.show_vertical_scrollbar, "test is vacuous without the scrollbar"
+ from rich.text import Text
+
+ button = screen.query_one("#material-pla", RadioButton)
+ # +4 for the toggle glyph and its padding.
+ needed = Text.from_markup(str(button.label)).cell_len + 4
+ assert screen.query_one("#material-set").content_size.width >= needed
+
+
async def test_narrow_terminal_scrolls_the_finished_run_into_view(tmp_path):
"""Stacked, the results start below the fold; the finished run must come up."""
_install_ready_settings(tmp_path)