Repository navigation
codegen: Swift emitter for CmuxNextDaemon request types (sdk-schema.json), with --check; Layout wire tests - #31805
Conversation
The tests name the emitter's contract: one generated file per request and enum under its family folder with an ownership header, the command name and capability from the schema, typed Swift ids and bound types from a bindings file, required fields always encoded and optional ones only when set, a --check that reports changed, missing and stale owned files, and refusals for every schema shape the bindings do not cover. Red here: codegen.swift_app does not exist yet (ModuleNotFoundError).
codegen/swift_app renders the app's daemon request types from the SDK schema (cmux-tui/spec/sdk-schema.json), the spec the Requests/* types mirror by hand today. The schema owns the wire: command names, capabilities, field names, presence, nullability, enum values and result shapes. swift_app/bindings.json owns only what the schema cannot say about Swift: the type name, the typed id an Id field carries, the Swift type a schema object is bound to, init parameter order, how the app reads a result, and docs. A shape the bindings do not cover is refused. Output is one *.generated.swift per request or enum in its Requests/<Family> folder, with a GENERATED header (the inventory counts these as generated; the god-file limit of three top-level types per file holds). --check reports changed, missing and stale owned files; --write updates them and deletes stale ones; files of other generators are left alone. codegen/generate.py runs this step after the SDKs and the page router, so its existing --check (cmux-tui-sdks.yml, cmux-tui-spec.yml) and --write (base autofix, next-batch) cover it. No CI job or step is added. The bindings list no family yet; the next commits convert Requests/Layout. Green here: PYTHONPATH=cmux-tui/bindings python3 -m unittest discover -s cmux-tui/bindings/codegen/tests (91 tests, 27 of them new, red at ba2a971 with ModuleNotFoundError), and generate.py --check (checked swift-app).
LayoutRequestWireTests encodes every Requests/Layout request (except new-row, which carries the shared spawn options and moves with the Creation family) through WireCoding and compares the whole JSON line, decodes the undo-layout and zoom-pane replies, and checks each request's capability gate. The next commit replaces the hand-written types with ones generated from cmux-tui/spec/sdk-schema.json; these tests must stay green across it, which shows the JSON on the wire did not change. The gate check accepts a gate from DaemonCapability.required: the schema names viewport-column-resize-v1 for set-viewport-pane-width and layout-undo-v1 for undo-layout, which the hand-written types left out. Every daemon the app connects to serves both (DaemonCompatibility refuses one that lacks a required capability), so that gate changes nothing. Expected green on this commit (hand-written types); run on the fleet.
….json The Layout request types now come from cmux-tui/spec/sdk-schema.json through codegen/swift_app (bindings.json lists the family): MoveColumn, SetColumnDock, SetColumnWidth (set-viewport-pane-width), SetRowHeights, SetSplitRatio, SwapPane, UndoLayout and ZoomPane requests, and the ColumnMoveDirection and PaneDirection enums. Ten *.generated.swift files replace six hand-written files and part of a seventh (177 lines). What the schema cannot say stays hand-written, as Swift-side API only: SwapTarget and SwapPaneRequest.init(pane:target:), the DockSnapshot init of SetColumnDockRequest, and UndoLayoutRequest.Response (the schema's untagged union of two reply shapes, read as one struct). new-row stays hand-written in NewRowRequest.swift: it carries the shared SpawnOptions and moves with the Creation family. The JSON on the wire is the same: LayoutRequestWireTests (previous commit, green on the hand-written types) must stay green. Two requests gain the schema's capability gate, set-viewport-pane-width (viewport-column-resize-v1) and undo-layout (layout-undo-v1); both are in DaemonCapability.required, which every connected daemon serves, so nothing changes at run time. set-column-dock gains the schema's optional `permanent` field, nil and not encoded. SetRowHeightsRequest.fit is now Bool? (nil omits it); its only caller passes it every time.
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
web and size checks: green Shipped in:
|
|
Found 6 test failures on Blacksmith runners: Failures
|
|
Review (independent subagent): BLOCK at 7f688b9 Blocking
Low (these matter only if committed output stays)
Checked and correct
|
…k-schema.json" This reverts commit 7f688b9. scripts/ci/generated_files_guard.py refuses a pull request that adds a committed generated file (Leo, 2026-10-21), and the converted family added ten Requests/Layout/*.generated.swift files. Lead decision: this PR keeps the emitter, its tests and --check, and the family conversion waits for build-time generation (backlog A4b). The hand-written Layout types are back unchanged; LayoutRequestWireTests stays and pins their JSON for that move.
… honors --schema (red) Review findings on #31805 (comment 6569578652), each as a test: - a family key or Swift type name from bindings.json became a path part unchecked, so `../../X` wrote outside output_root and `Not-Valid` gave invalid Swift; - a field the schema gates by capability was emitted with no word of it, while the module said every uncovered shape is refused; - codegen/generate.py ran the Swift step on the default schema even with --schema. Red here: 10 failures in test_swift_app.py (the new tests); the exit-code test passes already and stays as coverage.
…honors --schema - A family key must be one folder name and a Swift type name a plain, non-keyword identifier; both become output paths, so `../../X` no longer writes outside output_root. - An optional field the schema gates by a capability gets a doc line naming the capability and protocol; a required gated field is refused. The module docstring now lists exactly what is refused. - codegen/generate.py passes --schema through to the Swift step. - The repository-bindings test no longer passes over zero files: it checks output_root exists and the file count, and a second test renders the Layout binding (backlog A4b) against today's schema. Green here: PYTHONPATH=cmux-tui/bindings python3 -m unittest discover -s cmux-tui/bindings/codegen/tests (97 tests; red at e65dc2d with 10 failures), generate.py --check (checked swift-app).
|
Review (independent subagent 2): FIX at ba2ce27 The generator code is correct, and the Swift test is valid. The items below are a test gap, two errors in the PR body and one gap in the A4b proposal. None of them changes what the code does today, because Findings
Previous review (BLOCK at 7f688b9)
What I checked and found correct
|
Review 2 on #31805 (comment 6569885939) found eight refusals that no test made red when removed. Each has a test now: a required field gated by a capability, two fields with one Swift name, two bindings making one type, an unknown protocol, a bound type the schema lacks, a request that is not an object (the schema validator refuses it first), an output_root outside the repository, and a hand-written file at a generated path (never replaced, in --check and --write). Checked by mutation in a scratch copy: removing each of the seven emitter or writer checks turns its test red. 105 tests pass at this commit.
|
Review (independent subagent 3): FIX at ba61fa6 The repair delta is one file: Finding
Review 2 items
Checks I ran at the head
|
Review 3 on #31805 (comment 6570012552): the test used the temp folder as the repository, so `../Requests` was $TMPDIR/Requests, a folder every process shares. With the check removed (the mutation run), --write left fixture files there, and every later run on the VM failed (4 failures). The repository now sits one level down, so an escape lands in the test's own temp folder and tearDown removes it. Checked: the mutation run with a private TMPDIR turns all seven new tests red and leaves nothing behind; 105 tests pass. The leftover /work/tmp/Requests from the first mutation run is removed.
|
Review (independent subagent 3): SHIP at bace1ee My finding at What I ran at the head
Note (not a defect)
The four items of review 2 stay resolved (see my comment at |
![Fix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/fix-with-codesmith-light-3x.png)
The app's daemon request types (
Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Requests/*, withTree/*,Events/*,Conversations/*andCloudConversations/*: about 14,000 lines) are hand-written copies ofcmux-tui/spec/sdk-schema.json. They copy command names, field names, which fields are optional, enum values and capabilities. The seven SDKs generate theirs from that spec, but Swift had no emitter (layer-ownership.md V-30 and L6, cx-3bi.7). This PR adds the Swift emitter, its tests and its--check, and it pins the JSON of the first family to convert. It converts no family yet: the generated files must be made at build time, not committed (see "Next: A4b").What changed
cmux-tui/bindings/codegen/swift_app/renders CmuxNextDaemon request types from the SDK schema IR (codegen/ir.py), the same validated input the seven SDK emitters read.requiredCapability, each field's wire key, required/optional/nullable, enum values and result shapes.swift_app/bindings.jsonholds only what the schema can't say about Swift: the type name, the typed id anIdfield carries (PaneID,SplitID), the Swift type a schema object binds to, init parameter order, how the app reads a result, and doc comments.BindingError. These are: anIdfield with no id type, an unbound object or enum, a union, a non-empty result with no response binding, and a required field gated by a capability.float32isDoublein the app.*.generated.swiftper request or enum inRequests/<Family>/, with a// GENERATED by cmux-tui/bindings/codegen/swift_app ...header. At most three top-level types per file.codegen/generate.pyruns the step after the SDKs and the page router, and passes--schemathrough. Its existing--checkand--writecover the Swift step.bindings.jsonlists no family, so today both have nothing to write. No CI job or step is added.LayoutRequestWireTestspins the whole JSON line of everyRequests/Layoutrequest exceptnew-rowandmove-column(MoveColumnRequestTestsalready pins that one), decodes theundo-layoutandzoom-panereplies, and checks each capability gate, on today's hand-written types. The A4b conversion must keep it green.Why sdk-schema.json and not resource-operations-v2.json
The backlog named
resource-operations-v2.json, but none of the 231 command names the CmuxNextDaemon types send is a v2 operation. 197 aresdk-schema.jsoncommands (protocol 12); the other 34 are in no spec yet. The v2 operation names live elsewhere: about 650 string literals, mostly inConnection/*StateClient*.swift. They are the last step of the plan below, in a later PR (lead decision).Evidence
Commits:
ba2a9718a05emitter tests (red),c8e6c207b83emitter,8578dab0e09Layout wire tests,7f688b978c4Layout conversion,230c2b46f03its revert,e65dc2d2b61review-finding tests (red),ba2ce273b23their fix.PYTHONPATH=cmux-tui/bindings python3 -m unittest discover -s cmux-tui/bindings/codegen/tests:ba2a9718a05(ModuleNotFoundError);c8e6c207b83;e65dc2d2b61(10 failures, for review 1's findings), green atba2ce273b23(97 tests);python3 cmux-tui/bindings/codegen/generate.py --checkat the head: every SDK, the page router andswift-appchecked.python3 scripts/ci/generated_files_guard.py checkat the head: exit 0, no committed generated file added.python3 scripts/cmux-next/prepush-gate.py: no new violations.cmux-ci compile-check --app --package CmuxNext --recipe-version 2):8578dab0e09: jobff898b210e15a7121d18a6e4passed.ba2ce273b23: job9079741b4fca48d7450229cfpassed (CmuxNext, app and cmux-tui receipts ok). Later heads change only Python files undercmux-tui/bindings/codegen.Packages/tree is byte-identical to8578dab0e09(git diff 8578dab0e09 HEAD -- Packagesis empty).8578dab0e09, through proof-step.py: job506ace4f2c4837aa8f08c9e9ranLayoutRequestWireTests(11 tests),MoveColumnRequestTests(2),StandInReferenceTestswithStandInLineTests(3) andPinnedDockMoveRequestTests(2). 18 passed, 0 failed.7f688b978c4, the generated Layout family) was compiled (job8b30900f27e10c9d5cc9dd24) and ran the same 18 tests green (job32b7615ef9569947503e22d5). That shows the emitter's output sends the same JSON as the hand-written types.Observed / Not run
8578dab0e09, which ran them../scripts/lint-ios-package-conventions.sh --namespace-fixdoes not cover CmuxNext (iOS line only).scripts/cmux-next/base-autofix.sh:130lists only changed files undercmux-tui/bindings;scripts/ci/next_batch.pySDK_GENERATEDhas no Swift pattern.Next: A4b, generate the Swift at build time (proposal)
scripts/ci/generated_files_guard.pyrefuses a PR that adds a committed generated file (Leo, 2026-10-21). A first version of this PR committed the generated Layout family and was blocked for that. The family conversions wait for build-time generation. That touches the build path (risk:high) and needs an owner for the hq recipe.Why the generation must run inside SwiftPM. Every place that compiles CmuxNext runs SwiftPM:
cmux-nexttarget, through the package product;swift buildin the fleet recipecmux-next-swift-check(Linux);swift testinscripts/ci/package-test-lane.sh(Mac lane).Page strings are made in an Xcode script phase (
build-agent-pane-web.sh), but SwiftPM builds the package before any app-target phase, andswift buildruns no phase at all. A pre-build step in each script would leave every otherswift buildred with missing types.Design: a SwiftPM build tool plugin.
Packages/macOS/CmuxNext/Plugins/DaemonWireTypes/plugin.swift: aBuildToolPluginon theCmuxNextDaemontarget..prebuildCommandthat runspython3 cmux-tui/bindings/codegen/swift_app/generate.py --write --out <pluginWorkDirectory>.bindings.json, and a prebuild command may produce outputs SwiftPM does not know in advance..swiftfile under a target's folder and ignores.gitignore, so a generated file left inSources/beside the plugin's copy fails with "invalid redeclaration". A4b therefore removesoutput_rootfrombindings.json, andgenerate.pygets--out DIR(required for--write). Its in-tree--writeand--checkgo away, together with the hook incodegen/generate.py;--checkbecomes "the bindings render against the schema", which the codegen unit tests already run.generate.py --writealso refuses any--outinsidePackages/**/Sources. Then no agent, batch workflow or base autofix can write a second copy. This also settles review 1's findings onbase-autofix.shandnext_batch.py: they have no Swift output left to list.Requests/**/*.generated.swiftgoes into.gitignore. Each family then moves in its own PR: its hand-written files go, its binding is added, andLayoutRequestWireTests(or the family's own wire tests) stays green.Package.swift:.plugin(name: "DaemonWireTypes", capability: .buildTool())andplugins: ["DaemonWireTypes"]onCmuxNextDaemon.Builder changes:
cmux-nextbuild passes-skipPackagePluginValidation(scripts/reload.sh, the release and nightly scripts), or trusts the plugin once per checkout. A local Xcode GUI asks once.cmux-next-swift-check(hq,build-fleet/recipes) andpackage-test-lane.shshould need nothing for SwiftPM itself. Not proven: what SwiftPM's macOS plugin sandbox allows a prebuild command to read. It limits writes to the plugin work directory and blocks the network; I have not checked whether it also limits reads, and the inputs (cmux-tui/spec/sdk-schema.json, the codegen) are outside the package. The spike answers this. If reads outside the package are blocked, the plugin declares the inputs as files in the package (a symlinkedPlugins/DaemonWireTypes/inputs), or the recipes pass--disable-sandbox(swift) and-skipPackagePluginValidation(xcodebuild).swift build,swift test,xcodebuildofcmux-next) with an empty family list. It shows the plugin runs and the sandbox lets it read the schema, before any family moves.When Python is missing on a builder.
/usr/bin/python3. The Linux pool image has python3./usr/bin/python3, then/usr/bin/env python3. If both fail, it stops the build with an error that names the missing tool and the fix (xcode-select --install). It never falls back to stale or committed output.cmux-tuibinary, shipped as anartifactbundlebinary target the plugin runs. That is the same pinned-artifact path the app-ffi xcframework uses. This is not needed now.Conversion order after A4b (hand-written lines per family, and how many of its commands sdk-schema.json has; each PR is one family plus at most one emitter feature):
new-rowwith Creation)7f688b978c4)origin+mutation_idasMutationIdentity); selection enumsexpected_generation/expected_revisionnew-rowSpawnOptionsgroupDaemonEventdecoderclient-hello,loopback-open,browser-runtime-*Connection/*StateClient*.swiftand othersChangelog
none