Implement 0370: version discipline — API surface gates, protocol parity, packaging validity - #587
Merged
crs48 merged 10 commits intoJul 19, 2026
Conversation
added 9 commits
July 19, 2026 10:06
…UTING Phase 1 of exploration 0370. The version number can't be made honest -- npm forbids renumbering downward -- so write the promise down instead. STABILITY.md states what carries a compatibility promise (@public exports only, today just the react root contract), what doesn't (@beta/@alpha/ @internal), and that protocol versions are tracked separately from package semver. CONTRIBUTING gains a Versioning section with the two rules that matter when touching packages/*. Signed-off-by: xNet Test <test@xnet.dev>
Swift shipped protocolVersion = 3 while TypeScript and every conformance vector use 4. protocolVersion is inside the hashed canonical object, so Swift was producing different change hashes than the rest of the fleet. v3 was only ever 'reserved for multi-level crypto' and never carried by the change-signing path, so 3 was never a real wire version. It survived because Swift is only 'swift build'-ed in CI, never conformance-tested. Adds protocol-version-parity.test.ts, which reads the Rust and Swift literals and asserts they equal CURRENT_PROTOCOL_VERSION, plus the TS LWW copy and the frozen vector. Verified it fails on the original drift. Leaves HubConnection's client-handshake protocolVersion: 1 ALONE -- that is the hub WebSocket handshake version (hubProtocolVersion = 1), a different number sharing a field name. Sending 4 there would produce a spurious version-mismatch. Documented in place so it isn't 'fixed' later. Signed-off-by: xNet Test <test@xnet.dev>
It wrote placeholder {"schemas":[]} for BOTH sides of the comparison and
substituted a zeroed diff on error, so it reported breakingChanges: 0
unconditionally and always had. The CLI it called through is stubbed the
same way -- extractSchemas() reads a JSON file nothing produces.
Per CLAUDE.md 0294: a check with no decidable pass condition and no named
consumer is worse than no check, because a green tick reads as coverage
we don't have. Nothing consumed this lane; git remembers it if we ever
implement real schema extraction.
Leaves packages/cli schema diff alone -- diffSchemas() is real and tested;
only the workflow was the lie.
Signed-off-by: xNet Test <test@xnet.dev>
…version They both equal 4 today but are different constants: CURRENT_PROTOCOL_VERSION is the current change format, while LWW_TIEBREAK_KEY_VERSION records the version the grinding-resistant tiebreak ACTIVATED at and stays pinned when the format moves to 5. My first cut of the parity test asserted equality between them, which would have gone spuriously red on the next protocol bump and pressured someone into a wrong fix. Now asserts the real invariants: Swift tracks CURRENT_PROTOCOL_VERSION, Rust tracks LWW_TIEBREAK_KEY_VERSION, and LWW <= CURRENT. Cross-references the TS/Rust mirrors in place rather than generating them from one source -- cross-language codegen is disproportionate here, and the parity test already turns drift into a red build. Signed-off-by: xNet Test <test@xnet.dev>
The Stop hook checked only that a changeset EXISTS. Nothing checked the bump matched the diff, and no JS tool can decide that in general (exploration 0370). But one class of error IS decidable: a wire-visible constant moving under a patch or minor. Adds PROTOCOL_SENTINELS covering CURRENT_PROTOCOL_VERSION, LWW_TIEBREAK_KEY_VERSION, the runtime protocol bundle, the sqlite schema version and XNETPACK_FORMAT_VERSION. Only added/removed lines matching a sentinel count, so unrelated edits to the same file don't trip it. Verified all four cases: blocks on patch/minor, passes on major, passes on an unrelated edit to the same file, passes on a clean tree. Also mirrors the sentinel list into the ai-generate bump prompt. hub and editor stay out: both are private:true, so removing them from the changeset ignore list would be a no-op -- neither is on npm, so there is no semver bump to demand. Documented as a known gap in the hook header. Signed-off-by: xNet Test <test@xnet.dev>
Phase 3 of exploration 0370: scope semver to a surface we can actually hold, and make changes to it visible. - api-extractor on react/core/data/sync; reports committed under packages/*/etc/*.api.md, CODEOWNERS-gated so a human signs off - check:api-report regenerates and fails on any diff. NOTE: plain 'api-extractor run' reports a changed signature as a WARNING and exits 0, so it would have been a gate that could never fail -- exactly the 0294 antipattern. Runs --local and lets git decide instead, and checks git status (not git diff) so a brand-new untracked report can't slip through. Verified it fails on a removed export. - publint over all 19 publishable packages at --level error (the suggestion tier is unpassable by design) Two real bugs found by adding the gates: - 'types' was ordered AFTER 'import' in 48 export subpaths across 19 packages; conditions are order-sensitive, so TS could mis-resolve - @xnetjs/data advertised a ./portability subpath never added to its build -- the .xnetpack codec did not resolve for consumers at all Release tags are started, not finished: api-extractor treats untagged exports as @public, so the reports mark ~372 symbols public in react alone. STABILITY.md says so explicitly rather than implying the surface is already scoped. Signed-off-by: xNet Test <test@xnet.dev>
The first reports were generated against stale dists -- several packages had not been rebuilt since exports were added, so the baseline understated the real surface by ~880 lines (notably @xnetjs/data/portability, which had never been built at all, and sync's verifyChangeFast). The drift gate caught its own bad baseline, which is the intended behaviour. Reports now reflect 'pnpm turbo run build --filter=./packages/*'. Signed-off-by: xNet Test <test@xnet.dev>
Signed-off-by: xNet Test <test@xnet.dev>
…ions Signed-off-by: xNet Test <test@xnet.dev>
Contributor
🖼️ UI changes in this PRScreensAuto-captured by CI · run. Informational — not a blocking check. |
Contributor
|
Preview removed for PR #587. |
Signed-off-by: xNet Test <test@xnet.dev>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Implements phases 1–4 of exploration 0370. Phases 5–6 are deliberately left open — they're time-based (a six-week measurement) or external (emailing a stranger, contacting npm support), plus one judgement call that's yours.
The framing
The version number can't be made honest — npm forbids renumbering downward — so this writes the promise down and builds the gates that make it enforceable.
What landed
STABILITY.md— what carries a compatibility promise, what doesn't, how protocol versions differ from package semver. Linked from README andCONTRIBUTING.md#versioning.3→4) plusprotocol-version-parity.test.tscovering Rust and Swift.protocolVersionsits inside the hashed canonical object, so Swift was producing different change hashes than the rest of the fleet. It survived because Swift is onlyswift build-ed in CI, never conformance-tested.schema-check.ymldeleted — it wrote placeholder{"schemas":[]}for both sides and substituted a zeroed diff on error, so it reportedbreakingChanges: 0unconditionally.PROTOCOL_SENTINELSin the Stop hook — a wire-visible constant moving under a patch/minor is now blocked.packages/*/etc/, CODEOWNERS-gated, drift gate in CI.publintover all 19 publishable packages.Two real bugs the new gates found
Both were shipped and broken before this PR:
typeswas ordered afterimportin 48 export subpaths across 19 packages. Export conditions are order-sensitive, so TypeScript could mis-resolve types for every published package.@xnetjs/data/portabilitydidn't resolve at all. The package advertised the subpath but it was never added to the build — that's the.xnetpackcodec we tell users to back up with.Corrections I made to the exploration's own plan
Following the checklist literally would have introduced bugs:
HubConnection.swift'sprotocolVersion: 1is correct — it's the hub WebSocket handshake version, a different number sharing a field name. Changing it to 4 would cause a spuriousversion-mismatch. Left alone, documented in place.hub/editorfrom the changeset ignore list is a no-op — both areprivate: true, already outside the hook. Recorded as a known gap instead.LWW_TIEBREAK_KEY_VERSIONmust not be collapsed intoCURRENT_PROTOCOL_VERSION— same value today, different meanings. My first cut of the parity test asserted equality, which would have gone spuriously red on the next protocol bump. Now assertsLWW <= CURRENT.api-extractor runalone cannot gate — it reports a changed signature as a warning and exits 0, the exact "gate that can't fail" the exploration criticises. The gate runs--localand usesgit status(notgit diff, which ignores untracked files).One validation deliberately left failing
"Deleting an
@internalexport does not trip the gate." It does. api-extractor treats untagged exports as@public, and I tagged 5 symbols out of ~372 — so today these reports are a change-visibility gate, not a tiering gate. Real value on its own (a removed export can't land silently), but not the scoped promise the doc describes.STABILITY.mdsays so plainly rather than implying the surface is already narrow.For you to decide
useXNetis tagged@internal ... Not part of public APIin source but listed as stable root contract inpackages/react/README.md. Left untouched — that's a maintainer call.3.0.0as a stability re-declaration) — your judgement.xnetacquisition email and the npm-support question are yours to send.Verification
pnpm turbo run typecheck— 85/85 passpnpm test— 10,784 pass, 1 pre-existing failure:tests/reliability/restore/restore-drill.test.tsfails locally with an 8 KB pipe truncation. Not touched by this branch, and the nightly soak was already red on main this morning; docs(exploration): 0370 — version number discipline and renumbering #585 (docs-only) passed all required test shards, so it's environment-specific.🤖 Generated with Claude Code