fix(publish): honour the published frontier pin at render time (0362) - #578
Merged
Conversation
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>
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>
Contributor
🖼️ UI changes in this PRScreensAuto-captured by CI · run. Informational — not a blocking check. |
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>
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>
Contributor
|
Preview removed for PR #578. |
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._
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.









Closes the gap I flagged in #575:
publishedFrontierwas 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 inpackages/history/src/frontier.tsisRecord<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.
frontierEqualscompared 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 theY.Doca reader should see — the snapshot named bypublishedFrontier[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
publishedAtsurvives. 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