Skip to content
Merged
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
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,24 @@ breaking changes to the contract were allowed where they bought correctness (D5
`docs/PRODUCTION-PLAN.md`); each is recorded here. From 1.0.0, a change to the class contract is
a major version.

## 1.0.1 — unreleased

### Fixed

- **In Max, the object had no attributes and no messages.** 1.0.0's guard against a Python method
or field named like a message the Max object already answers (plan 8.2) asked Max's
`object_getmethod()` and took anything but null for an answer — but for a name the object does
not have, Max returns `method_false()`, a function, as the SDK documents, so every field and
method of every class was "reserved by the host" and the console said so for each; only audio
still worked. The mock kernel the unit tests run against answers null, which is why they passed.
The guard now recognizes `method_false()`, the unit test's kernel answers as Max does, and the
Mac session (8.8) is what would have caught it before the tag.

### Changed

- **The package has the Tap family's PythonTap icon** in place of min's template icon: the
"ground" version from TapHouse's `brand/`, rendered at 500×500.

## 1.0.0 — 2026-10-01

The first stable release: the class contract as the ReadMe states it, the production plan's
Expand Down
12 changes: 11 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ user's Python class as an audio object. `[tap.python~ name]` loads `python/name.
`class name`, turns its annotated public fields into Max attributes and its public methods into Max
messages, calls its `process()` on the signal, and hot-reloads on save. `ReadMe.md` is the user-facing
contract; **`docs/PRODUCTION-PLAN.md` is the authoritative roadmap** — its settled decisions (D1–D6),
its phases, and the audit findings behind them. Tick its items (with the PR) as they land.
its phases, and the audit findings behind them. Tick its items (with the PR) as they land. Its
Phase 9 is a second object, **`tap.python`**, a Python class as a Max object without audio (what a
method returns is what it outputs): designed in `docs/TAP-PYTHON-PLAN.md` (decisions D7–D11), not
built yet.

## Layout (D6: a host-independent core plus a thin Max wrapper)

Expand Down Expand Up @@ -131,6 +134,13 @@ attaches all the zips + SHA256s to a release — a pre-release for 0.x, a draft
which such a call crashes — so
the watcher is owned by a nobox helper with the SDK's signature (`tap.python_tilde_filewatch.h`).
Check the SDK's calling convention before exposing a Max-called method as a `message<>`.
And read the SDK's return contract before testing a Max call's result: `object_getmethod()`
answers `method_false()`, a function, not null, for a name an object does not have — a null
test reserved every Python name in 1.0.0 (`found_method()` in the object; its glue test's kernel
answers as Max does). The mock kernel is thinner than Max: when a stub decides a behavior, make
the test's stub faithful to the SDK (as `attr_args_offset` and `object_getmethod` are), and give
each fake function a body of its own: MSVC's Release link folds identical functions
(`/OPT:ICF`), which once gave a fake "found" method `method_false()`'s address on Windows only.
- **Say what is true of a class once, what is true of an instance per instance.** Many objects can
share one class file; only the processor whose `load()` ran the file (`load_script` says so)
announces the class — its `Loaded` line and its diagnostics — through `announce()` (plan 6.7).
Expand Down
329 changes: 329 additions & 0 deletions docs/AUDIT-TAP-PYTHON-PLAN.md

Large diffs are not rendered by default.

38 changes: 37 additions & 1 deletion docs/PRODUCTION-PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ note the PR that closed them.
| D4 | Python version policy | **Pin 3.13; upgrade deliberately.** One CPython minor per release; a move (e.g. to 3.14's deferred annotations) is its own PR with tests. No free-threaded or subinterpreter builds until numpy supports them. |
| D5 | Compatibility before 1.0 | **Breaking changes to the class contract are allowed** where they buy correctness (reserved names, file-based loading, signature dispatch). Each is recorded in a `CHANGELOG.md`; the shipped examples are updated in the same PR. |
| D6 | Architecture | **A host-independent core plus a thin Max wrapper**, the family's kernel/wrapper split (TapTools / TapTools-Max). Everything that talks to CPython — interpreter start-up, thread state, module loading, class introspection, value conversion, `process()` binding and reload, exception handling — lives in `core/` (`tap::python`, plain C++20 + CPython, no Max or min-api). The external maps the core's attribute and message descriptions onto `object_addattr`/`object_addmethod` and owns only Max concerns (package paths, the file watcher, atoms). Linux is the first test platform *for the core*: real audio/main threads and sanitizers in CI and in cloud sessions. A plugin front end (CLAP or VST3) is optional later work over the same core (Phase 7), not a test vehicle — the Max glue still needs its own tests. |
| D7–D11 | A second object, `tap.python`, without audio | **Decided 2026-10-02 in `docs/TAP-PYTHON-PLAN.md`** (Phase 9 below): the same core, folder, loader and class contract minus audio; a method's return value is the output, its return hint the outlet count; one inlet; a message runs on the thread it arrives on; the shared Max glue moves to `source/shared/`. |

## Phase 0 — the core split and a test foundation that can fail

Expand Down Expand Up @@ -611,7 +612,15 @@ down rather than discovered again. *This plan was itself audited before being ad
cord connected by `thispatcher`, with those methods in the class) — to run in the Mac session
(8.8), as is Build Collective for `fileusage`. In the mock kernel `object_getmethod()` always
answers null, so the guard's own effect is seen only in Max: the existing
`attributes-and-messages` runtime test is what would show it over-reserving.
`attributes-and-messages` runtime test is what would show it over-reserving. *And it did
over-reserve (found 2026-10-02 in Max, 1.0.0 released with it):* for a name the object does not
answer Max returns `method_false()`, a function — the SDK says so — not null, so the guard
reserved every field and method of every class; the object loaded, audio ran, and nothing else
worked. Fixed in 1.0.1: `found_method()` treats null and `method_false()` alike as "not found",
and the glue test's kernel now answers as Max does (`method_false()` for an unknown name, a
method for the names min registers and for one the test's class defines on purpose), so the
guard's effect is pinned without Max. The lesson for 8.8: a tag with a Mac session outstanding
ships what only Max can show.
- [x] **8.3 Worker mode never hangs Max (A1, A6 — `worker.h`).**
*A1:* `stop()` bounds its join. The worker records its OS thread identifier
(`PyThread_get_thread_ident()`, no GIL needed) as it starts; if the thread has not finished
Expand Down Expand Up @@ -756,6 +765,30 @@ draft until signing exists, as `release.yml` has it.
parameter lists — e.g. a fixed bank of N parameters the Python class declares — and to sharing
one interpreter with other plugins that embed Python in the same host process.

## Phase 9 — `tap.python`, a Python class as a Max object without audio (after 1.0)

The design — decisions D7–D11, the class contract, the output rules, what changes in the core, the
glue and the package — is `docs/TAP-PYTHON-PLAN.md`; keep it current where a PR decides
differently. One PR per item, in this order, each against tests that fail before it:

- [ ] **9.1 The core:** output items and their conversion in `value.h`; `message_info::return_count`,
`outlet_count()`, `call()` returning the output; the `bind_audio` option; a last parameter hinted
`list[…]` or `np.ndarray` taking the remaining atoms (both objects); the `Loaded` line per
binding. `test_output.cpp`. `tap.python~` unchanged and green.
- [ ] **9.2 The shared glue:** the attribute, message, trampoline, file-watcher and package headers
to `source/shared/tap/python_max/`, templated on the host object; the runtime CMake block to
`source/cmake/embedded-python.cmake`; `style.yml` follows. A pure move.
- [ ] **9.3 The object:** `source/projects/tap.python/` — ports with a dumpout, the output mapping,
`anything` forwarding, dynamic outlets on reload, reserved names, `MIN_DESCRIPTION`; the glue
test; the examples (`euclid.py`, `scale.py`, `note_name.py`, `default.py`'s `bang`); CI checks and
packaging over both externals; CHANGELOG 1.1.0 started.
- [ ] **9.4 Documentation:** the ReadMe section and output table, CLAUDE.md, the help patcher, the
reference page.
- [ ] **9.5 Runtime tests:** the `tap.python.*` patchers in `make_patchers.py`; `run.py` aware of
both externals.
- [ ] **9.6 The Mac session:** the whole suite for both objects, the help patcher checked and
re-saved, Max's pages committed, the hand checks, then tag `v1.1.0`.

## Sequencing (one PR each)

1. Phase 0.1–0.2 — the core split and the Linux battery; then 0.3–0.6.
Expand All @@ -771,6 +804,9 @@ draft until signing exists, as `release.yml` has it.
11. Phase 8 — the audit's findings, in the order above: 8.1 (docs and hardening, first, so the
ReadMe stops overclaiming while the fixes land), 8.2, 8.3, 8.4, 8.5, 8.6, 8.7 (#35), then the
Mac session 8.8 — decided on 2026-10-01 to follow 1.0.0 rather than gate it.
12. Phase 9 — `tap.python`: 9.1 (the core, no Max change), 9.2 (the shared glue, a pure move),
9.3 (the object), 9.4 and 9.5 (docs and runtime tests, independent of each other), then the
Mac session 9.6 and `v1.1.0`. After 8.8, or beside it: nothing in it touches what 8.8 checks.

## External prerequisites

Expand Down
Loading
Loading