Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
90beed6
fix(qa): a named descriptor that is not there is an error
Sep 9, 2026
1e9adfa
fix(qa): the selector grammar decides what Contrast and dom look at
Sep 9, 2026
7737f72
fix(qa): a control parked outside the window is not on screen
Sep 9, 2026
335db25
feat(qa): a check can require the visibility flag as well as a box
Sep 9, 2026
f8759cb
fix(qa): a setup value must reach the tree before the baseline does
Sep 9, 2026
8d38f04
fix(qa): stop reporting a declared budget as a measured latency
Sep 9, 2026
a485ca7
feat(qa): the open step takes its own arrival budget
Sep 9, 2026
427c73b
fix(qa): checks run in the order their files declare them
Sep 9, 2026
e8ad307
docs(ps-qa): the new check fields, and what dom now accepts
Sep 9, 2026
6624820
release: ps-qa 0.7.0
Sep 9, 2026
a9f8752
feat(qa): a control sized by its text can still be judged enabled
Sep 9, 2026
54b5bc3
docs(ps-qa): the fontless enabled-state flag, and the Present row it …
Sep 9, 2026
cd72b4d
refactor(protocol): the control core moves in from the runtime
Sep 9, 2026
8476768
refactor(protocol): the socket transport moves in
Sep 9, 2026
bc889f6
feat(protocol): drive a document in process, with no socket
Sep 9, 2026
5d70c98
refactor(protocol): one socket client, beside the server
Sep 9, 2026
e6cb27a
refactor: ps-blitz-debug-control moves to ps-blitz
Sep 9, 2026
508ffe2
release: ps-qa 0.7.1
Sep 9, 2026
ed8041e
test(protocol): the runtime's tests come with the code they cover
Sep 9, 2026
210b4d4
feat(protocol): the embedder answers for the process, not the runtime
Sep 9, 2026
42c2b34
fix(qa): verify post-action paint and native pointer gestures
Sep 11, 2026
d4f6d70
fix(qa): settle contrast within the declared outcome window
Sep 11, 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
25 changes: 21 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: dtolnay/rust-toolchain@2eae45db285e407f22119950686d47e1101e071b # 1.88
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
- run: cargo check -p blitz-control-protocol -p ps-blitz-debug-control -p ps-qa
- run: cargo check -p blitz-control-protocol -p ps-qa

core:
name: Protocol, transports, and driver (Linux)
Expand All @@ -37,16 +37,34 @@ jobs:
- name: Format
run: cargo fmt --all --check
- name: Clippy
run: cargo clippy -p blitz-control-protocol -p ps-blitz-debug-control -p ps-qa --all-targets -- -D warnings
run: cargo clippy --workspace --all-targets -- -D warnings
- name: Test
run: cargo test -p blitz-control-protocol -p ps-blitz-debug-control -p ps-qa
run: cargo test --workspace
# The workspace above builds the protocol with no features, which is the
# vocabulary and both transports' framing. The core is behind `engine`
# and `capture` because it needs the renderer, and a job that never
# enables them would leave the semantic tree, the capture surface and the
# in-process transport untested here.
- name: The core, and both transports over it
run: cargo test -p blitz-control-protocol --all-features
- name: Keep the driver renderer-free
run: |
cargo tree -p ps-qa > /tmp/ps-qa-tree.txt
if grep -Eq '(^| )((ps-)?blitz-(dom|html|paint|shell)|tauri|winit|wgpu) v' /tmp/ps-qa-tree.txt; then
echo 'ps-qa pulled a renderer or window runtime into its dependency tree' >&2
exit 1
fi
# The same boundary from the other side. The protocol crate carries the
# core now, so with no features enabled it must still cost a client
# nothing: an accidental non-optional renderer dependency would put the
# engine into every consumer of the vocabulary, including this one.
- name: Keep the vocabulary renderer-free
run: |
cargo tree -p blitz-control-protocol > /tmp/protocol-tree.txt
if grep -Eq '(^| )((ps-)?blitz-(dom|html|paint|shell)|tauri|winit|wgpu) v' /tmp/protocol-tree.txt; then
echo 'the default protocol build pulled in a renderer' >&2
exit 1
fi

package:
name: Package boundaries
Expand All @@ -64,7 +82,6 @@ jobs:
set -euo pipefail

cargo package -p blitz-control-protocol
cargo package -p ps-blitz-debug-control
# The new protocol cannot exist in the registry before this PR is
# merged. Verify the driver tarball against this exact
# protocol source without installing a persistent workspace patch;
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ jobs:
}

publish_if_new blitz-control-protocol
publish_if_new ps-blitz-debug-control
publish_if_new ps-qa
# The host embeds tauri-runtime-blitz, which in turn consumes the
# protocol above. Keep it last so a new protocol and driver can ship
Expand Down
3 changes: 1 addition & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
resolver = "3"
members = [
"crates/blitz-control-protocol",
"crates/ps-blitz-debug-control",
"crates/ps-qa",
]

Expand All @@ -13,7 +12,7 @@ license = "MIT OR Apache-2.0"
repository = "https://github.com/pathscale/ps-observability"

[workspace.dependencies]
blitz-control-protocol = { version = "^0.4", path = "crates/blitz-control-protocol" }
blitz-control-protocol = { version = "^0.5", path = "crates/blitz-control-protocol" }
endpoint-libs = { version = "^3", default-features = false, features = ["agent-control"] }
serde = { version = "^1", features = ["derive"] }
serde_json = "^1"
Expand Down
49 changes: 34 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,19 @@ keeps the protocol, transports, driver, and release documentation together so
the system has one ownership boundary.

```text
application ── tauri-runtime-blitz ── blitz-control-protocol ── ps-qa
chuzz-headless ────────┘

renderer embedder ── ps-blitz-debug-control ── WebDriver-style HTTP client
blitz-control-protocol
┌──────────┴──────────────┐
the vocabulary the core
│ (over blitz-dom)
┌─────────────┴─────────────┐ │
socket transport in-process transport
│ │ │
server client an embedder holding
│ │ the document
│ └── ps-qa (no blitz, no window)
├── tauri-runtime-blitz, for the application window
└── chuzz-headless, for a page with no window
```

The headless host is not here. It used to be, as `qa-inspect-host`, and that
Expand All @@ -27,12 +34,24 @@ same loader and the same engine a tab uses and serves this protocol over the
same socket. `ps-qa` still links no renderer, because that constraint was
always about the socket rather than about which repository the host lives in.

These are two deliberate alternatives, not two stacked transports.
`blitz-control-protocol` is the typed MCP/JSON-RPC inspection plane used by
`tauri-runtime-blitz`, the headless host, and `ps-qa`.
`ps-blitz-debug-control` is a smaller HTTP adapter for embedders that need a
WebDriver-shaped session and command channel; it does not depend on or duplicate
the typed protocol crate.
`blitz-control-protocol` is the whole surface: one vocabulary, one core, and
two transports over that core. The core reads and drives a `blitz-dom`
document and knows nothing about sockets. The socket transport is how a harness
or an agent outside the process reaches a running application; the in-process
transport is how an embedder that already holds the document calls straight in.
Both halves of the socket are here, because a peer can be both: an application
driven by agents while it drives a browser it embeds is a client and a server
at once.

Everything that needs the renderer is behind a feature, so a client pays for
none of it. `cargo tree -p ps-qa` showing no renderer, window runtime or GPU
stack is the check, and CI fails on it.

The loopback WebDriver-shaped adapter that used to be here, `ps-blitz-debug-control`,
lives in [pathscale/ps-blitz](https://github.com/pathscale/ps-blitz) now. It is
what `blitz-script` takes behind its `debug-control` feature, so keeping it here
made ps-blitz depend on this repository while this repository needs `blitz-dom`.
Two repositories pointing at each other have no release order.

`endpoint-libs` owns framing and MCP/JSON-RPC wire primitives. This workspace
owns observability semantics: commands, events, revision rules, session
Expand All @@ -41,9 +60,9 @@ instrumentation hooks but do not own a control server.

## Crates

- `blitz-control-protocol`: transport-neutral observability domain types and
their MCP wire encoding. It deliberately has no renderer dependency.
- `ps-blitz-debug-control`: loopback WebDriver-style transport adapter.
- `blitz-control-protocol`: the vocabulary, the core, and both transports. The
vocabulary and the transports have no renderer dependency; the core is behind
a feature and is the only part that does.
- `ps-qa`: the lightweight driver, audit runner, and report generator.

## Quick start
Expand Down
105 changes: 97 additions & 8 deletions crates/blitz-control-protocol/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "blitz-control-protocol"
description = "Wire types for the Blitz agent-control and diagnostics protocol"
version = "0.4.0"
description = "The Blitz agent-control and diagnostics surface: one vocabulary, one core, two transports"
version = "0.5.0"
edition.workspace = true
rust-version.workspace = true
license.workspace = true
Expand All @@ -13,19 +13,108 @@ keywords = ["observability", "testing", "blitz", "mcp", "ui"]
categories = ["development-tools::testing", "development-tools::debugging"]
publish = true

# Deliberately tiny. This crate exists so a client can speak the protocol
# without building the runtime that serves it: adding anything here that pulls
# in a window, a renderer or a font stack defeats the whole point. The check
# is `cargo tree -p blitz-control-protocol`, which should stay in the dozens
# of crates rather than the hundreds.
# The vocabulary is what this crate is by default, and it stays deliberately
# tiny. A client speaks the protocol without building the renderer that serves
# it: adding anything to the *default* dependencies that pulls in a window, a
# renderer or a font stack defeats the whole point. The check is
# `cargo tree -p blitz-control-protocol`, which should stay in the dozens of
# crates rather than the hundreds, and `cargo tree -p ps-qa`, which CI fails if
# a renderer or a window runtime appears in it.
#
# Everything that needs blitz is behind a feature, and the features are cut on
# that boundary rather than on what each consumer happened to want:
#
# engine the core. Reads and drives a `blitz-dom` document.
# capture the core's expensive half: offscreen paint, layout and
# computed-style snapshots, renderer metrics.
# server the socket transport, listening half. No blitz.
# client the socket transport, connecting half. No blitz.
#
# `server` and `client` carry no engine because a transport does not need one:
# the framing is shared, the document is not. `ps-qa` takes `client` and gets
# serde, endpoint-libs and tokio. A peer that is both, such as AgencyZero
# driving an embedded browser while agents drive it, takes both halves.
[features]
default = []
engine = [
"dep:blitz-dom",
"dep:blitz-script",
"dep:blitz-traits",
"dep:keyboard-types",
"dep:style",
# The role rules live in blitz-dom, which owns them for the AccessKit tree
# as well. Naming the feature rather than relying on it being a default:
# a consumer that turns blitz-dom's defaults off must still get the roles.
"blitz-dom/accessibility",
]
capture = [
"engine",
"dep:anyrender",
"dep:anyrender_vello_cpu",
"dep:base64",
"dep:blitz-paint",
# Frame timings and the deep-profiling session, which blitz-shell owns
# because it is what presents frames. This is the only reason the shell is
# in the graph, so a build that reads the semantic tree and drives input
# gets no window stack at all.
"dep:blitz-shell",
# Where the frame timings and the sampling guard are: `blitz-shell` keeps
# both behind this feature, and it forwards to `blitz-script/debug-control`
# for the script half of the same capture.
"blitz-shell/debug-control",
]
server = ["dep:tokio"]
client = ["dep:eyre", "dep:tokio"]

[dependencies]
endpoint-libs.workspace = true
serde.workspace = true
serde_json.workspace = true
schemars.workspace = true

# The core, and nothing else, reaches for these.
blitz-dom = { package = "ps-blitz-dom", version = "^0.4.8", optional = true }
# Naming stylo directly: the computed-style reader and the visibility check read
# `style::values::computed`, and blitz-dom does not re-export it. The version is
# the one blitz-dom resolves, so cargo unifies them instead of putting two
# incompatible copies of every computed value in the graph.
style = { version = "0.20.0", package = "stylo", optional = true }
# No `system-fonts`. It reaches `fontique/system` and, on Linux, the
# `yeslogic-fontconfig-sys` system library, so enabling it here would decide
# that every consumer needs one installed, including a headless one that reads
# no font catalogue. An application that wants the machine's fonts asks itself.
blitz-script = { package = "ps-blitz-script", version = "^0.4.8", optional = true }
blitz-traits = { package = "ps-blitz-traits", version = "^0.4.8", optional = true }
keyboard-types = { version = "0.7", optional = true }

# Capture. The CPU renderer on purpose: a capture must not need a GPU, a
# surface or a display server, or it stops working in the places it is most
# needed. It draws the same scene through the same `blitz-paint` entry point
# the window uses, so what it returns is the real frame rather than a second
# opinion about it.
anyrender = { package = "ps-anyrender", version = "^0.13.0", optional = true }
anyrender_vello_cpu = { package = "ps-anyrender-vello-cpu", version = "^0.17.0", optional = true }
blitz-paint = { package = "ps-blitz-paint", version = "^0.4.8", optional = true }
blitz-shell = { package = "ps-blitz-shell", version = "^0.4.8", default-features = false, optional = true }
base64 = { version = "0.22", optional = true }

# Both transports. A listener and a connector need the same runtime and the
# same framing, and nothing else.
tokio = { version = "1", features = [
"io-util",
"macros",
"net",
"rt",
"sync",
"time",
], optional = true }
eyre = { version = "0.6", optional = true }

[dev-dependencies]
# The framing round-trip test drives a real duplex pipe rather than asserting
# on a string, because the bug this protocol keeps hitting is at the seam
# between the typed value and the bytes, not inside serde.
tokio = { version = "1", features = ["io-util", "macros", "rt"] }
tokio = { version = "1", features = ["io-util", "macros", "net", "rt", "time"] }

[package.metadata.docs.rs]
all-features = true
Loading
Loading