Skip to content

docs(exploration): 0370 — version number discipline and renumbering - #585

Merged
crs48 merged 3 commits into
mainfrom
docs/0370-version-discipline
Jul 19, 2026
Merged

docs(exploration): 0370 — version number discipline and renumbering#585
crs48 merged 3 commits into
mainfrom
docs/0370-version-discipline

Conversation

@crs48

@crs48 crs48 commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Follow-on from #571 / #583, which made every human-readable surface say "alpha" while @xnetjs/core still says 2.5.0.

The short version

The renumber isn't available, and the number was never the problem.

  • npm never lets a version string be reused, and since npm 11 (which we pin in npm-release.yml:40) publish refuses to move latest backwards. @xnetjs/core@0.5.0 is publishable as a string but incoherent as a release.
  • The unpublish escape needs <300 downloads/week; core pulls 2,120. Door shut regardless of whether that traffic is real users or bots.
  • A true 0.x requires a scope rename@xnet/core returns 404, so it looks free. Costed as a real option rather than dismissed, since nobody's meaningfully depending on us yet.
  • Worth knowing: under node-semver ^0.2.3 and ~0.2.3 are identical. 0.x buys safety through total friction, not signalling.

Four enforcement gaps, all verified against the code

Gap Evidence
Stop hook checks a changeset exists, never that the bump matches the diff assert-coverage.mjs:95-105 — regex for name: patch|minor|major
schema-check.yml is a stub that can never fail :33,:41 placeholder {"schemas":[]} both sides; :49 zeroed fallback
~25 protocol constants hand-maintained, one already drifted TS CURRENT_PROTOCOL_VERSION = 4 vs Swift protocolVersion: Int64 = 3
Ignored packages ship wire-visible changes with no release intent hub (Docker image), editor (content-v4) both in .changeset ignore

The Swift drift is a live correctness bug, not just hygiene — it's the enforcement gap made visible.

Also relevant: JS has no tool that detects a breaking change. cargo-semver-checks has no equivalent; semver-ts.org's own tooling appendix recommends hand-written assertions. That's why CLAUDE.md's "bump from the diff" is a human instruction.

Recommendation

Keep the numbers. Write STABILITY.md. Scope semver to a surface we can actually hold via api-extractor release tags (@public/@beta/@alpha/@internal) — a smaller honest promise beats a bigger dishonest one. Fix the drift, delete or implement the stub. Then decide the rename separately, because a rename without gates lands us back at @xnet/2.5.0 in a year.

Verification

  • All 15 cited repo paths confirmed to exist
  • All 4 mermaid diagrams parse (validated against the site's mermaid build)
  • Registry facts pulled live from npm, not recalled

Two things flagged as unproven in the doc: whether the @xnet scope is genuinely claimable (an unpublished-but-reserved scope looks identical from outside), and what the 2,120/week download traffic actually is.

Docs only — no changeset needed.

🤖 Generated with Claude Code

Follow-on from #571/#583, which made every human-readable surface say
"alpha" while @xnetjs/core still says 2.5.0.

The headline finding is that the appealing fix is unavailable: npm never
lets a version string be reused, and since npm 11 (which we pin) publish
refuses to move `latest` backwards. The unpublish escape needs <300
downloads/week and core pulls 2,120. A true 0.x needs a scope rename —
@xnet/* looks free, and that option is costed rather than dismissed.

The more useful finding is that the number was never the problem. Four
verified enforcement gaps:

- the Stop hook checks a changeset EXISTS, never that the bump matches
  the diff — and no JS tool can (cargo-semver-checks has no equivalent)
- schema-check.yml is a stub: placeholder schemas both sides, zeroed
  fallback, so breakingChanges is always 0
- ~25 protocol constants are hand-maintained with no link to semver, and
  one has already drifted — TS says protocol version 4, Swift says 3
- ignored packages (hub, editor) ship wire-visible changes with no
  release intent

Recommends keeping the numbers, writing STABILITY.md, and scoping semver
to a surface we can hold via api-extractor release tags — a smaller
honest promise over a bigger dishonest one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Signed-off-by: xNet Test <test@xnet.dev>
@crs48
crs48 temporarily deployed to pr-585 July 19, 2026 16:36 — with GitHub Actions Inactive
@github-actions

github-actions Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

✓ Changelog fragment found — thanks!

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 #585.

github-actions Bot added a commit that referenced this pull request Jul 19, 2026
…e idea seriously

Two corrections and one real addition.

Correction 1 — namespaces. The draft treated "@xnet/core is 404" as evidence
the scope was free. It isn't: unscoped package names and org-derived scopes
are separate namespaces, and @Xnet is TAKEN as an org (confirmed by attempt).
So the rename that would have justified its cost is unavailable. @xnetts and
@xnetfyi are now secured as fallbacks, but both read worse than @xnetjs, so
Option B moves from "costed alternative" to "held, revisit only if xnet is
acquired". Adds the deprecation path (npm deprecate accepts a range) since
that was the stated worry about renaming.

Correction 2 — the xnet package is 4 years dormant, not 7: created 2019-10-24,
last published 2022-05-25, sole maintainer camilotd, 1 download/week. Notes
the hypothesis that this package is what reserves the @Xnet org — if true,
acquiring it unlocks the scope and is worth much more than the name.

Addition — Option D, the prerelease flag, which the first draft deferred too
quickly. The instinct is right, but there's a prerequisite: 2.5.0 came from
ordinary bumps, not from deliberate breaking changes, so "we ship a lot of
breaking changes" is currently UNMEASURED. A prerelease channel pointed at
unmeasured churn just relabels it. Tier the surface first, count for six
weeks, then let the number pick between pre mode (D1), a manual dist-tag
(D2), or simply accepting frequent majors (D3).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Signed-off-by: xNet Test <test@xnet.dev>
@crs48
crs48 temporarily deployed to pr-585 July 19, 2026 16:59 — with GitHub Actions Inactive
@crs48 crs48 added the skip-changelog Exclude this PR from the changelog label Jul 19, 2026
@crs48
crs48 temporarily deployed to pr-585 July 19, 2026 17:02 — with GitHub Actions Inactive
@crs48
crs48 merged commit 6c44387 into main Jul 19, 2026
9 of 10 checks passed
@crs48
crs48 deleted the docs/0370-version-discipline branch July 19, 2026 17:05
github-actions Bot added a commit that referenced this pull request Jul 19, 2026
crs48 added a commit that referenced this pull request Jul 19, 2026
…ty, packaging validity (#587)

Implements phases 1–4 of [exploration 0370](docs/explorations/). 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** (`3` → `4`) 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; #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](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-changelog Exclude this PR from the changelog

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant