Skip to content

Release: merge release into main - #10

Merged
b-re-w merged 710 commits into
mainfrom
release
Oct 3, 2026
Merged

b-re-w merged 710 commits into
mainfrom
release

Conversation

@github-actions

@github-actions github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Automated sync of develop (e888e7f) into release. Markdown outside README.md and docs/ subfolders is removed by tools/release/sync-release.sh.

b-re-w added 30 commits August 24, 2026 22:04
…ng it is visible

`60d471bc` added `UpcallBootstrap.publishToGlobals()` and converted `ksp-fixtures/artifact`'s four
tests to it. The other 21 -- 19 in `compose`, 2 in `app` -- kept hand-assembling
`ctypes.CFUNCTYPE`s over `UpcallStub`'s raw addresses, which meant they passed whether or not the
public entry point worked. The item was reported closed on the strength of the four.

Each `@BeforeTest` block collapses to `check(UpcallBootstrap.publishToGlobals()) { ... }`.
`UpcallStub` and `ctypes` no longer appear anywhere under `ksp-fixtures/{app,compose}`.

The check that this is worth anything: emptying `publishToGlobals()` to `return true` now fails
all 99 compose tests with "the raw upcall entry points are not bound". Before the sweep the same
tamper changed nothing.

compose 99/0, app 70/0, artifact 36/0 -- unchanged, as a call-site swap should leave them.
`09388516` committed `UpcallBootstrap.desktop.kt` reduced to `actual fun publishToGlobals():
Boolean { return true }` -- the state an agent left it in after proving the sweep was load-bearing,
and never restored. Every consumer of the public bootstrap therefore bound nothing, which is what
`ksp-fixtures:artifact` was reporting as "the raw upcall entry points are not bound" for its 24
failures.

Two things let it through. The agent reported restoring the file and reported a clean
`git diff --stat` for it; neither was true. And the commit was staged with `git add -A` after
`git status --short` printed 22 lines against a reported 21 changed files -- the extra line was
this, and the discrepancy was not questioned.

The verification missed it because the three fixture modules were run in one Gradle invocation:
`compose` and `app` passed, `artifact` passed behind them, and running `artifact` alone fails.
Each fixture module is now run as its own invocation.
…be injected

The rule: nothing in this repository may translate `androidx` into `pythonx`. It owns the language
boundary, and a boundary that knows a UI library by name is not one.

`PythonxAdapter` carried four such facts. `register_package('pythonx.compose', 'androidx.compose')`
and its `pythonx.kotlin` sibling are gone; `_VALUE_CLASS_ALLOWLIST`'s seeded `Dp` is gone; and the
`dp(value)` function -- the only one of the four with no general seam beside it, so that another
library's value class needed a core edit -- is now `value_of(kotlin_type_name, raw)`. The seams
that were already there (`register_package`, `allow_raw_primitive`) are the whole mechanism, and
`install(modules, rawPrimitiveValueClasses)` is how an embedder fills them. Nothing is registered
by default: with no map installed `pythonx.*` resolves nothing, which is the honest answer rather
than a guess.

`kotlin_package_for` had to change with it. A prefix substitution cannot express a rename in the
middle of a name, and the map is full of exactly that -- `pythonx.compose.layout` for
`androidx.compose.foundation.layout`, dropping `foundation.` -- which is why the runtime and the
generated `.pyi` stubs disagreed about module names. An exact match wins; failing that, the longest
mapped ancestor with the tail appended, because `Arrangement` is a submodule under a mapped package
rather than a mapped name of its own and no map would list every nested object. `_Finder` also
recognises intermediate steps (`pythonx.compose.foundation` is nobody's Kotlin package but leads to
one) as containers carrying nothing of their own.

`PythonxTableSourceTest` asserted the *opposite* of this rule -- that the rendered source contains
`register_package('pythonx.compose', 'androidx.compose')` and names `Dp`. It was pinning the binder
knowing a library. Inverted: the seams must exist, and no library name may.

Callers supply the map instead, and each set of fixtures now carries the file a consumer's
`pythonx-map.toml` stands in for. What remains of `androidx` in `commonMain` is one line of javap
output quoted in a KDoc.

Run one module per Gradle invocation: desktop 529/0 skipped=1, wasmJsNode 471 + browser 10,
plugin 175/0, app 70/0, compose 99/0, artifact 36/0, androidNative compiles clean.
…cks rather than regrets

Each step was locally defensible and the aggregate was not what was asked for. Seven ways that
happened, each written as something to check rather than something to feel bad about:

- The specification was `UI.ipynb`, and it was classified as a user file not to touch -- agents
  were told the same. ROADMAP and `docs/` were treated as requirements when they are output.
- "pythonx must be a real package that restructures androidx" was answered with an adapter that
  synthesises `pythonx.*` and puts `__path__ = []` on every module it makes, which is the one
  thing that stops a real package's files from ever loading. Then that package's code was deleted
  as redundant.
- Test counts stood in for progress. `pythonx-compose`'s 71 tests all assert an *absence*; no
  capability was added. Implementations, defect fixes, doc corrections and deletions were reported
  in one column.
- An audit prompt written here fixed its own answer: "the identifier must appear in the other
  repo" cannot detect a reimplementation in another language, so it returned "no transfer" for a
  transfer, and that was relayed as fact.
- Three verifications could not fail: three fixture modules in one Gradle invocation hid an
  ordering dependency; `git add -A` swept a tampered production file in against a `git status`
  line count that did not match the report; and 21 fixtures used raw addresses, so gutting the
  public API they were meant to prove changed nothing.
- Told the binder may not rename `androidx`, an agent was instructed to keep the hardcoded default
  "so existing callers don't break" -- the default being the forbidden thing.
- 159 GB deleted, three-day-old processes killed, a cross-repo API change dispatched: all decided
  and then reported.
…nto one module

Lands with known failures: `ksp-fixtures:compose` 51 of 99 and `ksp-fixtures:artifact` 3 of 36.
`python-multiplatform` desktop 529/0, wasmJs, and `ksp-fixtures:app` 70/0 all pass. The remaining
failures are one unresolved decision, described at the end.

**The rule.** The binder may not export a Kotlin namespace under a different name. `_ROOT`,
`_PACKAGES`, `register_package` and `kotlin_package_for` are gone -- with them the whole notion
that `androidx.compose.foundation.layout` could be reached as `pythonx.compose.layout`. A Kotlin
package is importable under its own name and no other. `install()` no longer takes a module map;
it takes only the set of value classes writable as a raw primitive, which is a fact about the
library that declares them rather than a rename.

This also frees `pythonx.*` for what it is supposed to be: a real distribution whose modules sit on
`sys.path` and whose code restructures the Kotlin surface into something Pythonic. The synthetic
`pythonx.*` modules carried `__path__ = []`, which took those names *and* guaranteed no real
package's files could ever be found under them.

**The merge.** Removing the second namespace exposed that two layers had been building separate
module objects for the same package. `PythonProxySource` puts proxy classes into `sys.modules`
under Kotlin names; this layer served top-level declarations and extensions under `pythonx.*`.
CPython answers from `sys.modules` before consulting any finder, so once both wanted the same name
the first one there won and the other was simply absent.

`_adapt_module` attaches this layer's `__getattr__` to whatever module object exists, creating one
only if none does, and it runs from `_register_table` rather than at the end of the source --
`_PACKAGES_SEEN` is empty until the table is registered, so a module created before that point
would never reach the finder. It re-attaches rather than skipping an already-adapted module: a
module outlives an `install()`, and one adapted by an earlier table closes over dictionaries the
current one has replaced. That was invisible while every such module hung off a single `pythonx`
root that could be dropped wholesale.

**What is left, and why it is not decided here.** Two kinds of name now meet inside one module, and
a name in a module's `__dict__` wins over its `__getattr__`, so a proxy class shadows a declaration
of the same name -- `Column() got an unexpected keyword argument 'content'`. Which of the two owns
a contested name is a design decision: `docs/kotlin-extensions-in-python.md` §4.2 proposed a hybrid
that is class and callable at once, and its metaclass form was measured and rejected there. Picking
a replacement is not a call to make while landing a refactor.
…visible

Sent a documentation-sync task to agy without it. The worktree was
untouched and the whole output was:

    jetski: no output produced — a tool required the "command" permission
    that headless mode cannot prompt for, so it was auto-denied.

The existing note in this section says to check the head of the log
because an argument error exits immediately and looks like success in a
background run. That is not enough here: the wrapper ended with
`echo "AGY_EXIT=$?"` and **that line never appeared**. So the failure is
invisible to the exit code as well as to a quick glance -- a background
run reports completion and simply has no artefacts.

The check that does work is looking at the worktree's own
`git status --short`. Recorded with the invocation, including
`--add-dir`, which is also needed for agy to read the tree at all.
…wo entries this proved wrong

Three corrections, all from measurements taken 2026-09-13.

1. `agy -p "/usage"` reports the actual remaining quota, and nothing else in
   this document said so. It is a slash command, not a subcommand -- `agy usage`
   is rejected outright. The output shows TWO windows, weekly and five-hour,
   per model family. The five-hour window is usually what blocks, and reading
   only the weekly one misleads: measured here, weekly stood at 65% and 33%
   while both five-hour windows were at 0% and nothing would run.

   This matters because the same `Individual quota reached` message was
   misdiagnosed twice in one day -- first as total exhaustion, then as a
   concurrency limit -- and both were wrong. The cost of guessing is real in
   both directions: a live account gets reported dead and hours of throughput
   are thrown away, or rounds keep being dispatched at one that is actually
   blocked. Query first, then judge.

2. The second account is alive. This document said it was dead of an expired
   subscription and told the next reader not to try reviving it. What actually
   appeared was `Failed to authenticate: OAuth session expired and could not be
   refreshed` -- a session, not a subscription -- and it works after logging in
   again. Both wrappers are recorded now, `claude-sub` and `agy-sub`, with the
   point that their quotas are counted separately from the main account's.

3. A fresh worktree has no vendored tree, so its gate is meaningless until one
   is assembled: every suite fails with `torch._C has no _aten_implemented`.
   Two rounds today judged their own work on such a gate. One reported "0 new
   failures" while 119 were already failing and hid its own; the other
   explained the failures away as build ordering. The coordinating session must
   run vendor_torch.sh and install_shim.sh before handing a worktree out --
   not the agent, because the permission classifier blocks it and then the
   rule-following agent stops while the rule-breaking one works around it.
…inating session

1. An agent's completion notification says its turn ended, not that its
   worktree is quiet. On 2026-09-17 a notification was read as "finished" and
   the coordinating session then ran `git stash`, `merge`, `pop`, a rebuild and
   its own gate in that same worktree. The agent's gate was still running. It
   died at `cat: test_shim.py.log: No such file` with **944 ok** on the board --
   a number that reads like a partial pass rather than a corrupted run.

   The cause was shared state: both gates derived one `$stage` from the
   repository path, and the second one's `rm -rf "$suite_logs"` deleted the
   first one's logs mid-run. The tooling is fixed now -- the gate takes a stage
   lock and refuses a second run, and log directories are per-run -- but
   stashing an agent's uncommitted work is dangerous on its own, so the check
   before touching a worktree stays.

2. The stage lock only covers one worktree. Gates in *different* worktrees hold
   different locks, run concurrently, and the CPU contention makes the CoreML
   `MLComputePlan` tests fail in batches. At load 10-12 a branch that changed
   only documentation came out with 4 FAIL, every one of them CoreML, every one
   of them contamination. Another round's vendoring -- a cargo build -- counts
   as load too.

Both entries exist because the same mistake was made more than once in one day,
and each time the failure looked like a regression in the branch under test.
Both were created by a coordinating session in earlier work and left at
`/Volumes/macMini/`, outside any repository, where nothing referenced them and
nothing would have noticed them going stale.

- `tools/agent_rules.md` — the common rules a subagent reads before starting.
- `tools/loop-verify.sh` — the verification gate for the paseo loop. Still live:
  its target worktree `/Volumes/macMini/worktrees/loop` exists.

`loop-verify.sh` keeps its hard-coded worktree path. Relocating the file is
tidying; changing where it looks is a behaviour change and belongs to whoever
next works on that loop.
89 worktrees were living at /Volumes/macMini/worktrees, outside any
repository. They are now at .worktrees/ and gitignored, and their git
registrations are repaired -- `git worktree list` reports 0 prunable where
it previously reported 89, because the metadata still pointed at the old
absolute path.

The repository is on the external volume already, so the constraint that put
them outside in the first place -- do not fill the internal SSD -- is
satisfied without scattering working trees across the disk.
Without a rule saying so, this session put its worktrees (86 of them, 267 GB) in
/Volumes/macMini/worktrees, its agent prompts in /Volumes/macMini/prompts, the RN benchmark and a
wasm build tree beside them, and dozens of logs in /tmp. Nobody managed those locations, they ended
up shared with other projects' sessions, and the user moved all of it back by hand.

§6 makes the repository root the boundary: worktrees under .worktrees/, benchmarks/, experiments/,
tools/, and the session scratch directory for anything temporary. Writing to another repository is
not an exception -- only when told to work there. Re-pointing the shared build caches is called out
separately because it reaches other projects.

Two stale paths in the same file pointed the other way and are corrected: the worktree location and
the build-dir cleanup command both named /Volumes/macMini/worktrees, and the cache line named
/Volumes/macMini/caches, which no longer exists.
This reverts 0308cfd. That commit made the adapter win a contested name in a
Kotlin-named module by deleting the proxy's positional function. The original
design gives each namespace one owner: Kotlin names (androidx.*, kotlin.*) belong
to PythonProxySource, and the keyword/default/value-class surface belongs to
pythonx, which is the pythonx-compose package's job, not the binder's. Letting
the adapter take over Kotlin names rebuilt a Pythonic layer inside the binder.

Also drops the stray empty a.o that commit added at the repository root.
On every push to develop, tools/release/sync-release.sh writes one commit to
release whose tree is develop minus the Markdown that must not reach main: every
root *.md except README.md, and every *.md directly under docs/. A workflow then
opens or updates the release -> main pull request. main-source-guard fails any
PR into main whose head is not release, and protect-main.sh prints (or, with
--apply, applies) the branch protection that makes that check required.
pages.yml publishes docs/guide from main.

Tests: tools/release/test-sync-release.sh (31 checks, scratch repos under .tmp/).
CLAUDE.md becomes AGENTS.md, rewritten in English as the shared core plus this
repository's rules; CLAUDE.md is now only `@AGENTS.md`. The torchnative-only
sections move out (they go to torchnative). tools/agent_rules.md is folded in.

docs/ keeps no Markdown at its root any more: design/, investigations/,
platforms/ and roadmap/ (ROADMAP.md moves there). Every reference to a moved
path is updated, including comments and generator-emitted strings and the tests
that assert them.

New: docs/INTENT.md, docs/SPEC.md, docs/locale/README_ko.md, and docs/guide, a
static en/ko site checked by tools/check_guide.py. README.md and PROJECT.md are
rewritten.
release is not a long-lived branch: CI builds it from each develop push. Each
run now makes exactly one commit on top of the source commit (tree = source
minus the Markdown main must not carry) and force-pushes it with a lease on the
remote sha it read. Stale release branches are ignored and overwritten instead
of becoming the base.

The PR step uses the optional RELEASE_PR_TOKEN secret: a PR opened with
GITHUB_TOKEN triggers no workflows, so main-source-guard would never report.
The 3.15t claim (2026-08-10) was corrected by ROADMAP section 9 two days later:
3.14t passes on desktop with -PpythonFreeThreaded=true, the default stays off,
and Android/iOS have no free-threaded prebuilts. The restructured docs had
copied the stale claim; threading-and-abi.md keeps its reasoning as history.
Also: release is CI-generated from each develop push, not a standing branch.
The wasm experiment demonstrated little the library did not already have; two
things were missing and are now in place:
- tools/wasm/build-cpython.sh builds, verifies and stages the CPython
  Emscripten interpreter inside the repo (.caches/), replacing a script and a
  default path outside it. On develop the wasm suite silently skipped because
  that path no longer existed; buildWasmPython wires the script into Gradle.
- The staged stdlib zip predated its own ABI patches (PYEMSCRIPTEN_PLATFORM_
  VERSION read as None). WasmInterpreterAbiTest pins it; WasmSharedMemory
  GrowthTest pins pointer validity across memory growth, which only the
  experiment proved before.
wasm-experiment/ is removed.

Root: build_log.txt, claude-code-sessions.zip (unrelated transcripts), the
assertion-less test_c_api probes and the already-applied update_*.py migration
scripts are removed. benchmarks/rn-benchmark brings in the React Native
comparison source only (heavy dirs ignored, out-of-repo paths made relative).
tools/loop-verify.sh runs in its own checkout and runs suites separately.
.gitignore drops the wasm-experiment lines and ignores .caches/ and .tmp/.
# Conflicts:
#	python-multiplatform/build.gradle.kts
#	python-multiplatform/src/wasmJsMain/README.md
#	python-multiplatform/src/wasmJsMain/kotlin/python/native/ffi/EmbedAPI.wasmJs.kt
#	python-multiplatform/src/wasmJsMain/kotlin/python/native/ffi/Wasm.kt
The rest of experiments/ becomes code and tests:
- build-cpython.sh gains `wheels` (pydantic_core and typing_extensions by pinned
  sha256, fetched into .caches) and `stock` (the unpatched PEP 776 build as a
  negative control). `build`/`all` now build the native prerequisites a fresh
  checkout lacks, which only worked before on a reused tree.
- WasmCompiledWheelTest imports a real compiled extension (pydantic_core).
- WasmCallTrampolineTest pins the arity CPython's wasm trampoline accepts.
- WasmCrossingOverheadTest and WasmUpcallRouteOverheadTest turn the
  experiment's measurements into tests that assert only orderings.
- WasmKotlinLeavesLinearMemoryAloneTest pins that Kotlin-only work leaves
  CPython's memory untouched.
The wasm CI job fetches the wheels. Benchmark.run returns its ns/op figure.

Not integrated: patch-memory-max and legacy Test B (Kotlin now imports its
memory), and the METH_O upcall variant (a calling-convention change).
Compose 1.6.11 -> 1.11.1 (material3 1.9.0), lifecycle 2.10.0, activity-compose
1.12.4. compileSdk goes to 36 because material3-android, navigationevent and
activity 1.12 require it; targetSdk stays 34.

The generator's $composer/$changed/$default slot assumptions hold on 1.11
(2727 slots pair; SecureTextField's new Char slot is declined by design). One
generator defect surfaced: material3 1.9's 326 entries overflowed the 64 KB
method limit in ArtifactFragment.entries(), so large fragments now split into
ordered helper methods.

Tests follow the API changes: AnchoredDraggableState lost its threshold and
animation arguments, dragAndDropSource lost its PointerInputScope overload,
lightColorScheme takes 48 colours, Icon gains a ColorProducer tint, and the
hidden clickable overload is replaced. sample drops iosX64, which 1.11 does not
publish.
The binder must never re-export a Kotlin namespace under another name. It did,
in three places, and the Compose render tests had been written against that:
- the binding layer renamed members and parameters to snake_case on
  androidx.* and answered there only when it happened to install last;
- the stub generator turned androidx.* into pythonx.* by default;
- the adapter occupied sys.modules["pythonx"] with __path__ = [], so the real
  pythonx-compose package could never load.

Now every function on a Kotlin-named module takes keyword arguments by Kotlin
parameter name and honours Kotlin defaults (KotlinSurface.kt), whichever layer
installed it: one owner per name, decided by the upcall-table epoch at call
time. The adapter installs as python_multiplatform.binding, with no name
conversion. Stubs are emitted under Kotlin names only; the Pythonic stub
product and its manifest belong to pythonx-compose.

Functions carry public metadata for pythonx-compose to re-expose them by rule:
inspect.signature (Kotlin names, KOTLIN_DEFAULT for defaults, no synthetic
slots) and python_multiplatform.describe(fn) (overloads, Kotlin types, value
classes, the trailing composable content parameter).

Tests move from snake_case to Kotlin parameter names. This reverts nothing of
the GIL, thread-state or upcall calling convention.
# Conflicts:
#	ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/ComposableRenderTest.kt
#	ksp-fixtures/compose/src/desktopTest/kotlin/fixture/compose/IconRenderTest.kt
Fix: A KSP result of a proxied Kotlin class is that class's proxy
Chore: Wasm npm lock at gradle/wasm-yarn.lock, no root kotlin-js-store
b-re-w added 4 commits October 3, 2026 19:44
…ng PyObjects (#98)

The traverse proof built PyObject(0xAAAA, borrowed=false); the first GC after it ran
Py_DecRef(0xAAAA) on the Cleaner thread and killed the JVM (hs_err: si_addr 0xaaaa,
Py_XDECREF <- topLevelDecRefAction). The upcall path's ownership is correct: it now uses two
real object() instances (new references, borrowed=false).
Fix: GeneratedTableTest wrapped fabricated addresses in owning PyObjects (JVM crash on GC)
@github-actions
github-actions Bot force-pushed the release branch 2 times, most recently from 3461702 to 93c4cb9 Compare October 3, 2026 11:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant