git:20261008.9f2d5f9 to git:20261013.1c93544

3 added, 3 removed. Audit A to A.

# partcad_ide_client
- The Python half of the socket protocol that connects `partcad` to the PartCAD IDE extension's **PartCAD
- Viewer**. Source: `./src/partcad_ide_client`. Tests: `./tests/partcad_ide_client`. Run all commands below from
+ The Python half of the socket protocol that connects `partcad` to the PartCAD IDE extension's **Digital
+ Thread**. Source: `./src/partcad_ide_client`. Tests: `./tests/partcad_ide_client`. Run all commands below from
the repo root unless noted.
**A package, not a distribution.** Nothing is published under this name: it is one of the packages inside the
single `partcad` wheel, so `pip install partcad` is what puts `import partcad_ide_client` on a machine. See
"The PartCAD IDE viewer client" in [../partcad/AGENTS.md](../partcad/AGENTS.md) for the rationale, including
why giving this directory a `pyproject.toml` of its own would be a mistake.
The other half of the protocol lives in `ide/vscode/src/viewer/protocol.ts`. **A change to the wire
format is a change to both files**, and the frame layout is specified once, in
`src/partcad_ide_client/protocol.py` — that docstring is the normative description.
## Constraints
Two properties of this package are deliberate and easy to break:
- **No dependencies.** It lands in whatever interpreter `partcad` is installed into. Depending on anything (a
CAD library above all — which is exactly what depending on `ocp_vscode` did) risks dragging a second,
differently-provisioned stack into that interpreter, and would put that dependency into the `partcad` wheel
along with it. Standard library only.
- **It does not import `partcad`.** Geometry has already been tessellated into glTF by a PartCAD sandbox
before it reaches here, so there is nothing to import. `partcad` imports *this*, lazily, from
`partcad.viewer`.
**One message is a question rather than a display: `state`**, which `pc ide state` sends. The IDE answers in
the acknowledgement it sends every message, under `state` -- the Explorer's selection, the Inspector's object
- and the Viewer's tabs, filters, selections and a screenshot path; the shape is the comment beside `MSG_STATE`.
+ and the Digital Thread's tabs, filters, selections and a screenshot path; the shape is the comment beside `MSG_STATE`.
An IDE from before it acknowledges it without a state, which is how `client.state()` tells "too old"
(`StateNotSupported`) from "no IDE" (`ViewerNotAvailable`). It is the one asynchronous answer on the IDE's side
-- two webviews are asked and a screenshot taken -- so `PartcadViewerServer.answerState` bounds it and fails
with a sentence rather than leave `pc` blocked.
A node names its geometry (`KEY_GLTF_REF`) rather than carrying it, and the root holds one entry per distinct
shape (`KEY_GEOMETRY`). An assembly that places the same bolt a hundred times therefore sends that bolt once and
names it a hundred times — possible only because a node's placement was never part of its geometry, which is the
same property that lets the tree be composed at all. `is_node()` recognises both spellings: a reader that only
knew `KEY_GLTF` would stop seeing every node of an assembly the day the geometry was shared.
The glTF payload codec (`encode_gltf`/`decode_gltf`) has two other implementations that have to agree with it:
`ocp_serialize.encode_gltf` in the sandbox, and `decodeGltf` in the extension. Neither can import this package,
which is why each carries its own copy; `tests/partcad/unit/test_viewer.py` and the extension's
`viewerProtocol.test.ts` are what catch a drift.
## Setup
All commands run **inside the dev container** — see "Where commands run" in the root
[AGENTS.md](../../AGENTS.md). Dependencies are already installed; re-run `poetry install` only after changing
`pyproject.toml`. Prefix commands with `poetry run`.
## Test and validate changes
```bash
poetry run pytest tests/partcad_ide_client -x -p no:error-for-skips -p no:warnings --dist no # matches CI
poetry run behave # integration tests
```
The client tests stand up a fake IDE on an ephemeral port and point the client at it with `PARTCAD_IDE_PORT`,
so they never collide with a PartCAD IDE the developer actually has open.
To exercise the whole path — a real part, tessellated in a sandbox, over a real socket — run
`dev-tools/fake_viewer.py`, which listens where the IDE listens, acknowledges every frame and prints each show
with its control tree and the geometry and frames attributed to the items that draw them. Give it `--port` and
the same port in `PARTCAD_IDE_PORT` to stay out of the way of a PartCAD IDE that is actually open, and remember
that the process doing the show is usually the **daemon** rather than the `pc` you ran, one per workspace.
`tests/partcad/unit/test_viewer.py` covers the core side with the sandbox stubbed out,
`tests/partcad/unit/test_shape_tree.py` covers what the tree holds, and the sandbox side is covered by the
render tests.
## Lint / format
```bash
poetry run black --check src/partcad_ide_client tests/partcad_ide_client
poetry run flake8 src/partcad_ide_client tests/partcad_ide_client
poetry run isort --check --filter-files src/partcad_ide_client tests/partcad_ide_client
```
`black`, `flake8` and `isort` all gate now — each is a `pre-commit` hook and a `Lint (...)` job in `test.yml` —
and the tree satisfies all three, so a finding from any of them is yours. flake8 only reads its configuration
because `Flake8-pyproject` is installed; without it it checks at 79 columns. See the root
[AGENTS.md](../../AGENTS.md).
## Commit
`pre-commit` hooks (`dev-tools/pre-commit-config.yaml`) run `pytest`, formatting, and lint checks on commit and
are required to pass in CI before a PR can merge.