Link Search Menu Expand Document

v4.0.0 Release Plan

The scope, the working method, and the order of execution for the next major release. This is the durable reference; the GitHub issues point here rather than restating it.

Tracking issue: #1144, now closed, which is itself a signal: the programme this plan opened with is finished, and what remains is a different shape. Branch policy is unchanged, per #377: each pass cuts its own branch from main’s tip and merges straight back, with two named exceptions recorded under “Long-lived branches” below.

Rewritten 2026-09-23, when OCCT 8.0.2 was confirmed for 25 September. That repin is now the spine of this release, and the previous version of this document did not mention it at all.

Why this plan exists, and why it isn’t optional

v3.0.0 shipped 2026-08-18 without one. It accumulated two breaking changes (#844, #943) and a tail of Pass 2a/2b/3 dedup work, and got tagged when it was time to cut a release rather than from a drawn scope line. That worked because it was small. It did not work again: the v4.0.0 line has accumulated fifteen breaking changes across 196 merged pull requests, and the next release was forced to be a major long before this paragraph was rewritten.

The complete register, with migrations, is SEMVER.md’s v4.0.0 section, assembled 2026-09-22. Do not maintain a second copy of it here. An earlier version of this document kept a five-row MAJOR table that drifted immediately, and one of its five rows was wrong:

#1138 claimed “14 more SAWireAnalysis functions return Bool?, not Bool”. Its merged diff is nine lines of comment in OCCTBridge_Healing.mm plus a census document. It touches no header and no Swift file, and those fourteen functions return plain Bool today. The change was never made.

That was caught at release assembly, by checking each stated impact against the source rather than against its PR body. Fourteen of fifteen held. Two further breaks (#1103, #1116) had been made by PRs that declared nothing and were re-derived from their diffs. Both facts are recorded in SEMVER.md rather than restated here.

Scope

Families A, B and C are complete. What is live is D, E and F.

E. OCCT 8.0.2 (the spine of this release)

Added 2026-09-23. Upstream lands 8.0.2 on 25 September. The pinned kernel moves from V8_0_1 to V8_0_2, which forces a repatch, a rebuild, a new kernel pre-release, a repin and a re-verification pass.

Workstream Issue State
Survey the 29 carried patches against the 8.0.2 tree to file Runs first, before any build
Repatch, rebuild, publish v4.0.0-kernel.2, repin to file Depends on the survey
New and deprecated 8.0.2 API, wrap or record to file A removal we wrapped is a v4.0.0 break
Re-validate the kernel-parity evidence (see F) to file The records are measured against 8.0.1

This is not a recompile, and the survey is the real work. Three things make the repatch non-mechanical, all of them recorded here because each has already cost this project once:

  • Some patches will be absorbed upstream and must be retired, not re-applied. Patch 0032 shipped four days after upstream fixed the same globals better, and was retired unshipped (#1371, PR #1472). CLAUDE.md’s “check upstream’s own recent activity” rule exists because of it.
  • The data-exchange thread-safety series is the exposed one. 0036 through 0041 (#1403) touch exactly the mutable-static code maintainer dpasukhi has been systematically refactoring. If 8.0.2 absorbed part of that series, a patch either fails to apply or applies on top of a different fix. 0031 already carries a standing note to retarget rather than drop it if OCCT#1076 merges.
  • “Applies cleanly” is not “compiles”, and patches are authored against the patched tree, not vanilla. That rule was written down after a patch cut from upstream master smuggled in master-versus-pin differences that no git apply could have reported.

Ordering, fixed here rather than decided in the moment. The survey produces a per-patch verdict (carry, retarget, retire) before build-occt.sh runs once. A rebuild that starts before the survey turns a patch conflict into a compile error several hundred files deep, and the two are not distinguishable from the log.

Versioning. 8.0.1 to 8.0.2 is an OCCT patch, so SEMVER.md Rule 1 (an OCCT major bump) does not fire and this does not force a new major line. It lands inside v4.0.0. What would be a v4.0.0 break is a deprecation upstream that removes something this package wraps, which is what the third row above is for. Migration notes go in occt-upgrades.md, per that file’s own role.

D. WebAssembly build target

Kept as its own family rather than a row in C: it is neither small nor adjacent to the epics above, and unlike everything else here it can return “no”.

Work moved to the long-lived branch wasm/1689-wasip1 after the first attempt (PR #2076) was closed. That PR did Path B implementation without the Phase 0 decision ever being recorded, which is the thing the phased plan exists to prevent, and #2175 says so directly.

Phase 0 step Issue State
Correct the patches-wasi registry and guard-sites doc #2166 Open
Gate preprocessor balance across the patched tree #2167 On branch
Gate that patches-wasi is cut from the patched tree #2168 On branch
Pin the wasm toolchain, prove a module builds and runs #2169 On branch
Threading shim for the six missing std names #2170 On branch
Measure what C++ exceptions do on wasm32-wasip1 #2171 On branch
Compile and archive TKernel alone #2172 On branch
wasi-osd-chronometer.patch cannot compile #2179 On branch
Close the remaining OSD platform gaps #2173 Open
Produce libOCCT-wasm.a, record its size #2174 Open
Link and run box, fuse and STEP export: the go/no-go #2175 Open

Every issue above still reads OPEN on GitHub because the closing keywords fire on merge to main, and this work lives on a branch. Seven have landed there. The branch is current with main.

#1689 is no longer milestoned to a beta. It was moved off v4.0.0-beta.3 on 2026-09-22 so a beta could cut while Phase 0 was still running. Re-milestone it when #2175 reports.

Three things Phase 0 still has to answer, unchanged from wasm-feasibility.md:

  • The file-path problem, which is the largest remaining API question. The STEP, IGES, STL and glTF bridge functions take file paths, and a WASI module has no filesystem unless one is preopened. Whether the export writes to a preopen or the API needs a bytes-in/bytes-out form is what #2175’s third function tests and what Phase 3 is about.
  • The size budget. #1689 asks for 5 MB gzipped against a prior-art figure of about 4.5 MB brotli for OCCT alone, before Swift and Foundation. #2174 measures it. A realistic budget is an output of Phase 0, not an input.
  • wasm32-unknown-unknown, which #1689 requests, is neither of the two real targets. The branch name records the resolution: wasm32-unknown-wasip1.

Phases 1 to 5 are not in v4.0.0. wasm-feasibility.md costs them at “weeks-to-months and highly path-dependent” and calls this a porting project rather than a recompile. Phase 0 reporting inside the v4 window is plausible; the port is not.

F. Retroactive test validity (#766)

Moved out of “explicitly deferred” on 2026-09-23. The previous version of this document deferred #766 on the grounds that further segmentation “depends on the spike” (#767). That was correct when written and is no longer the state: the work has been running on the long-lived branch v5.0.0-766-execution and has produced a substantial evidence base.

It contains no test code. Measured rather than assumed: 68 files and about 62,000 insertions, and zero changes under Sources/ or Tests/. What it contains is injection records, a test inventory, per-domain validity matrices, kernel-parity comparisons, five scripts and a branch-scoped workflow.

That shape is why it can merge early in slices while accepting that not all 272 execution issues will land. There is no suite risk, because there is no suite change. check-test-validity.py is already on main and is not in gate-scripts; it runs only in the branch’s own workflow.

Merge it before the 8.0.2 repin. The kernel-parity records are captured numeric outputs measured against 8.0.1 plus the 29 carried patches:

"bridge_output": { "type": "intersection_count", "data": { "count": 2 } },
"kernel_output": { "type": "intersection_count", "data": { "count": 2 } },
"status": "PASS"

Landing them first means the 8.0.2 integration can re-run them as its own regression signal, which is the most useful thing a version bump can be handed. Landing them after means rebasing across a kernel change with evidence in them that was measured against the old one.

Two things to decide at merge time rather than discover: the branch carries .github/workflows/766-execution.yml, whose branches: filter names the v5 branch and which would therefore land on main dormant, and it adds a Tooling section to prove-the-test-fails.md that should be read as a policy change rather than as evidence.

Banked: A, B and C, all complete

Kept as a record, not as work. Nothing in these three families is outstanding.

Family Workstreams State
A. Duplication audit (#377) Passes 4b-4d (#386-#388), Passes 5a-5d (#389-#392, 51 sub-issues), Phase 6 (#819) All closed
B. Refman-coverage audit (#807) Passes 3b/3c (#982/#983), 4b-4d companions (#812-#814), 5a-5d companions (#815-#818), Phase 6 (#820), comment staleness (#872), tails (#1044, #1045) All closed
C. Small adjacent work Curve2D.swift breakout (#687), large-file tracking (#396), unmeasured-values test half (#764) and arrow-notation sites (#781), orphaned Document-bridge findings (#949-#951) All closed

Two items the previous version listed as deferred have since closed on their own: #707, the thread-safety campaign, and #765, the unmeasured-value gate spike. #767, the mutation-coverage viability spike, is still open and still not a gate.

Long-lived branches

#377’s rule is that a pass cuts its own branch and merges straight back, with no shared integration branch. Two exceptions run today, both deliberate, both recorded so the rule is not quietly eroded:

Branch Why it is long-lived
wasm/1689-wasip1 Phase 0 can return “no”. Landing a partial port on main before the go/no-go is exactly what PR #2076 did and what #2175 was refiled to prevent.
v5.0.0-766-execution 272 execution issues producing evidence, merged in slices. It carries its own CI workflow so its gates do not run against main until the work does.

Neither is an integration branch for a pass. Both end: wasm/1689-wasip1 at #2175’s verdict, v5.0.0-766-execution as its slices land.

The rule that keeps this from becoming “hold everything, forever”

Every pass in this programme found more work mid-flight than it started with. #396 spawned #1071 spawned two more PRs. #928 spawned #930. Pass 4a’s own closing comment has a section literally titled “Added after this summary was first posted.” That is not a criticism of any of those passes; finding real, unscoped work is what an audit is for. It is why this section exists.

A finding discovered while working an in-scope issue ships in v4.0.0 if it stays inside that issue’s own lane. A finding that opens a materially new workstream gets filed and explicitly deferred to whatever ships after v4.0.0.

The rule held under pressure during the beta.3 cut and is worth the example. Retiring the three bridge-side mitigations the repin made obsolete (#1030, #603, #1515) surfaced that occtswift-wrapping-gaps.md had recorded the datum point accessor as blocked by one of those guards. Patch 0029 unblocks it, so wrapping it is now an ordinary job. It was filed and not done, inside a PR that was already touching the file, because it is new surface rather than the retirement’s own lane.

Order

Steps 1 to 6 of the previous version are complete and are recorded under “Banked” above. What remains:

  1. Merge the test-validity slices (F) from v5.0.0-766-execution, in domain-sized pieces. Before 8.0.2, for the reason in F.
  2. Survey the 29 carried patches against 8.0.2 (E), producing a carry/retarget/retire verdict per patch. Before any build.
  3. Repatch, rebuild, publish v4.0.0-kernel.2, repin. Per pinned-kernel-patch-check.md and Package.swift’s own SEQUENCING comment: publish the asset, confirm it resolves with curl -fsIL rather than only that its checksum matches, then land the pin.
  4. Record 8.0.2’s new and deprecated API. A removal this package wraps is a v4.0.0 break and goes in SEMVER.md. A new operation is optional for this release.
  5. Re-run the kernel-parity evidence against the new kernel, as the version bump’s own regression signal.
  6. Cut v4.0.0-beta.4 once the repin is verified.
  7. WASM Phase 0 reports (#2175), in parallel throughout, on its own branch.
  8. Release: SEMVER.md assembly, the kernel patch-count check, CHANGELOG transcription verification, RC, tag.

Beta and release-candidate plan

Betas are published as GitHub pre-releases, never as the repo’s latest release. A beta tag points at an ordinary merge commit on main: no release commit, no CHANGELOG restructuring (## Unreleased stays as it is until v4.0.0 itself), no version string to bump.

Tag Cut Points at Scope
v4.0.0-beta.1 2026-08-26 c27d2525 Pass 4a’s loose tail
v4.0.0-beta.2 2026-09-19 tip of main Pass 4b (#386) plus the CHANGELOG backfill (#1688)
v4.0.0-beta.3 2026-09-22 8fea0b61 Passes 5a-5d, Phase 6, and the v4.0.0-kernel.1 repin
v4.0.0-beta.4 not cut   OCCT 8.0.2 (E), plus whatever of F has landed

v4.0.0-beta.3 cut on the kernel, not on WASM. The previous version of this document gated it on #1689’s Phase 0 spike. That gate was released on 2026-09-22 and #1689 unmilestoned, because holding a beta for a spike that can return “no” inverts the “betas open at natural boundaries” rule. What beta.3 actually shipped is the thing that had been waiting: v4.0.0-kernel.1 carries all 29 carried patches where beta.2 carried 17, closing twelve patches that had been on disk and in no CI job, four of which were live consumer exposure.

The per-beta milestone convention was not followed for one stretch and is not backfilled. 4c, 4d, the OCAF lanes, their refman companions and #1044 all closed between beta.2 and beta.3 with no milestone opened. They carry the release:v4.0.0 label and their own parents. A milestone assembled after the fact from git history is not a real-time signpost, which is the only thing the milestone is for.

RC exit criteria, fixed here rather than decided when the moment arrives:

  1. Full swift test clean across enough runs to cross the known ~1-in-10 parallel-suite flake rate. One green run does not mean anything in this repo; #341/#344/#345’s own history says so.
  2. Scripts/tsan-stress.sh all clean.
  3. All twelve gates plus their --self-tests clean, the five censuses’ --self-tests clean, and check-doc-snippets.py, which gates separately because it needs a build. (The previous version said “all eight gates”, which was true when it was written and has been wrong since.)
  4. A real downstream consumer built and tested against the RC tag, not just this repo’s own suite. Not generic best practice: v2.0.0’s break table omitted #595 until building swiftGCS against the tag broke at two call sites nothing in this repo’s own testing could have predicted.

Promote to v4.0.0 once an RC meets all four with no new findings. A finding at RC stage is a new RC, not a footnote on the release notes.

SPI’s handling of a prerelease is verified, on 2026-09-23, after standing unverified for three betas. The page shows v3.0.0 as the latest release and v4.0.0-beta.3 separately as the latest beta release, which is the behaviour this plan wanted.

The mechanism is not what this document assumed, and the difference is operational. The previous text said a correctly-formed SemVer prerelease “should be excluded from latest resolution by construction”. It is not the tag’s form that does it: it is the Pre-release flag on the GitHub release. Every prerelease this repo has published carries it, verified:

v4.0.0-beta.3     prerelease=true   latest=false
v4.0.0-kernel.1   prerelease=true   latest=false
v4.0.0-beta.2     prerelease=true   latest=false
v4.0.0-beta.1     prerelease=true   latest=false
v3.0.0            prerelease=false  latest=true

So gh release create --prerelease is load-bearing, not cosmetic. A -beta.N or -kernel.N tag published without that flag would take the latest slot and be offered to every consumer resolving the package, whatever its tag spells. Treat a missing --prerelease as a release defect rather than a formatting one, and check isPrerelease after publishing rather than trusting the tag name:

gh release list --limit 5 --json tagName,isPrerelease,isLatest

The same flag is what keeps the kernel pre-releases (v4.0.0-kernel.1 and its predecessors) out of the stream, and those matter more than the betas: they carry an OCCT.xcframework.zip asset that Package.swift pins, and are published in the middle of a release window by design.

How the release is tracked

One milestone per beta (v4.0.0-beta.1 through .4), since that is the granularity actually being decided. Sub-issue links stay with each issue’s real parent, not the tracking issue: GitHub allows one parent per issue, so something already under #377 or #807 cannot also link under #1144.

Mechanism What it expresses
v4.0.0-beta.N milestones Which release chunk an issue ships in. The primary membership signal.
Sub-issue links, on each issue’s own natural parent Hierarchy/workstream, unchanged from #377/#807’s trees.
release:v4.0.0 label Membership in this major, independent of which beta.
phase:* labels Workstream.
Board Status Backlog, In Progress, PR Review, Code-Review, Ready, Done.

#1144, the tracking issue, is closed: the duplication and refman programmes it tracked are done. E, D and F are tracked by their own issues and branches rather than by reopening it.

Versioning

Rule 2 of SEMVER.md: accumulated breaking changes to the public Swift API. Fifteen of them, registered with migrations in that file’s v4.0.0 section.

Rule 1 does not fire. The kernel moves this release, from V8_0_1 to V8_0_2, but that is an OCCT patch. Rule 1 is about an OCCT major, which is what made v1.0.0 a major alongside OCCT 8.0.0 GA. A patch bump is a MINOR trigger at most and forces nothing, exactly as the v4.0.0-kernel.1 rebuild did. The previous version of this document said “the OCCT kernel is not expected to move for this release (no OCCT 9.0 signal)”, which conflated the two: there is still no 9.0 signal, and the kernel is still moving.

SEMVER.md is assembled once, at release, from each merged PR’s own ## SemVer impact statement, per semver-at-release.md. Do not hand-edit it before then. The v4.0.0 section exists already because beta.3 needed it; it is re-assembled, not appended to, when 8.0.2’s own breaks are known.

Two things that assembly taught, both now written into that file rather than here: a stated impact can describe a change that never landed, and a PR can make a break and declare nothing. Both happened in this line, and both were caught only by checking claims against the source.