Skip to content

Implement 0370: version discipline — API surface gates, protocol parity, packaging validity - #587

Merged
crs48 merged 10 commits into
mainfrom
claude/0370-version-number-discipline-and-renumbering
Jul 19, 2026
Merged

Implement 0370: version discipline — API surface gates, protocol parity, packaging validity#587
crs48 merged 10 commits into
mainfrom
claude/0370-version-number-discipline-and-renumbering

Conversation

@crs48

@crs48 crs48 commented Jul 19, 2026

Copy link
Copy Markdown
Owner

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 and CONTRIBUTING.md#versioning.
  • Swift protocol drift fixed (34) plus protocol-version-parity.test.ts covering Rust and Swift. protocolVersion sits inside the hashed canonical object, so Swift was producing different change hashes than the rest of the fleet. It survived because Swift is only swift build-ed in CI, never conformance-tested.
  • schema-check.yml deleted — it wrote placeholder {"schemas":[]} for both sides and substituted a zeroed diff on error, so it reported breakingChanges: 0 unconditionally.
  • PROTOCOL_SENTINELS in the Stop hook — a wire-visible constant moving under a patch/minor is now blocked.
  • api-extractor on react/core/data/sync, reports committed under packages/*/etc/, CODEOWNERS-gated, drift gate in CI.
  • publint over all 19 publishable packages.

Two real bugs the new gates found

Both were shipped and broken before this PR:

  1. types was ordered after import in 48 export subpaths across 19 packages. Export conditions are order-sensitive, so TypeScript could mis-resolve types for every published package.
  2. @xnetjs/data/portability didn't resolve at all. The package advertised the subpath but it was never added to the build — that's the .xnetpack codec 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's protocolVersion: 1 is correct — it's the hub WebSocket handshake version, a different number sharing a field name. Changing it to 4 would cause a spurious version-mismatch. Left alone, documented in place.
  • Removing hub/editor from the changeset ignore list is a no-op — both are private: true, already outside the hook. Recorded as a known gap instead.
  • LWW_TIEBREAK_KEY_VERSION must not be collapsed into CURRENT_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 asserts LWW <= CURRENT.
  • api-extractor run alone 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 --local and uses git status (not git diff, which ignores untracked files).

One validation deliberately left failing

"Deleting an @internal export 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.md says so plainly rather than implying the surface is already narrow.

For you to decide

  • useXNet is tagged @internal ... Not part of public API in source but listed as stable root contract in packages/react/README.md. Left untouched — that's a maintainer call.
  • C2 (ship 3.0.0 as a stability re-declaration) — your judgement.
  • The xnet acquisition email and the npm-support question are yours to send.

Verification

  • pnpm turbo run typecheck — 85/85 pass
  • pnpm test — 10,784 pass, 1 pre-existing failure: tests/reliability/restore/restore-drill.test.ts fails 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.
  • Every gate verified in both directions — blocks the bad case, passes the good one, no false positive.

🤖 Generated with Claude Code

xNet Test 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>
@crs48
crs48 temporarily deployed to pr-587 July 19, 2026 17:32 — with GitHub Actions Inactive
@github-actions

github-actions Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

🖼️ UI changes in this PR

Screens

✏️ Discover _(SSIM 0.982)_
before after diff
before after diff

Auto-captured by CI · run. Informational — not a blocking check.

github-actions Bot added a commit that referenced this pull request Jul 19, 2026
github-actions Bot added a commit that referenced this pull request Jul 19, 2026
@github-actions

github-actions Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Preview removed for PR #587.

github-actions Bot added a commit that referenced this pull request Jul 19, 2026
Signed-off-by: xNet Test <test@xnet.dev>
@crs48
crs48 temporarily deployed to pr-587 July 19, 2026 17:43 — with GitHub Actions Inactive
github-actions Bot added a commit that referenced this pull request Jul 19, 2026
@crs48
crs48 merged commit bd313b2 into main Jul 19, 2026
22 checks passed
@crs48
crs48 deleted the claude/0370-version-number-discipline-and-renumbering branch July 19, 2026 17:55
github-actions Bot added a commit that referenced this pull request Jul 19, 2026
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