aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-19 02:12:47 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-19 02:12:47 +0200
commit20a824118c09af15d6c338db4c9480ffe5cbcdb6 (patch)
tree39fa0c2056b12e3423eefe0cda78cf160ce17131 /packages
parentcc0820de8d8c33560c44bd6a0864de30e33a0109 (diff)
downloadmeshbay-20a824118c09af15d6c338db4c9480ffe5cbcdb6.tar.gz
docs: cite MESHBAY_DESIGN.md and a section instead of the merged notes
The per-feature design notes were merged into docs/MESHBAY_DESIGN.md and deleted from the tree on 2026-09-11, but ~230 comments across the three packages still named them — usually written `docs/musicbay.md §3.2`, as though the file were still in docs/. A reader had to know §16 existed to resolve any of them. They now name the section directly. Every mapping comes from §16, the concordance, which already records where each old section landed: musicbay -> §9.8, mediacenter -> §9.7 for the Videos app and §6.5 where the subject is derived data, photos -> §9.9, auth-confirm -> §3.6, refactoring-search -> §9.11, invite-pairing-v1 -> §3.4, per-node-identity-v1 -> §3.2, captcha -> §7.7, chat-sender-keys -> §4.5, apps/refactor-groups -> §9.1–§9.4, desktop-client-v1 per section. Bare citations of the same documents (`draft-v6 §2.11`, `§4.8`, `§3.4`) are retargeted too: those collide with real section numbers in the design document, so leaving them would have been worse than the named form. Four cases the concordance does not cover, each decided rather than guessed: Sub-item references into documents that no longer exist — mediacenter's `§3.3 row 4`, `§3.4b/c`, `apps.md §3b` — name rows and sub-items §9.7 and §9.2 do not reproduce. The module-level citation stays; the sub-item pointer is dropped. The V-findings keep their labels but lose the dead `§10.1/` prefix. §13.8 lists V1–V13 as per-application open items, which is not what the labels mean in these comments, so pointing them at §13.8 would have been a false citation. `apps.md §5`'s virtualization requirement has no counterpart anywhere in the design document. The requirement is stated in the comment itself, so the citation is dropped rather than aimed at a section that does not say it. Comments that attributed a *sentence* to an old note — musicbay's "several thousand files" example, its "what got measured" note, its measured ~11%/~26% cover-art figures, the "original no root, whole shared tree" call — state the fact without attribution now. §9.8 does not contain those sentences and citing it for them would have been wrong. CLAUDE.md's "a reference to a document that no longer exists" row now says the concordance is for git history and out-of-tree material; the code cites sections directly. Verified: 2851 passed, 4 skipped. The 12 errors in the run are the Firefox leg of test_sticky_header.py's browser harness, which is broken at the browser level on this machine — headless Firefox (snap) dies with `[GFX1-]: RenderCompositorSWGL failed mapping default framebuffer`, renders nothing, and the probe exits `{"error": "no measurement"}` after its full 90s wait. Chrome runs the same 12 assertions in 3.2s and passes. Nothing here can affect it: every changed line in style.css is inside a comment. Also checked: ast.parse on every changed .py, `node --check` on every changed .js, the /* */ balance in style.css, and that no changed line exceeds the width its file already used. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'packages')
-rw-r--r--packages/meshbay-client/src/main.js4
-rw-r--r--packages/meshbay-common/src/meshbay_common/__init__.py25
-rw-r--r--packages/meshbay-common/src/meshbay_common/adminop.py4
-rw-r--r--packages/meshbay-common/src/meshbay_common/chatbox.py2
-rw-r--r--packages/meshbay-common/src/meshbay_common/device.py2
-rw-r--r--packages/meshbay-common/src/meshbay_common/join.py2
-rw-r--r--packages/meshbay-common/src/meshbay_common/protocol.py16
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/api/users.py6
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/config.py2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/db/models.py2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/mail.py4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/apps.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/auth-page.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/file-utils.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/files-app.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/group-name.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/group-page.js12
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/helloworld-app-settings.js7
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/helloworld-app.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/hub-client.js11
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/keyderive.js10
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/music-app-settings.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/music-app.js39
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/music-player.js18
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/photos-app-settings.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/photos-app.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/platform.js5
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/profile-page.js4
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/search-page.js8
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/settings-page.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/source-merge.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/style.css12
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/transport.js34
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/video-app-settings.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/video-app.js32
-rw-r--r--packages/meshbay-hub/tests/test_desktop_shell.py11
-rw-r--r--packages/meshbay-hub/tests/test_downloads.py6
-rw-r--r--packages/meshbay-hub/tests/test_helloworld_proves_the_plugin_claim.py2
-rw-r--r--packages/meshbay-hub/tests/test_hook_ordering.py2
-rw-r--r--packages/meshbay-hub/tests/test_password_change.py2
-rw-r--r--packages/meshbay-hub/tests/test_password_reset.py2
-rw-r--r--packages/meshbay-hub/tests/test_recovery_email.py2
-rw-r--r--packages/meshbay-hub/tests/test_recovery_key.py2
-rw-r--r--packages/meshbay-hub/tests/test_rewrap_fanout.py2
-rw-r--r--packages/meshbay-hub/tests/test_search_files_unmerged.py4
-rw-r--r--packages/meshbay-hub/tests/test_search_media_merge.py2
-rw-r--r--packages/meshbay-hub/tests/test_search_source_merge.py2
-rw-r--r--packages/meshbay-hub/tests/test_sticky_band_ring.py7
-rw-r--r--packages/meshbay-hub/tests/test_transport_contracts.py2
-rw-r--r--packages/meshbay-hub/tests/test_zip_size_limit.py2
-rw-r--r--packages/meshbay-node/src/meshbay_node/bundle_store.py4
-rw-r--r--packages/meshbay-node/src/meshbay_node/chat/store.py2
-rw-r--r--packages/meshbay-node/src/meshbay_node/daemon.py77
-rw-r--r--packages/meshbay-node/src/meshbay_node/indexer/enrich.py64
-rw-r--r--packages/meshbay-node/src/meshbay_node/indexer/enrich_audio.py7
-rw-r--r--packages/meshbay-node/src/meshbay_node/indexer/enrich_photo.py7
-rw-r--r--packages/meshbay-node/src/meshbay_node/indexer/indexer.py13
-rw-r--r--packages/meshbay-node/src/meshbay_node/indexer/title_parse.py31
-rw-r--r--packages/meshbay-node/src/meshbay_node/media_cache.py36
-rw-r--r--packages/meshbay-node/src/meshbay_node/media_probe.py2
-rw-r--r--packages/meshbay-node/src/meshbay_node/musicbrainz.py6
-rw-r--r--packages/meshbay-node/src/meshbay_node/ops.py13
-rw-r--r--packages/meshbay-node/src/meshbay_node/roster.py16
-rw-r--r--packages/meshbay-node/src/meshbay_node/tmdb.py10
-rw-r--r--packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py72
-rw-r--r--packages/meshbay-node/src/meshbay_node/ui/app.py2
-rw-r--r--packages/meshbay-node/tests/conftest.py4
-rw-r--r--packages/meshbay-node/tests/test_audio_root_gates_enrichment.py9
-rw-r--r--packages/meshbay-node/tests/test_audio_transcode.py2
-rw-r--r--packages/meshbay-node/tests/test_bundle_store_recovery.py2
-rw-r--r--packages/meshbay-node/tests/test_chat_encryption.py2
-rw-r--r--packages/meshbay-node/tests/test_chat_history_binary.py2
-rw-r--r--packages/meshbay-node/tests/test_chat_multidevice.py5
-rw-r--r--packages/meshbay-node/tests/test_cli_dispatch.py2
-rw-r--r--packages/meshbay-node/tests/test_device_on_connection.py7
-rw-r--r--packages/meshbay-node/tests/test_enrich.py2
-rw-r--r--packages/meshbay-node/tests/test_enrich_photo.py2
-rw-r--r--packages/meshbay-node/tests/test_group_roster.py6
-rw-r--r--packages/meshbay-node/tests/test_media_cache.py2
-rw-r--r--packages/meshbay-node/tests/test_musicbrainz.py5
-rw-r--r--packages/meshbay-node/tests/test_musicbrainz_enabled_policy.py7
-rw-r--r--packages/meshbay-node/tests/test_roster_pairing.py2
-rw-r--r--packages/meshbay-node/tests/test_season_and_search_requests.py3
-rw-r--r--packages/meshbay-node/tests/test_security_regressions.py12
-rw-r--r--packages/meshbay-node/tests/test_startup_scan_enrichment.py4
-rw-r--r--packages/meshbay-node/tests/test_stream_video_transcode.py4
-rw-r--r--packages/meshbay-node/tests/test_title_parse.py12
-rw-r--r--packages/meshbay-node/tests/test_tmdb.py2
-rw-r--r--packages/meshbay-node/tests/test_tmdb_config_policy.py5
-rw-r--r--packages/meshbay-node/tests/test_tmdb_enabled_policy.py2
-rw-r--r--packages/meshbay-node/tests/test_tmdb_rematch_policy.py2
-rw-r--r--packages/meshbay-node/tests/test_tmdb_search_ladder.py4
-rw-r--r--packages/meshbay-node/tests/test_transfer_settings.py3
-rw-r--r--packages/meshbay-node/tests/test_video_root_gates_enrichment.py2
-rw-r--r--packages/meshbay-node/tests/test_webrtc_transport.py3
95 files changed, 431 insertions, 395 deletions
diff --git a/packages/meshbay-client/src/main.js b/packages/meshbay-client/src/main.js
index 5e00d0b..794477b 100644
--- a/packages/meshbay-client/src/main.js
+++ b/packages/meshbay-client/src/main.js
@@ -15,7 +15,7 @@
* That value is realised by reproducible builds and published hashes (18.7),
* not by the packaging format.
*
- * See docs/desktop-client-v1.md §2 and §3.
+ * See docs/MESHBAY_DESIGN.md §8.1, §8.2.
*/
'use strict';
@@ -466,7 +466,7 @@ function secretsBackend() {
//
// Note what this key is *not*: it is not a per-node identity key. Those are
// generated per node, pinned there, and never leave that relationship
-// (docs/per-node-identity-v1.md). Nothing here correlates a person across
+// (docs/MESHBAY_DESIGN.md §3.2). Nothing here correlates a person across
// operators, and nothing wraps a group key for it.
const DEVICE_KEY = 'device_auth_ed25519';
diff --git a/packages/meshbay-common/src/meshbay_common/__init__.py b/packages/meshbay-common/src/meshbay_common/__init__.py
index c5b0279..1d857fd 100644
--- a/packages/meshbay-common/src/meshbay_common/__init__.py
+++ b/packages/meshbay-common/src/meshbay_common/__init__.py
@@ -26,7 +26,8 @@ __version__ = "0.14.0"
# older client simply never sends/handles the new pair.
# 0.8: added `artist`/`album`/`track_no` to `IndexEntry`, and
# `musicbrainz_config`/`_ack`, `musicbrainz_enabled`/`_ack`,
-# `music_meta_req`/`_resp`, for the Music group app (docs/musicbay.md).
+# `music_meta_req`/`_resp`, for the Music group app
+# (docs/MESHBAY_DESIGN.md §9.8).
# Same shape as 0.5-0.7's Videos additions, and reuses `duration`/
# `thumb_hash`/`display_title` rather than declaring new ones. Additive:
# an older client simply doesn't render the new fields or send the new
@@ -38,29 +39,29 @@ __version__ = "0.14.0"
# client never sends the request, and simply can't play those two formats,
# same as before this existed.
# 0.10: added `audio_root`/`_ack` — the Music app's own entry point, same
-# shape as `video_root`/`_ack`. Reverses musicbay.md's original "no root,
-# whole shared tree" call: a real messy library showed that call was
-# wrong, and Music now requires one before showing/enriching anything,
-# exactly like Videos. Additive at the protocol level: an older client
-# never sends the op and never expects the field.
+# shape as `video_root`/`_ack`. Reverses the original "no root, whole
+# shared tree" call: a real messy library showed that call was wrong, and
+# Music now requires one before showing/enriching anything, exactly like
+# Videos (docs/MESHBAY_DESIGN.md §9.8). Additive at the protocol level: an
+# older client never sends the op and never expects the field.
# 0.11: added `taken_at`/`camera` to `IndexEntry` (best-effort, from a photo's
# own EXIF block) and `photo_roots`/`photo_roots_ack`, for the Photos group
-# app (docs/photos.md). Unlike `video_root`/`audio_root`, `photo_roots` is a
-# *set*, replaced whole in one signed op — a photo library is routinely
-# scattered across several folders, not one. Additive: an older client
+# app (docs/MESHBAY_DESIGN.md §9.9). Unlike `video_root`/`audio_root`,
+# `photo_roots` is a *set*, replaced whole in one signed op — a photo library
+# is routinely scattered across several folders, not one. Additive: an older client
# never sends the op and never expects either field.
# 0.12: added `link_preview_req`/`link_preview_resp` — the node unfurls a URL
# pasted in chat into an OpenGraph card. Additive: an older node logs "unknown
# type" and the client just shows the bare link, as it always did.
# 0.13: added `tmdb_rematch`/`tmdb_rematch_ack` — an operator dropping one
# file's cached TMDB match so it re-resolves with the current matcher
-# (§10.1/V13). Additive: an older node logs "unknown type", the client's
+# (V13). Additive: an older node logs "unknown type", the client's
# button just does nothing.
# 0.14: added an optional `bundle_enc_recovery` field on `keypair_bundle_store`
# and `keypair_bundle_resp` — a second copy of the identity bundle wrapped
# under the account's recovery key, so a forgotten passphrase does not strand
-# the identity (docs/auth-confirm.md §4.3). Additive: an older node ignores the
-# field on store and never returns one; an older client never sends it.
+# the identity (docs/MESHBAY_DESIGN.md §3.6). Additive: an older node ignores
+# the field on store and never returns one; an older client never sends it.
# 0.15: `file_chunk` and `index_sync` had forked between the transports — WebRTC
# sent binary, unsigned chunks and plain index entries, QUIC sent base64 chunks
# with a per-chunk Ed25519 signature and a `GroupIndex.serialize()` envelope. One
diff --git a/packages/meshbay-common/src/meshbay_common/adminop.py b/packages/meshbay-common/src/meshbay_common/adminop.py
index 3e9fe11..20b5080 100644
--- a/packages/meshbay-common/src/meshbay_common/adminop.py
+++ b/packages/meshbay-common/src/meshbay_common/adminop.py
@@ -63,7 +63,7 @@ OP_SET_SCAN_SETTINGS = "set_scan_settings"
# shown before signing names the outcome and not the operation.
OP_TRANSFER_LIMITS = "transfer_limits"
# Whether the node uses the operator's own API token/language instead of the
-# shipped default — node-wide (docs/mediacenter.md §5.5), one credential
+# shipped default — node-wide (docs/MESHBAY_DESIGN.md §9.7), one credential
# shared by every group. Signed like the rest: it turns on outbound
# third-party network traffic that did not exist before the Videos app
# (§8) — an unsigned change would let any member alter egress the operator
@@ -119,7 +119,7 @@ OP_GROUP_ATTACH = "group_attach"
OP_GROUP_DETACH = "group_detach"
# OP_GEK_BUNDLE_STORE is gone. Members no longer hand the node key material at
# all: the node holds the GEK and wraps it itself, for a key the recipient proved
-# they hold (see `join.py` and docs/invite-pairing-v1.md). The operation existed
+# they hold (see `join.py` and docs/MESHBAY_DESIGN.md §3.4). The operation existed
# only to make member-supplied bundles safe, and deleting the message is a
# stronger guarantee than authorizing it.
diff --git a/packages/meshbay-common/src/meshbay_common/chatbox.py b/packages/meshbay-common/src/meshbay_common/chatbox.py
index 740e829..f0534fa 100644
--- a/packages/meshbay-common/src/meshbay_common/chatbox.py
+++ b/packages/meshbay-common/src/meshbay_common/chatbox.py
@@ -1,7 +1,7 @@
"""
Chat message encryption and sender authentication.
-Design A of `docs/chat-sender-keys.md`, decided 2026-09-07. What it is, and what
+Design A of `docs/MESHBAY_DESIGN.md` §4.5, decided 2026-09-07. What it is, and what
it deliberately is not, in the order the decisions were made:
**Not a ratchet, and not sender keys.** With per-sender chains distributed under
diff --git a/packages/meshbay-common/src/meshbay_common/device.py b/packages/meshbay-common/src/meshbay_common/device.py
index cfa8dd6..3a598d0 100644
--- a/packages/meshbay-common/src/meshbay_common/device.py
+++ b/packages/meshbay-common/src/meshbay_common/device.py
@@ -27,7 +27,7 @@ Fields are length-prefixed and domain-separated, per L4 — the same rule as
of the connection carrying the message, so neither signature can be lifted onto
another connection, and `node_pk` binds an authorization to one node.
-See `docs/desktop-client-v1.md` §4.
+See `docs/MESHBAY_DESIGN.md` §3.3.
"""
from __future__ import annotations
diff --git a/packages/meshbay-common/src/meshbay_common/join.py b/packages/meshbay-common/src/meshbay_common/join.py
index 6ee543f..84ea18a 100644
--- a/packages/meshbay-common/src/meshbay_common/join.py
+++ b/packages/meshbay-common/src/meshbay_common/join.py
@@ -11,7 +11,7 @@ from the hub and wrap the group key for whatever came back. The hub is the key
directory, so a hub answering with its own key was handed the GEK by an honest
inviter following the protocol exactly. The key now comes from the peer over an
authenticated channel and is bound to an identity by a one-time pairing code the
-hub never sees. See `docs/invite-pairing-v1.md`.
+hub never sees. See `docs/MESHBAY_DESIGN.md` §3.4.
Fields are length-prefixed and domain-separated, per L4 — the same rule as
`handshake.py` and `adminop.py`. `nonce_node` is the handshake nonce the node just
diff --git a/packages/meshbay-common/src/meshbay_common/protocol.py b/packages/meshbay-common/src/meshbay_common/protocol.py
index c3cb099..c444711 100644
--- a/packages/meshbay-common/src/meshbay_common/protocol.py
+++ b/packages/meshbay-common/src/meshbay_common/protocol.py
@@ -69,11 +69,11 @@ class MNP:
# never once invoked. A live handler on both transports, plaintext media,
# and no client: removed rather than repaired.
#
- # Not a Double Ratchet message, and never was — `first-review.md` C1
- # rejected exactly that for groups. Since MNP 2.0 it is AES-256-GCM under a
+ # Not a Double Ratchet message, and never was — finding C1
+ # (`docs/MESHBAY_DESIGN.md` §13.1) rejected exactly that for groups. Since MNP 2.0 it is AES-256-GCM under a
# per-device subkey of the group's chat epoch key, signed over the
# ciphertext with the sending device's pinned Ed25519 key. There is no
- # plaintext form on the wire (`chatbox.py`, docs/chat-sender-keys.md);
+ # plaintext form on the wire (`chatbox.py`, docs/MESHBAY_DESIGN.md §4.5);
# `format` distinguishes a *stored* pre-2.0 row, which is still served.
CHAT_MESSAGE = "chat_msg" # one chat message, sealed and signed
CHAT_ATTACHMENT = "chat_attach" # attachment metadata
@@ -136,7 +136,7 @@ class MNP:
GEK_BUNDLE_RESP = "gek_bundle_resp" # node → client: wrapped GEK bundle
KEYPAIR_BUNDLE_STORE = "keypair_bundle_store" # client → node: store encrypted keypair bundle
# optional `bundle_enc_recovery` (MNP 0.14): a second copy wrapped under the
- # account's recovery key (docs/auth-confirm.md §4.3)
+ # account's recovery key (docs/MESHBAY_DESIGN.md §3.6)
KEYPAIR_BUNDLE_FETCH = "keypair_bundle_fetch" # client → node: request own keypair bundle
KEYPAIR_BUNDLE_RESP = "keypair_bundle_resp" # node → client: encrypted keypair bundle
# carries `bundle_enc_recovery` too when the node has one stored
@@ -178,9 +178,9 @@ class MNP:
TMDB_OVERRIDE_ACK = "tmdb_override_ack"
TMDB_REMATCH = "tmdb_rematch" # operator → node: drop one file's match
TMDB_REMATCH_ACK = "tmdb_rematch_ack"
- # Music app (docs/musicbay.md). Contact is derived from the owner's hub
- # email at login — no config/ack pair needed. Only the per-group toggle
- # remains.
+ # Music app (docs/MESHBAY_DESIGN.md §9.8). Contact is derived from the
+ # owner's hub email at login — no config/ack pair needed. Only the
+ # per-group toggle remains.
MUSICBRAINZ_ENABLED = "musicbrainz_enabled" # operator → node: enable/disable
MUSICBRAINZ_ENABLED_ACK = "musicbrainz_enabled_ack" # node → this group: new enabled state
MUSIC_META_REQ = "music_meta_req" # client → node: metadata for a path
@@ -268,7 +268,7 @@ class MNP:
# below, which is the operator's view of the whole node: this is scoped to
# one group and answers any member of it, because the point is that a member
# verifies another member's device *for themselves* rather than trusting the
- # node's `sender_id` (Tier 2, docs/desktop-client-v1.md §4.8).
+ # node's `sender_id` (Tier 2, docs/MESHBAY_DESIGN.md §3.3).
GROUP_ROSTER_REQ = "group_roster_req"
GROUP_ROSTER_RESP = "group_roster_resp"
ROSTER_READ = "roster_read" # operator → node: list pinned identities + members
diff --git a/packages/meshbay-hub/src/meshbay_hub/api/users.py b/packages/meshbay-hub/src/meshbay_hub/api/users.py
index 7b2fc8c..0394f53 100644
--- a/packages/meshbay-hub/src/meshbay_hub/api/users.py
+++ b/packages/meshbay-hub/src/meshbay_hub/api/users.py
@@ -107,7 +107,7 @@ class RegisterRequest(BaseModel):
email: str
password: str | None = None # deprecated — legacy native clients
auth_key: str | None = None # PBKDF2-derived, new clients
- # Client-generated account recovery key (docs/auth-confirm.md §4.4). Pure
+ # Client-generated account recovery key (docs/MESHBAY_DESIGN.md §3.6). Pure
# pass-through: appended to the verification e-mail so the user's mailbox
# backs it up, then dropped. Never written to any table, never logged.
recovery_key: str | None = None
@@ -901,7 +901,7 @@ async def verify_email_change(
# ── Passphrase change (Flow A) ──────────────────────────────────────────────
#
-# docs/auth-confirm.md §3. The passphrase derives two independent values on the
+# docs/MESHBAY_DESIGN.md §3.6. The passphrase derives two independent values on the
# client: auth_key (verified here) and bundle_key (AES-GCM key for the per-node
# identity bundles, which live on nodes and never on the hub). The client
# re-wraps those bundles from the old bundle_key to the new one on every
@@ -1023,7 +1023,7 @@ async def change_password(
# ── Passphrase reset (Flow B) ──────────────────────────────────────────────
#
-# docs/auth-confirm.md §4.2. An e-mail code re-opens hub login for someone who
+# docs/MESHBAY_DESIGN.md §3.6. An e-mail code re-opens hub login for someone who
# has lost their passphrase. It recovers no group content — that needs the
# recovery key, which the client applies on its own after the reset.
# reset-request never reveals whether an account exists.
diff --git a/packages/meshbay-hub/src/meshbay_hub/config.py b/packages/meshbay-hub/src/meshbay_hub/config.py
index 876583e..f2212de 100644
--- a/packages/meshbay-hub/src/meshbay_hub/config.py
+++ b/packages/meshbay-hub/src/meshbay_hub/config.py
@@ -75,7 +75,7 @@ class CaptchaConfig:
# Set this when that check is turned off in the reCAPTCHA console, which is
# what the desktop client needs: its page is served from `app://meshbay`,
# so the hostname Google sees is not the hub's and never can be. See
- # docs/captcha.md §6.
+ # docs/MESHBAY_DESIGN.md §7.7.
allowed_hosts: list[str] = field(default_factory=list)
# A solve from a page Google cannot attribute to a domain reports an empty
# hostname — `app://meshbay` does, measured live, and so does any other
diff --git a/packages/meshbay-hub/src/meshbay_hub/db/models.py b/packages/meshbay-hub/src/meshbay_hub/db/models.py
index f1fff41..62e4a11 100644
--- a/packages/meshbay-hub/src/meshbay_hub/db/models.py
+++ b/packages/meshbay-hub/src/meshbay_hub/db/models.py
@@ -201,7 +201,7 @@ class UserDevice(Base):
directory *others* read from, where a substituted key was handed the
GEK by an honest member.
* **It is not a node identity key.** Those are generated per node, pinned
- there, and never leave that relationship (`docs/per-node-identity-v1.md`).
+ there, and never leave that relationship (`docs/MESHBAY_DESIGN.md` §3.2).
A device holds one of these *plus* a different key per node, so nothing
here correlates a person across operators.
diff --git a/packages/meshbay-hub/src/meshbay_hub/mail.py b/packages/meshbay-hub/src/meshbay_hub/mail.py
index aa6b74e..126ed45 100644
--- a/packages/meshbay-hub/src/meshbay_hub/mail.py
+++ b/packages/meshbay-hub/src/meshbay_hub/mail.py
@@ -315,7 +315,7 @@ def send_verification_code(to: str, code: str, recovery_key: str | None = None)
"""
Registration verification e-mail. When `recovery_key` is given it is
appended to the body so the recipient's mailbox becomes the backup for it
- (docs/auth-confirm.md §4.4).
+ (docs/MESHBAY_DESIGN.md §3.6).
`recovery_key` is a **pass-through**: it is generated on the client, never
stored anywhere on the hub, and never logged — only whether one was present.
@@ -374,7 +374,7 @@ def send_email_change_code(to: str, code: str) -> None:
def send_password_reset_code(to: str, code: str) -> None:
"""
- Passphrase-reset code (docs/auth-confirm.md §4.2). This only re-opens hub
+ Passphrase-reset code (docs/MESHBAY_DESIGN.md §3.6). This only re-opens hub
login; it recovers no group content — that needs the recovery key.
"""
msg = EmailMessage()
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/apps.js b/packages/meshbay-hub/src/meshbay_hub/static/apps.js
index 461bd57..32f2314 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/apps.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/apps.js
@@ -70,7 +70,7 @@ const APPS = [
Component: MusicApp, Settings: MusicSettings },
{ key: 'photo', icon: 'image', labelKey: 'group.tab_photos',
Component: PhotosApp, Settings: PhotoSettings },
- // The reference implementation (docs/refactor-groups.md §4.1). `dev` keeps
+ // The reference implementation (docs/MESHBAY_DESIGN.md §9.4). `dev` keeps
// it out of an operator's way; everything else about it is an ordinary
// entry, which is the point.
{ key: 'helloworld', icon: 'chat', labelKey: 'group.tab_helloworld',
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js b/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js
index 04a00af..8958d44 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js
@@ -301,7 +301,7 @@ export function RegisterPage() {
setLoading(true);
try {
if (window.MeshBayKeys) {
- // The account recovery key (docs/auth-confirm.md §4.3/§4.4): generated
+ // The account recovery key (docs/MESHBAY_DESIGN.md §3.6): generated
// here, shown once on the next screen. When the user leaves "e-mail it"
// checked, the mnemonic goes in the register body so the hub appends it
// to the verification e-mail (and stores it nowhere); otherwise it is
@@ -511,7 +511,7 @@ export function RegisterPage() {
}
-// ── Passphrase reset — Flow B (docs/auth-confirm.md §4) ─────────────────────
+// ── Passphrase reset — Flow B (docs/MESHBAY_DESIGN.md §3.6) ─────────────────
//
// An e-mail code restores hub login. A recovery key, if the user still has one,
// restores the per-node identities in the same step: the fan-out reads each
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js b/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js
index b732734..decf916 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/file-utils.js
@@ -507,11 +507,11 @@ async function downloadEntry(transfers, transport, gek, entry) {
* only alternative is to build the whole thing in memory — so that path is
* offered but says what it costs first.
*
- * Lifted out of files-app.js (docs/photos.md §3) so photos-app.js's own
+ * Lifted out of files-app.js (docs/MESHBAY_DESIGN.md §9.9) so photos-app.js's own
* "zip this album" button calls the same implementation rather than a
* second one — nothing here is Files-specific once `entries`/`transport`/
* `gek`/`setError` are passed in, the same shared-context shape every app
- * already receives (apps.md §2).
+ * already receives (docs/MESHBAY_DESIGN.md §9.2).
*/
async function downloadDirectory(transfers, transport, gek, entries, dir, { setError }) {
if (!transport || !transport.connected) return;
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/files-app.js b/packages/meshbay-hub/src/meshbay_hub/static/files-app.js
index 7629459..3f4212e 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/files-app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/files-app.js
@@ -314,7 +314,7 @@ function FilesPanel({
}
}, [currentPath]);
- // The implementation lives in file-utils.js (docs/photos.md §3) so
+ // The implementation lives in file-utils.js (docs/MESHBAY_DESIGN.md §9.9) so
// photos-app.js's own "zip this album" button can call the same code
// rather than a second one.
const downloadDirectory = useCallback(async (dir) => {
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-name.js b/packages/meshbay-hub/src/meshbay_hub/static/group-name.js
index f109da7..f0ace6a 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/group-name.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/group-name.js
@@ -8,7 +8,7 @@ import { sourceLabel } from './source-merge.js';
* Group names are unique only per owner account (the hub enforces that), so the
* `@handle` is what tells two groups called "photos" apart. `owner` is the
* owner's username for a local group, or the source hub for a federated one
- * (decision 5 in ~/next/groupnames.md); the caller decides which.
+ * (docs/MESHBAY_DESIGN.md §7.3); the caller decides which.
*
* `inline` renders "name@owner" on one line, for places that cannot take a
* block — a badge, a `confirm()` string built elsewhere.
@@ -29,7 +29,7 @@ export function GroupName({ name, owner, inline = false }) {
* The Search view merges a file several groups share into one entry, so the
* badge under a card cannot always name a group. One source keeps naming it
* and keeps linking to it; more than one becomes a count, and *which* one was
- * picked is deliberately not shown (docs/refactoring-search.md §5.5).
+ * picked is deliberately not shown (docs/MESHBAY_DESIGN.md §9.11).
*
* `entries` is the whole unit — every episode of a show, every track of an
* album — not the entry the card was drawn from; `sourceLabel` explains why.
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
index 205fce2..9a86f2a 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/group-page.js
@@ -154,10 +154,10 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
// that). Null until the handshake ack arrives.
const [scanSettings, setScanSettings] = useState(null);
// TMDB on/off + whether a custom token is set, node-wide (not per-group) —
- // docs/mediacenter.md §5.5. Null until the handshake ack arrives.
+ // docs/MESHBAY_DESIGN.md §9.7. Null until the handshake ack arrives.
const [tmdbConfig, setTmdbConfig] = useState(null);
// Which folders each app works over. One shape for all of them — a list,
- // always, even where an app only wants one (docs/refactor-groups.md §1.6):
+ // always, even where an app only wants one (docs/MESHBAY_DESIGN.md §9.3):
// Videos and Music were single values, which meant a library spread over two
// drives could not be described at all. Empty means nothing configured yet,
// which every app reads as "show nothing", never "the whole group index".
@@ -171,7 +171,7 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
// Whether members' cross-group Search lists this group. Not an app setting:
// it is about the group as a whole, and it hides nothing from this page.
const [searchListed, setSearchListed] = useState(true);
- // MusicBrainz on/off (per-group) — docs/musicbay.md §3.2.
+ // MusicBrainz on/off (per-group) — docs/MESHBAY_DESIGN.md §9.8.
const [musicbrainzConfig, setMusicbrainzConfig] = useState(null);
// `op` rides through to the shell — 'replace', 'next' or 'append'
// (docs/playlists.md §9.2). It has to be named here: a wrapper that takes
@@ -320,7 +320,7 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
setDeviceReady(false);
gekRef.current = null;
if (!session.bundleKey) session.bundleKey = await _loadBundleKey();
- // Persisted (docs/auth-confirm.md §4.3) so a group joined in a later
+ // Persisted (docs/MESHBAY_DESIGN.md §3.6) so a group joined in a later
// session still leaves a recovery-wrapped identity copy on its node.
if (!session.recoveryKey) session.recoveryKey = await _loadRecoveryKey();
if (!session.bundleKey && window.MeshBayKeys) {
@@ -669,7 +669,7 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
// the only unambiguous answer once a group can have several writable roots.
// Chat has no folder to browse, so it needs one picked for it, and this is
// the same rule the node applies when a client names no root at all. It
- // becomes an operator-chosen directory in phase 2 (refactor-groups.md §1.7).
+ // becomes an operator-chosen directory in phase 2 (docs/MESHBAY_DESIGN.md §9.6).
//
const writableRoots = useMemo(
() => nodeRoots.filter((r) => r.writable && r.available !== false),
@@ -735,7 +735,7 @@ function GroupPage({ groupId, group, token, username, userId, userPrefs,
// rather than written out. Naming them here would mean adding an app
// required editing this file, which is the one thing the plugin
// architecture is supposed to have removed — and the reference app
- // (docs/refactor-groups.md §4.1) is what made the difference visible.
+ // (docs/MESHBAY_DESIGN.md §9.4) is what made the difference visible.
const perAppDirectories = useMemo(() => {
const out = {};
for (const app of APPS) out[`${app.key}Directories`] = appDirs(app.key);
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/helloworld-app-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/helloworld-app-settings.js
index 9058199..be26fbc 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/helloworld-app-settings.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/helloworld-app-settings.js
@@ -11,9 +11,10 @@ import { FolderPickerField } from './folder-tree.js';
* app's registry key by the page, and `ops.set_app_directories` stores the row
* under that name without knowing what it is.
*
- * It takes the shared props and no others (`docs/apps.md` §3b), which is what
- * `test_app_settings_plugin.py` checks of every pane — including this one, so
- * the reference implementation is held to the contract it demonstrates.
+ * It takes the shared props and no others (`docs/MESHBAY_DESIGN.md` §9.2),
+ * which is what `test_app_settings_plugin.py` checks of every pane — including
+ * this one, so the reference implementation is held to the contract it
+ * demonstrates.
*/
function HelloWorldSettings({ roots, dirs, settings, saveDirectories }) {
const { busy, msg, run } = useSaver();
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/helloworld-app.js b/packages/meshbay-hub/src/meshbay_hub/static/helloworld-app.js
index 01b4ed4..98ca8fa 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/helloworld-app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/helloworld-app.js
@@ -7,7 +7,7 @@ import { formatSize } from './file-utils.js';
* The smallest application this platform can host, and the proof that adding
* one costs nothing outside its own two files.
*
- * Everything else in `docs/refactor-groups.md` §3 is asserted by tests that
+ * Everything else in `docs/MESHBAY_DESIGN.md` §9.4 is asserted by tests that
* read source. This is the other kind of evidence: an app nobody wrote a line
* of plumbing for, that stores directories, appears as a tab, and lists files —
* because the registry entry beside it is genuinely all there is.
@@ -18,7 +18,7 @@ import { formatSize } from './file-utils.js';
* put a toy app in everybody's group; deleting the app would leave the claim
* resting entirely on tests that read text.
*
- * It takes the standard props — see `docs/apps.md` §2 — and reads
+ * It takes the standard props — see `docs/MESHBAY_DESIGN.md` §9.2 — and reads
* `helloworldDirectories`, which nothing on the node knows about by name:
* `ops.set_app_directories` keys the row by whatever the app is called.
*/
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/hub-client.js b/packages/meshbay-hub/src/meshbay_hub/static/hub-client.js
index 81185a5..f467720 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/hub-client.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/hub-client.js
@@ -136,7 +136,7 @@ async function clearAllCachedIndexes() {
// so importing modules can update either field without this module handing
// out a rebindable export.
//
-// `recoveryKey` (docs/auth-confirm.md §4.3) is the AES key that wraps the
+// `recoveryKey` (docs/MESHBAY_DESIGN.md §3.6) is the AES key that wraps the
// *recovery* copy of an identity bundle. In-memory only, and set only when the
// user has just generated or entered the recovery secret (registration, or the
// Flow B screen) — it cannot be re-derived from the passphrase.
@@ -169,10 +169,11 @@ async function _loadKey(slot) {
return val || null;
} catch { return null; }
}
-// 'bk' = passphrase-derived bundle key; 'rk' = recovery key (docs/auth-confirm.md
-// §4.3). Persisting 'rk' is what lets a group joined in a *later* session still
-// get a recovery-wrapped identity copy, instead of only groups joined in the
-// unbroken session that generated it. Cleared with everything else on sign-out.
+// 'bk' = passphrase-derived bundle key; 'rk' = recovery key
+// (docs/MESHBAY_DESIGN.md §3.6). Persisting 'rk' is what lets a group joined
+// in a *later* session still get a recovery-wrapped identity copy, instead of
+// only groups joined in the unbroken session that generated it. Cleared with
+// everything else on sign-out.
const _storeBundleKey = (key) => _storeKey('bk', key);
const _loadBundleKey = () => _loadKey('bk');
const _storeRecoveryKey = (key) => _storeKey('rk', key);
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/keyderive.js b/packages/meshbay-hub/src/meshbay_hub/static/keyderive.js
index edfa109..879f56f 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/keyderive.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/keyderive.js
@@ -191,7 +191,7 @@ async function deriveBundleKeys(password, username) {
// ── Account recovery key ─────────────────────────────────────────────────────
//
-// docs/auth-confirm.md §4.3. A full-entropy secret the user keeps outside the
+// docs/MESHBAY_DESIGN.md §3.6. A full-entropy secret the user keeps outside the
// passphrase — in their password manager, or (step 3) e-mailed to them. It
// wraps a *second* copy of every per-node identity bundle, so a forgotten
// passphrase does not strand the account's group identities.
@@ -323,7 +323,7 @@ async function registerUser(username, email, password, recoveryMnemonic, captcha
const payload = { username, email, auth_key: authKey };
// The recovery mnemonic, when the user opted to have it e-mailed: the hub
// appends it to the verification e-mail and stores it nowhere
- // (docs/auth-confirm.md §4.4). Omitted when they chose to save it themselves.
+ // (docs/MESHBAY_DESIGN.md §3.6). Omitted when they chose to save it themselves.
if (recoveryMnemonic) payload.recovery_key = recoveryMnemonic;
// reCAPTCHA response, when the hub has a captcha configured. The widget lives
// in RegisterPage (auth-page.js); this function just forwards its token. A
@@ -347,7 +347,7 @@ async function registerUser(username, email, password, recoveryMnemonic, captcha
* bundle goes to that node and nowhere else, and is what any other browser
* fetches to become the same person there. When `recoveryKey` is supplied a
* second copy wrapped under it rides along, so a forgotten passphrase does not
- * strand this identity (docs/auth-confirm.md §4.3).
+ * strand this identity (docs/MESHBAY_DESIGN.md §3.6).
*/
async function generateNodeIdentity(bundleKey, recoveryKey) {
const { skEdRaw, pkEdRaw, skXRaw, pkXRaw } = await generateKeypairs();
@@ -465,7 +465,7 @@ async function _bundleKeyPairFields(password, username) {
window.MeshBayKeys = {
registerUser, loginAndRecover, generateNodeIdentity, generateKeypairs, signBytes,
deriveAuthKey, decryptBundleWithKey, encryptBundleWithKey, bundleVersion,
- // Exposed for the passphrase change (docs/auth-confirm.md §3): re-wrapping a
+ // Exposed for the passphrase change (docs/MESHBAY_DESIGN.md §3.6): re-wrapping a
// node's identity bundle needs the old key (a {v2,v1} pair, since an old
// bundle may be v1) to read it and the new v2 key to write it back.
deriveEncryptionKey, deriveEncryptionKeyV1,
@@ -473,6 +473,6 @@ window.MeshBayKeys = {
// `session.bundleKey` uses this, so `v2hkdf` is never the field one sign-in
// path forgot (docs/playlists.md §3.4).
deriveBundleKeys, bundleKeyPairFields: _bundleKeyPairFields,
- // Account recovery key (docs/auth-confirm.md §4.3).
+ // Account recovery key (docs/MESHBAY_DESIGN.md §3.6).
generateRecoveryKey, deriveRecoveryKey,
};
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/music-app-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/music-app-settings.js
index 8ce2ab6..a9a3c95 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/music-app-settings.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/music-app-settings.js
@@ -8,7 +8,7 @@ import { FolderPickerField } from './folder-tree.js';
*
* Same shape as Videos, minus a credential: MusicBrainz's read endpoints need
* no API key, only a descriptive User-Agent, and that is one operator identity
- * held node-wide rather than a per-group setting (docs/musicbay.md §3.2).
+ * held node-wide rather than a per-group setting (docs/MESHBAY_DESIGN.md §9.8).
*
* Several folders, for the same reason Videos has several: a music library
* that lives on two drives had no way to say so.
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/music-app.js b/packages/meshbay-hub/src/meshbay_hub/static/music-app.js
index baa3520..e0308cb 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/music-app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/music-app.js
@@ -15,13 +15,13 @@ import * as P from './playlists.js';
//
// An album-grid (MusicBrainz-enriched, when a track has no usable embedded
// cover) or flat (tag/filename-only) browser for a group's audio files, per
-// docs/musicbay.md. Grouping is by `artist`/`album` -- already resolved at
-// index time from embedded tags, falling back to filename/folder parsing
-// (indexer/enrich_audio.py) -- never guessed here.
+// docs/MESHBAY_DESIGN.md §9.8. Grouping is by `artist`/`album` -- already
+// resolved at index time from embedded tags, falling back to filename/folder
+// parsing (indexer/enrich_audio.py) -- never guessed here.
//
// Unlike Videos, MusicBrainz is looked up only when a track has no embedded
-// cover art at all (musicbay.md section 2.1's tiered trust: tags first,
-// filename parsing second, MusicBrainz last) -- most of a real, well-ripped
+// cover art at all (docs/MESHBAY_DESIGN.md §9.8's order of trust: tags
+// first, filename parsing second, MusicBrainz last) -- most of a real, well-ripped
// library already carries good artist/album text and often its own cover, so
// this avoids a network round trip most tiles never need. Playback never
// touches this file: clicking a track calls the `onPlayQueue` prop the shell
@@ -52,7 +52,7 @@ function foldKey(s) {
}
// Same shape as video-app.js's underVideoRoot: an unset root means "show
-// nothing" (docs/musicbay.md's amended §2.1 — the node itself runs no
+// nothing" (docs/MESHBAY_DESIGN.md §9.8 — the node itself runs no
// tag/cover enrichment for this group before a root is chosen either,
// daemon.py's _enrich_new_audio_entries), not "the whole shared tree" —
// falling back to that would just show files nothing has enriched.
@@ -212,17 +212,16 @@ function useMusicMeta(transportRef, fileId, active) {
}
// A drawn CD standing in for a cover nothing supplied one for -- most tiles
-// in a real, older/well-ripped library land here (musicbay.md's own
-// measurement: ~11% embedded art, ~26% once sibling image files are counted
-// too), so this is the *default* look of the grid, not a rare fallback, and
-// needed to read as a deliberate piece of art rather than a broken image.
-// A flat single-color icon (the first version of this) looked exactly like
-// "missing", not "no cover" -- an actual disc, with the iridescent sheen a
-// real CD's data side has, reads as intentional at a glance. Genuinely
-// unique gradient ids: a `<radialGradient>` id is a plain DOM id, and a grid
-// full of these renders many instances at once -- reusing one literal id
-// would leave every disc after the first pointing at whichever def the
-// browser resolves first.
+// in a real, older/well-ripped library land here (measured: ~11% embedded
+// art, ~26% once sibling image files are counted too), so this is the
+// *default* look of the grid, not a rare fallback, and needed to read as a
+// deliberate piece of art rather than a broken image. A flat single-color icon
+// (the first version of this) looked exactly like "missing", not "no cover" --
+// an actual disc, with the iridescent sheen a real CD's data side has, reads
+// as intentional at a glance. Genuinely unique gradient ids: a
+// `<radialGradient>` id is a plain DOM id, and a grid full of these renders
+// many instances at once -- reusing one literal id would leave every disc
+// after the first pointing at whichever def the browser resolves first.
let _discIdSeq = 0;
function DiscPlaceholder({ cls }) {
@@ -517,8 +516,8 @@ function FlatArtistFolder({ artist, onPlayQueue, onMenu }) {
${open && html`
<div class="music-flat-children">
${/* A real artist folder with no album layer at all is common here
- -- a pile of loose singles, not one release (musicbay.md
- section 2.1's "flat per-artist folder" case). Nesting them
+ -- a pile of loose singles, not one release (a flat
+ per-artist folder, docs/MESHBAY_DESIGN.md §9.8). Nesting them
one more level behind their own always-empty "Unknown album"
row was exactly the friction reported live: an extra,
pointless expand before reaching a track that's playable
@@ -748,7 +747,7 @@ function MusicApp({
}
// foldKey rides along for the Search page's merge unit keys
-// (docs/refactoring-search.md §5.2). An album's *display* strings are the
+// (docs/MESHBAY_DESIGN.md §9.11). An album's *display* strings are the
// first-seen spelling, and which group is seen first is the order its index
// happened to arrive in — so keying a unit on them would let the chosen source
// change between page loads. The folded key is the one grouping actually used,
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/music-player.js b/packages/meshbay-hub/src/meshbay_hub/static/music-player.js
index c3a2868..7c81f4e 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/music-player.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/music-player.js
@@ -8,7 +8,7 @@ import { useStickyBand } from './sticky.js';
import { CHUNK_SIZE, pipelinedDownload } from './file-utils.js';
/**
- * The Music app's persistent player bar (docs/musicbay.md §2.3, §7.2).
+ * The Music app's persistent player bar (docs/MESHBAY_DESIGN.md §9.8).
*
* Owned and rendered by app.js — the router's parent — so playback survives
* navigating between groups, search, and other pages. Both group-page.js
@@ -23,11 +23,12 @@ import { CHUNK_SIZE, pipelinedDownload } from './file-utils.js';
* track is a few megabytes, so it is downloaded and decrypted once through
* the same chunk pipeline Files already uses (file-utils.js's
* pipelinedDownload), then played from a blob URL — the deliberate
- * simplification recorded in musicbay.md §2.2. WMA and Musepack are the one
- * exception (`NEEDS_TRANSCODE_RE` below): neither decodes in a browser's
- * <audio> element at all, tagged correctly or not, so those two go through
- * `transport.requestAudioTranscode` first — a node-side, cached-after-once
- * AAC/M4A conversion — before the same download/blob path runs.
+ * simplification recorded in docs/MESHBAY_DESIGN.md §9.8. WMA and Musepack are
+ * the one exception (`NEEDS_TRANSCODE_RE` below): neither decodes in a
+ * browser's <audio> element at all, tagged correctly or not, so those two go
+ * through `transport.requestAudioTranscode` first — a node-side,
+ * cached-after-once AAC/M4A conversion — before the same download/blob path
+ * runs.
*/
const MIME_BY_EXT = {
@@ -271,7 +272,7 @@ function MusicPlayerBar({ getConnection, queue, onClose, userPrefs, onSaveQueue
// Screen Wake Lock, opt-in only (Settings → music_keep_screen_on) and only
// while a track is actually playing — off by default because the ordinary
// expectation, matching Spotify/Deezer, is that the phone locks on its own
- // idle timer while listening (docs/musicbay.md §2.2). Unlike the video
+ // idle timer while listening (docs/MESHBAY_DESIGN.md §9.8). Unlike the video
// player's unconditional lock, this must not fight that default for
// everyone who never asked for it; it exists for whoever explicitly wants
// to trade battery for riding out the WebRTC screen-lock reconnect gap
@@ -368,7 +369,8 @@ function MusicPlayerBar({ getConnection, queue, onClose, userPrefs, onSaveQueue
}, [getConnection, evictOldBlobs]);
// Silently warms the cache for the next tracks so pressing "next" doesn't
- // visibly wait (musicbay.md §2.2) — best-effort, never surfaces an error.
+ // visibly wait (docs/MESHBAY_DESIGN.md §9.8) — best-effort, never surfaces
+ // an error.
//
// More than one: a screen lock can cost the transport several minutes (see
// the WebRTC auto-reconnect in transport.js — this is the other half of
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/photos-app-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/photos-app-settings.js
index 8656fa5..0407e67 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/photos-app-settings.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/photos-app-settings.js
@@ -6,7 +6,7 @@ import { FolderPickerField } from './folder-tree.js';
/**
* The Photos app's operator settings: folders, and nothing else.
*
- * Photos was always several folders (docs/photos.md §2.1) — a photo library
+ * Photos was always several folders (docs/MESHBAY_DESIGN.md §9.9) — a photo library
* is routinely scattered, with no single natural root — so this pane is what
* the other two grew into rather than the exception it used to be. No
* third-party service: EXIF is read locally on the node, and nothing about a
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/photos-app.js b/packages/meshbay-hub/src/meshbay_hub/static/photos-app.js
index 2c4356c..261f2af 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/photos-app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/photos-app.js
@@ -12,7 +12,7 @@ import { SourceTag } from './group-name.js';
// ── Photos ───────────────────────────────────────────────────────────────────
//
-// docs/photos.md. Unlike Videos/Music: several root folders per group
+// docs/MESHBAY_DESIGN.md §9.9. Unlike Videos/Music: several root folders per group
// (photoRoots is a list, §2.1), one album-grid view with no mode toggle and
// no third-party matching step (§2.3), and per-photo info read from the
// file's own EXIF at index time rather than fetched live. Every directory
@@ -383,7 +383,7 @@ function PhotosApp({
// groupPhotoAlbums is exported for the Search page, which needs the album a
// photo belongs to in order to merge duplicate sources per album rather than
-// per file (docs/refactoring-search.md §5.2). It calls this one, never a copy:
+// per file (docs/MESHBAY_DESIGN.md §9.11). It calls this one, never a copy:
// a second implementation of the album key would keep agreeing with this one
// right up until one of them changed.
export { PhotosApp, groupPhotoAlbums };
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/platform.js b/packages/meshbay-hub/src/meshbay_hub/static/platform.js
index 07e5b6f..cfa4d87 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/platform.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/platform.js
@@ -2,8 +2,9 @@
* What differs between running in a browser and running as an installed app.
*
* The interface is the same code either way — that is the whole reason Electron
- * was chosen over a shell that replaces the engine (docs/desktop-client-v1.md
- * §2). What genuinely differs is small and lives here:
+ * was chosen over a shell that replaces the engine
+ * (docs/MESHBAY_DESIGN.md §8.2). What genuinely differs is small and lives
+ * here:
*
* · **where the hub is.** Served from the hub, it is the current origin. Ship
* the interface in a package and it becomes a configured URL, because the
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/profile-page.js b/packages/meshbay-hub/src/meshbay_hub/static/profile-page.js
index 4d452e1..a19b543 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/profile-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/profile-page.js
@@ -83,7 +83,7 @@ export function ProfilePage({ user, onLogout }) {
setPinCount(window.MeshBayTransport?.pinnedNodeCount?.() ?? 0);
}, []);
- // ── Passphrase change (docs/auth-confirm.md §3) ─────────────────────────
+ // ── Passphrase change (docs/MESHBAY_DESIGN.md §3.6) ─────────────────────
const [cpOpen, setCpOpen] = useState(false);
const [cpOld, setCpOld] = useState('');
const [cpNew, setCpNew] = useState('');
@@ -168,7 +168,7 @@ export function ProfilePage({ user, onLogout }) {
}
}, [cpOld, cpNew, user]);
- // ── Recovery key backfill (docs/auth-confirm.md §4.3) ───────────────────
+ // ── Recovery key backfill (docs/MESHBAY_DESIGN.md §3.6) ─────────────────
// Enter the recovery key once per browser to add a recovery-wrapped copy of
// your identity to every group — covers groups joined before the key was
// loaded here.
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/search-page.js b/packages/meshbay-hub/src/meshbay_hub/static/search-page.js
index 26d4e4c..a096414 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/search-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/search-page.js
@@ -207,7 +207,7 @@ async function fetchGroupIndex(groupId, token, bundleKey, username, userId) {
};
// Which of the reader's groups sit on their own node — the tie-breaker
// when the same file is announced by several of them
- // (docs/refactoring-search.md §5.3). Computed by the node from its own
+ // (docs/MESHBAY_DESIGN.md §9.11). Computed by the node from its own
// record of who it belongs to (webrtc_server.py's _is_node_admin), never
// from a hub claim, and deliberately not written to the index cache: it
// describes this connection, not the group's content.
@@ -349,7 +349,7 @@ function cachedDirs(roots, appKey, legacyKey) {
// different people invited to different libraries — arrived here as two
// entries per file, so a film showed as two poster cards and every episode
// twice inside a show. `source-merge.js` folds them on the content hash and
-// resolves one source per *unit*. See docs/refactoring-search.md.
+// resolves one source per *unit*. See docs/MESHBAY_DESIGN.md §9.11.
//
// The units come from video-app.js's own `groupVideoEntries`, never from a
// second copy of its keys here: a copy would keep agreeing with the original
@@ -465,7 +465,7 @@ function SearchPage({ token, username, userId, groups, onPlayQueue, userPrefs })
// A group whose connection failed stops being chosen as a merged entry's
// source, so a unit fails over to another group that has the file
- // (docs/refactoring-search.md §5.4). Without this the merge could make a
+ // (docs/MESHBAY_DESIGN.md §9.11). Without this the merge could make a
// file *less* available than it was before it, which would be a regression
// dressed as a feature.
//
@@ -609,7 +609,7 @@ function SearchPage({ token, username, userId, groups, onPlayQueue, userPrefs })
}, [indexedGroups]);
// How a unit's source is chosen, shared by every merged view
- // (docs/refactoring-search.md §5.3). `isLocal` reads the flag the node itself
+ // (docs/MESHBAY_DESIGN.md §9.11). `isLocal` reads the flag the node itself
// put in the handshake ack — computed from its own record of who it belongs
// to (webrtc_server.py's `_is_node_admin`), never from a hub claim.
const mergeOpts = useMemo(() => ({
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/settings-page.js b/packages/meshbay-hub/src/meshbay_hub/static/settings-page.js
index 51d99cd..9a52059 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/settings-page.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/settings-page.js
@@ -19,7 +19,7 @@ export function SettingsPage({ user, theme, onThemeChange, groups, onPrefsChange
() => Object.fromEntries((groups || []).map(g => [g.id, !!g.muted])));
const [globalMute, setGlobalMute] = useState(false);
const [defaultTab, setDefaultTab] = useState('chat');
- // Off by default (musicbay.md §2.2): the ordinary expectation, matching
+ // Off by default (docs/MESHBAY_DESIGN.md §9.8): the ordinary expectation, matching
// Spotify/Deezer, is that the phone locks on its own idle timer while
// listening. This is for whoever would rather trade battery for it —
// e.g. to ride out the WebRTC screen-lock reconnect gap without waiting
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/source-merge.js b/packages/meshbay-hub/src/meshbay_hub/static/source-merge.js
index 2c08e06..b6ce0bd 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/source-merge.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/source-merge.js
@@ -6,7 +6,7 @@
* paths are already one entry. The Search page is what creates duplicates, by
* concatenating N independently keyed indexes into one list — a directory
* shared by two groups then shows every film twice, every episode twice, every
- * track twice. See docs/refactoring-search.md.
+ * track twice. See docs/MESHBAY_DESIGN.md §9.11.
*
* Two rules decide everything here:
*
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/style.css b/packages/meshbay-hub/src/meshbay_hub/static/style.css
index 2d07fd1..81ae895 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/style.css
+++ b/packages/meshbay-hub/src/meshbay_hub/static/style.css
@@ -1793,7 +1793,7 @@ button:disabled { opacity: 0.5; cursor: not-allowed; }
margin: 0;
}
-/* Photos app's root add/remove list (docs/photos.md §2.2) — a set, unlike
+/* Photos app's root add/remove list (docs/MESHBAY_DESIGN.md §9.9) — a set, unlike
the Videos/Music single-value picker above. */
.settings-root-list { list-style: none; margin: 4px 0 8px; padding: 0; }
.settings-root-list-item {
@@ -4031,7 +4031,7 @@ h2 .gn-owner, h3 .gn-owner { font-size: 0.55em; }
.video-flat-season { padding-left: 24px; margin-bottom: 8px; }
.video-flat-season .video-season-header { margin: 6px 0 4px; }
-/* Season picker — docs/mediacenter.md §5.4's per-season overview view.
+/* Season picker — docs/MESHBAY_DESIGN.md §9.7's per-season overview view.
Was a row of pills with `overflow-x: auto`: a show with a dozen seasons hid
most of them behind a horizontal scrollbar, worst on the narrow screens that
can least afford it. One trigger and a menu is one row high whatever the
@@ -4139,7 +4139,7 @@ h2 .gn-owner, h3 .gn-owner { font-size: 0.55em; }
border-radius: 4px;
}
-/* ── Music app (docs/musicbay.md) ─────────────────────────────────────────
+/* ── Music app (docs/MESHBAY_DESIGN.md §9.8) ──────────────────────────────
Reuses .video-overlay/.video-top-bar/.video-title/.video-close, .tb-btn/
.tb-search, .video-flat-list/.video-flat-row/.video-flat-info/
.video-flat-title/.video-flat-sub/.video-flat-folder/.video-flat-chevron
@@ -4346,7 +4346,7 @@ h2 .gn-owner, h3 .gn-owner { font-size: 0.55em; }
back rather than compounding with the rail. */
.music-flat-track { padding: 5px 8px; }
-/* ── Persistent player bar (docs/musicbay.md §2.3) ────────────────────────
+/* ── Persistent player bar (docs/MESHBAY_DESIGN.md §9.8) ──────────────────
`position: sticky`, not `fixed` — deliberately: CLAUDE.md's own history
records more than one layout bug from a fixed-position element quietly
double-reserving space against a page that also sized itself against the
@@ -4571,7 +4571,7 @@ h2 .gn-owner, h3 .gn-owner { font-size: 0.55em; }
.music-player-volume { display: none; }
}
-/* ── Photos app (photos-app.js, docs/photos.md) ───────────────────────────── */
+/* ── Photos app (photos-app.js, docs/MESHBAY_DESIGN.md §9.9) ──────────────── */
.photo-toolbar {
display: flex;
@@ -4924,7 +4924,7 @@ h2 .gn-owner, h3 .gn-owner { font-size: 0.55em; }
.folder-field-tbl .sdt-col-dir { font-size: 0.9em; }
.folder-field-actions { display: flex; align-items: center; gap: 10px; }
-/* HelloWorld (docs/refactor-groups.md §4.1) — the reference app, hidden
+/* HelloWorld (docs/MESHBAY_DESIGN.md §9.4) — the reference app, hidden
behind ?dev=1. Deliberately plain: it exists to prove the plumbing, and
anything decorative here would be a second thing to keep working. */
.hw-list { list-style: none; margin: 12px 0 0; padding: 0; }
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/transport.js b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
index 72c831f..f4979f4 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/transport.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/transport.js
@@ -893,7 +893,7 @@ class MeshBayTransport {
openErr = e;
// The passphrase key did not open the bundle. If we hold a recovery
// key and the node kept a recovery copy, try that — Flow B
- // (docs/auth-confirm.md §4.5): recovering an identity after a lost
+ // (docs/MESHBAY_DESIGN.md §3.6): recovering an identity after a lost
// passphrase, before re-wrapping it under the new one.
if (this._recoveryKey && kpResp.bundle_enc_recovery) {
try {
@@ -1357,7 +1357,7 @@ class MeshBayTransport {
}
/**
- * TMDB metadata for one file (Videos app, docs/mediacenter.md §5.4).
+ * TMDB metadata for one file (Videos app, docs/MESHBAY_DESIGN.md §9.7).
* Keyed by the entry's own `id` (its content hash) — never a path: a
* path names the *folder* a file is in (indexer.py's `_virtual_dir`), so
* two files sharing a folder (any multi-episode season) would resolve to
@@ -1388,7 +1388,7 @@ class MeshBayTransport {
}
/**
- * One season's own overview/air_date/poster (docs/mediacenter.md §5.4's
+ * One season's own overview/air_date/poster (docs/MESHBAY_DESIGN.md §9.7's
* per-season view) — a show's own tmdb_meta is one static field that does
* not necessarily describe every season alike, found live: a 3-season
* show whose overview read as season-3-specific for every season.
@@ -1445,7 +1445,7 @@ class MeshBayTransport {
/**
* Drop one file's cached TMDB match so it re-resolves with the node's
- * current matcher (§10.1/V13) — the one-click alternative to the full
+ * current matcher (V13) — the one-click alternative to the full
* search-and-pick flow. Signed for the same reason as overrideTmdbMatch.
*/
async rematchTmdbMatch(fileId, signFn) {
@@ -1466,7 +1466,7 @@ class MeshBayTransport {
* setTmdbEnabled below for the per-group on/off switch). Signed like
* setAppsEnabled/updateRoot — an unsigned change would let any
* member alter outbound third-party network traffic the operator never
- * agreed to (docs/mediacenter.md §5.5, §8). `token: ''` explicitly clears
+ * agreed to (docs/MESHBAY_DESIGN.md §9.7, §6.5). `token: ''` explicitly clears
* a previously-set custom token; omit it (undefined/null), like
* `language`, to leave whatever is stored unchanged.
*/
@@ -1598,7 +1598,7 @@ class MeshBayTransport {
}
/**
- * MusicBrainz metadata for one track (Music app, docs/musicbay.md §4.3)
+ * MusicBrainz metadata for one track (Music app, docs/MESHBAY_DESIGN.md §9.8)
* — same shape as fetchMediaMeta, minus a season/episode concept:
* album-level (release), resolved from the track's own artist/album
* fields already in the index. Keyed by the track's own `id` (content
@@ -1659,7 +1659,7 @@ class MeshBayTransport {
/**
* Whether MusicBrainz lookups run for this group at all — per-group from
- * the start (docs/musicbay.md §3.2/§6). Signed like setTmdbEnabled.
+ * the start (docs/MESHBAY_DESIGN.md §9.8). Signed like setTmdbEnabled.
*/
async setMusicbrainzEnabled(enabled, signFn) {
const msg = await this._sendAndWait({
@@ -1833,7 +1833,7 @@ class MeshBayTransport {
* Who is in this group and which device keys they hold — verified here, not
* taken on the node's word.
*
- * Tier 2 of `desktop-client-v1.md` §4.8. The node relays, for each device,
+ * Tier 2 of `docs/MESHBAY_DESIGN.md` §3.3. The node relays, for each device,
* the already-pinned key that countersigned it and the signature itself; this
* walks that from each account's first device outwards and keeps only the
* devices it could actually reach. A device the node asserts but cannot
@@ -1843,7 +1843,7 @@ class MeshBayTransport {
* The property this buys, stated exactly: once a client has seen an account,
* a node that later substitutes a key for it is **detected**. It buys nothing
* at first sight, where there is nothing to compare against — that boundary
- * is `per-node-identity-v1.md`'s and does not move.
+ * is `docs/MESHBAY_DESIGN.md` §3.2's and does not move.
*/
async groupRoster() {
if (this._roster) return this._roster;
@@ -1875,8 +1875,9 @@ class MeshBayTransport {
* 'changed' a key this account has not shown before and cannot evidence
*
* Only `changed` is worth a person's attention, and it is the one notice
- * §4.8 budgets for. `first` is not an alarm — every account is new once, and
- * treating that as a warning is how a warning stops being read.
+ * docs/MESHBAY_DESIGN.md §3.3 budgets for. `first` is not an alarm — every
+ * account is new once, and treating that as a warning is how a warning stops
+ * being read.
*/
async accountDeviceStatus(userId, devicePk) {
let roster;
@@ -2658,7 +2659,7 @@ class MeshBayTransport {
// Identity keys are per node, so a browser and a desktop client are two keys
// on one account here. A new one is admitted by a key this node already
// pinned — never by the hub, which holds no user keys and so cannot
- // countersign anything. See docs/desktop-client-v1.md §4.
+ // countersign anything. See docs/MESHBAY_DESIGN.md §3.3.
/**
* Ask to be added, and return the code to show the person.
@@ -3286,7 +3287,7 @@ class MeshBayTransport {
});
}
// Same shape: the operator dropped one file's match to have it
- // re-resolved (§10.1/V13). No tmdbId — the node re-derives it.
+ // re-resolved (V13). No tmdbId — the node re-derives it.
if (msg.type === 'tmdb_rematch_ack' && this._onTmdbOverride) {
this._onTmdbOverride({ fileId: msg.file_id || '', tmdbId: '', mediaType: '' });
}
@@ -4044,7 +4045,7 @@ function pinnedNodeCount() {
// ── Passphrase change: re-wrap every reachable identity bundle ───────────────
//
-// docs/auth-confirm.md §3.2. The passphrase-derived bundle_key encrypts this
+// docs/MESHBAY_DESIGN.md §3.6. The passphrase-derived bundle_key encrypts this
// account's per-node identity on every node it has joined. Changing the
// passphrase changes that key, so each bundle must be read with the old key and
// written back with the new one — on the node, while both keys are in hand.
@@ -4086,7 +4087,8 @@ function _acWithTimeout(promise, ms, label) {
* @param {string} o.userId
* @param {string} [o.oldPassphrase] omit in Flow B — connect falls back to the recovery copy
* @param {string} o.newPassphrase
- * @param {string} [o.recoveryKey] the recovery mnemonic (Flow B, docs/auth-confirm.md §4.5).
+ * @param {string} [o.recoveryKey] the recovery mnemonic (Flow B,
+ * docs/MESHBAY_DESIGN.md §3.6).
* When given, the recovery-wrapped copy is read where the
* passphrase copy cannot be, and a fresh one is written back.
* @param {(p:{done:number,total:number})=>void} [o.onProgress]
@@ -4100,7 +4102,7 @@ async function rewrapAllNodes(o) {
let oldKey, newKey;
if (o.bundleKey) {
// "Keep the current passphrase key, just add / refresh the recovery copy"
- // — the Profile backfill (docs/auth-confirm.md §4.3). `o.bundleKey` is the
+ // — the Profile backfill (docs/MESHBAY_DESIGN.md §3.6). `o.bundleKey` is the
// live {v2,v1} session key, so no passphrase is needed.
oldKey = newKey = o.bundleKey;
} else {
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/video-app-settings.js b/packages/meshbay-hub/src/meshbay_hub/static/video-app-settings.js
index 5c79bc5..4662c25 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/video-app-settings.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/video-app-settings.js
@@ -23,7 +23,7 @@ const TMDB_LANGUAGE_BY_LOCALE = {
* The TMDB parts are two independent settings that happen to sit together:
* the on/off switch is per group, while the API key and the query language
* are node-wide, because they are one operator's credential and one shared
- * cache (docs/mediacenter.md §5.5). They save separately for that reason.
+ * cache (docs/MESHBAY_DESIGN.md §9.7). They save separately for that reason.
*/
function VideoSettings({ roots, dirs, settings, saveDirectories, transport, signFn }) {
const { busy, msg, run } = useSaver();
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/video-app.js b/packages/meshbay-hub/src/meshbay_hub/static/video-app.js
index d4af4ef..d4ec01a 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/video-app.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/video-app.js
@@ -10,16 +10,16 @@ import { usePager, Pager, pageSizeFrom } from './pager.js';
// ── Videos ───────────────────────────────────────────────────────────────────
//
// A poster-grid (TMDB-enriched) or flat (thumbnail-only) browser for a
-// group's video files, per docs/mediacenter.md. Grouping: one card per movie,
-// one card per show — shows are grouped by `display_title` (already
+// group's video files, per docs/MESHBAY_DESIGN.md §9.7. Grouping: one card per
+// movie, one card per show — shows are grouped by `display_title` (already
// resolved/corroborated at index time, §3.4), not by folder path, since a
// client-side path convention would have to guess how many roots/subfolders
// deep a show folder sits, which display_title already settled once.
//
// TMDB metadata is fetched lazily, only for a tile once it is actually
-// visible (LazyTile below) — apps.md §5's virtualization requirement for a
-// grid of many tiles. Thumbnails go through the same `file_req`/chunk path
-// as a real file (docs/mediacenter.md §5.3) via MediaThumb, reusing
+// visible (LazyTile below) — the virtualization requirement for a grid of
+// many tiles. Thumbnails go through the same `file_req`/chunk path
+// as a real file (docs/MESHBAY_DESIGN.md §6.5) via MediaThumb, reusing
// chat-app.js's ChatImage pattern.
const VIEW_MODE_KEY = 'meshbay_video_view_mode';
@@ -121,7 +121,7 @@ function groupVideoEntries(entries, videoDirectories) {
return { movies, shows };
}
-// ── lazy-mount tile (apps.md §5 virtualization) ─────────────────────────────
+// ── lazy-mount tile (virtualization) ───────────────────────────────────────
const LAZY_TILE_MARGIN = 300;
@@ -485,7 +485,7 @@ function OverviewText({ text, reserve }) {
`;
}
-// ── season picker (docs/mediacenter.md §5.4's fix for a mis-scoped overview) ─
+// ── season picker (docs/MESHBAY_DESIGN.md §9.7's per-season text) ────────────
//
// A row of tabs, which this was, scrolls horizontally once a show has more
// seasons than fit — a scrollbar nobody finds, hiding the seasons that matter
@@ -708,7 +708,7 @@ function VideoDetailModal({
const [rematching, setRematching] = useState(false);
const mediaType = show ? 'tv' : 'movie';
- // §10.1/V13: drop this file's cached match on the node and let it
+ // V13: drop this file's cached match on the node and let it
// re-resolve with the current matcher — the one-click alternative to the
// full search-and-pick flow above.
const doRematch = useCallback(async () => {
@@ -779,10 +779,11 @@ function VideoDetailModal({
`}
${/* Both of these act on a TMDB match, and with TMDB off for this
group there is none to act on: "Fix match" opens a search the
- node answers with an empty result list (§5.7's silent
- degradation), and "Rematch" drops a cached match that was
- never made. Two buttons that cannot do anything, offered to
- the one person who already knows why. */''}
+ node answers with an empty result list
+ (docs/MESHBAY_DESIGN.md §9.7's silent degradation), and
+ "Rematch" drops a cached match that was never made. Two
+ buttons that cannot do anything, offered to the one person
+ who already knows why. */''}
${isNodeAdmin && tmdbEnabled && html`
<div class="video-admin-actions">
<button class="admin-btn video-fix-match" onClick=${() => setSearching(true)}>
@@ -1182,9 +1183,10 @@ function VideoApp({
`;
}
-// MediaThumb and LazyTile are also used by music-app.js (docs/musicbay.md
-// §7.1): the same "decrypt a thumb_hash via the chunk path into a cached
+// MediaThumb and LazyTile are also used by music-app.js
+// (docs/MESHBAY_DESIGN.md §9.8): the same "decrypt a thumb_hash via the
+// chunk path into a cached
// blob" and "mount only once actually scrolled near" mechanisms apply to a
// track's cover art unchanged, so Music imports them here rather than
-// re-implementing (apps.md §4's checklist).
+// re-implementing (docs/MESHBAY_DESIGN.md §9.4's checklist).
export { VideoApp, MediaThumb, LazyTile, groupVideoEntries, bumpMediaMetaGeneration, bumpThumbGeneration };
diff --git a/packages/meshbay-hub/tests/test_desktop_shell.py b/packages/meshbay-hub/tests/test_desktop_shell.py
index 5862ac3..fcf6c4d 100644
--- a/packages/meshbay-hub/tests/test_desktop_shell.py
+++ b/packages/meshbay-hub/tests/test_desktop_shell.py
@@ -8,8 +8,8 @@ the design depends on are present in the source, and it fails if one is removed
— which is the same treatment `test_downloads.py` gives the three
browser-specific save paths, for the same reason.
-Every assertion here corresponds to a sentence in `docs/desktop-client-v1.md`
-§3. Weak evidence, and the only evidence available without a packaged build; a
+Every assertion here corresponds to a sentence in `docs/MESHBAY_DESIGN.md`
+§8.2. Weak evidence, and the only evidence available without a packaged build; a
person with an installed client is what confirms the rest.
"""
@@ -370,9 +370,10 @@ def test_plain_http_is_refused_except_to_loopback():
def test_the_interface_is_copied_not_forked():
"""
- §2.7: the hub's static directory is the single source. A silent fork is the
- only real way to end up maintaining the interface twice, so the copy is
- generated and the generated tree is not committed.
+ docs/MESHBAY_DESIGN.md §8.3: the hub's static directory is the single
+ source. A silent fork is the only real way to end up maintaining the
+ interface twice, so the copy is generated and the generated tree is not
+ committed.
"""
sync = (CLIENT / "scripts" / "sync-ui.js").read_text(encoding="utf-8")
assert "meshbay-hub" in sync and "static" in sync
diff --git a/packages/meshbay-hub/tests/test_downloads.py b/packages/meshbay-hub/tests/test_downloads.py
index afb85d6..395053b 100644
--- a/packages/meshbay-hub/tests/test_downloads.py
+++ b/packages/meshbay-hub/tests/test_downloads.py
@@ -146,9 +146,9 @@ def test_a_length_is_only_promised_when_it_is_known(tmp_path):
assert "if (entry.size > 0)" in src
# The zip-directory download started in files-app.js (group-page refactor)
- # and was lifted into file-utils.js's downloadDirectory (docs/photos.md
- # §3) so photos-app.js's own "zip this album" button calls the same
- # implementation rather than a second one.
+ # and was lifted into file-utils.js's downloadDirectory
+ # (docs/MESHBAY_DESIGN.md §9.9) so photos-app.js's own "zip this album"
+ # button calls the same implementation rather than a second one.
# Anchored on the call, not on how its result is bound: the assignment
# became a bare `target = ...` inside a try when _openDownloadTarget gained
# the ability to refuse an oversized download (test_memory_ceiling.py).
diff --git a/packages/meshbay-hub/tests/test_helloworld_proves_the_plugin_claim.py b/packages/meshbay-hub/tests/test_helloworld_proves_the_plugin_claim.py
index a1baf92..451b57b 100644
--- a/packages/meshbay-hub/tests/test_helloworld_proves_the_plugin_claim.py
+++ b/packages/meshbay-hub/tests/test_helloworld_proves_the_plugin_claim.py
@@ -1,7 +1,7 @@
"""
The reference application, and what it is for.
-`docs/refactor-groups.md` claims that adding an application costs a registry
+`docs/MESHBAY_DESIGN.md` §9.4 claims that adding an application costs a registry
entry and the app's own files — no op, no MNP message, no route, no edit to the
pages that render it. Every other test of that claim reads source for the
*absence* of app names, which proves nobody wrote a special case for Videos. It
diff --git a/packages/meshbay-hub/tests/test_hook_ordering.py b/packages/meshbay-hub/tests/test_hook_ordering.py
index 78561b6..3c82bb0 100644
--- a/packages/meshbay-hub/tests/test_hook_ordering.py
+++ b/packages/meshbay-hub/tests/test_hook_ordering.py
@@ -37,7 +37,7 @@ STATIC_FILES = [
"video-player.js", "video-app.js", "music-app.js", "music-player.js",
"photos-app.js", "pager.js",
"group-settings.js",
- # The per-app settings architecture (docs/refactor-groups.md §3). Reached
+ # The per-app settings architecture (docs/MESHBAY_DESIGN.md §9.4). Reached
# through the apps.js registry rather than imported by name, so a file
# left out of this list is one nothing checks — the failure is silent.
"settings-ui.js", "folder-tree.js",
diff --git a/packages/meshbay-hub/tests/test_password_change.py b/packages/meshbay-hub/tests/test_password_change.py
index 4d2f303..b2fe586 100644
--- a/packages/meshbay-hub/tests/test_password_change.py
+++ b/packages/meshbay-hub/tests/test_password_change.py
@@ -1,5 +1,5 @@
"""
-Passphrase change — Flow A of docs/auth-confirm.md.
+Passphrase change — Flow A of docs/MESHBAY_DESIGN.md §3.6.
The hub's part is small: re-prove the current passphrase, swap the auth_key
verifier, invalidate every other session, keep the caller's. The re-wrapping of
diff --git a/packages/meshbay-hub/tests/test_password_reset.py b/packages/meshbay-hub/tests/test_password_reset.py
index 1273315..823807c 100644
--- a/packages/meshbay-hub/tests/test_password_reset.py
+++ b/packages/meshbay-hub/tests/test_password_reset.py
@@ -1,5 +1,5 @@
"""
-Passphrase reset by e-mail code — Flow B of docs/auth-confirm.md §4.2.
+Passphrase reset by e-mail code — Flow B of docs/MESHBAY_DESIGN.md §3.6.
The hub's part re-opens sign-in only: it swaps the auth_key verifier, kills
every session, and drops every registered device key so a stored one cannot
diff --git a/packages/meshbay-hub/tests/test_recovery_email.py b/packages/meshbay-hub/tests/test_recovery_email.py
index 07880d0..96dde5d 100644
--- a/packages/meshbay-hub/tests/test_recovery_email.py
+++ b/packages/meshbay-hub/tests/test_recovery_email.py
@@ -1,5 +1,5 @@
"""
-The recovery key in the registration e-mail (docs/auth-confirm.md §4.4).
+The recovery key in the registration e-mail (docs/MESHBAY_DESIGN.md §3.6).
When the client sends `recovery_key`, the hub appends it to the verification
e-mail and stores it nowhere. When it does not, the e-mail carries only the
diff --git a/packages/meshbay-hub/tests/test_recovery_key.py b/packages/meshbay-hub/tests/test_recovery_key.py
index 378758a..54415f6 100644
--- a/packages/meshbay-hub/tests/test_recovery_key.py
+++ b/packages/meshbay-hub/tests/test_recovery_key.py
@@ -1,5 +1,5 @@
"""
-The account recovery key (docs/auth-confirm.md §4.3).
+The account recovery key (docs/MESHBAY_DESIGN.md §3.6).
`generateRecoveryKey` / `deriveRecoveryKey` in keyderive.js are run here under
node against the real WebCrypto, rather than reimplemented: the mnemonic has to
diff --git a/packages/meshbay-hub/tests/test_rewrap_fanout.py b/packages/meshbay-hub/tests/test_rewrap_fanout.py
index 03d24dc..54ef67a 100644
--- a/packages/meshbay-hub/tests/test_rewrap_fanout.py
+++ b/packages/meshbay-hub/tests/test_rewrap_fanout.py
@@ -1,6 +1,6 @@
"""
`MeshBayTransport.rewrapAllNodes` — the passphrase-change / recovery fan-out
-(docs/auth-confirm.md §3.2, §4.5).
+(docs/MESHBAY_DESIGN.md §3.6).
The real function is run under node with its two boundaries stubbed: the hub
HTTP calls and the per-node `MeshBayTransport` handshake. What is exercised is
diff --git a/packages/meshbay-hub/tests/test_search_files_unmerged.py b/packages/meshbay-hub/tests/test_search_files_unmerged.py
index 6956dde..b84b25d 100644
--- a/packages/meshbay-hub/tests/test_search_files_unmerged.py
+++ b/packages/meshbay-hub/tests/test_search_files_unmerged.py
@@ -19,7 +19,7 @@ merge. Weak evidence, and the only kind available for the SPA — but the failur
it guards against is a one-line edit, which is exactly what a source-reading
test catches well.
-See docs/refactoring-search.md §6.1.
+See docs/MESHBAY_DESIGN.md §9.11.
"""
import re
@@ -54,7 +54,7 @@ def test_the_files_list_is_not_merged():
"fileEntries now goes through the source merge. The Files explorer "
"shows one folder per group and a member navigates into it; merging "
"two groups' copies of a file would remove it from one of those "
- "folders. See docs/refactoring-search.md §6.1")
+ "folders. See docs/MESHBAY_DESIGN.md §9.11")
@pytest.mark.parametrize("name", ["videoEntries", "musicEntries", "photoEntries"])
diff --git a/packages/meshbay-hub/tests/test_search_media_merge.py b/packages/meshbay-hub/tests/test_search_media_merge.py
index 65f85aa..3464902 100644
--- a/packages/meshbay-hub/tests/test_search_media_merge.py
+++ b/packages/meshbay-hub/tests/test_search_media_merge.py
@@ -23,7 +23,7 @@ with it — so what this counts is what the grid renders.
`t()` is stubbed to return its key: `groupMusicEntries` uses it for the two
placeholder album names, and a string is not what is under test here.
-See docs/refactoring-search.md.
+See docs/MESHBAY_DESIGN.md §9.11.
"""
import json
diff --git a/packages/meshbay-hub/tests/test_search_source_merge.py b/packages/meshbay-hub/tests/test_search_source_merge.py
index cadf095..b471891 100644
--- a/packages/meshbay-hub/tests/test_search_source_merge.py
+++ b/packages/meshbay-hub/tests/test_search_source_merge.py
@@ -20,7 +20,7 @@ it has no imports precisely so that it can be, and a copy of the picking rule
in a test would keep agreeing with the original right up until one of them
changed.
-See docs/refactoring-search.md.
+See docs/MESHBAY_DESIGN.md §9.11.
"""
import json
diff --git a/packages/meshbay-hub/tests/test_sticky_band_ring.py b/packages/meshbay-hub/tests/test_sticky_band_ring.py
index 2d57822..e5ef860 100644
--- a/packages/meshbay-hub/tests/test_sticky_band_ring.py
+++ b/packages/meshbay-hub/tests/test_sticky_band_ring.py
@@ -28,9 +28,10 @@ before `test_sticky_header.py` measured it.
Read out of the stylesheet rather than measured in a browser, deliberately.
A browser shows the 4px at one width, in one of the states that happen to put
something above a band; what has to hold is which bands are in which of two
-lists, and that is a fact about the source. `docs/apps.md` sends the author of
-a new application here to make its toolbar pin, and this is what says whether
-the toolbar they add needs the gap — it does not, and it must not have it.
+lists, and that is a fact about the source. `docs/MESHBAY_DESIGN.md` §9.2 sends
+the author of a new application here to make its toolbar pin, and this is what
+says whether the toolbar they add needs the gap — it does not, and it must not
+have it.
"""
import re
diff --git a/packages/meshbay-hub/tests/test_transport_contracts.py b/packages/meshbay-hub/tests/test_transport_contracts.py
index 9c3a88b..3c2b0b6 100644
--- a/packages/meshbay-hub/tests/test_transport_contracts.py
+++ b/packages/meshbay-hub/tests/test_transport_contracts.py
@@ -141,7 +141,7 @@ def test_the_view_only_follows_new_messages_when_already_at_the_bottom(chat):
def test_every_authorize_admin_op_call_is_registered_in_admin_op_types(transport):
"""
- Found live (docs/photos.md's photo_roots): `setPhotoRoots` called
+ Found live (docs/MESHBAY_DESIGN.md §9.9's photo_roots): `setPhotoRoots` called
`_authorizeAdminOp(msg, 'photo_roots', ...)` like every other admin op,
but `photo_roots` was never added to `ADMIN_OP_TYPES` — so its initial
request was never keyed `admin:photo_roots`, the node's `admin_challenge`
diff --git a/packages/meshbay-hub/tests/test_zip_size_limit.py b/packages/meshbay-hub/tests/test_zip_size_limit.py
index 9471b8a..26c5552 100644
--- a/packages/meshbay-hub/tests/test_zip_size_limit.py
+++ b/packages/meshbay-hub/tests/test_zip_size_limit.py
@@ -3,7 +3,7 @@ The arbitrary ceiling on a directory zip.
`downloadDirectory` is the one implementation behind every "download this
folder as a zip" button — Files' single folder, Files' multi-folder selection,
-and the Photos album button (docs/photos.md §3) — so the limit is checked
+and the Photos album button (docs/MESHBAY_DESIGN.md §9.9) — so the limit is checked
once, there, and holds for all of them.
Three things are worth pinning. That an oversized folder is refused *before*
diff --git a/packages/meshbay-node/src/meshbay_node/bundle_store.py b/packages/meshbay-node/src/meshbay_node/bundle_store.py
index c65b94d..36792ac 100644
--- a/packages/meshbay-node/src/meshbay_node/bundle_store.py
+++ b/packages/meshbay-node/src/meshbay_node/bundle_store.py
@@ -6,7 +6,7 @@ Keypair bundles: AES-GCM encrypted (Ed25519 + X25519) private keys, encrypted
with the user's password-derived bundle_key. Opaque to the node. An optional
second copy (bundle_enc_recovery) is wrapped under the account's recovery key
instead, so a forgotten passphrase does not strand the identity — see
-docs/auth-confirm.md §4.3.
+docs/MESHBAY_DESIGN.md §3.6.
Both are stored and served over the P2P DataChannel during MNP handshake.
"""
@@ -168,7 +168,7 @@ class BundleStore:
copy wrapped under the account's recovery key.
A call that omits bundle_enc_recovery — a plain re-backup, or a
- passphrase-change re-wrap (docs/auth-confirm.md §3.2) — must not erase a
+ passphrase-change re-wrap (docs/MESHBAY_DESIGN.md §3.6) — must not erase a
recovery copy already stored, so the upsert keeps the existing value
when the new one is None.
"""
diff --git a/packages/meshbay-node/src/meshbay_node/chat/store.py b/packages/meshbay-node/src/meshbay_node/chat/store.py
index 9e5ee90..17cd68e 100644
--- a/packages/meshbay-node/src/meshbay_node/chat/store.py
+++ b/packages/meshbay-node/src/meshbay_node/chat/store.py
@@ -4,7 +4,7 @@ MeshBay Node — SQLite-backed chat message store.
One database per group. The node is a relay and an archive: it stores what it
was handed, serves it back, and — once a group has chat encryption switched on —
cannot read any of it. Decryption happens in the client, which is the only place
-that holds the epoch key (`docs/chat-sender-keys.md` §5).
+that holds the epoch key (`docs/MESHBAY_DESIGN.md` §4.5).
Three things about the schema are load-bearing rather than incidental:
diff --git a/packages/meshbay-node/src/meshbay_node/daemon.py b/packages/meshbay-node/src/meshbay_node/daemon.py
index 26f73fc..ad4a513 100644
--- a/packages/meshbay-node/src/meshbay_node/daemon.py
+++ b/packages/meshbay-node/src/meshbay_node/daemon.py
@@ -192,7 +192,7 @@ class NodeDaemon:
# failure — a persistently unprobeable file (corrupt, still being
# written) does not get re-queued on every coalesced broadcast. A
# restart retries everything, matching the "disposable, rebuildable"
- # stance the rest of this cache takes (docs/mediacenter.md §1/§2).
+ # stance the rest of this cache takes (docs/MESHBAY_DESIGN.md §6.5).
# Shared across the video and audio enrichment paths — content-
# addressed ids never collide between the two. Keyed by
# (group_id, entry.id), not entry.id alone: the id is a content
@@ -446,7 +446,7 @@ class NodeDaemon:
"tmdb_enabled": await self._roster.tmdb_enabled(
group_cfg.id) if self._roster else True,
# Music app equivalent of tmdb_enabled — per-group from
- # the start (docs/musicbay.md §6).
+ # the start (docs/MESHBAY_DESIGN.md §9.8).
"musicbrainz_enabled": await self._roster.musicbrainz_enabled(
group_cfg.id) if self._roster else True,
}
@@ -474,7 +474,7 @@ class NodeDaemon:
# 6b. Media cache (Videos app — TMDB metadata + thumbnails).
# Node-wide like audit.db, not per-group: a thumbnail is the same
# bytes regardless of which group happens to share the file
- # (docs/mediacenter.md §2/§5.5).
+ # (docs/MESHBAY_DESIGN.md §6.5, §9.7).
media_cache_db = data_dir / "media_cache.db"
self._media_cache = MediaCache(db_path=media_cache_db)
await self._media_cache.open()
@@ -491,16 +491,18 @@ class NodeDaemon:
self._state["tmdb_token_customized"] = bool(tmdb_token)
self._state["tmdb_language"] = tmdb_language or ""
- # 6c. Music app (docs/musicbay.md) — same media_cache.db, its own
- # enricher (mutagen, not ffmpeg) and its own MusicBrainz client.
+ # 6c. Music app (docs/MESHBAY_DESIGN.md §9.8) — same
+ # media_cache.db, its own enricher (mutagen, not ffmpeg) and its
+ # own MusicBrainz client.
# The User-Agent contact is the owner's hub email, resolved at
# login — no roster setting or env var needed any more.
self._audio_enricher = AudioEnricher(self._media_cache)
self._musicbrainz_client = MusicBrainzClient(owner_email=session.email)
self._state["musicbrainz_contact_configured"] = bool(session.email)
- # 6d. Photos app (docs/photos.md) — same media_cache.db, its own
- # enricher (Pillow, not ffmpeg/mutagen). No credential, no
+ # 6d. Photos app (docs/MESHBAY_DESIGN.md §9.9) — same
+ # media_cache.db, its own enricher (Pillow, not ffmpeg/mutagen).
+ # No credential, no
# third-party client to construct: EXIF is read locally.
self._photo_enricher = PhotoEnricher(self._media_cache)
self._state["media_cache"] = self._media_cache
@@ -1352,7 +1354,7 @@ class NodeDaemon:
delta = idx.diff(previous)
self._last_broadcast_snapshot[group_id] = (idx.version, idx.entries_by_id())
- # Videos app (docs/mediacenter.md §5.2): schedule async technical
+ # Videos app (docs/MESHBAY_DESIGN.md §6.5): schedule async technical
# probe + title parse + thumbnail generation for every newly-seen
# video entry under the group's configured video_root. Never blocks
# this broadcast — enrichment fields arrive later as their own
@@ -1377,18 +1379,19 @@ class NodeDaemon:
seen = {e.id for e in new_entries}
new_entries = new_entries + [e for e in rebuilt if e.id not in seen]
spawn(self._enrich_new_video_entries(indexer, new_entries))
- # Music app (docs/musicbay.md §6): same shape, gated on audio_root
- # exactly like video_root above (added later — musicbay.md's
- # original "no root, whole shared tree" call didn't hold up).
+ # Music app (docs/MESHBAY_DESIGN.md §9.8): same shape, gated on
+ # audio_root exactly like video_root above (added later — the original
+ # "no root, whole shared tree" call didn't hold up).
spawn(self._enrich_new_audio_entries(indexer, new_entries))
- # Photos app (docs/photos.md §5): same shape, gated on photo_roots
- # (a list, not a single string — §2.1).
+ # Photos app (docs/MESHBAY_DESIGN.md §9.9): same shape, gated on
+ # photo_roots (a list, not a single string).
spawn(self._enrich_new_photo_entries(indexer, new_entries))
# A rename/move changes the very filename (or season folder) that
- # §3.3/§3.4's title-parse read display_title/season/episode from,
- # but leaves the file's content — and so its id and everything
- # ffprobe/thumbnailing already found — untouched. Only entries
+ # docs/MESHBAY_DESIGN.md §9.7's title-parse read
+ # display_title/season/episode from, but leaves the file's content —
+ # and so its id and everything ffprobe/thumbnailing already found —
+ # untouched. Only entries
# whose name or path actually differ from the last broadcast get a
# fresh pass; an update that is enrichment's own field-fill
# (duration/thumb_hash/... landing via _on_enriched below) leaves
@@ -1403,13 +1406,13 @@ class NodeDaemon:
# Videos/Music/Photos apps: a file that leaves the index also loses
# its thumbnail/cover and file->tmdb/file->mbid mapping — the "real
- # deletion obligation" docs/mediacenter.md §2/§8 calls out
- # explicitly rather than leaving implicit (docs/musicbay.md §6
+ # deletion obligation" docs/MESHBAY_DESIGN.md §6.5 calls out
+ # explicitly rather than leaving implicit (docs/MESHBAY_DESIGN.md §9.8
# follows the same rule). tmdb_meta/mbid_meta rows are left alone
- # (§2: shared across files).
+ # (shared across files).
#
- # Found live (docs/photos.md): a root removed and a new one added
- # for the identical content (an operator renaming/relocating a
+ # Found live (docs/MESHBAY_DESIGN.md §9.9): a root removed and a new
+ # one added for the identical content (an operator renaming/relocating a
# shared folder) pruned the thumbnail here — correctly, the content
# is gone from *this* root — but left the hash in
# `_enriched_attempted`, which is never otherwise cleared. The same
@@ -1480,8 +1483,8 @@ class NodeDaemon:
A group with no video_root set yet does not enrich anything — TMDB
lookups and ffmpeg thumbnailing are real, ongoing per-file cost
- (mediacenter.md §5.2/§10), and running them over an operator's whole
- shared index before they have chosen which folder is actually their
+ (docs/MESHBAY_DESIGN.md §6.5), and running them over an operator's
+ whole shared index before they have chosen which folder is actually their
media library would burn both TMDB's rate limit and the node's CPU
on files that were never meant to be in the Videos app at all. Once a
root is set, `_enrich_video_root_now` (called when it changes)
@@ -1560,14 +1563,14 @@ class NodeDaemon:
async def _enrich_new_audio_entries(self, indexer: DirectoryIndexer, entries: list) -> None:
"""
- Music app (docs/musicbay.md §2.1, §6): fire (never await further)
+ Music app (docs/MESHBAY_DESIGN.md §9.8): fire (never await further)
tag/cover enrichment for unattempted audio entries under the
group's configured audio_root — same gate as
- `_enrich_new_video_entries` above (musicbay.md's original "no root,
- whole shared tree" call turned out wrong against a real messy
- library: everything under every shared folder got mixed together
- with no way to scope it down). `_enriched_attempted` is shared with
- the video path — content-addressed ids never collide across the two.
+ `_enrich_new_video_entries` above (the original "no root, whole
+ shared tree" call turned out wrong against a real messy library:
+ everything under every shared folder got mixed together with no way
+ to scope it down). `_enriched_attempted` is shared with the video
+ path — content-addressed ids never collide across the two.
"""
if not self._audio_enricher or not self._roster:
return
@@ -1596,9 +1599,10 @@ class NodeDaemon:
# not the shared root it lives in — so the ancestor walk
# (enrich_audio._artist_album_from_ancestors) treats a flat
# top-level folder right under the configured Music directory as
- # ambiguous (artist-or-release, musicbay.md §2.1), rather than one
- # level too shallow when that directory is itself a subfolder.
- # With several configured, each file is measured against its own:
+ # ambiguous (artist-or-release, docs/MESHBAY_DESIGN.md §9.8),
+ # rather than one level too shallow when that directory is
+ # itself a subfolder. With several configured, each file is
+ # measured against its own:
# a single shared boundary would be wrong for all but one of them.
self._audio_enricher.spawn(entry, file_path, on_done,
boundaries.get(owner))
@@ -1639,8 +1643,8 @@ class NodeDaemon:
async def _enrich_new_photo_entries(self, indexer: DirectoryIndexer, entries: list) -> None:
"""
- Photos app (docs/photos.md §5): fire (never await further) thumbnail/
- EXIF enrichment for unattempted image entries under any of the
+ Photos app (docs/MESHBAY_DESIGN.md §9.9): fire (never await further)
+ thumbnail/EXIF enrichment for unattempted image entries under any of the
group's configured photo_roots. Same gate shape as
`_enrich_new_video_entries`/`_enrich_new_audio_entries` — no root
configured yet means no work, since thumbnailing every image in a
@@ -1678,8 +1682,9 @@ class NodeDaemon:
so a folder that already had photos in it before it was added to
photo_roots would otherwise never get enriched at all. Also covers
a root being *removed*: nothing un-enriches on removal (the cache
- entry is harmless, just unused — docs/photos.md's cache is
- disposable), so re-sweeping the new set is enough.
+ entry is harmless, just unused — the media cache is disposable and
+ tied to the index, docs/MESHBAY_DESIGN.md §6.5), so re-sweeping the
+ new set is enough.
"""
indexer = self._state.get("indexers", {}).get(group_id)
if not indexer:
diff --git a/packages/meshbay-node/src/meshbay_node/indexer/enrich.py b/packages/meshbay-node/src/meshbay_node/indexer/enrich.py
index 6eeb1ef..b44a246 100644
--- a/packages/meshbay-node/src/meshbay_node/indexer/enrich.py
+++ b/packages/meshbay-node/src/meshbay_node/indexer/enrich.py
@@ -4,7 +4,7 @@ filename parsing (title_parse), and thumbnail generation (ffmpeg) for a
newly-added video IndexEntry.
Runs through its own small bounded worker pool — separate from the streaming
-transcode pool (docs/mediacenter.md §5.2, mirroring webrtc_server.py's
+transcode pool (docs/MESHBAY_DESIGN.md §6.5, mirroring webrtc_server.py's
`_transcode_semaphore`) — so indexing a large library never blocks on this,
and enrichment never competes with an active viewer for CPU. The scan itself
already put the entry in the index with hash/size/type only; this fills in
@@ -33,26 +33,27 @@ THUMB_WIDTH = 320
# "Show/SeasonFolder/episode.mkv" is the expected shape, with a little slack
# for an extra wrapper folder — not an attempt to find the exact group root.
MAX_ANCESTOR_DEPTH = 4
-# Bounds the "borrow a title from a sibling episode filename" scan (§3.4) so
-# a folder with thousands of files costs a fixed, small amount of work.
+# Bounds the "borrow a title from a sibling episode filename" scan
+# (docs/MESHBAY_DESIGN.md §9.7) so a folder with thousands of files costs a
+# fixed, small amount of work.
MAX_SIBLINGS_CHECKED = 20
# Bounds the whole-show scan _synthetic_episode_number uses to rank a
-# season's files across more than one folder (§3.4c) — a show's total file
-# count, not just one folder's, so this needs more headroom than
-# MAX_SIBLINGS_CHECKED.
+# season's files across more than one folder (docs/MESHBAY_DESIGN.md §9.7) —
+# a show's total file count, not just one folder's, so this needs more
+# headroom than MAX_SIBLINGS_CHECKED.
MAX_SEASON_FILES_CHECKED = 500
def _season_and_show_from_ancestors(file_path: Path) -> tuple[int, Path] | None:
"""
- §3.4/§3.4c: walk up ancestor folders for a season-like one (a numbered
- season, or Specials/Bonus/Extras -> season 0) — season from the first
- (innermost) match, but the show's own name from *above every
- consecutive season-like ancestor*, not just the first one. A
- per-season Bonus folder (`Show/Season N/Bonus/file.ext`) is nested two
- levels inside the show, both of them season-like on their own
- ("Bonus" and "Season N") — stopping at the first would hand back
- "Season N" as the show's name instead of "Show".
+ docs/MESHBAY_DESIGN.md §9.7: walk up ancestor folders for a season-like one
+ (a numbered season, or Specials/Bonus/Extras -> season 0) — season from the
+ first (innermost) match, but the show's own name from *above every
+ consecutive season-like ancestor*, not just the first one. A per-season
+ Bonus folder (`Show/Season N/Bonus/file.ext`) is nested two levels inside
+ the show, both of them season-like on their own ("Bonus" and "Season N") —
+ stopping at the first would hand back "Season N" as the show's name instead
+ of "Show".
Trusted over any per-file guessit title once found: a bare episode
numbering convention with no show name embedded at all
@@ -87,15 +88,17 @@ def _season_and_show_from_ancestors(file_path: Path) -> tuple[int, Path] | None:
def _title_from_siblings(file_path: Path) -> str | None:
"""
- §3.4: an episode filename with no show name in it borrows the title from
- a representative sibling in the same folder, never from the folder name
- alone (an acronym-named show folder is a real, observed case).
+ docs/MESHBAY_DESIGN.md §9.7: an episode filename with no show name in it
+ borrows the title from a representative sibling in the same folder, never
+ from the folder name alone (an acronym-named show folder is a real,
+ observed case).
Requires the sibling to carry its own episode number too, not just a
- title — a folder where every file is a one-off-named Special (§3.4b)
- has plenty of `display_title`s (guessit reads *a* title off nearly
- anything) but none of them name the show; requiring a real episode
- number alongside is what tells apart a genuinely representative sibling
+ title — a folder where every file is a one-off-named Special
+ (docs/MESHBAY_DESIGN.md §9.7) has plenty of `display_title`s (guessit
+ reads *a* title off nearly anything) but none of them name the show;
+ requiring a real episode number alongside is what tells apart a
+ genuinely representative sibling
from another Special just like this one.
"""
try:
@@ -119,11 +122,11 @@ def _title_from_siblings(file_path: Path) -> str | None:
def _synthetic_episode_number(file_path: Path, show_root: Path, season: int) -> int:
"""
- §3.4b/§3.4c: a Specials/Bonus folder's files often carry no episode
- number at all — each is just named after its own one-off title. The
- frontend (video-app.js's buildSeasons) sorts within a season by this
- number but only needs it to provide a stable order, not to mean
- anything beyond that.
+ docs/MESHBAY_DESIGN.md §9.7: a Specials/Bonus folder's files often carry no
+ episode number at all — each is just named after its own one-off title. The
+ frontend (video-app.js's buildSeasons) sorts within a season by this number
+ but only needs it to provide a stable order, not to mean anything beyond
+ that.
Ranked across the *whole show*, not just this file's own folder: season
0 routinely spans more than one folder under the show's root — a
@@ -289,10 +292,11 @@ class Enricher:
and (title_parse.has_episode_marker(entry.name)
or title_parse.year_in(entry.name) is None)):
# No season-like ancestor at all (a flat library) but the
- # filename itself carries season+episode (§3.4) — *and* it
- # is a real marker, not guessit reading a bare number as
- # SxxExx. A movie whose "1080p" tag was truncated to "108",
- # or "1280" left in the name, otherwise parses to S01E08 /
+ # filename itself carries season+episode
+ # (docs/MESHBAY_DESIGN.md §9.7) — *and* it is a real marker,
+ # not guessit reading a bare number as SxxExx. A movie whose
+ # "1080p" tag was truncated to "108", or "1280" left in the
+ # name, otherwise parses to S01E08 /
# S12E80 and gets shelved as a nonexistent series
# (found live 2026-08-29). A genuine flat-dumped episode
# has an explicit SxxExx/1x08/"Episode N" marker; a movie
diff --git a/packages/meshbay-node/src/meshbay_node/indexer/enrich_audio.py b/packages/meshbay-node/src/meshbay_node/indexer/enrich_audio.py
index 4fa08ef..d484e35 100644
--- a/packages/meshbay-node/src/meshbay_node/indexer/enrich_audio.py
+++ b/packages/meshbay-node/src/meshbay_node/indexer/enrich_audio.py
@@ -1,7 +1,7 @@
"""
Index-time enrichment for the Music group app: embedded tag/cover
extraction (mutagen) and filename-parse fallback for a newly-added audio
-IndexEntry (docs/musicbay.md §2.1, §6).
+IndexEntry (docs/MESHBAY_DESIGN.md §9.8).
Runs through its own small bounded worker pool, the same discipline as the
Videos app's `enrich.py` — separate from any other pool, never blocking a
@@ -15,14 +15,13 @@ block the event loop.
MusicBrainz lookups are **not** done here. Tag/cover extraction is free and
local, so it runs for every audio file the Music app is enabled for,
regardless of whether MusicBrainz itself is turned on for the group — the
-flat view (docs/musicbay.md §5.2) needs nothing more than this. MusicBrainz
+flat view (docs/MESHBAY_DESIGN.md §9.8) needs nothing more than this. MusicBrainz
is a separate, lazy, per-request enrichment (`music_meta_req`, handled in
webrtc_server.py), the same "fetched on demand, cached once" shape TMDB
already uses.
**Revised 2026-08-24** against a real ~5700-file library (folder-per-artist
-mostly, but not uniformly — see musicbay.md's own "what got measured" note
-if one gets added). Two findings drove this revision, both confirmed with
+mostly, but not uniformly). Two findings drove this revision, both confirmed with
real data before writing the fix:
1. The original ancestor walk always went up two levels (parent = album,
diff --git a/packages/meshbay-node/src/meshbay_node/indexer/enrich_photo.py b/packages/meshbay-node/src/meshbay_node/indexer/enrich_photo.py
index 93a68cc..6613f85 100644
--- a/packages/meshbay-node/src/meshbay_node/indexer/enrich_photo.py
+++ b/packages/meshbay-node/src/meshbay_node/indexer/enrich_photo.py
@@ -3,7 +3,7 @@ Index-time enrichment for the Photos group app: a resized thumbnail and a
minimal, best-effort info set (`taken_at`, `camera`) read from the image's
own EXIF block, for a newly-added image IndexEntry.
-Deliberately small — docs/photos.md §2.4 is explicit that this app does not
+Deliberately small — docs/MESHBAY_DESIGN.md §9.9 is explicit that this app does not
build a full EXIF-viewer panel. Two fields only, both best-effort (missing
EXIF is the ordinary case for a screenshot or a re-saved/edited image, not
an error). GPS is never read here, on purpose: it is a location disclosure
@@ -12,7 +12,8 @@ module extracts, caches, or hands it to a caller.
Runs through its own small bounded worker pool, separate from the video
(ffmpeg) and audio (mutagen) enrichment pools — mirrors enrich.py exactly,
-per docs/photos.md §5's "never shared with either" rule, even though
+per docs/MESHBAY_DESIGN.md §6.5's "its own small bounded pool, never the
+streaming pool" rule, even though
Pillow's own work is comparatively cheap: a burst of hundreds of newly
shared photos should not peg every CPU core at once.
"""
@@ -100,7 +101,7 @@ def _read_image(file_path: Path) -> tuple[bytes, int, int, int | None, str | Non
# width/height and resizing — otherwise a phone photo stored
# "sideways" reports its raw, pre-rotation dimensions (swapped from
# what it actually displays as) and produces a sideways thumbnail
- # (docs/photos.md §2.4). Never reads Orientation itself as a
+ # (docs/MESHBAY_DESIGN.md §9.9). Never reads Orientation itself as a
# client-visible field; this is display correction only, and
# width/height must describe the *displayed* image, matching what
# the lightbox and the info panel show.
diff --git a/packages/meshbay-node/src/meshbay_node/indexer/indexer.py b/packages/meshbay-node/src/meshbay_node/indexer/indexer.py
index 887d435..3fe4f3e 100644
--- a/packages/meshbay-node/src/meshbay_node/indexer/indexer.py
+++ b/packages/meshbay-node/src/meshbay_node/indexer/indexer.py
@@ -82,7 +82,8 @@ def _is_indexable(path: Path) -> bool:
# Found live: a 1256-byte ".mp3" with no audio stream at all, just an ID3
# tag — a truncated/corrupted rip, sitting between two good tracks of the
-# same album (docs/musicbay.md). A source this small claiming to be audio
+# same album (docs/MESHBAY_DESIGN.md §9.8). A source this small claiming to
+# be audio
# is far more likely broken than real, so it is skipped before ever being
# hashed rather than indexed and left to fail at playback time. Scoped to
# audio only — a tiny real file of any other type is still worth indexing.
@@ -186,9 +187,8 @@ def _size_files(files: list[Path]) -> list[tuple[Path, int]]:
executor for the same reason `_walk_root` is (its own docstring above).
Previously a plain loop straight on the asyncio event loop thread: for a
root with many thousands of files (a real personal library, not a
- hypothetical — docs/musicbay.md's own "several thousand files" example)
- that blocked the entire daemon, every WebRTC session and the admin UI
- included, for as long as the stat() calls took — and did so *before*
+ hypothetical) that blocked the entire daemon, every WebRTC session and
+ the admin UI included, for as long as the stat() calls took — and did so *before*
`_scan_root` had even set `progress.scanning`, so a consumer polling it
saw "not scanning" the whole time real, blocking work was happening.
"""
@@ -673,8 +673,9 @@ class DirectoryIndexer:
Entries under a root that is gone from the config are dropped — the
operator removed it deliberately, which is not the same event as a
- volume disappearing, and conflating the two is what §6.9 exists to
- prevent. Roots that survive keep their entries; new ones are scanned.
+ volume disappearing, and conflating the two is what
+ docs/MESHBAY_DESIGN.md §6.2 exists to prevent. Roots that survive
+ keep their entries; new ones are scanned.
The set takes effect before anything is scanned: the roots table, the
watcher and `self.roots` all move at once. With ``wait=False`` the scan
diff --git a/packages/meshbay-node/src/meshbay_node/indexer/title_parse.py b/packages/meshbay-node/src/meshbay_node/indexer/title_parse.py
index beb0d3f..14728ff 100644
--- a/packages/meshbay-node/src/meshbay_node/indexer/title_parse.py
+++ b/packages/meshbay-node/src/meshbay_node/indexer/title_parse.py
@@ -1,18 +1,18 @@
"""
Filename -> title/year/season/episode parsing for the Videos group app.
-Wraps `guessit` and layers the fixes from docs/mediacenter.md §3.3/§3.4 on
+Wraps `guessit` and layers the fixes from docs/MESHBAY_DESIGN.md §9.7 on
top of it: none of them are per-title hacks, each is a generic rule found
by validating guessit's raw output against real TMDB search results over a
~1950-file library (movies, TV shows, and a small franchise set).
-Scope is deliberately narrow (§3.5): title, year, season, episode. Technical
+Scope is deliberately narrow: title, year, season, episode. Technical
facts (resolution, codec, duration) come from ffprobe, never the filename —
a mislabeled `1080p` tag is a real, observed failure mode.
This module never touches the filesystem or the network. The orchestration
that decides *which* file supplies a show's title (a representative episode
-filename, not the folder name — §3.4) lives in the indexer, which has the
+filename, not the folder name) lives in the indexer, which has the
directory listing; this module only parses strings it's handed.
"""
@@ -91,7 +91,7 @@ _YEAR_RE = re.compile(r"(?<!\d)(?:19|20)\d{2}(?!\d)")
def year_in(text: str) -> int | None:
"""First 19xx/20xx in `text`, or None — used to lift a year off a show
- folder name ("Some.Show.2022.S01") for the search fallback (§10.1/V8)."""
+ folder name ("Some.Show.2022.S01") for the search fallback (V8)."""
m = _YEAR_RE.search(text or "")
return int(m.group(0)) if m else None
@@ -102,7 +102,7 @@ def clean_query(s: str) -> str:
parenthesized-year stripping `naive_title` does. `naive_title` assumes
a real filename; a show's `display_title` is a folder basename
("Some.Show.Name" — `rsplit('.', 1)` would eat ".Name"), so it needs a
- gentler normaliser (§10.1/V8).
+ gentler normaliser (V8).
"""
s = re.sub(r"[._-]+", " ", s or "")
s = _strip_editions(s)
@@ -115,7 +115,7 @@ def _strip_editions(title: str) -> str:
def naive_title(filename: str) -> str:
"""
- The mandated fallback (§3.6, §4.1): strip the extension, replace every
+ The mandated fallback: strip the extension, replace every
`.`/`_`/`-` with a space, drop a trailing parenthesized year, collapse
whitespace. Always computable, never fails, used both as the flat-mode
display name of last resort and as a second TMDB query candidate.
@@ -150,14 +150,14 @@ def sequel_variants(title: str) -> list[str]:
A trailing sequel index often has no exact match in the real TMDB
title: the file has a digit where TMDB uses a Roman numeral (or the
reverse), spells the number out, or wraps it as "Part N" / "Chapitre N"
- (§3.3 row 4, §10.1/V10). Returns extra candidate titles to try — the
+ (V10). Returns extra candidate titles to try — the
index re-rendered as digit and as Roman numeral, plus (only when there
is no "Part"/"Episode"/… keyword) the bare base.
The bare base is withheld for a keyword'd index — "<Saga> Chapter III"
→ "<Saga>" — because a franchise's bare name is very often a real,
*different* film (the series' first entry), and that variant matched
- every later entry to it (§10.1/V14). Without the keyword ("<Franchise>
+ every later entry to it (V14). Without the keyword ("<Franchise>
3") the number is decoration and the bare base is the right thing to
try.
"""
@@ -190,7 +190,7 @@ def sequel_variants(title: str) -> list[str]:
def season_from_folder_name(name: str) -> int | None:
"""
- §3.4: a season-like ancestor folder, vocabulary-driven rather than
+ A season-like ancestor folder, vocabulary-driven rather than
assuming a numeric convention everywhere. A specials/bonus/extras
folder maps to season 0 (matching TMDB's own `season_number: 0`).
Returns None if `name` doesn't look like a season folder at all.
@@ -212,7 +212,8 @@ def season_from_folder_name(name: str) -> int | None:
@dataclass
class ParsedName:
display_title: str | None # None => caller must supply from elsewhere (e.g. a sibling file)
- alt_title: str | None = None # guessit's alternative_title, a second query candidate (§3.3 row 1)
+ alt_title: str | None = None # guessit's alternative_title, a second
+ # query candidate
naive_title: str = "" # always available, fully punctuation-normalized fallback
year: int | None = None
season: int | None = None
@@ -249,7 +250,7 @@ def parse_movie_filename(filename: str) -> ParsedName:
-# ── Music app (docs/musicbay.md §2.1) ────────────────────────────────────────
+# ── Music app (docs/MESHBAY_DESIGN.md §9.8) ────────────────────────────────────────
#
# Filename parsing is the *fallback* here, not the primary source (unlike
# Videos, where guessit does all the work): embedded ID3/Vorbis tags are read
@@ -307,7 +308,7 @@ def strip_track_prefix(text: str) -> str:
# "Season 1"/"Saison 1". guessit will also invent a season+episode from a
# bare 3-4 digit run ("1080p" truncated to "108" -> S01E08; "1280" ->
# S12E80), which is how a plain movie ends up shelved as a series
-# (§10.1/V14). The indexer uses this to tell a real flat-library episode
+# (V14). The indexer uses this to tell a real flat-library episode
# from that hallucination.
_EPISODE_MARKER_RE = re.compile(
r"s\d{1,2}[\s._-]*e\d{1,3}"
@@ -326,9 +327,9 @@ def has_episode_marker(filename: str) -> bool:
def parse_episode_filename(filename: str) -> ParsedName:
"""
Parse an episode filename. `display_title` may come back None (e.g.
- `S08E02.SUBFRENCH.720p.mkv` carries no show name at all, §3.2) — the
+ `S08E02.SUBFRENCH.720p.mkv` carries no show name at all) — the
indexer then supplies the show title from a representative sibling
- filename in the same folder rather than the folder name itself (§3.4).
+ filename in the same folder rather than the folder name itself.
"""
g = guessit(filename)
title = g.get("title")
@@ -351,7 +352,7 @@ def parse_episode_filename(filename: str) -> ParsedName:
)
-# A bare leading episode number, no show name attached (§3.4c) — the same
+# A bare leading episode number, no show name attached — the same
# shape as music's _TRACK_PREFIX_RE, capped at 3 digits for the same reason:
# a leading year ("2010 - Episode.mkv") is 4 digits and must not match.
# guessit's own `episode` is not a substitute here: given exactly 3 digits it
diff --git a/packages/meshbay-node/src/meshbay_node/media_cache.py b/packages/meshbay-node/src/meshbay_node/media_cache.py
index 2898dea..9dfdf73 100644
--- a/packages/meshbay-node/src/meshbay_node/media_cache.py
+++ b/packages/meshbay-node/src/meshbay_node/media_cache.py
@@ -4,8 +4,8 @@ Videos and Music group apps.
Node-wide (not per-group, `data_dir/media_cache.db`), same rationale as
`tmdb_enabled`/`tmdb_api_token` (and `musicbrainz_enabled`,
-docs/musicbay.md §6) living in `group_settings` under the `group_id=""`
-sentinel (docs/mediacenter.md §5.5): the credential/budget is one
+docs/MESHBAY_DESIGN.md §9.8) living in `group_settings` under the `group_id=""`
+sentinel (docs/MESHBAY_DESIGN.md §9.7): the credential/budget is one
operator's, and a thumbnail or cover image is the same bytes
regardless of which group happens to share the file. The `file_mbid`/
`mbid_meta` tables below are the Music app's equivalent of `file_tmdb`/
@@ -13,7 +13,8 @@ regardless of which group happens to share the file. The `file_mbid`/
release's cover is cached under a synthetic `musicbrainz:{mbid}` file_id,
the same trick `_fetch_and_cache_poster` uses for a TMDB poster_path).
-Disposable and rebuildable, like the rest of the file index (§1, §2) — never
+Disposable and rebuildable, like the rest of the file index
+(docs/MESHBAY_DESIGN.md §6.5) — never
a second identity for a file. Every row here is keyed off a value the node
can already derive (a file's own blake3 id, or a TMDB id), so losing this
database costs re-probing/re-fetching, not data.
@@ -98,14 +99,14 @@ CREATE TABLE IF NOT EXISTS mbid_meta (
json TEXT NOT NULL,
fetched_at REAL NOT NULL
);
--- Photos app (docs/photos.md): the technical/EXIF fields enrich_photo.py
--- reads alongside the thumbnail. Durable for the same reason `thumbs` is —
--- without this, only the thumbnail bytes survived a restart, and every
--- image was still fully re-decoded through Pillow just to re-derive
--- width/height/taken_at/camera, which get_thumb_hash_by_file_id's own
--- cache hit had already proven unnecessary. thumb_hash is not duplicated
--- here — get_thumb_hash_by_file_id(file_id) already answers that, and a
--- second copy would just be one more place for the two to drift.
+-- Photos app (docs/MESHBAY_DESIGN.md §9.9): the technical/EXIF fields
+-- enrich_photo.py reads alongside the thumbnail. Durable for the same reason
+-- `thumbs` is — without this, only the thumbnail bytes survived a restart, and
+-- every image was still fully re-decoded through Pillow just to re-derive
+-- width/height/taken_at/camera, which get_thumb_hash_by_file_id's own cache
+-- hit had already proven unnecessary. thumb_hash is not duplicated here —
+-- get_thumb_hash_by_file_id(file_id) already answers that, and a second copy
+-- would just be one more place for the two to drift.
CREATE TABLE IF NOT EXISTS photo_meta (
file_id TEXT PRIMARY KEY,
width INTEGER,
@@ -116,10 +117,10 @@ CREATE TABLE IF NOT EXISTS photo_meta (
"""
# TMDB overviews/ratings do drift; a file's own resolved tmdb_id does not
-# need re-checking on this schedule, only the metadata blob (§5.4, V3).
+# need re-checking on this schedule, only the metadata blob (V3).
TMDB_META_TTL_SECS = 30 * 86400
-# Same default as TMDB (docs/musicbay.md §6) — MusicBrainz release data is
+# Same default as TMDB (docs/MESHBAY_DESIGN.md §9.8) — MusicBrainz release data is
# not expected to drift faster; revisit if that proves wrong in practice.
MUSICBRAINZ_META_TTL_SECS = 30 * 86400
@@ -231,7 +232,7 @@ class MediaCache:
Full per-file reset: forget the match *and* any manual override
marker, so the next `media_meta_req` re-resolves from scratch with
the current matcher. This is the explicit operator "re-match this
- one" action (§10.1/V13) — deliberately stronger than
+ one" action (V13) — deliberately stronger than
`clear_file_tmdb`, which spares an override.
"""
await self._db.execute("DELETE FROM file_tmdb WHERE file_id = ?", (file_id,))
@@ -282,9 +283,10 @@ class MediaCache:
# ── tmdb id + season number -> season-level metadata json ────────────────
#
# A show's own overview (tmdb_meta above) is one static field an operator
- # found does not necessarily describe every season alike (mediacenter.md
- # §5.4) — this is TMDB's per-season `overview`/`air_date`/`poster_path`,
- # fetched and cached independently, on the same staleness schedule.
+ # found does not necessarily describe every season alike
+ # (docs/MESHBAY_DESIGN.md §9.7) — this is TMDB's per-season
+ # `overview`/`air_date`/`poster_path`, fetched and cached independently,
+ # on the same staleness schedule.
async def get_season_meta(self, tmdb_id: str, season: int) -> dict | None:
async with self._db.execute(
diff --git a/packages/meshbay-node/src/meshbay_node/media_probe.py b/packages/meshbay-node/src/meshbay_node/media_probe.py
index 81174c8..a6267d9 100644
--- a/packages/meshbay-node/src/meshbay_node/media_probe.py
+++ b/packages/meshbay-node/src/meshbay_node/media_probe.py
@@ -145,7 +145,7 @@ async def probe_video(path: str) -> VideoProbe:
width/height come from the same ffprobe call (one extra `-show_entries`
field, no second process spawn) — resolution is deliberately never
- guessed from the filename (docs/mediacenter.md §3.5).
+ guessed from the filename (docs/MESHBAY_DESIGN.md §9.7).
"""
from meshbay_node.platform import ffprobe_cmd
proc = await asyncio.create_subprocess_exec(
diff --git a/packages/meshbay-node/src/meshbay_node/musicbrainz.py b/packages/meshbay-node/src/meshbay_node/musicbrainz.py
index d5ca1e9..ce2d233 100644
--- a/packages/meshbay-node/src/meshbay_node/musicbrainz.py
+++ b/packages/meshbay-node/src/meshbay_node/musicbrainz.py
@@ -2,7 +2,7 @@
MusicBrainz (musicbrainz.org) + Cover Art Archive (coverartarchive.org)
client for the Music group app.
-Called only by the node, never by a client (docs/musicbay.md §3): the node
+Called only by the node, never by a client (docs/MESHBAY_DESIGN.md §9.8): the node
makes the one lookup per unique release, shared by every member, and self-
paces against MusicBrainz's shared rate limit rather than letting several
members' tile requests multiply it.
@@ -70,8 +70,8 @@ def _similarity(query: str, val: str | None) -> float:
def _best_match_release(artist: str, album: str, results: list[dict]) -> tuple[dict | None, float]:
"""
Same "trust the search's own ranking" shape as tmdb.py's `_best_match`
- (§3.3 of mediacenter.md found a locally-recomputed re-rank pick a
- coincidentally closer-looking wrong result once — no reason to expect
+ (docs/MESHBAY_DESIGN.md §9.7: a locally-recomputed re-rank was found to
+ pick a coincidentally closer-looking wrong result once — no reason to expect
MusicBrainz's own scored search to fare differently under the same
treatment). MusicBrainz already returns results ordered by its own
`score`; only the top one is considered.
diff --git a/packages/meshbay-node/src/meshbay_node/ops.py b/packages/meshbay-node/src/meshbay_node/ops.py
index 2221bea..98f67c5 100644
--- a/packages/meshbay-node/src/meshbay_node/ops.py
+++ b/packages/meshbay-node/src/meshbay_node/ops.py
@@ -283,7 +283,7 @@ async def unpin_member(state: dict, user_id: str) -> dict:
# the key, as `set_gek` does — would make every message anyone ever sent
# permanently unreadable to everybody, which is what a plain GEK-derived
# archive key would have done on the very first `member unpin`
-# (docs/chat-sender-keys.md F4).
+# (finding F4, docs/MESHBAY_DESIGN.md §13.6).
async def _wrap_for_node(state: dict, key: bytes) -> dict:
@@ -1369,7 +1369,8 @@ async def set_node_settings(state: dict, settings: dict) -> dict:
# `webrtc._stream_sem` was assigned here for months. That attribute
# has never existed -- the pool is `ctx["_transcode_sem"]` -- so the
# `hasattr` guard was always False and the setting only ever took
- # effect on a restart, which draft-v6 §2.11 says it does not need.
+ # effect on a restart, which docs/MESHBAY_DESIGN.md §6.8 says it
+ # does not need.
if webrtc is not None:
webrtc.set_capacity(
max_concurrent_streams=updated["max_concurrent_streams"])
@@ -1524,7 +1525,7 @@ async def set_tmdb_config(state: dict, token: str | None = None,
language: str | None = None) -> dict:
"""
Whether the node uses a custom API token instead of the shipped default,
- and in what language it queries TMDB (docs/mediacenter.md §5.5).
+ and in what language it queries TMDB (docs/MESHBAY_DESIGN.md §9.7).
Node-wide (roster.py group_settings, group_id="") rather than per-group
like set_enabled_apps: the token and the shared-cache
@@ -1554,8 +1555,8 @@ async def set_tmdb_config(state: dict, token: str | None = None,
async def set_tmdb_enabled(state: dict, group_id: str, enabled: bool) -> dict:
"""
- Whether TMDB lookups run for this group at all (docs/mediacenter.md
- §5.5) — per-group, unlike set_tmdb_config above: an operator running a
+ Whether TMDB lookups run for this group at all (docs/MESHBAY_DESIGN.md
+ §9.7) — per-group, unlike set_tmdb_config above: an operator running a
real media library alongside test/demo groups on one node wants
outbound TMDB traffic (and API quota) spent for the one that needs it,
not all of them just because one process serves both.
@@ -1576,7 +1577,7 @@ async def set_tmdb_enabled(state: dict, group_id: str, enabled: bool) -> dict:
async def set_musicbrainz_enabled(state: dict, group_id: str, enabled: bool) -> dict:
"""
Whether MusicBrainz lookups run for this group at all
- (docs/musicbay.md §6) — per-group from the start, same reasoning as
+ (docs/MESHBAY_DESIGN.md §9.8) — per-group from the start, same reasoning as
set_tmdb_enabled: a real media-library group and a test/demo group on
one node need not share the decision to make outbound requests.
"""
diff --git a/packages/meshbay-node/src/meshbay_node/roster.py b/packages/meshbay-node/src/meshbay_node/roster.py
index b0ca78b..064ab93 100644
--- a/packages/meshbay-node/src/meshbay_node/roster.py
+++ b/packages/meshbay-node/src/meshbay_node/roster.py
@@ -16,7 +16,7 @@ Three tables:
only in the operator's hands and the invitee's.
The code is what binds a public key to an account without asking the hub
-(finding H3). See `docs/invite-pairing-v1.md`.
+(finding H3). See `docs/MESHBAY_DESIGN.md` §3.4.
"""
from __future__ import annotations
@@ -63,7 +63,7 @@ DEFAULT_DEVICE_REQUEST_TTL = 3600
_SCHEMA = """\
-- One row per DEVICE, not per person. A browser and a desktop client are two
-- keys belonging to one account, and `user_id` alone as the key made the second
--- silently overwrite the first (INSERT OR REPLACE). See docs/desktop-client-v1.md §4.
+-- silently overwrite the first (INSERT OR REPLACE). See docs/MESHBAY_DESIGN.md §3.3.
CREATE TABLE IF NOT EXISTS identities (
user_id TEXT NOT NULL,
username TEXT NOT NULL,
@@ -81,7 +81,7 @@ CREATE TABLE IF NOT EXISTS identities (
-- transcript binds `nonce_node` — the approving connection's handshake
-- nonce — so even a stored signature is unverifiable without it.
--
- -- This is what Tier 2 needs (docs/desktop-client-v1.md §4.8): relayed with
+ -- This is what Tier 2 needs (docs/MESHBAY_DESIGN.md §3.3): relayed with
-- the roster, it lets a member verify for themselves that a second device
-- belongs to an account whose first device they have already pinned,
-- instead of taking the node's word. Verified and discarded until
@@ -407,7 +407,7 @@ class Roster:
Every live device of every active member of one group, with the evidence
that admitted it.
- For Tier 2 (`docs/desktop-client-v1.md` §4.8), and therefore
+ For Tier 2 (`docs/MESHBAY_DESIGN.md` §3.3), and therefore
**member-visible** — unlike `list_identities`, which answers the
operator. Two consequences of that, and both are the price of the
feature rather than oversights:
@@ -750,10 +750,10 @@ class Roster:
return apps
# The TMDB credential and query language are one operator's budget, not a
- # per-group concern (docs/mediacenter.md §5.5) — stored under the
+ # per-group concern (docs/MESHBAY_DESIGN.md §9.7) — stored under the
# group_id="" sentinel, the same precedent as `roster.get_member("",
- # user_id)` authorizing the operator node-wide (desktop-client-v1.md
- # §6.3). Unset means "the shipped default token, TMDB's own default
+ # user_id)` authorizing the operator node-wide (docs/MESHBAY_DESIGN.md
+ # §6.1). Unset means "the shipped default token, TMDB's own default
# language" — the same "absent means the old behaviour" discipline
# enabled_apps already follows.
#
@@ -791,7 +791,7 @@ class Roster:
# Which folder(s) inside the group's shared roots each application uses as
# its entry point. One storage shape for every app, keyed by the app's own
# name, so adding an application needs no change here at all — that is the
- # whole point of the plugin architecture (docs/refactor-groups.md §1.6).
+ # whole point of the plugin architecture (docs/MESHBAY_DESIGN.md §9.3).
#
# Always a JSON list, even for an app that only ever wants one directory.
# Two shapes for one idea is how `video_root` (scalar) and `photo_roots`
diff --git a/packages/meshbay-node/src/meshbay_node/tmdb.py b/packages/meshbay-node/src/meshbay_node/tmdb.py
index c9623df..9a0db12 100644
--- a/packages/meshbay-node/src/meshbay_node/tmdb.py
+++ b/packages/meshbay-node/src/meshbay_node/tmdb.py
@@ -1,7 +1,7 @@
"""
TMDB (themoviedb.org) client for the Videos group app.
-Called only by the node, never by a client (docs/mediacenter.md §2): the
+Called only by the node, never by a client (docs/MESHBAY_DESIGN.md §6.5): the
node holds the one credential and makes the one request per unique title,
shared by every member. Token resolution order (§5.5):
@@ -11,9 +11,9 @@ shared by every member. Token resolution order (§5.5):
exception, so a node with no token configured just serves thumbnails)
The real secret (whichever token resolves) never appears in source control:
-there is no literal fallback value in this file. See mediacenter.md's
-implementation notes on why a shipped default is a deployment concern, not
-a code concern.
+there is no literal fallback value in this file. Videos is the one
+credentialed application (docs/MESHBAY_DESIGN.md §9.7, §9.8), and which
+token ships is a deployment concern, not a code concern.
Results also come back in whatever language the operator configured
(roster.py's `tmdb_language`, e.g. "fr-FR") — one language for the whole
@@ -75,7 +75,7 @@ def _best_match(
top result is what's returned. The similarity ratio rides along purely
as a confidence signal for the caller's fallback decision.
- One narrow exception (§10.1/V9): when the top result is *not* a
+ One narrow exception (V9): when the top result is *not* a
confident textual hit (ratio < 0.6) and a `year` was requested, a
different result of that **exact** release year is preferred. TMDB
already year-filtered the search, so an entry landing on the requested
diff --git a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
index 3bb0df7..4540f2f 100644
--- a/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
+++ b/packages/meshbay-node/src/meshbay_node/transport/webrtc_server.py
@@ -174,7 +174,8 @@ USER_BLOB_ACCOUNT_MAX = 8 * 1024 * 1024
_USER_BLOB_KIND_RE = re.compile(
r"^(playlists|playlist:[A-Za-z0-9_-]{1,64})$")
-# Chat link-preview results, kept in memory only (draft-v6 §2.7: the node
+# Chat link-preview results, kept in memory only (docs/MESHBAY_DESIGN.md §6.5:
+# the node
# produces enrichment on demand and keeps nothing durable — the asking device
# caches). Bounded and time-limited so a busy group cannot grow it without end
# and a page that changed its card is picked up within the hour.
@@ -998,7 +999,7 @@ class WebRTCPeerSession:
self._ctx.get("daemon_state", {}).get("tmdb_token_customized", False)),
"tmdb_language": str(
self._ctx.get("daemon_state", {}).get("tmdb_language") or ""),
- # Music app (docs/musicbay.md §6) — same shape as the TMDB
+ # Music app (docs/MESHBAY_DESIGN.md §9.8) — same shape as the TMDB
# fields above. No language field: MusicBrainz search doesn't
# take one the way TMDB does.
"musicbrainz_enabled": bool(self._group_ctx().get("musicbrainz_enabled", True)),
@@ -1174,7 +1175,7 @@ class WebRTCPeerSession:
}
# The recovery-wrapped copy (MNP 0.14) rides along when present, so a
# client holding the recovery key can re-wrap it under a new
- # passphrase — docs/auth-confirm.md §4.5.
+ # passphrase — docs/MESHBAY_DESIGN.md §3.6.
if kp.get("bundle_enc_recovery"):
resp["bundle_enc_recovery"] = kp["bundle_enc_recovery"]
self._send(resp)
@@ -1558,7 +1559,7 @@ class WebRTCPeerSession:
# A person may hold several devices on one node. The authority admitting a
# new one is a key the node already pinned — never the hub, which has stored
# no user keys since 2026-08-14 and therefore cannot countersign anything.
- # See docs/desktop-client-v1.md §4.
+ # See docs/MESHBAY_DESIGN.md §3.3.
async def _do_device_request(self, msg: dict) -> None:
"""
@@ -1719,7 +1720,7 @@ class WebRTCPeerSession:
#
# This is what lets another member check for themselves that this device
# belongs to an account whose earlier device they have already pinned,
- # instead of taking the node's word (Tier 2, desktop-client-v1.md §4.8).
+ # instead of taking the node's word (Tier 2, docs/MESHBAY_DESIGN.md §3.3).
await roster.pin_identity(
user_id=self._user_id, username=self._username or "",
pk_ed25519=pk_ed_b64, pk_x25519=pk_x_b64, via="device",
@@ -1746,7 +1747,7 @@ class WebRTCPeerSession:
What is checked, in order: the key is a live device *of this account* in
the node's own roster (never a token claim — that is
- `per-node-identity-v1.md`'s rule), the timestamp is fresh, and the
+ `docs/MESHBAY_DESIGN.md` §3.2's rule), the timestamp is fresh, and the
signature verifies over a transcript naming this node, this group and
this connection's nonce. A key that is merely well-formed proves nothing.
@@ -1964,7 +1965,7 @@ class WebRTCPeerSession:
Admission policy for a group, read from the node's own configuration.
Never from the hub: a hub that could declare a group open would be handed
- the key to it (§3.4 of docs/invite-pairing-v1.md).
+ the key to it (docs/MESHBAY_DESIGN.md §3.4).
"""
gctx = (self._ctx.get("groups") or {}).get(group_id) or {}
return gctx.get("join_policy", "invite")
@@ -2290,9 +2291,9 @@ class WebRTCPeerSession:
# include "video" or "music" — both can make outbound third-party
# network calls (TMDB, MusicBrainz) once enabled, so an operator opts a
# group in explicitly rather than getting it for free
- # (docs/mediacenter.md §5.6, docs/musicbay.md §4.4).
- # `helloworld` is the reference implementation (docs/refactor-groups.md
- # §4.1), hidden client-side behind `?dev=1`. It is here because the
+ # (docs/MESHBAY_DESIGN.md §9.7, §9.8).
+ # `helloworld` is the reference implementation (docs/MESHBAY_DESIGN.md
+ # §9.4), hidden client-side behind `?dev=1`. It is here because the
# allow-list is server-side enforcement — a client that names an app this
# node does not know is refused — and an app the node refused could not
# demonstrate anything. This entry and the client's registry line are the
@@ -2363,7 +2364,7 @@ class WebRTCPeerSession:
per-group concern (see _do_tmdb_enabled for the per-group on/off
switch). Signed like the rest: this changes outbound third-party
network traffic the node did not have before the Videos app
- (docs/mediacenter.md §5.5, §8) — an unsigned change would let any
+ (docs/MESHBAY_DESIGN.md §9.7, §6.5) — an unsigned change would let any
member alter egress the operator never agreed to.
"""
token = msg.get("token")
@@ -2746,7 +2747,7 @@ class WebRTCPeerSession:
def _do_musicbrainz_enabled(self, msg: dict) -> None:
"""
Whether MusicBrainz lookups run for this group at all. Per-group
- from the start (docs/musicbay.md §3.2/§6) — signed like
+ from the start (docs/MESHBAY_DESIGN.md §9.8) — signed like
tmdb_enabled: it decides whether this group's members'
Music tab ever makes outbound MusicBrainz traffic.
"""
@@ -3813,13 +3814,13 @@ class WebRTCPeerSession:
self, thumb_hash: str, chunk_index: int, gek: bytes | None,
) -> dict | None:
"""
- docs/mediacenter.md §5.3: a thumbnail is served through the same
+ docs/MESHBAY_DESIGN.md §6.5: a thumbnail is served through the same
chunked file_req path as a real file, resolved against the media
cache instead of the index when the id doesn't match a file.
Sliced by `chunk_index` like a real file's chunks, not just handed
back whole: a thumbnail/poster/cover never approached CHUNK_SIZE so
this used to be equivalent to "only chunk 0 exists", but an audio
- transcode result (docs/musicbay.md, the WMA/Musepack exception) is
+ transcode result (docs/MESHBAY_DESIGN.md §9.8, the WMA/Musepack exception) is
cached in the same media_cache blob store and can be several MB —
genuinely multi-chunk, same as a file read straight off disk.
"""
@@ -3939,7 +3940,7 @@ class WebRTCPeerSession:
async def _fetch_and_cache_poster(media_cache, tmdb_client, poster_path: str | None) -> str | None:
"""
Downloads a TMDB poster/backdrop once, caches it under its own
- blake3 like a video thumbnail (docs/mediacenter.md §5.4), and
+ blake3 like a video thumbnail (docs/MESHBAY_DESIGN.md §9.7), and
returns the hash a client then fetches via the normal file_req/
chunk path (§5.3) — no client ever contacts image.tmdb.org directly.
@@ -3988,7 +3989,7 @@ class WebRTCPeerSession:
async def _do_audio_transcode_request(self, msg: dict) -> None:
"""
- docs/musicbay.md's one exception to "no node-side transcode pool":
+ docs/MESHBAY_DESIGN.md §9.8's one exception to "no node-side transcode pool":
WMA and Musepack tag/cover fine (enrich_audio.py) but decode in no
mainstream browser's <audio> element at all. Transcoded to AAC/M4A
once and cached under its own content hash — same "computed once,
@@ -4195,7 +4196,7 @@ class WebRTCPeerSession:
async def _do_music_meta_request(self, msg: dict) -> None:
"""
- docs/musicbay.md §4.3: MusicBrainz metadata for one track, resolved
+ docs/MESHBAY_DESIGN.md §9.8: MusicBrainz metadata for one track, resolved
from the group's index by its content id. Album-level (release), the
direct analogue of Videos' show-level TMDB caching: one search per
(artist, album) pair serves cover art and canonical naming to every
@@ -4275,7 +4276,7 @@ class WebRTCPeerSession:
async def _do_media_meta_request(self, msg: dict) -> None:
"""
- docs/mediacenter.md §5.4: TMDB metadata for one file, resolved from
+ docs/MESHBAY_DESIGN.md §9.7: TMDB metadata for one file, resolved from
the group's index by its content id (root+relpath the client already
knows from index_sync/index_delta identify the entry; its own `id`
is what actually names one file — never a raw filesystem path off
@@ -4300,7 +4301,7 @@ class WebRTCPeerSession:
media_cache = self._ctx.get("media_cache")
tmdb_client = self._ctx.get("tmdb_client")
- # Per-group, not node-wide (docs/mediacenter.md §5.5, 2026-08-24):
+ # Per-group, not node-wide (docs/MESHBAY_DESIGN.md §9.7, 2026-08-24):
# treated exactly like "no client configured" — same silent, no-error
# degradation, since a member's Videos tab already has to handle "no
# TMDB match" as the ordinary case.
@@ -4409,7 +4410,7 @@ class WebRTCPeerSession:
return
media_cache = self._ctx.get("media_cache")
tmdb_client = self._ctx.get("tmdb_client")
- # Per-group, not node-wide (docs/mediacenter.md §5.5, 2026-08-24) —
+ # Per-group, not node-wide (docs/MESHBAY_DESIGN.md §9.7, 2026-08-24) —
# same silent zero-confidence degradation as "no client configured".
if (media_cache is None or tmdb_client is None
or not self._group_ctx().get("tmdb_enabled", True)):
@@ -4447,7 +4448,7 @@ class WebRTCPeerSession:
async def _do_tmdb_search_request(self, msg: dict) -> None:
"""
Candidate TMDB matches for an operator correcting a wrong automatic
- match (docs/mediacenter.md, §V-whatever this becomes) — a plain
+ match (docs/MESHBAY_DESIGN.md §9.7, §V-whatever this becomes) — a plain
lookup, not a mutation, so unlike `tmdb_override` this needs no
admin authority: any member can see what TMDB itself would offer,
the same as the automatic search already silently does on their
@@ -4460,7 +4461,7 @@ class WebRTCPeerSession:
return
media_cache = self._ctx.get("media_cache")
tmdb_client = self._ctx.get("tmdb_client")
- # Per-group, not node-wide (docs/mediacenter.md §5.5, 2026-08-24) —
+ # Per-group, not node-wide (docs/MESHBAY_DESIGN.md §9.7, 2026-08-24) —
# same silent empty-results degradation as "no client configured":
# a member with TMDB off for this group sees the same "type it in
# yourself" affordance either way, never an error.
@@ -4593,7 +4594,7 @@ class WebRTCPeerSession:
def _do_tmdb_rematch(self, msg: dict) -> None:
"""
An operator dropping one file's cached TMDB match so it re-resolves
- with the current matcher (§10.1/V13) — the one-click alternative to
+ with the current matcher (V13) — the one-click alternative to
the full search-and-pick "Fix match" flow, and reachable without
SSH (`meshbay-node video rematch` clears a whole group). Signed like
`tmdb_override`: `media_cache` is shared node-wide.
@@ -4635,7 +4636,8 @@ class WebRTCPeerSession:
async def _tmdb_search(self, tmdb_client, entry, is_show: bool):
"""
- §3.3's retry ladder — same shape for movies and shows (§10.1/V8).
+ docs/MESHBAY_DESIGN.md §9.7's scored ladder — same shape for movies
+ and shows (V8).
TMDB's own top result is still trusted per query (§3.3's last row —
no local re-ranking of *its* list); what the ladder adds is that it
*scores every candidate query* and keeps the best, instead of
@@ -4681,7 +4683,7 @@ class WebRTCPeerSession:
specific than a punctuation-normalised restatement of `primary`
(an alternative_title, a sequel variant); when it does not and the
primary hit is already decent, the remaining calls are skipped
- (§10.1/V11 — they almost never win and cost a round trip each).
+ (V11 — they almost never win and cost a round trip each).
"""
def _year_of(res: dict) -> int | None:
d = str(res.get("release_date") or res.get("first_air_date") or "")
@@ -5124,7 +5126,8 @@ class WebRTCPeerSession:
async def _do_link_preview_request(self, msg: dict) -> None:
"""
- Unfurl a URL a member pasted into chat (draft-v6 §2.7 enrichment rule:
+ Unfurl a URL a member pasted into chat (docs/MESHBAY_DESIGN.md §6.5's
+ enrichment rule:
the client asks, the node produces on demand, the asking device
caches — nothing durable here).
@@ -5895,13 +5898,14 @@ class WebRTCPeerSession:
# anything failing loudly: a file uploaded from a phone could not be
# deleted from the same person's laptop, and the only symptom was
# "Signature verification failed" on their own file
- # (docs/desktop-client-v1.md §4.8 A).
+ # (docs/MESHBAY_DESIGN.md §3.3).
#
# `uploader_pk` is kept, and stops being the authorization key: it is
# now the audit record of *which device* did it. Authorization is by
# account, through the roster — never through a token claim, which is
- # the protection `per-node-identity-v1.md` added and which a lookup by
- # `uploader_id` in the hub's world would give straight back.
+ # the protection per-node identity keys give (docs/MESHBAY_DESIGN.md
+ # §3.2) and which a lookup by `uploader_id` in the hub's world would
+ # give straight back.
if not (await self._verify_admin_sig(transcript, sig)
or await self._verify_uploader_sig(entry, transcript, sig)):
self._send({"type": "error", "detail": "Signature verification failed"})
@@ -5915,7 +5919,7 @@ class WebRTCPeerSession:
) -> None:
# Node operator only. A group admin who does not run the node has no
# authority over who this node admits (deny by default). Delegation is
- # designed but deferred — see §6.2 of docs/invite-pairing-v1.md.
+ # designed but deferred — see docs/MESHBAY_DESIGN.md §3.4.
if not await self._verify_admin_sig(transcript, sig):
self._send({"type": "error", "detail": "Signature verification failed"})
self._audit("admin_auth_failed", f"invite_create:{pending['subject'][:16]}")
@@ -6220,7 +6224,7 @@ class WebRTCPeerSession:
# node's answer to it, since ffmpeg re-encodes these in real time on
# any machine that can run this daemon. Reported live against an
# Xvid/MP3 .avi. `transcode_incompatible_video`'s own documentation
- # (draft-v6 §2.11) already said "HEVC *and other browser-
+ # (docs/MESHBAY_DESIGN.md §6.8) already said "HEVC *and other browser-
# incompatible video codecs*"; only HEVC was ever wired up.
can_copy = (bool(codec_str)
and raw_video_codec not in BROWSER_INCOMPATIBLE_VIDEO_CODECS)
@@ -6906,9 +6910,9 @@ class WebRTCTransport:
`ctx["_transcode_sem"]`, and `hasattr(webrtc, "_stream_sem")` is always
False. So the hot-swap was a no-op and **`max_concurrent_streams` has
never taken effect from the Node page without a restart**, contrary to
- draft-v6 §2.11. This is the one implementation, on the object that owns
- the state, so the next two caps do not each grow their own copy of the
- mistake.
+ docs/MESHBAY_DESIGN.md §6.8. This is the one implementation, on the
+ object that owns the state, so the next two caps do not each grow their
+ own copy of the mistake.
What resizing means, stated because it is a decision and not a
detail: **the new cap governs new streams; the ones already running are
diff --git a/packages/meshbay-node/src/meshbay_node/ui/app.py b/packages/meshbay-node/src/meshbay_node/ui/app.py
index 96fa137..77491a0 100644
--- a/packages/meshbay-node/src/meshbay_node/ui/app.py
+++ b/packages/meshbay-node/src/meshbay_node/ui/app.py
@@ -9,7 +9,7 @@ are both clients of it. (Chat is served to browsers over MNP/WebRTC, not here.)
Served only on 127.0.0.1 — never network-exposed — and every request is gated
by a per-run session token (11.5.3) written to `<data_dir>/ui-token`. There is
no server-rendered UI: the Node page ships in the desktop client (see
-`docs/refactor-node-ui.md`).
+`docs/MESHBAY_DESIGN.md` §6.7).
"""
import logging
diff --git a/packages/meshbay-node/tests/conftest.py b/packages/meshbay-node/tests/conftest.py
index 692a118..2ca0c8a 100644
--- a/packages/meshbay-node/tests/conftest.py
+++ b/packages/meshbay-node/tests/conftest.py
@@ -8,7 +8,7 @@ from meshbay_node.roots import RootSet
# ffmpeg / ffprobe run via asyncio.create_subprocess_exec, which needs the
# ProactorEventLoop — but the repo-root conftest forces the SelectorEventLoop
-# on win32 so aiortc's ICE stack works there (see devel/windows-devel.md §5).
+# on win32 so aiortc's ICE stack works there (see docs/MESHBAY_DESIGN.md §11.2).
# The two are mutually exclusive on one Windows asyncio loop; until the media
# path gets a thread-based subprocess runner, these tests can't run on win32.
needs_subprocess = pytest.mark.skipif(
@@ -39,7 +39,7 @@ def _restore_media_tool_paths():
_plat._ffmpeg_path, _plat._ffprobe_path = before
-# Windows-only gaps still to close (see devel/windows-devel.md §5/§6).
+# Windows-only gaps still to close (see docs/MESHBAY_DESIGN.md §11.2).
win32_todo = pytest.mark.skipif(
sys.platform == "win32",
reason="Windows behaviour not implemented yet (W3 / platform specifics)",
diff --git a/packages/meshbay-node/tests/test_audio_root_gates_enrichment.py b/packages/meshbay-node/tests/test_audio_root_gates_enrichment.py
index d353d27..251617c 100644
--- a/packages/meshbay-node/tests/test_audio_root_gates_enrichment.py
+++ b/packages/meshbay-node/tests/test_audio_root_gates_enrichment.py
@@ -1,12 +1,11 @@
"""
-Music-app enrichment (tag/cover extraction, docs/musicbay.md §2.1/§6) only
+Music-app enrichment (tag/cover extraction, docs/MESHBAY_DESIGN.md §9.8) only
ever runs for a group that has an audio_root configured, and only for files
under it — see daemon.py's _enrich_new_audio_entries. Same reasoning as
Videos' video_root gate (test_video_root_gates_enrichment.py), added later:
-musicbay.md's original "no root, whole shared tree" call turned out wrong
-against a real messy library, where everything under every shared folder
-got mixed together with no way to scope Music down to just the actual
-music library.
+the original "no root, whole shared tree" call turned out wrong against a
+real messy library, where everything under every shared folder got mixed
+together with no way to scope Music down to just the actual music library.
Setting or changing the folder (ops.set_app_directory) fires a one-off sweep
(_enrich_audio_root_now) of whatever it already contains — same shape as
diff --git a/packages/meshbay-node/tests/test_audio_transcode.py b/packages/meshbay-node/tests/test_audio_transcode.py
index c2f7f64..3e6015d 100644
--- a/packages/meshbay-node/tests/test_audio_transcode.py
+++ b/packages/meshbay-node/tests/test_audio_transcode.py
@@ -1,6 +1,6 @@
"""
Tests for the Music app's one exception to "no node-side transcode pool"
-(docs/musicbay.md §2.2): WMA and Musepack tag/cover fine (enrich_audio.py)
+(docs/MESHBAY_DESIGN.md §9.8): WMA and Musepack tag/cover fine (enrich_audio.py)
but decode in no mainstream browser's <audio> element at all, so
`_do_audio_transcode_request` converts to AAC/M4A on request and caches the
result — served back through the ordinary file_req/chunk path, generalized
diff --git a/packages/meshbay-node/tests/test_bundle_store_recovery.py b/packages/meshbay-node/tests/test_bundle_store_recovery.py
index 10a3400..d649716 100644
--- a/packages/meshbay-node/tests/test_bundle_store_recovery.py
+++ b/packages/meshbay-node/tests/test_bundle_store_recovery.py
@@ -1,5 +1,5 @@
"""
-The recovery-wrapped keypair copy (docs/auth-confirm.md §4.3, MNP 0.14).
+The recovery-wrapped keypair copy (docs/MESHBAY_DESIGN.md §3.6, MNP 0.14).
`bundle_enc_recovery` is a second copy of the identity bundle wrapped under the
account's recovery key. The store has to add the column to a database that
diff --git a/packages/meshbay-node/tests/test_chat_encryption.py b/packages/meshbay-node/tests/test_chat_encryption.py
index 0401d27..e286306 100644
--- a/packages/meshbay-node/tests/test_chat_encryption.py
+++ b/packages/meshbay-node/tests/test_chat_encryption.py
@@ -1,7 +1,7 @@
"""
Chat encryption: what the node stores, what it refuses, and what survives.
-Design A of `docs/chat-sender-keys.md`. Every test here is written as "this
+Design A of `docs/MESHBAY_DESIGN.md` §4.5. Every test here is written as "this
does not work" or "this still works after X" — the regressions the plan's
register names, in the order they would bite.
diff --git a/packages/meshbay-node/tests/test_chat_history_binary.py b/packages/meshbay-node/tests/test_chat_history_binary.py
index 18efbf5..e2397a0 100644
--- a/packages/meshbay-node/tests/test_chat_history_binary.py
+++ b/packages/meshbay-node/tests/test_chat_history_binary.py
@@ -10,7 +10,7 @@ possible place to look for a wire-format error.
The fix keeps plaintext exactly where it has always been (a string in
`payload`, which older clients read) and gives ciphertext its own `ct` field.
-That way this is not a compatibility break either — `docs/chat-sender-keys.md`
+That way this is not a compatibility break either — `docs/MESHBAY_DESIGN.md` §4.5
R3.
"""
diff --git a/packages/meshbay-node/tests/test_chat_multidevice.py b/packages/meshbay-node/tests/test_chat_multidevice.py
index d718b2a..bb61285 100644
--- a/packages/meshbay-node/tests/test_chat_multidevice.py
+++ b/packages/meshbay-node/tests/test_chat_multidevice.py
@@ -11,8 +11,9 @@ what they said.
Neither shows up as an error anywhere. The first is a message that silently
reaches nobody after a second device connects and disconnects; the second is a
phone that never shows what was typed on the laptop. Both are
-`docs/chat-sender-keys.md` F7, and both are the same "keyed by account where it
-should be keyed by connection" mistake as `pin_identity`'s old INSERT OR REPLACE.
+finding F7 (`docs/MESHBAY_DESIGN.md` §13.6), and both are the same "keyed by
+account where it should be keyed by connection" mistake as `pin_identity`'s
+old INSERT OR REPLACE.
"""
from pathlib import Path
diff --git a/packages/meshbay-node/tests/test_cli_dispatch.py b/packages/meshbay-node/tests/test_cli_dispatch.py
index 64f96f1..9bf43d4 100644
--- a/packages/meshbay-node/tests/test_cli_dispatch.py
+++ b/packages/meshbay-node/tests/test_cli_dispatch.py
@@ -168,7 +168,7 @@ def test_the_verb_list_here_matches_the_parser():
f"VERBS above")
# The server-rendered admin UI (and its `ui` verb) were removed in
- # docs/refactor-node-ui.md phase 5. The control API stays; the browser
+ # docs/MESHBAY_DESIGN.md §6.7. The control API stays; the browser
# page does not.
assert "ui" not in declared, "the `ui` verb came back"
diff --git a/packages/meshbay-node/tests/test_device_on_connection.py b/packages/meshbay-node/tests/test_device_on_connection.py
index 3da8a8c..03ed9ca 100644
--- a/packages/meshbay-node/tests/test_device_on_connection.py
+++ b/packages/meshbay-node/tests/test_device_on_connection.py
@@ -12,7 +12,7 @@ oldest live device" and calling it the answer:
* `_admin_exec_file_delete`, which authorized deletion against **that exact
key** — so a person could not delete their own file from their other device,
and the only symptom was "Signature verification failed" on their own upload
- (`docs/desktop-client-v1.md` §4.8 A).
+ (`docs/MESHBAY_DESIGN.md` §3.3).
`device_hello` closes the first: additive, signed, refused unless the key is a
live device *of this account in the node's own roster*. The second is closed by
@@ -224,8 +224,9 @@ class _Entry:
async def test_a_second_device_can_delete_the_first_devices_upload(
tmp_path, roster):
"""
- §4.8 A. Alice uploads from her phone and deletes from her desktop. Before
- the fix this failed with "Signature verification failed" on her own file.
+ docs/MESHBAY_DESIGN.md §3.3. Alice uploads from her phone and deletes
+ from her desktop. Before the fix this failed with "Signature verification
+ failed" on her own file.
"""
sk_phone, pk_phone, pk_x_phone = _keys()
sk_desk, pk_desk, pk_x_desk = _keys()
diff --git a/packages/meshbay-node/tests/test_enrich.py b/packages/meshbay-node/tests/test_enrich.py
index 75cf0ee..e059bbe 100644
--- a/packages/meshbay-node/tests/test_enrich.py
+++ b/packages/meshbay-node/tests/test_enrich.py
@@ -134,7 +134,7 @@ def test_synthetic_episode_number_does_not_collide_across_per_season_bonus_folde
# ── end-to-end against a real (tiny, synthetic) video file ──────────────────
# ffprobe runs via asyncio subprocess, which the win32 selector loop (forced
-# for aiortc, see devel/windows-devel.md §5) cannot spawn.
+# for aiortc, see docs/MESHBAY_DESIGN.md §11.2) cannot spawn.
pytestmark_ffmpeg = pytest.mark.skipif(
not _HAVE_FFMPEG or sys.platform == "win32",
reason="needs ffprobe installed and a ProactorEventLoop",
diff --git a/packages/meshbay-node/tests/test_enrich_photo.py b/packages/meshbay-node/tests/test_enrich_photo.py
index 7684f40..81367eb 100644
--- a/packages/meshbay-node/tests/test_enrich_photo.py
+++ b/packages/meshbay-node/tests/test_enrich_photo.py
@@ -172,7 +172,7 @@ async def test_enricher_corrects_orientation(tmp_path, media_cache):
def test_gps_is_never_read_by_this_module():
"""
- docs/photos.md §2.4/§11: GPS must never be extracted, cached, or handed
+ docs/MESHBAY_DESIGN.md §9.9: GPS must never be extracted, cached, or handed
to a caller — a location disclosure the instant it is surfaced to every
group member. Grep-based, the same discipline test_hub_address_seam.py/
test_task_lifetime.py already apply elsewhere in this codebase to a
diff --git a/packages/meshbay-node/tests/test_group_roster.py b/packages/meshbay-node/tests/test_group_roster.py
index cb9828c..a41cce2 100644
--- a/packages/meshbay-node/tests/test_group_roster.py
+++ b/packages/meshbay-node/tests/test_group_roster.py
@@ -1,9 +1,9 @@
"""
Tier 2: a member verifies another member's device for themselves.
-`docs/desktop-client-v1.md` §4.8, and `docs/chat-sender-keys.md` §13, which
-recorded why it could not ship with the encryption: **the evidence was not being
-kept.** `_do_device_add` verified the countersignature and stored only
+`docs/MESHBAY_DESIGN.md` §3.3, which records why it could not ship with the
+encryption: **the evidence was not being kept.** `_do_device_add` verified the
+countersignature and stored only
`added_by_pk` — *which* key approved, never the proof — and the transcript binds
`nonce_node`, the approving connection's handshake nonce, so even a stored
signature was unverifiable by anyone who was not on that connection.
diff --git a/packages/meshbay-node/tests/test_media_cache.py b/packages/meshbay-node/tests/test_media_cache.py
index f12a366..dc78d8d 100644
--- a/packages/meshbay-node/tests/test_media_cache.py
+++ b/packages/meshbay-node/tests/test_media_cache.py
@@ -133,7 +133,7 @@ async def test_prune_file_also_clears_the_override_marker(cache):
assert await cache.clear_tmdb_matches(["gone"]) == 1
-# ── Music app (docs/musicbay.md §6) — file_mbid/mbid_meta ────────────────────
+# ── Music app (docs/MESHBAY_DESIGN.md §9.8) — file_mbid/mbid_meta ────────────
@pytest.mark.asyncio
async def test_file_mbid_round_trip(cache):
diff --git a/packages/meshbay-node/tests/test_musicbrainz.py b/packages/meshbay-node/tests/test_musicbrainz.py
index cff7fea..ccf7597 100644
--- a/packages/meshbay-node/tests/test_musicbrainz.py
+++ b/packages/meshbay-node/tests/test_musicbrainz.py
@@ -156,7 +156,8 @@ async def test_no_contact_configured_makes_no_request():
result, ratio = await client.search_release("Anyone", "Anything")
assert result is None
- assert calls == [], "an unidentified client must never be sent — see musicbay.md §3.1"
+ assert calls == [], ("an unidentified client must never be sent "
+ "— see docs/MESHBAY_DESIGN.md §9.8")
await client.close()
@@ -227,7 +228,7 @@ async def test_cover_art_found_returns_bytes():
@pytest.mark.asyncio
async def test_calls_are_paced_at_least_min_interval_apart():
"""
- docs/musicbay.md §3.2: the ~1 req/s courtesy limit is this node's own
+ docs/MESHBAY_DESIGN.md §9.8: the ~1 req/s courtesy limit is this node's own
job, not something the server hands out — verified by timing two calls
back to back rather than mocking the clock, so a change to the pacing
implementation that still meets the contract doesn't break this test.
diff --git a/packages/meshbay-node/tests/test_musicbrainz_enabled_policy.py b/packages/meshbay-node/tests/test_musicbrainz_enabled_policy.py
index e86a3f3..76b687e 100644
--- a/packages/meshbay-node/tests/test_musicbrainz_enabled_policy.py
+++ b/packages/meshbay-node/tests/test_musicbrainz_enabled_policy.py
@@ -1,7 +1,8 @@
"""
-Whether MusicBrainz lookups run *at all* for a group — docs/musicbay.md
-§3.2/§6. Per-group from the start (unlike tmdb_enabled, which started
-node-wide and moved per-group later once the lesson was already learned).
+Whether MusicBrainz lookups run *at all* for a group —
+docs/MESHBAY_DESIGN.md §9.8. Per-group from the start (unlike tmdb_enabled,
+which started node-wide and moved per-group later once the lesson was already
+learned).
Same shape as test_tmdb_enabled_policy.py: a signed operator instruction,
scoped to self._group_id (not passed explicitly on the wire), stored via
roster.py's group_settings table under the real group_id.
diff --git a/packages/meshbay-node/tests/test_roster_pairing.py b/packages/meshbay-node/tests/test_roster_pairing.py
index 14285e7..2e8c183 100644
--- a/packages/meshbay-node/tests/test_roster_pairing.py
+++ b/packages/meshbay-node/tests/test_roster_pairing.py
@@ -7,7 +7,7 @@ or a mistake that must not work. The one to keep an eye on is
sovereignty inert as shipped, and it fails closed, so nothing else in the suite
notices if it comes back.
-See `docs/invite-pairing-v1.md`.
+See `docs/MESHBAY_DESIGN.md` §3.4.
"""
import base64
diff --git a/packages/meshbay-node/tests/test_season_and_search_requests.py b/packages/meshbay-node/tests/test_season_and_search_requests.py
index 44d7da6..9dcc6ce 100644
--- a/packages/meshbay-node/tests/test_season_and_search_requests.py
+++ b/packages/meshbay-node/tests/test_season_and_search_requests.py
@@ -1,6 +1,7 @@
"""
`_do_season_meta_request` (per-season TMDB overview/poster/air_date, for the
-season-tab view — docs/mediacenter.md §5.4's fix for a 3-season show whose
+season-tab view — docs/MESHBAY_DESIGN.md §9.7's per-season text, found live
+against a 3-season show whose
overview read as season-3-specific for every season) and
`_do_tmdb_search_request` (raw TMDB candidates for an operator correcting a
wrong automatic match). Neither is a signed admin op — see each handler's own
diff --git a/packages/meshbay-node/tests/test_security_regressions.py b/packages/meshbay-node/tests/test_security_regressions.py
index a229153..d6ecb71 100644
--- a/packages/meshbay-node/tests/test_security_regressions.py
+++ b/packages/meshbay-node/tests/test_security_regressions.py
@@ -1,10 +1,10 @@
"""
Phase 11.5 security regression tests.
-Each test here encodes a finding from `second-review.md`. They are negative tests:
-they assert that an attack does NOT work. The pre-11.5 code passed 209 feature
-tests while every one of these attacks succeeded — the suite only ever exercised
-happy paths, never an authorization boundary.
+Each test here encodes a finding from `docs/MESHBAY_DESIGN.md` §13.3. They are
+negative tests: they assert that an attack does NOT work. The pre-11.5 code
+passed 209 feature tests while every one of these attacks succeeded — the suite
+only ever exercised happy paths, never an authorization boundary.
If one of these starts failing, a fix has been reverted. Do not "fix" the test.
"""
@@ -447,7 +447,7 @@ def test_daemon_sets_no_global_chat_store(tmp_path):
def test_no_member_can_hand_the_node_key_material(tmp_path):
"""
- C5b, strengthened by the invite redesign (docs/invite-pairing-v1.md).
+ C5b, strengthened by the invite redesign (docs/MESHBAY_DESIGN.md §3.4).
This test used to assert that `gek_bundle_store` answered with an admin
challenge and stored nothing without an operator signature. The message is now
@@ -818,7 +818,7 @@ def test_node_control_api_serves_no_html():
H2 was stored XSS in the server-rendered admin dashboard: a member-chosen
filename, or a hub-supplied username, landed in an HTML page on the
operator's machine unescaped. That dashboard is gone
- (docs/refactor-node-ui.md phase 5) — the control API is JSON only, so there
+ (docs/MESHBAY_DESIGN.md §6.7) — the control API is JSON only, so there
is no server-side template to inject into. The Node page that replaced it
ships in the desktop client and escapes by default (Preact).
diff --git a/packages/meshbay-node/tests/test_startup_scan_enrichment.py b/packages/meshbay-node/tests/test_startup_scan_enrichment.py
index d2cbc3e..f43c6e9 100644
--- a/packages/meshbay-node/tests/test_startup_scan_enrichment.py
+++ b/packages/meshbay-node/tests/test_startup_scan_enrichment.py
@@ -12,8 +12,8 @@ is already running (seen by the watchdog), would trigger it. Found live
against a real library after the first restart with this feature enabled.
Enrichment only runs once a group has a video_root configured (a group
-with none set gets no TMDB/thumbnail work at all, docs/mediacenter.md
-§5.2/§10) — this test's fake roster reports the shared root itself as the
+with none set gets no TMDB/thumbnail work at all, docs/MESHBAY_DESIGN.md
+§6.5) — this test's fake roster reports the shared root itself as the
configured video_root, so the enrichment-scheduling behaviour under test
is exercised the same way a real operator's group would be.
"""
diff --git a/packages/meshbay-node/tests/test_stream_video_transcode.py b/packages/meshbay-node/tests/test_stream_video_transcode.py
index 0fd12c6..316fdce 100644
--- a/packages/meshbay-node/tests/test_stream_video_transcode.py
+++ b/packages/meshbay-node/tests/test_stream_video_transcode.py
@@ -19,8 +19,8 @@ None — there is nothing to put in `stream_init` for the client to check. Until
live against an episode rip in a `.avi` — mpeg4 video, mp3 audio, 720x404 —
which the reporting machine re-encodes at about six times playback speed. The setting
that governs it, `transcode_incompatible_video`, was documented from the start
-as covering "HEVC *and other browser-incompatible video codecs*" (draft-v6
-§2.11); only HEVC was ever wired up.
+as covering "HEVC *and other browser-incompatible video codecs*"
+(docs/MESHBAY_DESIGN.md §6.8); only HEVC was ever wired up.
These tests spawn real ffmpeg/ffprobe against small synthetic files (lavfi
test sources, ~1s), the same style as test_stream_audio_transcode.py.
diff --git a/packages/meshbay-node/tests/test_title_parse.py b/packages/meshbay-node/tests/test_title_parse.py
index bbe9afa..2c4573a 100644
--- a/packages/meshbay-node/tests/test_title_parse.py
+++ b/packages/meshbay-node/tests/test_title_parse.py
@@ -1,6 +1,6 @@
"""
Tests for indexer/title_parse.py — synthetic filenames only, one per
-docs/mediacenter.md §3.3/§3.4 rule. The real ~1950-file library validation
+docs/MESHBAY_DESIGN.md §9.7 rule. The real ~1950-file library validation
is a manual acceptance step (§11), not something this repo's corpus holds.
"""
@@ -72,7 +72,7 @@ def test_sequel_variants_empty_when_no_trailing_digit():
assert sequel_variants("Some Movie") == []
-# ── §10.1/V10: wider sequel-index handling ──────────────────────────────────
+# ── V10: wider sequel-index handling ────────────────────────────────────────
def test_sequel_variants_roman_numeral_offers_the_digit_form():
v = sequel_variants("Old Frontier III")
@@ -81,7 +81,7 @@ def test_sequel_variants_roman_numeral_offers_the_digit_form():
def test_sequel_variants_rewrites_a_part_keyword_index_but_keeps_the_name():
- # §10.1/V14: with a "Part"/"Episode"/… keyword the bare base is
+ # V14: with a "Part"/"Episode"/… keyword the bare base is
# withheld — "Some Saga" alone collides with a franchise-origin film.
v = sequel_variants("Some Saga Part 2")
assert "Some Saga II" in v
@@ -97,7 +97,7 @@ def test_sequel_variants_reads_a_spelled_out_index():
def test_sequel_variants_saga_shape_does_not_offer_the_bare_franchise():
# Every "<Saga> Chapter <N>" was matching the franchise's first entry
- # because the bare "<Saga>" variant hit it at ratio 1.0 (§10.1/V14).
+ # because the bare "<Saga>" variant hit it at ratio 1.0 (V14).
v = sequel_variants("Some Saga Chapter III")
assert "Some Saga" not in v
assert "Some Saga 3" in v
@@ -125,7 +125,7 @@ def test_clean_query_despaces_a_folder_name_without_eating_the_last_word():
assert naive_title("Some.Show.Name") != "Some Show Name" # the trap it avoids
-# ── §10.1/V14: telling a real episode marker from a mangled number ──────────
+# ── V14: telling a real episode marker from a mangled number ────────────────
def test_has_episode_marker_accepts_real_markers():
for name in ["Some.Show.S01E08.mkv", "some.show.s1.e8.mkv",
@@ -244,7 +244,7 @@ def test_non_season_folder_name_returns_none():
assert season_from_folder_name("Some Show Name") is None
-# ── §3.4c: a bare leading episode number, guessit's 3-digit blind spot ───────
+# ── V-findings: a bare leading episode number, guessit's 3-digit blind spot ─
def test_leading_episode_number_reads_the_whole_number():
assert leading_episode_number("001 Episode's Own Title.mkv") == 1
diff --git a/packages/meshbay-node/tests/test_tmdb.py b/packages/meshbay-node/tests/test_tmdb.py
index ab452ce..299ff2a 100644
--- a/packages/meshbay-node/tests/test_tmdb.py
+++ b/packages/meshbay-node/tests/test_tmdb.py
@@ -40,7 +40,7 @@ async def test_search_movie_returns_top_result_and_confidence():
await client.close()
-# ── §10.1/V9: year-exact preference, only when the top hit is weak ──────────
+# ── V9: year-exact preference, only when the top hit is weak ────────────────
@pytest.mark.asyncio
async def test_year_exact_result_wins_when_the_top_hit_is_low_confidence():
diff --git a/packages/meshbay-node/tests/test_tmdb_config_policy.py b/packages/meshbay-node/tests/test_tmdb_config_policy.py
index c1781e3..cef4fda 100644
--- a/packages/meshbay-node/tests/test_tmdb_config_policy.py
+++ b/packages/meshbay-node/tests/test_tmdb_config_policy.py
@@ -1,6 +1,7 @@
"""
-The operator's custom TMDB API token and query language — docs/mediacenter.md
-§5.5. Same shape as test_apps_enabled_policy.py/test_scan_settings_policy.py:
+The operator's custom TMDB API token and query language —
+docs/MESHBAY_DESIGN.md §9.7. Same shape as
+test_apps_enabled_policy.py/test_scan_settings_policy.py:
a signed operator instruction, node-wide (group_id="") rather than per-group,
stored via roster.py's group_settings table.
diff --git a/packages/meshbay-node/tests/test_tmdb_enabled_policy.py b/packages/meshbay-node/tests/test_tmdb_enabled_policy.py
index 8e945ad..0a748cd 100644
--- a/packages/meshbay-node/tests/test_tmdb_enabled_policy.py
+++ b/packages/meshbay-node/tests/test_tmdb_enabled_policy.py
@@ -1,5 +1,5 @@
"""
-Whether TMDB lookups run *at all* for a group — docs/mediacenter.md §5.5.
+Whether TMDB lookups run *at all* for a group — docs/MESHBAY_DESIGN.md §9.7.
Per-group (2026-08-24 — used to be node-wide, folded into tmdb_config): a
real media-library group and a test/demo group on the same node need not
share the decision to spend TMDB quota and make outbound requests. Same
diff --git a/packages/meshbay-node/tests/test_tmdb_rematch_policy.py b/packages/meshbay-node/tests/test_tmdb_rematch_policy.py
index ff259c1..198d75b 100644
--- a/packages/meshbay-node/tests/test_tmdb_rematch_policy.py
+++ b/packages/meshbay-node/tests/test_tmdb_rematch_policy.py
@@ -1,5 +1,5 @@
"""
-`tmdb_rematch` (§10.1/V13) — an operator dropping one file's cached TMDB
+`tmdb_rematch` (V13) — an operator dropping one file's cached TMDB
match so it re-resolves with the current matcher. Signed like
`tmdb_override` (media_cache is shared node-wide); unlike `clear_file_tmdb`
it forgets a manual override marker too, since the operator is explicitly
diff --git a/packages/meshbay-node/tests/test_tmdb_search_ladder.py b/packages/meshbay-node/tests/test_tmdb_search_ladder.py
index 9865c77..dc83733 100644
--- a/packages/meshbay-node/tests/test_tmdb_search_ladder.py
+++ b/packages/meshbay-node/tests/test_tmdb_search_ladder.py
@@ -208,7 +208,7 @@ async def test_strong_direct_match_costs_a_single_request():
assert client.calls == [("A Quiet Film", 2010)]
-# ── §10.1/V11: a decent primary hit with nothing more specific to try ──────
+# ── V11: a decent primary hit with nothing more specific to try ────────────
async def test_decent_primary_with_no_stronger_candidate_costs_one_call():
result, _, client = await _run(
@@ -222,7 +222,7 @@ async def test_decent_primary_with_no_stronger_candidate_costs_one_call():
"is not worth a second request once the primary hit is decent")
-# ── §10.1/V8: the show branch uses the same scored ladder ─────────────────
+# ── V8: the show branch uses the same scored ladder ───────────────────────
async def test_show_scored_ladder_beats_a_weak_primary_hit():
result, _, _ = await _run_show(
diff --git a/packages/meshbay-node/tests/test_transfer_settings.py b/packages/meshbay-node/tests/test_transfer_settings.py
index 79502e7..aa24782 100644
--- a/packages/meshbay-node/tests/test_transfer_settings.py
+++ b/packages/meshbay-node/tests/test_transfer_settings.py
@@ -4,7 +4,8 @@ of setting.
The **pools** are the machine's: how many transfers this node runs at once,
across every group, from `[node]` in node.toml with a roster override — the
-§2.11 pattern, changed from the Node page or the CLI, applied live.
+docs/MESHBAY_DESIGN.md §6.8 pattern, changed from the Node page or the CLI,
+applied live.
The **per-member cap** is a group's: how many one member may run at once here.
It lives on the node like every other group setting (not the hub, which would
diff --git a/packages/meshbay-node/tests/test_video_root_gates_enrichment.py b/packages/meshbay-node/tests/test_video_root_gates_enrichment.py
index fd0e040..d06b3f4 100644
--- a/packages/meshbay-node/tests/test_video_root_gates_enrichment.py
+++ b/packages/meshbay-node/tests/test_video_root_gates_enrichment.py
@@ -1,5 +1,5 @@
"""
-Videos-app enrichment (ffprobe/thumbnailing/TMDB, mediacenter.md §5.2/§10)
+Videos-app enrichment (ffprobe/thumbnailing/TMDB, docs/MESHBAY_DESIGN.md §6.5)
only ever runs for a group that has a video_root configured, and only for
files under it — see daemon.py's _enrich_new_video_entries. Burning TMDB's
rate limit and the node's CPU on an operator's whole shared index before
diff --git a/packages/meshbay-node/tests/test_webrtc_transport.py b/packages/meshbay-node/tests/test_webrtc_transport.py
index 3010fe9..0839d42 100644
--- a/packages/meshbay-node/tests/test_webrtc_transport.py
+++ b/packages/meshbay-node/tests/test_webrtc_transport.py
@@ -768,7 +768,8 @@ async def test_webrtc_peer_cleanup_on_close(sk_node, sk_hub, gek, shared_dir):
browser_pc, channel, received = await _setup_peer(
transport, sk_hub, gek, "peer-cleanup")
- # Keyed per connection, not per account (docs/chat-sender-keys.md F7), so
+ # Keyed per connection, not per account (finding F7,
+ # docs/MESHBAY_DESIGN.md §13.6), so
# membership is asserted by the session object rather than by user_id —
# one account may hold several entries here.
peers = transport._ctx["_peers"]