RFCs

Alpha — published as written. A working document, not a specification. Nothing in it is settled and any of it can change without notice. It is published because the reasoning is part of the work, not only its result.

alpha RFC 0002 · 2026-10-05

Alpha — published as written. This is a working document, not a specification. Nothing in it is settled and any of it can change without notice; the sections that are still being argued about are the most likely to be rewritten. It is published because the reasoning is part of the work, not only its result: a design you cannot argue with is a design you cannot check.

1. What is being asked for

Two kinds of artifact to show the world, and one workflow built on top of them.

  1. A provenance view. A readable, public representation of what has been attested: non-tampered chains, evidence, signatures, trust standing and time, the parts that come from Nostoi, WeftMark, Frog and Ephor and that carry the EU AI Act weight. The trivial form of the artifact is a single self-contained HTML file.
  2. A letter to a named reader. A sender, pseudonymous or named with keys and vouching attestations, writes a letter, states the intended recipients in clear text, and gets a secret link. Whoever holds the link may read it. The sender can see that it was opened, can see a read receipt, and can see that a reply was started.

2. Most of this already exists, and that is the finding that matters

The inventory was taken before any design. The gap is much smaller than it looks.

The askAlready presentWhere
a published artifactscrolls, kinds dialogdoc/dialogue/document/page/image/othersrc/scrolls.js:29
reachable by link but absent from every listingaudience = 'unlisted', filtered out of listScrolls(), the listings and the home page, still served by idsrc/scrolls.js:105-111
a letter, renderedrenderPublishedLetter, a letter to the curatorsrc/render.js:405-414
an unlisted route that refuses to confirm itself/correspondence/inbox answers 404, not 403, to non-curatorssrc/worker.js:186
a signed, append-only log of every state changechain_records, nostoi-v1, appendWith in the same db.batchsrc/chain.js:32-42
signature, trust and time as three separate questionsstated as an invariant, rendered as three blocks on /attestation/<id>src/attestations.js:5-24, src/render.js:447-492
attestations from third parties, possibly mutualtrust_claims kinds witness.authorship, witness.credit_match, author.delegates, librarian.authorship, operator.ratifies, operator.trusts_librarianmigrations/0008_trust_claims.sql
revocations, with effective periodstrust_revocations, half-open periods, bitemporal fold()src/trust.js, test/trust.test.mjs
a secret token whose hash is storednewSessionToken() returns a token and a hash, stores only the hash; timingSafeEqual comparessrc/identity/crypto.js:83-114

The largest gap is documented in the code itself. src/trust.js:1-4 says no public ingestion route uses this module yet. No file in src/ queries trust_claims; only the tests call it. The provenance resolver exists, is tested, and is unwired. Slice 1 is connecting it.

3. What is genuinely missing

  1. A bearer secret for a scroll. An unlisted scroll is addressed by 0x plus namespace plus content digest. That is not a secret: the id is a function of the publication record, so anyone holding the manifest can compute it. A real secret link needs a random token, stored only as a hash, compared in constant time. The session pattern is already proven.
  2. A visit log. Nothing in the project records a visitor. See section 5.
  3. Read receipt and reply-draft state. Neither exists. Both are state changes on someone else’s object, so invariant 2 applies: chain them, in the same db.batch.
  4. Recipients in clear text. scrolls.audience is an enum and there is no free-text addressee field.
  5. A provenance page. The data is there; the view is not.

4. Vocabulary, so we do not invent nouns

5. Three things we cannot settle alone

5.1 Logging an IP collides with a written invariant

The request is to log times, ip, known sessions and the like. The codebase has twice declined this, and the evidence is concrete rather than a matter of tone:

So this is not a wall to argue with; it is a decision the project has already made twice, and adding an IP log would reverse it.

Options, cheapest first:

Recommendation: (a).

5.2 A reply draft is not a read receipt

The request, corrected: not watching a recipient write, but letting one part of a dialogue say how far a plausible replier has come, so the other part can calibrate. The states suggested are open, skimmed, read-and-will-read-again, read-and-will-reply-today. That is a different thing from a read receipt, and the difference is who chooses.

A read receipt is a fact about the past: this reader reached the end. A progress note is a statement of intent about the future, offered by the reader, and it is the only kind of signal here that can be honest. Sending one unasked is surveillance; offering one is courtesy. So the mechanism is not a button the sender polls, it is a message the reader sends.

The states, and what each actually promises:

the reader saysit meansit does not mean
openedthe link worked and something renderedthat a word was read
skimmingpartway, in and out, not yet a positionthat attention was low
readreached the end at least oncethat it was understood or agreed with
read, will read againforming a view, not ready to answerthat a reply is coming soon
read, will replya reply is intendedwhen, or whether, it arrives
will not replythe answer is no, and that is the answeranything at all about the reader

No state names a day. The first draft of this table had read, will reply today, and it was wrong twice over. It was self-contradictory, since the phrase and the day together promised exactly what the neighbouring column denied; and a state that carries a date turns a courtesy into a debt. The reader would be making a promise to a stranger’s expectations at the moment they least have the information to keep it, and the sender would be holding a date rather than a disposition. Timeframes belong to the reply, where the person who can honour them writes them down. will reply says what is true and costs nothing to say: it is the reader telling the sender how to weigh the silence.

This is the same rule as the hour-floor in the fourth point below, applied to the other side. There, the museum coarsens what it observes; here the vocabulary declines to manufacture what was never observed. A signal that has been smoothed on one edge and sharpened on the other is not honest, it is just quiet.

Four rules, and the reasons are the design:

  1. The reader sends; the sender never polls for it. A pollable flag is a hidden camera. This is the answer to the failure mode in the original worry, and it costs one POST.
  2. Each state replaces the last; nothing is appended. A reader who moves from skimming to read has not made two statements, they have made one. The chain keeps the whole history for honesty, the sender’s view shows only the latest.
  3. It is a first-class chain record, not a counter. scroll.state_changed with the state in the body. It is evidence about the dialogue, so it belongs where evidence lives, and because it is chained, invariant 2 is satisfied for free.
  4. No timestamps finer than the hour, and never in the reader’s view. The reader must be able to see exactly what the sender sees, including the coarsening. A receipt the reader cannot inspect is not informed consent, it is a surprise.

One asymmetry worth keeping. opened is the only state the museum can observe by itself, and even that it observes as a sighting with no state attached. The other five are given. If the reader never gives anything, the sender learns that the link was opened and nothing more, which is the correct default and also the honest one.

Where this contradicts the invariants, and how it does not. Naming intended recipients in clear text plus a per-link sighting log is a de-anonymisation surface for the reader, not the sender, so invariant 4 is untouched as long as the log records no address (see 5.1) and the reply never carries a byline the sender has not already been given.

5.3 Elias.Venn has no licence

tabenius/Elias.Venn is private and has no LICENSE file, so default copyright applies and all rights are reserved. Since you are the author this is administrative rather than legal, but it should be a deliberate act rather than an accident of omission, and the siblings differ: weftmark is Apache-2.0, while nostoi and BAZ.AI-Governance are MIT.

Recommendation: add an explicit licence to Elias.Venn before anything is copied out of it, and prefer extending Mouseion, which already adopted the Elias.Venn identity layer and re-termed article to scroll, over forking a second copy. A separate editors repository earns its keep only when a second consumer actually appears; a copy made for the future is a fork with extra steps and no second user.

6. The slices

Slice 1, the provenance view. Unblocked, no decision needed, no migration. Wire src/trust.js to a public page: claims, effective revocations, the chain records they point at, and the attestations, rendered as the three separated facts the project already distinguishes. This follows doc/03-Attestation_federation.md:170-173, whose checklist items 5 to 8 are unchecked and which already specifies that public artifact views list claims and effective revocations, rendering all untrusted strings as escaped text. Gate: a test that a claim appears, a revocation removes it, and nothing renders unescaped.

Slice 2, the secret link. Blocked on 5.1. A scroll_seals table with scroll_id, key_hash, created_at, created_by, revoked_at. A route of the form /s/<token>/<slug> added before the fallthrough, noindex, absent from the nav at src/render.js:67, answering 404 rather than 403 when the token is wrong, primary host only. Each successful read appends a sighting record to the chain. Gate: a wrong token is indistinguishable from a nonexistent scroll, the raw token is never stored, and a revoked seal kills the link.

Slice 3, the reader’s own state of mind. Blocked on 5.1. POST handlers must be matched before pages(), which returns null for anything but GET and HEAD (src/worker.js:79), so they belong in /api next to the existing scroll mutations. The reader posts one of the six states in 5.2; the museum appends scroll.state_changed carrying that state and never infers one. The sender’s view is a page on the scroll, never on the reader’s link, and it shows only the latest state. Gate: a chosen state survives a reload, is chained, a second choice replaces rather than appends, and nothing identifying the reader is written beyond what 5.1 allows.

Slice 4, a self-contained HTML artifact. A scroll kind that renders to one HTML file with its CSS inlined and no external requests, so the artifact can be handed over as a single file. /leave already exists to make leaving the museum explicit, and the raw origin already serves scroll bytes unframed. Gate: the file renders identically with the network off.

7. What we are not doing yet

8. Open questions for the roadmap

  1. Does a letters-to-named-people feature belong in Mouseion at all, given invariant 4? A letter names its recipient, and a pseudonym byline must stay unconnected even by inference from who was addressed.
  2. Should the provenance view be one page per scroll or one global page for the whole museum? The chain is global; claims are per subject.
  3. gift-manifest.md, the one artifact the Marija handover directory asks for and which does not exist anywhere, is a ragbaz.artifact/v1 object living in ragbaz-cf-template/extensions/artifacts/, not a Mouseion scroll. Recorded here so it is not built twice.

9. A second front door: message.ragbaz.cc

The letter workflow has a different reader and a different risk profile from a scroll. A scroll is looked up by anyone; a letter is handed to one person and must be indistinguishable from nothing at all when it has not been opened. Serving both from one origin means one hostname is both the most public thing here and the most private, and every future host decision becomes a decision about both.

So the message side gets its own front door, message.ragbaz.cc, alongside the existing raw.mouseion.ragbaz.cc. classifyHost in src/origins.js already returns a kind per host (raw, primary, alias, raw-alias, or null for an unknown host, which is the right default), so a message kind is one more case and no new mechanism. What changes:

9.1 Skins belong to the sender, not the reader

The first draft of this section made a skin a reader-side chrome, which is wrong: a skin is what an author chooses when writing or editing online. Two things hang off it, and they are separable, which is why the word earns its place:

  1. A stylesheet. The presentation the author writes in and the reader first meets. Papyrus, a plain sheet, and whatever else earns its place. Presentation only: it cannot change what is stored, what is chained, or who may read it.
  2. An editor. The editing surface itself, with more to come. This is the Elias.Venn question, and it is now the load-bearing half rather than a nice-to-have, because online authoring is where a sender actually spends time.

So there are two seams, not one, and conflating them is how a message system ends up with a theme picker where the editor should be:

seambelongs tovariesmust never vary
skinthe sender, at authoring timestylesheet, template, editorwhat is stored, chained, or readable
front doorthe reader, at the hostnamewhich surface is servedthe data behind it

Rules that keep the skin seam from becoming a leak or a fork:

  1. A skin is data, resolved through one lookup. A closed registry, an unknown id falling back to the default rather than erroring. A third editor is a new entry, not a refactor.
  2. A skin cannot widen anything. It selects presentation. It cannot add a route, widen an audience, or reveal a field the underlying data does not already carry for that reader.
  3. The choice is recorded, not implied. Which skin a scroll was written in belongs in the publication metadata and therefore in the chain, so the artifact’s appearance is as checkable as its bytes.
  4. It is not a query parameter. The sender’s choice is stored; a reader cannot switch skins to impersonate a different surface.

This raises the stakes on 5.3. An editor is not a nice-to-have import, it is the authoring surface the sender uses, and tabenius/Elias.Venn still has no LICENSE. Adopting its roughly 140 KB of block-editor code under default copyright is not a shortcut; it needs the licence added first, deliberately.

10. What kind of thing is a scroll, for a person who is not a publisher

The museum’s own first line already says it: *things made by people, by agents and by the two together.* This RFC records what that requires in code, because two of the three cases are not yet properly served and one of them is the most interesting.

Already fine. A hand-written HTML scroll publishes, stores, chains and displays exactly like any other. It needs no model, no harness and no provenance: generation is absent and the colophon says so. The compiled single file (src/single-file.js, route /single/<id>) is precisely the artifact this audience wants: one file, opens anywhere, renders with the network off, and the reader can hash it and compare it against the manifest.

Gap one: the colophon cannot say “no model was involved”. provenanceHtml renders an absent generation as Not recorded, which is ambiguous between written by a person and *we do not know*. Those are different claims and only the publisher can make the second. The fix is a distinction, not a default: absence of generation is the museum saying it holds no model record, never that a model was or was not used. A publisher who wants to assert authorship by hand says so with an author.authorship claim, which is signed and checkable.

Gap two: an old-school hand-coded page cannot be edited here yet. Writing HTML by hand in a text editor and publishing the file works. Editing it in the museum means the editor, and the editor means Elias.Venn’s roughly 140 KB of block-editor code, which has no licence (5.3). The single-file compiler is the useful half without it: hand-written in, one file out.

Gap three, and it is a positioning question, not a feature. If the museum takes work written entirely by a person, then the provenance story has to work for a person who will never read a chain. The honest version of the provenance page for that reader is one sentence and a hash, not a table of claims. Whatever the provenance view grows into, it needs a plain-language floor, or it will end up explaining itself to nobody.

Colophon

Deployed
Unavailable locally