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 ``` + + + + plate doctor: config, MQTT, and FTPS health checks with TLS-pin verification against a real printer + + 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. - - - - plate doctor: config, MQTT, and FTPS health checks with TLS-pin verification against a real printer - +

+ plate tui: a live printer dashboard — state, temperatures, progress and AMS trays — and the two-column prepare screen it starts prints from +

## 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)