v2.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: #669. Branch: refactor/381-pass1b, and every child PR targets that branch rather than main.
Scope
Four workstreams, and nothing else:
| Workstream | Issue | State |
|---|---|---|
| Pass 1a, geometry primitives duplication audit | #380 | Closed, flagged for a re-review pass |
| Pass 1b, C++ bridge header duplication audit | #381 | All 9 findings fixed, awaiting the merge to main to close |
| OCCT 8.0.1 absorb | #654 | Kernel landed, follow-through open |
| The three file breakdowns | #393, #394, #395 | Detailed, ready to dispatch |
Everything from the branch code reviews rides along with these, grouped into clusters below.
Deferred to after 2.0.0: audit Passes 2a through 5d (#382 to #392) and the further breakout backlog (#396). They are real work and they stay on the board, but they are not gating this release. #377 records that split.
Not in scope: #342 (bridge thread-handling contract) and #369 (OSD_ThreadPool root cause). Both are architecture-scale investigations whose findings would reshape the bridge; starting them inside a release that is already redistributing 55,000 lines invites two rewrites of the same code.
Why this plan exists: the ground we keep re-covering
The repo’s own history names the failure mode. Nearly every closed issue records a variant of one sentence: the census said N, the measurement said M.
- #558: census said 14 sites, measurement said 28
- #571: census said 3, measured 6
- #583: census said five, measured six
- #595: census said six, measured nine
- #573: the issue’s own fix table contained a fresh error
- #507, #553, #562: the census criterion itself was wrong
The repeated ground is not the fixes. It is that each issue rebuilds the same census from scratch, usually by grep, and gets it wrong, then the next issue in the same family rebuilds it again and gets it wrong differently. Three of the gate scripts on this branch were confidently wrong for exactly this reason (#618, #624, #626).
The rule
Build each census once, as a committed executable artifact, before fixing anything in its family.
A census artifact is a probe under Scripts/repro/<cluster>/ that enumerates and measures its family against the real kernel, and prints a table. Not a grep, not a list in an issue body. Every issue in the cluster consumes that artifact instead of re-deriving it, and the artifact is updated (not replaced) when a member issue proves it incomplete.
This is the difference between twenty investigations and six. It also gives every fix in the cluster a before/after that costs nothing extra to produce.
Two corollaries learned the hard way during the 8.0.1 absorb:
- A grep-shaped census is blind to indirection.
check-null-handle-guards.pymisses handles obtained locally and passed onward, which is how #656’s SIGSEGV survived every gate. - A green suite is not evidence where there is no coverage. No test in this repo uses INTERNAL/EXTERNAL edge orientation, and #645 records that the Gordon tests assert only status ordinals. In those areas a passing run proves nothing, so the coverage has to come first.
- A passing test is worth nothing until you have watched it fail. Every new test and every new
--self-testcase gets run once with its subject broken: inject the defect, confirm the failure, restore, confirm the pass, and report both.derive-shape-domain-split.pyshipped a--self-testthat passed 6/6 while one of its cases proved nothing, twice over, and only the removal check found it. Policy:okf/policies/prove-the-test-fails.md.
Clusters
Each cluster gets a tracking issue that owns its census artifact. Member issues are sub-issues of it and must not start before the census lands.
A. Sub-shape enumeration and orientation. edges() collapses 24 occurrences to 12 distinct (#638); detectPocketsAAG() rides the lossy enumeration and returns different answers for the same geometry depending on compound member order (#642); the nbEdges/nbVertices/nbFaces counts contradict their own documentation (#651). #613 and #614 already fixed the faces() half of this. Highest leverage in the release: fixing the root changes what B even needs.
B. Fillet and chamfer edge-set contract. A duplicate edge index silently discards a radius (#633); the family cannot tell a caller that OCCT declined edges it named (#639). Both are about how the family interprets the caller’s edge list, so both depend on A.
C. Crash guards. Curve3D.extrema on parallel curves (#636), GeomTools_Curve2dSet/SurfaceSet null handles (#643), Surface.appSurf with a single curve (#644). #656 is the same family and is already fixed; #636 and #643 are now fixed too, leaving #644. The census here is not a probe but a checker upgrade: teach check-null-handle-guards.py the shape it is currently blind to, and it enumerates the rest.
D. Continuity. #513 is already an explicit census of 38 unaudited functions and four incompatible request encodings. #437 (plateSurface can never accept .g2) and #438 (two public APIs over one OCCT class) are instances of it. Do the census, then the instances fall out.
E. Approximation error reporting. GeomFill_Sweep and ShapeUpgrade_UnifySameDomain accept an approximation without reading its error (#597); the Gordon tests cannot fail (#645). Both are the tail of #522’s blast radius, and #645 is a coverage prerequisite, not an ordinary fix.
Standalone, owned directly by #669: #598 (the two pipe sweep modes are wired to each other’s OCCT mode, a breaking fix) and #640 (solver dimension arguments trap, leastSquares reads out of bounds).
Order
- The three breakdowns (#395, then #394, #393). They touch 55,000 lines across three files that nearly every other issue also edits. Doing them first means every later fix lands in the new layout; doing them last means three enormous rebases. #395 is fully mechanical (see the issue) so it goes first and de-risks the pattern.
- Coverage prerequisites: #645, and the INTERNAL/EXTERNAL gap named in #655.
- Cluster censuses, in parallel once the layout is stable.
- Cluster A (#664), then B (#665). C (#666), D (#667) and E (#668) are independent of A and of each other.
- Standalones and 8.0.1 follow-through (#598, #640, #655, #657) any time after step 1.
- Release:
url:/checksum:bump, prebuilt bridge re-enabled, docs, tag.
How the release is tracked
Four mechanisms, each doing one job, so none of them has to be maintained by hand from the others:
| Mechanism | What it expresses |
|---|---|
Milestone v2.0.0 | Membership. 123 issues, derived by walking the #669 sub-issue tree rather than typed out. Gives the progress bar. |
| Sub-issue links | Hierarchy. GitHub derives the board’s read-only Parent issue and Sub-issues progress fields from these, so the tree needs no separate upkeep. |
phase:* labels | Workstream. Extends the existing audit-pass taxonomy with phase:cluster-a to phase:cluster-e, phase:occt-801, and the pre-existing phase:file-breakout. |
Board Status | State. Backlog, In Progress, PR Review, Code-Review, Ready, Done. |
release:v2.0.0 duplicates the milestone as a label, because the board can filter on labels in views where milestone filtering is awkward. The two are kept in exact agreement; if they ever disagree the milestone wins, since it is the one derived from the issue tree.
One trap worth knowing. 31 issues in this milestone are fixed and merged but still show open, because Closes #N only fires when a PR merges into the default branch and everything here merges into refactor/381-pass1b. They are marked Done on the board and will close themselves when the branch reaches main. Check the board Status before picking anything up: the GitHub open or closed state is not the truth during this release.
Execution mechanics
The bridge always builds from source on this branch. Package.swift forces it and ignores OCCTSWIFT_BRIDGE_PREBUILT. Nearly every issue here edits Sources/OCCTBridge/src/*.mm, and a prebuilt that predates the edit links silently and reports a pass for code that was never compiled. That is not hypothetical: it nearly happened during the 8.0.1 absorb. Restoring the switch is part of the release commit.
build-and-test is green, and green is the merge criterion. This changed on 2026-08-04. It resolves Package.swift’s pinned asset, which is now the v2.0.0-kernel.1 pre-release carrying the eleven unreleased patches, so the job finally builds the kernel this branch’s tests are written against.
Before that the asset was v1.15.18, which could not contain an unreleased patch, so the job was structurally red (#585) and the criterion was parity with the base branch: same failing set, same count. If you are reading a PR from before that date, that is the rule it was merged under.
When a new kernel patch lands mid-release the problem returns, because the pinned asset predates it. The fix is to publish the next vX.Y.Z-kernel.N pre-release and bump the pin, not to fall back to parity. docs/guides/building-occt.md has the procedure. Any change in the failure set or mode is a real signal and must be investigated, not waved through. During the 8.0.1 absorb the first run died with a SIGSEGV rather than the expected assertion failures, which turned out to be the known #344/#345 flake, but only checking established that.
kernel-integration.yml is the real check. It is the only job that compiles the patched kernel, and it takes roughly 80 minutes. Run it at cluster boundaries rather than per PR.
RESOLVED 2026-08-07. This block recorded that carried patches were validated by nothing automated. They now are.
The branch carried fourteen patches (
0010-0012,0014-0024) against a pinned asset holding eleven, so0022,0023and0024sat outside the binarybuild-and-testresolves and could not be exercised by it.kernel-integration.ymlcould not pass at all under #727, and0023shipped with no Swift test. Three carried patches therefore had no automated assertion of their behaviour on any kernel, which is the #585 failure mode: green because the suite tests a kernel nobody ships.Cleared by
v2.0.0-kernel.2(#512), verified before publishing: all fourteen applied with zero stray modifications toocct-src, objects newer than the patched sources,0024proven present in the binary with no override-linked TUs, and 5442 tests, 0 failures against it.Then reopened within the hour, exactly as predicted, and re-closed by
v2.0.0-kernel.3. The paragraph below used to end by warning that0025(#597) was in flight and would reopen the gap. It did, and0018(#555) changed at the same time under upstream review on OCCT#1417, so the pin was two patches stale rather than one. Current state: fifteen carried patches,0010-0012and0014-0025, all inv2.0.0-kernel.3, verified 15/15 applied with zero stray modifications (50 files owned by a patch, 50 modified) and 5481 tests, 0 failures.Two things that rebuild taught, worth more than the numbers:
- Reset
occt-srcto a clean tag before applying, do not patch incrementally. A patch whose content changed will not stack on its own earlier form. It either fails to apply or, worse, half-applies.- Verify the published asset by re-downloading it, not by checksumming the local zip. The
kernel.3upload was briefly namedOCCT3.ziprather thanOCCT.xcframework.zip, so the conventional URL returned 404 while the checksum was perfectly correct. A checksum-only check passes that every time.The general rule stands and this instance proves it rather than closing it. A patch carried in the tree but absent from the pinned asset is invisible to CI, and the gap reopens the moment the next patch lands. It has now reopened twice in one day. Watch for it at release: the count in
Package.swift’s comment and the patch count inScripts/patches/must agree, and if they do not, the difference is the set nothing is testing.CLAUDE.md’s Project Summary carries the same check, and it went stale within minutes of being written.
Versioning
v2.0.0 is triggered by Rule 2 in SEMVER.md, the accumulated breaking changes to the public Swift API, not by the OCCT version. The 8.0.1 re-pin rides along rather than causing it.
Every break lands in the register in SEMVER.md with its migration before the tag, not after. Known breaks so far: #609’s twelve mass-property signature changes (already held for this major), #619, #495, #499, #541, #568, #613, #498, #502, and #598. The breakdowns (#393 to #395) must add none, and that is an acceptance criterion on each.