Skip to content

fix(publish): honour the published frontier pin at render time (0362) - #578

Merged
crs48 merged 5 commits into
mainfrom
claude/0362-honour-the-frontier-pin
Jul 19, 2026
Merged

fix(publish): honour the published frontier pin at render time (0362)#578
crs48 merged 5 commits into
mainfrom
claude/0362-honour-the-frontier-pin

Conversation

@crs48

@crs48 crs48 commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Closes the gap I flagged in #575: publishedFrontier was stored but not honoured, so a published post showed readers the author's latest draft rather than the version they published.

Two real defects, not just a missing feature

1. The frontier shape was wrong. I shipped Record<string, string>. The repo's actual contract in packages/history/src/frontier.ts is Record<NodeId, { hash, yjsSnapshotRef? }>. That second field is the whole point: without it a frontier pins only the record lane, so a published post's prose drifts with every keystroke. The pin would have looked correct and done nothing.

2. frontierEquals compared entries with ===. Once entries became objects that was reference equality, so every frontier reloaded from storage would have reported "edited since publish". Now compares contents.

What closes it

resolvePublishedDoc() returns the Y.Doc a reader should see — the snapshot named by publishedFrontier[postId].yjsSnapshotRef, or the live document when the post pins no document lane.

The snapshot store lives in @xnetjs/history, which depends on @xnetjs/data. Importing it would drag the data layer into a package whose point is running in a bare static build, so the resolver is injected. A pin that can't be resolved (pruned past the history horizon) falls back to the live document with a warning — rendering current text under an old publication date is a smaller lie than rendering nothing, but it's still a lie, so the caller gets told.

Proof

The D2 guarantee is now an executable test: publish → edit the live doc → the reader still gets the published words; re-publish → the edit becomes visible; and the original publishedAt survives. 75 tests in @xnetjs/publish (7 new).

Validation item "Editing a published post does not alter the published version until re-published" is now checked. Exploration status: 13/41 implementation, 5/18 validation — the doc stays [_]; phases 2–5 are untouched.

🤖 Generated with Claude Code

The first pass stored publishedFrontier without honouring it, so a reader
saw the author's latest draft rather than the published version. Two things
were wrong:

- The frontier shape was Record<string, string>. The repo's real contract
  (packages/history/src/frontier.ts) is Record<NodeId, {hash, yjsSnapshotRef?}>.
  Without the yjsSnapshotRef arm a frontier pins only the record lane, so the
  pin would have looked correct and done nothing for prose.
- frontierEquals compared entries with ===, which after the type change was
  reference equality: every frontier reloaded from storage would have looked
  changed.

resolvePublishedDoc() now returns the doc a reader should see. The snapshot
resolver is injected rather than imported, so @xnetjs/publish keeps its single
yjs dependency and still runs in a bare static build. An unresolvable pin
falls back to the live document with a warning, never silently.

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

Signed-off-by: xNet Test <test@xnet.dev>
@crs48
crs48 temporarily deployed to pr-578 July 19, 2026 05:26 — with GitHub Actions Inactive
xNet Test added 2 commits July 18, 2026 22:43
…ions

The repo-wide format:check gate was already failing on main for
course.ts/course.test.ts (0359) and CommunityFeed.tsx, so no PR could go
green without them. Pure Prettier output, no semantic change.

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

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

github-actions Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

🖼️ UI changes in this PR

Screens

✏️ Data _(SSIM 0.772)_
before after diff
before after diff
✏️ Discover _(SSIM 0.849)_
before after diff
before after diff
✏️ Home _(SSIM 0.868)_
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
Three CI failures, three real defects:

- @xnetjs/cli (published) depended on @xnetjs/publish (private), so
  `npm i @xnetjs/cli` would install a broken closure. Publishing it is also
  correct on its own terms: MIT, one dependency, useful standalone.
- '@xnetjs/data/database' had no vitest alias, so it resolved through package
  exports into dist/ and only worked after a build. Placed BEFORE the bare
  '@xnetjs/data' entry, per the rule already documented at the top of the
  alias map: Vite takes the first match.
- TAB_VIEWS.post emitted /post/:id but ROUTE_PREFIXES had no /post/ entry, so
  tabFromPathname returned null and every Post navigation was a dead link —
  pre-existing on main, and load-bearing now that tabless is the default.

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

Signed-off-by: xNet Test <test@xnet.dev>
@crs48
crs48 temporarily deployed to pr-578 July 19, 2026 06:01 — with GitHub Actions Inactive
github-actions Bot added a commit that referenced this pull request Jul 19, 2026
CommunityFeed imported { markFirstPosts, welcomeQueue } from '@xnetjs/social',
whose root barrel export *s the Node-only archive importers — so the web build
failed resolving node:fs. Every other web import of social already uses a
subpath. Adds the './community' subpath (the sub-barrel already existed, it
just was not reachable) and points the component at it, per the repo's barrel
policy.

Also waives the /post/ route in the electron parity gate: community forum
topics are web-first and desktop has no comms layer, the same reason 'channel'
is waived.

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

Signed-off-by: xNet Test <test@xnet.dev>
@crs48
crs48 temporarily deployed to pr-578 July 19, 2026 06:19 — with GitHub Actions Inactive
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 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 #578.

@crs48
crs48 merged commit e8f0076 into main Jul 19, 2026
23 checks passed
@crs48
crs48 deleted the claude/0362-honour-the-frontier-pin branch July 19, 2026 06:39
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
…posal process (#579)

Implements [exploration
0361](docs/explorations/0361_[x]_VOICE_AS_THE_COMPLEMENT_TO_EXIT_OSTROM_COLLECTIVE_CHOICE_AND_XNET_GOVERNANCE.md)
— all 6 implementation and 5 validation items.

## The core change

`GOVERNANCE.md` legitimated BDFL by pointing at **exit**: "you can fork
the code or re-implement the protocol." That is the posture Hirschman
warns degrades quality — when leaving is the only move, the
quality-sensitive members go first and quietly, and you lose the
customer *and* the signal. Exploration 0358 flagged it and deferred it
here.

Ostrom's design principle 3 supplies the missing half, and the important
detail is what it does **not** require: not a vote, not consensus — a
**path**. That is what makes this implementable at current size.

So BDFL now rests on three things rather than one:

> 1. **The right to leave.** MIT code, open protocol.
> 2. **The right to be heard, on the record.** Anyone affected by an
operational rule can propose changing it and gets a public written
answer with reasoning.
> 3. **This document commits to growing past BDFL** on the triggers
below.

## What landed

**The process** — [`docs/RULE_CHANGES.md`](docs/RULE_CHANGES.md).
Defines an operational rule (binds non-maintainers · changed by
maintainer commit · enumerated), lists the **six in-scope rules** from
the exploration's inventory, and states what is out of scope *with
reasons* (protocol → XPP, conduct, trademark, security embargoes).
Anyone affected may propose — explicitly including self-hosters and
plugin authors who have never contributed code.

**The commitment** — every RCP gets a public **accept / decline / defer
/ redirect with reasoning within 30 days**. Set generously on purpose: a
missed window is worse than no promise, so "deferred, here's why" counts
as a response.

**The intake** —
[`.github/ISSUE_TEMPLATE/rule-change.yml`](.github/ISSUE_TEMPLATE/rule-change.yml),
modelled on the XPP template: which rule, what change, who is affected,
what breaks if we do nothing.

**The record** —
[`docs/decisions/rule-changes.md`](docs/decisions/rule-changes.md),
append-only, seeded with **four real decisions** back-filled from the
exploration record and clearly marked `retroactive`:

| | Decision | Outcome |
|---|---|---|
| RCP-0001 | "No context capture" added to Charter §6 | Accepted |
| RCP-0002 | "No marketplace self-preferencing" added to §6 | Accepted |
| RCP-0003 | Relicense the core under GPL/AGPL (0345) | **Declined** |
| RCP-0004 | Charge for admission to the xNet Index (0365→0366) |
**Declined / withdrawn** |

Two declines and a reversal, because a log of only the accepted
proposals is marketing. RCP-0004 is specifically a lane we proposed and
then refused — under the append-only rule both stay visible.

**The receipt** — a `governance-rule-change-path` claim in the claims
ledger asserts all three surfaces exist *and* that the log still matches
`**Declined**`, so the honesty cannot be edited out. Verified it bites:
deleting the log fails that claim and only that claim.

**Cross-links** — Charter §6 and the ECONOMICS Moat Register now each
name themselves as in-scope and point at the path. §6's covenant is
answerable rather than merely stated; a covenant only the covenantor may
amend, silently, is a preference.

## Scope discipline

Not a vote, not a veto, not a foundation. The BDFL still decides — what
is removed is the ability to change a rule that binds other people
without saying why, in public, on the record. Options C (advisory
council) and D (foundation now) were declined as premature: both need a
constituency that does not exist yet, and 0241 stages D on triggers we
have not hit.

## Verification

- Claims ledger green (9 tests); **negative test** — removing the
decision log fails `governance-rule-change-path` and nothing else
- `pnpm typecheck` — 97/97
- `pnpm lint` — 0 errors
- `check-humane-patterns` — 2266 files OK
- All relative links in the five touched docs resolve; issue-template
YAML parses and validates
- README → GOVERNANCE → RULE_CHANGES is two clicks

No changeset: the only `packages/*` change is a test file.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---

_Note: an earlier revision of this branch also carried fixes for four
unrelated CI failures on `main` (the `@xnetjs/social` browser-bundle
break, a vitest alias gap, a missing `/post/` route prefix, and seven
unformatted files). [#578](#578)
landed equivalent fixes independently while this was open, so those
commits were dropped and `main` merged in. The diff here is now purely
the 0361 governance work._
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