Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
186 changes: 186 additions & 0 deletions .claude/skills/sync-cwapi3d-stubs/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
---
name: sync-cwapi3d-stubs
description: "Sync this repo's .pyi stubs with the CwAPI3D pybind11 bindings in the cadwork 3d C++ source. Extracts every controller module, bound function and cadwork type from CCwAPI3DPythonController.cpp, diffs it against main, and writes the missing declarations with docstrings derived from the Doxygen contracts on the ICwAPI3D* interface headers. Use when the C++ API has gained functions or types that cwapi3d does not expose yet, or to audit how far the stubs have drifted."
allowed-tools: Read, Grep, Glob, Edit, Write, Bash
model: sonnet
---

# Sync CwAPI3D Python stubs

The `cwapi3d` package is a **stub-only** distribution hand-maintained against
`CCwAPI3DPythonController.cpp`. Nothing keeps the two in sync, so a new `m.def(...)`
in C++ silently never reaches the stubs. This skill closes that gap: it re-derives
the full binding inventory from the C++ source and writes what is missing.

**All C++-side access is read-only.** The only files written are inside this repo.

| Writes | Role |
| ------ | ---- |
| `src/<controller>/__init__.pyi` | the missing `def`s, appended |
| `src/<controller>/` (+ `py.typed`) | a whole module the stubs never had |
| `src/cadwork/<type>.pyi` | a registered `py::class_` / `py::enum_` with no stub |
| `src/cadwork/__init__.pyi` | re-export line + `__all__` entry for a new type |
| `docs/documentation/*.md`, `mkdocs.yml` | docs page + nav entry for a new module/type |
| `pyproject.toml` | `packages` entry for a new module, and `[project].version` |

## Invocation

```
/sync-cwapi3d-stubs [--only <module>] [--report-only] [--on-main]
```

- `--only <module>` — restrict to one module (repeatable). `cadwork` covers the types.
- `--report-only` — stop after step 4; write nothing.
- `--on-main` — write the changes straight onto `main` instead of a working branch
(see step 3 for why that is not the default).

## Step 1 — Preflight

Resolve the config pair. `config.toml` ships with the skill; `config.personal.toml`
is git-ignored and holds only machine paths.

```powershell
python "<skill-path>/scripts/sync_stubs.py" --dry-run --json
```

Exit `2` means a config or parse error — read the message on stderr and stop:

- **`[paths].cadlib_root is not set`** → the operator has no `config.personal.toml`.
Tell them to copy `config.personal.toml.example` next to it and set `cadlib_root`
to their cadwork 3d source root (e.g. `D:\source\cadlib\v_33.0\3d`). Do not guess
the path and do not write the file for them without asking.
- **`binding source not found`** / **`version header not found`** → `cadlib_root`
points somewhere without `CwAPI3D/CCwAPI3DPythonController.cpp` or
`CwAPI3D/include/CwAPI3DVersion.h`. Ask which checkout to use.

The first line of the report echoes the `versionMinor` the run read out of that
header. Sanity-check it against the checkout the operator meant to sync against —
it is what the published version will carry (step 5).

Then check the working tree:

```powershell
git -C "<repo>" status --porcelain
```

If it is dirty, **stop and report**. Never stash, reset, or check out over the
operator's uncommitted work.

## Step 2 — Land on `main`

`main` is the comparison baseline, so the run starts there regardless of the branch
the checkout is sitting on. Report the branch being left.

```powershell
git -C "<repo>" fetch origin
git -C "<repo>" checkout main
git -C "<repo>" pull --ff-only
```

A non-fast-forward `pull` means local `main` has diverged — stop and report; do not
merge or rebase it.

## Step 3 — Cut a working branch

```powershell
git -C "<repo>" switch -c sync/cwapi3d-stubs-<yyyy-mm-dd>
```

The publish workflow uploads to **real PyPI on every push to `main`** that touches
`src/**`, so generated stubs do not land on `main` directly. Skip this step only when
the operator passed `--on-main`.

## Step 4 — Report the gap

Run the dry-run from step 1 again if needed and present the result as a table
grouped by controller: how many declarations are missing, which are whole missing
modules, which cadwork types have no stub. Also relay:

- **blacklisted** — skipped by `[blacklist]` in `config.toml` (per-controller
`get_last_error` / `clear_errors` and similar plumbing). Say how many, not each one.
Name any **whole module** in `[blacklist].modules` explicitly, though: today
`event_controller` is skipped entirely, and a reader should not mistake that for
"already in sync". Enabling one is a config edit, never a hand-written stub.
- **orphans** — stub functions with no C++ binding. These are **reported, never
deleted**; they usually mean a binding was removed upstream or renamed.

Stop here on `--report-only`.

## Step 5 — Apply

```powershell
python "<skill-path>/scripts/sync_stubs.py" --apply
```

The script re-parses every `.pyi` it touched with `ast` before returning; a syntax
error is reported as a `SYNTAX ERROR` warning and exits `2`. Nothing else in this
repo catches a broken stub — there are no tests, and setuptools does not compile
`.pyi`, so treat that exit as a hard failure and report it verbatim.

Re-running is safe: the tool is additive and idempotent, and the version only moves
when something under `src/` actually changed — no `src/**` change means no publish
run, so no new version is needed.

### The version comes from the C++ header

`[project].version` in `pyproject.toml` is never invented and never carried over by
hand. Every run reads the `versionMinor` tag out of `[source].version_header`
(`CwAPI3D/include/CwAPI3DVersion.h`) and derives the new version from it:

| `versionMinor` vs. the version in `pyproject.toml` | New version |
| -------------------------------------------------- | ----------- |
| higher — the stubs have not shipped for this build yet | `<major>.<versionMinor>.0` (`33.322.7` → `33.328.0`) |
| **the same** — another sync against a build already shipped for | patch + 1 (`33.328.0` → `33.328.1`) |

The major is **never** taken from the header. `versionMajor` there is the marketing
year (2026) while the package's major is the cadwork product major (`33`), and PyPI
accepts no version sorting below one already uploaded. Two cases produce a warning
instead of following the header, and both need a human:

- **`versionMinor` is *lower* than the packaged minor** — the run is pointed at an
older cadlib checkout. The higher minor is kept and the patch bumped; fix
`[paths].cadlib_root` if that was not intended.
- **no `versionMinor` found** — the header moved or was reshaped. The run falls back
to a patch bump; the version is a guess until someone confirms it.

## Step 6 — Hand off

Leave the changes **uncommitted** on the working branch. Report:

1. The files written, and the version change with the `versionMinor` it came from
(`33.322.0` → `33.328.0`, from `versionMinor = 328`).
2. Every warning, in full. The ones that need a human are:
- the two version warnings from step 5 — a `versionMinor` below the packaged
minor, or no `versionMinor` at all.
- *"no Doxygen @brief — docstring is a placeholder"* — the C++ side has no
documentation to derive from. The stub is syntactically fine but the prose is
a stand-in.
- *"carries a C++ @par Example that was NOT translated"* — the interface header
has a worked example in C++. It is deliberately not machine-translated; a wrong
example in the published docs is worse than none. Offer to port it by hand.
- *"C++ types with no Python mapping (annotated Any)"* — add an entry to
`[type_map]` or `[doxygen_hint_map]` in `config.toml` and re-run.
- *"no member of C++ enum ... could be resolved"* — the declaring header is
outside `[source].enum_search_dirs`. The enum is **not** written in that case.
3. For a brand-new module, the `TODO` module docstring left in
`src/<module>/__init__.pyi` — the bindings carry no module-level documentation,
so that one paragraph has to be written by a human.

Do not commit, push, or open a PR unless the operator asks.

## Constraints

- **Run this skill on Sonnet.** The work is mechanical — a script run plus a report.
If the session is on another model, switch to Sonnet (`/model sonnet`) before step 1.
- **Additive only.** Never delete or rewrite an existing stub declaration; drift in
the other direction is reported, not resolved.
- **Never hand-edit the stubs to "fix" a generator gap.** Fix `config.toml` and
re-run, so the next sync stays correct.
- **Never hand-write `[project].version`.** It is derived from the C++
`versionMinor` (step 5). If it looks wrong, the checkout or the header is wrong.
- Do not reformat, re-sort, or re-serialise `mkdocs.yml`, `pyproject.toml`, or
`src/cadwork/__init__.pyi`. The script splices single lines and preserves each
file's CRLF endings; a whole-file rewrite buries the real change.
- Do not touch anything under `cadlib_root`. The C++ side is the source of truth.
- Do not put paths, or anything else machine-specific, into the versioned
`config.toml` — that belongs in `config.personal.toml`.
14 changes: 14 additions & 0 deletions .claude/skills/sync-cwapi3d-stubs/config.personal.toml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# cwapi3d stub-sync — PERSONAL config. GIT-IGNORED.
#
# Copy to config.personal.toml (same directory) and edit the paths.
# Machine paths only: no secrets, no shared settings. Anything set here
# deep-merges OVER config.toml and wins on conflict.

[paths]
# Root of the cadwork 3d source tree that contains CwAPI3D/.
# The [source] paths in config.toml are resolved relative to this.
cadlib_root = "D:\\source\\cadlib\\v_33.0\\3d"

# Root of this repo (the cwapi3d stub checkout). Optional — when omitted the
# tool uses the git root discovered from the config file's location.
stub_repo = "D:\\MkDocs\\cwapi3dpython"
110 changes: 110 additions & 0 deletions .claude/skills/sync-cwapi3d-stubs/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Shared stub-sync config (committed). Machine paths go in the git-ignored
# config.personal.toml, which is deep-merged over this file.
# Env overrides: CWSTUBS_CONFIG / CWSTUBS_PERSONAL_CONFIG.

[source]
# Relative to [paths].cadlib_root.
python_controller = "CwAPI3D/CCwAPI3DPythonController.cpp"
interface_include_dir = "CwAPI3D/include"
# `versionMinor` here is the cadwork build number the package version tracks.
version_header = "CwAPI3D/include/CwAPI3DVersion.h"

[target]
# Relative to [paths].stub_repo (defaults to this repo's git root).
src_dir = "src"
docs_dir = "docs/documentation"
mkdocs = "mkdocs.yml"
pyproject = "pyproject.toml"
compare_branch = "main"

[emit]
# New enums are appended to this shared page instead of getting their own.
enums_page = "enums.md"
# Publish runs on every push touching src/**, so a stale version collides. The new
# version comes from [source].version_header: a fresh versionMinor resets the patch,
# the same one bumps it. The major is never taken from the header.
bump_version = true

[blacklist]
# Whole modules: neither functions nor the package are created.
modules = ["event_controller"]

# Per-controller error plumbing; the int32_t out-param does not survive pybind11.
methods = ["get_last_error", "clear_errors"]

# <module>.<python_name> one-offs.
qualified = [
"utility_controller.init_LxSDK",
"utility_controller.redirect_python_output_to_logger",
"utility_controller.print_error",
"utility_controller.print_message",
"utility_controller.print_to_console",
# "utility_controller.get_3d_hwnd",
# Unregistered C++ structs; trampolines return nullptr.
"visualization_controller.save_visibility_state",
"visualization_controller.restore_visibility_state",
"visualization_controller.save_activation_state",
"visualization_controller.restore_activation_state",
# Only bound in auto-attribute mode.
"cadwork.get_auto_attribute_elements",
"cadwork.set_auto_attribute",
]

types = ["PythonLogger", "visibility_state", "activation_state"]

# camelCase duplicates of the snake_case methods on cadwork.element_type.
class_method_patterns = ["^is[A-Z]"]

[type_map]
# C++ (as written in the cwp_* trampoline) -> Python annotation.
# Registered cadwork types and enums are mapped automatically.
"void" = "None"
"bool" = "bool"
"double" = "float"
"float" = "float"
"int" = "int"
"int32_t" = "int"
"uint32_t" = "int"
"int64_t" = "int"
"size_t" = "int"
"uintptr_t" = "int"
"uint64_t" = "ElementId"
"std::string" = "str"
"CwAPI3D::character*" = "str"
"std::vector<uint64_t>" = "list[ElementId]"
"std::vector<std::string>" = "list[str]"
"std::vector<int32_t>" = "list[int]"
"std::vector<int>" = "list[int]"
"std::vector<double>" = "list[float]"
"std::vector<bool>" = "list[bool]"
"std::tuple<int, int>" = "tuple[int, int]"
"py::function" = "Callable[..., None]"
"py::dict" = "dict"
"py::object" = "object"
"CwAPI3D::materialID" = "MaterialId"
"CwAPI3D::elementID" = "ElementId"

[doxygen_hint_map]
# `@param[in] aSetId [@ref multiLayerSetID]` -> annotation, recovering the
# specific type the trampoline flattened to uint64_t. Unlisted hints fall back to
# a registered cadwork type of the same name, then a snake_case src/cadwork/*.pyi.
elementID = "ElementId"
materialID = "MaterialId"
colorID = "ColorId"
endtypeID = "EndtypeId"
axisID = "AxisId"
multiLayerSetID = "MultiLayerSetId"
userAttributeID = "UserAttributeId"
menuIndex = "MenuIndex"
referenceSide = "ReferenceSide"
# A bare integer hint is a count/index -- ids are spelled @ref elementID etc.
uint64_t = "UnsignedInt"
uint32_t = "UnsignedInt"
"ICwAPI3DElementIDList*" = "list[ElementId]"
"ICwAPI3DString*" = "str"

[param_names]
# Fallback parameter names by annotation, when the interface has no @param.
"list[ElementId]" = "element_id_list"
"ElementId" = "element_id"
"MaterialId" = "material_id"
Loading