@appsoftwareltd/etherpk-mcp 0.1.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/main.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"main.js","names":[],"sources":["../package.json","../../client/src/lib/crypto/bytes.ts","../../client/src/lib/crypto/recovery-code.ts","../../client/src/lib/crypto/device-sas.ts","../../client/src/lib/crypto/envelope.ts","../../client/src/lib/crypto/identity.ts","../../client/src/lib/crypto/keyring.ts","../../client/src/lib/crypto/protection.ts","../../client/src/lib/crypto/protection-key.ts","../../client/src/lib/crypto/sealed.ts","../../client/src/lib/crypto/vault.ts","../../client/src/lib/sync/sync-api.ts","../../client/src/lib/sync/sync-config.ts","../../client/src/lib/sync/sync-token.ts","../src/account.ts","../src/config.ts","../../client/src/lib/diagnostics/performance.ts","../../../packages/shared/src/managed-service.ts","../../../packages/shared/src/sync-protocol.ts","../../client/src/lib/sync/state-vector.ts","../../client/src/lib/sync/doc-sync.ts","../../client/src/lib/sync/messages.ts","../../client/src/lib/sync/presence-identity.ts","../../client/src/lib/sync/presence-session.ts","../../client/src/lib/sync/graph-sync.ts","../../client/src/lib/storage/idb-connection.ts","../../client/src/lib/sync/local-cache.ts","../src/node-transport.ts","../src/graph-names.ts","../../client/src/lib/document/wikilink/model.ts","../../client/src/lib/document/wikilink/parser.ts","../../client/src/lib/document/wikilink/code-ranges.ts","../../client/src/lib/document/wikilink/source.ts","../../client/src/lib/document/backlinks/backlink-index.ts","../../client/src/lib/document/index-db-sqlite.ts","../../client/src/lib/storage/fs/frontmatter-span.ts","../../client/src/lib/document/fenced-code.ts","../../client/src/lib/document/indent-unit.ts","../../client/src/lib/document/block-model.ts","../../client/src/lib/document/protection/fence-info.ts","../../client/src/lib/document/task-tags.ts","../../client/src/lib/document/index-derive.ts","../../client/src/lib/document/search-query.ts","../../client/src/lib/document/index-db.ts","../../client/src/lib/document/index-worker/core.ts","../../client/src/lib/document/index-worker/client.ts","../../client/src/lib/document/index-worker/transport.ts","../../client/src/lib/document/calendar/month-grid-core.ts","../../client/src/lib/document/journal-concept.ts","../../client/src/lib/storage/fs/frontmatter.ts","../../client/src/lib/storage/fs/identity.ts","../../client/src/lib/document/frontmatter/identity.ts","../../client/src/lib/document/types.ts","../../client/src/lib/activity/breathe.ts","../../client/src/lib/document/wikilink/rename.ts","../../client/src/lib/document/protection/cipher-fence.ts","../../client/src/lib/storage/merge.ts","../../client/src/lib/storage/rename.ts","../../client/src/lib/storage/rename-plan.ts","../../client/src/lib/storage/server/server-document-store.ts","../src/persistence.ts","../src/headless-graph.ts","../../client/src/lib/sync/device-approval.ts","../src/login.ts","../src/tools.ts","../src/mcp-server.ts","../src/main.ts"],"sourcesContent":["{\n \"name\": \"@appsoftwareltd/etherpk-mcp\",\n \"version\": \"0.1.2\",\n \"license\": \"Elastic-2.0\",\n \"description\": \"EtherPK Headless Client: an MCP server over a synced knowledge graph, run beside the agent on the user's own machine.\",\n \"type\": \"module\",\n \"private\": false,\n \"bin\": {\n \"etherpk-mcp\": \"./bin/etherpk-mcp.js\"\n },\n \"files\": [\n \"bin\",\n \"dist\"\n ],\n \"scripts\": {\n \"build\": \"vite build\",\n \"check\": \"tsc -p tsconfig.json --noEmit\",\n \"test\": \"vitest run\",\n \"test:watch\": \"vitest\",\n \"prepack\": \"pnpm build\"\n },\n \"dependencies\": {\n \"@lezer/markdown\": \"^1.6.4\",\n \"@modelcontextprotocol/sdk\": \"^1.30.0\",\n \"@noble/curves\": \"^2.2.0\",\n \"@noble/hashes\": \"^2.2.0\",\n \"@sqlite.org/sqlite-wasm\": \"3.53.0-build1\",\n \"fake-indexeddb\": \"^6.2.5\",\n \"lib0\": \"^0.2.117\",\n \"y-protocols\": \"^1.0.7\",\n \"yaml\": \"^2.9.0\",\n \"yjs\": \"^13.6.31\",\n \"zod\": \"^4.3.6\"\n },\n \"devDependencies\": {\n \"@appsoftwareltd/etherpk-shared\": \"workspace:*\",\n \"@codemirror/view\": \"^6.43.0\",\n \"@types/node\": \"^25.6.0\",\n \"typescript\": \"^6.0.2\",\n \"vite\": \"^8.0.8\",\n \"vitest\": \"^4.1.4\"\n },\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"homepage\": \"https://docs.etherpk.com/using-ai-agents-with-your-notes\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/appsoftwareltd/etherpk\",\n \"directory\": \"apps/mcp\"\n },\n \"keywords\": [\n \"etherpk\",\n \"mcp\",\n \"model-context-protocol\",\n \"knowledge-graph\",\n \"notes\",\n \"agent\"\n ],\n \"engines\": {\n \"node\": \">=22\"\n }\n}","/**\n * Byte-array and encoding primitives for the crypto core. No cryptography here.\n * lib/crypto must stay self-contained (no imports from outside lib/crypto).\n */\nexport function utf8(text: string): Uint8Array {\n return new TextEncoder().encode(text)\n}\n\nexport function concatBytes(...parts: Uint8Array[]): Uint8Array {\n const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0))\n let offset = 0\n for (const p of parts) {\n out.set(p, offset)\n offset += p.length\n }\n return out\n}\n\n/** Constant-time-shaped comparison (length leak is fine; contents don't short-circuit). */\nexport function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {\n if (a.length !== b.length) return false\n let diff = 0\n for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i]\n return diff === 0\n}\n\nexport function randomBytes(n: number): Uint8Array {\n const out = new Uint8Array(n)\n crypto.getRandomValues(out)\n return out\n}\n\nexport function toBase64Url(bytes: Uint8Array): string {\n let raw = ''\n for (const b of bytes) raw += String.fromCharCode(b)\n return btoa(raw).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '')\n}\n\nexport function fromBase64Url(text: string): Uint8Array {\n const raw = atob(text.replaceAll('-', '+').replaceAll('_', '/'))\n const out = new Uint8Array(raw.length)\n for (let i = 0; i < raw.length; i++) out[i] = raw.charCodeAt(i)\n return out\n}\n","/**\n * The Recovery Code (ADR 0026): 128 bits of generated entropy the user saves once.\n * Crockford base32 (no I, L, O, U) so it survives handwriting and re-typing;\n * normalization maps the confusables back. HKDF only — the code is high-entropy,\n * so no password-hardening KDF is needed anywhere in the system.\n */\nimport { randomBytes, utf8 } from './bytes'\n\nconst ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'\nconst PREFIX = 'EPK1'\nconst BODY_LENGTH = 26 // 16 bytes → ceil(128/5) = 26 base32 chars\n\nexport class RecoveryCodeError extends Error {}\n\n/** e.g. EPK1-4R9TN-0WXA2-JH6MK-P3VZE-C58DQF */\nexport function generateRecoveryCode(): string {\n return formatKit(encodeBase32(randomBytes(16)))\n}\n\nfunction formatKit(body: string): string {\n return `${PREFIX}-${body.slice(0, 5)}-${body.slice(5, 10)}-${body.slice(10, 15)}-${body.slice(15, 20)}-${body.slice(20)}`\n}\n\n/** Crockford base32 (no I, L, O, U) — shared with the device-approval SAS, which is\n * human-compared across two screens and needs the same confusable-resistance. */\nexport function encodeCrockford32(bytes: Uint8Array): string {\n return encodeBase32(bytes)\n}\n\nfunction encodeBase32(bytes: Uint8Array): string {\n let bits = 0\n let value = 0\n let out = ''\n for (const b of bytes) {\n value = (value << 8) | b\n bits += 8\n while (bits >= 5) {\n out += ALPHABET[(value >>> (bits - 5)) & 31]\n bits -= 5\n }\n }\n if (bits > 0) out += ALPHABET[(value << (5 - bits)) & 31]\n return out\n}\n\nfunction decodeBase32(chars: string): Uint8Array {\n let bits = 0\n let value = 0\n const out: number[] = []\n for (const c of chars) {\n const index = ALPHABET.indexOf(c)\n if (index < 0) throw new RecoveryCodeError(`invalid recovery code character \"${c}\"`)\n value = (value << 5) | index\n bits += 5\n if (bits >= 8) {\n out.push((value >>> (bits - 8)) & 255)\n bits -= 8\n }\n }\n return new Uint8Array(out)\n}\n\n/** Uppercase, strip spaces/dashes, map O→0 and I/L→1, verify prefix + length; return canonical form. */\nexport function normalizeRecoveryCode(input: string): string {\n const cleaned = input\n .toUpperCase()\n .replace(/[\\s-]/g, '')\n .replaceAll('O', '0')\n .replaceAll('I', '1')\n .replaceAll('L', '1')\n if (!cleaned.startsWith(PREFIX)) throw new RecoveryCodeError('not a Recovery Code (missing EPK1 prefix)')\n const body = cleaned.slice(PREFIX.length)\n if (body.length !== BODY_LENGTH) {\n throw new RecoveryCodeError(`expected ${BODY_LENGTH} characters after the prefix, got ${body.length}`)\n }\n return formatKit(body)\n}\n\nexport function recoveryCodeToBytes(code: string): Uint8Array {\n const body = normalizeRecoveryCode(code).slice(PREFIX.length + 1).replaceAll('-', '')\n return decodeBase32(body)\n}\n\n/** The vault wrap key: HKDF-SHA-256 over the code's bytes. */\nexport async function deriveVaultWrapKey(code: string): Promise<Uint8Array> {\n const ikm = await crypto.subtle.importKey('raw', recoveryCodeToBytes(code) as BufferSource, 'HKDF', false, [\n 'deriveBits',\n ])\n const bits = await crypto.subtle.deriveBits(\n {\n name: 'HKDF',\n hash: 'SHA-256',\n salt: utf8('etherpk-vault-v1') as BufferSource,\n info: utf8('vault-wrap') as BufferSource,\n },\n ikm,\n 256,\n )\n return new Uint8Array(bits)\n}\n","/**\n * The device-approval SAS (Short Authentication String): a short code both devices derive\n * from the requesting device's ephemeral public key and display for the user to compare.\n * The server brokers that key, so a malicious server could substitute its own — but then\n * the two screens derive DIFFERENT codes and the user refuses. One shot: a mismatch means\n * starting over with a fresh key, so 40 displayed bits leave no room to search for a\n * matching substitute. Same trust move as the invite fingerprint (identity.ts).\n */\nimport { concatBytes, utf8 } from './bytes'\nimport { encodeCrockford32 } from './recovery-code'\n\nconst SAS_CONTEXT = utf8('etherpk/device-approval-sas/v1')\n\n/** e.g. \"7Q4M-KX2A\" — first 40 bits of SHA-256(context || ephemeral public key). */\nexport async function deviceApprovalSas(ephemeralPublicKey: Uint8Array): Promise<string> {\n const digest = new Uint8Array(\n await crypto.subtle.digest('SHA-256', concatBytes(SAS_CONTEXT, ephemeralPublicKey) as BufferSource),\n )\n const code = encodeCrockford32(digest.subarray(0, 5))\n return `${code.slice(0, 4)}-${code.slice(4, 8)}`\n}\n","/**\n * The one binary envelope for symmetric (epoch-keyed) content — updates, snapshots,\n * the vault, asset chunks. Self-delimiting layout, version 1:\n *\n * [0] version = 1\n * [1] kind = 1 (symmetric; sealed boxes are kind 2 — sealed.ts)\n * [2..5] epochId, u32 little-endian (0 where epochs don't apply, e.g. the vault)\n * [6..17] AES-GCM nonce (12 bytes, fresh random per seal)\n * [18..] ciphertext + GCM tag\n *\n * AAD binds the envelope to its context — (graph, doc, kind) — so a compromised server\n * cannot splice a valid blob into another document (design doc → Cryptography).\n */\nimport { concatBytes, randomBytes, utf8 } from './bytes'\n\nexport const ENVELOPE_VERSION = 1\nexport const KIND_SYMMETRIC = 1\nconst HEADER_LENGTH = 18\n\nexport class EnvelopeError extends Error {}\n\n/** Canonical AAD builder: contextAad('update', 'graph:<id>', 'doc:<id>'). */\nexport function contextAad(...parts: string[]): Uint8Array {\n return utf8(`etherpk:v1|${parts.join('|')}`)\n}\n\nasync function importAesKey(key: Uint8Array, usage: KeyUsage): Promise<CryptoKey> {\n return crypto.subtle.importKey('raw', key as BufferSource, 'AES-GCM', false, [usage])\n}\n\nexport async function sealSymmetric(opts: {\n key: Uint8Array\n epochId: number\n plaintext: Uint8Array\n aad: Uint8Array\n}): Promise<Uint8Array> {\n const nonce = randomBytes(12)\n const aesKey = await importAesKey(opts.key, 'encrypt')\n const ciphertext = new Uint8Array(\n await crypto.subtle.encrypt(\n // TS 6 + DOM lib: algorithm byte fields need BufferSource casts too, or check fails\n { name: 'AES-GCM', iv: nonce as BufferSource, additionalData: opts.aad as BufferSource },\n aesKey,\n opts.plaintext as BufferSource,\n ),\n )\n const header = new Uint8Array(HEADER_LENGTH)\n header[0] = ENVELOPE_VERSION\n header[1] = KIND_SYMMETRIC\n new DataView(header.buffer).setUint32(2, opts.epochId, true)\n header.set(nonce, 6)\n return concatBytes(header, ciphertext)\n}\n\n/** Read the epoch id without decrypting (the client picks the keyring entry from it). */\nexport function envelopeEpochId(envelope: Uint8Array): number {\n if (envelope.length < HEADER_LENGTH) throw new EnvelopeError('envelope too short')\n if (envelope[0] !== ENVELOPE_VERSION) throw new EnvelopeError(`unknown envelope version ${envelope[0]}`)\n if (envelope[1] !== KIND_SYMMETRIC) throw new EnvelopeError(`unexpected envelope kind ${envelope[1]}`)\n return new DataView(envelope.buffer, envelope.byteOffset).getUint32(2, true)\n}\n\nexport async function openSymmetric(opts: {\n keyForEpoch: (epochId: number) => Uint8Array | undefined\n envelope: Uint8Array\n aad: Uint8Array\n}): Promise<{ plaintext: Uint8Array; epochId: number }> {\n const epochId = envelopeEpochId(opts.envelope)\n const key = opts.keyForEpoch(epochId)\n if (!key) throw new EnvelopeError(`no key for epoch ${epochId}`)\n const aesKey = await importAesKey(key, 'decrypt')\n try {\n const plaintext = new Uint8Array(\n await crypto.subtle.decrypt(\n {\n name: 'AES-GCM',\n iv: opts.envelope.subarray(6, 18) as BufferSource,\n additionalData: opts.aad as BufferSource,\n },\n aesKey,\n opts.envelope.subarray(HEADER_LENGTH) as BufferSource,\n ),\n )\n return { plaintext, epochId }\n } catch {\n throw new EnvelopeError('envelope authentication failed')\n }\n}\n","/**\n * The account identity keypair (X25519) and its display fingerprint (ADR 0026).\n * @noble/curves is pure JS (no WASM) and clamps private scalars internally.\n */\nimport { x25519 } from '@noble/curves/ed25519.js' // extensioned subpath — required by @noble/curves v2's exports map\nimport { randomBytes } from './bytes'\n\nexport interface IdentityKeyPair {\n publicKey: Uint8Array\n privateKey: Uint8Array\n}\n\nexport function generateIdentityKeyPair(): IdentityKeyPair {\n const privateKey = randomBytes(32)\n return { privateKey, publicKey: x25519.getPublicKey(privateKey) }\n}\n\n/**\n * SHA-256 of the public key, first 128 bits as 8 groups of 4 hex chars —\n * the out-of-band verification string two users compare (ADR 0026, TOFU).\n */\nexport async function fingerprint(publicKey: Uint8Array): Promise<string> {\n const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', publicKey as BufferSource))\n const hex = [...digest.subarray(0, 16)].map((b) => b.toString(16).padStart(2, '0')).join('')\n return hex.toUpperCase().match(/.{4}/g)!.join(' ')\n}\n","/**\n * The Graph Key as it actually exists: an ordered list of epoch keys (ADR 0026).\n * The last entry is the current epoch; older epochs decrypt not-yet-compacted history.\n * Immutable: bumpEpoch returns a new keyring (removing a Player = bump + re-seal).\n */\nimport { fromBase64Url, randomBytes, toBase64Url, utf8 } from './bytes'\n\nexport interface EpochKey {\n epochId: number\n key: Uint8Array\n}\n\nexport interface GraphKeyring {\n graphId: string\n /** Ascending by epochId; last is current. */\n epochs: EpochKey[]\n}\n\nexport function createGraphKeyring(graphId: string): GraphKeyring {\n return { graphId, epochs: [{ epochId: 1, key: randomBytes(32) }] }\n}\n\nexport function bumpEpoch(keyring: GraphKeyring): GraphKeyring {\n const nextId = currentEpoch(keyring).epochId + 1\n return { ...keyring, epochs: [...keyring.epochs, { epochId: nextId, key: randomBytes(32) }] }\n}\n\nexport function currentEpoch(keyring: GraphKeyring): EpochKey {\n return keyring.epochs[keyring.epochs.length - 1]\n}\n\nexport function keyForEpoch(keyring: GraphKeyring, epochId: number): Uint8Array | undefined {\n return keyring.epochs.find((e) => e.epochId === epochId)?.key\n}\n\nexport interface KeyringJson {\n graphId: string\n epochs: Array<{ epochId: number; key: string }>\n}\n\nexport function keyringsToJson(keyrings: GraphKeyring[]): KeyringJson[] {\n return keyrings.map((k) => ({\n graphId: k.graphId,\n epochs: k.epochs.map((e) => ({ epochId: e.epochId, key: toBase64Url(e.key) })),\n }))\n}\n\nexport function keyringsFromJson(json: KeyringJson[]): GraphKeyring[] {\n return json.map((k) => ({\n graphId: k.graphId,\n epochs: k.epochs.map((e) => ({ epochId: e.epochId, key: fromBase64Url(e.key) })),\n }))\n}\n\n/** Plaintext bytes — only ever stored *inside* a vault or sealed box, never bare. */\nexport function serializeKeyrings(keyrings: GraphKeyring[]): Uint8Array {\n return utf8(JSON.stringify(keyringsToJson(keyrings)))\n}\n\nexport function deserializeKeyrings(bytes: Uint8Array): GraphKeyring[] {\n return keyringsFromJson(JSON.parse(new TextDecoder().decode(bytes)) as KeyringJson[])\n}\n","/**\n * The `etherpk-cipher` envelope: a [[Protected Document]]'s ciphertext,\n * as it sits inline in the fence (ADR 0028) — the same bytes on a Filesystem Backend, in an\n * Export, in a Local Mirror, in the Local Cache and on the Sync Server.\n *\n * ADR 0028 requires the envelope be **self-delimiting** and **carry a write timestamp**, because\n * two offline rewrites merge as two intact envelopes concatenated inside one fence and every\n * client must split them and resolve last-write-wins identically, without the key.\n *\n * Layout, version 1 (all multi-byte fields big-endian):\n *\n * [0] version = 1 (the envelope family — see envelope.ts)\n * [1] kind = 4 (1 symmetric, 2 sealed, 3 vault, 4 protected)\n * [2..9] writtenAt, u64 ms since the epoch\n * [10..17] key fingerprint, 8 bytes (non-secret — see {@link keyFingerprint})\n * [18..21] u32 byte length of the inner envelope\n * [22..] inner symmetric envelope, sealed under the Protection Key\n *\n * The header is **not** merely a prefix: `writtenAt` and the fingerprint are bound into the inner\n * envelope's AAD. A Player cannot read a protected fence but can still write to the `Y.Text`\n * holding it, so an unauthenticated timestamp would let them bump a stale envelope to win\n * last-write-wins. Editing either field now makes the envelope fail to open.\n *\n * The length prefix is u32, not u16: a Protected Document's whole body is one envelope and a page\n * of notes very easily exceeds 64 KiB.\n */\nimport { bytesEqual, concatBytes, fromBase64Url, toBase64Url, utf8 } from './bytes'\nimport { ENVELOPE_VERSION, contextAad, openSymmetric, sealSymmetric } from './envelope'\n\nexport const KIND_PROTECTED = 4\n/** version + kind + writtenAt + fingerprint + inner length. */\nexport const PROTECTION_HEADER_LENGTH = 22\nexport const FINGERPRINT_LENGTH = 8\n\nexport class ProtectionError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'ProtectionError'\n }\n}\n\n/**\n * The non-secret fingerprint of a Protection Key. Keys are personal (ADR 0057), so two members of\n * one shared graph hold different keys in it; without the fingerprint a client could not tell\n * *yours, locked* from *another member's, permanently unreadable*, and would offer an unlock\n * prompt no passphrase of yours would satisfy.\n *\n * Truncated to 8 bytes — 64 bits is far past collision range for the handful of keys a graph ever\n * sees, and it keeps the header small enough that a short Protected Document stays small.\n */\nexport async function keyFingerprint(key: Uint8Array): Promise<Uint8Array> {\n const digest = await crypto.subtle.digest(\n 'SHA-256',\n concatBytes(utf8('etherpk:protection-fingerprint:v1'), key) as BufferSource,\n )\n return new Uint8Array(digest).subarray(0, FINGERPRINT_LENGTH)\n}\n\n/** AAD binding the inner envelope to the header fields, so neither can be edited in place. */\nfunction protectionAad(writtenAt: number, fingerprint: Uint8Array): Uint8Array {\n return contextAad('protected', String(writtenAt), toBase64Url(fingerprint))\n}\n\nfunction headerView(envelope: Uint8Array): DataView {\n if (envelope.length < PROTECTION_HEADER_LENGTH) throw new ProtectionError('protected envelope too short')\n if (envelope[0] !== ENVELOPE_VERSION) throw new ProtectionError(`unknown envelope version ${envelope[0]}`)\n if (envelope[1] !== KIND_PROTECTED) throw new ProtectionError(`unexpected envelope kind ${envelope[1]}`)\n return new DataView(envelope.buffer, envelope.byteOffset, envelope.byteLength)\n}\n\n/** The write timestamp, readable without the key — this is what last-write-wins sorts on. */\nexport function protectedEnvelopeWrittenAt(envelope: Uint8Array): number {\n return Number(headerView(envelope).getBigUint64(2, false))\n}\n\n/** The Protection Key fingerprint, readable without the key. */\nexport function protectedEnvelopeFingerprint(envelope: Uint8Array): Uint8Array {\n headerView(envelope)\n return envelope.subarray(10, 10 + FINGERPRINT_LENGTH)\n}\n\n/** Total byte length of the envelope starting at offset 0, or throws if it is not whole. */\nfunction envelopeLength(envelope: Uint8Array): number {\n const innerLength = headerView(envelope).getUint32(18, false)\n const total = PROTECTION_HEADER_LENGTH + innerLength\n if (envelope.length < total) throw new ProtectionError('protected envelope truncated')\n return total\n}\n\nexport async function sealProtected(opts: {\n key: Uint8Array\n fingerprint: Uint8Array\n plaintext: string\n writtenAt: number\n}): Promise<Uint8Array> {\n const inner = await sealSymmetric({\n key: opts.key,\n epochId: 0,\n plaintext: utf8(opts.plaintext),\n aad: protectionAad(opts.writtenAt, opts.fingerprint),\n })\n const header = new Uint8Array(PROTECTION_HEADER_LENGTH)\n const view = new DataView(header.buffer)\n header[0] = ENVELOPE_VERSION\n header[1] = KIND_PROTECTED\n view.setBigUint64(2, BigInt(opts.writtenAt), false)\n header.set(opts.fingerprint.subarray(0, FINGERPRINT_LENGTH), 10)\n view.setUint32(18, inner.length, false)\n return concatBytes(header, inner)\n}\n\nexport async function openProtected(opts: {\n key: Uint8Array\n envelope: Uint8Array\n}): Promise<{ plaintext: string; writtenAt: number }> {\n const writtenAt = protectedEnvelopeWrittenAt(opts.envelope)\n const fingerprint = protectedEnvelopeFingerprint(opts.envelope)\n const inner = opts.envelope.subarray(PROTECTION_HEADER_LENGTH, envelopeLength(opts.envelope))\n try {\n const { plaintext } = await openSymmetric({\n keyForEpoch: () => opts.key,\n envelope: inner,\n aad: protectionAad(writtenAt, fingerprint),\n })\n return { plaintext: new TextDecoder().decode(plaintext), writtenAt }\n } catch {\n // One message for a wrong key and for a tampered header alike: the caller cannot act on\n // the difference, and distinguishing them would confirm a guessed key to an attacker.\n throw new ProtectionError('protected content could not be opened with this key')\n }\n}\n\n/**\n * Split a fence body that holds several envelopes end to end. Yjs never interleaves within one\n * insert, so an offline rewrite on each of two devices merges as two intact envelopes side by\n * side (ADR 0028) — this is what recovers them. Trailing bytes that are not a whole envelope are\n * dropped rather than throwing: a partially-typed or hand-mangled fence must still show the\n * envelopes it does contain.\n */\nexport function splitProtectedEnvelopes(bytes: Uint8Array): Uint8Array[] {\n const out: Uint8Array[] = []\n let offset = 0\n while (offset < bytes.length) {\n const rest = bytes.subarray(offset)\n let length: number\n try {\n length = envelopeLength(rest)\n } catch {\n break\n }\n out.push(rest.subarray(0, length))\n offset += length\n }\n return out\n}\n\n/**\n * ADR 0028's resolution: the newest envelope wins and the losers are discarded. Ties break on the\n * envelope bytes so every client — including one that cannot read any of them — converges on the\n * same winner without coordinating.\n *\n * The losers are genuinely dropped. ADR 0059 records this as an accepted risk: with the projected\n * editing model, two of your own unlocked devices can race, and the loser's work goes with it.\n */\nexport function resolveLastWriteWins(envelopes: readonly Uint8Array[]): Uint8Array | null {\n let winner: Uint8Array | null = null\n let winnerAt = -1\n for (const envelope of envelopes) {\n let writtenAt: number\n try {\n writtenAt = protectedEnvelopeWrittenAt(envelope)\n } catch {\n continue\n }\n if (writtenAt > winnerAt || (writtenAt === winnerAt && winner && compareBytes(envelope, winner) > 0)) {\n winner = envelope\n winnerAt = writtenAt\n }\n }\n return winner\n}\n\nfunction compareBytes(a: Uint8Array, b: Uint8Array): number {\n if (bytesEqual(a, b)) return 0\n const shared = Math.min(a.length, b.length)\n for (let i = 0; i < shared; i++) if (a[i] !== b[i]) return a[i] < b[i] ? -1 : 1\n return a.length < b.length ? -1 : 1\n}\n\n/**\n * Armour an envelope for the fence body. base64url has no backtick, so armoured content can never\n * be mistaken for a closing fence, and no newline, so one envelope is one line — which keeps a\n * Filesystem Backend graph's git diffs to a single changed line per write.\n */\nexport function armourProtected(envelope: Uint8Array): string {\n return toBase64Url(envelope)\n}\n\n/**\n * Read every envelope out of a fence body. Handles both shapes a merge can produce: one envelope\n * per line (the normal case), and several concatenated within one line. Blank and undecodable\n * lines are skipped — a hand-edited fence must still yield what it holds.\n */\nexport function unarmourProtected(body: string): Uint8Array[] {\n const out: Uint8Array[] = []\n for (const line of body.split('\\n')) {\n const trimmed = line.trim()\n if (!trimmed) continue\n try {\n out.push(...splitProtectedEnvelopes(fromBase64Url(trimmed)))\n } catch {\n continue\n }\n }\n return out\n}\n","/**\n * The Protection Key and its two wraps (ADR 0057).\n *\n * The key itself is random and is **never stored**. What a graph stores is a\n * {@link ProtectionRecord}: the non-secret fingerprint, the Argon2id cost parameters, and the key\n * wrapped under a key derived from the [[Protection Passphrase]]. That is the portable root — it\n * travels with the graph, so any device that knows the passphrase can reach the content.\n *\n * A second, optional wrap exists per device: {@link DeviceProtectionWrap}, keyed by a WebAuthn\n * passkey's PRF secret and held in device-local IndexedDB, **never synced and never exported**.\n * This is what keeps a synced passkey's provider outside the trust boundary — iCloud Keychain or\n * Google Password Manager replicate the PRF secret, but the blob it unwraps exists only on the\n * device that made it.\n *\n * Why Argon2id for one and plain HKDF for the other: a passphrase is human-chosen and attacked\n * offline (the wrapped blob travels in an Export), so it must be stretched by something\n * memory-hard. A PRF secret is 32 bytes of authenticator-derived entropy with nothing to guess,\n * which is the same reason `recovery-code.ts` correctly uses bare HKDF for a Recovery Code.\n */\nimport { argon2idAsync } from '@noble/hashes/argon2.js'\n\nimport { fromBase64Url, randomBytes, toBase64Url, utf8 } from './bytes'\nimport { contextAad, openSymmetric, sealSymmetric } from './envelope'\nimport { keyFingerprint } from './protection'\n\nexport const PROTECTION_RECORD_VERSION = 1\nconst PROTECTION_KEY_LENGTH = 32\nconst SALT_LENGTH = 16\nconst PASSPHRASE_AAD = contextAad('protection-key', 'passphrase')\nconst DEVICE_AAD = contextAad('protection-key', 'device')\n\n/**\n * OWASP's Argon2id minimum. RFC 9106's first-choice profile (2 GiB) and second-choice profile\n * (64 MiB, t=3) are both out of reach for a pure-JS implementation in a browser — 64 MiB / t=3\n * measures ~2.4s on a desktop and several times that on a low-end phone, which would make the\n * idle lock punishing enough that people stop protecting documents.\n *\n * Stored per record, so raising them later costs nothing: an old record keeps opening with its\n * own parameters, and the next passphrase change re-wraps under the new ones.\n */\nexport const DEFAULT_KDF_PARAMS = { m: 19456, t: 2, p: 1 } as const\n\nexport interface ProtectionKdfParams {\n /** Argon2id memory cost in KiB. */\n m: number\n /** Argon2id time cost (iterations). */\n t: number\n /** Argon2id parallelism. */\n p: number\n /** base64url, unique per record — two graphs sharing a passphrase must not share a key. */\n salt: string\n}\n\n/** What a graph stores so a passphrase can reach its Protection Key. Holds no secret. */\nexport interface ProtectionRecord {\n v: typeof PROTECTION_RECORD_VERSION\n /** base64url of the key fingerprint — matches the `fp` in every fence written under it. */\n fingerprint: string\n kdf: ProtectionKdfParams\n /** base64url of the passphrase-wrapped Protection Key. */\n wrapped: string\n}\n\n/** A device-local wrap under a passkey's PRF secret. Never synced, never exported. */\nexport interface DeviceProtectionWrap {\n v: typeof PROTECTION_RECORD_VERSION\n fingerprint: string\n /** The WebAuthn credential whose PRF output unwraps this. */\n credentialId: string\n wrapped: string\n}\n\nexport class ProtectionPassphraseError extends Error {\n constructor(message = 'that passphrase does not unlock this graph') {\n super(message)\n this.name = 'ProtectionPassphraseError'\n }\n}\n\n/** Bounds a record's Argon2id parameters must sit within to be used at all, in KiB and rounds. */\nexport const KDF_BOUNDS = { m: [8 * 1024, 1024 * 1024], t: [1, 32], p: [1, 16], saltBytes: [16, 64] } as const\n\n/**\n * A record's parameters come from a file - `etherpk/protection.json` in an imported folder, or\n * a vault entry - and an unbounded record could name a memory cost that pins the tab, or one so\n * small that the passphrase wrap is cheap to attack. Outside these bounds the record is treated\n * as unreadable, never as instructions.\n */\nexport function kdfParamsSane(kdf: { m: number; t: number; p: number; salt: string }): boolean {\n const within = (value: number, [low, high]: readonly [number, number]) =>\n Number.isInteger(value) && value >= low && value <= high\n if (!within(kdf.m, KDF_BOUNDS.m) || !within(kdf.t, KDF_BOUNDS.t) || !within(kdf.p, KDF_BOUNDS.p)) return false\n try {\n return within(fromBase64Url(kdf.salt).length, KDF_BOUNDS.saltBytes)\n } catch {\n return false\n }\n}\n\nexport function newKdfParams(): ProtectionKdfParams {\n return { ...DEFAULT_KDF_PARAMS, salt: toBase64Url(randomBytes(SALT_LENGTH)) }\n}\n\nexport function createProtectionKey(): Uint8Array {\n return randomBytes(PROTECTION_KEY_LENGTH)\n}\n\n/**\n * Stretch a passphrase into a wrapping key. `asyncTick` lets the worker yield between Argon2id\n * blocks so a slow unlock cannot freeze the thread it runs on.\n */\nasync function deriveWrapKey(passphrase: string, kdf: ProtectionKdfParams): Promise<Uint8Array> {\n return argon2idAsync(utf8(passphrase), fromBase64Url(kdf.salt), {\n t: kdf.t,\n m: kdf.m,\n p: kdf.p,\n dkLen: PROTECTION_KEY_LENGTH,\n asyncTick: 20,\n })\n}\n\nasync function wrap(key: Uint8Array, wrapKey: Uint8Array, aad: Uint8Array): Promise<string> {\n return toBase64Url(await sealSymmetric({ key: wrapKey, epochId: 0, plaintext: key, aad }))\n}\n\nasync function unwrap(wrapped: string, wrapKey: Uint8Array, aad: Uint8Array): Promise<Uint8Array> {\n try {\n const { plaintext } = await openSymmetric({\n keyForEpoch: () => wrapKey,\n envelope: fromBase64Url(wrapped),\n aad,\n })\n return plaintext\n } catch {\n throw new ProtectionPassphraseError()\n }\n}\n\n/**\n * Mint a protection record. Pass `existingKey` to re-root an existing key under a new passphrase —\n * that is how a device holding only a passkey wrap recovers from a forgotten passphrase without\n * re-encrypting a single document.\n */\nexport async function createProtectionRecord(\n passphrase: string,\n kdf: ProtectionKdfParams = newKdfParams(),\n existingKey?: Uint8Array,\n): Promise<{ record: ProtectionRecord; key: Uint8Array }> {\n const key = existingKey ?? createProtectionKey()\n const wrapKey = await deriveWrapKey(passphrase, kdf)\n return {\n key,\n record: {\n v: PROTECTION_RECORD_VERSION,\n fingerprint: toBase64Url(await keyFingerprint(key)),\n kdf,\n wrapped: await wrap(key, wrapKey, PASSPHRASE_AAD),\n },\n }\n}\n\nexport async function unlockProtectionRecord(record: ProtectionRecord, passphrase: string): Promise<Uint8Array> {\n return unwrap(record.wrapped, await deriveWrapKey(passphrase, record.kdf), PASSPHRASE_AAD)\n}\n\n/**\n * Re-wrap under a new passphrase. The Protection Key is unchanged, so no protected document is\n * touched and every fence already written stays readable — the same shape as regenerating a\n * Recovery Code. A fresh salt and fresh parameters are minted so the old blob gives no help\n * against the new one.\n */\nexport async function changeProtectionPassphrase(\n record: ProtectionRecord,\n current: string,\n next: string,\n kdf: ProtectionKdfParams = newKdfParams(),\n): Promise<ProtectionRecord> {\n const key = await unlockProtectionRecord(record, current)\n return (await createProtectionRecord(next, kdf, key)).record\n}\n\nexport async function deviceWrapFromPrf(\n key: Uint8Array,\n prfSecret: Uint8Array,\n credentialId: string,\n): Promise<DeviceProtectionWrap> {\n return {\n v: PROTECTION_RECORD_VERSION,\n fingerprint: toBase64Url(await keyFingerprint(key)),\n credentialId,\n wrapped: await wrap(key, await derivePrfWrapKey(prfSecret), DEVICE_AAD),\n }\n}\n\nexport async function unlockWithDeviceWrap(wrapRecord: DeviceProtectionWrap, prfSecret: Uint8Array): Promise<Uint8Array> {\n return unwrap(wrapRecord.wrapped, await derivePrfWrapKey(prfSecret), DEVICE_AAD)\n}\n\n/** HKDF over the PRF secret — no stretching needed, it is already high-entropy. */\nasync function derivePrfWrapKey(prfSecret: Uint8Array): Promise<Uint8Array> {\n const ikm = await crypto.subtle.importKey('raw', prfSecret as BufferSource, 'HKDF', false, ['deriveBits'])\n const bits = await crypto.subtle.deriveBits(\n {\n name: 'HKDF',\n hash: 'SHA-256',\n salt: utf8('etherpk-protection-device-v1') as BufferSource,\n info: utf8('protection-device-wrap') as BufferSource,\n },\n ikm,\n PROTECTION_KEY_LENGTH * 8,\n )\n return new Uint8Array(bits)\n}\n\nexport function serialiseProtectionRecord(record: ProtectionRecord): string {\n return JSON.stringify(record)\n}\n\n/**\n * Why stored text is not a usable record. Two kinds, because they want different advice: a\n * `damaged` record wants restoring from a backup, a `newer-version` one wants this build updated -\n * telling someone to restore a backup over a perfectly good record would be the very mistake the\n * distinction exists to prevent.\n */\nexport type ProtectionRecordProblem = 'damaged' | 'newer-version'\n\n/** Stored text, or a vault entry, that is not a record this build can use. Never \"no record\". */\nexport class ProtectionRecordFormatError extends Error {\n constructor(\n readonly problem: ProtectionRecordProblem,\n detail: string,\n ) {\n super(detail)\n this.name = 'ProtectionRecordFormatError'\n }\n}\n\n/**\n * Parse the JSON a store holds. Anything that is not a usable record throws - a mangled file, an\n * empty one, a record from a newer build, parameters outside {@link KDF_BOUNDS} - and is never\n * read as \"no record\" (ASTRA F13). This used to answer null for all of those, the same answer as\n * for an absent record, and the stores passed it on: enabling protection then wrote a fresh key\n * over a record it could not read, orphaning every fence sealed under the old one, which no\n * passphrase or Recovery Code brings back (ADR 0057). Whether any bytes existed at all is the\n * store's call, not the parser's.\n */\nexport function parseProtectionRecord(text: string): ProtectionRecord {\n let parsed: unknown\n try {\n parsed = JSON.parse(text)\n } catch (error) {\n throw new ProtectionRecordFormatError('damaged', `not JSON: ${(error as Error).message}`)\n }\n return validateProtectionRecord(parsed)\n}\n\n/**\n * The same check over a value that is already parsed - a vault entry, which arrives as an object\n * inside the decrypted vault rather than as text of its own. Only the fields a record has are\n * kept, so whatever else a stored copy carried does not travel on.\n */\nexport function validateProtectionRecord(value: unknown): ProtectionRecord {\n const damaged = (detail: string) => new ProtectionRecordFormatError('damaged', detail)\n if (typeof value !== 'object' || value === null || Array.isArray(value)) throw damaged('not a record object')\n const raw = value as Record<string, unknown>\n if (raw.v !== PROTECTION_RECORD_VERSION) {\n // A whole number above ours was written by a build after this one. Anything else was\n // never written by any build, so it is damage rather than the future.\n if (typeof raw.v === 'number' && Number.isInteger(raw.v) && raw.v > PROTECTION_RECORD_VERSION) {\n throw new ProtectionRecordFormatError(\n 'newer-version',\n `record version ${raw.v}; this build reads version ${PROTECTION_RECORD_VERSION}`,\n )\n }\n throw damaged(`record version ${String(raw.v)} is not ${PROTECTION_RECORD_VERSION}`)\n }\n if (typeof raw.fingerprint !== 'string' || typeof raw.wrapped !== 'string') throw damaged('fingerprint or wrapped key missing')\n const kdf = raw.kdf as Partial<ProtectionKdfParams> | null | undefined\n if (!kdf || typeof kdf !== 'object') throw damaged('KDF parameters missing')\n if (typeof kdf.m !== 'number' || typeof kdf.t !== 'number' || typeof kdf.p !== 'number' || typeof kdf.salt !== 'string') {\n throw damaged('KDF parameters missing')\n }\n const params: ProtectionKdfParams = { m: kdf.m, t: kdf.t, p: kdf.p, salt: kdf.salt }\n if (!kdfParamsSane(params)) throw damaged('KDF parameters outside the bounds this build will run')\n return { v: PROTECTION_RECORD_VERSION, fingerprint: raw.fingerprint, wrapped: raw.wrapped, kdf: params }\n}\n","/**\n * Sealed box: encrypt *to* an X25519 public key so only its holder can open it —\n * how a graph keyring travels to a Player and how the vault key reaches an approved\n * device (ADR 0026). ECIES construction:\n *\n * ephemeral X25519 pair → ECDH(ephemeral, recipient)\n * → HKDF-SHA-256(shared, salt = ephPub || recipientPub, info = 'etherpk/sealed/v1')\n * → AES-256-GCM\n *\n * Layout v1: [0] version=1 | [1] kind=2 | [2..33] ephemeral pub | [34..45] nonce | [46..] ct+tag\n */\nimport { x25519 } from '@noble/curves/ed25519.js' // extensioned subpath — required by @noble/curves v2's exports map\nimport { concatBytes, randomBytes, utf8 } from './bytes'\nimport { ENVELOPE_VERSION, EnvelopeError } from './envelope'\n\nexport const KIND_SEALED = 2\nconst HEADER_LENGTH = 46\n\nasync function deriveSealKey(\n shared: Uint8Array,\n ephemeralPublic: Uint8Array,\n recipientPublic: Uint8Array,\n): Promise<CryptoKey> {\n const ikm = await crypto.subtle.importKey('raw', shared as BufferSource, 'HKDF', false, ['deriveKey'])\n return crypto.subtle.deriveKey(\n {\n name: 'HKDF',\n hash: 'SHA-256',\n salt: concatBytes(ephemeralPublic, recipientPublic) as BufferSource,\n info: utf8('etherpk/sealed/v1') as BufferSource,\n },\n ikm,\n { name: 'AES-GCM', length: 256 },\n false,\n ['encrypt', 'decrypt'],\n )\n}\n\nexport async function sealToPublicKey(\n recipientPublicKey: Uint8Array,\n plaintext: Uint8Array,\n aad: Uint8Array,\n): Promise<Uint8Array> {\n const ephemeralPrivate = randomBytes(32)\n const ephemeralPublic = x25519.getPublicKey(ephemeralPrivate)\n const shared = x25519.getSharedSecret(ephemeralPrivate, recipientPublicKey)\n const key = await deriveSealKey(shared, ephemeralPublic, recipientPublicKey)\n const nonce = randomBytes(12)\n const ciphertext = new Uint8Array(\n await crypto.subtle.encrypt(\n { name: 'AES-GCM', iv: nonce as BufferSource, additionalData: aad as BufferSource },\n key,\n plaintext as BufferSource,\n ),\n )\n const header = new Uint8Array(HEADER_LENGTH)\n header[0] = ENVELOPE_VERSION\n header[1] = KIND_SEALED\n header.set(ephemeralPublic, 2)\n header.set(nonce, 34)\n return concatBytes(header, ciphertext)\n}\n\nexport async function openSealed(\n recipientPrivateKey: Uint8Array,\n envelope: Uint8Array,\n aad: Uint8Array,\n): Promise<Uint8Array> {\n if (envelope.length < HEADER_LENGTH + 16) throw new EnvelopeError('sealed envelope too short')\n if (envelope[0] !== ENVELOPE_VERSION) throw new EnvelopeError(`unknown envelope version ${envelope[0]}`)\n if (envelope[1] !== KIND_SEALED) throw new EnvelopeError(`unexpected envelope kind ${envelope[1]}`)\n const ephemeralPublic = envelope.subarray(2, 34)\n const shared = x25519.getSharedSecret(recipientPrivateKey, ephemeralPublic)\n const recipientPublic = x25519.getPublicKey(recipientPrivateKey)\n const key = await deriveSealKey(shared, ephemeralPublic, recipientPublic)\n try {\n return new Uint8Array(\n await crypto.subtle.decrypt(\n {\n name: 'AES-GCM',\n iv: envelope.subarray(34, 46) as BufferSource,\n additionalData: aad as BufferSource,\n },\n key,\n envelope.subarray(HEADER_LENGTH) as BufferSource,\n ),\n )\n } catch {\n throw new EnvelopeError('sealed envelope authentication failed')\n }\n}\n","/**\n * The key vault (ADR 0026): everything a device needs — the identity keypair and\n * every graph keyring — as one envelope. Stored server-side as an opaque blob;\n * losing local storage is harmless.\n *\n * Format v2 (device approval): the vault content is encrypted under a random VAULT KEY,\n * and the vault key travels alongside, wrapped under the Recovery-Code-derived wrap key:\n *\n * [0] version = 1 (envelope family)\n * [1] kind = 3 (vault container)\n * [2..3] u16 BE — byte length of the wrapped-vault-key envelope\n * [4..] wrapped vault key (symmetric envelope under the WRAP key, aad 'vault-key')\n * [rest] vault content (symmetric envelope under the VAULT key, aad 'vault')\n *\n * The indirection is what device approval seals to a new device (the vault key, never the\n * code), and it makes Regenerate Recovery Code a re-WRAP: the vault key — the thing every\n * unlocked device caches — survives, so other devices stay unlocked.\n *\n * Legacy (phase 1) blobs are the content envelope directly under the wrap key; they still\n * open, and any write upgrades them to v2 with a freshly minted vault key.\n */\nimport { fromBase64Url, randomBytes, toBase64Url, utf8 } from './bytes'\nimport { ENVELOPE_VERSION, EnvelopeError, KIND_SYMMETRIC, contextAad, openSymmetric, sealSymmetric } from './envelope'\nimport { type GraphKeyring, deserializeKeyrings, serializeKeyrings } from './keyring'\nimport type { ProtectionRecord } from './protection-key'\n\nexport interface KeyVault {\n identityPrivateKey: Uint8Array\n identityPublicKey: Uint8Array\n keyrings: GraphKeyring[]\n /**\n * Protection records by graph id (ADR 0057), for graphs on a Server Backend. Personal, never\n * shared with Players — which is why they live here and not in the graph's encrypted *shared*\n * metadata (ADR 0031), where every Player would hold the blob and could attack the passphrase\n * offline. A Filesystem Backend graph has no vault and keeps its record in `etherpk/` instead.\n *\n * Nesting is deliberate and load-bearing: the record holds only a passphrase-wrapped key, so\n * reaching the vault — which a cached wrap key in `localStorage` does — still does not reach\n * protected content.\n */\n protection?: Record<string, ProtectionRecord>\n}\n\nexport const KIND_VAULT_V2 = 3\nconst VAULT_AAD = contextAad('vault')\nconst VAULT_KEY_AAD = contextAad('vault-key')\nconst CONTAINER_HEADER = 4\n\ninterface VaultJson {\n identityPrivateKey: string\n identityPublicKey: string\n keyrings: string\n /** Absent in every vault written before protection existed — read as \"no protected graphs\". */\n protection?: Record<string, ProtectionRecord>\n}\n\nexport interface EncryptedVault {\n envelope: Uint8Array\n /** The key the device caches — it opens the vault content directly. */\n vaultKey: Uint8Array\n}\n\nexport interface OpenedVault {\n vault: KeyVault\n /** The key that decrypts the vault content — cache THIS on the device. */\n vaultKey: Uint8Array\n /** True for a phase-1 blob (no indirection); the opening key was the wrap key itself. */\n legacy: boolean\n}\n\nasync function sealContent(vault: KeyVault, vaultKey: Uint8Array): Promise<Uint8Array> {\n const json: VaultJson = {\n identityPrivateKey: toBase64Url(vault.identityPrivateKey),\n identityPublicKey: toBase64Url(vault.identityPublicKey),\n keyrings: new TextDecoder().decode(serializeKeyrings(vault.keyrings)),\n // Omitted entirely when empty, so a vault holding no protected graph is byte-identical to\n // one written before the feature existed.\n ...(vault.protection && Object.keys(vault.protection).length > 0 ? { protection: vault.protection } : {}),\n }\n return sealSymmetric({ key: vaultKey, epochId: 0, plaintext: utf8(JSON.stringify(json)), aad: VAULT_AAD })\n}\n\nasync function openContent(envelope: Uint8Array, vaultKey: Uint8Array): Promise<KeyVault> {\n const { plaintext } = await openSymmetric({ keyForEpoch: () => vaultKey, envelope, aad: VAULT_AAD })\n const json = JSON.parse(new TextDecoder().decode(plaintext)) as VaultJson\n return {\n identityPrivateKey: fromBase64Url(json.identityPrivateKey),\n identityPublicKey: fromBase64Url(json.identityPublicKey),\n keyrings: deserializeKeyrings(utf8(json.keyrings)),\n ...(json.protection ? { protection: json.protection } : {}),\n }\n}\n\nfunction buildContainer(wrapped: Uint8Array, content: Uint8Array): Uint8Array {\n const out = new Uint8Array(CONTAINER_HEADER + wrapped.length + content.length)\n out[0] = ENVELOPE_VERSION\n out[1] = KIND_VAULT_V2\n new DataView(out.buffer).setUint16(2, wrapped.length, false)\n out.set(wrapped, CONTAINER_HEADER)\n out.set(content, CONTAINER_HEADER + wrapped.length)\n return out\n}\n\nfunction splitContainer(envelope: Uint8Array): { wrapped: Uint8Array; content: Uint8Array } {\n if (envelope.length < CONTAINER_HEADER) throw new EnvelopeError('vault container too short')\n const wrappedLength = new DataView(envelope.buffer, envelope.byteOffset).getUint16(2, false)\n if (envelope.length < CONTAINER_HEADER + wrappedLength) throw new EnvelopeError('vault container truncated')\n return {\n wrapped: envelope.subarray(CONTAINER_HEADER, CONTAINER_HEADER + wrappedLength),\n content: envelope.subarray(CONTAINER_HEADER + wrappedLength),\n }\n}\n\n/**\n * Encrypt a vault under `wrapKey` (v2). Pass `vaultKey` to preserve an existing vault key —\n * a Recovery Code regenerate re-wraps without invalidating other devices' cached keys; omit\n * it to mint a fresh one (new account, or upgrading a legacy blob).\n */\nexport async function encryptVault(vault: KeyVault, wrapKey: Uint8Array, vaultKey?: Uint8Array): Promise<EncryptedVault> {\n const key = vaultKey ?? randomBytes(32)\n const wrapped = await sealSymmetric({ key: wrapKey, epochId: 0, plaintext: key, aad: VAULT_KEY_AAD })\n return { envelope: buildContainer(wrapped, await sealContent(vault, key)), vaultKey: key }\n}\n\n/**\n * Open a vault with whatever key the device holds: the Recovery-Code-derived wrap key\n * (legacy or v2), or the vault key itself (v2 — the device-approval / cached case).\n */\nexport async function openVault(envelope: Uint8Array, key: Uint8Array): Promise<OpenedVault> {\n if (envelope.length >= 2 && envelope[0] === ENVELOPE_VERSION && envelope[1] === KIND_VAULT_V2) {\n const { wrapped, content } = splitContainer(envelope)\n // Only the UNWRAP is speculative: it answers \"is the held key the wrap key or the\n // vault key?\". Opening the content is not, so it stays outside the try. Wrapping both\n // meant a corrupt vault - unwrap succeeded, content failed - fell through to the\n // fallback and surfaced as \"envelope authentication failed\" against the wrong key,\n // pointing the user at the wrong cause (quality audit S17).\n let vaultKey: Uint8Array\n try {\n // The held key as the WRAP key: unwrap the vault key first.\n vaultKey = (await openSymmetric({ keyForEpoch: () => key, envelope: wrapped, aad: VAULT_KEY_AAD })).plaintext\n } catch {\n // The held key as the VAULT key: open the content directly under it.\n return { vault: await openContent(content, key), vaultKey: key, legacy: false }\n }\n return { vault: await openContent(content, vaultKey), vaultKey, legacy: false }\n }\n if (envelope.length >= 2 && envelope[1] === KIND_SYMMETRIC) {\n // Phase-1 blob: content directly under the wrap key — no separate vault key exists.\n return { vault: await openContent(envelope, key), vaultKey: key, legacy: true }\n }\n throw new EnvelopeError(`unexpected vault envelope kind ${envelope[1] ?? 'none'}`)\n}\n\n/**\n * Re-encrypt UPDATED CONTENT against the envelope it was read from. A v2 envelope keeps its\n * wrapped-key segment verbatim (content writers usually hold only the vault key, never the\n * wrap key); a legacy envelope is upgraded to v2 — there the opening key WAS the wrap key,\n * so a fresh vault key is minted under it. Cache the returned vaultKey.\n */\nexport async function reencryptVault(\n vault: KeyVault,\n previousEnvelope: Uint8Array,\n opened: Pick<OpenedVault, 'vaultKey' | 'legacy'>,\n): Promise<EncryptedVault> {\n if (opened.legacy) return encryptVault(vault, opened.vaultKey)\n const { wrapped } = splitContainer(previousEnvelope)\n return { envelope: buildContainer(wrapped, await sealContent(vault, opened.vaultKey)), vaultKey: opened.vaultKey }\n}\n","import type { SyncAccountSummary } from '@appsoftwareltd/etherpk-shared'\n\n/**\n * The client's REST bridge to the sync server (plan Phase 4). Authenticates with a\n * Personal Access Token (bearer) — no cookies, so it works cross-origin. A PAT authenticates\n * but cannot decrypt (ADR 0026); every graph/vault/identity payload it fetches is ciphertext.\n *\n * Pure over an injected `fetch` + base URL so it unit-tests without a network.\n */\nexport interface SyncApiDeps {\n baseUrl: string\n token: string | (() => Promise<string>)\n fetch?: typeof fetch\n}\n\nexport interface GraphStorageFigures {\n docBytes: number\n assetBytes: number\n}\n\nexport interface ServerGraphRecord {\n id: string\n rootDocId: string\n role: string\n /** Storage Footprint (ADR 0033); absent from older servers. */\n storage?: GraphStorageFigures\n}\n\n/** The quota-anticipating rollup over graphs the user owns (ADR 0033). */\nexport interface OwnedStorageTotals extends GraphStorageFigures {\n graphs: number\n}\n\nexport class SyncApiError extends Error {\n constructor(\n message: string,\n readonly status: number,\n readonly code?: string,\n readonly retryable = false,\n ) {\n super(message)\n }\n}\n\nexport function createSyncApi(deps: SyncApiDeps) {\n const f = deps.fetch ?? fetch\n const base = deps.baseUrl.replace(/\\/$/, '')\n\n async function call<T>(path: string, init?: RequestInit): Promise<T> {\n const token = typeof deps.token === 'string' ? deps.token : await deps.token()\n const res = await f(`${base}${path}`, {\n ...init,\n headers: {\n Authorization: `Bearer ${token}`,\n ...(init?.body ? { 'Content-Type': 'application/json' } : {}),\n ...init?.headers,\n },\n })\n if (!res.ok) {\n const body = (await res.json().catch(() => null)) as {\n error?: { message?: string } | 'quota_denied'\n code?: string\n retryable?: boolean\n } | null\n const message = typeof body?.error === 'object'\n ? body.error.message\n : body?.code\n throw new SyncApiError(message ?? `HTTP ${res.status}`, res.status, body?.code, body?.retryable === true)\n }\n return (await res.json()) as T\n }\n\n return {\n /** Resolve the authenticated service-local account before loading account-scoped data. */\n me: () => call<SyncAccountSummary>('/api/v1/sync/me'),\n createGraph: () => call<ServerGraphRecord>('/api/v1/sync/graphs', { method: 'POST' }),\n listGraphs: () => call<{ graphs: ServerGraphRecord[] }>('/api/v1/sync/graphs').then((r) => r.graphs),\n /** Graphs plus the owned-storage rollup (one request - same endpoint as listGraphs). */\n graphsOverview: () =>\n call<{ graphs: ServerGraphRecord[]; ownedStorage: OwnedStorageTotals }>('/api/v1/sync/graphs'),\n graphStorage: (graphId: string) =>\n call<GraphStorageFigures>(`/api/v1/sync/graphs/${graphId}/storage`),\n mintSyncToken: (graphId: string) =>\n call<{ token: string }>('/api/v1/sync/token', {\n method: 'POST',\n body: JSON.stringify({ graphId }),\n }).then((r) => r.token),\n getVault: () =>\n call<{ vault: string; version: number }>('/api/v1/sync/vault').catch((e: unknown) => {\n if (e instanceof SyncApiError && e.status === 404) return null\n throw e\n }),\n putVault: (vault: string, expectedVersion: number) =>\n call<{ version: number }>('/api/v1/sync/vault', {\n method: 'PUT',\n body: JSON.stringify({ vault, expectedVersion }),\n }).then((r) => r.version),\n getIdentity: (userId?: string) =>\n call<{ userId: string; publicKey: string }>(\n `/api/v1/sync/identity${userId ? `?userId=${encodeURIComponent(userId)}` : ''}`,\n ).catch((e: unknown) => {\n if (e instanceof SyncApiError && e.status === 404) return null\n throw e\n }),\n putIdentity: (publicKey: string) =>\n call<{ ok: true }>('/api/v1/sync/identity', {\n method: 'PUT',\n body: JSON.stringify({ publicKey }),\n }),\n getIdentityByEmail: (email: string) =>\n call<{ userId: string; publicKey: string }>(`/api/v1/sync/identity?email=${encodeURIComponent(email)}`).catch(\n (e: unknown) => {\n if (e instanceof SyncApiError && (e.status === 404 || e.status === 409)) return null\n throw e\n },\n ),\n createInvite: (graphId: string, inviteeEmail: string, sealedKeyring: string) =>\n call<{ id: string }>('/api/v1/sync/invites', {\n method: 'POST',\n body: JSON.stringify({ graphId, inviteeEmail, sealedKeyring }),\n }),\n listInvites: () =>\n call<{ invites: Array<{ id: string; graphId: string; rootDocId: string; sealedKeyring: string }> }>(\n '/api/v1/sync/invites',\n ).then((r) => r.invites),\n acceptInvite: (id: string) =>\n call<{ graphId: string }>(`/api/v1/sync/invites/${id}/accept`, { method: 'POST' }).then((r) => r.graphId),\n graphMembers: (graphId: string) =>\n call<{ members: Array<{ userId: string; email: string; role: string }> }>(\n `/api/v1/sync/graphs/${graphId}/members`,\n ).then((r) => r.members),\n leaveGraph: (graphId: string) =>\n call<{ ok: true }>(`/api/v1/sync/graphs/${graphId}/leave`, { method: 'POST' }),\n deleteGraph: (graphId: string) =>\n call<{ ok: true }>(`/api/v1/sync/graphs/${graphId}`, { method: 'DELETE' }),\n transferOwnership: (graphId: string, newOwnerUserId: string) =>\n call<{ ok: true }>(`/api/v1/sync/graphs/${graphId}/transfer`, {\n method: 'POST',\n body: JSON.stringify({ newOwnerUserId }),\n }),\n resetPreview: () =>\n call<{ graphs: Array<{ graphId: string; otherMembers: Array<{ userId: string; email: string; role: string }>; solo: boolean }> }>(\n '/api/v1/sync/reset',\n ).then((r) => r.graphs),\n resetAccount: () =>\n call<{ ownedGraphsDeleted: number; membershipsDropped: number; hadVault: boolean }>('/api/v1/sync/reset', {\n method: 'POST',\n }),\n // Device approval (ADR 0026 flows) — the server brokers blind: an ephemeral public\n // key out, a sealed vault key back.\n createDeviceApproval: (ephemeralPublicKey: string) =>\n call<{ id: string }>('/api/v1/sync/device-approvals', {\n method: 'POST',\n body: JSON.stringify({ ephemeralPublicKey }),\n }),\n listDeviceApprovals: () =>\n call<{ approvals: Array<{ id: string; ephemeralPublicKey: string; createdAt: string }> }>(\n '/api/v1/sync/device-approvals',\n ).then((r) => r.approvals),\n pollDeviceApproval: (id: string) =>\n call<{ status: string; sealedVaultKey?: string }>(`/api/v1/sync/device-approvals/${id}`),\n sealDeviceApproval: (id: string, sealedVaultKey: string) =>\n call<{ ok: true }>(`/api/v1/sync/device-approvals/${id}`, {\n method: 'POST',\n body: JSON.stringify({ sealedVaultKey }),\n }),\n cancelDeviceApproval: (id: string) =>\n call<{ ok: true }>(`/api/v1/sync/device-approvals/${id}`, { method: 'DELETE' }),\n }\n}\n\nexport type SyncApi = ReturnType<typeof createSyncApi>\n","/**\n * Client-side sync configuration (plan Phase 4): the sync server's base URL + the user's\n * connection choice. Managed authentication never stores bearer credentials here; custom\n * deployments keep their PAT as explicit device-local configuration.\n */\nexport const SYNC_CONFIG_STORAGE_KEY = 'etherpk:sync-config'\nexport const SYNC_CONFIG_CHANGED_EVENT = 'etherpk:sync-config-changed'\n\nexport interface ManagedSyncConfig {\n mode: 'managed'\n}\n\nexport interface CustomSyncConfig {\n mode: 'custom'\n /** e.g. https://app.etherpk.example (the sync server origin). */\n serverBaseUrl: string\n /** Personal Access Token (epk_pat_…). */\n token: string\n}\n\nexport type SyncConfig = ManagedSyncConfig | CustomSyncConfig\n\nexport function readSyncConfig(): SyncConfig | null {\n if (typeof localStorage === 'undefined') return null\n const raw = localStorage.getItem(SYNC_CONFIG_STORAGE_KEY)\n if (!raw) return null\n try {\n const parsed = JSON.parse(raw) as Record<string, unknown>\n if (parsed.mode === 'managed') return { mode: 'managed' }\n if (parsed.mode === 'custom' && typeof parsed.serverBaseUrl === 'string' && typeof parsed.token === 'string') {\n return { mode: 'custom', serverBaseUrl: parsed.serverBaseUrl, token: parsed.token }\n }\n } catch {\n /* fall through */\n }\n return null\n}\n\nexport function writeSyncConfig(config: SyncConfig): void {\n if (typeof localStorage === 'undefined') return\n localStorage.setItem(SYNC_CONFIG_STORAGE_KEY, JSON.stringify(config))\n notifySyncConfigChanged()\n}\n\nexport function clearSyncConfig(): void {\n if (typeof localStorage === 'undefined') return\n localStorage.removeItem(SYNC_CONFIG_STORAGE_KEY)\n notifySyncConfigChanged()\n}\n\n/**\n * The browser `storage` event only reaches other tabs. This same-tab event keeps persistent\n * application chrome in step when Graphs saves or removes the device's Sync connection.\n */\nfunction notifySyncConfigChanged(): void {\n if (typeof window !== 'undefined') {\n window.dispatchEvent(new Event(SYNC_CONFIG_CHANGED_EVENT))\n }\n}\n\n/** Derive the `ws(s)://…/sync` relay URL from the server base URL. */\nexport function relayUrlFrom(serverBaseUrl: string): string {\n const base = serverBaseUrl.replace(/\\/$/, '')\n return base.replace(/^http/, 'ws') + '/sync'\n}\n","/**\n * Sync tokens are short-lived (15 minutes, server-side) and authenticate one graph\n * (ADR 0026). Anything that OUTLIVES one token - an import, an open graph session, a\n * WebSocket reconnect - must therefore ask for a token at the moment it needs one rather\n * than capture a string when it is constructed.\n *\n * Capturing one is what killed a live import: the asset uploader held the token minted at\n * the start of the run, uploaded 4592 assets over exactly fifteen minutes, and then every\n * `begin` call 401'd. A caller that holds a `SyncTokenSource` cannot make that mistake.\n *\n * The token AUTHENTICATES only - it is not a Graph Key and decrypts nothing - so re-minting\n * is cheap and carries no key material.\n */\n\nimport { fromBase64Url } from '$lib/crypto'\n\n/**\n * Supplies a currently-valid sync token, re-minting when the held one is near expiry.\n *\n * `force` discards the held token first. It exists for the one case the expiry maths cannot\n * cover: the server rejecting a token this source still believes is live, which means the\n * two clocks disagree by more than the refresh margin. A caller that sees a 401 asks again\n * with `force` and retries, rather than presenting the same dead token until it gives up.\n */\nexport type SyncTokenSource = (opts?: { force?: boolean }) => Promise<string>\n\n/**\n * Re-mint once a quarter of the token's life remains. A fixed minute of headroom is not\n * enough: `exp` is stamped by the SERVER's clock and compared against the BROWSER's, and a\n * client running a few minutes slow would hand out a token the server has already retired.\n * An over-eager mint costs one small request.\n */\nconst REFRESH_AT_FRACTION = 0.25\nconst MIN_MARGIN_MS = 60_000\n\n/** Assumed life of a token whose `exp` cannot be read (the dev/e2e gate injects its own). */\nconst OPAQUE_TOKEN_LIFETIME_MS = 5 * 60_000\n\n/**\n * Read `exp` out of a sync token WITHOUT verifying it. The server is the only verifier;\n * the client reads the claim solely to know when to ask for a replacement, so a forged or\n * malformed token costs nothing here - it simply refreshes on a conservative default.\n */\nexport function syncTokenExpiry(token: string): number | null {\n const dot = token.lastIndexOf('.')\n if (dot < 0) return null\n try {\n const json = new TextDecoder().decode(fromBase64Url(token.slice(0, dot)))\n const exp = (JSON.parse(json) as { exp?: unknown }).exp\n return typeof exp === 'number' ? exp : null\n } catch {\n return null\n }\n}\n\n/**\n * A token that never refreshes. For the dev/e2e gate, which injects a token in the URL and\n * has no API to mint from, and for one-shot sessions that cannot outlive a single token.\n */\nexport function fixedSyncToken(token: string): SyncTokenSource {\n return () => Promise.resolve(token)\n}\n\n/**\n * A refreshing source over `mint` (normally `api.mintSyncToken(graphId)`). Concurrent\n * callers share one in-flight mint: the asset uploader runs six uploads at once, and six\n * simultaneous expiries must cost one round trip, not six.\n */\nexport function createSyncTokenSource(\n mint: () => Promise<string>,\n opts?: { now?: () => number },\n): SyncTokenSource {\n const now = opts?.now ?? (() => Date.now())\n let held: { token: string; refreshAt: number } | undefined\n let inFlight: Promise<string> | undefined\n\n return async (opts) => {\n if (opts?.force) held = undefined\n if (held && now() < held.refreshAt) return held.token\n // A forced call joins a mint already running: six uploads rejected at once must cost\n // one new token, not six.\n if (!inFlight) {\n inFlight = mint()\n .then((token) => {\n const issuedAt = now()\n const expiresAt = syncTokenExpiry(token) ?? issuedAt + OPAQUE_TOKEN_LIFETIME_MS\n const lifetime = Math.max(0, expiresAt - issuedAt)\n const margin = Math.max(MIN_MARGIN_MS, lifetime * REFRESH_AT_FRACTION)\n held = { token, refreshAt: expiresAt - margin }\n return token\n })\n .finally(() => {\n inFlight = undefined\n })\n }\n return inFlight\n }\n}\n","/**\n * The account side of the [[Headless Client]]: the same REST bridge and vault the browser uses,\n * driven from a config file instead of a session. A [[Personal Access Token]] authenticates; the\n * vault key decrypts. Neither is minted here - `login.ts` obtains the second, the portal the first.\n */\n\nimport { fromBase64Url, openVault, type GraphKeyring, type KeyVault } from '$lib/crypto'\nimport { createSyncApi, type ServerGraphRecord, type SyncApi } from '$lib/sync/sync-api'\nimport { relayUrlFrom } from '$lib/sync/sync-config'\nimport { createSyncTokenSource, type SyncTokenSource } from '$lib/sync/sync-token'\n\nimport type { HeadlessConfig } from './config'\n\nexport interface HeadlessAccount {\n readonly api: SyncApi\n readonly serverBaseUrl: string\n readonly relayUrl: string\n /** Who the PAT resolves to, for the login summary. */\n readonly principal: { id: string; email: string | null; name: string | null }\n /** The EtherPK Client for this deployment, when the Server declares one (older ones do not). */\n readonly clientUrl: string | null\n /** A token source for one graph, re-minted on every reconnect (a session outlives a token). */\n tokenFor(graphId: string): SyncTokenSource\n}\n\nexport function createHeadlessAccount(config: Pick<HeadlessConfig, 'server' | 'pat'>): Omit<HeadlessAccount, 'principal' | 'clientUrl'> {\n const api = createSyncApi({ baseUrl: config.server, token: config.pat })\n return {\n api,\n serverBaseUrl: config.server,\n relayUrl: relayUrlFrom(config.server),\n tokenFor: (graphId) => createSyncTokenSource(() => api.mintSyncToken(graphId)),\n }\n}\n\n/** Resolve the PAT to its principal; a graph-scoped or revoked token fails here, in words. */\nexport async function connectAccount(config: Pick<HeadlessConfig, 'server' | 'pat'>): Promise<HeadlessAccount> {\n const account = createHeadlessAccount(config)\n const me = await account.api.me()\n return {\n ...account,\n principal: { id: me.principal.id, email: me.principal.email, name: me.principal.name },\n clientUrl: me.clientUrl ?? null,\n }\n}\n\n/**\n * Open the account vault with the cached vault key. Never writes: a device that only reads the\n * vault cannot create keyrings, mint a Recovery Code or upgrade a blob, which is right for a\n * process that exists to serve one graph.\n */\nexport async function openAccountVault(api: SyncApi, vaultKey: Uint8Array): Promise<KeyVault> {\n const stored = await api.getVault()\n if (!stored) {\n throw new Error('This account has no encryption keys yet. Open EtherPK, create or accept a synced graph, and save the Recovery Code first.')\n }\n return (await openVault(fromBase64Url(stored.vault), vaultKey)).vault\n}\n\nexport interface ResolvedGraph {\n record: ServerGraphRecord\n keyring: GraphKeyring\n}\n\n/**\n * The graph the user named, by id, from the graphs the PAT can see and the keyrings the vault\n * holds. A graph the account is a member of but has no keyring for is an invite not yet\n * accepted in EtherPK - said so, rather than reported as \"not found\".\n */\nexport function resolveGraphById(graphs: ServerGraphRecord[], vault: KeyVault, graphId: string): ResolvedGraph {\n const record = graphs.find((graph) => graph.id === graphId)\n if (!record) {\n throw new Error(`No synced graph with id ${graphId} is reachable with this token. Run \\`etherpk-mcp graphs\\` to list them.`)\n }\n const keyring = vault.keyrings.find((entry) => entry.graphId === graphId)\n if (!keyring) {\n throw new Error(`This account holds no key for graph ${graphId}. Open the graph in EtherPK (accept its invite if there is one) so the key lands in your vault, then try again.`)\n }\n return { record, keyring }\n}\n","/**\n * What `login` leaves behind and `serve` reads: the Sync Server origin, the account-wide\n * [[Personal Access Token]], and the vault key that opens the account's keys on this machine.\n *\n * The file is the same trust class as the browser's `localStorage` cache of the same key\n * (DESIGN.md → Key storage between sessions): whoever can read this user's files can read the\n * account. It is written `0600` in the user's config directory, never anywhere a project\n * checkout could pick it up, and `ETHERPK_MCP_CONFIG` overrides the path for tests and for a\n * box that keeps its secrets elsewhere. An OS keychain would be the next step (ADR 0072).\n */\n\nimport { chmod, mkdir, readFile, writeFile } from 'node:fs/promises'\nimport { homedir } from 'node:os'\nimport { dirname, join } from 'node:path'\n\nexport interface HeadlessConfig {\n /** `https://…`, no trailing slash. */\n server: string\n /** The account-wide PAT (a graph-scoped one cannot reach the vault). */\n pat: string\n /** The vault key, base64url. Absent until `login` has unlocked the account. */\n vaultKey?: string\n}\n\nexport function defaultConfigPath(env: NodeJS.ProcessEnv = process.env): string {\n const override = env.ETHERPK_MCP_CONFIG?.trim()\n if (override) return override\n const base = env.XDG_CONFIG_HOME?.trim() || join(homedir(), '.config')\n return join(base, 'etherpk', 'mcp.json')\n}\n\n/** Tolerant of a hand-edited file: keeps the fields it knows, refuses the rest. */\nexport function parseConfig(raw: string): HeadlessConfig | null {\n let parsed: unknown\n try {\n parsed = JSON.parse(raw)\n } catch {\n return null\n }\n if (typeof parsed !== 'object' || parsed === null) return null\n const rec = parsed as Record<string, unknown>\n if (typeof rec.server !== 'string' || typeof rec.pat !== 'string') return null\n const out: HeadlessConfig = { server: rec.server.replace(/\\/$/, ''), pat: rec.pat }\n if (typeof rec.vaultKey === 'string' && rec.vaultKey !== '') out.vaultKey = rec.vaultKey\n return out\n}\n\nexport function serialiseConfig(config: HeadlessConfig): string {\n return `${JSON.stringify(config, null, 2)}\\n`\n}\n\nexport async function readConfig(path: string): Promise<HeadlessConfig | null> {\n try {\n return parseConfig(await readFile(path, 'utf8'))\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null\n throw error\n }\n}\n\n/** Write with owner-only permissions, creating the directory; the mode is set even on overwrite. */\nexport async function writeConfig(path: string, config: HeadlessConfig): Promise<void> {\n await mkdir(dirname(path), { recursive: true, mode: 0o700 })\n await writeFile(path, serialiseConfig(config), { mode: 0o600 })\n await chmod(path, 0o600)\n}\n","/**\n * Content-free performance diagnostics for graph and editor hot paths.\n *\n * Recording is opt-in so normal sessions do not retain timing data. Callers use stable\n * operation names and numeric dimensions only. Concepts, note text, key material and\n * ciphertext must never be passed as labels or detail.\n */\nexport interface PerformanceMetric {\n name: string\n durationMs: number\n at: number\n dimensions: Readonly<Record<string, number>>\n}\n\nexport interface PerformanceRecorder {\n measure<T>(\n name: string,\n dimensions: Readonly<Record<string, number>>,\n operation: () => T,\n ): T\n measureAsync<T>(\n name: string,\n dimensions: Readonly<Record<string, number>>,\n operation: () => Promise<T>,\n ): Promise<T>\n mark(name: string, dimensions?: Readonly<Record<string, number>>): void\n snapshot(): PerformanceMetric[]\n clear(): void\n}\n\nfunction assertSafeName(name: string): void {\n if (!/^[a-z0-9][a-z0-9._-]*$/u.test(name)) {\n throw new Error('Performance metric names may contain only lower-case stable identifiers')\n }\n}\n\nexport function createPerformanceRecorder(options: {\n enabled: boolean\n now?: () => number\n capacity?: number\n}): PerformanceRecorder {\n const now = options.now ?? (() => performance.now())\n const capacity = options.capacity ?? 2_000\n const metrics: PerformanceMetric[] = []\n\n function record(\n name: string,\n startedAt: number,\n dimensions: Readonly<Record<string, number>>,\n ): void {\n if (!options.enabled) return\n assertSafeName(name)\n const endedAt = now()\n metrics.push({\n name,\n durationMs: Math.max(0, endedAt - startedAt),\n at: endedAt,\n dimensions: { ...dimensions },\n })\n if (metrics.length > capacity) metrics.splice(0, metrics.length - capacity)\n\n if (typeof performance !== 'undefined' && performance.mark && performance.measure) {\n const start = `etherpk.${name}.start`\n const end = `etherpk.${name}.end`\n performance.mark(start, { startTime: startedAt })\n performance.mark(end, { startTime: endedAt })\n performance.measure(`etherpk.${name}`, start, end)\n performance.clearMarks(start)\n performance.clearMarks(end)\n }\n }\n\n return {\n measure(name, dimensions, operation) {\n if (!options.enabled) return operation()\n assertSafeName(name)\n const startedAt = now()\n try {\n return operation()\n } finally {\n record(name, startedAt, dimensions)\n }\n },\n async measureAsync(name, dimensions, operation) {\n if (!options.enabled) return operation()\n assertSafeName(name)\n const startedAt = now()\n try {\n return await operation()\n } finally {\n record(name, startedAt, dimensions)\n }\n },\n mark(name, dimensions = {}) {\n if (!options.enabled) return\n assertSafeName(name)\n const at = now()\n metrics.push({ name, durationMs: 0, at, dimensions: { ...dimensions } })\n if (metrics.length > capacity) metrics.splice(0, metrics.length - capacity)\n performance.mark(`etherpk.${name}`)\n },\n snapshot: () => metrics.map((metric) => ({ ...metric, dimensions: { ...metric.dimensions } })),\n clear: () => {\n metrics.length = 0\n },\n }\n}\n\nexport const performanceRecorder = createPerformanceRecorder({\n enabled: typeof window !== 'undefined' && import.meta.env.DEV,\n})\n","import { z } from 'zod'\n\nexport const MANAGED_SYNC_AUDIENCE = 'urn:etherpk:managed-sync'\nexport const MANAGED_SYNC_SCOPE = 'sync'\nexport const ENTITLEMENT_AUDIENCE = 'urn:etherpk:sync-entitlements'\nexport const ENTITLEMENT_SERVICE = 'managed-sync'\nexport const ENTITLEMENT_READ_SCOPE = 'entitlements:read'\nexport const MANAGED_USAGE_SCOPE = 'managed-usage:read'\nexport const ENTITLEMENT_SUBJECT_CLAIM = 'https://etherpk.com/claims/entitlement-subject'\n\n/** The `billing-account:<id>` prefix every entitlement subject carries. */\nexport const ENTITLEMENT_SUBJECT_PREFIX = 'billing-account:'\n\n/**\n * Version-agnostic, like `isUuid` on the Server. The subject was matched by four different\n * hand-rolled patterns that happened to agree only because billing account ids are v4 today:\n * one accepted v1 to v5, another v1 to v8, another any 36 characters of hex and dashes\n * (quality audit B4). If an id ever became a uuidv7 - which is what better-auth already mints\n * - the strictest of them would have silently stopped matching, and the Stripe webhook's\n * metadata fallback would have answered 409 \"Unknown Stripe customer\" for a real subscription.\n * One pattern, here, next to the claim it belongs to.\n */\nexport const ENTITLEMENT_SUBJECT_PATTERN =\n /^billing-account:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i\n\n/** Is this a well-formed `billing-account:<uuid>` subject? */\nexport function isEntitlementSubject(value: unknown): value is string {\n return typeof value === 'string' && ENTITLEMENT_SUBJECT_PATTERN.test(value)\n}\n\nconst httpsUrl = z.url().refine((value) => {\n const url = new URL(value)\n return url.protocol === 'https:' || url.hostname === 'localhost' || url.hostname === '127.0.0.1'\n}, 'URL must use HTTPS except on localhost')\n\nconst authCapabilitySchema = z.discriminatedUnion('mode', [\n z.strictObject({\n mode: z.literal('standalone'),\n portalUrl: httpsUrl,\n pat: z.boolean(),\n }),\n z.strictObject({\n mode: z.literal('oidc'),\n portalUrl: httpsUrl,\n issuer: httpsUrl,\n pat: z.boolean(),\n }),\n])\n\nexport const syncServerCapabilitiesSchema = z.strictObject({\n syncApiVersion: z.literal(1),\n auth: authCapabilitySchema,\n})\n\nexport type SyncServerCapabilities = z.infer<typeof syncServerCapabilitiesSchema>\n\nexport const entitlementLimitsSchema = z.strictObject({\n ownedGraphs: z.int().nonnegative(),\n ownedStorageBytes: z.int().nonnegative(),\n playersPerGraph: z.int().nonnegative(),\n assetBytes: z.int().nonnegative(),\n assetChunks: z.int().nonnegative(),\n})\n\nexport type EntitlementLimits = z.infer<typeof entitlementLimitsSchema>\n\nexport const serviceEntitlementSchema = z.strictObject({\n eventId: z.uuid(),\n issuer: httpsUrl,\n audience: z.literal(ENTITLEMENT_AUDIENCE),\n subject: z.string().regex(ENTITLEMENT_SUBJECT_PATTERN),\n service: z.literal(ENTITLEMENT_SERVICE),\n revision: z.int().positive(),\n status: z.enum(['active', 'grace', 'read_only', 'suspended', 'identity_disabled']),\n plan: z.string().min(1).max(64),\n limits: entitlementLimitsSchema,\n effectiveAt: z.iso.datetime({ offset: true }),\n expiresAt: z.iso.datetime({ offset: true }),\n}).refine(\n ({ effectiveAt, expiresAt }) => Date.parse(expiresAt) > Date.parse(effectiveAt),\n { message: 'expiresAt must be later than effectiveAt', path: ['expiresAt'] },\n)\n\nexport type ServiceEntitlement = z.infer<typeof serviceEntitlementSchema>\n\n/** Content-free aggregate usage which the Sync data plane may expose to Corporate. */\nexport const managedUsageSchema = z.strictObject({\n subject: z.string().regex(ENTITLEMENT_SUBJECT_PATTERN),\n service: z.literal(ENTITLEMENT_SERVICE),\n measuredAt: z.iso.datetime({ offset: true }),\n ownedGraphs: z.int().nonnegative(),\n ownedStorageBytes: z.int().nonnegative(),\n})\n\nexport type ManagedUsage = z.infer<typeof managedUsageSchema>\n\nconst syncAuthenticationSchema = z.discriminatedUnion('mode', [\n z.strictObject({\n mode: z.literal('standalone'),\n method: z.enum(['session', 'pat']),\n }),\n z.strictObject({\n mode: z.literal('managed'),\n method: z.enum(['oidc', 'pat']),\n }),\n])\n\n/**\n * The authenticated, provider-neutral account view exposed by a Sync Server.\n * Provider subjects and entitlement subjects stay server-side because callers only\n * need the stable service-local Principal, display profile, limits, and usage.\n */\nexport const syncAccountSummarySchema = z.strictObject({\n principal: z.strictObject({\n id: z.uuid(),\n email: z.email().nullable(),\n name: z.string().min(1).nullable(),\n image: httpsUrl.nullable(),\n }),\n authentication: syncAuthenticationSchema,\n /**\n * Where the EtherPK Client for this deployment lives (`CLIENT_PUBLIC_URL`), so a device that\n * knows only the Sync Server - the Headless Client asking to be approved - can name the app\n * the user must open. Optional: an older Server does not send it.\n */\n clientUrl: httpsUrl.optional(),\n entitlement: z.strictObject({\n plan: z.string().min(1).max(64),\n status: serviceEntitlementSchema.shape.status,\n limits: entitlementLimitsSchema,\n usage: z.strictObject({\n ownedGraphs: z.int().nonnegative(),\n ownedStorageBytes: z.int().nonnegative(),\n }),\n }),\n})\n\nexport type SyncAccountSummary = z.infer<typeof syncAccountSummarySchema>\n\nexport const quotaErrorCodeSchema = z.enum([\n 'entitlement_inactive',\n 'owned_graph_limit',\n 'owned_storage_limit',\n 'players_per_graph_limit',\n 'asset_size_limit',\n 'asset_chunk_limit',\n 'ownership_transfer_limit',\n])\n\nexport type QuotaErrorCode = z.infer<typeof quotaErrorCodeSchema>\n\nexport const quotaErrorResponseSchema = z.strictObject({\n error: z.literal('quota_denied'),\n code: quotaErrorCodeSchema,\n ownerPrincipalId: z.uuid().optional(),\n retryable: z.literal(true),\n})\n\nexport type QuotaErrorResponse = z.infer<typeof quotaErrorResponseSchema>\n","import { z } from 'zod'\nimport { quotaErrorCodeSchema } from './managed-service'\n\nexport const SYNC_PROTOCOL_VERSION = 2 as const\n\nexport interface SyncProtocolLimits {\n maxMessageBytes: number\n maxEnvelopeBytes: number\n maxSubscriptionDocIds: number\n maxCatchupRows: number\n maxCatchupBytes: number\n malformedMessageBudget: number\n /**\n * A presence envelope (one encrypted awareness update, a few hundred bytes in practice)\n * is bounded separately from content. The relay keeps the latest one per connection and\n * document and replays it to every late subscriber with no database work in the way, so\n * sharing the content ceiling turned one upload into an outbound amplifier (ASTRA F11).\n */\n maxPresenceEnvelopeBytes: number\n /**\n * Document subscriptions one connection may hold at once. `maxSubscriptionDocIds` bounds a\n * single message; this bounds the cumulative set, which used to grow without limit.\n */\n maxSubscriptionsPerConnection: number\n /**\n * Outbound bytes the relay lets queue for one socket before it gives up on the reader.\n * The client resubscribes and catches up from durable storage on reconnect, so dropping a\n * reader that stops draining loses nothing except its live feed.\n */\n maxOutboundBufferedBytes: number\n /**\n * Sockets one user may hold open on one graph. Every browser tab holds its own socket, so\n * this is well above ordinary duplicate-tab use and only bites a script.\n */\n maxSocketsPerUserGraph: number\n}\n\nexport const SYNC_PROTOCOL_LIMITS: Readonly<SyncProtocolLimits> = Object.freeze({\n maxMessageBytes: 12 * 1024 * 1024,\n maxEnvelopeBytes: 8 * 1024 * 1024,\n maxSubscriptionDocIds: 512,\n maxCatchupRows: 256,\n maxCatchupBytes: 8 * 1024 * 1024,\n malformedMessageBudget: 3,\n maxPresenceEnvelopeBytes: 16 * 1024,\n maxSubscriptionsPerConnection: 4096,\n maxOutboundBufferedBytes: 64 * 1024 * 1024,\n maxSocketsPerUserGraph: 10,\n})\n\nexport type SyncProtocolLimitOverrides = Partial<SyncProtocolLimits>\n\nexport const SYNC_ERROR_CODES = [\n 'unsupported_version',\n 'invalid_message',\n 'oversized_message',\n 'stale_generation',\n 'missing_sequence',\n 'membership_revoked',\n 'outbox_conflict',\n 'quota_denied',\n /** A subscribe that would take the connection past `maxSubscriptionsPerConnection`; nothing was subscribed. */\n 'subscription_limit',\n /** A snapshot read-back named a (generation, throughSeq) the relay does not hold. */\n 'snapshot_missing',\n 'internal_error',\n] as const\n\nexport type SyncErrorCode = (typeof SYNC_ERROR_CODES)[number]\n\nconst safeInteger = z.number().int().nonnegative().safe()\nconst generation = z.number().int().positive().safe()\nconst uuid = z.uuid()\nconst base64Url = z\n .string()\n .min(1)\n .regex(/^[A-Za-z0-9_-]+$/)\n .refine((value) => value.length % 4 !== 1, 'Invalid unpadded base64url length')\n\n/**\n * The presence envelope's tighter bound, restated in the schema so a message that somehow\n * reaches `safeParse` without the preflight is still refused. The preflight normally answers\n * first, with the more specific `oversized_message` code.\n */\nfunction presenceEnvelopeSchema(limits: SyncProtocolLimits) {\n return base64Url.refine(\n (value) => (decodedBase64UrlBytes(value) ?? 0) <= limits.maxPresenceEnvelopeBytes,\n 'Presence envelope exceeds the configured byte limit',\n )\n}\n\nfunction clientMessageSchema(limits: SyncProtocolLimits) {\n const version = z.literal(SYNC_PROTOCOL_VERSION)\n const presenceEnvelope = presenceEnvelopeSchema(limits)\n const subscribe = z.union([\n z.object({ v: version, type: z.literal('subscribe'), all: z.literal(true) }).strict(),\n z\n .object({\n v: version,\n type: z.literal('subscribe'),\n docIds: z.array(uuid).max(limits.maxSubscriptionDocIds),\n })\n .strict(),\n ])\n\n return z.discriminatedUnion('type', [\n // Zod cannot discriminate the two subscribe shapes itself, so validate their union\n // as one schema in a final outer union below.\n z\n .object({\n v: version,\n type: z.literal('unsubscribe'),\n docIds: z.array(uuid).max(limits.maxSubscriptionDocIds),\n })\n .strict(),\n z\n .object({\n v: version,\n type: z.literal('watermarks'),\n requestId: uuid,\n docIds: z.array(uuid).min(1).max(limits.maxSubscriptionDocIds),\n })\n .strict(),\n z\n .object({\n v: version,\n type: z.literal('append'),\n docId: uuid,\n outboxId: uuid,\n generation,\n epochId: safeInteger,\n envelope: base64Url,\n })\n .strict(),\n z\n .object({\n v: version,\n type: z.literal('delete'),\n docId: uuid,\n outboxId: uuid,\n generation,\n })\n .strict(),\n z\n .object({\n v: version,\n type: z.literal('resurrect'),\n docId: uuid,\n outboxId: uuid,\n generation,\n epochId: safeInteger,\n envelope: base64Url,\n })\n .strict(),\n z\n .object({\n v: version,\n type: z.literal('catchup'),\n requestId: uuid,\n docId: uuid,\n generation,\n afterSeq: safeInteger,\n priority: z.enum(['foreground', 'background']),\n maxRows: z.number().int().positive().max(limits.maxCatchupRows),\n maxBytes: z.number().int().positive().max(limits.maxCatchupBytes),\n })\n .strict(),\n z\n .object({\n v: version,\n type: z.literal('snapshot_put'),\n docId: uuid,\n generation,\n throughSeq: safeInteger,\n epochId: safeInteger,\n envelope: base64Url,\n })\n .strict(),\n // Read-back verification (ADR 0025, amended 2026-09-12): the compacting client asks\n // for the snapshot it just stored, decodes it, and only then declares it verified.\n // The relay prunes history only behind a verified snapshot.\n z\n .object({\n v: version,\n type: z.literal('snapshot_get'),\n docId: uuid,\n generation,\n throughSeq: safeInteger,\n })\n .strict(),\n z\n .object({\n v: version,\n type: z.literal('snapshot_verified'),\n docId: uuid,\n generation,\n throughSeq: safeInteger,\n })\n .strict(),\n z.object({ v: version, type: z.literal('presence'), docId: uuid, envelope: presenceEnvelope }).strict(),\n z.object({ v: version, type: z.literal('ack_confirm'), outboxId: uuid }).strict(),\n ]).or(subscribe)\n}\n\nfunction serverMessageSchema(limits: SyncProtocolLimits) {\n const version = z.literal(SYNC_PROTOCOL_VERSION)\n const presenceEnvelope = presenceEnvelopeSchema(limits)\n const update = z\n .object({\n seq: safeInteger,\n epochId: safeInteger,\n envelope: base64Url,\n })\n .strict()\n const snapshot = z\n .object({\n throughSeq: safeInteger,\n epochId: safeInteger,\n envelope: base64Url,\n })\n .strict()\n\n return z.discriminatedUnion('type', [\n z\n .object({\n v: version,\n type: z.literal('ack'),\n outboxId: uuid,\n generation,\n state: z.enum(['active', 'deleted']),\n seq: safeInteger,\n })\n .strict(),\n z.object({ v: version, type: z.literal('update'), docId: uuid, generation }).extend(update.shape).strict(),\n /** The relay stored a snapshot_put; the client may now read it back. */\n z\n .object({\n v: version,\n type: z.literal('snapshot_ack'),\n docId: uuid,\n generation,\n throughSeq: safeInteger,\n })\n .strict(),\n /** The stored snapshot, served back by exact identity for verification. */\n z\n .object({\n v: version,\n type: z.literal('snapshot_data'),\n docId: uuid,\n generation,\n throughSeq: safeInteger,\n epochId: safeInteger,\n envelope: base64Url,\n })\n .strict(),\n z\n .object({\n v: version,\n type: z.literal('catchup_batch'),\n /** Omitted only for unsolicited delete lifecycle broadcasts. */\n requestId: uuid.optional(),\n docId: uuid,\n generation,\n state: z.enum(['active', 'deleted']),\n throughSeq: safeInteger,\n hasMore: z.boolean(),\n updates: z.array(update).max(limits.maxCatchupRows),\n snapshot: snapshot.optional(),\n })\n .strict(),\n z.object({ v: version, type: z.literal('presence'), docId: uuid, envelope: presenceEnvelope }).strict(),\n z\n .object({\n v: version,\n type: z.literal('watermarks'),\n requestId: uuid,\n documents: z\n .array(\n z\n .object({\n docId: uuid,\n generation,\n state: z.enum(['active', 'deleted']),\n lastSeq: safeInteger,\n })\n .strict(),\n )\n .max(limits.maxSubscriptionDocIds),\n })\n .strict(),\n z\n .object({\n v: version,\n type: z.literal('error'),\n code: z.enum(SYNC_ERROR_CODES),\n message: z.string().min(1).max(256),\n docId: uuid.optional(),\n currentGeneration: generation.optional(),\n quotaCode: quotaErrorCodeSchema.optional(),\n retryable: z.boolean().optional(),\n })\n .strict(),\n ])\n}\n\n// Exported because the types below are derived from them with `typeof`, and a module-private\n// binding used only in type position reads as dead code to every tool that looks.\nexport const defaultClientMessageSchema = clientMessageSchema(SYNC_PROTOCOL_LIMITS)\nexport const defaultServerMessageSchema = serverMessageSchema(SYNC_PROTOCOL_LIMITS)\n\n/** The exact JSON message accepted from a sync client. */\nexport type SyncClientMessage = z.infer<typeof defaultClientMessageSchema>\n/** The exact JSON message accepted from a sync server. */\nexport type SyncServerMessage = z.infer<typeof defaultServerMessageSchema>\nexport type SyncUpdate = Extract<SyncServerMessage, { type: 'catchup_batch' }>['updates'][number]\n\nexport type SyncProtocolParseResult<T> =\n | { ok: true; value: T }\n | { ok: false; code: 'unsupported_version' | 'invalid_message' | 'oversized_message'; message: string }\n\nfunction byteLength(value: string): number {\n return new TextEncoder().encode(value).byteLength\n}\n\n/** Exact decoded size for valid, unpadded base64url without allocating decoded bytes. */\nexport function decodedBase64UrlBytes(value: string): number | null {\n if (!/^[A-Za-z0-9_-]+$/.test(value) || value.length % 4 === 1) return null\n return Math.floor((value.length * 3) / 4)\n}\n\nexport function validateBase64UrlEnvelope(\n value: unknown,\n limits: Pick<SyncProtocolLimits, 'maxEnvelopeBytes'> = SYNC_PROTOCOL_LIMITS,\n): SyncProtocolParseResult<string> {\n if (typeof value !== 'string' || value.length === 0) {\n return { ok: false, code: 'invalid_message', message: 'Envelope is not valid base64url' }\n }\n const bytes = decodedBase64UrlBytes(value)\n if (bytes === null) return { ok: false, code: 'invalid_message', message: 'Envelope is not valid base64url' }\n if (bytes > limits.maxEnvelopeBytes) {\n return { ok: false, code: 'oversized_message', message: 'Envelope exceeds the configured byte limit' }\n }\n return { ok: true, value }\n}\n\nfunction envelopePreflight(\n parsed: Record<string, unknown>,\n limits: SyncProtocolLimits,\n): SyncProtocolParseResult<true> {\n const envelopes: unknown[] = []\n if ('envelope' in parsed) envelopes.push(parsed.envelope)\n if (parsed.type === 'catchup_batch') {\n if (Array.isArray(parsed.updates)) {\n for (const update of parsed.updates) {\n if (typeof update === 'object' && update !== null) {\n envelopes.push((update as Record<string, unknown>).envelope)\n }\n }\n }\n if (typeof parsed.snapshot === 'object' && parsed.snapshot !== null) {\n envelopes.push((parsed.snapshot as Record<string, unknown>).envelope)\n }\n }\n\n // Presence is bounded on its own, in both directions: the relay parses it inbound and the\n // client parses the relay's broadcast and late-join replay of the very same envelope.\n const envelopeLimit =\n parsed.type === 'presence' ? { maxEnvelopeBytes: limits.maxPresenceEnvelopeBytes } : limits\n let catchupBytes = 0\n for (const envelope of envelopes) {\n const result = validateBase64UrlEnvelope(envelope, envelopeLimit)\n if (!result.ok) return result\n catchupBytes += decodedBase64UrlBytes(result.value) ?? 0\n }\n if (parsed.type === 'catchup_batch' && catchupBytes > limits.maxCatchupBytes) {\n return { ok: false, code: 'oversized_message', message: 'Catch-up page exceeds the configured byte limit' }\n }\n return { ok: true, value: true }\n}\n\nfunction parseMessage<T>(\n raw: string,\n schema: z.ZodType<T>,\n limits: SyncProtocolLimits,\n): SyncProtocolParseResult<T> {\n if (byteLength(raw) > limits.maxMessageBytes) {\n return { ok: false, code: 'oversized_message', message: 'Message exceeds the configured byte limit' }\n }\n\n let parsed: unknown\n try {\n parsed = JSON.parse(raw)\n } catch {\n return { ok: false, code: 'invalid_message', message: 'Message is not valid JSON' }\n }\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n return { ok: false, code: 'invalid_message', message: 'Message must be a JSON object' }\n }\n const record = parsed as Record<string, unknown>\n if (record.v !== SYNC_PROTOCOL_VERSION) {\n return { ok: false, code: 'unsupported_version', message: 'Unsupported sync protocol version' }\n }\n const envelopeResult = envelopePreflight(record, limits)\n if (!envelopeResult.ok) return envelopeResult\n\n const result = schema.safeParse(parsed)\n if (!result.success) {\n return { ok: false, code: 'invalid_message', message: 'Message does not match the sync protocol' }\n }\n return { ok: true, value: result.data }\n}\n\nexport interface SyncProtocol {\n readonly limits: SyncProtocolLimits\n parseClientMessage(raw: string): SyncProtocolParseResult<SyncClientMessage>\n parseServerMessage(raw: string): SyncProtocolParseResult<SyncServerMessage>\n serializeClientMessage(message: SyncClientMessage): string\n serializeServerMessage(message: SyncServerMessage): string\n}\n\nexport function createSyncProtocol(overrides: SyncProtocolLimitOverrides = {}): SyncProtocol {\n const limits = Object.freeze({ ...SYNC_PROTOCOL_LIMITS, ...overrides })\n const clientSchema = clientMessageSchema(limits)\n const serverSchema = serverMessageSchema(limits)\n\n const parseClient = (raw: string) => parseMessage(raw, clientSchema, limits)\n const parseServer = (raw: string) => parseMessage(raw, serverSchema, limits)\n\n function serialize<T>(message: T): string {\n const raw = JSON.stringify(message)\n if (byteLength(raw) > limits.maxMessageBytes) {\n throw new Error('oversized_message: Message exceeds the configured byte limit')\n }\n return raw\n }\n\n return {\n limits,\n parseClientMessage: parseClient,\n parseServerMessage: parseServer,\n serializeClientMessage: (message) => serialize(message),\n serializeServerMessage: (message) => serialize(message),\n }\n}\n\nconst defaultProtocol = createSyncProtocol()\n\nexport const parseClientMessage = defaultProtocol.parseClientMessage\nexport const parseServerMessage = defaultProtocol.parseServerMessage\nexport const serializeClientMessage = defaultProtocol.serializeClientMessage\nexport const serializeServerMessage = defaultProtocol.serializeServerMessage\n","import * as encoding from 'lib0/encoding'\nimport * as Y from 'yjs'\n\n/**\n * A Yjs state vector is a set of per-client clocks. Relay acknowledgement and remote-update\n * order are independent, so replacing one vector with another can forget already-confirmed\n * structs. The union is the component-wise maximum.\n */\nexport function mergeStateVectors(...vectors: readonly Uint8Array[]): Uint8Array {\n const clocks = new Map<number, number>()\n for (const vector of vectors) {\n if (vector.byteLength === 0) continue\n for (const [client, clock] of Y.decodeStateVector(vector)) {\n clocks.set(client, Math.max(clocks.get(client) ?? 0, clock))\n }\n }\n\n const encoder = encoding.createEncoder()\n encoding.writeVarUint(encoder, clocks.size)\n // Stable ordering is not required by Yjs, but deterministic bytes make cache diagnostics\n // and tests much easier to reason about.\n for (const [client, clock] of [...clocks].sort(([a], [b]) => a - b)) {\n encoding.writeVarUint(encoder, client)\n encoding.writeVarUint(encoder, clock)\n }\n return encoding.toUint8Array(encoder)\n}\n\n/** True when every client clock in `required` is present in `actual` at least as far. */\nexport function stateVectorCovers(actual: Uint8Array, required: Uint8Array): boolean {\n const actualClocks = Y.decodeStateVector(actual)\n for (const [client, clock] of Y.decodeStateVector(required)) {\n if ((actualClocks.get(client) ?? 0) < clock) return false\n }\n return true\n}\n","/**\n * One encrypted Yjs document sync engine. Local state is written to IndexedDB before\n * transmission, and at most one durable append per document is in flight. A reconnect\n * therefore retries the same outbox ID and ciphertext instead of manufacturing a new\n * operation from volatile state.\n */\nimport * as Y from 'yjs'\nimport { Awareness, applyAwarenessUpdate, encodeAwarenessUpdate, removeAwarenessStates } from 'y-protocols/awareness'\nimport { contextAad, envelopeEpochId, openSymmetric, sealSymmetric, toBase64Url, fromBase64Url } from '$lib/crypto'\nimport type { GraphKeyring } from '$lib/crypto'\nimport { currentEpoch, keyForEpoch } from '$lib/crypto'\nimport { SYNC_PROTOCOL_LIMITS } from '@appsoftwareltd/etherpk-shared'\nimport type { RelayClientMessage, RelayServerMessage } from './messages'\nimport { mergeStateVectors } from './state-vector'\n\n/** Transaction origin marking a change as relay-originated and never independently re-sent. */\nexport const REMOTE = Symbol('etherpk-remote')\n/** Local IndexedDB hydration is restored state, not a new edit or remote delta. */\nexport const CACHE_SEED = Symbol('etherpk-cache-seed')\n/** Structural lifecycle changes which must never be mistaken for a user's edit. */\nexport const SUPPRESSED = Symbol('etherpk-suppressed')\n\nexport interface DocCacheState {\n update: Uint8Array\n lastSeq: number\n lastSyncedStateVector: Uint8Array\n generation?: number\n lifecycle?: 'active' | 'deleted'\n /**\n * Persistence-boundary tokens for local edits present in `update` but not yet covered\n * by a durable outbox operation. A set is required because several tabs can save the\n * same document independently before any one of them reaches its append debounce.\n */\n dirtyTokens?: readonly string[]\n}\n\nexport interface DocCacheSaveOptions {\n /** Add one local-edit boundary without clearing boundaries written by another tab. */\n dirtyToken?: string\n /** Replace this session's preceding boundary atomically with `dirtyToken`. */\n supersededDirtyToken?: string\n /**\n * Whether this save observed an effective Yjs update. Local Cache uses this O(1) signal\n * for ordinary documents; root documents still compare their registry projection.\n */\n indexContentChanged?: boolean\n}\n\nexport interface DocCacheEnqueueOptions {\n /** Clear only boundaries represented by this exact encrypted operation. */\n settledDirtyTokens?: readonly string[]\n}\n\ninterface NewOutboxBase {\n outboxId: string\n generation: number\n createdAt: number\n}\n\nexport interface NewAppendOperation extends NewOutboxBase {\n kind: 'append' | 'resurrect'\n epochId: number\n envelope: string\n stateVector: Uint8Array\n}\n\nexport interface NewDeleteOperation extends NewOutboxBase {\n kind: 'delete'\n}\n\nexport type NewOutboxOperation = NewAppendOperation | NewDeleteOperation\n\nexport type DurableOutboxOperation = NewOutboxOperation & {\n graphId: string\n docId: string\n attemptCount: number\n lastAttemptAt: number | null\n}\n\nexport interface DocCache {\n load(): Promise<DocCacheState | null>\n save(state: DocCacheState, options?: DocCacheSaveOptions): Promise<void>\n /** Store the current document and its exact encrypted operation in one transaction. */\n enqueue(\n operation: NewOutboxOperation,\n state: DocCacheState,\n options?: DocCacheEnqueueOptions,\n ): Promise<void>\n pending(): Promise<DurableOutboxOperation[]>\n /**\n * Rewrite the row's update as a garbage-collected state: the same clocks, the same\n * information about what exists, and nothing about what was deleted. `mergeUpdates`\n * never collects, so a document's original body stays in the unencrypted row for the\n * life of the document otherwise - readable from the profile directory with no key of any\n * kind once the document has been protected (ADR 0057's third adversary).\n */\n compact(): Promise<void>\n markAttempt(outboxId: string, attemptedAt: number): Promise<void>\n /** Advance state and remove the matching operation in one transaction. */\n commitAcknowledgement(\n outboxId: string,\n generation: number,\n seq: number,\n update: Uint8Array,\n lastSyncedStateVector: Uint8Array,\n ): Promise<boolean>\n commitDeleteAcknowledgement(outboxId: string, generation: number): Promise<boolean>\n discard(outboxId: string): Promise<void>\n purge(): Promise<void>\n}\n\nexport interface DocSyncDeps {\n docId: string\n graphId: string\n keyring: GraphKeyring\n send(message: RelayClientMessage): void\n persist: DocCache\n debounceMs?: number\n now?: () => number\n generation?: number\n newOutboxId?: () => string\n /** Cache and transport failures are user-visible diagnostics, not swallowed promises. */\n onError?: (error: Error) => void\n /** Lets the graph release an unretained engine once its durable work has settled. */\n onIdle?: () => void\n /**\n * When true of the document, its cache row is rewritten as a garbage-collected state\n * after the next persist, once per stretch of being true. For a [[Protected Document]]:\n * the row otherwise keeps the pre-protection body merged in for ever, on every device that\n * receives the protect, not only the one that did it.\n */\n collectRowWhen?: (doc: Y.Doc) => boolean\n /** Foreground views overtake background hydration at the graph scheduler. */\n catchupPriority?: () => 'foreground' | 'background'\n /**\n * When an idle document uploads a consolidated snapshot (ADR 0025, amended 2026-09-12):\n * once the unsnapshotted tail reaches `minUpdates` rows, or holds `minBytes` more than\n * the document's own encoded state, after `idleMs` without a local edit.\n */\n compaction?: {\n minUpdates?: number\n minBytes?: number\n idleMs?: number\n }\n}\n\nexport interface DocSync {\n readonly doc: Y.Doc\n readonly awareness: Awareness\n ready(): Promise<void>\n receive(message: RelayServerMessage): Promise<void>\n receivePresence(envelope: string): Promise<void>\n /** Resolves once this engine's first relay catch-up page has been applied and persisted. */\n firstCatchupPage(): Promise<void>\n /** Resolves only when the current catch-up cycle reaches a terminal relay page. */\n caughtUp(): Promise<void>\n resync(): void\n compact(): Promise<void>\n flush(): Promise<void>\n delete(): Promise<void>\n lifecycle(): 'seeding' | 'active' | 'deleting' | 'deleted' | 'resurrecting'\n health(): SyncHealth\n staleGeneration(currentGeneration: number): Promise<void>\n /** Relay an awareness tombstone before the graph drops this document subscription. */\n clearPresence(): Promise<void>\n /**\n * Re-send the current local awareness state. A subscribe carries no presence, and a\n * state set while the socket was closed (or before a reconnect) was dropped — this is\n * the client half of the late-join handshake: advertise right after subscribing so the\n * relay can store the envelope and replay it to whoever subscribes next.\n */\n advertisePresence(): Promise<void>\n /** True only when destroying the engine cannot strand a locally queued operation. */\n isIdle(): boolean\n destroy(): void\n}\n\nexport type SyncHealth = 'healthy' | 'key-unavailable' | 'ciphertext-corrupt' | 'generation-stale' | 'sequence-gap'\n\ninterface SyncCompletion {\n readonly promise: Promise<void>\n readonly settled: boolean\n resolve(): void\n reject(error: Error): void\n}\n\nfunction newSyncCompletion(): SyncCompletion {\n let resolvePromise!: () => void\n let rejectPromise!: (error: Error) => void\n let settled = false\n const promise = new Promise<void>((resolve, reject) => {\n resolvePromise = resolve\n rejectPromise = reject\n })\n // A document can be used without both readiness milestones being observed. Attach a\n // rejection handler here so a cache failure is still available to callers without\n // becoming an unrelated global unhandled-rejection event.\n void promise.catch(() => undefined)\n return {\n promise,\n get settled() {\n return settled\n },\n resolve() {\n if (settled) return\n settled = true\n resolvePromise()\n },\n reject(error) {\n if (settled) return\n settled = true\n rejectPromise(error)\n },\n }\n}\n\nfunction asError(error: unknown): Error {\n return error instanceof Error ? error : new Error(String(error))\n}\n\nexport function createDocSync(deps: DocSyncDeps): DocSync {\n const { docId, graphId, keyring, send, persist } = deps\n const debounceMs = deps.debounceMs ?? 300\n const now = deps.now ?? Date.now\n const generation = deps.generation ?? 1\n const newOutboxId = deps.newOutboxId ?? (() => crypto.randomUUID())\n const aad = contextAad('update', `graph:${graphId}`, `doc:${docId}`)\n const presenceAad = contextAad('presence', `graph:${graphId}`, `doc:${docId}`)\n const doc = new Y.Doc()\n const awareness = new Awareness(doc)\n\n // Presentation and reconciliation have deliberately different readiness boundaries.\n // A cold editor can render after one page, but cache/index reconciliation must retain\n // its cross-tab lock until every continuation page in the current cycle has landed.\n const firstCatchupPage = newSyncCompletion()\n let catchupCompletion = newSyncCompletion()\n let catchupActive = false\n let lastSeq = 0\n let documentGeneration = generation\n let lifecycleState: ReturnType<DocSync['lifecycle']> = 'seeding'\n let syncHealth: SyncHealth = 'healthy'\n let lastSyncedStateVector = Y.encodeStateVector(doc)\n /**\n * The document state (state vector AND delete set) the relay is known to hold, advanced\n * at acknowledgement boundaries. Session-local: it seeds from the cached document, whose\n * deletions are either relay-originated or replayed by the durable outbox. Snapshot\n * equality is the drain's \"nothing to send\" test. State vectors alone cannot see\n * deletions, and diff length cannot ignore them.\n */\n let lastSyncedSnapshot: Y.Snapshot | undefined\n /** Boundary snapshots captured when each in-flight operation's diff was encoded. */\n const boundarySnapshots = new Map<string, Y.Snapshot>()\n let durableQueue: DurableOutboxOperation[] = []\n let debounceTimer: ReturnType<typeof setTimeout> | undefined\n let drainPromise: Promise<void> | undefined\n const futureUpdates = new Map<number, { envelope: string; generation: number }>()\n let persistenceTail = Promise.resolve()\n let lastPersistenceError: Error | undefined\n let seeding = false\n /** Incremented only when Yjs says an update changed this live document. */\n let indexMutationVersion = 0\n /** A local edit landed before the cache seed finished, so the live doc is ahead of it. */\n let localEditsBeforeSeed = false\n /** Dirty boundaries loaded from a prior tab/session and not yet moved into the outbox. */\n const recoveredDirtyTokens = new Set<string>()\n const dirtySessionId = crypto.randomUUID()\n let dirtyBoundary = 0\n let activeDirtyToken: string | undefined\n let destroyed = false\n let updatesSinceSnapshot = 0\n let bytesSinceSnapshot = 0\n let compactionTimer: ReturnType<typeof setTimeout> | undefined\n const compactionPolicy = {\n minUpdates: deps.compaction?.minUpdates ?? 32,\n minBytes: deps.compaction?.minBytes ?? 4 * 1024,\n idleMs: deps.compaction?.idleMs ?? 30_000,\n }\n /** The snapshot uploaded and not yet read back, with the exact state it encoded. */\n let pendingSnapshot: { generation: number; throughSeq: number; state: Y.Snapshot } | undefined\n /** A read-back failed: the next idle uploads a fresh snapshot whatever the tail size. */\n let snapshotRetryWanted = false\n\n function requestCatchup(afterSeq: number): void {\n if (!catchupActive) {\n // The first cycle owns the completion created with the engine, so callers may\n // safely start waiting before the socket opens. Every later resync receives a\n // fresh pending completion instead of reusing an already-resolved promise.\n if (catchupCompletion.settled) catchupCompletion = newSyncCompletion()\n catchupActive = true\n }\n send({\n type: 'catchup',\n requestId: newOutboxId(),\n docId,\n generation: documentGeneration,\n afterSeq,\n priority: deps.catchupPriority?.() ?? 'background',\n maxRows: SYNC_PROTOCOL_LIMITS.maxCatchupRows,\n maxBytes: SYNC_PROTOCOL_LIMITS.maxCatchupBytes,\n })\n }\n\n function completeCatchup(): void {\n firstCatchupPage.resolve()\n catchupActive = false\n catchupCompletion.resolve()\n }\n\n function failCatchup(error: unknown): Error {\n const reported = asError(error)\n firstCatchupPage.reject(reported)\n catchupActive = false\n catchupCompletion.reject(reported)\n return reported\n }\n\n const encrypt = (update: Uint8Array) =>\n sealSymmetric({\n key: currentEpoch(keyring).key,\n epochId: currentEpoch(keyring).epochId,\n plaintext: update,\n aad,\n })\n\n function stateSnapshot(): DocCacheState {\n return {\n update: Y.encodeStateAsUpdate(doc),\n lastSeq,\n lastSyncedStateVector,\n generation: documentGeneration,\n lifecycle: lifecycleState === 'deleted' ? 'deleted' : 'active',\n }\n }\n\n /**\n * Serialising cache work prevents an older background save from landing after an\n * acknowledgement transaction and regressing its watermark or state vector.\n */\n function persistTask<T>(work: () => Promise<T>): Promise<T> {\n const result = persistenceTail.then(work)\n persistenceTail = result.then(\n () => {\n lastPersistenceError = undefined\n },\n (error: unknown) => {\n lastPersistenceError = asError(error)\n // A write still in flight when the workspace was torn down rejects here, and\n // used to surface as a \"could not save\" notice over whatever opened next.\n if (!destroyed) deps.onError?.(lastPersistenceError)\n },\n )\n return result\n }\n\n function detached(work: Promise<unknown>): void {\n void work.catch((error: unknown) => {\n const reported = asError(error)\n if (!destroyed && reported !== lastPersistenceError) deps.onError?.(reported)\n })\n }\n\n function saveCurrent(options?: DocCacheSaveOptions): Promise<void> {\n const state = stateSnapshot()\n return persistTask(() => persist.save(state, options)).then(collectRowIfWanted)\n }\n\n /**\n * Collect the cache row after a persist that made the document read as wanted - chained\n * on the persistence tail, so it runs over the row that already holds the persisted state.\n * Once per stretch: a document that stops reading as wanted and later reads so again is\n * collected again.\n */\n let rowCollected = false\n function collectRowIfWanted(): Promise<void> {\n if (!deps.collectRowWhen) return Promise.resolve()\n if (!deps.collectRowWhen(doc)) {\n rowCollected = false\n return Promise.resolve()\n }\n if (rowCollected) return Promise.resolve()\n rowCollected = true\n return persistTask(() => persist.compact())\n }\n\n async function transmit(operation: DurableOutboxOperation): Promise<void> {\n await persistTask(() => persist.markAttempt(operation.outboxId, now()))\n if (operation.kind === 'delete') {\n send({\n type: 'delete',\n docId,\n outboxId: operation.outboxId,\n generation: operation.generation,\n })\n return\n }\n send({\n type: operation.kind,\n docId,\n outboxId: operation.outboxId,\n generation: operation.generation,\n epochId: operation.epochId,\n envelope: operation.envelope,\n })\n }\n\n async function drainCurrentDiff(): Promise<void> {\n await persistenceTail\n if (destroyed || durableQueue.length > 0) return\n // \"Nothing to send\" must be judged by SNAPSHOT equality (state vector + delete\n // set), never by the encoded diff's length: Yjs writes the document's ENTIRE\n // delete set into every state-vector difference, so a document with any deletion\n // history always produces a non-empty diff. With the ack handler draining again\n // after each acknowledgement, that byte-length test self-looped one client into\n // ~40 appends per second per document (live, 2026-07-30: three documents at\n // sequence 660,000+, every open degraded, the index churning in every tab).\n const currentSnapshot = Y.snapshot(doc)\n if (lastSyncedSnapshot && Y.equalSnapshots(currentSnapshot, lastSyncedSnapshot)) return\n const diff = Y.encodeStateAsUpdate(doc, lastSyncedStateVector)\n // Yjs encodes an empty state-vector difference as two bytes.\n if (diff.length <= 2) return\n // Capture the boundary synchronously with the diff. Encryption yields to the event\n // loop; a later keystroke must not be included in this operation's acknowledged\n // vector when it is absent from this operation's plaintext.\n const operationStateVector = Y.encodeStateVector(doc)\n const settledDirtyTokens = [...recoveredDirtyTokens]\n if (activeDirtyToken) settledDirtyTokens.push(activeDirtyToken)\n // Rotate before encryption yields. A local edit made while encryption is running\n // receives a new token and cannot be cleared by this older operation boundary.\n activeDirtyToken = undefined\n\n let operation: NewOutboxOperation\n try {\n const epoch = currentEpoch(keyring)\n const kind = lifecycleState === 'deleted' ? 'resurrect' : 'append'\n // Resurrection starts a fresh server generation, so it must be independently\n // materialisable. A state-vector difference can depend on structs deleted with\n // the old generation and would decode as an empty document on another client.\n const plaintext = kind === 'resurrect' ? Y.encodeStateAsUpdate(doc) : diff\n operation = {\n outboxId: newOutboxId(),\n kind,\n generation: documentGeneration,\n epochId: epoch.epochId,\n envelope: toBase64Url(await encrypt(plaintext)),\n stateVector: operationStateVector,\n createdAt: now(),\n }\n const state = stateSnapshot()\n await persistTask(() =>\n persist.enqueue(operation, state, { settledDirtyTokens }),\n )\n await collectRowIfWanted()\n } catch (error) {\n // The cache still carries these tokens. Retain them in memory too so a retry in\n // this session can settle the same durable boundaries without another edit.\n for (const token of settledDirtyTokens) recoveredDirtyTokens.add(token)\n throw error\n }\n for (const token of settledDirtyTokens) recoveredDirtyTokens.delete(token)\n boundarySnapshots.set(operation.outboxId, currentSnapshot)\n const durable: DurableOutboxOperation = {\n graphId,\n docId,\n ...operation,\n attemptCount: 0,\n lastAttemptAt: null,\n }\n durableQueue.push(durable)\n if (operation.kind === 'resurrect') lifecycleState = 'resurrecting'\n await transmit(durable)\n }\n\n function requestDrain(): Promise<void> {\n if (!drainPromise) {\n drainPromise = drainCurrentDiff().finally(() => {\n drainPromise = undefined\n deps.onIdle?.()\n })\n }\n return drainPromise\n }\n\n function scheduleDrain(): void {\n if (debounceTimer) clearTimeout(debounceTimer)\n debounceTimer = setTimeout(() => {\n debounceTimer = undefined\n detached(requestDrain())\n }, debounceMs)\n }\n\n /**\n * Whether the unsnapshotted tail is worth collapsing (ADR 0025, amended 2026-09-12).\n * Either the tail has reached the row floor - a cold load replays that many envelopes -\n * or the log holds at least `minBytes` more than the document's own encoded state, which\n * is what pruning behind a snapshot would actually free. Judged only when the idle timer\n * fires, because the state size is an O(document) encode. A failed read-back forces the\n * next attempt regardless.\n */\n function compactionDue(): boolean {\n if (snapshotRetryWanted) return true\n if (updatesSinceSnapshot === 0) return false\n if (updatesSinceSnapshot >= compactionPolicy.minUpdates) return true\n return bytesSinceSnapshot - Y.encodeStateAsUpdate(doc).byteLength >= compactionPolicy.minBytes\n }\n\n function scheduleAutomaticCompaction(): void {\n if (destroyed || (updatesSinceSnapshot === 0 && !snapshotRetryWanted)) return\n if (compactionTimer) clearTimeout(compactionTimer)\n compactionTimer = setTimeout(() => {\n compactionTimer = undefined\n if (!compactionDue()) return\n detached(\n uploadSnapshot().then((uploaded) => {\n if (!uploaded) scheduleAutomaticCompaction()\n }),\n )\n }, compactionPolicy.idleMs)\n }\n\n const onUpdate = (_update: Uint8Array, origin: unknown) => {\n if (destroyed || seeding) return\n indexMutationVersion += 1\n if (lifecycleState === 'seeding' && origin !== REMOTE) localEditsBeforeSeed = true\n if (compactionTimer) {\n clearTimeout(compactionTimer)\n compactionTimer = undefined\n }\n // Persist local edits immediately, including edits made while an operation is in\n // flight. Relay handlers persist once after an update or whole catch-up page has\n // applied; saving here as well rewrites the complete merged document for every\n // envelope in that page.\n if (origin !== REMOTE) {\n const supersededDirtyToken = activeDirtyToken\n activeDirtyToken = `${dirtySessionId}:${++dirtyBoundary}`\n detached(\n saveCurrent({\n dirtyToken: activeDirtyToken,\n supersededDirtyToken,\n indexContentChanged: true,\n }),\n )\n }\n if (origin !== REMOTE && origin !== SUPPRESSED) scheduleDrain()\n }\n doc.on('update', onUpdate)\n\n let suppressAutomaticAwareness = false\n\n async function sendAwareness(changed: number[]): Promise<void> {\n const update = encodeAwarenessUpdate(awareness, changed)\n const envelope = toBase64Url(\n await sealSymmetric({\n key: currentEpoch(keyring).key,\n epochId: currentEpoch(keyring).epochId,\n plaintext: update,\n aad: presenceAad,\n }),\n )\n send({ type: 'presence', docId, envelope })\n }\n\n const onAwareness = (\n { added, updated, removed }: { added: number[]; updated: number[]; removed: number[] },\n origin: unknown,\n ) => {\n if (destroyed || origin === REMOTE || suppressAutomaticAwareness) return\n const changed = [...added, ...updated, ...removed]\n detached(sendAwareness(changed))\n }\n awareness.on('update', onAwareness)\n\n let skippedUpdates = 0\n\n async function applyEnvelope(envelope: string, seq: number, incomingGeneration = documentGeneration): Promise<boolean> {\n if (incomingGeneration !== documentGeneration) {\n if (\n lifecycleState === 'deleted' &&\n incomingGeneration === documentGeneration + 1 &&\n seq === 1\n ) {\n // A peer's explicit resurrection is broadcast as the complete first update\n // of the new generation.\n documentGeneration = incomingGeneration\n lifecycleState = 'active'\n lastSeq = 0\n } else {\n syncHealth = 'generation-stale'\n return false\n }\n }\n if (seq <= lastSeq) return true\n if (seq !== lastSeq + 1) {\n syncHealth = 'sequence-gap'\n if (futureUpdates.size < 256) {\n futureUpdates.set(seq, { envelope, generation: incomingGeneration })\n }\n requestCatchup(lastSeq)\n return false\n }\n try {\n const bytes = fromBase64Url(envelope)\n const epochId = envelopeEpochId(bytes)\n if (!keyForEpoch(keyring, epochId)) {\n syncHealth = 'key-unavailable'\n return false\n }\n const { plaintext } = await openSymmetric({\n keyForEpoch: (id) => keyForEpoch(keyring, id),\n envelope: bytes,\n aad,\n })\n Y.applyUpdate(doc, plaintext, REMOTE)\n // A relay update is server-confirmed state. Retain it in the baseline even when\n // a local operation is in flight, otherwise its later acknowledgement replaces\n // this knowledge and requestDrain() relays the peer's update back as a new append.\n lastSyncedStateVector = mergeStateVectors(\n lastSyncedStateVector,\n Y.encodeStateVectorFromUpdate(plaintext),\n )\n // With no local work in any stage (queued, draining, or debounced), the doc is\n // exactly baseline-plus-relay-updates: all relay-known, delete sets included.\n if (durableQueue.length === 0 && !debounceTimer && !drainPromise) {\n lastSyncedSnapshot = Y.snapshot(doc)\n }\n } catch (error) {\n syncHealth = 'ciphertext-corrupt'\n skippedUpdates += 1\n if (skippedUpdates <= 3 || skippedUpdates % 50 === 0) {\n console.warn(`[sync] update seq ${seq} for doc ${docId} skipped (${skippedUpdates} so far):`, error)\n }\n return false\n }\n if (seq > lastSeq) lastSeq = seq\n updatesSinceSnapshot += 1\n bytesSinceSnapshot += fromBase64Url(envelope).byteLength\n scheduleAutomaticCompaction()\n syncHealth = 'healthy'\n const next = futureUpdates.get(lastSeq + 1)\n if (next) {\n futureUpdates.delete(lastSeq + 1)\n await applyEnvelope(next.envelope, lastSeq + 1, next.generation)\n }\n return true\n }\n\n async function uploadSnapshot(): Promise<boolean> {\n if (\n lastSeq === 0 ||\n durableQueue.length > 0 ||\n syncHealth !== 'healthy' ||\n lifecycleState !== 'active'\n ) {\n return false\n }\n // Capture the encoded state and its Y.Snapshot together, before encryption yields:\n // the read-back is compared against exactly what was encoded, not against whatever\n // the document holds by the time the relay answers.\n const consolidated = Y.encodeStateAsUpdate(doc)\n const captured = Y.snapshot(doc)\n const throughSeq = lastSeq\n const snapshotGeneration = documentGeneration\n const epochId = currentEpoch(keyring).epochId\n const envelope = toBase64Url(await encrypt(consolidated))\n if (destroyed) return false\n pendingSnapshot = { generation: snapshotGeneration, throughSeq, state: captured }\n snapshotRetryWanted = false\n send({\n type: 'snapshot_put',\n docId,\n generation: snapshotGeneration,\n throughSeq,\n epochId,\n envelope,\n })\n updatesSinceSnapshot = 0\n bytesSinceSnapshot = 0\n return true\n }\n\n /**\n * The relay stored the snapshot. Read it straight back: the blind server cannot check a\n * snapshot, so the client that produced it proves the stored bytes decode to the state\n * it captured, and only then may the server prune behind it (ADR 0025, amended\n * 2026-09-12). Anything naming a snapshot other than the one in flight is ignored.\n */\n function onSnapshotAck(message: { generation: number; throughSeq: number }): void {\n const pending = pendingSnapshot\n if (!pending || pending.generation !== message.generation || pending.throughSeq !== message.throughSeq) return\n send({ type: 'snapshot_get', docId, generation: pending.generation, throughSeq: pending.throughSeq })\n }\n\n async function onSnapshotData(message: { generation: number; throughSeq: number; envelope: string }): Promise<void> {\n const pending = pendingSnapshot\n if (!pending || pending.generation !== message.generation || pending.throughSeq !== message.throughSeq) return\n pendingSnapshot = undefined\n let verified = false\n const scratch = new Y.Doc()\n try {\n const { plaintext } = await openSymmetric({\n keyForEpoch: (id) => keyForEpoch(keyring, id),\n envelope: fromBase64Url(message.envelope),\n aad,\n })\n Y.applyUpdate(scratch, plaintext)\n // State vector AND delete set, the same equality the drain uses: a snapshot that\n // dropped a deletion would still match on state vector alone.\n verified = Y.equalSnapshots(Y.snapshot(scratch), pending.state)\n } catch {\n verified = false\n } finally {\n scratch.destroy()\n }\n if (destroyed) return\n if (!verified) {\n // The relay keeps the log, so nothing is lost; a fresh snapshot goes up at the\n // next idle and gets its own read-back.\n console.warn(`[sync] snapshot ${pending.throughSeq} for doc ${docId} did not read back as stored; left unverified`)\n snapshotRetryWanted = true\n scheduleAutomaticCompaction()\n return\n }\n send({ type: 'snapshot_verified', docId, generation: pending.generation, throughSeq: pending.throughSeq })\n }\n\n return {\n doc,\n awareness,\n async receivePresence(envelope: string) {\n try {\n const { plaintext } = await openSymmetric({\n keyForEpoch: (id) => keyForEpoch(keyring, id),\n envelope: fromBase64Url(envelope),\n aad: presenceAad,\n })\n applyAwarenessUpdate(awareness, plaintext, REMOTE)\n } catch {\n // Presence is ephemeral. An unknown epoch is safely ignored.\n }\n },\n async compact() {\n await uploadSnapshot()\n },\n async ready() {\n const [cached, pending] = await Promise.all([persist.load(), persist.pending()])\n seeding = true\n try {\n if (cached) {\n Y.applyUpdate(doc, cached.update, CACHE_SEED)\n lastSeq = cached.lastSeq\n lastSyncedStateVector = cached.lastSyncedStateVector\n documentGeneration = cached.generation ?? generation\n lifecycleState = cached.lifecycle ?? 'active'\n for (const token of cached.dirtyTokens ?? []) recoveredDirtyTokens.add(token)\n }\n durableQueue = pending.sort(\n (a, b) => a.createdAt - b.createdAt || a.outboxId.localeCompare(b.outboxId),\n )\n } finally {\n seeding = false\n }\n // A cached document can be ahead of a replayed outbox operation. For example,\n // an edit made while the first operation is in flight is saved immediately but\n // cannot enter the outbox until that operation is acknowledged. Treating the\n // whole cached document as relay-known after restart would silently strand that\n // later edit. Without a session-local operation boundary, leave the snapshot\n // unknown: the acknowledged state vector will still suppress known structs, and\n // at worst a deletion set is sent once more before the new boundary is captured.\n const hasPendingContent = durableQueue.some((operation) => operation.kind !== 'delete')\n if (hasPendingContent || recoveredDirtyTokens.size > 0) {\n lastSyncedSnapshot = undefined\n } else if (localEditsBeforeSeed) {\n // A local edit can also land while the cache read is in flight. Rebuild the\n // baseline from cached bytes so that live-only work remains drainable.\n const baseline = new Y.Doc()\n if (cached) Y.applyUpdate(baseline, cached.update)\n lastSyncedSnapshot = Y.snapshot(baseline)\n baseline.destroy()\n } else {\n lastSyncedSnapshot = Y.snapshot(doc)\n }\n if (lifecycleState === 'seeding') lifecycleState = 'active'\n },\n async receive(message) {\n switch (message.type) {\n case 'ack': {\n const operation = durableQueue[0]\n if (!operation || operation.outboxId !== message.outboxId) return\n let committed: boolean\n if (operation.kind === 'delete') {\n committed = await persistTask(() =>\n persist.commitDeleteAcknowledgement(message.outboxId, message.generation),\n )\n } else {\n // An ack proves that this local operation occupies `message.seq`; it\n // does not prove that this tab has applied every earlier relay update.\n // Only move the catch-up watermark when the ack is the next contiguous\n // sequence. Otherwise catch up from the last sequence actually known.\n const acknowledgedSeq =\n message.seq === lastSeq + 1 ? message.seq : lastSeq\n const acknowledgedStateVector = mergeStateVectors(\n lastSyncedStateVector,\n operation.stateVector,\n )\n committed = await persistTask(() =>\n persist.commitAcknowledgement(\n message.outboxId,\n message.generation,\n acknowledgedSeq,\n Y.encodeStateAsUpdate(doc),\n acknowledgedStateVector,\n ),\n )\n }\n if (!committed) return\n documentGeneration = message.generation\n // The relay now holds everything this operation's diff represented,\n // including its delete set. A replayed outbox operation has no captured\n // boundary; leaving the last snapshot unchanged is conservative and\n // costs at most one redundant, idempotent append.\n const boundary = boundarySnapshots.get(message.outboxId)\n boundarySnapshots.delete(message.outboxId)\n if (boundary && operation.kind !== 'delete') lastSyncedSnapshot = boundary\n if (operation.kind === 'delete') {\n lastSeq = 0\n lastSyncedStateVector = Y.encodeStateVector(new Y.Doc())\n lifecycleState = 'deleted'\n } else {\n if (message.seq === lastSeq + 1) {\n lastSeq = message.seq\n } else if (message.seq > lastSeq + 1) {\n syncHealth = 'sequence-gap'\n requestCatchup(lastSeq)\n }\n lastSyncedStateVector = mergeStateVectors(\n lastSyncedStateVector,\n operation.stateVector,\n )\n lifecycleState = 'active'\n updatesSinceSnapshot += 1\n bytesSinceSnapshot += fromBase64Url(operation.envelope).byteLength\n scheduleAutomaticCompaction()\n }\n durableQueue.shift()\n send({ type: 'ack_confirm', outboxId: message.outboxId })\n if (durableQueue[0]) await transmit(durableQueue[0])\n else if (operation.kind !== 'delete') await requestDrain()\n break\n }\n case 'update': {\n const beforeUpdate = indexMutationVersion\n if (\n await applyEnvelope(\n message.envelope,\n message.seq,\n message.generation,\n )\n ) {\n await saveCurrent({\n indexContentChanged: indexMutationVersion !== beforeUpdate,\n })\n }\n break\n }\n case 'catchup_batch': {\n try {\n const beforeBatch = indexMutationVersion\n if (message.generation < documentGeneration) break\n if (message.state === 'deleted') {\n documentGeneration = message.generation\n lifecycleState = 'deleted'\n lastSeq = 0\n seeding = true\n try {\n const content = doc.getText('content')\n if (content.length > 0) {\n doc.transact(() => content.delete(0, content.length), SUPPRESSED)\n }\n } finally {\n seeding = false\n }\n await persist.purge()\n durableQueue = []\n boundarySnapshots.clear()\n recoveredDirtyTokens.clear()\n activeDirtyToken = undefined\n // The relay declared this generation deleted; the emptied document\n // is the shared baseline. Without this, the delete-set left by the\n // content clear reads as unsent local work and a reconnect drain\n // would resurrect the document.\n lastSyncedSnapshot = Y.snapshot(doc)\n futureUpdates.clear()\n completeCatchup()\n break\n }\n documentGeneration = message.generation\n lifecycleState = 'active'\n if (message.snapshot) {\n // A snapshot establishes its own contiguous watermark, and it\n // covers everything before it: an update held for a gap the\n // snapshot has closed is stale, and the tail counters start again.\n lastSeq = message.snapshot.throughSeq - 1\n for (const seq of [...futureUpdates.keys()]) {\n if (seq <= message.snapshot.throughSeq) futureUpdates.delete(seq)\n }\n await applyEnvelope(message.snapshot.envelope, message.snapshot.throughSeq)\n updatesSinceSnapshot = 0\n bytesSinceSnapshot = 0\n }\n for (const update of message.updates) {\n await applyEnvelope(update.envelope, update.seq)\n }\n await saveCurrent({\n indexContentChanged: indexMutationVersion !== beforeBatch,\n })\n if (message.hasMore) {\n firstCatchupPage.resolve()\n requestCatchup(lastSeq)\n } else {\n completeCatchup()\n }\n } catch (error) {\n throw failCatchup(error)\n }\n break\n }\n case 'snapshot_ack':\n onSnapshotAck(message)\n break\n case 'snapshot_data':\n await onSnapshotData(message)\n break\n default:\n break\n }\n },\n firstCatchupPage: () => firstCatchupPage.promise,\n caughtUp: () => catchupCompletion.promise,\n resync() {\n requestCatchup(lastSeq)\n if (durableQueue[0]) detached(transmit(durableQueue[0]))\n else detached(requestDrain())\n },\n async flush() {\n if (debounceTimer) {\n clearTimeout(debounceTimer)\n debounceTimer = undefined\n }\n await persistenceTail\n await requestDrain()\n if (lastPersistenceError) throw lastPersistenceError\n },\n async delete() {\n if (lifecycleState === 'deleted' || lifecycleState === 'deleting') return\n await persistenceTail\n lifecycleState = 'deleting'\n if (debounceTimer) clearTimeout(debounceTimer)\n const content = doc.getText('content')\n seeding = true\n try {\n if (content.length > 0) doc.transact(() => content.delete(0, content.length), SUPPRESSED)\n } finally {\n seeding = false\n }\n for (const operation of durableQueue.splice(0)) await persist.discard(operation.outboxId)\n boundarySnapshots.clear()\n // The cleared document is the baseline while the delete settles; only a real\n // edit (resurrection) should ever drain from here.\n lastSyncedSnapshot = Y.snapshot(doc)\n const operation: NewDeleteOperation = {\n outboxId: newOutboxId(),\n kind: 'delete',\n generation: documentGeneration,\n createdAt: now(),\n }\n const settledDirtyTokens = [...recoveredDirtyTokens]\n if (activeDirtyToken) settledDirtyTokens.push(activeDirtyToken)\n activeDirtyToken = undefined\n try {\n await persistTask(() =>\n persist.enqueue(operation, stateSnapshot(), { settledDirtyTokens }),\n )\n } catch (error) {\n for (const token of settledDirtyTokens) recoveredDirtyTokens.add(token)\n throw error\n }\n for (const token of settledDirtyTokens) recoveredDirtyTokens.delete(token)\n durableQueue.push({\n graphId,\n docId,\n ...operation,\n attemptCount: 0,\n lastAttemptAt: null,\n })\n await transmit(durableQueue[0])\n },\n lifecycle: () => lifecycleState,\n health: () => syncHealth,\n async staleGeneration(currentGeneration) {\n syncHealth = 'generation-stale'\n for (const operation of durableQueue.splice(0)) await persist.discard(operation.outboxId)\n boundarySnapshots.clear()\n documentGeneration = currentGeneration\n lastSeq = 0\n requestCatchup(0)\n },\n async advertisePresence() {\n if (destroyed || awareness.getLocalState() === null) return\n await sendAwareness([doc.clientID])\n },\n async clearPresence() {\n if (destroyed || awareness.getLocalState() === null) return\n suppressAutomaticAwareness = true\n try {\n awareness.setLocalState(null)\n } finally {\n suppressAutomaticAwareness = false\n }\n await sendAwareness([doc.clientID])\n },\n isIdle: () => durableQueue.length === 0 && !debounceTimer && !drainPromise,\n destroy() {\n destroyed = true\n boundarySnapshots.clear()\n // A background persisted-index catch-up may be waiting while the browser is\n // offline. Ending the engine also ends that wait, so workspace disposal cannot\n // retain a detached graph session indefinitely.\n firstCatchupPage.resolve()\n completeCatchup()\n if (debounceTimer) clearTimeout(debounceTimer)\n if (compactionTimer) clearTimeout(compactionTimer)\n doc.off('update', onUpdate)\n awareness.off('update', onAwareness)\n removeAwarenessStates(awareness, [doc.clientID], REMOTE)\n awareness.destroy()\n doc.destroy()\n },\n }\n}\n","/**\n * Application-facing sync messages derived from the shared wire contract. The `v` field is\n * added and checked only at the serialisation boundary so the document engine does not repeat\n * a transport constant on every operation.\n */\nimport {\n SYNC_PROTOCOL_VERSION,\n parseServerMessage as parseWireServerMessage,\n serializeClientMessage as serializeWireClientMessage,\n type SyncClientMessage,\n type SyncServerMessage,\n} from '@appsoftwareltd/etherpk-shared'\n\ntype WithoutVersion<T> = T extends { v: typeof SYNC_PROTOCOL_VERSION } ? Omit<T, 'v'> : never\n\nexport type RelayClientMessage = WithoutVersion<SyncClientMessage>\nexport type RelayServerMessage = WithoutVersion<SyncServerMessage>\nexport type RelayUpdate = Extract<RelayServerMessage, { type: 'catchup_batch' }>['updates'][number]\n\n/** Returns null for anything malformed, oversized or from an unsupported protocol version. */\nexport function parseServerMessage(raw: string): RelayServerMessage | null {\n const result = parseWireServerMessage(raw)\n if (!result.ok) return null\n const { v: _version, ...message } = result.value\n return message as RelayServerMessage\n}\n\nexport function serializeClientMessage(message: RelayClientMessage): string {\n return serializeWireClientMessage({ v: SYNC_PROTOCOL_VERSION, ...message } as SyncClientMessage)\n}\n","/**\n * Who a caret belongs to. y-codemirror renders every remote cursor from the awareness\n * `user` field — nothing in the app ever SET that field, so every collaborator was an\n * identical default-blue \"Anonymous\" bar (live, 2026-07-28: read as \"presence is broken\").\n *\n * The sync layer has no account identity to draw on (the client authenticates with a PAT;\n * the portal session lives on another origin), so the identity is per-device: a friendly\n * pseudonym and a stable colour, minted once and kept in localStorage. Replacing this with\n * the real account name when an identity endpoint exists only means changing `mint()`.\n *\n * The colour here is only this device's PREFERENCE. Tabs share localStorage, and eight\n * palette entries collide across devices, so the per-session allocator in\n * presence-session.ts owns the colour actually advertised: it keeps the preference when it\n * is free and deterministically moves to a free palette entry when an earlier live session\n * in the same graph already shows it.\n */\n\nconst KEY = 'etherpk.presence-identity'\n\nexport interface PresenceIdentity {\n name: string\n color: string\n /** Selection-highlight tint; y-codemirror falls back to color + '33' without one. */\n colorLight: string\n}\n\n/**\n * Pastels: soft enough not to shout beside the prose, saturated enough that a 2px caret bar\n * still reads on a white surface, and light enough that dark label text works on all of them\n * in both themes. The selection tint is the same colour at 40% alpha (`66`) — the old 20%\n * tint of a primary was already faint; a pastel at 20% disappears on white.\n *\n * The same eight are offered as the ready-made toolbar colours in Graph Settings (ADR 0071):\n * one palette, so a graph's bar and a member's caret speak the same language. The label is\n * for a swatch there; presence itself never shows it (`label`, not `name`: an entry is\n * spread into a {@link PresenceIdentity}, whose `name` is the pseudonym).\n */\nexport const PRESENCE_PALETTE: ReadonlyArray<{ label: string; color: string; colorLight: string }> = [\n { label: 'Sky', color: '#7dd3fc', colorLight: '#7dd3fc66' },\n { label: 'Amber', color: '#fcd34d', colorLight: '#fcd34d66' },\n { label: 'Mint', color: '#6ee7b7', colorLight: '#6ee7b766' },\n { label: 'Salmon', color: '#fca5a5', colorLight: '#fca5a566' },\n { label: 'Lavender', color: '#c4b5fd', colorLight: '#c4b5fd66' },\n { label: 'Pink', color: '#f9a8d4', colorLight: '#f9a8d466' },\n { label: 'Peach', color: '#fdba74', colorLight: '#fdba7466' },\n { label: 'Lime', color: '#bef264', colorLight: '#bef26466' },\n]\n\nconst ADJECTIVES = ['Amber', 'Bold', 'Calm', 'Deft', 'Eager', 'Fleet', 'Keen', 'Quiet', 'Swift', 'Vivid']\nconst ANIMALS = ['Fox', 'Owl', 'Wren', 'Hare', 'Lynx', 'Newt', 'Ibis', 'Mole', 'Swan', 'Toad']\n\nfunction mint(): PresenceIdentity {\n const pick = (list: readonly string[]) => list[Math.floor(Math.random() * list.length)]\n const paletteEntry = PRESENCE_PALETTE[Math.floor(Math.random() * PRESENCE_PALETTE.length)]\n return { name: `${pick(ADJECTIVES)} ${pick(ANIMALS)}`, ...paletteEntry }\n}\n\n/** This device's presence identity, minted on first use and stable thereafter. */\nexport function presenceIdentity(): PresenceIdentity {\n try {\n const stored = localStorage.getItem(KEY)\n if (stored) {\n const parsed = JSON.parse(stored) as Partial<PresenceIdentity>\n if (typeof parsed.name === 'string' && typeof parsed.color === 'string' && typeof parsed.colorLight === 'string') {\n // Only a palette colour is advertised: the allocator in presence-session.ts can\n // only de-duplicate against palette entries, so a colour from an earlier palette\n // (or a hand-edited one) is re-minted while the name stays the device's.\n if (PRESENCE_PALETTE.some((entry) => entry.color === parsed.color)) return parsed as PresenceIdentity\n return remember({ ...mint(), name: parsed.name })\n }\n }\n } catch {\n // Unreadable storage or corrupt JSON — mint a fresh one below.\n }\n return remember(mint())\n}\n\nfunction remember(identity: PresenceIdentity): PresenceIdentity {\n try {\n localStorage.setItem(KEY, JSON.stringify(identity))\n } catch {\n // Private mode: a per-session identity is still better than \"Anonymous\".\n }\n return identity\n}\n","/**\n * One live client session's presence identity, applied to every retained document's\n * awareness (live, 2026-07-30: four collaborators all rendered blue, and a client could\n * vanish from its peers entirely).\n *\n * Two separate faults are owned here:\n *\n * - Colour uniqueness. The stored device identity is only a preference — same-profile tabs\n * share it, and independent eight-entry random picks collide across devices. Each session\n * advertises `since` (arrival time) and `seat` (a random total-order tiebreak) alongside\n * its colour. When a session sees its own colour on a peer that arrived earlier, the later\n * session deterministically moves to the first palette colour no visible collaborator is\n * using. Earlier sessions never move, so colours are stable within a session, and the rule\n * converges because movement always goes to a free colour under a total order. With more\n * visible collaborators than palette entries, uniqueness is impossible; the session then\n * keeps its colour rather than flap.\n *\n * - Advertisement liveness. y-protocols' `setLocalStateField` is a NO-OP once the local\n * state has been cleared (`setLocalState(null)` — exactly what releasing a document does),\n * and the awareness renewal timer also skips null states. Re-applying identity through\n * `setLocalState` resurrects a cleared state, so a re-retained document advertises again\n * instead of leaving this client permanently invisible.\n */\nimport type { Awareness } from 'y-protocols/awareness'\nimport { PRESENCE_PALETTE, type PresenceIdentity } from './presence-identity'\n\n/** The awareness `user` field this session advertises. y-codemirror reads name/colour. */\nexport interface PresenceUser extends PresenceIdentity {\n /** Session arrival time; on a colour collision the LATER session moves. */\n since: number\n /** Random total-order tiebreak for sessions arriving in the same millisecond. */\n seat: string\n}\n\n/** What a peer's advertised `user` field may carry (absent fields ⇒ an older client). */\ninterface PeerUser {\n color?: unknown\n since?: unknown\n seat?: unknown\n}\n\nexport interface PresenceSession {\n /** The identity currently advertised (colour may differ from the preference). */\n user(): PresenceUser\n /**\n * Apply this session's identity to a document's awareness and keep its colour\n * collision-free against that document's visible collaborators. Returns a detach\n * which stops observing; it does NOT clear the awareness state (the doc engine's\n * clearPresence owns the tombstone).\n */\n attach(awareness: Awareness): () => void\n}\n\nexport function createPresenceSession(\n identity: PresenceIdentity,\n options: { now?: () => number; seat?: string } = {},\n): PresenceSession {\n const since = (options.now ?? Date.now)()\n const seat = options.seat ?? crypto.randomUUID()\n let color = identity.color\n let colorLight = identity.colorLight ?? `${identity.color}33`\n const attached = new Set<Awareness>()\n let evaluateQueued = false\n\n const user = (): PresenceUser => ({ name: identity.name, color, colorLight, since, seat })\n\n function apply(awareness: Awareness): void {\n // Never setLocalStateField: it silently no-ops on a cleared (null) state, which is\n // precisely the state a released-then-re-retained document is in.\n const state = awareness.getLocalState()\n awareness.setLocalState({ ...(state ?? {}), user: user() })\n }\n\n /** A peer arrived earlier when its (since, seat) sorts below ours; it keeps the colour. */\n function outranks(peer: PeerUser): boolean {\n const peerSince = typeof peer.since === 'number' ? peer.since : Number.NEGATIVE_INFINITY\n if (peerSince !== since) return peerSince < since\n const peerSeat = typeof peer.seat === 'string' ? peer.seat : ''\n return peerSeat < seat\n }\n\n function visiblePeers(): PeerUser[] {\n const peers: PeerUser[] = []\n for (const awareness of attached) {\n for (const [clientId, state] of awareness.getStates()) {\n if (clientId === awareness.clientID) continue\n const peer = (state as { user?: PeerUser }).user\n if (peer && typeof peer.color === 'string') peers.push(peer)\n }\n }\n return peers\n }\n\n function evaluate(): void {\n const peers = visiblePeers()\n const collision = peers.some((peer) => peer.color === color && outranks(peer))\n if (!collision) return\n const inUse = new Set(peers.map((peer) => peer.color))\n const free = PRESENCE_PALETTE.find((entry) => !inUse.has(entry.color))\n // Every palette entry visible ⇒ uniqueness is impossible; keep the colour steady.\n if (!free || free.color === color) return\n color = free.color\n colorLight = free.colorLight\n for (const awareness of attached) {\n if (awareness.getLocalState() !== null) apply(awareness)\n }\n }\n\n function scheduleEvaluate(): void {\n if (evaluateQueued) return\n evaluateQueued = true\n queueMicrotask(() => {\n evaluateQueued = false\n evaluate()\n })\n }\n\n return {\n user,\n attach(awareness) {\n attached.add(awareness)\n apply(awareness)\n const onChange = () => scheduleEvaluate()\n awareness.on('change', onChange)\n scheduleEvaluate()\n return () => {\n awareness.off('change', onChange)\n attached.delete(awareness)\n }\n },\n }\n}\n","/**\n * Graph-level sync (plan Phase 3 Task 5): one WebSocket per open graph, multiplexing all\n * documents. Owns per-doc engines (created lazily), the reconnect loop, and the graph-root\n * document — a hidden Y.Doc whose `registry` Y.Map (docId → {kind,title?,date?}) and `meta`\n * Y.Map (graph name, settings) are how a blind server's clients discover the doc set.\n *\n * The transport is injected (a `connect(url)` factory) so this unit-tests without real sockets.\n */\nimport * as Y from 'yjs'\nimport type { GraphKeyring } from '$lib/crypto'\nimport { performanceRecorder } from '$lib/diagnostics/performance'\nimport { SYNC_PROTOCOL_LIMITS } from '@appsoftwareltd/etherpk-shared'\nimport { CACHE_SEED, createDocSync, type DocSync } from './doc-sync'\nimport type { GraphCache } from './local-cache'\nimport { parseServerMessage, serializeClientMessage, type RelayClientMessage } from './messages'\nimport { createPresenceSession, type PresenceSession } from './presence-session'\nimport type { SyncTokenSource } from './sync-token'\n\nexport interface TransportSocket {\n send(data: string): void\n close(): void\n onOpen(cb: () => void): void\n onMessage(cb: (data: string) => void): void\n onClose(cb: () => void): void\n}\n\nexport interface GraphSyncDeps {\n graphId: string\n rootDocId: string\n keyring: GraphKeyring\n relayUrl: string\n /**\n * Asked for a token on every connect, NOT captured once: a session outlives its\n * token's 15 minutes, and a reconnect carrying an expired one is rejected forever.\n */\n token: SyncTokenSource\n cache: GraphCache\n connect(url: string): TransportSocket\n debounceMs?: number\n /** Called whenever the registry Y.Map changes (membership of the doc set). */\n onRegistryChange?: () => void\n /** Passed to every document engine - see `DocSyncDeps.collectRowWhen`. */\n collectRowWhen?: (doc: Y.Doc) => boolean\n /**\n * This device's PREFERRED identity for the awareness `user` field — what y-codemirror\n * renders a remote caret's colour and name from. Absent ⇒ collaborators all show as\n * the library's default-blue \"Anonymous\". The per-session allocator owns the colour\n * actually advertised (presence-session.ts): the preference is kept when free and\n * deterministically moved when an earlier live session already shows it.\n */\n presence?: { name: string; color: string; colorLight?: string }\n /** Surfaces cache quota and transport failures to the owning workspace. */\n onError?: (error: Error) => void\n}\n\nexport interface RegistryEntry {\n kind: 'journal' | 'page'\n title?: string\n date?: string\n /**\n * Alternative names resolving to this document ([[Alias]]). A server document has no\n * frontmatter - identity lives only here (ADR 0024) - so this is where a rename's\n * \"keep the old name working\" arm puts the old name (ADR 0037).\n */\n aliases?: string[]\n}\n\n/**\n * The root doc's `meta` Y.Map: the [[Graph Name]] (identity metadata — one canonical,\n * any-member-writable name) and the shared [[Graph Settings]] object. Both are content:\n * encrypted like everything else, so the blind server never sees them.\n */\nexport interface GraphMeta {\n name?: string\n /** GraphSettings-shaped; consumers sanitize (storage/fs/graph-settings). */\n settings?: Record<string, unknown>\n}\n\nexport interface GraphSync {\n readonly rootDoc: Y.Doc\n docSync(docId: string): DocSync\n retainDoc(docId: string): () => void\n /** Queue an ordered, durable lifecycle delete before the encrypted registry changes. */\n deleteDoc(docId: string): Promise<void>\n /** Drop deleted content from a document's cache row - after protecting it (ADR 0057). */\n compactDoc(docId: string): Promise<void>\n /**\n * Upload a consolidated encrypted snapshot of a document to the relay now, rather than\n * waiting for the idle trigger; the engine reads it back and verifies it (ADR 0025).\n */\n uploadSnapshot(docId: string): Promise<void>\n registry(): Y.Map<RegistryEntry>\n /** Snapshot of the root doc's `meta` map (graph name + shared settings). */\n getMeta(): GraphMeta\n setMetaName(name: string): void\n setMetaSettings(settings: Record<string, unknown>): void\n /** Fires on any `meta` change (local or remote). Returns an unsubscribe. */\n onMetaChange(listener: () => void): () => void\n /** Resolves once the socket has opened (first connect). Short-lived flows need this\n * before flushing — a flush into a never-opened socket dies with dispose. */\n connected(): Promise<void>\n /** Resolves once the ROOT doc's current catch-up reaches its terminal relay page. */\n rootCaughtUp(): Promise<void>\n /** Fires whenever ANY document's content (Y.Doc) changes — local or remote. The docId is\n * the graph-root id for registry changes, else the content doc id. */\n onDocUpdate(listener: (docId: string) => void): () => void\n ready(): Promise<void>\n /**\n * Resolves when ONE doc's engine has seeded from the [[Local Cache]] (creating the\n * engine if needed). This is what lets an opened document show \"loading\" instead of\n * sitting silently empty while the background materialisation walk is still running.\n */\n whenReady(docId: string): Promise<void>\n /**\n * Hydrate a cold-index batch from the Local Cache without starting relay catch-up.\n * The caller must later promote stale documents through normal synchronized readiness.\n */\n seedDocsFromCache(docIds: readonly string[]): Promise<readonly Y.Doc[]>\n /** Seed only this batch, without re-awaiting every engine created earlier. */\n readyDocs(docIds: readonly string[]): Promise<void>\n /** Release background-only engines once their batch consumer has read them. */\n retireDocs(docIds: readonly string[]): void\n /** Resolves after ONE doc's first relay page is applied, for presentation readiness. */\n firstCatchupPageDoc(docId: string): Promise<void>\n /** Resolves at the terminal page of ONE doc's current catch-up cycle. */\n caughtUpDoc(docId: string): Promise<void>\n /**\n * Compare the Local Cache with one bounded relay metadata request per batch. Warm indexes\n * only materialise documents whose server watermark actually moved while this browser\n * was away.\n */\n docsNeedingCatchup(docIds: readonly string[]): Promise<string[]>\n /**\n * Cache rows which changed after the last explicit index commit. The acknowledgement\n * closure clears only the exact tokens captured by this call.\n */\n pendingIndexChanges(): Promise<GraphIndexCheckpoint>\n /** Whether the relay socket is open right now — gate anything that waits on the server. */\n isConnected(): boolean\n /**\n * Encrypt and send every pending update. On a large [[Import]] this is where the minutes\n * go, so it reports as each document's engine finishes - without it the Activity sat at\n * \"0 of 2401\" until the first acknowledgement arrived, looking hung.\n */\n flushAll(options?: { onProgress?: (flushed: number, total: number) => void }): Promise<void>\n /**\n * Wait until the server has acknowledged every operation in the durable outbox.\n *\n * `flushAll()` only gets bytes as far as `socket.send`; it never waited for the relay.\n * Anything that must not claim \"the server has it\" - an [[Import]] declaring itself\n * done, before the socket is disposed - has to wait here instead.\n *\n * Resolves `settled: false` when no ack has arrived for `stallMs`: the caller decides\n * what a stall means. It is NOT an error - unacked updates are already in the\n * durable outbox and replay on the next open, so a stall is a weaker claim, not a loss.\n */\n awaitAcked(options?: AwaitAckedOptions): Promise<AckResult>\n diagnostics(): { activeEngines: number; retainedDocuments: number }\n dispose(): void\n}\n\nexport interface GraphIndexCheckpoint {\n docIds: readonly string[]\n /** Root bytes merged from durable cache and this tab's live CRDT, for identity mapping. */\n rootUpdate: Uint8Array | null\n /**\n * Read content at least as new as the captured cache token. A dirty missing row is a\n * deletion tombstone and deliberately does not fall back to a stale live engine.\n */\n readDocument(docId: string): Promise<Uint8Array | null>\n acknowledge(): Promise<void>\n}\n\nexport interface AwaitAckedOptions {\n /** Called with the number of appends still outstanding, whenever that number falls. */\n onProgress?: (outstanding: number) => void\n signal?: AbortSignal\n /** Give up after this long with no ack at all. Default 30s. */\n stallMs?: number\n}\n\nexport interface AckResult {\n /** True when everything was acked; false when the wait gave up on a stall. */\n settled: boolean\n /** Appends still unacked (0 when `settled`). */\n outstanding: number\n}\n\n/** Reconnect after a dropped socket (simple; backoff tuned later). */\nconst RECONNECT_MS = 50\n/** Retry after a token mint failed - a server round trip, so slower than a reconnect. */\nconst TOKEN_RETRY_MS = 2_000\n\n/** Internal signal: a watermark request is safe to repeat on the next socket generation. */\nclass WatermarkConnectionInterruptedError extends Error {}\n\nexport function createGraphSync(deps: GraphSyncDeps): GraphSync {\n const engines = new Map<string, DocSync>()\n const retained = new Map<string, number>([[deps.rootDocId, 1]])\n const retiring = new Set<string>()\n const presenceSession: PresenceSession | undefined = deps.presence\n ? createPresenceSession({\n name: deps.presence.name,\n color: deps.presence.color,\n colorLight: deps.presence.colorLight ?? `${deps.presence.color}33`,\n })\n : undefined\n const presenceDetach = new Map<string, () => void>()\n const docUpdateListeners = new Set<(docId: string) => void>()\n // ack carries only outboxId (no docId); remember which doc issued each append so an\n // ack routes back to the right engine.\n const ackRoute = new Map<string, string>()\n let socket: TransportSocket | undefined\n let open = false\n let disposed = false\n const reportError = (error: unknown) => {\n if (!disposed) deps.onError?.(error instanceof Error ? error : new Error(String(error)))\n }\n const outboundSnapshots: string[] = []\n type CatchupRequest = Extract<RelayClientMessage, { type: 'catchup' }>\n const foregroundCatchups: CatchupRequest[] = []\n const backgroundCatchups: CatchupRequest[] = []\n let catchupInFlight: CatchupRequest | undefined\n let resolveFirstOpen: (() => void) | undefined\n const firstOpen = new Promise<void>((resolve) => {\n resolveFirstOpen = resolve\n })\n const connectionWaiters = new Set<{\n resolve: () => void\n reject: (error: Error) => void\n }>()\n type DocumentWatermark = {\n docId: string\n generation: number\n state: 'active' | 'deleted'\n lastSeq: number\n }\n const watermarkRequests = new Map<\n string,\n {\n resolve: (documents: DocumentWatermark[]) => void\n reject: (error: Error) => void\n timer: ReturnType<typeof setTimeout>\n }\n >()\n\n /**\n * `connected()` is deliberately a first-open milestone. Watermark reconciliation can\n * start hours later, so it needs the socket that is live NOW rather than that settled\n * historical promise.\n */\n function waitForCurrentConnection(): Promise<void> {\n if (open && socket) return Promise.resolve()\n if (disposed) return Promise.reject(new Error('the graph sync session was disposed'))\n return new Promise<void>((resolve, reject) => {\n connectionWaiters.add({ resolve, reject })\n })\n }\n\n async function requestWatermarks(docIds: readonly string[]): Promise<DocumentWatermark[]> {\n while (!disposed) {\n await waitForCurrentConnection()\n const activeSocket = socket\n // A close can land between the waiter resolving and this continuation running.\n if (!open || !activeSocket) continue\n\n try {\n return await new Promise<DocumentWatermark[]>((resolve, reject) => {\n const requestId = crypto.randomUUID()\n const timer = setTimeout(() => {\n watermarkRequests.delete(requestId)\n reject(new Error('the sync relay did not answer a watermark check'))\n }, 15_000)\n watermarkRequests.set(requestId, { resolve, reject, timer })\n activeSocket.send(\n serializeClientMessage({\n type: 'watermarks',\n requestId,\n docIds: [...docIds],\n }),\n )\n })\n } catch (error) {\n // The request is read-only and idempotent. A socket generation ending while\n // it is in flight means \"try on the reconnect\", not a failed reconciliation.\n if (error instanceof WatermarkConnectionInterruptedError && !disposed) continue\n throw error\n }\n }\n throw new Error('the graph sync session was disposed')\n }\n\n /**\n * Documents the CURRENT socket has subscribed to. The relay keeps and forwards presence\n * only for a subscribed document (ASTRA F11), and this mirrors that rule at the source:\n * y-protocols renews every engine's awareness state every fifteen seconds, and an engine\n * created for a background walk starts with an empty `{}` state, so without the gate every\n * unretained engine encrypted and sent presence the relay would only drop. Cleared with\n * the socket; the reconnect's subscribe rebuilds it before anything is advertised.\n */\n const subscribed = new Set<string>()\n\n function sendNow(message: RelayClientMessage): void {\n if (message.type === 'presence' && !subscribed.has(message.docId)) return\n const data = serializeClientMessage(message)\n if (open && socket) {\n socket.send(data)\n if (message.type === 'subscribe' && 'docIds' in message) for (const docId of message.docIds) subscribed.add(docId)\n if (message.type === 'unsubscribe') for (const docId of message.docIds) subscribed.delete(docId)\n } else if (message.type === 'snapshot_put') outboundSnapshots.push(data)\n // Appends are already durable and replay on open. Catch-up is regenerated from the\n // current watermark. Presence is ephemeral, and a lost ack confirmation merely\n // leaves a harmless server receipt, so none belongs in a volatile socket queue.\n }\n\n /**\n * Subscribe to every retained document on a freshly opened socket, in batches the relay\n * accepts. One message used to carry the whole set, and above the protocol's per-message\n * limit the relay rejects it as invalid, which left a tab with many open views silently\n * without live updates for anything after a reconnect (ASTRA F11).\n */\n function subscribeRetained(): void {\n const docIds = [...retained.keys()]\n const batchSize = SYNC_PROTOCOL_LIMITS.maxSubscriptionDocIds\n for (let start = 0; start < docIds.length; start += batchSize) {\n sendNow({ type: 'subscribe', docIds: docIds.slice(start, start + batchSize) })\n }\n }\n\n function pumpCatchups(): void {\n if (!open || !socket || catchupInFlight) return\n catchupInFlight = foregroundCatchups.shift() ?? backgroundCatchups.shift()\n if (catchupInFlight) socket.send(serializeClientMessage(catchupInFlight))\n }\n\n function queueCatchup(message: CatchupRequest): void {\n if (!open) return\n if (\n catchupInFlight?.docId === message.docId &&\n catchupInFlight.generation === message.generation &&\n catchupInFlight.afterSeq === message.afterSeq\n ) {\n return\n }\n // A foreground open supersedes an unsent background request for the same document.\n const removeQueued = (queue: CatchupRequest[]) => {\n const index = queue.findIndex((candidate) => candidate.docId === message.docId)\n if (index >= 0) queue.splice(index, 1)\n }\n removeQueued(foregroundCatchups)\n removeQueued(backgroundCatchups)\n const queue = message.priority === 'foreground' ? foregroundCatchups : backgroundCatchups\n queue.push(message)\n pumpCatchups()\n }\n\n function finishCatchup(requestId?: string): void {\n if (!requestId || catchupInFlight?.requestId !== requestId) return\n const completedDocId = catchupInFlight.docId\n catchupInFlight = undefined\n pumpCatchups()\n retireEngineIfIdle(completedDocId)\n }\n\n function rawSend(message: RelayClientMessage): void {\n if (message.type === 'catchup') {\n queueCatchup(message)\n return\n }\n sendNow(message)\n }\n\n // Cache seeding and relay synchronization are separate operations. A cold index needs\n // bounded, cache-only batches; foreground documents and post-commit reconciliation\n // explicitly promote an engine into synchronization.\n const readied = new Map<string, Promise<void>>()\n const syncEnabled = new Set<string>()\n\n function activate(docId: string, e: DocSync): void {\n readied.set(docId, e.ready())\n }\n\n function enableSync(docId: string, e: DocSync): void {\n if (syncEnabled.has(docId)) return\n syncEnabled.add(docId)\n // A lazily-created engine, including one first used by a cache-only index walk,\n // must catch up when a real consumer promotes it. Keep the cache failure visible\n // through `readied`; this detached branch only reports it without an unhandled\n // rejection while no caller is awaiting yet.\n void (readied.get(docId) ?? Promise.resolve())\n .then(() => {\n if (!disposed && open && engines.get(docId) === e) e.resync()\n })\n .catch(reportError)\n }\n\n /**\n * Identity is applied on RETENTION, not engine creation: background index and\n * materialisation walks create engines for documents nobody is editing, and an engine\n * can outlive a release/retain cycle. The session's apply also resurrects a state\n * cleared by clearPresence — setLocalStateField would silently no-op on it, leaving\n * this client permanently invisible after re-opening a document (live, 2026-07-30).\n */\n function attachPresence(docId: string, e: DocSync): void {\n if (!presenceSession || docId === deps.rootDocId || presenceDetach.has(docId)) return\n presenceDetach.set(docId, presenceSession.attach(e.awareness))\n }\n\n function detachPresence(docId: string): void {\n presenceDetach.get(docId)?.()\n presenceDetach.delete(docId)\n }\n\n function retireEngineIfIdle(docId: string): void {\n if (docId === deps.rootDocId || retained.has(docId) || !retiring.has(docId)) return\n if (\n catchupInFlight?.docId === docId ||\n foregroundCatchups.some((request) => request.docId === docId) ||\n backgroundCatchups.some((request) => request.docId === docId)\n ) {\n return\n }\n const candidate = engines.get(docId)\n if (!candidate || !candidate.isIdle()) return\n retiring.delete(docId)\n detachPresence(docId)\n candidate.destroy()\n engines.delete(docId)\n readied.delete(docId)\n syncEnabled.delete(docId)\n performanceRecorder.mark('sync.engine.count', {\n active: engines.size,\n subscriptions: retained.size,\n })\n }\n\n function engine(docId: string, synchronize = true): DocSync {\n let e = engines.get(docId)\n if (!e) {\n e = createDocSync({\n docId,\n graphId: deps.graphId,\n keyring: deps.keyring,\n send: (message) => {\n if (\n message.type === 'append' ||\n message.type === 'delete' ||\n message.type === 'resurrect'\n ) {\n ackRoute.set(message.outboxId, docId)\n }\n rawSend(message)\n },\n persist: deps.cache.docCache(\n docId,\n docId === deps.rootDocId ? 'root' : 'document',\n ),\n debounceMs: deps.debounceMs,\n collectRowWhen: docId === deps.rootDocId ? undefined : deps.collectRowWhen,\n // The disposed-guarded reporter, not the raw callback: an engine's late\n // rejection after dispose must not reach the next workspace's notice.\n onError: reportError,\n onIdle: () => queueMicrotask(() => retireEngineIfIdle(docId)),\n catchupPriority: () => {\n return retained.has(docId) ? 'foreground' : 'background'\n },\n })\n engines.set(docId, e)\n performanceRecorder.mark('sync.engine.count', {\n active: engines.size,\n subscriptions: retained.size,\n })\n // Name this device's caret before any cursor is advertised, so a collaborator\n // never sees the default-blue \"Anonymous\" flash into a named one.\n if (retained.has(docId)) attachPresence(docId, e)\n // Any content change (local edit or remote update) notifies onDocUpdate, so the\n // store's onChange (index, mirror) fires on edits, not only on registry changes.\n e.doc.on('update', (_update, origin) => {\n // Rehydrating the already-indexed IndexedDB snapshot on every hard refresh\n // is not a content change. Genuine relay updates use REMOTE and still flow.\n if (origin === CACHE_SEED) return\n for (const l of docUpdateListeners) l(docId)\n })\n activate(docId, e)\n }\n if (synchronize) enableSync(docId, e)\n return e\n }\n\n async function indexDocumentUpdate(\n docId: string,\n missingCacheMeansDeleted: boolean,\n ): Promise<Uint8Array | null> {\n const cached = await deps.cache.docCache(docId).load()\n if (cached?.lifecycle === 'deleted' || (!cached && missingCacheMeansDeleted)) {\n return null\n }\n const live = engines.get(docId)\n if (!cached && !live) return null\n\n // A tab can be older than a cache boundary written by a peer, while a local edit can\n // be newer than its own asynchronous cache save. CRDT-merging both views gives the\n // index a snapshot at least as new as the captured token without creating a relay\n // subscription solely to read derived data.\n const merged = new Y.Doc()\n try {\n if (cached) Y.applyUpdate(merged, cached.update, CACHE_SEED)\n if (live) Y.applyUpdate(merged, Y.encodeStateAsUpdate(live.doc), CACHE_SEED)\n return Y.encodeStateAsUpdate(merged)\n } finally {\n merged.destroy()\n }\n }\n\n // The root document is just another synced doc.\n const root = engine(deps.rootDocId)\n const registryMap = root.doc.getMap<RegistryEntry>('registry')\n if (deps.onRegistryChange) registryMap.observe(() => deps.onRegistryChange?.())\n const metaMap = root.doc.getMap<unknown>('meta')\n\n function handleMessage(raw: string): void {\n const message = parseServerMessage(raw)\n if (!message) return\n if (message.type === 'catchup_batch') {\n performanceRecorder.mark('sync.catchup.batch', {\n rows: message.updates.length,\n bytes: new TextEncoder().encode(raw).byteLength,\n continuation: message.hasMore ? 1 : 0,\n })\n }\n if (message.type === 'error') {\n if (\n message.code === 'stale_generation' &&\n message.docId &&\n message.currentGeneration !== undefined\n ) {\n if (catchupInFlight?.docId === message.docId) {\n catchupInFlight = undefined\n }\n void engine(message.docId).staleGeneration(message.currentGeneration)\n pumpCatchups()\n }\n return\n }\n if (message.type === 'watermarks') {\n const pending = watermarkRequests.get(message.requestId)\n if (!pending) return\n clearTimeout(pending.timer)\n watermarkRequests.delete(message.requestId)\n pending.resolve(message.documents)\n return\n }\n const report = reportError\n if (message.type === 'presence') {\n if (!retained.has(message.docId)) return\n void engine(message.docId).receivePresence(message.envelope).catch(report)\n return\n }\n if (message.type === 'ack') {\n const docId = ackRoute.get(message.outboxId)\n if (docId) {\n void engine(docId)\n .receive(message)\n .then(() => ackRoute.delete(message.outboxId))\n .catch(report)\n }\n return\n }\n if (message.type === 'catchup_batch') {\n void engine(message.docId)\n .receive(message)\n .catch(report)\n .finally(() => finishCatchup(message.requestId))\n return\n }\n void engine(message.docId).receive(message).catch(report)\n }\n\n /**\n * A token is fetched for EVERY connect, never captured: the source re-mints when the\n * held token nears expiry, so a socket that drops an hour in still reconnects. Sends\n * Encrypted appends issued while the token is in flight remain in IndexedDB and replay\n * on open. Only rebuildable snapshot uploads use a volatile queue.\n */\n function connect(): void {\n if (disposed) return\n void deps.token().then(\n (token) => {\n if (disposed) return\n const s = deps.connect(`${deps.relayUrl}?token=${encodeURIComponent(token)}`)\n socket = s\n s.onOpen(() => {\n if (disposed || socket !== s) return\n open = true\n resolveFirstOpen?.()\n for (const waiter of connectionWaiters) waiter.resolve()\n connectionWaiters.clear()\n subscribeRetained()\n for (const data of outboundSnapshots.splice(0)) s.send(data)\n // A local awareness state set while the socket was closed was dropped\n // (presence never queues). Advertise every retained document now so the\n // relay can store the envelope and replay it to late subscribers.\n for (const docId of retained.keys()) {\n if (docId === deps.rootDocId) continue\n const e = engines.get(docId)\n if (e) void e.advertisePresence().catch(reportError)\n }\n // Resync only engines promoted into synchronization. Cache-only cold-index\n // batches must not turn one quick derivation into thousands of hidden\n // downloads before post-commit reconciliation acquires its graph lock.\n for (const [docId, e] of engines) {\n if (!syncEnabled.has(docId)) continue\n void (readied.get(docId) ?? Promise.resolve())\n .then(() => e.resync())\n .catch(reportError)\n }\n })\n s.onMessage(handleMessage)\n s.onClose(() => {\n if (socket !== s) return\n open = false\n socket = undefined\n // Routes and subscriptions belong to one socket generation. Durable\n // IndexedDB rows, not these entries, determine outstanding work and are\n // replayed below; the next open resubscribes every retained document.\n subscribed.clear()\n ackRoute.clear()\n catchupInFlight = undefined\n foregroundCatchups.length = 0\n backgroundCatchups.length = 0\n for (const pending of watermarkRequests.values()) {\n clearTimeout(pending.timer)\n pending.reject(new WatermarkConnectionInterruptedError())\n }\n watermarkRequests.clear()\n if (!disposed) setTimeout(connect, RECONNECT_MS)\n })\n },\n // No token, no socket - normally offline, or a server that is down. Back off\n // further than a dropped socket does: minting is a server round trip.\n () => {\n if (!disposed) setTimeout(connect, TOKEN_RETRY_MS)\n },\n )\n }\n connect()\n\n return {\n rootDoc: root.doc,\n docSync: (docId) => engine(docId),\n retainDoc(docId) {\n const count = retained.get(docId) ?? 0\n retained.set(docId, count + 1)\n retiring.delete(docId)\n performanceRecorder.mark('sync.subscription.count', {\n subscriptions: retained.size,\n active: engines.size,\n })\n const retainedEngine = engine(docId)\n // The engine may predate retention (background index walk) or have been\n // released and re-retained — both must (re)apply the session identity here.\n attachPresence(docId, retainedEngine)\n if (count === 0 && open) {\n rawSend({ type: 'subscribe', docIds: [docId] })\n // The relay stores the latest presence envelope per subscriber; advertising\n // right after subscribe makes this client immediately visible to the room\n // and replayable to whoever subscribes next.\n void retainedEngine.advertisePresence().catch(reportError)\n void (readied.get(docId) ?? Promise.resolve()).then(() => retainedEngine.resync())\n }\n let released = false\n return () => {\n if (released || docId === deps.rootDocId) return\n released = true\n const next = (retained.get(docId) ?? 1) - 1\n if (next > 0) {\n retained.set(docId, next)\n return\n }\n retained.delete(docId)\n retiring.add(docId)\n performanceRecorder.mark('sync.subscription.count', {\n subscriptions: retained.size,\n active: engines.size,\n })\n // Awareness removal is an encrypted, ephemeral message. Preserve its wire\n // ordering before unsubscribe so peers do not retain a stale collaborator.\n detachPresence(docId)\n void retainedEngine\n .clearPresence()\n .catch(reportError)\n .finally(() => {\n if (disposed) return\n if (retained.has(docId)) {\n // Re-retained while the tombstone was in flight: re-apply the\n // session identity (this resurrects the cleared state) and\n // re-advertise, or this client stays invisible in this doc.\n attachPresence(docId, retainedEngine)\n if (open) void retainedEngine.advertisePresence().catch(reportError)\n return\n }\n rawSend({ type: 'unsubscribe', docIds: [docId] })\n retireEngineIfIdle(docId)\n })\n }\n },\n deleteDoc: (docId) => engine(docId).delete(),\n compactDoc: (docId) => deps.cache.docCache(docId).compact(),\n uploadSnapshot: (docId) => engine(docId).compact(),\n registry: () => registryMap,\n getMeta() {\n const name = metaMap.get('name')\n const settings = metaMap.get('settings')\n return {\n ...(typeof name === 'string' && name !== '' ? { name } : {}),\n ...(typeof settings === 'object' && settings !== null ? { settings: settings as Record<string, unknown> } : {}),\n }\n },\n setMetaName(name) {\n metaMap.set('name', name)\n },\n setMetaSettings(settings) {\n metaMap.set('settings', settings)\n },\n onMetaChange(listener) {\n metaMap.observe(listener)\n return () => metaMap.unobserve(listener)\n },\n connected: () => firstOpen,\n rootCaughtUp: () => root.caughtUp(),\n onDocUpdate(listener) {\n docUpdateListeners.add(listener)\n return () => docUpdateListeners.delete(listener)\n },\n async ready() {\n // Durable operations must replay even when their document is not otherwise\n // opened during this session.\n for (const docId of await deps.cache.pendingDocIds()) engine(docId)\n await Promise.all([...readied.values()])\n },\n async whenReady(docId) {\n engine(docId) // creating on demand also queues its cache read NOW, not at walk order\n await readied.get(docId)\n },\n async seedDocsFromCache(docIds) {\n const batch = docIds.map((docId) => engine(docId, false))\n await Promise.all(\n docIds.map((docId) => readied.get(docId) ?? Promise.resolve()),\n )\n return batch.map((entry) => entry.doc)\n },\n async readyDocs(docIds) {\n for (const docId of docIds) engine(docId)\n await Promise.all(\n docIds.map((docId) => readied.get(docId) ?? Promise.resolve()),\n )\n },\n retireDocs(docIds) {\n for (const docId of docIds) {\n if (docId === deps.rootDocId || retained.has(docId)) continue\n retiring.add(docId)\n retireEngineIfIdle(docId)\n }\n },\n firstCatchupPageDoc: (docId) => engine(docId).firstCatchupPage(),\n caughtUpDoc: (docId) => engine(docId).caughtUp(),\n async docsNeedingCatchup(docIds) {\n const changed: string[] = []\n for (\n let start = 0;\n start < docIds.length;\n start += SYNC_PROTOCOL_LIMITS.maxSubscriptionDocIds\n ) {\n const batch = docIds.slice(\n start,\n start + SYNC_PROTOCOL_LIMITS.maxSubscriptionDocIds,\n )\n const local = await deps.cache.watermarks(batch)\n const remote = await requestWatermarks(batch)\n for (const watermark of remote) {\n const cached = local.get(watermark.docId)\n if (\n (!cached &&\n (watermark.generation !== 1 ||\n watermark.state !== 'active' ||\n watermark.lastSeq !== 0)) ||\n (cached &&\n (cached.generation !== watermark.generation ||\n cached.lifecycle !== watermark.state ||\n cached.lastSeq !== watermark.lastSeq))\n ) {\n changed.push(watermark.docId)\n }\n }\n }\n return changed\n },\n async pendingIndexChanges() {\n const changes = await deps.cache.pendingIndexChanges()\n const dirtyDocIds = new Set(changes.map((change) => change.docId))\n return {\n docIds: changes.map((change) => change.docId),\n rootUpdate: await indexDocumentUpdate(\n deps.rootDocId,\n dirtyDocIds.has(deps.rootDocId),\n ),\n readDocument: (docId) =>\n indexDocumentUpdate(docId, dirtyDocIds.has(docId)),\n acknowledge: () => deps.cache.acknowledgeIndexChanges(changes),\n }\n },\n isConnected: () => open,\n async flushAll({ onProgress } = {}) {\n const all = [...engines.values()]\n let flushed = 0\n await Promise.all(all.map((e) => e.flush().then(() => onProgress?.(++flushed, all.length))))\n },\n async awaitAcked({ onProgress, signal, stallMs = 30_000 } = {}) {\n const initial = await deps.cache.countPending()\n if (initial === 0) return { settled: true, outstanding: 0 }\n onProgress?.(initial)\n\n return new Promise<AckResult>((resolve) => {\n let last = initial\n let lastProgressAt = Date.now()\n const POLL_MS = 100\n let timer: ReturnType<typeof setTimeout> | undefined\n const finish = (result: AckResult) => {\n if (timer) clearTimeout(timer)\n resolve(result)\n }\n const poll = async () => {\n try {\n const current = await deps.cache.countPending()\n if (current < last) {\n last = current\n lastProgressAt = Date.now()\n onProgress?.(current)\n }\n if (current === 0) return finish({ settled: true, outstanding: 0 })\n if (signal?.aborted) return finish({ settled: false, outstanding: current })\n if (Date.now() - lastProgressAt >= stallMs) {\n return finish({ settled: false, outstanding: current })\n }\n timer = setTimeout(poll, POLL_MS)\n } catch (error) {\n reportError(error)\n finish({ settled: false, outstanding: last })\n }\n }\n timer = setTimeout(poll, POLL_MS)\n })\n },\n diagnostics: () => ({\n activeEngines: engines.size,\n retainedDocuments: retained.size,\n }),\n dispose() {\n disposed = true\n socket?.close()\n for (const detach of presenceDetach.values()) detach()\n presenceDetach.clear()\n for (const e of engines.values()) e.destroy()\n engines.clear()\n retiring.clear()\n syncEnabled.clear()\n foregroundCatchups.length = 0\n backgroundCatchups.length = 0\n catchupInFlight = undefined\n for (const pending of watermarkRequests.values()) {\n clearTimeout(pending.timer)\n pending.reject(new Error('the graph sync session was disposed'))\n }\n watermarkRequests.clear()\n for (const waiter of connectionWaiters) {\n waiter.reject(new Error('the graph sync session was disposed'))\n }\n connectionWaiters.clear()\n },\n }\n}\n\n/** The real browser WebSocket adapter (not used in unit tests). */\nexport function browserTransport(url: string): TransportSocket {\n const ws = new WebSocket(url)\n return {\n send: (data) => ws.send(data),\n close: () => ws.close(),\n onOpen: (cb) => ws.addEventListener('open', () => cb()),\n onMessage: (cb) => ws.addEventListener('message', (e) => cb(String((e as MessageEvent).data))),\n onClose: (cb) => ws.addEventListener('close', () => cb()),\n }\n}\n","/**\n * A raw IndexedDB connection that survives the browser closing it underneath the page.\n *\n * Browsers close a connection without the page asking when they evict or reclaim storage,\n * when a discarded tab is restored (common on Android), when the storage process crashes,\n * and when another tab opens a newer database version. From then on every `transaction()`\n * on the old handle throws `InvalidStateError: The database connection is closing`. Before\n * 2026-09-01 the sync Local Cache held one such handle for the life of the workspace, so a\n * page that stayed open kept failing every save until a reload, and the keystrokes typed in\n * between reached neither the cache nor the relay.\n *\n * This wrapper keeps the \"one connection per cache\" shape but notices the close (via the\n * `close` event, the `versionchange` event, or the synchronous throw) and reopens on the\n * next use. A reopen that fails is reported to the caller once rather than retried in a\n * loop, and `dispose()` closes for good.\n */\nexport interface ReopenableConnection {\n /** The live database, opening a fresh connection first if the last one was closed. */\n current(): Promise<IDBDatabase>\n /**\n * `current().transaction(stores, mode)`, retried once through a reopen when the handle\n * turns out to be closed. Call sites place their first request synchronously after\n * awaiting this, exactly as they would after a bare `db.transaction()`.\n */\n transaction(stores: string | string[], mode: IDBTransactionMode): Promise<IDBTransaction>\n /** Close for good. Later calls reject with {@link ConnectionDisposedError} rather than reopen. */\n dispose(): void\n}\n\nexport class ConnectionDisposedError extends Error {\n constructor() {\n super('The IndexedDB connection has been disposed')\n this.name = 'ConnectionDisposedError'\n }\n}\n\n/**\n * The error `IDBDatabase.transaction()` throws once the connection has closed. Every engine\n * uses the `InvalidStateError` name for it; only the message wording differs.\n */\nexport function isClosedConnectionError(error: unknown): boolean {\n return error instanceof Error && error.name === 'InvalidStateError'\n}\n\nexport async function createReopenableConnection(\n open: () => Promise<IDBDatabase>,\n): Promise<ReopenableConnection> {\n let live: IDBDatabase | null = null\n let opening: Promise<IDBDatabase> | null = null\n let disposed = false\n\n const forget = (handle: IDBDatabase) => {\n if (live === handle) live = null\n }\n\n const attach = (handle: IDBDatabase): IDBDatabase => {\n live = handle\n // Fired when the browser terminates the connection abnormally. It is NOT fired for\n // the page's own close(), so the synchronous-throw path below still matters.\n handle.onclose = () => forget(handle)\n // A newer version is being opened elsewhere, typically a deploy picked up by another\n // tab. Releasing the handle lets that upgrade proceed instead of blocking it; the\n // next use reopens at whatever version the browser now holds, and a VersionError\n // there surfaces as a \"reload this tab\" rather than a silently stuck upgrade.\n handle.onversionchange = () => {\n handle.close()\n forget(handle)\n }\n return handle\n }\n\n attach(await open())\n\n async function current(): Promise<IDBDatabase> {\n if (disposed) throw new ConnectionDisposedError()\n if (live) return live\n if (!opening) {\n // One reopen at a time, and a failed one is not memoised: the next caller tries\n // afresh, which is what makes a transient VersionError recoverable after the\n // other tab's upgrade completes.\n opening = open()\n .then(attach)\n .finally(() => {\n opening = null\n })\n }\n return opening\n }\n\n return {\n current,\n async transaction(stores, mode) {\n const first = await current()\n try {\n return first.transaction(stores, mode)\n } catch (error) {\n if (!isClosedConnectionError(error)) throw error\n // Closed without a `close` event reaching us yet. Drop the handle and go\n // through exactly one reopen; a second failure is the caller's to report.\n forget(first)\n return (await current()).transaction(stores, mode)\n }\n },\n dispose() {\n disposed = true\n live?.close()\n live = null\n },\n }\n}\n","/**\n * Browser-private sync storage. A document row is normally a rebuildable Yjs cache. A\n * dirty boundary temporarily makes its newer bytes recoverable user work until an exact\n * encrypted operation is committed to the outbox before the matching socket send.\n */\nimport type { DocCache, DocCacheState, DurableOutboxOperation, NewOutboxOperation } from './doc-sync'\nimport * as Y from 'yjs'\nimport { createReopenableConnection } from '$lib/storage/idb-connection'\nimport { mergeStateVectors, stateVectorCovers } from './state-vector'\n\nconst DB_NAME = 'etherpk-sync'\nconst DB_VERSION = 3\nconst DOC_STORE = 'docs'\nconst OUTBOX_STORE = 'outbox'\nconst OUTBOX_GRAPH_INDEX = 'by_graph'\nconst OUTBOX_GRAPH_DOC_INDEX = 'by_graph_doc'\nconst INDEX_DIRTY_STORE = 'index-dirty'\nconst INDEX_DIRTY_GRAPH_INDEX = 'by_graph'\n\ninterface DocRow extends DocCacheState {\n key: string\n graphId: string\n docId: string\n}\n\ntype OutboxRow = DurableOutboxOperation & { key: string }\n\ninterface IndexDirtyRow {\n key: string\n graphId: string\n docId: string\n token: string\n}\n\n/** One exact Local Cache persistence boundary which has not yet reached the index. */\nexport interface IndexDirtyChange {\n docId: string\n token: string\n}\n\nexport interface GraphCache {\n /** Root writes compare registry identity; ordinary documents conservatively dirty on change. */\n docCache(docId: string, indexRole?: 'document' | 'root'): DocCache\n listDocIds(): Promise<string[]>\n watermarks(\n docIds: readonly string[],\n ): Promise<\n Map<\n string,\n {\n generation: number\n lifecycle: 'active' | 'deleted'\n lastSeq: number\n }\n >\n >\n pendingDocIds(): Promise<string[]>\n countPending(): Promise<number>\n /** Index-visible cache changes which have not crossed an explicit index commit. */\n pendingIndexChanges(): Promise<IndexDirtyChange[]>\n /** Clear only the exact boundaries represented by a committed worker operation. */\n acknowledgeIndexChanges(changes: readonly IndexDirtyChange[]): Promise<void>\n dispose(): void\n}\n\nfunction openDb(): Promise<IDBDatabase> {\n return new Promise((resolve, reject) => {\n let settled = false\n const request = indexedDB.open(DB_NAME, DB_VERSION)\n request.onupgradeneeded = (event) => {\n const db = request.result\n if (!db.objectStoreNames.contains(DOC_STORE)) {\n db.createObjectStore(DOC_STORE, { keyPath: 'key' })\n }\n if (!db.objectStoreNames.contains(OUTBOX_STORE)) {\n const outbox = db.createObjectStore(OUTBOX_STORE, { keyPath: 'key' })\n outbox.createIndex(OUTBOX_GRAPH_INDEX, 'graphId')\n outbox.createIndex(OUTBOX_GRAPH_DOC_INDEX, ['graphId', 'docId'])\n }\n if (!db.objectStoreNames.contains(INDEX_DIRTY_STORE)) {\n const dirty = db.createObjectStore(INDEX_DIRTY_STORE, { keyPath: 'key' })\n dirty.createIndex(INDEX_DIRTY_GRAPH_INDEX, 'graphId')\n }\n if (event.oldVersion > 0 && event.oldVersion < 3) {\n // A legacy cache may already be ahead of its separately-persisted index.\n // Backfill every document as dirty once; a root row naturally forces the\n // first post-upgrade open through a safe full replacement.\n const upgrade = request.transaction\n const docs = upgrade?.objectStore(DOC_STORE)\n const dirty = upgrade?.objectStore(INDEX_DIRTY_STORE)\n if (docs && dirty) {\n docs.openCursor().onsuccess = (cursorEvent) => {\n const cursor = (cursorEvent.target as IDBRequest<IDBCursorWithValue | null>)\n .result\n if (!cursor) return\n const row = cursor.value as Partial<DocRow>\n if (typeof row.graphId === 'string' && typeof row.docId === 'string') {\n dirty.put({\n key: `${row.graphId}/${row.docId}`,\n graphId: row.graphId,\n docId: row.docId,\n token: crypto.randomUUID(),\n } satisfies IndexDirtyRow)\n }\n cursor.continue()\n }\n }\n }\n }\n request.onsuccess = () => {\n if (settled) {\n request.result.close()\n return\n }\n settled = true\n // Version-change and abnormal-close handling belong to the connection wrapper in\n // openGraphCache, which reopens on the next use; a one-shot caller such as\n // deleteGraphCache closes its own handle in a finally.\n resolve(request.result)\n }\n request.onerror = () => {\n if (settled) return\n settled = true\n reject(request.error)\n }\n request.onblocked = () => {\n if (settled) return\n settled = true\n reject(new Error('The sync cache upgrade is blocked by another open tab'))\n }\n })\n}\n\nfunction requestResult<T>(request: IDBRequest<T>): Promise<T> {\n return new Promise((resolve, reject) => {\n request.onsuccess = () => resolve(request.result)\n request.onerror = () => reject(request.error)\n })\n}\n\nfunction transactionDone(transaction: IDBTransaction): Promise<void> {\n return new Promise((resolve, reject) => {\n transaction.oncomplete = () => resolve()\n transaction.onabort = () => reject(transaction.error ?? new Error('IndexedDB transaction aborted'))\n transaction.onerror = () => reject(transaction.error ?? new Error('IndexedDB transaction failed'))\n })\n}\n\nfunction graphRange(graphId: string): IDBKeyRange {\n return IDBKeyRange.only(graphId)\n}\n\nfunction graphDocRange(graphId: string, docId: string): IDBKeyRange {\n return IDBKeyRange.only([graphId, docId])\n}\n\nexport async function openGraphCache(graphId: string): Promise<GraphCache> {\n // One connection per cache, but one the browser cannot kill for the rest of the\n // workspace's life: an eviction, a discarded-and-restored tab or another tab's schema\n // upgrade closes the handle, and the next operation reopens it (idb-connection.ts).\n const connection = await createReopenableConnection(openDb)\n const docKey = (docId: string) => `${graphId}/${docId}`\n const outboxKey = (outboxId: string) => `${graphId}/${outboxId}`\n const indexDirtyKey = (docId: string) => `${graphId}/${docId}`\n\n function markIndexDirty(transaction: IDBTransaction, docId: string): void {\n transaction.objectStore(INDEX_DIRTY_STORE).put({\n key: indexDirtyKey(docId),\n graphId,\n docId,\n // Replacing the token makes acknowledgement compare-and-delete. If another tab\n // writes while the worker is ingesting, that later boundary remains durable.\n token: crypto.randomUUID(),\n } satisfies IndexDirtyRow)\n }\n\n function stateRow(docId: string, state: DocCacheState): DocRow {\n return { key: docKey(docId), graphId, docId, ...state }\n }\n\n function withDirtyTokens(state: DocCacheState, tokens: Iterable<string>): DocCacheState {\n const { dirtyTokens: _previous, ...rest } = state\n const dirtyTokens = [...new Set(tokens)].sort()\n return dirtyTokens.length > 0 ? { ...rest, dirtyTokens } : rest\n }\n\n function mergeState(existing: DocRow | undefined, incoming: DocCacheState): DocCacheState {\n if (!existing) return withDirtyTokens(incoming, incoming.dirtyTokens ?? [])\n const existingGeneration = existing.generation ?? 1\n const incomingGeneration = incoming.generation ?? 1\n if (incomingGeneration < existingGeneration) {\n return existing\n }\n if (incomingGeneration > existingGeneration) {\n return withDirtyTokens(incoming, incoming.dirtyTokens ?? [])\n }\n let update: Uint8Array\n try {\n update = Y.mergeUpdates([existing.update, incoming.update])\n } catch {\n // The cache is rebuildable. A damaged legacy row must not prevent a fresh,\n // valid server state from replacing it.\n update = incoming.update\n }\n let lastSyncedStateVector: Uint8Array\n try {\n lastSyncedStateVector = mergeStateVectors(\n existing.lastSyncedStateVector,\n incoming.lastSyncedStateVector,\n )\n } catch {\n lastSyncedStateVector = incoming.lastSyncedStateVector\n }\n return withDirtyTokens(\n {\n update,\n lastSeq: Math.max(existing.lastSeq, incoming.lastSeq),\n lastSyncedStateVector,\n generation: incomingGeneration,\n lifecycle:\n existing.lifecycle === 'deleted' || incoming.lifecycle === 'deleted'\n ? 'deleted'\n : 'active',\n },\n [...(existing.dirtyTokens ?? []), ...(incoming.dirtyTokens ?? [])],\n )\n }\n\n function sameYjsState(left: Uint8Array, right: Uint8Array): boolean {\n const equalBytes = (a: Uint8Array, b: Uint8Array) =>\n a.byteLength === b.byteLength && a.every((value, index) => value === b[index])\n if (equalBytes(left, right)) return true\n try {\n // mergeUpdates canonicalises struct and delete-set ordering without materialising\n // a Y.Doc. This distinguishes a real CRDT change from a save which only advanced\n // sequence or confirmed-state metadata.\n return equalBytes(Y.mergeUpdates([left]), Y.mergeUpdates([right]))\n } catch {\n // Damaged legacy rows are rebuildable. Treat uncertainty as changed so the index\n // is conservatively repaired alongside the fresh valid cache write.\n return false\n }\n }\n\n function stableJson(value: unknown): string {\n if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`\n if (value !== null && typeof value === 'object') {\n return `{${Object.entries(value)\n .filter(([, child]) => child !== undefined)\n .sort(([left], [right]) => left.localeCompare(right))\n .map(([key, child]) => `${JSON.stringify(key)}:${stableJson(child)}`)\n .join(',')}}`\n }\n return JSON.stringify(value) ?? String(value)\n }\n\n /**\n * Root metadata shares a Y.Doc with registry identity but does not feed the document index.\n * Only root writes pay to compare that projection. Ordinary content changes stay on the\n * cheap structural path and conservatively journal any real Yjs change.\n */\n function sameIndexedState(\n left: Uint8Array,\n right: Uint8Array,\n indexRole: 'document' | 'root',\n ): boolean {\n if (sameYjsState(left, right)) return true\n if (indexRole === 'document') return false\n\n const projection = (update: Uint8Array): string | undefined => {\n const doc = new Y.Doc()\n try {\n Y.applyUpdate(doc, update)\n const identities: unknown[][] = []\n doc.getMap<Record<string, unknown>>('registry').forEach((entry, docId) => {\n const kind = entry?.kind\n const concept = kind === 'journal' ? (entry?.date ?? '') : (entry?.title ?? '')\n const aliases = Array.isArray(entry?.aliases)\n ? [...entry.aliases].sort((leftAlias, rightAlias) =>\n stableJson(leftAlias).localeCompare(stableJson(rightAlias)),\n )\n : (entry?.aliases ?? [])\n identities.push([docId, kind, concept, aliases])\n })\n identities.sort(([leftId], [rightId]) =>\n String(leftId).localeCompare(String(rightId)),\n )\n return stableJson(identities)\n } catch {\n return undefined\n } finally {\n doc.destroy()\n }\n }\n const leftProjection = projection(left)\n return leftProjection !== undefined && leftProjection === projection(right)\n }\n\n return {\n docCache(docId: string, indexRole = 'document'): DocCache {\n return {\n async load() {\n const transaction = await connection.transaction(DOC_STORE, 'readonly')\n const row = await requestResult(\n transaction.objectStore(DOC_STORE).get(docKey(docId)) as IDBRequest<DocRow | undefined>,\n )\n if (!row) return null\n return {\n update: row.update,\n lastSeq: row.lastSeq,\n lastSyncedStateVector: row.lastSyncedStateVector,\n ...(row.generation === undefined ? {} : { generation: row.generation }),\n ...(row.lifecycle === undefined ? {} : { lifecycle: row.lifecycle }),\n ...(row.dirtyTokens === undefined ? {} : { dirtyTokens: row.dirtyTokens }),\n }\n },\n async save(state, options) {\n const transaction = await connection.transaction(\n [DOC_STORE, INDEX_DIRTY_STORE],\n 'readwrite',\n )\n const store = transaction.objectStore(DOC_STORE)\n const existing = await requestResult(\n store.get(docKey(docId)) as IDBRequest<DocRow | undefined>,\n )\n const merged = mergeState(existing, state)\n const dirtyTokens = [...(merged.dirtyTokens ?? [])]\n if (options?.supersededDirtyToken) {\n const index = dirtyTokens.indexOf(options.supersededDirtyToken)\n if (index >= 0) dirtyTokens.splice(index, 1)\n }\n if (options?.dirtyToken) dirtyTokens.push(options.dirtyToken)\n store.put(stateRow(docId, withDirtyTokens(merged, dirtyTokens)))\n const indexChanged =\n !existing ||\n (indexRole === 'document' &&\n options?.indexContentChanged !== undefined\n ? options.indexContentChanged\n : !sameIndexedState(existing.update, merged.update, indexRole)) ||\n (existing.lifecycle ?? 'active') !== (merged.lifecycle ?? 'active')\n if (indexChanged) markIndexDirty(transaction, docId)\n await transactionDone(transaction)\n },\n async enqueue(operation: NewOutboxOperation, state: DocCacheState, options) {\n const transaction = await connection.transaction(\n [DOC_STORE, OUTBOX_STORE, INDEX_DIRTY_STORE],\n 'readwrite',\n )\n const docs = transaction.objectStore(DOC_STORE)\n const existing = await requestResult(\n docs.get(docKey(docId)) as IDBRequest<DocRow | undefined>,\n )\n const merged = mergeState(existing, state)\n const settled = new Set(options?.settledDirtyTokens ?? [])\n docs.put(\n stateRow(\n docId,\n withDirtyTokens(\n merged,\n (merged.dirtyTokens ?? []).filter((token) => !settled.has(token)),\n ),\n ),\n )\n const row: OutboxRow = {\n key: outboxKey(operation.outboxId),\n graphId,\n docId,\n ...operation,\n attemptCount: 0,\n lastAttemptAt: null,\n }\n transaction.objectStore(OUTBOX_STORE).add(row)\n const indexChanged =\n operation.kind === 'delete' ||\n !existing ||\n !sameIndexedState(existing.update, merged.update, indexRole) ||\n (existing.lifecycle ?? 'active') !== (merged.lifecycle ?? 'active')\n if (indexChanged) markIndexDirty(transaction, docId)\n await transactionDone(transaction)\n },\n async pending() {\n const transaction = await connection.transaction(OUTBOX_STORE, 'readonly')\n const rows = await requestResult(\n transaction\n .objectStore(OUTBOX_STORE)\n .index(OUTBOX_GRAPH_DOC_INDEX)\n .getAll(graphDocRange(graphId, docId)) as IDBRequest<OutboxRow[]>,\n )\n return rows\n .sort((a, b) => a.createdAt - b.createdAt || a.outboxId.localeCompare(b.outboxId))\n .map(({ key: _key, ...operation }) => operation)\n },\n async markAttempt(outboxId, attemptedAt) {\n const transaction = await connection.transaction(OUTBOX_STORE, 'readwrite')\n const store = transaction.objectStore(OUTBOX_STORE)\n const key = outboxKey(outboxId)\n const row = await requestResult(store.get(key) as IDBRequest<OutboxRow | undefined>)\n if (row) {\n row.attemptCount += 1\n row.lastAttemptAt = attemptedAt\n store.put(row)\n }\n await transactionDone(transaction)\n },\n async commitAcknowledgement(\n outboxId,\n generation,\n seq,\n update,\n lastSyncedStateVector,\n ) {\n const transaction = await connection.transaction(\n [DOC_STORE, OUTBOX_STORE, INDEX_DIRTY_STORE],\n 'readwrite',\n )\n const docs = transaction.objectStore(DOC_STORE)\n const outbox = transaction.objectStore(OUTBOX_STORE)\n const row = await requestResult(\n outbox.get(outboxKey(outboxId)) as IDBRequest<OutboxRow | undefined>,\n )\n const existing = await requestResult(\n docs.get(docKey(docId)) as IDBRequest<DocRow | undefined>,\n )\n if (!row || row.docId !== docId || row.kind === 'delete') {\n // Another tab can receive and durably commit the same idempotent ack\n // first. The exact outbox row is then gone, while the shared confirmed\n // state vector proves whether that operation's Yjs structs are settled.\n // This remains valid when its sequence could not yet advance because a\n // preceding update from another connection is still missing.\n let settled = false\n try {\n settled =\n !row &&\n existing !== undefined &&\n (existing.generation ?? 1) === generation &&\n stateVectorCovers(\n existing.lastSyncedStateVector,\n lastSyncedStateVector,\n )\n } catch {\n // A malformed legacy vector is rebuildable and cannot prove an ack.\n }\n await transactionDone(transaction)\n return settled\n }\n const acknowledged: DocCacheState = {\n update,\n lastSeq: seq,\n lastSyncedStateVector,\n generation,\n lifecycle: 'active',\n }\n let merged: DocCacheState\n if (\n row.kind === 'resurrect' &&\n existing &&\n generation > (existing.generation ?? 1)\n ) {\n // The relay confirms only the resurrection operation's captured\n // boundary. Another tab, or this tab after encryption yielded, may\n // already have added structs beyond that boundary to the shared old-\n // generation row. Promote those unconfirmed structs with the ack and\n // retain their recovery tokens instead of replacing the whole row.\n let promotedUpdate = update\n try {\n const unconfirmed = Y.diffUpdate(existing.update, row.stateVector)\n promotedUpdate = Y.mergeUpdates([update, unconfirmed])\n } catch {\n // A damaged cache is rebuildable. Keep the valid acknowledged\n // snapshot while preserving its dirty tokens for a visible retry.\n }\n merged = withDirtyTokens(\n { ...acknowledged, update: promotedUpdate },\n existing.dirtyTokens ?? [],\n )\n } else {\n merged = mergeState(existing, acknowledged)\n }\n docs.put(stateRow(docId, merged))\n // A resurrection acknowledgement is the moment the cached lifecycle\n // becomes active. An older checkpoint may still be indexing the deleted\n // snapshot, so rotate its token in this same transaction. The older\n // compare-and-delete can then clear only its own boundary.\n const indexChanged =\n !existing ||\n (existing.generation ?? 1) !== (merged.generation ?? 1) ||\n (existing.lifecycle ?? 'active') !== (merged.lifecycle ?? 'active')\n if (indexChanged) markIndexDirty(transaction, docId)\n outbox.delete(row.key)\n await transactionDone(transaction)\n return true\n },\n async commitDeleteAcknowledgement(outboxId, generation) {\n const transaction = await connection.transaction(\n [DOC_STORE, OUTBOX_STORE, INDEX_DIRTY_STORE],\n 'readwrite',\n )\n const outbox = transaction.objectStore(OUTBOX_STORE)\n const row = await requestResult(\n outbox.get(outboxKey(outboxId)) as IDBRequest<OutboxRow | undefined>,\n )\n if (!row || row.docId !== docId || row.kind !== 'delete') {\n transaction.abort()\n try {\n await transactionDone(transaction)\n } catch {\n // An explicit abort is expected for an unknown acknowledgement.\n }\n return false\n }\n transaction.objectStore(DOC_STORE).delete(docKey(docId))\n outbox.delete(row.key)\n markIndexDirty(transaction, docId)\n await transactionDone(transaction)\n void generation\n return true\n },\n async discard(outboxId) {\n const transaction = await connection.transaction(OUTBOX_STORE, 'readwrite')\n transaction.objectStore(OUTBOX_STORE).delete(outboxKey(outboxId))\n await transactionDone(transaction)\n },\n async compact() {\n const transaction = await connection.transaction(DOC_STORE, 'readwrite')\n const store = transaction.objectStore(DOC_STORE)\n const existing = await requestResult(store.get(docKey(docId)) as IDBRequest<DocRow | undefined>)\n if (!existing) return\n // Applying the row into a fresh document collects its deleted items at the\n // end of that transaction (nothing pins them there), and encoding the\n // result is the same state without their content. Built from the row, not\n // from a live engine, so another tab's saves since are never overwritten.\n const collected = new Y.Doc()\n try {\n Y.applyUpdate(collected, existing.update)\n store.put({ ...existing, update: Y.encodeStateAsUpdate(collected) })\n } finally {\n collected.destroy()\n }\n await transactionDone(transaction)\n },\n async purge() {\n const transaction = await connection.transaction(\n [DOC_STORE, OUTBOX_STORE, INDEX_DIRTY_STORE],\n 'readwrite',\n )\n transaction.objectStore(DOC_STORE).delete(docKey(docId))\n transaction\n .objectStore(OUTBOX_STORE)\n .index(OUTBOX_GRAPH_DOC_INDEX)\n .openKeyCursor(graphDocRange(graphId, docId)).onsuccess = (event) => {\n const cursor = (event.target as IDBRequest<IDBCursor | null>).result\n if (!cursor) return\n transaction.objectStore(OUTBOX_STORE).delete(cursor.primaryKey)\n cursor.continue()\n }\n markIndexDirty(transaction, docId)\n await transactionDone(transaction)\n },\n }\n },\n async listDocIds() {\n const transaction = await connection.transaction(DOC_STORE, 'readonly')\n const keys = await requestResult(transaction.objectStore(DOC_STORE).getAllKeys() as IDBRequest<string[]>)\n const prefix = `${graphId}/`\n return keys.filter((key) => key.startsWith(prefix)).map((key) => key.slice(prefix.length))\n },\n async watermarks(docIds) {\n const transaction = await connection.transaction(DOC_STORE, 'readonly')\n const store = transaction.objectStore(DOC_STORE)\n const rows = await Promise.all(\n docIds.map((docId) =>\n requestResult(\n store.get(docKey(docId)) as IDBRequest<DocRow | undefined>,\n ),\n ),\n )\n return new Map(\n rows.flatMap((row, index) =>\n row\n ? [\n [\n docIds[index],\n {\n generation: row.generation ?? 1,\n lifecycle: row.lifecycle ?? 'active',\n lastSeq: row.lastSeq,\n },\n ] as const,\n ]\n : [],\n ),\n )\n },\n async pendingDocIds() {\n const transaction = await connection.transaction(OUTBOX_STORE, 'readonly')\n const rows = await requestResult(\n transaction.objectStore(OUTBOX_STORE).index(OUTBOX_GRAPH_INDEX).getAll(graphRange(graphId)) as IDBRequest<\n OutboxRow[]\n >,\n )\n return [...new Set(rows.map((row) => row.docId))].sort()\n },\n async countPending() {\n const transaction = await connection.transaction(OUTBOX_STORE, 'readonly')\n return requestResult(\n transaction.objectStore(OUTBOX_STORE).index(OUTBOX_GRAPH_INDEX).count(graphRange(graphId)),\n )\n },\n async pendingIndexChanges() {\n const transaction = await connection.transaction(INDEX_DIRTY_STORE, 'readonly')\n const rows = await requestResult(\n transaction\n .objectStore(INDEX_DIRTY_STORE)\n .index(INDEX_DIRTY_GRAPH_INDEX)\n .getAll(graphRange(graphId)) as IDBRequest<IndexDirtyRow[]>,\n )\n return rows\n .sort((a, b) => a.docId.localeCompare(b.docId))\n .map(({ docId, token }) => ({ docId, token }))\n },\n async acknowledgeIndexChanges(changes) {\n if (changes.length === 0) return\n const transaction = await connection.transaction(INDEX_DIRTY_STORE, 'readwrite')\n const store = transaction.objectStore(INDEX_DIRTY_STORE)\n // Queue every read while the transaction is synchronously active. The token\n // comparison below is the IndexedDB equivalent of compare-and-delete.\n const rows = await Promise.all(\n changes.map((change) =>\n requestResult(\n store.get(indexDirtyKey(change.docId)) as IDBRequest<\n IndexDirtyRow | undefined\n >,\n ),\n ),\n )\n rows.forEach((row, index) => {\n if (row?.token === changes[index]?.token) store.delete(row.key)\n })\n await transactionDone(transaction)\n },\n dispose() {\n connection.dispose()\n },\n }\n}\n\n/**\n * Remove both rebuildable state and recoverable unsent ciphertext after the caller has\n * explicitly forgotten, left, reset or deleted the graph.\n */\nexport async function deleteGraphCache(graphId: string): Promise<void> {\n const db = await openDb()\n try {\n const transaction = db.transaction(\n [DOC_STORE, OUTBOX_STORE, INDEX_DIRTY_STORE],\n 'readwrite',\n )\n const docPrefix = IDBKeyRange.bound(`${graphId}/`, `${graphId}/\\uffff`)\n transaction.objectStore(DOC_STORE).delete(docPrefix)\n transaction\n .objectStore(OUTBOX_STORE)\n .index(OUTBOX_GRAPH_INDEX)\n .openKeyCursor(graphRange(graphId)).onsuccess = (event) => {\n const cursor = (event.target as IDBRequest<IDBCursor | null>).result\n if (!cursor) return\n transaction.objectStore(OUTBOX_STORE).delete(cursor.primaryKey)\n cursor.continue()\n }\n transaction\n .objectStore(INDEX_DIRTY_STORE)\n .index(INDEX_DIRTY_GRAPH_INDEX)\n .openKeyCursor(graphRange(graphId)).onsuccess = (event) => {\n const cursor = (event.target as IDBRequest<IDBCursor | null>).result\n if (!cursor) return\n transaction.objectStore(INDEX_DIRTY_STORE).delete(cursor.primaryKey)\n cursor.continue()\n }\n await transactionDone(transaction)\n } finally {\n db.close()\n }\n}\n","/**\n * The relay socket for Node: the client's `TransportSocket` over the global `WebSocket` Node 22+\n * ships, so `graph-sync.ts` runs unchanged. The browser's `browserTransport` is the same five\n * lines over the same API; it is not imported here only because the client's module lives\n * beside code that reads `window`.\n */\n\nimport type { TransportSocket } from '$lib/sync/graph-sync'\n\nexport function nodeTransport(url: string): TransportSocket {\n const ws = new WebSocket(url)\n return {\n send: (data) => ws.send(data),\n close: () => ws.close(),\n onOpen: (cb) => ws.addEventListener('open', () => cb()),\n onMessage: (cb) => ws.addEventListener('message', (e) => cb(String((e as MessageEvent).data))),\n onClose: (cb) => ws.addEventListener('close', () => cb()),\n }\n}\n","/**\n * A synced graph's name lives in its root document's encrypted `meta` map (ADR 0031), so the\n * Sync Server's graph list carries ids only. Naming a graph on the command line, or listing\n * graphs by name, means opening each root document far enough to read the map - a bounded\n * relay catch-up on one small document per graph, then gone.\n */\n\nimport 'fake-indexeddb/auto'\n\nimport type { GraphKeyring } from '$lib/crypto'\nimport { createGraphSync, type TransportSocket } from '$lib/sync/graph-sync'\nimport { openGraphCache } from '$lib/sync/local-cache'\nimport type { SyncTokenSource } from '$lib/sync/sync-token'\n\nimport { nodeTransport } from './node-transport'\n\nexport interface GraphNameDeps {\n graphId: string\n rootDocId: string\n keyring: GraphKeyring\n relayUrl: string\n token: SyncTokenSource\n connect?: (url: string) => TransportSocket\n timeoutMs?: number\n}\n\n/** The graph's name, or null when it has none yet or the relay could not be reached in time. */\nexport async function readGraphName(deps: GraphNameDeps): Promise<string | null> {\n const cache = await openGraphCache(`${deps.graphId}-name`)\n const sync = createGraphSync({\n graphId: deps.graphId,\n rootDocId: deps.rootDocId,\n keyring: deps.keyring,\n relayUrl: deps.relayUrl,\n token: deps.token,\n cache,\n connect: deps.connect ?? nodeTransport,\n })\n try {\n const timeout = new Promise<'timeout'>((resolve) => setTimeout(() => resolve('timeout'), deps.timeoutMs ?? 10_000))\n const outcome = await Promise.race([sync.rootCaughtUp().then(() => 'ok' as const), timeout])\n if (outcome === 'timeout') return null\n return sync.getMeta().name ?? null\n } finally {\n sync.dispose()\n cache.dispose()\n }\n}\n","/**\n * The wikilink model: the pure data the parser yields and the segment queries the\n * editor decoration and publish renderer consume. No DOM, no CodeMirror, no storage.\n *\n * A wikilink yields only its **concept** (its identity); the on-disk file name and\n * the publish Slug are derived elsewhere (see `derive.ts`). See\n * docs/docs/technical/Wikilinks.md and ADR 0011.\n */\n\n/** One parsed wikilink (at any nesting level). Offsets are relative to the parsed string. */\nexport interface Wikilink {\n /** Full matched text, including the outer `[[` `]]`. */\n text: string\n /** Concept name: outer brackets stripped, inner brackets retained (scoped concepts). */\n concept: string\n /** Inclusive offset of the leading `[`. */\n start: number\n /** Inclusive offset of the trailing `]`. */\n end: number\n}\n\n/** A non-overlapping run of characters whose innermost covering wikilink is `wikilink`. */\nexport interface WikilinkSegment {\n /** Inclusive start offset. */\n start: number\n /** Exclusive end offset. */\n end: number\n wikilink: Wikilink\n}\n\n/** The concept for a matched `[[…]]` text: strip exactly the outer `[[` and `]]`. */\nexport function conceptOf(text: string): string {\n return text.slice(2, -2)\n}\n\n/**\n * The innermost (smallest) wikilink covering an offset, or undefined if none.\n * Maps a cursor/click position to the link it should act on.\n */\nexport function innermostWikilinkAt(links: Wikilink[], offset: number): Wikilink | undefined {\n let best: Wikilink | undefined\n for (const link of links) {\n if (offset < link.start || offset > link.end) continue\n if (!best || link.end - link.start < best.end - best.start) best = link\n }\n return best\n}\n\n/**\n * Split the covered span into non-overlapping, contiguous segments, each owned by the\n * innermost wikilink at that position. Gaps between sibling links (plain text) produce\n * no segment. End offsets are exclusive.\n */\nexport function wikilinkSegments(links: Wikilink[]): WikilinkSegment[] {\n if (links.length === 0) return []\n let min = Infinity\n let max = -Infinity\n for (const l of links) {\n if (l.start < min) min = l.start\n if (l.end > max) max = l.end\n }\n\n const segments: WikilinkSegment[] = []\n let current: WikilinkSegment | undefined\n for (let offset = min; offset <= max; offset++) {\n const inner = innermostWikilinkAt(links, offset)\n if (!inner) {\n current = undefined // a gap between siblings breaks the run\n continue\n }\n if (current && current.wikilink === inner) {\n current.end = offset + 1\n } else {\n current = { start: offset, end: offset + 1, wikilink: inner }\n segments.push(current)\n }\n }\n return segments\n}\n","/**\n * The wikilink parser: turns a string into all wikilinks at every nesting level,\n * in document order. Pure; offsets are relative to the input. Treats `[[` as an\n * open token and `]]` as a close token and matches them with a stack, so\n * `[[[[Physics]] Quantum Mechanics]]` yields the outer link and the inner one.\n *\n * Malformed input degrades gracefully: an unterminated `[[` or a stray `]]`\n * simply produces no link (see ADR 0011 — tolerance, not a feature).\n */\n\nimport { conceptOf, type Wikilink } from './model'\n\nexport function parseWikilinks(input: string): Wikilink[] {\n const links: Wikilink[] = []\n const openStarts: number[] = []\n let i = 0\n while (i < input.length) {\n if (input[i] === '[' && input[i + 1] === '[') {\n openStarts.push(i)\n i += 2\n continue\n }\n if (input[i] === ']' && input[i + 1] === ']') {\n const start = openStarts.pop()\n if (start !== undefined) {\n const end = i + 1 // inclusive offset of the second ']'\n const text = input.slice(start, end + 1)\n links.push({ text, concept: conceptOf(text), start, end })\n }\n i += 2\n continue\n }\n i += 1\n }\n // Document order. (Starts are always distinct; the secondary key is a stable tiebreak.)\n links.sort((a, b) => a.start - b.start || b.end - a.end)\n return links\n}\n","/**\n * Code-region detection for wikilink suppression. Rather than hand-roll a fence\n * scanner, we use `@lezer/markdown` (the framework-agnostic parser CodeMirror's\n * lang-markdown wraps) to report the ranges of code nodes; the wikilink scanner\n * then ignores any segment overlapping one. Pure and headless — the same rule\n * serves the editor, the index, and publish (ADR 0011, docs/.../Wikilinks.md).\n */\n\nimport { parser } from '@lezer/markdown'\n\n/** Node types whose ranges are code and must not contain wikilinks. */\nconst CODE_NODES = new Set([\n 'FencedCode', // ``` … ``` and ~~~ … ~~~ (incl. fences)\n 'CodeBlock', // indented code\n 'InlineCode', // `…` (incl. backticks)\n 'HTMLBlock',\n 'CommentBlock',\n 'Comment',\n])\n\nexport interface CodeRange {\n from: number\n to: number\n}\n\n/** The code ranges in `source`, as absolute `[from, to)` offsets. */\nexport function codeRanges(source: string): CodeRange[] {\n const tree = parser.parse(source)\n const ranges: CodeRange[] = []\n tree.iterate({\n enter: (node) => {\n if (CODE_NODES.has(node.name)) ranges.push({ from: node.from, to: node.to })\n },\n })\n return ranges\n}\n\n/** Whether the half-open range `[from, to)` overlaps any code range. */\nexport function isInCode(ranges: readonly CodeRange[], from: number, to: number): boolean {\n return ranges.some((r) => from < r.to && to > r.from)\n}\n","/**\n * Document-level wikilink parsing: the integration point the editor decoration,\n * the backlink index, and publish all use. Parses each line (wikilinks never span\n * lines), maps offsets to absolute document positions, and drops any segment that\n * falls inside a code range (`@lezer/markdown`). Pure.\n */\n\nimport { codeRanges, isInCode, type CodeRange } from './code-ranges'\nimport { type WikilinkSegment, wikilinkSegments } from './model'\nimport { parseWikilinks } from './parser'\n\n/** One wikilink occurrence located in a document — what the backlink index consumes. */\nexport interface WikilinkOccurrence {\n /** The link's concept (outer brackets stripped; inner brackets kept for scoped concepts). */\n concept: string\n /** 0-based line the link appears on. */\n line: number\n /** The full text of that line (the backlink context). */\n lineText: string\n /** Column of the opening `[` within the line. */\n matchStart: number\n /** Column just past the closing `]` within the line (exclusive). */\n matchEnd: number\n}\n\n/**\n * Every wikilink occurrence in `source` — including links nested inside others —\n * with code-suppressed links dropped. Unlike {@link wikilinkSegmentsInSource}\n * (which merges into rendering segments), this yields one entry per `[[…]]` so the\n * backlink index can record each reference. Pure.\n */\nexport function wikilinkOccurrencesInSource(source: string): WikilinkOccurrence[] {\n const ranges = codeRanges(source)\n const out: WikilinkOccurrence[] = []\n const lines = source.split('\\n')\n let lineStart = 0\n for (let line = 0; line < lines.length; line++) {\n const lineText = lines[line]\n for (const wl of parseWikilinks(lineText)) {\n const absStart = wl.start + lineStart\n const absEnd = wl.end + lineStart\n if (isInCode(ranges, absStart, absEnd)) continue\n out.push({\n concept: wl.concept,\n line,\n lineText,\n matchStart: wl.start,\n matchEnd: wl.end + 1, // wl.end is the inclusive trailing ']'\n })\n }\n lineStart += lineText.length + 1 // +1 for the consumed newline\n }\n return out\n}\n\n/**\n * All wikilink segments in `source`, in absolute offsets, with links inside code\n * (fences / inline code) suppressed. Segments are returned in ascending start order.\n */\nexport function wikilinkSegmentsInSource(\n source: string,\n ranges: readonly CodeRange[] = codeRanges(source),\n): WikilinkSegment[] {\n const out: WikilinkSegment[] = []\n let lineStart = 0\n for (const line of source.split('\\n')) {\n for (const seg of wikilinkSegments(parseWikilinks(line))) {\n const start = seg.start + lineStart\n const end = seg.end + lineStart\n if (isInCode(ranges, start, end)) continue\n const wl = seg.wikilink\n out.push({\n start,\n end,\n wikilink: { ...wl, start: wl.start + lineStart, end: wl.end + lineStart },\n })\n }\n lineStart += line.length + 1 // +1 for the consumed newline\n }\n return out\n}\n","/**\n * The pure backlink (linked-references) index — Logseq-style. Built from a snapshot\n * of every document's body, it answers \"which documents reference this concept?\",\n * folding aliases into the canonical concept and matching case-insensitively\n * (ADR 0011). Browser-private and fully rebuildable from the markdown (DESIGN.md →\n * Derived state is browser-private), so a simple in-memory build is the source of\n * truth; a SQLite-WASM/OPFS persistence layer is a later optimisation.\n *\n * Inspired by the AS Notes index (concept→sources, alias-folding, case-insensitive\n * filename match). EtherPK keeps the line of source as the reference context; the\n * AS Notes outline-chain enrichment (grouping references by their indentation\n * ancestry) is a noted follow-on.\n */\n\nimport type { DocumentKind } from '$lib/storage'\n\nimport { wikilinkOccurrencesInSource } from '../wikilink'\n\n/** Case-insensitive concept identity key (mirrors storage identity / ADR 0011). */\nexport function conceptKey(concept: string): string {\n return concept.toLowerCase()\n}\n\n/** One document fed into the index: identity, aliases, and the body to scan. */\nexport interface IndexDoc {\n concept: string\n kind: DocumentKind\n /** Alternate names declared in frontmatter (`aliases:`). */\n aliases: string[]\n /** The document body (frontmatter stripped) to scan for wikilinks. */\n text: string\n}\n\n/** One reference to a concept, located in a source document. */\nexport interface BacklinkRef {\n sourceConcept: string\n sourceKind: DocumentKind\n /** 0-based line in the source body. */\n line: number\n /** The source line (the reference's context). */\n lineText: string\n /** Column range of the `[[…]]` within `lineText` (for highlighting). */\n matchStart: number\n matchEnd: number\n}\n\n/** References to one source document, in line order. */\nexport interface BacklinkGroup {\n sourceConcept: string\n sourceKind: DocumentKind\n refs: BacklinkRef[]\n}\n\nexport interface BacklinkIndex {\n /** conceptKey(link concept) → every reference to it. */\n byTarget: Map<string, BacklinkRef[]>\n /** aliasKey → canonical conceptKey. */\n aliasToCanonical: Map<string, string>\n /** Every key that resolves to a real document (canonical concept or alias). */\n existing: Set<string>\n /** key → a display concept name (canonical casing as authored). */\n display: Map<string, string>\n}\n\nexport function buildBacklinkIndex(docs: Iterable<IndexDoc>): BacklinkIndex {\n const byTarget = new Map<string, BacklinkRef[]>()\n const aliasToCanonical = new Map<string, string>()\n const existing = new Set<string>()\n const display = new Map<string, string>()\n\n // 1) Register identities + aliases first, so resolution sees the whole graph.\n for (const doc of docs) {\n const ck = conceptKey(doc.concept)\n existing.add(ck)\n display.set(ck, doc.concept)\n for (const alias of doc.aliases) {\n const ak = conceptKey(alias)\n existing.add(ak)\n aliasToCanonical.set(ak, ck)\n if (!display.has(ak)) display.set(ak, alias)\n }\n }\n\n // 2) Record every wikilink occurrence as a backlink against its concept key.\n for (const doc of docs) {\n for (const occ of wikilinkOccurrencesInSource(doc.text)) {\n const tk = conceptKey(occ.concept)\n const ref: BacklinkRef = {\n sourceConcept: doc.concept,\n sourceKind: doc.kind,\n line: occ.line,\n lineText: occ.lineText,\n matchStart: occ.matchStart,\n matchEnd: occ.matchEnd,\n }\n const list = byTarget.get(tk)\n if (list) list.push(ref)\n else byTarget.set(tk, [ref])\n }\n }\n\n return { byTarget, aliasToCanonical, existing, display }\n}\n\n/** True when the concept resolves to a real document (by canonical name or alias). */\nexport function conceptExists(index: BacklinkIndex, concept: string): boolean {\n return index.existing.has(conceptKey(concept))\n}\n\n/** The canonical concept key for a concept (following an alias to its page). */\nexport function canonicalKey(index: BacklinkIndex, concept: string): string {\n const key = conceptKey(concept)\n return index.aliasToCanonical.get(key) ?? key\n}\n\n/** Every key that targets the same canonical concept (the canonical key + its aliases). */\nfunction namesForCanonical(index: BacklinkIndex, canonical: string): Set<string> {\n const names = new Set<string>([canonical])\n for (const [alias, c] of index.aliasToCanonical) if (c === canonical) names.add(alias)\n return names\n}\n\n/**\n * All references to a concept — pooling the concept's own name and any of its\n * aliases (Logseq/AS Notes alias-folding), case-insensitively. Returned grouped by\n * source document: journals first (most-recent date first), then pages\n * alphabetically; references within a document in line order.\n */\nexport function backlinksFor(index: BacklinkIndex, concept: string): BacklinkGroup[] {\n const canonical = canonicalKey(index, concept)\n const names = namesForCanonical(index, canonical)\n\n const refs: BacklinkRef[] = []\n for (const name of names) refs.push(...(index.byTarget.get(name) ?? []))\n\n // Group by source document.\n const groups = new Map<string, BacklinkGroup>()\n for (const ref of refs) {\n let group = groups.get(ref.sourceConcept)\n if (!group) {\n group = { sourceConcept: ref.sourceConcept, sourceKind: ref.sourceKind, refs: [] }\n groups.set(ref.sourceConcept, group)\n }\n group.refs.push(ref)\n }\n for (const group of groups.values()) {\n group.refs.sort((a, b) => a.line - b.line || a.matchStart - b.matchStart)\n }\n\n return [...groups.values()].sort(compareGroups)\n}\n\n/** Journals first (date desc), then pages (concept asc, case-insensitive). */\nfunction compareGroups(a: BacklinkGroup, b: BacklinkGroup): number {\n if (a.sourceKind !== b.sourceKind) return a.sourceKind === 'journal' ? -1 : 1\n if (a.sourceKind === 'journal') return b.sourceConcept.localeCompare(a.sourceConcept)\n return conceptKey(a.sourceConcept).localeCompare(conceptKey(b.sourceConcept))\n}\n\n/** Total reference count across all groups (for the view's summary). */\nexport function backlinkCount(groups: BacklinkGroup[]): number {\n return groups.reduce((n, g) => n + g.refs.length, 0)\n}\n","/**\n * Adapter from the `@sqlite.org/sqlite-wasm` oo1 DB to the engine-agnostic\n * {@link SqlDb} the index query layer uses. Shared by node tests (in-memory) and the\n * browser runtime (OPFS). Keeping the SQL surface tiny is what lets the query layer\n * (index-db.ts) be node-tested without a browser.\n */\n\nimport { createSchema, type SqlDb } from './index-db'\n\n/**\n * The oo1 `Database` (and `:memory:` constructor) typed loosely at this single\n * boundary — the package's overloaded `exec` signatures are awkward to satisfy\n * structurally, and this adapter is the only place that touches them.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype Oo1Db = any\n\n/**\n * Open a fresh in-memory SQLite DB with the index schema. sqlite-wasm is loaded\n * dynamically so it is pulled into the bundle only when a graph actually opens (not\n * by every module that transitively imports the document barrel).\n *\n * NOTE: this is the **main-thread, in-memory** runtime — rebuilt from documents each\n * session, exactly like the in-memory index it replaces. OPFS/worker *persistence*\n * (ADR 0015) is the remaining runtime step; the schema, derivation, and queries are\n * identical regardless of where the DB physically lives.\n */\nexport async function openInMemorySqlDb(): Promise<SqlDb> {\n const { default: sqlite3InitModule } = await import('@sqlite.org/sqlite-wasm')\n const sqlite3 = await sqlite3InitModule()\n const db = wrapOo1Db(new sqlite3.oo1.DB(':memory:') as Oo1Db)\n createSchema(db)\n return db\n}\n\nexport function wrapOo1Db(db: Oo1Db): SqlDb {\n let closed = false\n // Overwrite freed pages with zeros. A document's rows leave the index when it is protected,\n // but without this the pages they sat on keep the text until reused - readable from the\n // OPFS file by anyone with the profile directory, which is the adversary protection is for.\n db.exec('PRAGMA secure_delete = ON')\n return {\n exec(sql) {\n db.exec(sql)\n },\n run(sql, params = []) {\n db.exec({ sql, bind: params as unknown[] })\n },\n all<T = Record<string, unknown>>(sql: string, params: unknown[] = []) {\n const resultRows: unknown[] = []\n db.exec({ sql, bind: params, rowMode: 'object', resultRows })\n return resultRows as T[]\n },\n close() {\n if (closed) return\n closed = true\n db.close()\n },\n }\n}\n","/**\n * The one rule for what counts as [[Frontmatter]] (CONTEXT.md).\n *\n * There were three, and they disagreed. `parseFrontmatter` and `frontmatterLineOffset` closed on\n * `---` only and treated an unterminated opener as no frontmatter; the editor's analysis also\n * accepted `...`, tolerated trailing whitespace on a delimiter, and treated an unterminated\n * opener as frontmatter running to the END of the document. A file closed with `...` was\n * therefore frontmatter to the editor's completion guards and plain body to storage - its\n * `title:` never read, so the document's identity silently came from its file name instead.\n *\n * The rule, stated once:\n *\n * - A line of exactly `---` at offset 0, trailing spaces or tabs allowed.\n * - Closed by the next line of exactly `---`, same allowance. **Only** `---`: `...` is a YAML\n * document terminator, but no tool this format meets writes it (Jekyll, Obsidian, Logseq,\n * AS Notes), and accepting it suppressed editor completions across whole documents on the\n * strength of a terminator nobody types.\n * - An **unterminated** opener is not Frontmatter. It is plain text until the document balances -\n * the same rule `editor-analysis.ts` applies to a freshly typed ``` fence, and for the same\n * reason: otherwise typing `---` restyles everything below it while you are still typing.\n * - CRLF tolerated throughout.\n *\n * Pure string work, no parser and no editor: the storage parse, the editor's analysis and the\n * reveal offset all measure from this, so they cannot drift apart again.\n */\n\n/** Where a document's Frontmatter sits. Offsets are into the whole document text. */\nexport interface FrontmatterSpan {\n /** Offset just past the closing delimiter's line terminator (or the end of the text). */\n end: number\n /** Lines the block occupies, both delimiters included — what a body-relative offset needs. */\n lines: number\n /** The YAML between the delimiters, terminators excluded. */\n body: string\n /** Offsets of {@link body} within the document. */\n bodyFrom: number\n bodyTo: number\n}\n\n/** A line that is exactly a `---` delimiter (trailing spaces and tabs allowed). */\nconst DELIMITER = /^---[ \\t]*\\r?$/\n\n/** Whether `line` is a delimiter line: exactly `---`, trailing spaces or tabs allowed. */\nexport function isFrontmatterDelimiter(line: string): boolean {\n return DELIMITER.test(line)\n}\n\n/** The Frontmatter of `text`, or `null` when it has none — including an unterminated opener. */\nexport function frontmatterSpan(text: string): FrontmatterSpan | null {\n const firstBreak = text.indexOf('\\n')\n // A document that is only `---` has no closing delimiter, so it is not Frontmatter.\n if (firstBreak === -1 || !DELIMITER.test(text.slice(0, firstBreak))) return null\n\n const bodyFrom = firstBreak + 1\n let lineStart = bodyFrom\n let lines = 1\n while (lineStart <= text.length) {\n const nextBreak = text.indexOf('\\n', lineStart)\n const lineEnd = nextBreak === -1 ? text.length : nextBreak\n lines += 1\n if (DELIMITER.test(text.slice(lineStart, lineEnd))) {\n // The body stops before the newline that begins the closing delimiter's line - and\n // before that line's `\\r` under CRLF, which belongs to the terminator, not the YAML.\n let bodyTo = Math.max(bodyFrom, lineStart - 1)\n if (bodyTo > bodyFrom && text[bodyTo - 1] === '\\r') bodyTo -= 1\n return {\n end: nextBreak === -1 ? text.length : nextBreak + 1,\n lines,\n body: text.slice(bodyFrom, bodyTo),\n bodyFrom,\n bodyTo,\n }\n }\n if (nextBreak === -1) break\n lineStart = nextBreak + 1\n }\n return null // unterminated: plain text until the document balances\n}\n\n/**\n * How many lines a document's [[Frontmatter]] occupies - opener and closer included - given the\n * document already split into lines, or 0 when it has none. The same rule as {@link frontmatterSpan}\n * (a lone `---` is not a block; an unterminated one is not a block), for the line-shaped consumers\n * in the editor that treat the block as opaque: the outliner's scans, the bullet dots, the guides,\n * the clamp. Those already hold `lines`, and re-joining them per keystroke to ask the text form\n * would be the only O(n) step in an otherwise line-local pass.\n */\nexport function frontmatterLines(lines: readonly string[]): number {\n if (lines.length < 2 || !DELIMITER.test(lines[0])) return 0\n for (let i = 1; i < lines.length; i++) {\n if (DELIMITER.test(lines[i])) return i + 1\n }\n return 0\n}\n","/**\n * Pure logic for [[Fenced Code Block]]s (ADR 0018, ADR 0020). No CodeMirror imports — the\n * syntax-tree / view glue lives in `view/outliner-context.ts` and `view/fence-guard.ts`.\n *\n * A fence is a run of three or more backticks; the run length is mirrored on completion so\n * a 4-backtick fence balances with 4 (lets a 3-backtick block sit inside one) — and ONLY with\n * 4: a closer is exactly the opener's length, never merely \"at least\" it as CommonMark allows,\n * so a longer opener below can never be taken as a shorter block's closer (ADR 0020, amended\n * 2026-09-12). The optional info-string after the opener selects the language.\n */\n\n/** A clean opener line is exactly a backtick run (3+) and an optional info-string. */\nconst FENCE_OPENER = /^(`{3,})\\s*([\\w+#.-]*)\\s*$/\n/** A fence line (opener or closer): a bare backtick/tilde run. */\nconst FENCE_LINE = /^(`{3,}|~{3,})\\s*$/\n/** A backtick fence line, tolerant of a leading plain-bullet marker (`- `) so `- ``` ` is a fence.\n * Backticks only (this slice) — tildes are left untouched, so they are not auto-completed. */\nconst FENCE_LINE_INFO = /^(\\s*(?:-\\s)?)(`{3,})\\s*(\\S*)\\s*$/\n\nexport interface FenceInfo {\n /** Column where the backtick/tilde run starts (after any indent + bullet marker) — the clamp edge. */\n col: number\n /** The full fence run, e.g. ` ``` ` or ` ```` `. */\n run: string\n /** The fence character: `` ` `` or `~`. */\n char: string\n /** Length of the run (3+). */\n len: number\n /** The info-string after the run; `''` for a bare fence (a closer is always bare). */\n info: string\n /** The fence sits on a bullet line (`- ``` `) — so it OPENS a block (form-1) and can never be a\n * closer: a sibling bullet's opener directly below must not be mistaken for this one's closer. */\n bullet: boolean\n}\n\n/** Parse a line as a fence line (opener or closer), tolerant of a leading `- ` bullet. Null if not. */\nexport function fenceLineInfo(line: string): FenceInfo | null {\n const m = FENCE_LINE_INFO.exec(line)\n if (!m) return null\n return { col: m[1].length, run: m[2], char: m[2][0], len: m[2].length, info: m[3], bullet: m[1].includes('-') }\n}\n\n/** Whether `lines[a]` would close an open fence `b` (same char, run of the SAME length, no info, and not\n * itself a bullet-line opener). */\nfunction closes(a: FenceInfo, open: FenceInfo): boolean {\n return a.info === '' && !a.bullet && a.char === open.char && a.len === open.len\n}\n\nexport interface FencedBlockRange {\n /** 0-based line index of the opening fence. */\n start: number\n /** 0-based line index of the closing fence. */\n end: number\n /** Column where the fence/content sits (the clamp edge). */\n fenceColumn: number\n}\n\n/**\n * The **complete** fenced blocks (opener paired with a closer) by line index — **balance-aware** and\n * **outliner-block-scoped**, so styling never bleeds past where a block actually ends. An unterminated\n * opener is omitted entirely (its region stays unstyled until a closer completes it).\n *\n * Pairing (a fence run is 3+ backticks; tildes are not handled this slice):\n * - A **closer** is a BARE fence (no info-string) at the SAME column as its opener and of the SAME\n * length. Each closer is consumed once — it can't also close a fence higher up. Exact length\n * (not CommonMark's \"at least\") is what keeps a ```` block below a stray ``` intact: the app\n * mirrors lengths on completion, so nothing legitimate is lost, and a longer opener is never eaten.\n * - A fence with an **info-string** is unambiguously an OPENER, never a closer: an unterminated opener\n * therefore can't swallow the start of an already-completed block below it (the reported bug).\n * - Pairing is **column-scoped** (one outliner block). A deeper fence (greater column) opens a nested\n * block that completes independently; a non-blank line that **dedents below** an open fence's column\n * leaves that block, so the fence can no longer be closed (its closer must live in the same block);\n * a same-column fence that is LONGER than an unterminated opener — bare or not — or an equal-length\n * info opener *replaces* it instead of nesting: a longer run can never be a shorter block's content\n * (CommonMark would close the block with it), so it is the next block's opener, and the shorter\n * opener above it was a stray. That is what keeps a ```` block whole beneath a dangling ```.\n * - A shorter same-column fence is literal content (e.g. a ``` block inside a ```` one). To hold a\n * fence as content, the outer fence must be the longer one — the CommonMark convention.\n *\n * `closerTolerance` (default 0 — strict) lets a bare closer sit up to N columns RIGHT of its opener\n * and still pair. Only the source hard-guard uses it (with CommonMark's 3), to recognise a closer\n * that a raw edit/paste nudged right so it can snap it back; every other consumer stays col-exact.\n */\nexport function fencedBlocks(lines: readonly string[], closerTolerance = 0): FencedBlockRange[] {\n const blocks: FencedBlockRange[] = []\n const stack: { info: FenceInfo; index: number }[] = []\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i]\n if (line.trim() !== '') {\n // A non-blank line below an open fence's column has left that fence's block — abandon it.\n const indent = line.length - line.trimStart().length\n while (stack.length && stack[stack.length - 1].info.col > indent) stack.pop()\n }\n const f = fenceLineInfo(line)\n if (!f) continue\n const top = stack.length ? stack[stack.length - 1] : null\n if (top && f.col >= top.info.col && f.col - top.info.col <= closerTolerance && closes(f, top.info)) {\n stack.pop()\n blocks.push({ start: top.index, end: i, fenceColumn: top.info.col })\n } else if (!top || f.col > top.info.col) {\n stack.push({ info: f, index: i }) // a fresh opener, or a nested block in a deeper column\n } else if (f.col === top.info.col && (f.len > top.info.len || (f.info !== '' && f.len === top.info.len))) {\n stack.pop() // an unterminated opener can't contain a longer fence, or an info opener, at its column\n stack.push({ info: f, index: i })\n }\n // else: a shorter same-column fence, or a bare fence that closes nothing — literal content, ignore.\n }\n return blocks\n}\n\n/** A fence line with its backtick/tilde run defanged (indent and bullet marker kept), so a re-scan\n * pairs the other fences as if this one were plain text. */\nexport function defangFence(line: string): string {\n return line.replace(/[`~]+/, 'x')\n}\n\n/**\n * Whether the fence line at `index` is an **unterminated opener** — the one shape Enter completes.\n * This is fence completion's balance check. A fence a complete block already accounts for — its\n * opener, whether the block is empty or holds content, its closer, or a shorter fence that is its\n * content — is never completed again: doing so grew a second closer under the opener and orphaned the\n * real one (found live: Enter at the end of a content-bearing block's opener). Pairing is the\n * column-scoped scan ({@link fencedBlocks}), so stray or orphaned fences elsewhere in the document\n * never make a fresh opener look paired.\n *\n * One refinement for the moment of typing. Pairing is first-with-next, so an opener typed above an\n * existing block swallows it: a prose opener takes the block's bare opener as its closer, and a bullet\n * opener (which no bullet fence can close) runs on to the block's own closer. Setting the candidate\n * aside and re-pairing tells the two apart by TALLY, counting complete blocks from the candidate down:\n * a genuine opener's block dissolves, so the tally drops — even when its orphaned closer then heads a\n * block of its own by taking the next equal-length opener below, that re-pairing runs one fence\n * short — whereas a fresh opener that had swallowed the block below leaves\n * the tally intact, because setting it aside merely hands that block its own opener back. Asking\n * instead whether any block *starts inside* the candidate's conflated the two (live, 2026-09-11: Enter\n * on the opener of a complete block grew a second closer whenever an equal block followed).\n * Blocks starting above the candidate are left out of the tally: a stray opener up there adopting the\n * candidate's closer says nothing about the candidate. `pending` — the analysis's freshly typed,\n * still unclosed fence — is set aside the same way, as every other fence consumer does, and is itself\n * unterminated by definition.\n */\nexport function isUnterminatedOpener(lines: readonly string[], index: number, pending: number | null = null): boolean {\n if (!fenceLineInfo(lines[index])) return false\n if (pending === index) return true\n const scanned = pending === null ? lines : lines.map((l, i) => (i === pending ? defangFence(l) : l))\n const paired = fencedBlocks(scanned)\n const block = paired.find((b) => b.start <= index && index <= b.end)\n if (!block) return true // pairs with nothing\n if (block.start !== index) return false // its closer, or a shorter fence that is content\n const without = fencedBlocks(scanned.map((l, i) => (i === index ? defangFence(l) : l)))\n const tally = (blocks: readonly FencedBlockRange[], from: number) => blocks.filter((b) => b.start >= from).length\n return tally(without, index + 1) >= tally(paired, index)\n}\n\n/** Parse a line as a fence opener; null if it is not a clean opener. Leading indent is ignored. */\nexport function parseFenceOpener(line: string): { ticks: string; info: string } | null {\n const m = FENCE_OPENER.exec(line.trimStart())\n if (!m) return null\n return { ticks: m[1], info: m[2] }\n}\n\nexport interface FenceCompletion {\n /** Text to insert at the end of the opener line (which already holds the indent + ticks). */\n insert: string\n /** Caret position as a character offset into {@link insert}. */\n caretOffset: number\n}\n\n/**\n * Build the text that completes a fence on Enter: the (optional) default info-string to finish\n * the opener line, an empty middle line, and a balanced closer — all at `indent` (the content\n * column for a bullet, `''` for prose). Assumes the caret is at the end of the opener line.\n */\nexport function buildFenceCompletion(opts: {\n indent: string\n ticks: string\n info: string\n defaultLang: string\n}): FenceCompletion {\n const { indent, ticks, info, defaultLang } = opts\n const head = info === '' ? defaultLang : '' // finish the opener line with the default lang when bare\n const insert = `${head}\\n${indent}\\n${indent}${ticks}`\n return { insert, caretOffset: `${head}\\n${indent}`.length }\n}\n\n/**\n * Normalise a fenced block's lines so it stays valid markdown (ADR 0020 hard guard): pad any\n * under-indented content line up to `fenceColumn`, and clamp + rebalance the **closing** fence\n * (the last line, when it is a bare fence) to `fenceColumn` + the opener's tick count. The opener\n * (line 0) is left untouched, as are blank lines and any fence-looking *content* lines (so a\n * 3-backtick block can live inside a 4-backtick one).\n */\nexport function normaliseFenceLines(lines: string[], fenceColumn: number, openerTicks: string): string[] {\n const pad = ' '.repeat(fenceColumn)\n const last = lines.length - 1\n return lines.map((line, i) => {\n if (i === 0) return line\n const trimmed = line.trimStart()\n if (i === last && FENCE_LINE.test(trimmed)) return pad + openerTicks\n if (trimmed === '') return line\n const indent = line.length - trimmed.length\n return indent < fenceColumn ? pad + trimmed : line\n })\n}\n","/**\n * The **Indent Unit** (CONTEXT.md; ADR 0067): one level of [[Block]] nesting is two spaces in the\n * markdown source, for every knowledge graph. There is no per-graph setting - [[Import]] and paste\n * normalise foreign text onto the grid, the editor writes it, and text on any other grid (a\n * four-space vault, a tab-indented Logseq page, a file edited in another tool) is read\n * **structurally**: a bullet is a child of the nearest open bullet with a strictly smaller indent\n * and a sibling of one with an equal indent. That one walk is shared by the normaliser, the block\n * model and the editor's per-line depth, so import, index and rendering can never disagree about\n * what is a child of what.\n *\n * Only `-` bullets are nodes here, as everywhere in EtherPK. Fenced code and [[Frontmatter]] are\n * opaque: a `- item` inside them is code or YAML, and a fenced block travels whole with the bullet\n * that owns it.\n *\n * Two measures of leading whitespace, one per side of the boundary. The **normaliser** measures\n * **columns**, a tab advancing to the next multiple of four as CommonMark reads it — that is what\n * makes a tab-indented Logseq page or a mixed vault come out right. The **editor and the block\n * model** measure **characters**, because everything else in the editor does (the guides, the\n * keymap's branch and sibling primitives, the caret clamp, a fence's column): one measure inside\n * the editor is what keeps the clamp, the guides and the keys reading the same tree from the same\n * text, and text that has been through the boundary holds no tabs, so the two measures agree on\n * it. A tab-indented file that reaches the editor unnormalised is read as a consistent\n * one-character grid.\n */\n\nimport { frontmatterLines } from '$lib/storage/fs/frontmatter-span'\n\nimport { type FencedBlockRange, fencedBlocks } from './fenced-code'\n\n/** Spaces per nesting level in the markdown source. */\nexport const INDENT_UNIT = 2\n\n/** The Indent Unit as text - what Tab inserts. */\nexport const INDENT = ' '.repeat(INDENT_UNIT)\n\n/** Width of the `- ` marker, which sets a bullet's content column (`indent + MARKER_WIDTH`). */\nconst MARKER_WIDTH = 2\n\n/** A tab advances to the next multiple of this column, as CommonMark reads it. */\nconst TAB_STOP = 4\n\n/** Whether the line is a `- ` bullet or task, tolerant of leading whitespace of either kind. */\nfunction isBullet(line: string): boolean {\n return /^[ \\t]*-\\s/.test(line)\n}\n\n/** A heading line, tolerant of leading whitespace — a node in its own right, never a continuation. */\nfunction isHeading(line: string): boolean {\n return /^[ \\t]*#{1,6}\\s/.test(line)\n}\n\n/** Whether the line is whitespace-only (an empty line included). */\nfunction isBlank(line: string): boolean {\n return line.trim() === ''\n}\n\n/** The leading spaces and tabs of a line. */\nfunction leading(line: string): string {\n return /^[ \\t]*/.exec(line)![0]\n}\n\n/** The width of a line's leading whitespace in columns, tabs advancing to the next tab stop. */\nexport function indentColumns(line: string): number {\n let col = 0\n for (const ch of leading(line)) col = ch === '\\t' ? col + TAB_STOP - (col % TAB_STOP) : col + 1\n return col\n}\n\n/** The width of a line's leading whitespace in characters — the editor's measure (module comment). */\nfunction indentChars(line: string): number {\n return leading(line).length\n}\n\n/** How a walk measures leading whitespace. */\ntype Measure = (line: string) => number\n\n/** One line's place in the outline, as the walk reads it. */\nexport interface OutlineLine {\n /**\n * Nesting depth of the line's node, or of the node that owns it. A top-level bullet or prose\n * line is 0; a continuation line, a fenced code line and an indented soft line take their\n * owner's depth. Lines outside any node (a bare blank line, frontmatter) are 0.\n */\n depth: number\n /** 0-based index of the **bullet** whose block this line belongs to (a bullet owns itself), or -1. */\n owner: number\n}\n\ninterface Node {\n /** The raw indent this node was read at, in columns. */\n raw: number\n /** The indent it is written at once normalised. */\n written: number\n depth: number\n /** Line index of the node's bullet, or -1 for a prose node. */\n bullet: number\n}\n\ninterface Visit {\n depth: number\n owner: number\n /** Columns to shift the line (and, for a fenced block, every line of it) onto the grid. */\n delta: number\n /** Lines this visit covers: a fenced block's whole extent from its opener, else 1. */\n span: number\n}\n\n/**\n * The structural walk under both {@link outlineLines} and {@link normaliseIndentUnit}: visits each\n * line in order with the node stack resolved, reporting the line's owner (the nearest open bullet\n * whose block the line is inside) and the delta that would put it on the grid.\n *\n * Nodes are bullets and **unowned** non-blank lines (prose, headings): a deeper plain line nests\n * under the plain line before it, as the block model has always read it, but a plain line never\n * owns a fenced block or a soft line - only a bullet does. A non-bullet, non-heading line that sits\n * at or past the nearest open bullet's content column is that bullet's [[Continuation Line]] and is\n * not a node; a heading is always a node, as the outliner's boundary rules read it.\n * A blank line pops what it is shallower than and belongs to what remains, so an indented soft line\n * stays inside its block while a flush-left empty line closes every open branch (indent decides,\n * ADR 0021). Fenced blocks are visited as one unit at the opener; frontmatter lines are visited as\n * depth-0 lines owned by nothing.\n */\nfunction walk(lines: readonly string[], blocks: readonly FencedBlockRange[], measure: Measure, visit: (index: number, info: Visit) => void): void {\n const starts = new Map<number, FencedBlockRange>()\n for (const b of blocks) starts.set(b.start, b)\n const stack: Node[] = []\n const top = (): Node | null => (stack.length ? stack[stack.length - 1] : null)\n const popTo = (indent: number) => {\n while (stack.length && stack[stack.length - 1].raw >= indent) stack.pop()\n }\n /** The nearest open bullet on the stack, or null when only prose nodes (or nothing) remain. */\n const owningBullet = (): Node | null => {\n for (let k = stack.length - 1; k >= 0; k--) if (stack[k].bullet >= 0) return stack[k]\n return null\n }\n const first = frontmatterLines(lines)\n for (let i = 0; i < first; i++) visit(i, { depth: 0, owner: -1, delta: 0, span: 1 })\n for (let i = first; i < lines.length; i++) {\n const line = lines[i]\n const block = starts.get(i)\n const span = block ? block.end - i + 1 : 1\n const indent = measure(line)\n popTo(indent)\n if (isBlank(line) && !block) {\n // Indent decides: what this blank is not shallower than owns it.\n const owner = owningBullet()\n visit(i, { depth: top()?.depth ?? 0, owner: owner ? owner.bullet : -1, delta: owner ? owner.written - owner.raw : 0, span })\n continue\n }\n if (isBullet(line)) {\n const parent = top()\n const node: Node = { raw: indent, written: parent ? parent.written + INDENT_UNIT : 0, depth: stack.length, bullet: i }\n stack.push(node)\n visit(i, { depth: node.depth, owner: i, delta: node.written - node.raw, span })\n i += span - 1 // a form-1 opener's fence lines travel with the bullet\n continue\n }\n const parent = top()\n if (parent && parent.bullet >= 0 && indent >= parent.raw + MARKER_WIDTH && !isHeading(line)) {\n // A continuation line (a soft line, a form-2 fenced block) inside the bullet's block. A heading\n // is never one: the outliner reads it as a boundary wherever it sits, and so does this walk.\n visit(i, { depth: parent.depth, owner: parent.bullet, delta: parent.written - parent.raw, span })\n i += span - 1\n continue\n }\n if (block) {\n // A fenced block owned by no bullet (top-level code, code under a prose line) is verbatim\n // text, not a node, and is never moved.\n visit(i, { depth: parent ? parent.depth + 1 : 0, owner: -1, delta: 0, span })\n i += span - 1\n continue\n }\n // A prose node: written where it was read - a plain line's indent is never rewritten, since\n // at four columns outside a list it is an indented code block in CommonMark.\n const node: Node = { raw: indent, written: indent, depth: stack.length, bullet: -1 }\n stack.push(node)\n visit(i, { depth: node.depth, owner: -1, delta: 0, span })\n }\n}\n\n/**\n * Per-line depth and owner for every line of the document ({@link OutlineLine}), measured in\n * characters as the editor and the block model measure (module comment).\n */\nexport function outlineLines(lines: readonly string[], blocks: readonly FencedBlockRange[] = fencedBlocks(lines)): OutlineLine[] {\n const out = new Array<OutlineLine>(lines.length)\n walk(lines, blocks, indentChars, (i, { depth, owner, span }) => {\n for (let k = i; k < i + span; k++) out[k] = { depth, owner }\n })\n return out\n}\n\n/**\n * Rewrite a line's leading whitespace: the part of it lying within the first `within` columns is\n * re-measured (a tab advancing to its stop) and written as spaces, shifted by `delta` and floored\n * at column 0; whatever leading whitespace lies beyond `within` is kept verbatim. Every line\n * outside a fence passes `Infinity`: its whole indent becomes columns. Inside a fenced block\n * `within` is the fence column, so the structural indent up to the fence is put in spaces (a\n * fence pairs only with a closer at its own column, and a moved block must keep pairing) while a\n * tab in the code itself, past the fence column, stays a tab.\n */\nfunction relead(line: string, within: number, delta: number): string {\n let col = 0\n let chars = 0\n for (const ch of leading(line)) {\n if (col >= within) break\n col = ch === '\\t' ? col + TAB_STOP - (col % TAB_STOP) : col + 1\n chars++\n }\n return ' '.repeat(Math.max(0, col + delta)) + line.slice(chars)\n}\n\n/**\n * The document with every bullet on the Indent Unit grid: each bullet one unit under its parent\n * (a top-level bullet at column 0, over-nesting collapsed), its continuation lines, soft lines and\n * fenced block moved by the same amount so their indentation beyond the content column is kept\n * (an indented code block inside a bullet stays code). Tabs in leading whitespace become columns\n * (inside a fenced block only up to the fence column: a tab in the code is code); a line owned\n * by no bullet keeps its indent otherwise. Frontmatter is never touched. A document already on\n * the grid comes back byte-identical, so callers may compare by reference.\n */\nexport function normaliseIndentUnit(text: string): string {\n const lines = text.split('\\n')\n const out = lines.slice()\n const frontmatter = frontmatterLines(lines)\n let changed = false\n walk(lines, fencedBlocks(lines), indentColumns, (i, { delta, span }) => {\n if (i < frontmatter) return // YAML: every indent is metadata\n const within = span > 1 ? indentColumns(lines[i]) : Infinity\n for (let k = i; k < i + span; k++) {\n const next = relead(out[k], within, delta)\n if (next !== out[k]) {\n out[k] = next\n changed = true\n }\n }\n })\n return changed ? out.join('\\n') : text\n}\n","/**\n * The unified derived-block model (ADR 0016): blocks inferred from the markdown\n * source, never stored. One tree from three interlocking signals —\n *\n * 1. **Heading level** (`#` > `##` > `###`) is the outer spine: everything between\n * a heading and the next equal-or-higher heading nests beneath it.\n * 2. **Bullet / plain-line indentation** nests further within a section: a line\n * is one deeper than the nearest open line with a smaller indent (the Indent\n * Unit walk, ADR 0067), so a four-space or tab-indented file reads the same\n * tree as a two-space one.\n * 3. **Continuation lines** — non-bullet lines indented to a bullet's content\n * column belong to the *same* block (the soft-newline-within-a-block).\n *\n * A block has a kind (heading / paragraph / bullet / task). Blocks have no\n * persistent identity: a block is its source range in the current parse.\n *\n * Pure and DOM-free; the editor (keyboard ops, fold) and the derived index consume\n * it. See Dual Mode Editor.md.\n */\n\nimport { outlineLines } from './indent-unit'\n\nexport type BlockKind = 'heading' | 'paragraph' | 'bullet' | 'task'\n\nexport interface Block {\n type: BlockKind\n /**\n * Structural nesting depth (0 = top level), read from the indentation by the\n * Indent Unit walk. Headings are recorded at their depth too (usually 0); their\n * *hierarchy* comes from {@link level}, not this.\n */\n depth: number\n /** Heading rank: the `#` count (1–6). Only set for `heading` blocks. */\n level?: number\n /** Completion state. Only set for `task` blocks. */\n done?: boolean\n /** Source text — own lines (incl. continuation lines), indentation trimmed, joined by `\\n`. */\n text: string\n /** 0-based inclusive source line range of this block's *own* lines (incl. continuations). */\n startLine: number\n endLine: number\n children: Block[]\n}\n\nfunction indentOf(line: string): number {\n return line.length - line.trimStart().length\n}\n\nfunction headingLevel(line: string): number | undefined {\n const m = /^(#{1,6})\\s/.exec(line.trimStart())\n return m ? m[1].length : undefined\n}\n\nfunction isBullet(line: string): boolean {\n return /^-\\s/.test(line.trimStart())\n}\n\n/** A bullet whose content is `[ ]` / `[x]` is a task; returns its done state. */\nfunction taskDone(line: string): boolean | undefined {\n const m = /^-\\s+\\[([ xX])\\]/.exec(line.trimStart())\n if (!m) return undefined\n return m[1].toLowerCase() === 'x'\n}\n\ntype Flat = Omit<Block, 'children'>\n\n/**\n * Parse markdown into a tree of derived blocks (ADR 0016).\n *\n * {@link Block.depth} is **structural** (ADR 0067): a line is one deeper than the nearest open\n * line with a strictly smaller indent, whatever grid the text is on — the same walk the editor\n * renders from (`indent-unit.ts`), so the index and the editor never disagree about a four-space\n * or tab-indented document. The content column for continuation lines stays marker-based\n * (`indent + 2`).\n */\nexport function parseBlocks(markdown: string): Block[] {\n const lines = markdown.split('\\n')\n const outline = outlineLines(lines)\n\n // 1) Flat blocks in document order.\n const flat: Flat[] = []\n let i = 0\n while (i < lines.length) {\n if (lines[i].trim() === '') {\n i++\n continue\n }\n const depth = outline[i].depth\n const level = headingLevel(lines[i])\n\n if (level !== undefined) {\n flat.push({\n type: 'heading',\n depth,\n level,\n text: lines[i].trimStart(),\n startLine: i,\n endLine: i,\n })\n i++\n continue\n }\n\n if (isBullet(lines[i])) {\n // A bullet/task: gather continuation lines — non-blank, non-heading,\n // non-bullet lines indented to (or past) the marker's content column.\n const contentCol = indentOf(lines[i]) + 2 // `- `\n const done = taskDone(lines[i])\n const start = i\n const buf: string[] = [lines[i].trimStart()]\n i++\n while (\n i < lines.length &&\n lines[i].trim() !== '' &&\n headingLevel(lines[i]) === undefined &&\n !isBullet(lines[i]) &&\n indentOf(lines[i]) >= contentCol\n ) {\n buf.push(lines[i].trimStart())\n i++\n }\n flat.push({\n type: done === undefined ? 'bullet' : 'task',\n depth,\n ...(done === undefined ? {} : { done }),\n text: buf.join('\\n'),\n startLine: start,\n endLine: i - 1,\n })\n continue\n }\n\n // A paragraph: contiguous non-blank, non-heading, non-bullet lines at the\n // same indentation.\n const start = i\n const buf: string[] = [lines[i].trimStart()]\n i++\n while (\n i < lines.length &&\n lines[i].trim() !== '' &&\n headingLevel(lines[i]) === undefined &&\n !isBullet(lines[i]) &&\n indentOf(lines[i]) === indentOf(lines[start])\n ) {\n buf.push(lines[i].trimStart())\n i++\n }\n flat.push({ type: 'paragraph', depth, text: buf.join('\\n'), startLine: start, endLine: i - 1 })\n }\n\n // 2) Build the tree (ADR 0016 stack rule):\n // - a heading pops every open non-heading block and every heading of >= its level;\n // - a non-heading pops every open non-heading block of >= its indentation depth\n // (never a heading — it nests under the enclosing section).\n const roots: Block[] = []\n const stack: Block[] = []\n for (const f of flat) {\n const block: Block = { ...f, children: [] }\n if (block.type === 'heading') {\n const level = block.level!\n while (\n stack.length > 0 &&\n (stack[stack.length - 1].type !== 'heading' || stack[stack.length - 1].level! >= level)\n ) {\n stack.pop()\n }\n } else {\n while (\n stack.length > 0 &&\n stack[stack.length - 1].type !== 'heading' &&\n stack[stack.length - 1].depth >= block.depth\n ) {\n stack.pop()\n }\n }\n if (stack.length === 0) roots.push(block)\n else stack[stack.length - 1].children.push(block)\n stack.push(block)\n }\n return roots\n}\n","/**\n * The one name for the protected fence, and the one way to recognise one in raw text.\n *\n * Deliberately dependency-free. The [[Derived Index]] worker must be able to ask \"is this a\n * protected fence?\" without pulling in the crypto stack (Argon2id, the envelope, the key record),\n * and the answer must be the same string the editor and the storage layer use — a second regex\n * somewhere else is how a guard silently stops guarding.\n */\n\n/** The info-string that marks a fence as protected content. */\nexport const CIPHER_FENCE_INFO = 'etherpk-cipher'\n\n/**\n * Whether any line of `text` opens a cipher fence. Multiline and deliberately generous: an index\n * unit that contains a cipher fence *anywhere* is excluded whole, rather than the index trying to\n * reason about where the fence starts and stops.\n */\nexport function containsCipherFence(text: string): boolean {\n return CIPHER_FENCE_OPENER.test(text)\n}\n\n// Tolerates a leading bullet marker (`- ```etherpk-cipher`), matching fenceLineInfo's own rule,\n// and anything after the info-string, so a fence written by a newer client still counts.\nconst CIPHER_FENCE_OPENER = new RegExp(`^\\\\s*(?:-\\\\s)?\\`{3,}\\\\s*${CIPHER_FENCE_INFO}\\\\b`, 'mu')\n","/**\n * The [[Task Tag]] grammar (ADR 0032), as one pure parser.\n *\n * A [[Task]]'s metadata is a **contiguous leading run of hash-tags immediately after the\n * checkbox**, and nothing else on the line is metadata. Parsing therefore stops dead at the\n * first token that is not a tag - which is the whole point of the grammar: `#hashtags` in\n * prose stay prose, so a task can talk about `#P1 incidents` without claiming to be one.\n *\n * Lives here rather than inside the index derivation because three consumers need the same\n * answer: the derivation (which stores the parsed values as columns), the [[Tasks View]]\n * (which strips the run for display), and - when they land - the editor's tag rendering and\n * kanban [[Lane]]s. ADR 0032 is explicit that those should parse this grammar rather than\n * invent a parallel one.\n *\n * Pure and DOM-free.\n */\n\n/** Priority is a three-level scale; ADR 0032 dropped the below-normal levels at import. */\nexport type TaskPriority = 1 | 2 | 3\n\nexport interface TaskTags {\n priority: TaskPriority | null\n waiting: boolean\n /** `#D` — doing (in progress). One of ADR 0032's three minted extensions; dated `#D-` is due. */\n doing: boolean\n /** `#C` — minted; a cancelled task is *checked*, so the tag carries the truth. */\n cancelled: boolean\n /** `YYYY-MM-DD`, or null. Kept as a string: it sorts and compares correctly as one. */\n due: string | null\n completion: string | null\n /** `#S-` — minted, from Logseq's SCHEDULED. */\n scheduled: string | null\n /** The text with the leading tag run removed — what a task list displays. */\n text: string\n}\n\nconst EMPTY: Omit<TaskTags, 'text'> = {\n priority: null,\n waiting: false,\n doing: false,\n cancelled: false,\n due: null,\n completion: null,\n scheduled: null,\n}\n\n/**\n * Tags are **case-sensitive** (ADR 0032), so `#p1` and `#d` are prose, not metadata.\n * Dates are shape-matched, not calendar-validated: `#D-2026-13-45` is still a date-shaped\n * tag. Shape is what makes string ordering equal date ordering, and a wrong date is the\n * user's to see and fix, not ours to silently drop.\n */\nconst DATE = /^(\\d{4}-\\d{2}-\\d{2})$/\n\n/** Where the checkbox ends on a raw task line, or -1 when the line is not a task. */\nconst CHECKBOX = /^\\s*-\\s+\\[([ xX])\\]\\s?/\n\ntype TagKind = 'priority' | 'waiting' | 'doing' | 'cancelled' | 'due' | 'completion' | 'scheduled'\n\n/** Read one token as a tag, or null if it is not one (which ends the run). */\nfunction readTag(token: string): { kind: TagKind; value: string } | null {\n if (token === '#W') return { kind: 'waiting', value: '' }\n // A bare letter is a state; the same letter with a date (matched below) is a date. So\n // `#D` is doing where `#D-YYYY-MM-DD` is due, and `#C` is cancelled where `#C-YYYY-MM-DD`\n // is completion. The words the two were minted as (`#doing`, `#cancelled`) are NOT read:\n // ADR 0032 re-minted them before any production graph carried them.\n if (token === '#D') return { kind: 'doing', value: '' }\n if (token === '#C') return { kind: 'cancelled', value: '' }\n if (token === '#P1' || token === '#P2' || token === '#P3') {\n return { kind: 'priority', value: token.slice(2) }\n }\n const dated = /^#([DCS])-(.+)$/.exec(token)\n if (dated && DATE.test(dated[2])) {\n const kind = dated[1] === 'D' ? 'due' : dated[1] === 'C' ? 'completion' : 'scheduled'\n return { kind, value: dated[2] }\n }\n return null\n}\n\n/**\n * Parse the leading tag run of a task's text — the label, i.e. the line with its `- [ ]`\n * marker already stripped. {@link parseTaskLine} handles a raw source line instead.\n *\n * **First of a kind wins** (ADR 0032): a second `#P2` after a `#P1` is still a tag, so it\n * does not end the run, but it is ignored. That way a stray duplicate costs the user the\n * tag, never the rest of their metadata.\n */\nexport function parseTaskTags(label: string): TaskTags {\n const tags = { ...EMPTY }\n // Tokens are whitespace-separated; `rest` tracks where the run ended in the ORIGINAL\n // string so the returned text keeps its own spacing rather than a re-joined guess.\n let rest = label\n for (;;) {\n const match = /^(\\s*)(\\S+)/.exec(rest)\n if (!match) break\n const tag = readTag(match[2])\n if (!tag) break\n switch (tag.kind) {\n case 'priority':\n if (tags.priority === null) tags.priority = Number(tag.value) as TaskPriority\n break\n case 'waiting':\n tags.waiting = true\n break\n case 'doing':\n tags.doing = true\n break\n case 'cancelled':\n tags.cancelled = true\n break\n case 'due':\n if (tags.due === null) tags.due = tag.value\n break\n case 'completion':\n if (tags.completion === null) tags.completion = tag.value\n break\n case 'scheduled':\n if (tags.scheduled === null) tags.scheduled = tag.value\n break\n }\n rest = rest.slice(match[0].length)\n }\n return { ...tags, text: rest.trimStart() }\n}\n\n/** A raw source line's task state, or null when the line is not a [[Task]]. */\nexport interface ParsedTaskLine extends TaskTags {\n done: boolean\n}\n\n/**\n * Parse a whole source line as a task. Returns null for anything that is not a task line,\n * which is what makes this usable as the [[Tasks View]]'s stale-line guard: a line that no\n * longer parses as a task is proof the index has drifted from the document.\n */\nexport function parseTaskLine(line: string): ParsedTaskLine | null {\n const marker = CHECKBOX.exec(line)\n if (!marker) return null\n return { done: marker[1].toLowerCase() === 'x', ...parseTaskTags(line.slice(marker[0].length)) }\n}\n\n/**\n * Flip a task line's checkbox, preserving everything else about the line byte for byte —\n * indentation, marker spacing, tag run and text. Returns null when the line is not a task.\n */\nexport function toggleTaskLine(line: string, done: boolean): string | null {\n const marker = /^(\\s*-\\s+\\[)([ xX])(\\])/.exec(line)\n if (!marker) return null\n return line.slice(0, marker[1].length) + (done ? 'x' : ' ') + line.slice(marker[1].length + 1)\n}\n\n/** A change to make to a [[Task]]'s tag run, as the `#` helper issues them. */\nexport type TaskTagCommand =\n | { kind: 'priority'; value: TaskPriority }\n | { kind: 'waiting' }\n | { kind: 'doing' }\n | { kind: 'cancelled' }\n | { kind: 'due' | 'scheduled' | 'completion'; value: string }\n\n/** The order tags are written back in. Fixed, so the same set always reads the same way. */\nconst TAG_ORDER = ['priority', 'waiting', 'doing', 'cancelled', 'scheduled', 'due', 'completion'] as const\n\nfunction serialiseTags(tags: TaskTags): string[] {\n const out: string[] = []\n for (const kind of TAG_ORDER) {\n if (kind === 'priority' && tags.priority !== null) out.push(`#P${tags.priority}`)\n else if (kind === 'waiting' && tags.waiting) out.push('#W')\n else if (kind === 'doing' && tags.doing) out.push('#D')\n else if (kind === 'cancelled' && tags.cancelled) out.push('#C')\n else if (kind === 'scheduled' && tags.scheduled !== null) out.push(`#S-${tags.scheduled}`)\n else if (kind === 'due' && tags.due !== null) out.push(`#D-${tags.due}`)\n else if (kind === 'completion' && tags.completion !== null) out.push(`#C-${tags.completion}`)\n }\n return out\n}\n\n/**\n * Apply one tag change to a task line, returning the rewritten line (null if not a task).\n *\n * The semantics are AS Notes': issuing a **state** tag that is already set removes it, so the\n * same menu row both sets and clears — which is what makes the helper usable without a\n * separate \"remove\" list. Issuing a *different* priority replaces the current one, because\n * only the first of a kind counts (ADR 0032) and leaving both would silently ignore one.\n * A **date** tag always replaces: picking a date means that date.\n *\n * The run is rewritten in a fixed order rather than appended to, so a task's metadata reads\n * the same way whatever order it was set in. That does reorder tags the user typed by hand —\n * accepted deliberately: the run is an unordered set by definition (first of a kind wins),\n * and a predictable rendering is worth more than preserving an order that carries no meaning.\n */\nexport function applyTaskTag(line: string, command: TaskTagCommand): string | null {\n const marker = /^(\\s*)(-\\s+\\[[ xX]\\])\\s?/.exec(line)\n if (!marker) return null\n const [, indent, checkbox] = marker\n const tags = parseTaskTags(line.slice(marker[0].length))\n\n switch (command.kind) {\n case 'priority':\n tags.priority = tags.priority === command.value ? null : command.value\n break\n case 'waiting':\n tags.waiting = !tags.waiting\n break\n case 'doing':\n tags.doing = !tags.doing\n break\n case 'cancelled':\n tags.cancelled = !tags.cancelled\n break\n default:\n tags[command.kind] = command.value\n }\n\n // Exactly one space between the checkbox, each tag, and the text — and no trailing\n // space when a task has tags but no text yet, which is how it looks mid-typing.\n return [indent + checkbox, ...serialiseTags(tags), tags.text].filter((part) => part !== '').join(' ')\n}\n","/**\n * The pure index-derivation layer (Dual Mode Editor.md → The derived index store).\n * Turns one document's text into the rows the SQLite index stores: blocks (with\n * parent/order/kind/range), wikilink occurrences (tagged with their containing\n * block, for outline-chain backlinks), and tasks.\n *\n * This is the *shared* derivation — the same pure code the server runs over the same\n * content, which is what makes the client and server indexes compatible by\n * construction (ADR 0015). Pure and DOM-free.\n */\n\nimport { conceptKey } from './backlinks/backlink-index'\nimport { type Block, parseBlocks } from './block-model'\nimport { fencedBlocks } from './fenced-code'\nimport { containsCipherFence } from './protection/fence-info'\nimport { parseTaskTags, type TaskPriority } from './task-tags'\nimport { wikilinkOccurrencesInSource } from './wikilink'\n\nexport interface BlockRow {\n /** Stable only within this document's derivation (0-based DFS order). */\n localId: number\n parentId: number | null\n /** Sibling order under the parent. */\n ord: number\n kind: Block['type']\n depth: number\n done?: boolean\n startLine: number\n endLine: number\n text: string\n /** Display label — first line, marker stripped (the outline-chain breadcrumb text). */\n label: string\n}\n\nexport interface LinkRow {\n concept: string\n line: number\n lineText: string\n matchStart: number\n matchEnd: number\n /** The block whose own lines contain this link, or null if outside any block. */\n blockLocalId: number | null\n}\n\nexport interface TaskRow {\n blockLocalId: number\n done: boolean\n /** The block label — the leading [[Task Tag]] run INCLUDED, exactly as authored. */\n text: string\n line: number\n priority: TaskPriority | null\n waiting: boolean\n doing: boolean\n cancelled: boolean\n due: string | null\n completion: string | null\n scheduled: string | null\n}\n\n/**\n * One [[Task Concept]] a task answers to (ADR 0051), from a wikilink on the task's own line\n * or on any ancestor [[Block]]. The document's own concept — the virtual root of that chain —\n * is added by the index when it writes these, because derivation sees text and not identity.\n */\nexport interface TaskConceptRow {\n blockLocalId: number\n /** As written, case preserved; the index keys it. */\n concept: string\n}\n\nexport interface DerivedRows {\n blocks: BlockRow[]\n links: LinkRow[]\n tasks: TaskRow[]\n taskConcepts: TaskConceptRow[]\n}\n\n/**\n * A predicate over source line numbers: is this line inside a [[Fenced Code Block]]?\n *\n * The [[Block]] model does not treat fences as opaque, so `- [ ] x` written inside a fence\n * parses as a [[Task]] like any other bullet. That was invisible while nothing listed tasks;\n * the [[Tasks View]] makes it visible, as phantom tasks lifted out of code examples. Two\n * things follow from excluding them here rather than in the block model: the editor's outliner\n * behaviour is untouched (a fence's lines still belong to their block, which is what the\n * keyboard layer expects), and a [[Protected Document]] - an `etherpk-cipher` fence - can never\n * contribute a task, whatever it holds. That last one is a rule, not an optimisation: the\n * [[Derived Index]] is plaintext at rest and outlives the session.\n *\n * \"Inside a fence\" is the EDITOR's answer, not a second one: the column-scoped pairing of\n * `fencedBlocks` (Editor Content Rules → \"Inside a block\"), so a task is indexed exactly when\n * the editor shows a task. This scan used to have rules of its own, and two live bugs on\n * 2026-09-11 each hid a task the author could plainly see: a fence opened on a bullet line\n * (`- ``` `) was read as text, and a stray unterminated fence was run on to the next\n * top-level block. Under the shared scan a fence on a bullet line opens a block and never\n * closes one, an unterminated opener is no fence at all (its region is plain text in the\n * editor too), and a non-blank line dedenting below a fence's column abandons that fence.\n *\n * The one exception is a [[Protected Document]]'s `etherpk-cipher` opener with no closer:\n * that still runs to the end of the document. Nothing beneath it can be trusted as plaintext,\n * and the index is plaintext at rest, so the conservative reading is the only safe one.\n */\nfunction fencedLines(text: string): (line: number) => boolean {\n const lines = text.split('\\n')\n const spans: Array<[number, number]> = fencedBlocks(lines).map((b) => [b.start, b.end])\n const inside = (line: number) => spans.some(([from, to]) => line >= from && line <= to)\n for (let index = 0; index < lines.length; index++) {\n if (inside(index)) continue\n if (containsCipherFence(lines[index])) {\n spans.push([index, Number.MAX_SAFE_INTEGER])\n break\n }\n }\n if (spans.length === 0) return () => false\n return inside\n}\n\n/** First line of a block's text with its bullet/heading marker stripped. */\nexport function blockLabel(block: Block): string {\n const first = block.text.split('\\n', 1)[0]\n if (block.type === 'heading') return first.replace(/^#{1,6}\\s+/, '')\n return bulletLabel(first)\n}\n\n/**\n * The label of a bullet or [[Task]] line — its marker and checkbox stripped, indentation\n * ignored. Exported because the [[Tasks View]]'s write-back guard has to ask \"is the line in\n * the document still the task the index recorded?\", and the only honest way to answer is with\n * the very function that produced the recorded label. Two regexes that agree today would not\n * stay agreed.\n */\nexport function bulletLabel(line: string): string {\n return line.trimStart().replace(/^-\\s+(\\[[ xX]\\]\\s+)?/, '')\n}\n\n/** Flatten the block tree into rows in document (DFS) order, recording parentage. */\nfunction flatten(roots: Block[]): BlockRow[] {\n const rows: BlockRow[] = []\n const walk = (block: Block, parentId: number | null, ord: number) => {\n const localId = rows.length\n rows.push({\n localId,\n parentId,\n ord,\n kind: block.type,\n depth: block.depth,\n ...(block.done === undefined ? {} : { done: block.done }),\n startLine: block.startLine,\n endLine: block.endLine,\n text: block.text,\n label: blockLabel(block),\n })\n block.children.forEach((child, i) => walk(child, localId, i))\n }\n roots.forEach((root, i) => walk(root, null, i))\n return rows\n}\n\n/** Derive the index rows for a single document's text. Pure. */\nexport function deriveDoc(text: string): DerivedRows {\n const blocks = flatten(parseBlocks(text))\n\n // Map each source line to the deepest block whose own range covers it.\n const lineToBlock = new Map<number, number>()\n for (const b of blocks) {\n for (let line = b.startLine; line <= b.endLine; line++) lineToBlock.set(line, b.localId)\n }\n\n const links: LinkRow[] = wikilinkOccurrencesInSource(text).map((occ) => ({\n concept: occ.concept,\n line: occ.line,\n lineText: occ.lineText,\n matchStart: occ.matchStart,\n matchEnd: occ.matchEnd,\n blockLocalId: lineToBlock.get(occ.line) ?? null,\n }))\n\n // Scanning for fences costs a regex per line of the document, so it is worth deciding\n // whether there is anything to exclude first. Most documents hold no tasks at all, and on\n // a graph of a few thousand this runs for every one of them on a rebuild.\n const candidateTasks = blocks.filter((b) => b.kind === 'task')\n const fenced = candidateTasks.length === 0 ? () => false : fencedLines(text)\n const taskBlocks = candidateTasks.filter((b) => !fenced(b.startLine))\n const tasks: TaskRow[] = taskBlocks.map((b) => {\n const tags = parseTaskTags(b.label)\n return {\n blockLocalId: b.localId,\n done: b.done ?? false,\n text: b.label,\n line: b.startLine,\n priority: tags.priority,\n waiting: tags.waiting,\n doing: tags.doing,\n cancelled: tags.cancelled,\n due: tags.due,\n completion: tags.completion,\n scheduled: tags.scheduled,\n }\n })\n\n return { blocks, links, tasks, taskConcepts: deriveTaskConcepts(blocks, links, taskBlocks) }\n}\n\n/**\n * The [[Task Concept]] rows for one document (ADR 0051): for each task, every concept\n * wikilinked on its own block or on ANY ancestor, up to the root.\n *\n * Headings are ancestors here because they are ancestors in the [[Block]] tree — the heading\n * spine is the same tree the [[Backlink]] breadcrumb walks. That is what makes\n * `# Work on [[Acme]]` claim every task in the section, which is the intended, load-bearing\n * consequence rather than a leak.\n */\nfunction deriveTaskConcepts(blocks: BlockRow[], links: LinkRow[], taskBlocks: BlockRow[]): TaskConceptRow[] {\n if (taskBlocks.length === 0) return []\n const byBlock = new Map<number, string[]>()\n for (const link of links) {\n if (link.blockLocalId === null) continue\n const existing = byBlock.get(link.blockLocalId)\n if (existing) existing.push(link.concept)\n else byBlock.set(link.blockLocalId, [link.concept])\n }\n // Nothing is linked anywhere, so every task's only Task Concept is its document's —\n // which the index adds. Skipping the walk keeps the common journal page free.\n if (byBlock.size === 0) return []\n\n const rows: TaskConceptRow[] = []\n for (const task of taskBlocks) {\n const seen = new Set<string>()\n for (let id: number | null = task.localId; id !== null; id = blocks[id]?.parentId ?? null) {\n for (const concept of byBlock.get(id) ?? []) {\n const key = conceptKey(concept)\n if (seen.has(key)) continue\n seen.add(key)\n rows.push({ blockLocalId: task.localId, concept })\n }\n }\n }\n return rows\n}\n\n/** The ancestor labels of a block, root-first (the outline-chain breadcrumb). */\nexport function ancestorChain(blocks: BlockRow[], localId: number | null): string[] {\n const chain: string[] = []\n let id = localId\n while (id !== null) {\n const b = blocks[id]\n if (!b) break\n chain.unshift(b.label)\n id = b.parentId\n }\n return chain\n}\n","/**\n * Turning what a user typed into an FTS5 `MATCH` expression — and turning what comes back\n * into renderable segments.\n *\n * Pure, so it unit-tests without a database.\n *\n * The governing rule: **user input never reaches the FTS parser as syntax.** FTS5 has its own\n * grammar (`\"` `*` `-` `^` `:` `AND` `OR` `NOT` `NEAR`), so `C++`, `don't` and `orphan - asset`\n * would be a syntax error or, worse, a silent negation. In a live-as-you-type box a parse\n * error is not an edge case — it is the normal state halfway through typing a query. So we\n * tokenize ourselves and quote every term. No operator syntax is exposed, ever; structured\n * querying has a home already in [[Query]].\n */\n\n/** Below this a query is not specific enough to be worth a round trip. Counts TYPED\n * characters, not tokenized ones: `C++` is three characters a user deliberately typed, and\n * telling them it is too short would be nonsense. */\nexport const MIN_TEXT_QUERY_LENGTH = 2\n\n/**\n * A final term shorter than this is matched EXACTLY rather than as a prefix.\n *\n * This, not the length gate, is what stops a query matching most of the graph: `c*` matches\n * every word beginning with c, while `c` matches only a standalone `c` token — which is\n * exactly what someone searching `C++` meant.\n */\nconst MIN_PREFIX_LENGTH = 2\n\n/**\n * Characters FTS5's `snippet()` wraps matches in. Control characters, because they cannot\n * occur in markdown anyone types — a printable sentinel could appear in a note and would then\n * be rendered as a highlight that is not one.\n */\nexport const MATCH_OPEN = '\\u0002'\nexport const MATCH_CLOSE = '\\u0003'\n\n/** One run of snippet text, flagged as matched or not. Rendered as a text node or a `<mark>`. */\nexport interface SearchSegment {\n text: string\n match: boolean\n}\n\ninterface ParsedTerm {\n text: string\n /** A `\"quoted phrase\"` is taken literally and never gets a prefix `*`. */\n phrase: boolean\n}\n\n/**\n * Split input into terms: `\"quoted phrases\"` survive whole, everything else is words, and all\n * punctuation is discarded. An unclosed quote is treated as if closed at the end, so a query\n * stays valid while the user is still typing it.\n */\nexport function parseSearchTerms(input: string): ParsedTerm[] {\n const terms: ParsedTerm[] = []\n let index = 0\n while (index < input.length) {\n const char = input[index]\n if (char === '\"') {\n const end = input.indexOf('\"', index + 1)\n const raw = end === -1 ? input.slice(index + 1) : input.slice(index + 1, end)\n const words = wordsIn(raw)\n if (words.length > 0) terms.push({ text: words.join(' '), phrase: true })\n index = end === -1 ? input.length : end + 1\n continue\n }\n // A run up to the next quote, tokenized into words.\n const nextQuote = input.indexOf('\"', index)\n const chunk = nextQuote === -1 ? input.slice(index) : input.slice(index, nextQuote)\n for (const word of wordsIn(chunk)) terms.push({ text: word, phrase: false })\n index = nextQuote === -1 ? input.length : nextQuote\n }\n return terms\n}\n\n/**\n * The words in a run of text, as `unicode61` would tokenize them: letters and digits, with\n * everything else acting as a separator. Keeping this in step with the tokenizer is what makes\n * `[[Physics]]` findable by typing `physics` — the brackets are separators on both sides.\n */\nfunction wordsIn(text: string): string[] {\n return text.match(/[\\p{L}\\p{N}]+/gu) ?? []\n}\n\n/**\n * The `MATCH` expression for `input`, or `null` when there is nothing to search for.\n *\n * Terms are ANDed (FTS5's default), each one quoted so its content can never be read as\n * syntax. The LAST term gets a prefix `*` unless it was a quoted phrase, which is what makes\n * results narrow as you type: `orph` finds *orphaned* before you have finished the word.\n */\nexport function buildFtsMatch(input: string): string | null {\n const terms = parseSearchTerms(input)\n if (terms.length === 0) return null\n return terms\n .map((term, i) => {\n const quoted = `\"${term.text.replace(/\"/g, '\"\"')}\"`\n const isLast = i === terms.length - 1\n const prefixable = !term.phrase && term.text.length >= MIN_PREFIX_LENGTH\n return isLast && prefixable ? `${quoted}*` : quoted\n })\n .join(' ')\n}\n\n/** True when a query is worth sending to the text index at all. */\nexport function isSearchableTextQuery(input: string): boolean {\n if (parseSearchTerms(input).length === 0) return false\n return input.trim().length >= MIN_TEXT_QUERY_LENGTH\n}\n\n/**\n * Split a `snippet()` result on its sentinels into renderable segments.\n *\n * Returned as data, never as HTML: the text is the user's own notes, and this is the one\n * place in Search where an injection would land.\n */\nexport function snippetSegments(snippet: string): SearchSegment[] {\n const segments: SearchSegment[] = []\n let rest = snippet\n while (rest.length > 0) {\n const open = rest.indexOf(MATCH_OPEN)\n if (open === -1) {\n segments.push({ text: rest, match: false })\n break\n }\n if (open > 0) segments.push({ text: rest.slice(0, open), match: false })\n const close = rest.indexOf(MATCH_CLOSE, open + 1)\n if (close === -1) {\n // Unbalanced (a truncated snippet): keep the remainder as plain text rather than\n // dropping it — showing the words matters more than showing them highlighted.\n segments.push({ text: rest.slice(open + 1), match: false })\n break\n }\n segments.push({ text: rest.slice(open + 1, close), match: true })\n rest = rest.slice(close + 1)\n }\n return segments.filter((s) => s.text !== '')\n}\n","/**\n * The SQLite schema + query layer for the derived index (ADR 0015,\n * Dual Mode Editor.md). Engine-agnostic: it talks to a tiny {@link SqlDb} interface\n * so the same code runs over an in-memory DB in node tests and a sqlite-wasm/OPFS DB\n * in the browser. Derived and rebuildable — `ingest` clears and repopulates.\n *\n * Absorbs the in-memory backlink index: it answers `backlinksFor` (now enriched with\n * the outline-chain breadcrumb) and `existingConceptKeys`. Concept identity is\n * case-insensitive (ADR 0011).\n */\n\nimport type { DocumentKind } from '$lib/storage'\n\nimport { conceptKey } from './backlinks'\nimport { type BlockRow, deriveDoc, type TaskConceptRow, type TaskRow } from './index-derive'\nimport { containsCipherFence } from './protection/fence-info'\n\n/** One document fed into the index (same shape the in-memory index consumed). */\nexport interface IndexDoc {\n concept: string\n kind: DocumentKind\n aliases: string[]\n text: string\n}\n\n/** One node of a block reference's rendered subtree (the matched block + descendants). */\nexport interface RefSubtreeNode {\n label: string\n /** Depth relative to the matched block (0 = the matched block itself). */\n depth: number\n /** True for the block that actually holds the wikilink. */\n isMatch: boolean\n}\n\n/** A prose reference's surrounding text, truncated to 1000 chars centred on the match. */\nexport interface RefContext {\n text: string\n matchStart: number\n matchEnd: number\n truncated: boolean\n}\n\n/**\n * One reference, rendered two ways (Dual Mode Editor.md → references). A **block**\n * reference (wikilink in a bullet/task) carries the matched block + its descendant\n * subtree (Logseq 0.10.15 behaviour). A **prose** reference (wikilink in a paragraph\n * or heading) carries the surrounding paragraph text. Both carry a breadcrumb:\n * full ancestry for block refs, the heading tree for prose refs.\n */\nexport interface DbBacklinkRef {\n sourceConcept: string\n sourceKind: DocumentKind\n line: number\n kind: 'block' | 'prose'\n /** Ancestor labels, root-first. Empty when the reference is direct on the page. */\n breadcrumb: string[]\n /** Block refs: the matched block + descendants. Empty for prose refs. */\n subtree: RefSubtreeNode[]\n /** Prose refs: the surrounding (possibly truncated) text. Null for block refs. */\n context: RefContext | null\n}\n\nexport interface DbBacklinkGroup {\n sourceConcept: string\n sourceKind: DocumentKind\n refs: DbBacklinkRef[]\n}\n\nconst MAX_CONTEXT = 1000\n\n/** The minimal SQL surface the index needs — implemented over sqlite-wasm. */\nexport interface SqlDb {\n exec(sql: string): void\n run(sql: string, params?: unknown[]): void\n all<T = Record<string, unknown>>(sql: string, params?: unknown[]): T[]\n /** Release the underlying SQLite handle. Safe to call more than once. */\n close(): void\n}\n\n/**\n * Bumped whenever the schema OR the derivation changes (ADR 0015's `PRAGMA user_version`\n * pattern, ADR 0041 §5). A persisted index built by an older parser is not merely old, it is\n * WRONG - it holds the output of derivation logic that no longer exists - so a mismatch means\n * discard and rebuild. Changing `index-derive.ts` without bumping this is the trap.\n */\nexport const INDEX_SCHEMA_VERSION = 8\n\nconst SCHEMA = `\nCREATE TABLE IF NOT EXISTS index_metadata (\n key TEXT PRIMARY KEY, value INTEGER NOT NULL);\n-- protected: the stored text holds a cipher fence (fence-info.ts). Kept on the page row so a\n-- listing can wear a padlock without opening the document - the one cheap answer the tab has.\nCREATE TABLE IF NOT EXISTS pages (\n id INTEGER PRIMARY KEY, concept TEXT NOT NULL, concept_key TEXT NOT NULL, kind TEXT NOT NULL,\n text_hash TEXT NOT NULL DEFAULT '', generation INTEGER NOT NULL DEFAULT 1,\n protected INTEGER NOT NULL DEFAULT 0);\nCREATE TABLE IF NOT EXISTS aliases (\n page_id INTEGER NOT NULL, alias_key TEXT NOT NULL, display TEXT NOT NULL);\nCREATE TABLE IF NOT EXISTS blocks (\n page_id INTEGER NOT NULL, local_id INTEGER NOT NULL, parent_local_id INTEGER,\n ord INTEGER NOT NULL, kind TEXT NOT NULL, depth INTEGER NOT NULL, done INTEGER,\n start_line INTEGER NOT NULL, end_line INTEGER NOT NULL, label TEXT NOT NULL, text TEXT NOT NULL);\nCREATE TABLE IF NOT EXISTS links (\n page_id INTEGER NOT NULL, concept TEXT NOT NULL, concept_key TEXT NOT NULL,\n line INTEGER NOT NULL, line_text TEXT NOT NULL, match_start INTEGER NOT NULL,\n match_end INTEGER NOT NULL, block_local_id INTEGER);\n-- The [[Task Tag]] run (ADR 0032) parsed into columns, so the [[Tasks View]] can filter,\n-- order and PAGE in SQL. Keeping the run as text would make LIMIT return the wrong rows.\nCREATE TABLE IF NOT EXISTS tasks (\n page_id INTEGER NOT NULL, block_local_id INTEGER NOT NULL, done INTEGER NOT NULL,\n text TEXT NOT NULL, line INTEGER NOT NULL,\n priority INTEGER, waiting INTEGER NOT NULL DEFAULT 0, doing INTEGER NOT NULL DEFAULT 0,\n cancelled INTEGER NOT NULL DEFAULT 0, due TEXT, completion TEXT, scheduled TEXT);\n-- [[Task Concept]] (ADR 0051): one row per (task, concept it answers to). Concepts are stored\n-- AS WRITTEN and pooled over [[Alias]]es at query time, exactly as backlinksFor does — baking\n-- the canonical name in would go stale the moment a page is renamed.\nCREATE TABLE IF NOT EXISTS task_concepts (\n page_id INTEGER NOT NULL, block_local_id INTEGER NOT NULL, concept_key TEXT NOT NULL);\nCREATE INDEX IF NOT EXISTS links_concept_key ON links(concept_key);\nCREATE INDEX IF NOT EXISTS blocks_page ON blocks(page_id);\n-- Re-indexing ONE document deletes its rows by page_id and finds its page by concept_key;\n-- without these every edit would scan the whole table (ingestOne).\nCREATE INDEX IF NOT EXISTS pages_concept_key ON pages(concept_key);\nCREATE INDEX IF NOT EXISTS pages_generation ON pages(generation);\nCREATE INDEX IF NOT EXISTS aliases_alias_key ON aliases(alias_key);\nCREATE INDEX IF NOT EXISTS aliases_page ON aliases(page_id);\nCREATE INDEX IF NOT EXISTS links_page ON links(page_id);\nCREATE INDEX IF NOT EXISTS tasks_page ON tasks(page_id);\n-- The Name Filter's whole selectivity: without it, filtering to a concept scans every task\n-- row in the graph. Mirrors links_concept_key, and like it stays put during a bulk rebuild.\nCREATE INDEX IF NOT EXISTS task_concepts_key ON task_concepts(concept_key);\n-- Search's text index. One row per block, so a document's rank can be \"how many blocks\n-- mention this\" - the ordering the results actually display. See the block_fts notes above\n-- createSchema for why unicode61 rather than trigram, and why the rowid encodes the block.\nCREATE VIRTUAL TABLE IF NOT EXISTS block_fts USING fts5(text, tokenize = 'unicode61');\n`\n\n/**\n * [[Search]]'s text index (`block_fts` in {@link SCHEMA}) holds one row per [[Block]], so a\n * document can be ranked by how many of its blocks mention the query - the ordering the\n * results actually display.\n *\n * `unicode61` (word tokens), NOT `trigram`: trigram gives true substring matching but needs\n * >=3 characters, ranks close to meaninglessly under bm25, and runs 3-4x the text in an OPFS\n * store ADR 0041 already worries about. The accepted loss is that `graph` does not find\n * `subgraph`; the win is that the final term can carry an implicit `*`, so results narrow as\n * the user types.\n *\n * The rowid ENCODES the block: `page_id * BLOCK_FTS_STRIDE + local_id`. That is what makes\n * retiring a page a primary-key range delete rather than a scan of the whole text index -\n * an FTS5 column cannot be indexed for that, and `ingestOne` runs on every keystroke.\n *\n * Blocks per page in that encoding: 2^20 - far past any real document (a block is a line or a\n * bullet), and `page_id * STRIDE` stays inside `Number.MAX_SAFE_INTEGER`. 2^20 - far past any real document\n */\nexport const BLOCK_FTS_STRIDE = 1_048_576\n\n/** The rowid range covering every text-index row for one page: `[from, to)`. */\nfunction blockFtsRange(pageId: number): [number, number] {\n return [pageId * BLOCK_FTS_STRIDE, (pageId + 1) * BLOCK_FTS_STRIDE]\n}\n\nexport function createSchema(db: SqlDb): void {\n db.exec(SCHEMA)\n db.run(\"INSERT OR IGNORE INTO index_metadata (key, value) VALUES ('active_generation', 1)\")\n db.run(\"INSERT OR IGNORE INTO index_metadata (key, value) VALUES ('revision', 0)\")\n db.exec(`PRAGMA user_version = ${INDEX_SCHEMA_VERSION}`)\n}\n\nexport function activeIndexGeneration(db: SqlDb): number {\n return (\n db.all<{ value: number }>(\n \"SELECT value FROM index_metadata WHERE key = 'active_generation'\",\n )[0]?.value ?? 1\n )\n}\n\nexport function indexRevision(db: SqlDb): number {\n return db.all<{ value: number }>(\"SELECT value FROM index_metadata WHERE key = 'revision'\")[0]\n ?.value ?? 0\n}\n\nexport function advanceIndexRevision(db: SqlDb): number {\n db.run(\"UPDATE index_metadata SET value = value + 1 WHERE key = 'revision'\")\n return indexRevision(db)\n}\n\n/**\n * Ready an existing database for use, or report that it cannot be trusted.\n *\n * Returns false when the file was written by a different schema or derivation version — the\n * caller discards it and rebuilds. Never migrates: the index is derived and rebuildable, so\n * rebuilding is always both available and cheaper than being careful (ADR 0015).\n */\nexport function isUsableIndex(db: SqlDb): boolean {\n try {\n const rows = db.all<{ user_version: number }>('PRAGMA user_version')\n return rows[0]?.user_version === INDEX_SCHEMA_VERSION\n } catch {\n return false\n }\n}\n\n/** Every indexed document's content hash, for deciding what actually needs re-deriving. */\nexport function documentHashes(db: SqlDb): Map<string, string> {\n const rows = db.all<{ concept_key: string; text_hash: string }>(\n 'SELECT concept_key, text_hash FROM pages WHERE generation = ?',\n [activeIndexGeneration(db)],\n )\n return new Map(rows.map((r) => [r.concept_key, r.text_hash]))\n}\n\nexport function documentHash(db: SqlDb, key: string): string | undefined {\n return db.all<{ text_hash: string }>(\n 'SELECT text_hash FROM pages WHERE generation = ? AND concept_key = ? LIMIT 1',\n [activeIndexGeneration(db), key],\n )[0]?.text_hash\n}\n\n/**\n * cyrb53 — a fast, non-cryptographic 53-bit string hash. Not a security boundary: it decides\n * whether a document's derived rows are still current. A collision leaves one page stale until\n * its next edit, which a never-authoritative store survives (ADR 0041 §5).\n */\nexport function hashText(text: string): string {\n let h1 = 0xdeadbeef\n let h2 = 0x41c6ce57\n for (let i = 0; i < text.length; i++) {\n const ch = text.charCodeAt(i)\n h1 = Math.imul(h1 ^ ch, 2654435761)\n h2 = Math.imul(h2 ^ ch, 1597334677)\n }\n h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909)\n h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909)\n return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(36)\n}\n\n/**\n * Whether a [[Block]] contributes to [[Search]]'s text index.\n *\n * A [[Protected Document]] contributes **nothing** - not its plaintext and not its ciphertext.\n * This is a permanent rule, not a limitation waiting to be lifted (CONTEXT.md): the\n * [[Derived Index]] is plaintext at rest in OPFS and outlives the session, so indexing\n * decrypted cipher content would leave it on disk in a queryable file, surviving after the\n * key is forgotten - which defeats the whole feature. Indexing the ciphertext instead would\n * be merely useless: base64 matches nothing a human types, and it bloats the index.\n *\n * This is also what makes the lock cheap (ADR 0058): there is nothing here to evict when the\n * key goes, because protected content was never written here in the first place.\n */\nfunction isTextSearchable(block: BlockRow): boolean {\n return !containsCipherFence(block.text)\n}\n\n/** Write one document's derived rows under an already-established page id. */\nfunction insertDerived(db: SqlDb, pageId: number, doc: IndexDoc): void {\n for (const alias of doc.aliases) {\n db.run('INSERT INTO aliases (page_id, alias_key, display) VALUES (?,?,?)', [pageId, conceptKey(alias), alias])\n }\n const { blocks, links, tasks, taskConcepts } = deriveDoc(doc.text)\n for (const b of blocks) {\n db.run(\n 'INSERT INTO blocks (page_id, local_id, parent_local_id, ord, kind, depth, done, start_line, end_line, label, text) VALUES (?,?,?,?,?,?,?,?,?,?,?)',\n [pageId, b.localId, b.parentId, b.ord, b.kind, b.depth, b.done == null ? null : b.done ? 1 : 0, b.startLine, b.endLine, b.label, b.text],\n )\n if (isTextSearchable(b) && b.localId < BLOCK_FTS_STRIDE) {\n db.run('INSERT INTO block_fts (rowid, text) VALUES (?,?)', [\n pageId * BLOCK_FTS_STRIDE + b.localId,\n b.text,\n ])\n }\n }\n for (const l of links) {\n db.run(\n 'INSERT INTO links (page_id, concept, concept_key, line, line_text, match_start, match_end, block_local_id) VALUES (?,?,?,?,?,?,?,?)',\n [pageId, l.concept, conceptKey(l.concept), l.line, l.lineText, l.matchStart, l.matchEnd, l.blockLocalId],\n )\n }\n for (const t of tasks) {\n db.run(\n `INSERT INTO tasks (page_id, block_local_id, done, text, line,\n priority, waiting, doing, cancelled, due, completion, scheduled)\n VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`,\n [\n pageId,\n t.blockLocalId,\n t.done ? 1 : 0,\n t.text,\n t.line,\n t.priority,\n t.waiting ? 1 : 0,\n t.doing ? 1 : 0,\n t.cancelled ? 1 : 0,\n t.due,\n t.completion,\n t.scheduled,\n ],\n )\n }\n insertTaskConcepts(db, pageId, doc, tasks, taskConcepts)\n}\n\n/**\n * Write one document's [[Task Concept]] rows (ADR 0051).\n *\n * Derivation supplies the wikilinked half; the document's own concept is added HERE, as the\n * virtual root of every task's ancestry chain, because derivation reads text and never learns\n * the document's identity. Without that row, filtering to a project page returns nothing for\n * the tasks written directly on it — the case the \"Use <active tab>\" shortcut exists for.\n *\n * Only the document's own concept goes in, never its aliases: the query resolves a filtered\n * name to its page and matches on every name that page answers to, so an alias row here would\n * be redundant now and stale after a rename.\n */\nfunction insertTaskConcepts(\n db: SqlDb,\n pageId: number,\n doc: IndexDoc,\n tasks: readonly TaskRow[],\n taskConcepts: readonly TaskConceptRow[],\n): void {\n if (tasks.length === 0) return\n const ownKey = conceptKey(doc.concept)\n const written = new Set<string>()\n const write = (blockLocalId: number, key: string) => {\n const seen = `${blockLocalId} ${key}`\n if (written.has(seen)) return\n written.add(seen)\n db.run('INSERT INTO task_concepts (page_id, block_local_id, concept_key) VALUES (?,?,?)', [\n pageId,\n blockLocalId,\n key,\n ])\n }\n for (const t of tasks) write(t.blockLocalId, ownKey)\n for (const tc of taskConcepts) write(tc.blockLocalId, conceptKey(tc.concept))\n}\n\n/**\n * Run `work` as ONE transaction. Without this every INSERT is its own implicit transaction,\n * which is most of what made a full rebuild cost seconds rather than milliseconds.\n */\nfunction inTransaction(db: SqlDb, work: () => void): void {\n db.exec('BEGIN')\n try {\n work()\n db.exec('COMMIT')\n } catch (err) {\n db.exec('ROLLBACK')\n throw err\n }\n}\n\n/**\n * Indexes that exist to make a ONE-document re-index cheap (`ingestOne`). During a bulk\n * rebuild they are pure cost — every one of ~124k inserts maintains them — so the full path\n * drops and rebuilds them around the load, which SQLite does far faster in one pass.\n */\nconst MAINTENANCE_INDEXES = [\n 'pages_concept_key',\n 'aliases_alias_key',\n 'aliases_page',\n 'links_page',\n 'tasks_page',\n 'task_concepts_page',\n]\n\nconst CREATE_MAINTENANCE_INDEXES = `\nCREATE INDEX IF NOT EXISTS pages_concept_key ON pages(concept_key);\nCREATE INDEX IF NOT EXISTS aliases_alias_key ON aliases(alias_key);\nCREATE INDEX IF NOT EXISTS aliases_page ON aliases(page_id);\nCREATE INDEX IF NOT EXISTS links_page ON links(page_id);\nCREATE INDEX IF NOT EXISTS tasks_page ON tasks(page_id);\nCREATE INDEX IF NOT EXISTS task_concepts_page ON task_concepts(page_id);`\n\nfunction clearForRebuild(db: SqlDb): void {\n db.exec(\n 'DELETE FROM pages; DELETE FROM aliases; DELETE FROM blocks; DELETE FROM links; DELETE FROM tasks; DELETE FROM task_concepts; DELETE FROM block_fts;',\n )\n for (const name of MAINTENANCE_INDEXES) db.exec(`DROP INDEX IF EXISTS ${name}`)\n}\n\n/** SQLite has no boolean: the `protected` column as stored. */\nfunction protectedFlag(doc: IndexDoc): number {\n return containsCipherFence(doc.text) ? 1 : 0\n}\n\nfunction insertPage(db: SqlDb, pageId: number, generation: number, doc: IndexDoc): void {\n db.run(\n 'INSERT INTO pages (id, concept, concept_key, kind, text_hash, generation, protected) VALUES (?,?,?,?,?,?,?)',\n [pageId, doc.concept, conceptKey(doc.concept), doc.kind, hashText(doc.text), generation, protectedFlag(doc)],\n )\n insertDerived(db, pageId, doc)\n}\n\n/** Rebuild the whole index from the documents (derived; never authoritative). */\nexport function ingest(db: SqlDb, docs: Iterable<IndexDoc>): void {\n inTransaction(db, () => {\n clearForRebuild(db)\n const generation = activeIndexGeneration(db)\n let pageId = 0\n for (const doc of docs) insertPage(db, ++pageId, generation, doc)\n db.exec(CREATE_MAINTENANCE_INDEXES)\n })\n}\n\n/** Documents per transaction in a progressive rebuild — small enough to yield often. */\nconst REBUILD_BATCH = 100\n\nexport interface ProgressiveIngestOptions {\n /** Yield to the event loop; see `activity/breathe`. Without one this still batches, but never repaints. */\n breathe?: () => Promise<void> | void\n onProgress?: (done: number, total: number) => void\n}\n\n/**\n * The same full rebuild, in slices that hand the thread back between them.\n *\n * A 2431-document graph takes seconds to index however fast each insert is, and doing it in\n * one synchronous call means a frozen tab and a loading screen that cannot paint. The index\n * is derived and rebuildable, so a partially-built index mid-rebuild is not a correctness\n * problem — it is simply incomplete, which is what the progress the caller renders says.\n */\nexport async function ingestProgressively(\n db: SqlDb,\n docs: readonly IndexDoc[],\n options: ProgressiveIngestOptions = {},\n): Promise<void> {\n inTransaction(db, () => clearForRebuild(db))\n const generation = activeIndexGeneration(db)\n options.onProgress?.(0, docs.length)\n for (let start = 0; start < docs.length; start += REBUILD_BATCH) {\n const batch = docs.slice(start, start + REBUILD_BATCH)\n inTransaction(db, () => {\n batch.forEach((doc, n) => insertPage(db, start + n + 1, generation, doc))\n })\n options.onProgress?.(Math.min(start + REBUILD_BATCH, docs.length), docs.length)\n await options.breathe?.()\n }\n inTransaction(db, () => db.exec(CREATE_MAINTENANCE_INDEXES))\n}\n\nfunction deleteGeneration(db: SqlDb, generation: number): void {\n // Generation retirement is one relation-sized operation per table. Deleting four\n // child relations one page at a time made a 2,400-page commit execute roughly 9,600\n // synchronous wasm calls in one uninterruptible worker turn, so newly attached tabs\n // could wait many seconds even though their persisted index was already usable.\n // The text index first, and by ROWID: its rowids are derived from `blocks`, so once the\n // block rows are gone there is nothing left to compute them from, and the only remaining\n // way to find them would be a scan of the whole text index.\n db.run(\n `DELETE FROM block_fts WHERE rowid IN (\n SELECT b.page_id * ${BLOCK_FTS_STRIDE} + b.local_id FROM blocks b\n JOIN pages p ON p.id = b.page_id WHERE p.generation = ?)`,\n [generation],\n )\n for (const table of ['aliases', 'blocks', 'links', 'tasks', 'task_concepts']) {\n db.run(\n `DELETE FROM ${table} WHERE page_id IN (SELECT id FROM pages WHERE generation = ?)`,\n [generation],\n )\n }\n db.run('DELETE FROM pages WHERE generation = ?', [generation])\n}\n\n/** Begin an invisible rebuild generation and remove debris from any interrupted predecessor. */\nexport function beginIndexRebuild(db: SqlDb): number {\n const active = activeIndexGeneration(db)\n inTransaction(db, () => {\n const inactive = db\n .all<{ generation: number }>(\n 'SELECT DISTINCT generation FROM pages WHERE generation <> ?',\n [active],\n )\n .map((row) => row.generation)\n for (const generation of inactive) deleteGeneration(db, generation)\n })\n return (\n db.all<{ generation: number }>(\n 'SELECT MAX(generation) AS generation FROM pages',\n )[0]?.generation ?? active\n ) + 1\n}\n\n/** Ingest a rebuild chunk immediately without exposing it to active queries. */\nexport function ingestIndexRebuildChunk(\n db: SqlDb,\n generation: number,\n docs: readonly IndexDoc[],\n): void {\n inTransaction(db, () => {\n let pageId =\n db.all<{ id: number }>('SELECT COALESCE(MAX(id), 0) AS id FROM pages')[0]?.id ?? 0\n for (const doc of docs) insertPage(db, ++pageId, generation, doc)\n })\n}\n\n/** Atomically expose a complete staged generation, then reclaim the previous one. */\nexport function commitIndexRebuild(db: SqlDb, generation: number): number {\n return inTransactionWithResult(db, () => {\n db.run(\"UPDATE index_metadata SET value = ? WHERE key = 'active_generation'\", [\n generation,\n ])\n const revision = advanceIndexRevision(db)\n const obsolete = db\n .all<{ generation: number }>(\n 'SELECT DISTINCT generation FROM pages WHERE generation <> ?',\n [generation],\n )\n .map((row) => row.generation)\n for (const oldGeneration of obsolete) deleteGeneration(db, oldGeneration)\n return revision\n })\n}\n\nexport function abortIndexRebuild(db: SqlDb, generation: number): void {\n inTransaction(db, () => deleteGeneration(db, generation))\n}\n\nfunction inTransactionWithResult<T>(db: SqlDb, work: () => T): T {\n db.exec('BEGIN')\n try {\n const result = work()\n db.exec('COMMIT')\n return result\n } catch (error) {\n db.exec('ROLLBACK')\n throw error\n }\n}\n\n/**\n * Re-index ONE document, leaving every other page's rows untouched.\n *\n * This is what an edit costs. Rebuilding the whole graph for one keystroke was measured at\n * ~3.9s of blocked main thread on a 2431-document graph; the derived rows of one document\n * are a few dozen inserts.\n *\n * The document's own rows are replaced wholesale rather than diffed, deliberately: block\n * `local_id`s come from parse ORDER, so inserting a single line renumbers every block after\n * it. There is no stable block identity to diff against, and at ~40 blocks a page the\n * replacement is not worth avoiding. Nothing outside this page is touched.\n */\nexport function ingestOne(db: SqlDb, doc: IndexDoc): void {\n inTransaction(db, () => {\n const key = conceptKey(doc.concept)\n const generation = activeIndexGeneration(db)\n const found = db.all<{ id: number }>(\n 'SELECT id FROM pages WHERE generation = ? AND concept_key = ?',\n [generation, key],\n )\n let pageId: number\n if (found.length > 0) {\n pageId = found[0].id\n db.run('DELETE FROM aliases WHERE page_id = ?', [pageId])\n const [ftsFrom, ftsTo] = blockFtsRange(pageId)\n // A primary-key range delete, which is why the rowid encodes the page: this runs\n // on every keystroke, and a scan of the text index would not survive that.\n db.run('DELETE FROM block_fts WHERE rowid >= ? AND rowid < ?', [ftsFrom, ftsTo])\n db.run('DELETE FROM blocks WHERE page_id = ?', [pageId])\n db.run('DELETE FROM links WHERE page_id = ?', [pageId])\n db.run('DELETE FROM tasks WHERE page_id = ?', [pageId])\n db.run('DELETE FROM task_concepts WHERE page_id = ?', [pageId])\n db.run('UPDATE pages SET concept = ?, kind = ?, text_hash = ?, protected = ? WHERE id = ?', [\n doc.concept,\n doc.kind,\n hashText(doc.text),\n protectedFlag(doc),\n pageId,\n ])\n } else {\n const max = db.all<{ n: number }>('SELECT COALESCE(MAX(id), 0) AS n FROM pages')\n pageId = (max[0]?.n ?? 0) + 1\n db.run(\n 'INSERT INTO pages (id, concept, concept_key, kind, text_hash, generation, protected) VALUES (?,?,?,?,?,?,?)',\n [pageId, doc.concept, key, doc.kind, hashText(doc.text), generation, protectedFlag(doc)],\n )\n }\n insertDerived(db, pageId, doc)\n })\n}\n\n/** Every concept key that resolves to a real document (canonical name or alias). */\nexport function existingConceptKeys(db: SqlDb): string[] {\n const generation = activeIndexGeneration(db)\n const pages = db.all<{ k: string }>(\n 'SELECT concept_key AS k FROM pages WHERE generation = ?',\n [generation],\n )\n const aliases = db.all<{ k: string }>(\n 'SELECT a.alias_key AS k FROM aliases a JOIN pages p ON p.id = a.page_id WHERE p.generation = ?',\n [generation],\n )\n return [...pages, ...aliases].map((r) => r.k)\n}\n\n/** What a completion candidate resolves to. Pages/journals/aliases exist; `pageless` does not. */\nexport type ConceptCandidateKind = 'page' | 'journal' | 'alias' | 'pageless'\n\n/** One row the wikilink-completion popover can offer (Dual Mode Editor.md → Wikilink completion). */\nexport interface ConceptCandidate {\n /** Case-preserved name shown and inserted (the alias's own name for aliases). */\n display: string\n /** Case-insensitive identity key (ADR 0011). */\n key: string\n kind: ConceptCandidateKind\n /** Aliases only: the canonical page's display name (what it resolves to). */\n canonical?: string\n /**\n * Pageless only: how many wikilink occurrences reference this concept. It is what\n * distinguishes a pageless row from a draft row in [[Quick Find]] and [[Search]] - the\n * one thing a \"make this up\" row can never show (ADR 0050).\n */\n references?: number\n /**\n * The document this resolves to is a [[Protected Document]] - its stored text holds a cipher\n * fence. Set on the page's own row AND on each of its aliases, since an alias row opens that\n * same document. Present only when true, so an unprotected candidate's shape is unchanged.\n *\n * This is what lets a listing - [[Search]]'s Names, the Sidebar's Favourites and Recents -\n * wear the padlock without opening anything: `open()` retains a sync engine on a Server\n * Backend, and the tab renderer already learned the hard way not to call it from a repaint.\n * It says nothing about the lock STATE; that is per graph and read from the session.\n */\n protected?: true\n}\n\n/**\n * Every concept the wikilink-completion popover can offer, as one flat snapshot:\n * existing pages and journals, each alias (carrying its canonical page), and\n * `pageless` concepts — referenced by some wikilink but with no page yet (\"Not yet\n * created\"). Scoped concepts (display contains a nested `[[…]]`) are included — a real\n * page named e.g. `This is a [[Mini Inside]] tab` must be linkable; the popover only\n * hides them when completing *inside* a nested bracket (Dual Mode Editor.md → Wikilink\n * completion). Built fresh after each ingest by the live index, never queried per keystroke.\n */\nexport function conceptCandidates(db: SqlDb): ConceptCandidate[] {\n const generation = activeIndexGeneration(db)\n const pages = db.all<{ concept: string; key: string; kind: DocumentKind; protected: number }>(\n 'SELECT concept, concept_key AS key, kind, protected FROM pages WHERE generation = ? ORDER BY concept_key',\n [generation],\n )\n const aliases = db.all<{ display: string; key: string; canonical: string; protected: number }>(\n 'SELECT a.display AS display, a.alias_key AS key, p.concept AS canonical, p.protected AS protected FROM aliases a JOIN pages p ON p.id = a.page_id WHERE p.generation = ? ORDER BY a.alias_key',\n [generation],\n )\n // SQLite was previously asked to run two correlated anti-joins for every link row. On a\n // fully hydrated 2,400-document graph that made each persisted-index open spend several\n // seconds rebuilding this same cache. We already have the complete page and alias sets\n // above, so group link targets once and perform the inexpensive membership test in JS.\n //\n // The display is the MAJORITY casing among the concept's wikilink instances, not an\n // arbitrary one: a [[Draft]] shows this name from the moment it opens and promotes with\n // it, so it decides the page's canonical name (ADR 0050). Ties break on BINARY collation,\n // which puts the Title-Cased variant first - the right default for a page title.\n const referenced = db.all<{ display: string; key: string; references: number }>(\n `SELECT key, display, refs AS \"references\" FROM (\n SELECT l.concept_key AS key, l.concept AS display,\n SUM(COUNT(*)) OVER (PARTITION BY l.concept_key) AS refs,\n ROW_NUMBER() OVER (\n PARTITION BY l.concept_key ORDER BY COUNT(*) DESC, l.concept ASC\n ) AS rn\n FROM links l JOIN pages source ON source.id = l.page_id\n WHERE source.generation = ?\n GROUP BY l.concept_key, l.concept\n ) WHERE rn = 1 ORDER BY key`,\n [generation],\n )\n const existing = new Set([...pages.map((page) => page.key), ...aliases.map((alias) => alias.key)])\n const out: ConceptCandidate[] = []\n for (const page of pages) {\n out.push(withProtected({ display: page.concept, key: page.key, kind: page.kind }, page.protected))\n }\n for (const alias of aliases) {\n out.push(\n withProtected(\n { display: alias.display, key: alias.key, kind: 'alias', canonical: alias.canonical },\n alias.protected,\n ),\n )\n }\n for (const link of referenced) {\n if (!existing.has(link.key)) {\n out.push({\n display: link.display,\n key: link.key,\n kind: 'pageless',\n references: link.references,\n })\n }\n }\n return out\n}\n\n/** Attach the padlock flag only when set, so unprotected candidates keep their exact shape. */\nfunction withProtected(candidate: ConceptCandidate, flag: number): ConceptCandidate {\n return flag ? { ...candidate, protected: true } : candidate\n}\n\n/** Resolve only the candidate keys affected by one document ingest. */\nexport function conceptCandidatesForKeys(\n db: SqlDb,\n keys: Iterable<string>,\n): Map<string, ConceptCandidate> {\n const generation = activeIndexGeneration(db)\n const result = new Map<string, ConceptCandidate>()\n for (const key of new Set(keys)) {\n const page = db.all<{ concept: string; kind: DocumentKind; protected: number }>(\n 'SELECT concept, kind, protected FROM pages WHERE generation=? AND concept_key=? LIMIT 1',\n [generation, key],\n )[0]\n if (page) {\n result.set(key, withProtected({ display: page.concept, key, kind: page.kind }, page.protected))\n continue\n }\n const alias = db.all<{ display: string; canonical: string; protected: number }>(\n `SELECT a.display, p.concept AS canonical, p.protected AS protected\n FROM aliases a JOIN pages p ON p.id=a.page_id\n WHERE p.generation=? AND a.alias_key=? LIMIT 1`,\n [generation, key],\n )[0]\n if (alias) {\n result.set(\n key,\n withProtected(\n { display: alias.display, key, kind: 'alias', canonical: alias.canonical },\n alias.protected,\n ),\n )\n continue\n }\n // Majority casing and occurrence count, exactly as `conceptCandidates` derives them\n // for the full snapshot — the two must agree or a candidate would change name when\n // one document is re-ingested.\n const pageless = db.all<{ display: string; references: number }>(\n `SELECT display, refs AS \"references\" FROM (\n SELECT l.concept AS display,\n SUM(COUNT(*)) OVER () AS refs,\n ROW_NUMBER() OVER (ORDER BY COUNT(*) DESC, l.concept ASC) AS rn\n FROM links l JOIN pages p ON p.id=l.page_id\n WHERE p.generation=? AND l.concept_key=?\n GROUP BY l.concept\n ) WHERE rn = 1`,\n [generation, key],\n )[0]\n if (pageless) {\n result.set(key, {\n display: pageless.display,\n key,\n kind: 'pageless',\n references: pageless.references,\n })\n }\n }\n return result\n}\n\nexport interface IndexedDocumentFacts {\n aliases: string[]\n linkTargets: string[]\n}\n\nexport function indexedDocumentFacts(db: SqlDb, key: string): IndexedDocumentFacts {\n const generation = activeIndexGeneration(db)\n const page = db.all<{ id: number }>(\n 'SELECT id FROM pages WHERE generation=? AND concept_key=? LIMIT 1',\n [generation, key],\n )[0]\n if (!page) return { aliases: [], linkTargets: [] }\n return {\n aliases: db\n .all<{ key: string }>('SELECT alias_key AS key FROM aliases WHERE page_id=?', [\n page.id,\n ])\n .map((row) => row.key),\n linkTargets: db\n .all<{ key: string }>(\n 'SELECT DISTINCT concept_key AS key FROM links WHERE page_id=?',\n [page.id],\n )\n .map((row) => row.key),\n }\n}\n\nexport function indexDocumentFactKeys(doc: IndexDoc): IndexedDocumentFacts {\n return {\n aliases: doc.aliases.map(conceptKey),\n linkTargets: [\n ...new Set(deriveDoc(doc.text).links.map((link) => conceptKey(link.concept))),\n ],\n }\n}\n\nexport function conceptExists(db: SqlDb, concept: string): boolean {\n const key = conceptKey(concept)\n const generation = activeIndexGeneration(db)\n const hit = db.all<{ n: number }>(\n `SELECT (\n EXISTS(SELECT 1 FROM pages WHERE generation=? AND concept_key=?)\n OR EXISTS(\n SELECT 1 FROM aliases a JOIN pages p ON p.id=a.page_id\n WHERE p.generation=? AND a.alias_key=?\n )\n ) AS n`,\n [generation, key, generation, key],\n )\n return !!hit[0]?.n\n}\n\n/** The canonical page id a concept resolves to (by its own name or an alias), or null. */\nfunction canonicalPageId(db: SqlDb, key: string): number | null {\n const generation = activeIndexGeneration(db)\n const direct = db.all<{ id: number }>(\n 'SELECT id FROM pages WHERE generation=? AND concept_key=? LIMIT 1',\n [generation, key],\n )\n if (direct[0]) return direct[0].id\n const viaAlias = db.all<{ id: number }>(\n `SELECT a.page_id AS id FROM aliases a JOIN pages p ON p.id=a.page_id\n WHERE p.generation=? AND a.alias_key=? LIMIT 1`,\n [generation, key],\n )\n return viaAlias[0]?.id ?? null\n}\n\n/** All concept keys that target the same page (its own name + aliases). */\nfunction namesForPage(db: SqlDb, pageId: number): string[] {\n const page = db.all<{ k: string }>('SELECT concept_key AS k FROM pages WHERE id=?', [pageId])\n const aliases = db.all<{ k: string }>('SELECT alias_key AS k FROM aliases WHERE page_id=?', [pageId])\n return [...page.map((r) => r.k), ...aliases.map((r) => r.k)]\n}\n\ninterface LinkHit {\n page_id: number\n sourceConcept: string\n sourceKind: DocumentKind\n line: number\n line_text: string\n match_start: number\n match_end: number\n block_local_id: number | null\n}\n\n/** Ancestor labels of a block, root-first. `headingsOnly` keeps just the heading tree. */\nfunction ancestorLabels(blocks: BlockRow[], id: number, headingsOnly: boolean): string[] {\n const out: string[] = []\n let cur = blocks[id]?.parentId ?? null\n while (cur !== null) {\n const b = blocks[cur]\n if (!b) break\n if (!headingsOnly || b.kind === 'heading') out.unshift(b.label)\n cur = b.parentId\n }\n return out\n}\n\n/** The matched block plus its descendant subtree (Logseq block-reference behaviour). */\nfunction collectSubtree(blocks: BlockRow[], rootId: number): RefSubtreeNode[] {\n const childrenByParent = new Map<number, BlockRow[]>()\n for (const block of blocks) {\n if (!block || block.parentId === null) continue\n const children = childrenByParent.get(block.parentId) ?? []\n children.push(block)\n childrenByParent.set(block.parentId, children)\n }\n for (const children of childrenByParent.values()) {\n children.sort((a, b) => a.ord - b.ord)\n }\n const out: RefSubtreeNode[] = []\n const walk = (id: number, depth: number) => {\n const b = blocks[id]\n if (!b) return\n out.push({ label: b.label, depth, isMatch: id === rootId })\n for (const child of childrenByParent.get(id) ?? []) walk(child.localId, depth + 1)\n }\n walk(rootId, 0)\n return out\n}\n\n/** Truncate `text` to MAX_CONTEXT chars centred on the match, with ellipses. */\nfunction truncateContext(text: string, matchStart: number, matchEnd: number): RefContext {\n if (text.length <= MAX_CONTEXT) return { text, matchStart, matchEnd, truncated: false }\n const mid = Math.floor((matchStart + matchEnd) / 2)\n let start = Math.max(0, mid - Math.floor(MAX_CONTEXT / 2))\n const end = Math.min(text.length, start + MAX_CONTEXT)\n start = Math.max(0, end - MAX_CONTEXT)\n const prefix = start > 0 ? '…' : ''\n const suffix = end < text.length ? '…' : ''\n const shift = prefix.length - start\n return {\n text: prefix + text.slice(start, end) + suffix,\n matchStart: Math.max(0, matchStart + shift),\n matchEnd: Math.max(0, matchEnd + shift),\n truncated: true,\n }\n}\n\n/** Build a reference: a block subtree for bullets/tasks, prose context otherwise. */\nfunction buildRef(h: LinkHit, blocks: BlockRow[]): DbBacklinkRef {\n const base = { sourceConcept: h.sourceConcept, sourceKind: h.sourceKind, line: h.line }\n const block = h.block_local_id != null ? blocks[h.block_local_id] : undefined\n\n if (block && (block.kind === 'bullet' || block.kind === 'task')) {\n return {\n ...base,\n kind: 'block',\n breadcrumb: ancestorLabels(blocks, block.localId, false),\n subtree: collectSubtree(blocks, block.localId),\n context: null,\n }\n }\n\n // Prose (paragraph / heading / no block): the surrounding text + the heading tree.\n const source = block?.text ?? h.line_text\n const matchText = h.line_text.slice(h.match_start, h.match_end)\n const offset = source.indexOf(matchText)\n const ms = offset >= 0 ? offset : 0\n const me = offset >= 0 ? offset + matchText.length : 0\n return {\n ...base,\n kind: 'prose',\n breadcrumb: block ? ancestorLabels(blocks, block.localId, true) : [],\n subtree: [],\n context: truncateContext(source, ms, me),\n }\n}\n\n/**\n * All references to a concept, pooled across its own name and aliases\n * (case-insensitive), grouped by source document (journals newest-first, then pages).\n * Each ref renders as a Logseq-style block subtree (bullets) or prose context\n * (paragraphs/headings) — see {@link DbBacklinkRef}. Replaces the in-memory index.\n */\nexport function backlinksFor(db: SqlDb, concept: string): DbBacklinkGroup[] {\n const key = conceptKey(concept)\n const pageId = canonicalPageId(db, key)\n const names = pageId === null ? [key] : namesForPage(db, pageId)\n if (names.length === 0) return []\n\n const placeholders = names.map(() => '?').join(',')\n const generation = activeIndexGeneration(db)\n const hits = db.all<LinkHit>(\n `SELECT l.page_id, p.concept AS sourceConcept, p.kind AS sourceKind, l.line, l.line_text,\n l.match_start, l.match_end, l.block_local_id\n FROM links l JOIN pages p ON p.id = l.page_id\n WHERE p.generation=? AND l.concept_key IN (${placeholders})`,\n [generation, ...names],\n )\n\n // Fetch full block rows for the involved pages once (ancestry + subtree + text).\n const blocksByPage = new Map<number, BlockRow[]>()\n for (const pid of new Set(hits.map((h) => h.page_id))) {\n const rows = db.all<{\n local_id: number\n parent_local_id: number | null\n ord: number\n kind: BlockRow['kind']\n depth: number\n label: string\n text: string\n }>(\n 'SELECT local_id, parent_local_id, ord, kind, depth, label, text FROM blocks WHERE page_id=? ORDER BY local_id',\n [pid],\n )\n const blocks: BlockRow[] = []\n for (const r of rows) {\n blocks[r.local_id] = {\n localId: r.local_id,\n parentId: r.parent_local_id,\n ord: r.ord,\n kind: r.kind,\n depth: r.depth,\n startLine: 0,\n endLine: 0,\n text: r.text,\n label: r.label,\n }\n }\n blocksByPage.set(pid, blocks)\n }\n\n const groups = new Map<string, DbBacklinkGroup>()\n for (const h of hits) {\n let group = groups.get(h.sourceConcept)\n if (!group) {\n group = { sourceConcept: h.sourceConcept, sourceKind: h.sourceKind, refs: [] }\n groups.set(h.sourceConcept, group)\n }\n group.refs.push(buildRef(h, blocksByPage.get(h.page_id) ?? []))\n }\n for (const g of groups.values()) g.refs.sort((a, b) => a.line - b.line)\n return [...groups.values()].sort(compareGroups)\n}\n\n/** Journals first (date desc), then pages (concept asc, case-insensitive). */\nfunction compareGroups(a: DbBacklinkGroup, b: DbBacklinkGroup): number {\n if (a.sourceKind !== b.sourceKind) return a.sourceKind === 'journal' ? -1 : 1\n if (a.sourceKind === 'journal') return b.sourceConcept.localeCompare(a.sourceConcept)\n return conceptKey(a.sourceConcept).localeCompare(conceptKey(b.sourceConcept))\n}\n\n// ---- [[Search]]: the text group ------------------------------------------------------\n\nimport {\n buildFtsMatch,\n MATCH_CLOSE,\n MATCH_OPEN,\n snippetSegments,\n type SearchSegment,\n} from './search-query'\n\n/** Matching documents counted beyond this are reported as \"1000+\" rather than tallied. */\nexport const SEARCH_COUNT_CAP = 1000\n\n/** Matching blocks shown per document before the \"+N more\" expander. */\nexport const SEARCH_HITS_PER_DOCUMENT = 3\n\n/** Roughly two lines of context around a match, in tokens. */\nconst SNIPPET_TOKENS = 24\n\n/** One matching [[Block]] — a locator, not something to read in place. */\nexport interface SearchBlockHit {\n /** 0-based source line, for opening the document at this block. */\n line: number\n /** Ancestor labels, root-first. A bare bullet is often meaningless without them. */\n breadcrumb: string[]\n /** The snippet, pre-split into plain and matched runs. Never HTML. */\n snippet: SearchSegment[]\n}\n\n/** One document's matches, which is what a Text result row actually is. */\nexport interface SearchDocumentGroup {\n concept: string\n kind: DocumentKind\n /** Every matching block in this document, not just the ones listed in `hits`. */\n matches: number\n hits: SearchBlockHit[]\n}\n\nexport interface SearchTextPage {\n groups: SearchDocumentGroup[]\n /** True when a further page exists — from fetching one row beyond the limit. */\n hasMore: boolean\n}\n\n/**\n * Documents whose text matches `query`, most-mentions-first, one page at a time.\n *\n * Ordering is **matching-block count**, with `bm25()` only as a tie-break. Ranking by best\n * block instead would be the obvious choice and is wrong here: the FTS rows are blocks, so\n * bm25 scores how much a *block* is about the term, which rewards short ones - a one-line\n * `see [[Orphaned Asset]]` outranks the page that explains orphaned assets. Summing block\n * scores merely reintroduces the long-document bias bm25 exists to remove. Counting is cruder,\n * robust, and explainable in one sentence - and it orders by a number the row displays.\n */\nexport function searchText(\n db: SqlDb,\n query: string,\n offset: number,\n limit: number,\n): SearchTextPage {\n const match = buildFtsMatch(query)\n if (match === null) return { groups: [], hasMore: false }\n const generation = activeIndexGeneration(db)\n\n // One extra row decides `hasMore` without a second COUNT.\n const ranked = db.all<{ page_id: number; matches: number }>(\n // MATERIALIZED is load-bearing, not a hint: an FTS5 auxiliary function like bm25()\n // is only legal where the virtual table is queried directly, and the planner would\n // otherwise flatten this subquery into the join and reject it outright.\n `WITH hits AS MATERIALIZED (\n SELECT rowid / ${BLOCK_FTS_STRIDE} AS page_id, bm25(block_fts) AS score\n FROM block_fts WHERE block_fts MATCH ?\n )\n SELECT h.page_id AS page_id, COUNT(*) AS matches, MIN(h.score) AS best\n FROM hits h\n JOIN pages p ON p.id = h.page_id AND p.generation = ?\n GROUP BY h.page_id\n ORDER BY matches DESC, best ASC, h.page_id ASC\n LIMIT ? OFFSET ?`,\n [match, generation, limit + 1, offset],\n )\n const hasMore = ranked.length > limit\n const page = hasMore ? ranked.slice(0, limit) : ranked\n if (page.length === 0) return { groups: [], hasMore: false }\n\n const ids = page.map((row) => row.page_id)\n const placeholders = ids.map(() => '?').join(',')\n const pages = new Map(\n db\n .all<{ id: number; concept: string; kind: DocumentKind }>(\n `SELECT id, concept, kind FROM pages WHERE id IN (${placeholders})`,\n ids,\n )\n .map((row) => [row.id, row]),\n )\n // Ancestry for the whole page of documents in one read; the breadcrumb is walked in JS.\n const blocks = db.all<{\n page_id: number\n local_id: number\n parent_local_id: number | null\n label: string\n start_line: number\n }>(\n `SELECT page_id, local_id, parent_local_id, label, start_line FROM blocks\n WHERE page_id IN (${placeholders})`,\n ids,\n )\n const byPage = new Map<number, Map<number, (typeof blocks)[number]>>()\n for (const block of blocks) {\n let map = byPage.get(block.page_id)\n if (!map) byPage.set(block.page_id, (map = new Map()))\n map.set(block.local_id, block)\n }\n\n const groups: SearchDocumentGroup[] = []\n for (const row of page) {\n const meta = pages.get(row.page_id)\n if (!meta) continue\n const [from, to] = blockFtsRange(row.page_id)\n // Document order, not relevance order: a list of locators reads top-to-bottom.\n const hits = db.all<{ rowid: number; snippet: string }>(\n `SELECT rowid, snippet(block_fts, 0, ?, ?, '…', ${SNIPPET_TOKENS}) AS snippet\n FROM block_fts\n WHERE block_fts MATCH ? AND rowid >= ? AND rowid < ?\n ORDER BY rowid LIMIT ?`,\n [MATCH_OPEN, MATCH_CLOSE, match, from, to, SEARCH_HITS_PER_DOCUMENT],\n )\n const blockMap = byPage.get(row.page_id)\n groups.push({\n concept: meta.concept,\n kind: meta.kind,\n matches: row.matches,\n hits: hits.map((hit) => {\n const localId = hit.rowid - from\n const block = blockMap?.get(localId)\n return {\n line: block?.start_line ?? 0,\n breadcrumb: blockMap ? searchBreadcrumb(blockMap, localId) : [],\n snippet: snippetSegments(hit.snippet),\n }\n }),\n })\n }\n return { groups, hasMore }\n}\n\n/**\n * Ancestor labels of a block, root-first, from the flat map `searchText` already loaded.\n * Distinct from the same-named helper above, which walks the in-memory derivation's rows.\n */\nfunction searchBreadcrumb(\n blocks: Map<number, { parent_local_id: number | null; label: string }>,\n localId: number,\n): string[] {\n const chain: string[] = []\n let parent = blocks.get(localId)?.parent_local_id ?? null\n // Bounded by the map size: a corrupt parent cycle must not hang the worker.\n for (let guard = 0; parent !== null && guard <= blocks.size; guard++) {\n const block = blocks.get(parent)\n if (!block) break\n chain.unshift(block.label)\n parent = block.parent_local_id\n }\n return chain\n}\n\n/**\n * How many documents match, capped at {@link SEARCH_COUNT_CAP}.\n *\n * Capped rather than exact because a prefix query - which is every query, mid-typing - can\n * match most of the graph, and this runs beside the rows it must never delay.\n */\nexport function searchTextCount(db: SqlDb, query: string): { total: number; capped: boolean } {\n const match = buildFtsMatch(query)\n if (match === null) return { total: 0, capped: false }\n const rows = db.all<{ n: number }>(\n `SELECT COUNT(*) AS n FROM (\n SELECT m.page_id FROM (\n SELECT DISTINCT rowid / ${BLOCK_FTS_STRIDE} AS page_id FROM block_fts\n WHERE block_fts MATCH ?\n ) m\n JOIN pages p ON p.id = m.page_id AND p.generation = ?\n LIMIT ?)`,\n [match, activeIndexGeneration(db), SEARCH_COUNT_CAP + 1],\n )\n const n = rows[0]?.n ?? 0\n return n > SEARCH_COUNT_CAP ? { total: SEARCH_COUNT_CAP, capped: true } : { total: n, capped: false }\n}\n\n// ── Asset usage ─────────────────────────────────────────────────────────────\n\n/** One document that holds at least one [[Asset Reference]] to the asset asked about. */\nexport interface AssetUsageDocument {\n concept: string\n kind: DocumentKind\n /** How many references this document holds — two images of one asset count as two. */\n references: number\n}\n\nexport interface AssetUsage {\n /** References across the whole graph, including the one the caller is standing on. */\n references: number\n /** The documents holding them, in concept order so the dialog reads the same every time. */\n documents: AssetUsageDocument[]\n}\n\n/** Escape a literal for a LIKE pattern, so an asset name can never behave as a wildcard. */\nfunction likeLiteral(needle: string): string {\n return needle.replace(/[\\\\%_]/g, (c) => `\\\\${c}`)\n}\n\n/** Occurrences of `needle` in `text`, non-overlapping. */\nfunction occurrences(text: string, needle: string): number {\n let n = 0\n for (let at = text.indexOf(needle); at !== -1; at = text.indexOf(needle, at + needle.length)) n += 1\n return n\n}\n\n/**\n * Which documents reference an [[Asset]], and how many times — the safety gate in front of a\n * permanent delete (ADR 0054). `needles` are the forms the reference can be written in: the\n * asset's identity (its on-disk name on a [[Filesystem Backend]], its id on a [[Server\n * Backend]]) and, when it differs, the percent-encoded form.\n *\n * Deliberately a **substring scan of every block's source**, not a search over `block_fts`:\n * the text index tokenises, so it cannot be trusted to match a punctuated file name, and a\n * missed match here would destroy bytes another document still needs. The same conservative\n * rule the [[Orphaned Asset]] scanners use — a mention anywhere counts, fences and prose\n * alike — for the same reason: over-counting refuses a delete, under-counting loses data.\n *\n * `blocks` carries no index on `text`, so this is a scan. That is the right shape: on the\n * largest graph this was built against it is ~85k rows inside the index worker, tens of\n * milliseconds, and it runs once per trash click rather than per keystroke.\n */\nexport function assetUsage(db: SqlDb, needles: readonly string[]): AssetUsage {\n const distinct = [...new Set(needles)].filter((n) => n.length > 0)\n if (distinct.length === 0) return { references: 0, documents: [] }\n const where = distinct.map(() => `b.text LIKE ? ESCAPE '\\\\'`).join(' OR ')\n const rows = db.all<{ concept: string; kind: DocumentKind; text: string }>(\n `SELECT p.concept AS concept, p.kind AS kind, b.text AS text\n FROM blocks b\n JOIN pages p ON p.id = b.page_id AND p.generation = ?\n WHERE ${where}`,\n [activeIndexGeneration(db), ...distinct.map((n) => `%${likeLiteral(n)}%`)],\n )\n\n // Count in JS rather than SQL: one block can hold two references to the same asset, and\n // \"used in more than one place\" has to see both.\n const byConcept = new Map<string, AssetUsageDocument>()\n let references = 0\n for (const row of rows) {\n const n = distinct.reduce((sum, needle) => sum + occurrences(row.text, needle), 0)\n if (n === 0) continue\n references += n\n const existing = byConcept.get(row.concept)\n if (existing) existing.references += n\n else byConcept.set(row.concept, { concept: row.concept, kind: row.kind, references: n })\n }\n return {\n references,\n documents: [...byConcept.values()].sort((a, b) => a.concept.localeCompare(b.concept)),\n }\n}\n\n// ── The Tasks View ──────────────────────────────────────────────────────────\n\n/**\n * The five [[Task]] states, as a partition. They are mutually exclusive by construction, so\n * a task is in exactly one and the Status chips can be a plain multi-select:\n *\n * waiting open and `#W` doing open, `#D`, not `#W`\n * open open and neither done checked, not `#C`\n * cancelled checked and `#C`\n *\n * `cancelled` earns a state of its own because ADR 0032 makes a cancelled task *checked* -\n * without one it would hide inside `done` with no way to ask for it or exclude it.\n */\nexport type TaskStatus = 'open' | 'doing' | 'waiting' | 'done' | 'cancelled'\n\nexport const TASK_STATUSES: readonly TaskStatus[] = ['open', 'doing', 'waiting', 'done', 'cancelled']\n\n/** The states a task that is not finished can be in - the Tasks View's default selection. */\nexport const OPEN_TASK_STATUSES: readonly TaskStatus[] = ['open', 'doing', 'waiting']\n\n/** A priority chip. `null` is the real, selectable \"no priority\" bucket, not \"unset\". */\nexport type TaskPriorityFilter = 1 | 2 | 3 | null\n\nexport const TASK_PRIORITY_FILTERS: readonly TaskPriorityFilter[] = [1, 2, 3, null]\n\nexport type TaskDueWindow = 'any' | 'overdue' | 'today' | 'next7'\n\nexport type TaskGroupBy = 'document' | 'priority' | 'due'\n\nexport interface TaskQuery {\n /** The [[Name Filter]]'s selection, or null for every task in the graph. */\n concept: string | null\n statuses: readonly TaskStatus[]\n priorities: readonly TaskPriorityFilter[]\n due: TaskDueWindow\n groupBy: TaskGroupBy\n /**\n * Today as `YYYY-MM-DD`, supplied by the caller rather than read from the clock here.\n * The query then stays a pure function of its inputs - which is what makes the date\n * windows testable, and what keeps the worker from having an opinion about the user's\n * timezone, which only the tab knows.\n */\n today: string\n}\n\n/** One task, as the [[Tasks View]] renders it. */\nexport interface TaskHit {\n /** Identity for the ticked-in-this-session set, and the write-back coordinates. */\n pageId: number\n blockLocalId: number\n /** The source document's concept - the ViewRef target to open, and the Document group. */\n concept: string\n kind: DocumentKind\n /** 0-based source line, for `revealLine` and for the write-back's stale-line guard. */\n line: number\n done: boolean\n /** The block label with its [[Task Tag]] run still on it, exactly as authored. */\n text: string\n priority: number | null\n waiting: boolean\n doing: boolean\n cancelled: boolean\n due: string | null\n completion: string | null\n scheduled: string | null\n /** Ancestor labels, root-first - why this task matched, when it matched via an ancestor. */\n breadcrumb: string[]\n}\n\nexport interface TaskPage {\n hits: TaskHit[]\n /** True when a further page exists - from fetching one row beyond the limit. */\n hasMore: boolean\n}\n\n/** `YYYY-MM-DD` for a local date, `days` from it. Local, because \"today\" is the user's. */\nexport function taskDateKey(date: Date, days = 0): string {\n const shifted = new Date(date.getFullYear(), date.getMonth(), date.getDate() + days)\n const month = String(shifted.getMonth() + 1).padStart(2, '0')\n const day = String(shifted.getDate()).padStart(2, '0')\n return `${shifted.getFullYear()}-${month}-${day}`\n}\n\n/** The SQL predicate for one status, over the `tasks` columns. */\nconst STATUS_SQL: Record<TaskStatus, string> = {\n open: '(t.done = 0 AND t.waiting = 0 AND t.doing = 0)',\n doing: '(t.done = 0 AND t.doing = 1 AND t.waiting = 0)',\n waiting: '(t.done = 0 AND t.waiting = 1)',\n done: '(t.done = 1 AND t.cancelled = 0)',\n cancelled: '(t.done = 1 AND t.cancelled = 1)',\n}\n\n/**\n * Ordering per grouping. The FIRST key must be the group key, because the view splits an\n * already-ordered page into groups rather than re-sorting - which is what lets a group\n * straddle a page boundary and simply continue.\n *\n * Within a group the order is due-date then priority (AS Notes' behaviour), with NULLs last\n * both times: an undated task is not urgent, and SQLite would otherwise sort NULL first.\n * `page_id, block_local_id` finally makes the order total, so paging cannot repeat or skip.\n */\nconst WITHIN_GROUP = '(t.due IS NULL), t.due ASC, (t.priority IS NULL), t.priority ASC'\nconst ORDER_SQL: Record<TaskGroupBy, string> = {\n document: `p.concept_key ASC, ${WITHIN_GROUP}`,\n priority: `(t.priority IS NULL), t.priority ASC, (t.due IS NULL), t.due ASC`,\n due: `(t.due IS NULL), t.due ASC, (t.priority IS NULL), t.priority ASC`,\n}\n\ninterface TaskRowResult {\n page_id: number\n concept: string\n kind: DocumentKind\n block_local_id: number\n line: number\n done: number\n text: string\n priority: number | null\n waiting: number\n doing: number\n cancelled: number\n due: string | null\n completion: string | null\n scheduled: string | null\n}\n\n\n/**\n * The WHERE clause and parameters shared by {@link tasksMatching} and its count, so the two\n * can never drift into answering different questions about the same filter.\n *\n * Returns null when the filter cannot match anything - an empty Status or Priority selection\n * (a genuine \"show me nothing\", not a missing value), or a [[Name Filter]] naming a concept\n * with no page and no links.\n */\nfunction taskPredicate(db: SqlDb, query: TaskQuery): { where: string; params: unknown[] } | null {\n if (query.statuses.length === 0 || query.priorities.length === 0) return null\n\n const where: string[] = ['p.generation = ?']\n const params: unknown[] = [activeIndexGeneration(db)]\n\n if (query.concept !== null) {\n // The [[Name Filter]] resolves to a page and matches EVERY name that page answers to\n // (its own plus [[Alias]]es), the pooling backlinksFor performs - so filtering by an\n // alias finds tasks written against the canonical name and vice versa. A name with no\n // page is a [[Pageless Concept]] and matches only itself, which is right: it has none.\n const key = conceptKey(query.concept)\n const pageId = canonicalPageId(db, key)\n const names = pageId === null ? [key] : namesForPage(db, pageId)\n if (names.length === 0) return null\n where.push(\n `EXISTS (SELECT 1 FROM task_concepts tc\n WHERE tc.page_id = t.page_id AND tc.block_local_id = t.block_local_id\n AND tc.concept_key IN (${names.map(() => '?').join(',')}))`,\n )\n params.push(...names)\n }\n\n if (query.statuses.length < TASK_STATUSES.length) {\n where.push(`(${query.statuses.map((s) => STATUS_SQL[s]).join(' OR ')})`)\n }\n\n if (query.priorities.length < TASK_PRIORITY_FILTERS.length) {\n const levels = query.priorities.filter((p): p is 1 | 2 | 3 => p !== null)\n const clauses: string[] = []\n if (levels.length > 0) {\n clauses.push(`t.priority IN (${levels.map(() => '?').join(',')})`)\n params.push(...levels)\n }\n if (query.priorities.includes(null)) clauses.push('t.priority IS NULL')\n where.push(`(${clauses.join(' OR ')})`)\n }\n\n if (query.due !== 'any') {\n const today = query.today\n if (query.due === 'overdue') {\n where.push('(t.due IS NOT NULL AND t.due < ?)')\n params.push(today)\n } else if (query.due === 'today') {\n where.push('t.due = ?')\n params.push(today)\n } else {\n where.push('(t.due IS NOT NULL AND t.due >= ? AND t.due <= ?)')\n params.push(today, addDays(today, 7))\n }\n }\n\n return { where: where.join(' AND '), params }\n}\n\ninterface TaskRowResult {\n page_id: number\n concept: string\n kind: DocumentKind\n block_local_id: number\n line: number\n done: number\n text: string\n priority: number | null\n waiting: number\n doing: number\n cancelled: number\n due: string | null\n completion: string | null\n scheduled: string | null\n}\n\n/**\n * One page of [[Task]]s matching a [[Tasks View]] query, ordered so the caller groups by\n * simply walking the rows in order.\n *\n * Every row carries its ancestor breadcrumb, which the [[Tasks View]] is not decorating with:\n * under ADR 0051 a task can match through a distant ancestor - a heading, most visibly - so\n * without the chain a legitimately matching row looks like a bug.\n */\nexport function tasksMatching(db: SqlDb, query: TaskQuery, offset: number, limit: number): TaskPage {\n const predicate = taskPredicate(db, query)\n if (predicate === null) return { hits: [], hasMore: false }\n\n // One extra row decides `hasMore` without a second COUNT, as searchText does.\n const rows = db.all<TaskRowResult>(\n `SELECT t.page_id, p.concept, p.kind, t.block_local_id, t.line, t.done, t.text,\n t.priority, t.waiting, t.doing, t.cancelled, t.due, t.completion, t.scheduled\n FROM tasks t JOIN pages p ON p.id = t.page_id\n WHERE ${predicate.where}\n ORDER BY ${ORDER_SQL[query.groupBy]}, t.page_id ASC, t.block_local_id ASC\n LIMIT ? OFFSET ?`,\n [...predicate.params, limit + 1, offset],\n )\n const hasMore = rows.length > limit\n const page = hasMore ? rows.slice(0, limit) : rows\n if (page.length === 0) return { hits: [], hasMore: false }\n\n const breadcrumbs = breadcrumbsFor(db, page)\n return {\n hits: page.map((row) => ({\n ...toHit(row),\n breadcrumb: breadcrumbs.get(row.page_id)?.get(row.block_local_id) ?? [],\n })),\n hasMore,\n }\n}\n\n/**\n * Ancestor labels for a whole page of tasks: one read for every document involved, then the\n * chains walked in JS. The same shape searchText uses, and for the same reason - a recursive\n * SQL walk per row would be one query per task.\n */\nfunction breadcrumbsFor(db: SqlDb, rows: readonly TaskRowResult[]): Map<number, Map<number, string[]>> {\n const ids = [...new Set(rows.map((row) => row.page_id))]\n const placeholders = ids.map(() => '?').join(',')\n const blocks = db.all<{\n page_id: number\n local_id: number\n parent_local_id: number | null\n label: string\n }>(\n `SELECT page_id, local_id, parent_local_id, label FROM blocks WHERE page_id IN (${placeholders})`,\n ids,\n )\n const byPage = new Map<number, Map<number, { parent_local_id: number | null; label: string }>>()\n for (const block of blocks) {\n let page = byPage.get(block.page_id)\n if (!page) byPage.set(block.page_id, (page = new Map()))\n page.set(block.local_id, { parent_local_id: block.parent_local_id, label: block.label })\n }\n const out = new Map<number, Map<number, string[]>>()\n for (const row of rows) {\n const page = byPage.get(row.page_id)\n if (!page) continue\n let chains = out.get(row.page_id)\n if (!chains) out.set(row.page_id, (chains = new Map()))\n chains.set(row.block_local_id, searchBreadcrumb(page, row.block_local_id))\n }\n return out\n}\n\n/** How many tasks match. Uncapped: this counts an indexed join, not a text index. */\nexport function tasksMatchingCount(db: SqlDb, query: TaskQuery): number {\n const predicate = taskPredicate(db, query)\n if (predicate === null) return 0\n const rows = db.all<{ n: number }>(\n `SELECT COUNT(*) AS n FROM tasks t JOIN pages p ON p.id = t.page_id WHERE ${predicate.where}`,\n predicate.params,\n )\n return rows[0]?.n ?? 0\n}\n\n/** `YYYY-MM-DD` shifted by whole days, so the caller never leaves string space. */\nfunction addDays(day: string, days: number): string {\n const [year, month, date] = day.split('-').map(Number)\n return taskDateKey(new Date(year, month - 1, date), days)\n}\n\nfunction toHit(row: TaskRowResult): Omit<TaskHit, 'breadcrumb'> {\n return {\n pageId: row.page_id,\n blockLocalId: row.block_local_id,\n concept: row.concept,\n kind: row.kind,\n line: row.line,\n done: row.done !== 0,\n text: row.text,\n priority: row.priority,\n waiting: row.waiting !== 0,\n doing: row.doing !== 0,\n cancelled: row.cancelled !== 0,\n due: row.due,\n completion: row.completion,\n scheduled: row.scheduled,\n }\n}\n","/**\n * The index server, free of any transport (ADR 0041 §2). It owns the SQLite database and\n * answers {@link IndexRequest}s; it does not know whether it is inside a SharedWorker, a\n * dedicated Worker, or running inline in a test.\n *\n * Keeping it transport-free is what lets the whole of the index's behaviour be node-tested\n * against an in-memory database, exactly as ADR 0015 established for the query layer — no\n * browser, no worker, no OPFS.\n */\n\nimport {\n type ConceptCandidate,\n type IndexDoc,\n type SqlDb,\n abortIndexRebuild,\n activeIndexGeneration,\n assetUsage,\n advanceIndexRevision,\n backlinksFor,\n beginIndexRebuild,\n commitIndexRebuild,\n conceptCandidates,\n conceptCandidatesForKeys,\n createSchema,\n documentHash,\n existingConceptKeys,\n hashText,\n indexDocumentFactKeys,\n indexRevision,\n ingestOne,\n indexedDocumentFacts,\n ingestIndexRebuildChunk,\n isUsableIndex,\n searchText,\n searchTextCount,\n tasksMatching,\n tasksMatchingCount,\n} from '../index-db'\nimport { conceptKey } from '../backlinks/backlink-index'\nimport type { IndexDelta, IndexRequest, IndexResponse } from './protocol'\n\n/** How a host supplies a database for a graph. Injected so tests need no OPFS. */\nexport interface IndexDbHost {\n open(graphId: string): Promise<{ db: SqlDb; persisted: boolean; blocked?: 'held' | 'unsupported' }>\n /** Discard a database whose version stamp no longer matches, and hand back a fresh one. */\n discard(graphId: string): Promise<{ db: SqlDb; persisted: boolean; blocked?: 'held' | 'unsupported' }>\n}\n\nexport interface IndexCore {\n handle(request: IndexRequest): Promise<IndexResponse[]>\n dispose(): void\n}\n\nexport function createIndexCore(host: IndexDbHost): IndexCore {\n type SnapshotResponse = Extract<IndexResponse, { type: 'snapshot' }>\n let db: SqlDb | undefined\n let persisted = false\n /** Why persistence is unavailable, when known. Travels with `opened` for the notice. */\n let persistenceBlocked: 'held' | 'unsupported' | undefined\n let openGraphId: string | undefined\n let pendingRebuild:\n | { id: string; generation: number; total: number; done: number }\n | undefined\n /**\n * The exact synchronous-read cache most recently pushed to clients. A shared worker can\n * serve many tabs, and querying every link again for every idempotent `open` made the third\n * and fourth tabs slower than the cold build. SQLite remains authoritative; this mirrors\n * its active generation and is updated by the same commit/delta boundaries clients see.\n */\n let visibleSnapshot: SnapshotResponse | undefined\n\n /**\n * End this core's ownership of the current SQLite handle.\n *\n * The OPFS host may unlink or reopen the same graph immediately afterwards, so merely\n * dropping the JavaScript reference is not enough. Clear the core state even if SQLite\n * reports a cleanup error, then close the underlying oo1 handle.\n */\n function closeActiveDatabase(): void {\n const closing = db\n const interruptedRebuild = pendingRebuild\n db = undefined\n pendingRebuild = undefined\n openGraphId = undefined\n persisted = false\n persistenceBlocked = undefined\n visibleSnapshot = undefined\n if (!closing) return\n\n try {\n if (interruptedRebuild) {\n abortIndexRebuild(closing, interruptedRebuild.generation)\n }\n } finally {\n closing.close()\n }\n }\n\n /** Read the active generation once after opening or atomically replacing it. */\n function readSnapshot(): SnapshotResponse {\n return {\n type: 'snapshot',\n revision: db ? indexRevision(db) : 0,\n existing: db ? existingConceptKeys(db) : [],\n candidates: db ? conceptCandidates(db) : [],\n }\n }\n\n /** Clone the pushed cache for inline transports too, where postMessage does not clone it. */\n function snapshot(rebuildId?: string): SnapshotResponse {\n visibleSnapshot ??= readSnapshot()\n return {\n ...visibleSnapshot,\n existing: [...visibleSnapshot.existing],\n candidates: visibleSnapshot.candidates.map((candidate) => ({ ...candidate })),\n ...(rebuildId ? { rebuildId } : {}),\n }\n }\n\n function replaceSnapshot(rebuildId?: string): SnapshotResponse {\n // If any SQL read fails after a generation commit, a later recovery must retry from\n // that new active generation. It must never fall back to the previous cached revision.\n visibleSnapshot = undefined\n visibleSnapshot = readSnapshot()\n return snapshot(rebuildId)\n }\n\n /** Keep the worker's cache at the same revision as the delta broadcast to every tab. */\n function applyDeltaToSnapshot(\n previous: SnapshotResponse | undefined,\n delta: IndexDelta,\n ): void {\n if (!previous || previous.revision !== delta.revision - 1) return\n const existing = new Set(previous.existing)\n for (const key of delta.existingAdded) existing.add(key)\n for (const key of delta.existingRemoved) existing.delete(key)\n const candidates = new Map(\n previous.candidates.map((candidate) => [candidate.key, candidate]),\n )\n for (const candidate of delta.candidateUpserts) candidates.set(candidate.key, candidate)\n for (const key of delta.candidateRemoved) candidates.delete(key)\n visibleSnapshot = {\n type: 'snapshot',\n revision: delta.revision,\n existing: [...existing],\n candidates: [...candidates.values()],\n }\n }\n\n function pageCount(): number {\n if (!db) return 0\n return (\n db.all<{ n: number }>('SELECT COUNT(*) AS n FROM pages WHERE generation = ?', [\n activeIndexGeneration(db),\n ])[0]?.n ?? 0\n )\n }\n\n const candidateExists = (candidate: ConceptCandidate | undefined) =>\n candidate !== undefined && candidate.kind !== 'pageless'\n\n function deltaFor(\n revision: number,\n before: Map<string, ConceptCandidate>,\n after: Map<string, ConceptCandidate>,\n backlinkTargetsChanged: Set<string>,\n ): IndexDelta {\n const existingAdded: string[] = []\n const existingRemoved: string[] = []\n const candidateUpserts: ConceptCandidate[] = []\n const candidateRemoved: string[] = []\n for (const key of new Set([...before.keys(), ...after.keys()])) {\n const previous = before.get(key)\n const next = after.get(key)\n if (!candidateExists(previous) && candidateExists(next)) existingAdded.push(key)\n if (candidateExists(previous) && !candidateExists(next)) existingRemoved.push(key)\n if (!next) candidateRemoved.push(key)\n else if (JSON.stringify(previous) !== JSON.stringify(next)) candidateUpserts.push(next)\n }\n return {\n revision,\n existingAdded,\n existingRemoved,\n candidateUpserts,\n candidateRemoved,\n backlinkTargetsChanged: [...backlinkTargetsChanged],\n }\n }\n\n /**\n * The stored index is wrecked (SQLite reports corruption). It is derived, so the answer is\n * ADR 0015's: discard it and start again — a fresh, empty database for the same graph, the\n * caller told so it can re-derive. Until this, a corrupt file was permanent: every ingest\n * failed, reads that missed the damage still answered, and the sidebars silently stopped\n * following the documents (live, 2026-09-04: \"the checkbox goes sidebar → document but not\n * document → sidebar\", then a manual rebuild reporting SQLITE_CORRUPT).\n */\n async function discardWreckedDatabase(graphId: string, cause: unknown): Promise<void> {\n console.warn(`[index] persisted index for ${graphId} is corrupt; discarding and re-deriving:`, cause)\n try {\n closeActiveDatabase()\n } catch {\n // A wreck need not even close cleanly; the state is already cleared.\n }\n const fresh = await host.discard(graphId)\n db = fresh.db\n persisted = fresh.persisted\n persistenceBlocked = fresh.blocked\n createSchema(db)\n openGraphId = graphId\n }\n\n async function dispatch(request: IndexRequest): Promise<IndexResponse[]> {\n switch (request.type) {\n case 'open': {\n // Idempotent per graph: with several tabs attached to one worker\n // (ADR 0042), every tab opens on arrival. A second `open` must\n // answer from the live database, not stack another connection\n // onto the same file.\n if (db && openGraphId === request.graphId) {\n return [\n {\n type: 'opened',\n persisted,\n indexedDocuments: pageCount(),\n ...(request.openId !== undefined ? { openId: request.openId } : {}),\n ...(persistenceBlocked ? { persistenceBlocked } : {}),\n },\n snapshot(),\n ]\n }\n // A worker normally serves one graph, but inline transports and tests can\n // switch. Never retain the old graph's SQLite handle while opening another.\n closeActiveDatabase()\n const opened = await host.open(request.graphId)\n // A database written by an older schema or an older DERIVATION is not\n // stale, it is wrong. Never migrate — discard and rebuild (ADR 0015).\n if (!isUsableIndex(opened.db)) {\n // OPFS cannot unlink the stale file until its oo1 database releases it.\n opened.db.close()\n const fresh = await host.discard(request.graphId)\n db = fresh.db\n persisted = fresh.persisted\n persistenceBlocked = fresh.blocked\n createSchema(db)\n } else {\n db = opened.db\n persisted = opened.persisted\n persistenceBlocked = opened.blocked\n }\n openGraphId = request.graphId\n return [\n {\n type: 'opened',\n persisted,\n indexedDocuments: pageCount(),\n ...(request.openId !== undefined ? { openId: request.openId } : {}),\n ...(persistenceBlocked ? { persistenceBlocked } : {}),\n },\n replaceSnapshot(),\n ]\n }\n\n case 'rebuild-begin': {\n if (!db) throw new Error('index not open')\n if (pendingRebuild) abortIndexRebuild(db, pendingRebuild.generation)\n pendingRebuild = {\n id: request.rebuildId,\n generation: beginIndexRebuild(db),\n total: request.total,\n done: 0,\n }\n return []\n }\n\n case 'rebuild-docs': {\n if (!pendingRebuild) throw new Error('rebuild not begun')\n // Another tab may have superseded this rebuild. Its remaining chunks\n // can still be in a MessagePort queue, so stale identity is a no-op,\n // not a protocol failure.\n if (request.rebuildId !== pendingRebuild.id) return []\n if (!db) throw new Error('index not open')\n ingestIndexRebuildChunk(db, pendingRebuild.generation, request.docs)\n pendingRebuild.done += request.docs.length\n return [\n {\n type: 'progress',\n phase: 'indexing',\n done: pendingRebuild.done,\n total: pendingRebuild.total,\n },\n ]\n }\n\n case 'rebuild-commit': {\n if (!db) throw new Error('index not open')\n if (!pendingRebuild) throw new Error('rebuild not begun')\n if (request.rebuildId !== pendingRebuild.id) return []\n const rebuildId = pendingRebuild.id\n commitIndexRebuild(db, pendingRebuild.generation)\n pendingRebuild = undefined\n const committed = replaceSnapshot(rebuildId)\n return [committed]\n }\n\n case 'ingest': {\n if (!db) throw new Error('index not open')\n const changed: IndexDoc[] = []\n for (const doc of request.docs) {\n if (documentHash(db, conceptKey(doc.concept)) !== hashText(doc.text)) {\n changed.push(doc)\n }\n }\n if (changed.length === 0) return []\n\n const affected = new Set<string>()\n const backlinkTargetsChanged = new Set<string>()\n for (const doc of changed) {\n const key = conceptKey(doc.concept)\n const before = indexedDocumentFacts(db, key)\n const after = indexDocumentFactKeys(doc)\n affected.add(key)\n for (const alias of [...before.aliases, ...after.aliases]) affected.add(alias)\n for (const target of [...before.linkTargets, ...after.linkTargets]) {\n affected.add(target)\n backlinkTargetsChanged.add(target)\n }\n }\n const before = conceptCandidatesForKeys(db, affected)\n // `ingestOne` commits each document independently. Invalidate before the\n // first write so a later failure cannot leave same-worker opens reading a\n // cache which predates a partially-applied batch.\n const previousSnapshot = visibleSnapshot\n visibleSnapshot = undefined\n for (const doc of changed) ingestOne(db, doc)\n const after = conceptCandidatesForKeys(db, affected)\n const delta = deltaFor(\n advanceIndexRevision(db),\n before,\n after,\n backlinkTargetsChanged,\n )\n applyDeltaToSnapshot(previousSnapshot, delta)\n return [{ type: 'delta', ...delta }]\n }\n\n case 'snapshot-request':\n return [snapshot()]\n\n case 'barrier':\n // The serial router invokes this only after every earlier request on the\n // connection has completed. It lets the source clear a durable dirty marker\n // only after the corresponding SQLite ingest or generation commit is visible.\n return [{ type: 'barrier', id: request.id }]\n\n case 'backlinks': {\n const groups = db ? backlinksFor(db, request.concept) : []\n return [{ type: 'backlinks', id: request.id, groups }]\n }\n\n case 'search-text': {\n const page = db\n ? searchText(db, request.query, request.offset, request.limit)\n : { groups: [], hasMore: false }\n return [\n {\n type: 'search-text',\n id: request.id,\n groups: page.groups,\n hasMore: page.hasMore,\n },\n ]\n }\n\n case 'search-count': {\n const count = db\n ? searchTextCount(db, request.query)\n : { total: 0, capped: false }\n return [\n {\n type: 'search-count',\n id: request.id,\n total: count.total,\n capped: count.capped,\n },\n ]\n }\n\n case 'tasks': {\n const page = db\n ? tasksMatching(db, request.query, request.offset, request.limit)\n : { hits: [], hasMore: false }\n return [\n {\n type: 'tasks',\n id: request.id,\n hits: page.hits,\n hasMore: page.hasMore,\n total: db ? tasksMatchingCount(db, request.query) : 0,\n },\n ]\n }\n\n case 'asset-usage': {\n // No database means no evidence, and the caller must NOT read that as\n // \"unreferenced\" — it reads it as \"cannot answer\" and refuses the delete.\n const usage = db ? assetUsage(db, request.needles) : { references: 0, documents: [] }\n return [{ type: 'asset-usage', id: request.id, usage }]\n }\n\n case 'close':\n closeActiveDatabase()\n return []\n }\n }\n\n return {\n async handle(request: IndexRequest): Promise<IndexResponse[]> {\n try {\n return await dispatch(request)\n } catch (err) {\n const graphId = openGraphId\n if (!graphId || !isSqliteCorruption(err)) throw err\n await discardWreckedDatabase(graphId, err)\n // The requester learns its request failed AND why, with the prefix the client\n // keys its recovery on; the fresh (empty) snapshot follows so no tab keeps\n // answering \"exists\" from a generation that no longer does.\n const message = `${INDEX_DISCARDED_PREFIX} ${err instanceof Error ? err.message : String(err)}`\n return [\n {\n type: 'error',\n message,\n ...('id' in request ? { id: request.id } : {}),\n ...('rebuildId' in request ? { rebuildId: request.rebuildId } : {}),\n ...(request.type === 'open' && request.openId !== undefined\n ? { openId: request.openId }\n : {}),\n },\n replaceSnapshot(),\n ]\n }\n },\n dispose() {\n closeActiveDatabase()\n },\n }\n}\n\n/**\n * Prefix on the error a wrecked-and-discarded index reports, so the client can tell \"this\n * request failed\" from \"this request failed and the whole index is now empty — re-derive\".\n */\nexport const INDEX_DISCARDED_PREFIX = 'index-discarded:'\n\n/**\n * Whether an error is SQLite saying the file itself is damaged, as opposed to a bad query or\n * a busy lock. sqlite-wasm throws `SQLite3Error` with a `resultCode`; the message form is\n * matched too because the error may have been re-wrapped on its way here.\n */\nexport function isSqliteCorruption(err: unknown): boolean {\n const code = (err as { resultCode?: unknown } | null)?.resultCode\n if (code === SQLITE_CORRUPT || code === SQLITE_NOTADB) return true\n const message = err instanceof Error ? err.message : String(err)\n return /SQLITE_CORRUPT|SQLITE_NOTADB|disk image is malformed|file is not a database/i.test(message)\n}\n\nconst SQLITE_CORRUPT = 11\nconst SQLITE_NOTADB = 26\n\n/** Re-exported so a host can build the doc shape without reaching into index-db. */\nexport type { IndexDoc }\n","/**\n * The tab's half of the [[Derived Index]] (ADR 0041).\n *\n * It owns everything that needs the documents — which change, when to coalesce, and pulling\n * their text out of the store — because the worker cannot read them. It owns nothing about\n * SQL.\n *\n * The two reads the editor makes constantly (`conceptExists` on every keystroke for\n * missing-link styling, `allConcepts` for wikilink completion and Quick Find) are answered\n * from the snapshot the worker pushes after each ingest, so they stay synchronous and never\n * cross a message boundary. `backlinks` is a real round trip; its two callers tolerate it.\n */\n\nimport type {\n AssetUsage,\n ConceptCandidate,\n DbBacklinkGroup,\n IndexDoc,\n SearchDocumentGroup,\n TaskHit,\n TaskQuery,\n} from '../index-db'\nimport { conceptKey } from '../backlinks/backlink-index'\nimport type {\n IndexChangeCheckpoint,\n IndexSource,\n IndexProgress,\n StoreChange,\n} from '../backlinks/live-index'\nimport { INDEX_DISCARDED_PREFIX } from './core'\nimport type { IndexRequest, IndexResponse } from './protocol'\nimport { performanceRecorder } from '$lib/diagnostics/performance'\n\n/**\n * Dirty documents above which replacing the whole index is cheaper than re-indexing each.\n * Bulk import and external reconciliation can report hundreds of named changes together.\n */\nconst INCREMENTAL_LIMIT = 200\n\n/** Documents per rebuild postMessage — bounds each structured clone to a few hundred KB. */\nconst REBUILD_SEND_CHUNK = 150\n\n/** One cold full derivation per graph across every tab in this browser profile. */\nconst REBUILD_LOCK_PREFIX = 'etherpk-index-rebuild:'\n\n/** One persisted-history reconciliation per graph across every tab in this browser profile. */\nconst CATCH_UP_LOCK_PREFIX = 'etherpk-index-catch-up:'\n\n/** A connection to an index server, wherever it runs. */\nexport interface IndexTransport {\n send(request: IndexRequest): void\n onMessage(listener: (response: IndexResponse) => void): void\n close(): void\n}\n\n/**\n * The transport could not get an index `open` accepted or completed. The workspace may retry\n * this specific failure inline; source/rebuild failures are deliberately not this type, because\n * repeating the same graph walk on the main thread only duplicates work and hides the real error.\n */\nexport class IndexTransportOpenError extends Error {\n override name = 'IndexTransportOpenError'\n}\n\nexport interface RemoteGraphIndex {\n /** Synchronous — from the pushed snapshot; safe on every keystroke. */\n conceptExists(concept: string): boolean\n /** Synchronous — likewise pushed, never a query. */\n allConcepts(): readonly ConceptCandidate[]\n /** A round trip to the index server. */\n backlinks(concept: string): Promise<DbBacklinkGroup[]>\n /**\n * [[Search]]'s text group: one page of matching documents. A round trip, like\n * `backlinks` — which is exactly why Search runs it separately from the name group,\n * whose answers come from the synchronous snapshot above and always land first.\n */\n searchText(query: string, offset: number, limit: number): Promise<SearchTextResult>\n /** How many documents match, capped. Issued in parallel; never blocks the rows. */\n searchTextCount(query: string): Promise<SearchCountResult>\n /**\n * Which documents reference an [[Asset]], and how many times. A round trip, issued once\n * when the user asks to delete one — never speculatively. See ADR 0054 for why the count\n * comes from here rather than from a walk of every document.\n */\n assetUsage(needles: readonly string[]): Promise<AssetUsage>\n /**\n * One page of the [[Tasks View]]'s list, with its total. A round trip like `backlinks` —\n * the Tasks View re-issues it when a filter changes or the index updates, never per\n * keystroke: the Name Filter's typeahead reads the synchronous `allConcepts` snapshot.\n */\n tasks(query: TaskQuery, offset: number, limit: number): Promise<TaskPageResult>\n /**\n * Start the transport/worker open without reading the source. `refresh()` consumes this\n * prepared result, so store hydration and a delayed cross-tab attachment can overlap.\n * Failures remain attached to the prepared result and surface from `refresh()`.\n */\n prepare(): Promise<void>\n /**\n * Open on a persisted generation immediately, then let the source catch up genuine\n * changes in the background. A cold index dispatches a staged full stream before it\n * has results to show.\n */\n refresh(): Promise<void>\n /**\n * Discard and re-derive the whole index from the source, on request. The one thing a\n * user can do when they doubt it: the index is never authoritative and always\n * rebuildable (CONTEXT.md → Derived Index), and until this there was no way to invoke that\n * rule short of clearing site data. Streams through a staged generation and swaps at the\n * end, so queries keep answering from the old one meanwhile; resolves once the swap has\n * been confirmed and the snapshot pushed. A rebuild already in flight is awaited, not\n * doubled.\n */\n rebuild(): Promise<void>\n onUpdated(listener: (update: IndexUpdate) => void): () => void\n /** True when this index survives a reload; false means it was rebuilt from scratch. */\n isPersisted(): boolean\n /**\n * Why persistence is unavailable, when known: 'held' means another live context (typically\n * a stale window) holds this graph's pool, which the user can act on; 'unsupported' means\n * this environment cannot persist at all.\n */\n persistenceBlocked(): 'held' | 'unsupported' | undefined\n /** Observe failover between a temporary and persisted worker after the initial open. */\n onPersistenceChanged(listener: (status: IndexPersistenceStatus) => void): () => void\n /**\n * True while the INITIAL build is still deriving the graph (cold open, nothing\n * persisted). Consumers with an empty snapshot can say \"loading\" instead of showing\n * nothing — the wikilink completion's empty popover read as a hang (live, 2026-07-29).\n */\n isBuilding(): boolean\n dispose(): void\n}\n\nexport interface SearchTextResult {\n groups: SearchDocumentGroup[]\n hasMore: boolean\n}\n\nexport interface SearchCountResult {\n total: number\n /** True when there are more matches than were counted — rendered as `1000+`. */\n capped: boolean\n}\n\nexport interface TaskPageResult {\n hits: TaskHit[]\n hasMore: boolean\n /** Every match, not just this page — uncapped, so the header can say \"1–100 of 412\". */\n total: number\n}\n\nexport interface IndexPersistenceStatus {\n persisted: boolean\n blocked?: 'held' | 'unsupported'\n}\n\nexport interface IndexUpdate {\n full: boolean\n changedConceptKeys: ReadonlySet<string>\n backlinkTargetsChanged: ReadonlySet<string>\n}\n\nexport interface RemoteIndexOptions {\n graphId: string\n debounceMs?: number\n /**\n * How long `refresh()` waits for the worker's first answer before giving up. Generous by\n * default — the first answer includes fetching ~900KB of sqlite wasm — but finite, so a\n * worker that cannot start rejects the open instead of hanging it; the caller then falls\n * back to the inline index (ADR 0041: fail closed).\n */\n openTimeoutMs?: number\n /**\n * Idle patience after a ferried port proves the owner worker adopted it, but before the\n * replayed open reaches that worker. Chromium can split those deliveries across long\n * owner-page scheduling gaps. Defaults to 60 seconds.\n */\n adoptedOpenTimeoutMs?: number\n onProgress?: (progress: IndexProgress) => void\n /** A warm-index catch-up failure must be visible without failing the interactive open. */\n onBackgroundError?: (error: Error) => void\n}\n\nexport function createRemoteGraphIndex(\n source: IndexSource,\n transport: IndexTransport,\n options: RemoteIndexOptions,\n): RemoteGraphIndex {\n const debounceMs = options.debounceMs ?? 300\n let existing = new Set<string>()\n let candidates = new Map<string, ConceptCandidate>()\n let revision = 0\n let persisted = false\n let persistenceBlocked: 'held' | 'unsupported' | undefined\n let persistenceKnown = false\n const persistenceChanged = new Set<(status: IndexPersistenceStatus) => void>()\n /**\n * Armed by the shared transport's `transport-changed` marker and cleared by the next\n * honoured open answer. The fresh-empty-worker recovery below fires only while armed,\n * so duplicate open answers, including the compatibility nudge each ferry candidate carries,\n * can never masquerade as a worker replacement (live, 2026-07-31: every such duplicate\n * scheduled another full derivation during bring-up).\n */\n let workerReplaced = false\n /** Set while refresh()'s initial full derivation runs; cleared by its snapshot. */\n let building = false\n const updated = new Set<(update: IndexUpdate) => void>()\n let timer: ReturnType<typeof setTimeout> | undefined\n let rebuildTail = Promise.resolve()\n /** Source events accumulate immediately, but cannot mutate a worker before open completes. */\n let backgroundRebuildsEnabled = false\n let disposed = false\n let activeRebuildId: string | undefined\n let activeRebuildCompletion:\n | {\n promise: Promise<void>\n resolve: () => void\n reject: (error: Error) => void\n }\n | undefined\n let rebuildInvalidated = false\n /** A concurrent worker ingest is not represented in our source-side dirty set. */\n let rebuildRequiresFullRetry = false\n /** A cold generation must exist before its source begins shared history hydration. */\n let coldCatchUpPending = false\n\n /** Concepts whose content changed, or `null` for \"the document set moved — replace all\". */\n let dirty: Set<string> | null = new Set()\n\n let nextId = 1\n let nextOpenId = 1\n const pending = new Map<number, (response: IndexResponse) => void>()\n let onOpened: ((response: Extract<IndexResponse, { type: 'opened' }>) => void) | undefined\n let onOpenAttached: (() => void) | undefined\n /** Stops open-startup patience once the worker confirms that it accepted the request. */\n let onOpenAccepted: ((openId: number) => void) | undefined\n /** A replay onto a replacement worker needs a fresh startup-patience window. */\n let onOpenTransportChanged: (() => void) | undefined\n let awaitedOpenId: number | undefined\n let onOpenFailed: ((error: Error) => void) | undefined\n const rebuildLockAbort = new AbortController()\n /** Any inbound worker message counts as life; see openIndex's activity-based patience. */\n let lastTransportActivity = performance.now()\n let transportGeneration = 0\n type OpenedResponse = Extract<IndexResponse, { type: 'opened' }>\n interface PreparedOpen {\n openId: number\n generation: number\n settled: boolean\n promise: Promise<OpenedResponse>\n }\n let preparedOpen: PreparedOpen | undefined\n\n transport.onMessage((response) => {\n lastTransportActivity = performance.now()\n switch (response.type) {\n case 'snapshot': {\n // Snapshots pushed after the initial rebuild started mean it has content\n // to show (the empty pre-rebuild snapshot arrives WITH 'opened', before\n // `building` is ever set).\n if (!building || response.rebuildId) building = false\n revision = response.revision\n // A full replacement can remove real concepts and pageless candidates.\n // Include both generations so an open editor restyles links which changed\n // from existing to missing, or disappeared from completion entirely.\n const changedConceptKeys = new Set([\n ...existing,\n ...candidates.keys(),\n ...response.existing,\n ...response.candidates.map((candidate) => candidate.key),\n ])\n existing = new Set(response.existing)\n candidates = new Map(response.candidates.map((candidate) => [candidate.key, candidate]))\n for (const listener of updated) {\n listener({\n full: true,\n changedConceptKeys,\n backlinkTargetsChanged: new Set(),\n })\n }\n if (response.rebuildId && activeRebuildId) {\n // A different id means another tab superseded our stream and committed\n // an authoritative full generation. Either way, our stream no longer\n // owns the pending rebuild.\n const shouldRetry = rebuildInvalidated\n const completion = activeRebuildCompletion\n if (rebuildRequiresFullRetry) dirty = null\n activeRebuildId = undefined\n activeRebuildCompletion = undefined\n rebuildInvalidated = false\n rebuildRequiresFullRetry = false\n completion?.resolve()\n // A cold generation is only the cache snapshot. Even when a foreground\n // relay update invalidated it while streaming, post-commit catch-up is\n // the one authoritative retry path. Scheduling another full derivation\n // here duplicated the entire graph before reconciliation had even begun.\n if (coldCatchUpPending && !disposed) {\n coldCatchUpPending = false\n catchUpWarmIndex({ verifyExternalSource: false })\n } else if (shouldRetry && !disposed) {\n queueRebuild()\n }\n }\n return\n }\n case 'delta': {\n if (response.revision !== revision + 1) {\n transport.send({ type: 'snapshot-request' })\n return\n }\n revision = response.revision\n if (activeRebuildId) {\n // Another attached tab changed the worker's visible generation while our\n // staged replacement was being built. The staged commit would overwrite\n // that delta, and its source concept is not necessarily known in this tab.\n rebuildInvalidated = true\n rebuildRequiresFullRetry = true\n }\n for (const key of response.existingAdded) existing.add(key)\n for (const key of response.existingRemoved) existing.delete(key)\n for (const candidate of response.candidateUpserts) {\n candidates.set(candidate.key, candidate)\n }\n for (const key of response.candidateRemoved) candidates.delete(key)\n const changedConceptKeys = new Set([\n ...response.existingAdded,\n ...response.existingRemoved,\n ])\n const update: IndexUpdate = {\n full: false,\n changedConceptKeys,\n backlinkTargetsChanged: new Set(response.backlinkTargetsChanged),\n }\n performanceRecorder.mark('index.delta.received', {\n bytes: new TextEncoder().encode(JSON.stringify(response)).byteLength,\n changed: changedConceptKeys.size,\n })\n for (const listener of updated) listener(update)\n return\n }\n case 'progress':\n options.onProgress?.({ phase: 'indexing', done: response.done, total: response.total })\n return\n case 'attached':\n // The owner's worker has adopted the ferried port. Chromium can still defer\n // the replayed inbound open until a later owner-page scheduling gap, so move\n // from the short \"worker never started\" budget to adoption patience.\n onOpenAttached?.()\n return\n case 'open-accepted':\n // This is stronger than a generic liveness pulse: the open has reached the\n // worker and is now ordered behind any existing core work. A storm-history\n // catch-up can keep that queue busy for minutes, but abandoning it for an\n // inline rebuild would duplicate the whole graph and lose persistence.\n onOpenAccepted?.(response.openId)\n return\n case 'transport-changed':\n workerReplaced = true\n transportGeneration += 1\n if (preparedOpen) {\n if (preparedOpen.settled) {\n // The sharing facade replays its latest correlated open immediately\n // after this marker. Start waiting before that synchronous replay is\n // sent, rather than issuing a duplicate open from refresh later.\n preparedOpen = createPreparedOpen({\n openId: preparedOpen.openId,\n send: false,\n })\n } else {\n // The same in-flight request is replayed. Move its result onto the new\n // generation and restore the timer which open-accepted may have stopped\n // for the worker that just disappeared.\n preparedOpen.generation = transportGeneration\n onOpenTransportChanged?.()\n }\n } else {\n // Cold refresh re-opens directly while holding the rebuild lock rather\n // than through the preparation cache. Its replay needs the same renewed\n // failure detection.\n onOpenTransportChanged?.()\n }\n return\n case 'opened': {\n // Compatibility ferrying can replay opens. Never let a late answer for an\n // older correlated request settle the open currently awaited by this tab.\n if (\n onOpened &&\n response.openId !== undefined &&\n response.openId !== awaitedOpenId\n ) {\n return\n }\n const persistenceDidChange =\n !persistenceKnown ||\n persisted !== response.persisted ||\n persistenceBlocked !== response.persistenceBlocked\n persisted = response.persisted\n persistenceBlocked = response.persistenceBlocked\n persistenceKnown = true\n if (persistenceDidChange) {\n const status = {\n persisted,\n ...(persistenceBlocked ? { blocked: persistenceBlocked } : {}),\n }\n for (const listener of persistenceChanged) listener(status)\n }\n if (onOpened) {\n workerReplaced = false\n onOpened(response)\n } else if (!workerReplaced) {\n // A duplicate answer from the ferry's compatibility nudge, not a\n // replacement. Ignoring it is the whole point of the marker.\n } else if (response.indexedDocuments === 0) {\n // Unsolicited after a real swap, and empty: the shared transport moved\n // onto a fresh worker (owner failover, reclaiming a dead holder's pool,\n // or giving up on an unreachable owner, ADR 0042 §4) whose database\n // holds nothing. Its snapshot has just wiped this tab's concept caches;\n // without a rebuild every wikilink would style as missing from here on\n // (live, 2026-07-28).\n workerReplaced = false\n if (building) {\n // A cold caller has no usable generation and must fall back rather\n // than publish the empty caches from the replacement worker.\n failActiveRebuild(new Error('the index worker was replaced during rebuild'))\n } else {\n // A detached rebuild is opportunistic. Cancel its superseded stream\n // quietly and let the replacement worker's scheduled rebuild take over.\n cancelActiveRebuild()\n }\n rebuildInvalidated = false\n rebuildRequiresFullRetry = false\n scheduleRebuild()\n } else {\n // Unsolicited after a real swap, with content: the replacement pool is\n // adopted as-is, exactly as before the swap markers existed. Known gap,\n // deliberately unchanged here: a takeover of a pool whose last commit\n // predates recent edits serves that older state until the next edit or\n // full refresh reconciles it.\n workerReplaced = false\n }\n return\n }\n default: {\n const id = 'id' in response ? response.id : undefined\n if (id !== undefined) {\n pending.get(id)?.(response)\n pending.delete(id)\n return\n }\n // An error with no request id is the worker itself failing (its script or\n // wasm would not load). If an open is in flight, it must reject, not hang.\n if (response.type === 'error') {\n if (\n onOpenFailed &&\n response.openId !== undefined &&\n response.openId !== awaitedOpenId\n ) {\n return\n }\n // A rebuild stream's chunks and commit are fire-and-forget, so its\n // failures can arrive after the stream has already been failed and a\n // replacement started — a wrecked index does exactly this: the discard\n // fails the stream, then the commit it had already posted reports\n // 'rebuild not begun'. Those must not fail the replacement.\n if (response.rebuildId !== undefined && response.rebuildId !== activeRebuildId) return\n const error = new Error(response.message)\n onOpenFailed?.(error)\n failActiveRebuild(error)\n recoverIfDiscarded(error)\n }\n }\n }\n })\n\n function newRebuildCompletion() {\n let resolve!: () => void\n let reject!: (error: Error) => void\n const promise = new Promise<void>((onResolve, onReject) => {\n resolve = onResolve\n reject = onReject\n })\n // Detached rebuilds keep their rejection handled here as well as at the caller so\n // disposing or replacing a worker cannot produce an unhandled rejection between\n // those two turns of the microtask queue.\n void promise.catch(() => {})\n return { promise, resolve, reject }\n }\n\n function failActiveRebuild(error: Error): void {\n const completion = activeRebuildCompletion\n activeRebuildId = undefined\n activeRebuildCompletion = undefined\n building = false\n completion?.reject(error)\n }\n\n const isIndexDiscarded = (error: Error) => error.message.startsWith(INDEX_DISCARDED_PREFIX)\n\n /**\n * The worker found its stored index corrupt, threw it away and is now serving an EMPTY one\n * (core.ts). Whatever request tripped over it has already failed; what matters is that the\n * graph gets re-derived without anyone asking — the sidebars are answering \"nothing\" until\n * it does. Queued through the ordinary debounce so it lands behind whatever the failure\n * interrupted, and as a full replacement, because there is nothing left to be incremental\n * against.\n */\n function recoverIfDiscarded(error: Error): void {\n if (disposed || !isIndexDiscarded(error)) return\n dirty = null\n queueRebuild()\n }\n\n function cancelActiveRebuild(): void {\n const completion = activeRebuildCompletion\n activeRebuildId = undefined\n activeRebuildCompletion = undefined\n building = false\n completion?.resolve()\n }\n\n function request<T extends IndexResponse>(build: (id: number) => IndexRequest): Promise<T> {\n const id = nextId++\n return new Promise<T>((resolve, reject) => {\n pending.set(id, (response) => {\n if (response.type === 'error') {\n const error = new Error(response.message)\n recoverIfDiscarded(error)\n reject(error)\n } else resolve(response as T)\n })\n transport.send(build(id))\n })\n }\n\n /**\n * The ownership lock in share.ts protects the ONE worker, not the source-side graph walk.\n * Several cold tabs can attach to that worker before its first generation commits, observe\n * the same empty database and each start streaming every document. Serialize that decision\n * separately, then re-open under the lock so followers see the winner's committed result.\n */\n async function withRebuildLock(work: () => Promise<void>): Promise<void> {\n const manager =\n typeof navigator !== 'undefined' &&\n typeof navigator.locks?.request === 'function'\n ? navigator.locks\n : undefined\n if (!manager) {\n await work()\n return\n }\n let entered = false\n try {\n await manager.request(\n REBUILD_LOCK_PREFIX + options.graphId,\n { mode: 'exclusive', signal: rebuildLockAbort.signal },\n async () => {\n entered = true\n if (!disposed) await work()\n },\n )\n } catch (error) {\n if (disposed || rebuildLockAbort.signal.aborted) return\n // A failure inside the protected work is authoritative. Only an unavailable\n // lock service degrades to the old single-tab behaviour.\n if (entered) throw error\n await work()\n }\n }\n\n /**\n * Source catch-up reads and advances shared cache watermarks. Serialising that read/advance\n * pair stops every newly-opened tab from downloading and decrypting the same long server\n * history concurrently. The follower re-checks watermarks only after the winner commits.\n */\n async function withCatchUpLock(work: () => Promise<void>): Promise<void> {\n const manager =\n typeof navigator !== 'undefined' &&\n typeof navigator.locks?.request === 'function'\n ? navigator.locks\n : undefined\n if (!manager) {\n await work()\n return\n }\n let entered = false\n try {\n await manager.request(\n CATCH_UP_LOCK_PREFIX + options.graphId,\n { mode: 'exclusive', signal: rebuildLockAbort.signal },\n async () => {\n entered = true\n if (!disposed) await work()\n },\n )\n } catch (error) {\n if (disposed || rebuildLockAbort.signal.aborted) return\n if (entered) throw error\n await work()\n }\n }\n\n async function openIndex({\n openId = nextOpenId++,\n send = true,\n }: {\n openId?: number\n send?: boolean\n } = {}): Promise<Extract<IndexResponse, { type: 'opened' }>> {\n if (disposed) throw new IndexTransportOpenError('the index was disposed')\n try {\n return await new Promise<Extract<IndexResponse, { type: 'opened' }>>(\n (resolve, reject) => {\n // Startup patience is ACTIVITY-based, not a fixed cliff. Ferry attempts\n // keep it alive until a worker accepts the open. Once `open-accepted`\n // arrives the request is safely ordered in that worker's serial queue,\n // so there is deliberately no client deadline on legitimate work ahead\n // of it. A worker which cannot start never acknowledges and still fails\n // closed. The worker owns a separate terminal safety bound.\n const timeoutMs = options.openTimeoutMs ?? 15_000\n const adoptedTimeoutMs = options.adoptedOpenTimeoutMs ?? 60_000\n let idleTimeoutMs = timeoutMs\n awaitedOpenId = openId\n const cadence = Math.max(25, Math.min(1_000, Math.floor(timeoutMs / 2)))\n let startedAt = performance.now()\n let timer: ReturnType<typeof setInterval> | undefined\n const stopStartupTimer = () => {\n if (timer === undefined) return\n clearInterval(timer)\n timer = undefined\n }\n const startStartupTimer = () => {\n stopStartupTimer()\n startedAt = performance.now()\n timer = setInterval(() => {\n const idleSince = Math.max(startedAt, lastTransportActivity)\n if (performance.now() - idleSince >= idleTimeoutMs) {\n stopStartupTimer()\n reject(new Error('the index worker did not answer'))\n }\n }, cadence)\n }\n startStartupTimer()\n onOpenAccepted = (acceptedOpenId) => {\n if (acceptedOpenId === openId) stopStartupTimer()\n }\n onOpenTransportChanged = () => {\n idleTimeoutMs = timeoutMs\n startStartupTimer()\n }\n onOpenAttached = () => {\n idleTimeoutMs = Math.max(idleTimeoutMs, adoptedTimeoutMs)\n }\n onOpened = (response) => {\n stopStartupTimer()\n resolve(response)\n }\n onOpenFailed = (error) => {\n stopStartupTimer()\n reject(error)\n }\n if (send) transport.send({ type: 'open', graphId: options.graphId, openId })\n },\n ).finally(() => {\n onOpened = undefined\n onOpenAttached = undefined\n onOpenAccepted = undefined\n onOpenTransportChanged = undefined\n onOpenFailed = undefined\n awaitedOpenId = undefined\n })\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n throw new IndexTransportOpenError(message)\n }\n }\n\n function createPreparedOpen({\n openId,\n send,\n }: {\n openId?: number\n send?: boolean\n } = {}): PreparedOpen {\n const correlatedOpenId = openId ?? nextOpenId++\n const pending = openIndex({ openId: correlatedOpenId, send })\n const next: PreparedOpen = {\n openId: correlatedOpenId,\n generation: transportGeneration,\n settled: false,\n promise: pending.then(\n (response) => {\n next.settled = true\n return response\n },\n (error) => {\n next.settled = true\n throw error\n },\n ),\n }\n // GraphWorkspace deliberately starts preparation before awaiting it. Attach a\n // rejection observer now, while preserving the same rejecting promise for refresh.\n void next.promise.catch(() => {})\n return next\n }\n\n function ensurePreparedOpen(): PreparedOpen {\n if (preparedOpen?.generation === transportGeneration) return preparedOpen\n preparedOpen = createPreparedOpen()\n return preparedOpen\n }\n\n async function preparedOpenResponse(consume: boolean): Promise<OpenedResponse> {\n for (;;) {\n const candidate = ensurePreparedOpen()\n let response: OpenedResponse\n try {\n response = await candidate.promise\n } catch (error) {\n if (candidate.generation !== transportGeneration) {\n if (preparedOpen === candidate) preparedOpen = undefined\n continue\n }\n throw error\n }\n if (candidate.generation !== transportGeneration) {\n if (preparedOpen === candidate) preparedOpen = undefined\n continue\n }\n if (consume && preparedOpen === candidate) preparedOpen = undefined\n return response\n }\n }\n\n function catchUpWarmIndex({ verifyExternalSource = true } = {}): void {\n // `dirty` is the monotonic queue of source events observed since subscription.\n // A target preload can emit while a busy shared worker is still answering `open`;\n // clearing here would lose that event after its Local Cache watermark had already\n // advanced. Only `rebuild()` may take and reset the queue. Repeated named changes\n // are harmless because the worker's content hash makes ingest idempotent.\n // The persisted generation is already the browser's durable derived state.\n // A synced source catches documents up from its stored sequence watermarks;\n // only genuine remote changes emit named `onChange` events and cross into the\n // worker. Externally mutable sources without that seam retain the safe full\n // verification path.\n const catchUp = Promise.resolve().then(() =>\n withCatchUpLock(async () => {\n if (source.catchUpPersistedIndex) {\n await source.catchUpPersistedIndex()\n // Cache watermarks and the SQLite index live in different databases. Drain\n // the durable source journal and wait for the worker commit before releasing\n // the cross-tab lock, otherwise a close in this gap can make the stale index\n // look permanently current to the next tab.\n if (timer !== undefined) {\n clearTimeout(timer)\n timer = undefined\n }\n await runRebuild()\n return\n }\n // A follower which just waited for this browser's rebuild lock observed the\n // generation that the winner committed moments ago. Re-reading the same\n // filesystem source immediately would be a second derivation in disguise.\n if (!verifyExternalSource) return\n dirty = null\n await runRebuild()\n }),\n )\n void catchUp.catch((error: unknown) =>\n options.onBackgroundError?.(\n error instanceof Error ? error : new Error(String(error)),\n ),\n )\n }\n\n /** Hand the worker every document. Used when nothing usable is persisted. */\n async function rebuildAll(\n checkpointStream?: IndexChangeCheckpoint['streamForIndex'],\n ): Promise<void> {\n if (disposed) return\n if (activeRebuildId) {\n await activeRebuildCompletion?.promise\n return\n }\n performanceRecorder.mark('index.rebuild.full')\n const rebuildId = crypto.randomUUID()\n activeRebuildId = rebuildId\n const completion = newRebuildCompletion()\n activeRebuildCompletion = completion\n rebuildInvalidated = false\n rebuildRequiresFullRetry = false\n let total: number\n let batches: AsyncIterable<readonly IndexDoc[]>\n try {\n const streamSource = checkpointStream ?? source.streamForIndex\n if (streamSource) {\n const stream = await streamSource({\n onProgress: options.onProgress,\n })\n total = stream.total\n batches = stream.batches\n } else {\n const docs = await source.snapshotForIndex({ onProgress: options.onProgress })\n total = docs.length\n batches = (async function* () {\n for (let start = 0; start < docs.length; start += REBUILD_SEND_CHUNK) {\n yield docs.slice(start, start + REBUILD_SEND_CHUNK)\n }\n })()\n }\n if (disposed || activeRebuildId !== rebuildId) {\n await completion.promise\n return\n }\n transport.send({ type: 'rebuild-begin', rebuildId, total })\n let sent = 0\n for await (const batch of batches) {\n if (disposed || activeRebuildId !== rebuildId) {\n await completion.promise\n return\n }\n if (sent > 0) await new Promise((resolve) => setTimeout(resolve))\n const docs = [...batch]\n transport.send({ type: 'rebuild-docs', rebuildId, docs })\n sent += docs.length\n }\n if (!disposed && activeRebuildId === rebuildId) {\n transport.send({ type: 'rebuild-commit', rebuildId })\n // Posting the commit only puts it in the worker's queue. A cold workspace\n // cannot mount editors until the worker confirms that the generation was\n // atomically swapped and pushes the caches used for wikilink styling.\n await completion.promise\n }\n } catch (error) {\n const reported = error instanceof Error ? error : new Error(String(error))\n if (activeRebuildId === rebuildId) failActiveRebuild(reported)\n throw reported\n }\n }\n\n async function rebuild(): Promise<void> {\n if (disposed) return\n const checkpoint = await source.pendingIndexChanges?.()\n const checkpointDocuments = new Map(\n (checkpoint?.documents ?? []).map((document) => [\n conceptKey(document.concept),\n document,\n ]),\n )\n if (checkpoint?.changes === null) {\n dirty = null\n } else if (checkpoint && dirty !== null) {\n for (const change of checkpoint.changes) dirty.add(change.concept)\n }\n const changed = dirty\n dirty = new Set()\n try {\n const checkpointCoversChanged =\n changed !== null &&\n [...changed].every((concept) =>\n checkpointDocuments.has(conceptKey(concept)),\n )\n if (\n changed === null ||\n (changed.size > INCREMENTAL_LIMIT && !checkpointCoversChanged)\n ) {\n await rebuildAll(checkpoint?.streamForIndex)\n } else if (changed.size > 0) {\n const docs: IndexDoc[] = []\n for (const concept of changed) {\n const doc =\n checkpointDocuments.get(conceptKey(concept)) ??\n (await source.snapshotDocument?.(concept))\n if (doc) docs.push(doc)\n }\n if (docs.length !== changed.size) {\n // A named document disappeared before its snapshot was read. Only a full\n // replacement can remove its old rows safely.\n await rebuildAll(checkpoint?.streamForIndex)\n } else {\n // A cold post-commit hydration can make every cache row dirty at once.\n // Those rows already have exact, cache-bound snapshots, so replacing the\n // just-committed generation would be a duplicate derivation. Send bounded\n // targeted chunks instead and acknowledge them behind one ordered barrier.\n for (let start = 0; start < docs.length; start += REBUILD_SEND_CHUNK) {\n transport.send({\n type: 'ingest',\n docs: docs.slice(start, start + REBUILD_SEND_CHUNK),\n })\n }\n await request<Extract<IndexResponse, { type: 'barrier' }>>((id) => ({\n type: 'barrier',\n id,\n }))\n }\n }\n await checkpoint?.acknowledge()\n } catch (error) {\n // Keep volatile work retryable in this session. A durable checkpoint remains\n // unacknowledged as the cross-restart source of truth as well.\n if (changed === null) dirty = null\n else if (dirty !== null) for (const concept of changed) dirty.add(concept)\n throw error\n }\n }\n\n /** Source events and the terminal catch-up flush share one ordered main-thread pipeline. */\n function runRebuild(): Promise<void> {\n const next = rebuildTail.then(() => rebuild())\n rebuildTail = next.catch(() => undefined)\n return next\n }\n\n function queueRebuild(): void {\n if (timer !== undefined) clearTimeout(timer)\n timer = setTimeout(() => {\n timer = undefined\n void runRebuild().catch((error: unknown) =>\n options.onBackgroundError?.(\n error instanceof Error ? error : new Error(String(error)),\n ),\n )\n }, debounceMs)\n }\n\n function scheduleRebuild(change?: StoreChange): void {\n if (!change || !source.snapshotDocument) dirty = null\n else if (dirty !== null) dirty.add(change.concept)\n if (activeRebuildId) {\n rebuildInvalidated = true\n return\n }\n if (!backgroundRebuildsEnabled) return\n queueRebuild()\n }\n\n const unsubscribe = source.onChange(scheduleRebuild)\n\n return {\n conceptExists: (concept) => existing.has(conceptKey(concept)),\n allConcepts: () =>\n [...candidates.values()].sort((a, b) => a.key.localeCompare(b.key)),\n async backlinks(concept) {\n const response = await request<Extract<IndexResponse, { type: 'backlinks' }>>((id) => ({\n type: 'backlinks',\n id,\n concept,\n }))\n return response.groups\n },\n async assetUsage(needles) {\n const response = await request<Extract<IndexResponse, { type: 'asset-usage' }>>((id) => ({\n type: 'asset-usage',\n id,\n needles: [...needles],\n }))\n return response.usage\n },\n async searchText(query, offset, limit) {\n const response = await request<Extract<IndexResponse, { type: 'search-text' }>>(\n (id) => ({ type: 'search-text', id, query, offset, limit }),\n )\n return { groups: response.groups, hasMore: response.hasMore }\n },\n async searchTextCount(query) {\n const response = await request<Extract<IndexResponse, { type: 'search-count' }>>(\n (id) => ({ type: 'search-count', id, query }),\n )\n return { total: response.total, capped: response.capped }\n },\n async tasks(query, offset, limit) {\n const response = await request<Extract<IndexResponse, { type: 'tasks' }>>((id) => ({\n type: 'tasks',\n id,\n query,\n offset,\n limit,\n }))\n return { hits: response.hits, hasMore: response.hasMore, total: response.total }\n },\n prepare() {\n const preparation = preparedOpenResponse(false).then(() => {})\n // A caller may intentionally fire-and-overlap this with store hydration. The\n // authoritative failure is still returned and later rethrown by refresh.\n void preparation.catch(() => {})\n return preparation\n },\n isPersisted: () => persisted,\n persistenceBlocked: () => persistenceBlocked,\n onPersistenceChanged(listener) {\n persistenceChanged.add(listener)\n return () => persistenceChanged.delete(listener)\n },\n isBuilding: () => building,\n async refresh() {\n const opened = await preparedOpenResponse(true)\n // Something usable was already on disk. Trust it for the interactive open.\n // Subsequent document deltas correct changed rows, and an explicit full refresh\n // streams content through a staged generation before swapping it into view.\n if (opened.indexedDocuments > 0) {\n backgroundRebuildsEnabled = true\n catchUpWarmIndex()\n return\n }\n // Keep every follower on GraphWorkspace's loading surface while the first tab\n // derives and commits the shared generation.\n building = true\n await withRebuildLock(async () => {\n // Another tab may have completed the graph walk while this tab waited.\n // Re-open is idempotent in the shared worker and avoids touching the source.\n const afterLock = await openIndex()\n if (afterLock.indexedDocuments > 0) {\n building = false\n backgroundRebuildsEnabled = true\n catchUpWarmIndex({ verifyExternalSource: false })\n return\n }\n dirty = null\n coldCatchUpPending = true\n await runRebuild()\n backgroundRebuildsEnabled = true\n })\n },\n async rebuild() {\n if (disposed) return\n // A null queue is the full-replacement signal `rebuild()` already honours for an\n // over-limit change set: every document is streamed, the worker inserts each into\n // the staged generation without consulting its stored hash, and the commit swaps\n // the whole thing. Queued on the same ordered tail as source events, so a keystroke\n // landing mid-rebuild is not lost — it is ingested after the swap.\n dirty = null\n try {\n await runRebuild()\n } catch (error) {\n // The rebuild is precisely what a user reaches for when the index is wrecked,\n // and a wreck fails the first attempt by discarding itself. Once over.\n if (!(error instanceof Error) || !isIndexDiscarded(error)) throw error\n dirty = null\n await runRebuild()\n }\n },\n onUpdated(listener) {\n updated.add(listener)\n return () => updated.delete(listener)\n },\n dispose() {\n disposed = true\n onOpenFailed?.(new Error('the index was disposed'))\n preparedOpen = undefined\n rebuildLockAbort.abort()\n // Disposal is an expected workspace lifecycle event, not a background failure.\n cancelActiveRebuild()\n if (timer !== undefined) clearTimeout(timer)\n unsubscribe()\n updated.clear()\n persistenceChanged.clear()\n transport.send({ type: 'close' })\n transport.close()\n },\n }\n}\n","/**\n * Choosing where the index server runs, and talking to it (ADR 0041 §2–§4).\n *\n * Two hosts, one protocol:\n *\n * 1. **Dedicated Worker**: the elected graph owner persists the index to OPFS and serves\n * ferried ports from every other tab. A temporary follower worker can be configured\n * memory-only while a known owner is unreachable (ADR 0042).\n * 2. **Inline** — no Worker at all (node, vitest, a CSP that refuses one). Same core, same\n * messages, on the calling thread.\n *\n * **Why not a SharedWorker**, which ADR 0041 originally chose for cross-tab sharing: measured\n * in Chromium 149, a SharedWorker cannot use the `opfs-sahpool` VFS (\"Missing required OPFS\n * APIs\") and cannot spawn a nested Worker to hold it either (\"Worker is not defined\"). Sharing\n * and persistence are therefore mutually exclusive, and persistence is the half that fixes a\n * multi-second open. See ADR 0041's amendment.\n *\n * Nothing above this file knows which one it got.\n */\n\nimport { createIndexCore, type IndexDbHost } from './core'\nimport { openInMemorySqlDb } from '../index-db-sqlite'\nimport type { IndexResponse } from './protocol'\nimport type { IndexTransport } from './client'\n\n/** In-memory host: no OPFS, so nothing survives the session. */\nexport function memoryDbHost(blocked?: 'held' | 'unsupported'): IndexDbHost {\n return {\n async open() {\n return {\n db: await openInMemorySqlDb(),\n persisted: false,\n ...(blocked ? { blocked } : {}),\n }\n },\n async discard() {\n return {\n db: await openInMemorySqlDb(),\n persisted: false,\n ...(blocked ? { blocked } : {}),\n }\n },\n }\n}\n\n/** Drive the core directly on this thread — the last-resort host, and the one tests use. */\nexport function inlineTransport(host: IndexDbHost = memoryDbHost()): IndexTransport {\n const core = createIndexCore(host)\n const listeners = new Set<(response: IndexResponse) => void>()\n // One request at a time, in order — the same contract the worker's connection router\n // gives the core. Unserialised, a request arriving while the core is mid-way through an\n // async step (discarding a corrupt database, say) saw no database at all.\n let tail: Promise<void> = Promise.resolve()\n return {\n send(request) {\n tail = tail\n .then(() => core.handle(request))\n .then((responses) => {\n for (const response of responses) for (const l of listeners) l(response)\n })\n .catch((err: unknown) => {\n const message = err instanceof Error ? err.message : String(err)\n // Shaped as the connection router shapes it: the failed request's id, and\n // its rebuild stream, so the client can match and discount it.\n for (const l of listeners) {\n l({\n type: 'error',\n message,\n ...('id' in request ? { id: request.id } : {}),\n ...('rebuildId' in request ? { rebuildId: request.rebuildId } : {}),\n ...(request.type === 'open' && request.openId !== undefined\n ? { openId: request.openId }\n : {}),\n })\n }\n })\n },\n onMessage(listener) {\n listeners.add(listener)\n },\n close() {\n listeners.clear()\n core.dispose()\n },\n }\n}\n\n/** Wrap a MessagePort or a Worker in the transport shape. */\nexport function portTransport(port: MessagePort | Worker, stop: () => void): IndexTransport {\n const listeners = new Set<(response: IndexResponse) => void>()\n port.addEventListener('message', (event) => {\n const response = (event as MessageEvent<IndexResponse>).data\n for (const l of listeners) l(response)\n })\n // A worker whose SCRIPT or wasm cannot load fires 'error', never 'message'. Without\n // this, the client awaited 'opened' forever and the graph never finished opening\n // (live, 2026-07-28: a broken service worker rejecting the worker's fetches).\n port.addEventListener('error', (event) => {\n const message = (event as ErrorEvent).message || 'the index worker failed to start'\n for (const l of listeners) l({ type: 'error', message })\n })\n if ('start' in port) port.start()\n return {\n send: (request) => port.postMessage(request),\n onMessage: (listener) => listeners.add(listener),\n close() {\n listeners.clear()\n stop()\n },\n }\n}\n\n/** A dedicated index worker plus the hooks that ferry other tabs' ports into it (ADR 0042). */\nexport interface DedicatedWorkerHost {\n transport: IndexTransport\n adopt(port: MessagePort): void\n /**\n * Give the worker a port the BROKER holds the other end of, so ferried client ports\n * reach the worker without crossing this page's main thread. A wedged owner page must\n * not starve every other tab's attach (live, 2026-07-31).\n */\n adoptBridge(port: MessagePort): void\n}\n\nexport interface DedicatedWorkerHostOptions {\n /** A temporary follower already knows another worker owns OPFS, so it must use memory. */\n persistence?: 'opfs' | 'memory'\n}\n\n/** Spawn the dedicated index worker, or null where Workers do not exist. */\nexport function createDedicatedWorkerHost(\n options: DedicatedWorkerHostOptions = {},\n): DedicatedWorkerHost | null {\n if (typeof Worker === 'undefined') return null\n try {\n const worker = new Worker(new URL('./index-worker.ts', import.meta.url), { type: 'module' })\n if (options.persistence === 'memory') {\n // Worker messages from one sender are ordered. Configure the host before the\n // queued `open` is replayed, avoiding a futile 12-second OPFS contention loop\n // when a live owner is already known to hold this graph's pool.\n worker.postMessage({ type: 'configure-host', persistence: 'memory' })\n }\n return {\n transport: portTransport(worker, () => worker.terminate()),\n adopt: (port) => worker.postMessage({ type: 'adopt-client' }, [port]),\n adoptBridge: (port) => worker.postMessage({ type: 'adopt-bridge' }, [port]),\n }\n } catch {\n return null\n }\n}\n\n/**\n * The best transport this browser can give us. Never throws: every failure step degrades to\n * the next host, because an index that cannot be shared or persisted is a slower app, while\n * no index at all is a broken one.\n */\nexport function createIndexTransport(): IndexTransport {\n return createDedicatedWorkerHost()?.transport ?? inlineTransport()\n}\n\n/** A worker-backed in-memory index for temporary service while another tab owns OPFS. */\nexport function createMemoryIndexTransport(): IndexTransport {\n return (\n createDedicatedWorkerHost({ persistence: 'memory' })?.transport ??\n inlineTransport(memoryDbHost('held'))\n )\n}\n","/**\n * Pure date helpers for the month grid both calendars paint — the [[Journal Calendar]] in the\n * [[Sidebar]] and the Command Menu's pick-a-date popover (Dual Mode Editor.md → *Command Menu*,\n * \"Date Picker\"). No DOM, no CodeMirror: the ISO format a [[Journal Concept]] is written in, a\n * Monday-start month grid, and the twelve-month grid the year view pages by. The renderer that\n * turns these into elements is `month-grid.ts`.\n *\n * Everything here is **local time**. A journal entry belongs to the day its author was living\n * in, not to a UTC instant, so `formatISODate(new Date())` is the one definition of \"today\" the\n * app uses (ADR 0056).\n */\n\n/** A single calendar cell. `iso` is the `YYYY-MM-DD` the day resolves to. */\nexport interface DayCell {\n year: number\n /** 0-based month. */\n month: number\n day: number\n /** False for the leading/trailing days that belong to the adjacent month. */\n inMonth: boolean\n iso: string\n}\n\n/** `YYYY-MM-DD` for a local date — the journal file-name format (fs/identity.ts). */\nexport function formatISODate(date: Date): string {\n const y = date.getFullYear()\n const m = String(date.getMonth() + 1).padStart(2, '0')\n const d = String(date.getDate()).padStart(2, '0')\n return `${y}-${m}-${d}`\n}\n\n/** Today, as the ISO day the user is living in. The app's single definition of \"today\". */\nexport function todayISO(now: Date = new Date()): string {\n return formatISODate(now)\n}\n\n/**\n * Milliseconds from `now` until the next local midnight — when `todayISO()` changes.\n *\n * For anything painted from \"today\" that stays on screen for days: a tab left open overnight\n * keeps the old day's mark until it repaints, so callers schedule one repaint this far ahead.\n * Local, like everything here, so a DST change lands on the calendar day the clock does. Always\n * at least 1ms: a timer set exactly at midnight must not fire in the same day again.\n */\nexport function msUntilNextLocalMidnight(now: Date = new Date()): number {\n const next = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1)\n return Math.max(1, next.getTime() - now.getTime())\n}\n\nconst ISO_SHAPE = /^(\\d{4})-(\\d{2})-(\\d{2})$/\n\n/**\n * The local `Date` a `YYYY-MM-DD` string names, or `null` when it names no day that exists.\n *\n * The shape test alone is not enough: `2026-02-30` and `2026-13-01` match it and are not days.\n * Round-tripping through `Date` catches both, because JS silently rolls an out-of-range field\n * forward (31 February becomes 3 March) and the re-format then disagrees with the input.\n */\nexport function parseISODate(value: string): Date | null {\n const match = ISO_SHAPE.exec(value.trim())\n if (!match) return null\n const [, y, m, d] = match\n const date = new Date(Number(y), Number(m) - 1, Number(d))\n return formatISODate(date) === value.trim() ? date : null\n}\n\n/** True when `value` names a calendar day that actually exists. */\nexport function isCalendarDay(value: string): boolean {\n return parseISODate(value) !== null\n}\n\n/** A new Date `days` after `date` (normalises across month/year boundaries). */\nexport function shiftDate(date: Date, days: number): Date {\n return new Date(date.getFullYear(), date.getMonth(), date.getDate() + days)\n}\n\n/** A new Date `months` after `date`, clamping the day to the target month's length. */\nexport function shiftMonth(date: Date, months: number): Date {\n const target = new Date(date.getFullYear(), date.getMonth() + months, 1)\n const lastDay = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate()\n return new Date(target.getFullYear(), target.getMonth(), Math.min(date.getDate(), lastDay))\n}\n\n/** A new Date `years` after `date`. Clamps 29 February onto a non-leap year's 28th. */\nexport function shiftYear(date: Date, years: number): Date {\n return shiftMonth(date, years * 12)\n}\n\n/** Day-of-week with **Monday = 0** … Sunday = 6 (British week start). */\nfunction mondayIndex(date: Date): number {\n return (date.getDay() + 6) % 7\n}\n\n/**\n * The 42 cells (6 weeks × 7 days, Monday-first) covering `month` of `year` (0-based month),\n * including the leading/trailing adjacent-month days that pad the grid. Each cell carries its\n * own year/month/day, an `inMonth` flag, and its ISO string.\n *\n * Always six rows, even for a month that fits in five: a grid that changed height as the user\n * paged would shunt everything below it in the Sidebar.\n */\nexport function monthGridDays(year: number, month: number): DayCell[] {\n const first = new Date(year, month, 1)\n const start = shiftDate(first, -mondayIndex(first)) // back up to the Monday of week 1\n const cells: DayCell[] = []\n for (let i = 0; i < 42; i++) {\n const d = shiftDate(start, i)\n cells.push({\n year: d.getFullYear(),\n month: d.getMonth(),\n day: d.getDate(),\n inMonth: d.getMonth() === month && d.getFullYear() === year,\n iso: formatISODate(d),\n })\n }\n return cells\n}\n\n/** One cell of the year view: a month of `year`, with the short name it is labelled by. */\nexport interface MonthCell {\n year: number\n /** 0-based month. */\n month: number\n label: string\n}\n\n/**\n * The twelve months of `year`, for the view the month header opens. Paging a month at a time\n * to reach a distant year is not navigation, it is a chore; from here any month of any year is\n * two clicks away.\n */\nexport function yearGridMonths(year: number): MonthCell[] {\n const cells: MonthCell[] = []\n for (let month = 0; month < 12; month++) {\n cells.push({\n year,\n month,\n label: new Date(year, month, 1).toLocaleString(undefined, { month: 'short' }),\n })\n }\n return cells\n}\n","/**\n * What makes a [[Concept]] a [[Journal Concept]] — the one predicate that decides where a\n * promoting [[Draft]] writes, and which concepts refuse to be renamed (ADR 0056).\n *\n * It is deliberately **not** the `\\d{4}-\\d{2}-\\d{2}` shape it grew out of. While the shape only\n * guarded rename, letting `2026-13-45` through cost nothing; now that the same answer decides\n * whether a file lands in `journals/` or `pages/`, a loose test would mint a journal entry for a\n * day no calendar can display or reach again.\n *\n * Pure; no DOM, no I/O.\n */\n\nimport { isCalendarDay } from './calendar/month-grid-core'\n\n/**\n * True when `concept` names a calendar day that exists — a [[Journal Entry]]'s identity.\n *\n * Journal identity is the day itself, so this is case- and alias-free: there is nothing to\n * normalise beyond surrounding whitespace.\n */\nexport function isJournalConcept(concept: string): boolean {\n return isCalendarDay(concept)\n}\n","/**\n * Parse a document's YAML frontmatter. Frontmatter is authoritative for identity\n * (DESIGN.md → On-disk layout); the rest of the storage layer reads `title`,\n * `aliases`, etc. from `data`.\n *\n * A hand-edited file must never crash the scan, so malformed or unterminated\n * frontmatter degrades to \"no frontmatter\" (`data: {}`, `body` = the whole text)\n * rather than throwing.\n */\n\nimport { parse as parseYaml } from 'yaml'\n\nimport { frontmatterSpan } from './frontmatter-span'\n\nexport interface ParsedFrontmatter {\n /** The parsed YAML object, or `{}` when there is none / it is malformed. */\n data: Record<string, unknown>\n /** The document text after the frontmatter block (the whole text if none). */\n body: string\n}\n\nexport function parseFrontmatter(text: string): ParsedFrontmatter {\n // What counts as Frontmatter is decided in one place (frontmatter-span.ts), so the editor's\n // styling, its completion guards and this parse cannot disagree about the same file.\n const span = frontmatterSpan(text)\n if (!span) return { data: {}, body: text }\n\n const body = text.slice(span.end)\n try {\n const parsed = parseYaml(span.body)\n // YAML can parse to a scalar/array/null; only a plain object is frontmatter.\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n return { data: parsed as Record<string, unknown>, body }\n }\n return { data: {}, body }\n } catch {\n // Malformed YAML in a hand-edited file: treat as no frontmatter.\n return { data: {}, body }\n }\n}\n","/**\n * Concept identity, as fixed by CONTEXT.md → Concept / Alias / Slug:\n *\n * - A page's canonical concept name is its frontmatter `title`; the filename is a\n * derived convenience (and the fallback when `title` is absent). This is why a\n * filesystem-mangled name never corrupts identity (DESIGN.md → On-disk layout).\n * - Identity is **case-insensitive** (`conceptKey`), display is **case-preserving**.\n * - A journal entry is identified by its ISO date (its filename stem).\n *\n * Pure; no DOM, no I/O.\n */\n\nimport type { Subdir } from './directory-adapter'\n\nexport type DocumentKind = 'journal' | 'page'\n\n/** The kind a content subdir holds, or `null` for non-document subdirs. */\nexport function documentKindOf(subdir: Subdir): DocumentKind | null {\n if (subdir === 'journals') return 'journal'\n if (subdir === 'pages') return 'page'\n return null\n}\n\n/** Strip a single trailing `.md` extension. */\nexport function fileStem(fileName: string): string {\n return fileName.replace(/\\.md$/i, '')\n}\n\n/** The case-insensitive identity key for a concept (display casing is preserved elsewhere). */\nexport function conceptKey(concept: string): string {\n return concept.toLowerCase()\n}\n\n/** The display concept name for a page: frontmatter `title` if a non-empty string, else the filename stem. */\nexport function conceptOf(fm: { data: Record<string, unknown> }, fileNameStem: string): string {\n const title = fm.data.title\n if (typeof title === 'string' && title.trim() !== '') return title\n return fileNameStem\n}\n\n/** The concept (ISO date) of a journal entry, derived from its filename. */\nexport function journalConceptOf(fileName: string): string {\n return fileStem(fileName)\n}\n\n/** The aliases declared in frontmatter (`aliases:`), as a string array; `[]` when absent/invalid. */\nexport function aliasesOf(fm: { data: Record<string, unknown> }): string[] {\n const aliases = fm.data.aliases\n if (!Array.isArray(aliases)) return []\n return aliases.filter((a): a is string => typeof a === 'string')\n}\n","/**\n * What [[Frontmatter]] says about a document's identity, and how to rewrite it (ADR 0061).\n *\n * Frontmatter is real text on both backends. On a [[Filesystem Backend]] it is authoritative\n * (ADR 0007); on a [[Server Backend]] the encrypted registry is, and the block is a *proposal*\n * about it. Either way exactly two properties carry identity - `title` and `aliases` - and\n * everything else in the block is kept verbatim for whatever feature defines it. This module is\n * the one place that reads those two keys out of a block and writes them back into one, so the\n * stores, the editor's proposal detection and the mirror cannot disagree about what a block\n * claims.\n *\n * Pure: no store, no editor. The YAML rules (what counts as a block, how it parses) come from\n * `storage/fs`, shared with the scan and the editor's analysis, so there is one rule.\n */\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml'\n\nimport { parseFrontmatter } from '$lib/storage/fs/frontmatter'\nimport { frontmatterSpan } from '$lib/storage/fs/frontmatter-span'\nimport { aliasesOf, conceptKey } from '$lib/storage/fs/identity'\n\n/**\n * What an edit to a property does when the editing episode ends:\n * - `confirm`: opens a dialog before anything changes (a rename is a graph-wide operation).\n * - `silent`: applied as typed.\n * - `none`: recognised and documented, but nothing reads it yet.\n */\nexport type FrontmatterPolicy = 'confirm' | 'silent' | 'none'\n\nexport interface FrontmatterProperty {\n key: string\n policy: FrontmatterPolicy\n /** One line for the docs table and any UI that lists what the block can do. */\n summary: string\n}\n\n/**\n * The known properties, in the order their steps run. Silent steps come before confirmed ones,\n * so a rename cascade sees the aliases the user just wrote. A future property that needs its own\n * confirmation gets a row here and a step in `proposal.ts`; the sequencing needs no change.\n */\nexport const FRONTMATTER_PROPERTIES: readonly FrontmatterProperty[] = [\n {\n key: 'aliases',\n policy: 'silent',\n summary: 'Other names the document answers to. Applied when you leave the block.',\n },\n {\n key: 'title',\n policy: 'confirm',\n summary: 'The document’s name. Changing it opens the rename dialog when you leave the block.',\n },\n {\n key: 'public',\n policy: 'none',\n summary: 'Reserved for publishing. Kept in the text; nothing reads it yet.',\n },\n]\n\nexport interface FrontmatterIdentity {\n /** The `title` value, or null when the block has none (or only a blank one). */\n title: string | null\n aliases: string[]\n /** Whether the document carries a balanced block at all. No block is no claim. */\n hasBlock: boolean\n}\n\n/** What the block claims. An unterminated block is not a block, as everywhere else. */\nexport function frontmatterIdentity(text: string): FrontmatterIdentity {\n const hasBlock = frontmatterSpan(text) !== null\n const fm = parseFrontmatter(text)\n return { title: titleOf(fm.data), aliases: aliasesOf(fm), hasBlock }\n}\n\nfunction titleOf(data: Record<string, unknown>): string | null {\n const title = data.title\n return typeof title === 'string' && title.trim() !== '' ? title : null\n}\n\n/**\n * Aliases as identity sees them: trimmed, non-blank, unique case-insensitively (first spelling\n * wins), and never the document's own name - a document is not an alias of itself.\n */\nexport function normaliseAliases(aliases: readonly string[], concept?: string): string[] {\n const seen = new Set<string>(concept === undefined ? [] : [conceptKey(concept)])\n const out: string[] = []\n for (const raw of aliases) {\n const alias = raw.trim()\n if (alias === '') continue\n const key = conceptKey(alias)\n if (seen.has(key)) continue\n seen.add(key)\n out.push(alias)\n }\n return out\n}\n\n/** Set equality by identity key: order and case carry no meaning. */\nexport function sameAliases(a: readonly string[], b: readonly string[]): boolean {\n const left = new Set(a.map(conceptKey))\n const right = new Set(b.map(conceptKey))\n return left.size === right.size && [...left].every((key) => right.has(key))\n}\n\nexport interface IdentityPatch {\n /** A string sets the title; null removes it; undefined leaves it alone. */\n title?: string | null\n /** Empty removes the key; undefined leaves it alone. */\n aliases?: readonly string[]\n}\n\nexport interface WriteOptions {\n /**\n * Add a block to a document that has none. Off by default: a document without a block has\n * made no claim, and a synced document never grows one unprompted (ADR 0061).\n */\n addBlock?: boolean\n}\n\n/**\n * The text with its identity keys rewritten. Other keys keep their values and their order; a\n * new `title` goes first and new `aliases` last, which is how a fresh page's block reads. The\n * body is untouched. Returns the very same string when nothing would change, so callers can\n * write back on every save without reformatting a block that already agrees. A block whose YAML\n * does not parse is left alone: rewriting it would destroy whatever the user was typing.\n */\nexport function withFrontmatterIdentity(text: string, patch: IdentityPatch, options: WriteOptions = {}): string {\n const span = frontmatterSpan(text)\n if (!span) {\n if (!options.addBlock) return text\n const data: Record<string, unknown> = {}\n if (typeof patch.title === 'string' && patch.title.trim() !== '') data.title = patch.title\n const aliases = patch.aliases ? [...patch.aliases] : []\n if (aliases.length > 0) data.aliases = aliases\n if (Object.keys(data).length === 0) return text\n return `---\\n${stringifyYaml(data)}---\\n${text}`\n }\n\n const data = parseBlock(span.body)\n if (data === null) return text\n const currentTitle = titleOf(data)\n const currentAliases = aliasesOf({ data })\n const wantTitle = patch.title === undefined ? currentTitle : patch.title\n const wantAliases = patch.aliases === undefined ? currentAliases : [...patch.aliases]\n if (wantTitle === currentTitle && sameAliases(wantAliases, currentAliases)) return text\n\n const next: Record<string, unknown> = {}\n if (wantTitle !== null && !('title' in data)) next.title = wantTitle\n for (const [key, value] of Object.entries(data)) {\n if (key === 'title') {\n if (wantTitle !== null) next.title = wantTitle\n continue\n }\n if (key === 'aliases') {\n if (wantAliases.length > 0) next.aliases = wantAliases\n continue\n }\n next[key] = value\n }\n if (wantAliases.length > 0 && !('aliases' in data)) next.aliases = wantAliases\n return `---\\n${serialise(next)}---\\n${text.slice(span.end)}`\n}\n\n/**\n * The text with `title` and `aliases` removed from its block, and the block itself removed if\n * nothing else was in it. For an [[Import]] into a synced graph, where identity goes into the\n * registry and the rest of the block stays with the document.\n */\nexport function withoutIdentityKeys(text: string): string {\n const span = frontmatterSpan(text)\n if (!span) return text\n const data = parseBlock(span.body)\n if (data === null) return text\n if (!('title' in data) && !('aliases' in data)) return text\n const rest: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(data)) {\n if (key !== 'title' && key !== 'aliases') rest[key] = value\n }\n if (Object.keys(rest).length === 0) return text.slice(span.end)\n return `---\\n${serialise(rest)}---\\n${text.slice(span.end)}`\n}\n\n/** The block's YAML as a plain object, or null when it is malformed or not an object. */\nfunction parseBlock(yaml: string): Record<string, unknown> | null {\n try {\n const parsed: unknown = parseYaml(yaml)\n if (parsed === null || parsed === undefined) return {}\n if (typeof parsed !== 'object' || Array.isArray(parsed)) return null\n return parsed as Record<string, unknown>\n } catch {\n return null\n }\n}\n\nfunction serialise(data: Record<string, unknown>): string {\n return Object.keys(data).length === 0 ? '' : stringifyYaml(data)\n}\n","/**\n * The backend-agnostic document seam.\n *\n * The editor binds to an {@link EditorDocument}; the backend owns the text —\n * CodeMirror is never the source of truth (see ADR 0010). The Local engine will\n * implement {@link DocumentStore} over a file buffer, the Server engine over a\n * Yjs Y.Text; the harness uses an in-memory store. All three look identical here.\n */\n\nimport type { ViewRef } from '$lib/layout'\n\nexport class DocumentNotFoundError extends Error {\n constructor(readonly target: string) {\n super(`No document for \"${target}\"`)\n this.name = 'DocumentNotFoundError'\n }\n}\n\nexport class DocumentSyncDegradedError extends Error {\n constructor(\n readonly target: string,\n readonly status:\n | 'key-unavailable'\n | 'ciphertext-corrupt'\n | 'generation-stale'\n | 'sequence-gap',\n ) {\n super(`Document \"${target}\" is sync-degraded: ${status}`)\n this.name = 'DocumentSyncDegradedError'\n }\n}\n\n/** A {@link ViewRef} narrowed to documents — `kind` is always `'document'`. */\nexport interface DocumentRef extends ViewRef {\n kind: 'document'\n /** The document's stable id (its concept name / file id). */\n target: string\n}\n\n/** A single text edit, expressed as a range replacement (CodeMirror-shaped). */\nexport interface TextChange {\n /** UTF-16 offset where the replacement starts. */\n from: number\n /** UTF-16 offset where the replacement ends (`from` for a pure insert). */\n to: number\n /** Replacement text (`''` for a pure delete). */\n insert: string\n}\n\n/**\n * Who made an edit — which decides whether the document's subscribers hear about it.\n *\n * `editor` is the default and the historical behaviour: the editor that called\n * {@link EditorDocument.applyChange} already shows the edit, so echoing it back would be a\n * pointless round trip (and, for CodeMirror, a fight with its own transaction).\n *\n * `external` is an edit made by anything that is NOT the editor — the [[Tasks View]] ticking a\n * checkbox, for one. Those MUST notify, or an open editor keeps rendering text the buffer no\n * longer holds, and its next keystroke applies an offset against a document it disagrees with.\n */\nexport type ChangeOrigin = 'editor' | 'external'\n\n/**\n * A live handle to one document's text. The editor reads {@link getText} once to\n * seed itself, pushes local edits via {@link applyChange}, and is pushed *external*\n * changes (git reload, a remote CRDT update) via {@link subscribe}.\n */\nexport interface EditorDocument {\n readonly id: string\n getText(): string\n /** Apply an edit. Defaults to `editor` origin — see {@link ChangeOrigin}. */\n applyChange(change: TextChange, origin?: ChangeOrigin): void\n /**\n * Observe changes that did NOT originate from this editor's own\n * {@link applyChange} call — i.e. external/remote edits the editor must\n * reflect. Returns an unsubscribe function.\n */\n subscribe(listener: (text: string) => void): () => void\n}\n\n/** Opens documents by id. One store backs one knowledge graph. */\nexport interface DocumentStore {\n open(target: string): EditorDocument\n /**\n * Resolves when the document's backing content has actually loaded (e.g. a Server\n * Backend's cache seed). Absent on stores whose `open` returns real text synchronously;\n * the editor shows a loading state only while this is pending.\n */\n whenReady?(target: string): Promise<void>\n}\n","/**\n * The yield-and-cancel checkpoint (plan: 2026-07-27 Import Progress And Activities).\n *\n * A long synchronous loop starves the main thread: Svelte cannot repaint, so a status\n * line jumps from nothing straight to its final value, possibly via the browser's\n * \"page unresponsive\" prompt. `breathe` hands control back to the event loop.\n *\n * It yields on **elapsed time, not item count**. A graph of one-line journals and a graph\n * of 5000-line pages want wildly different batch sizes, and the thing we actually care\n * about is how long a frame has been blocked - so measure that directly.\n *\n * The yield point is also the natural cancel checkpoint, so cancellation rides along at\n * no extra cost (ADR 0035 §3): callers thread one `AbortSignal` and get both.\n */\n\n/** Longest a loop may hold the thread before yielding. ~3 frames: smooth enough to repaint, coarse enough not to dominate. */\nconst SLICE_MS = 50\n\n/** Per-call-site state, so two concurrent loops do not starve each other's budget. */\nexport interface Breather {\n (signal?: AbortSignal): Promise<void>\n}\n\n/**\n * Create a breather. Call `await breathe(signal)` inside a loop: it returns immediately\n * while the current slice has budget left, and yields when it does not. It throws the\n * signal's reason as soon as the signal aborts, whether or not it yields.\n */\nexport function createBreather(now: () => number = () => performance.now()): Breather {\n let lastYield = now()\n return async (signal?: AbortSignal) => {\n signal?.throwIfAborted()\n if (now() - lastYield < SLICE_MS) return\n // setTimeout (a macrotask) rather than queueMicrotask or await Promise.resolve():\n // a microtask does NOT let the browser paint, which is the whole point.\n await new Promise((resolve) => setTimeout(resolve))\n lastYield = now()\n signal?.throwIfAborted()\n }\n}\n","/**\n * Rewriting [[Wikilink]] targets across a document's source, for the rewrite arm of a page\n * rename (ADR 0037). Pure, so the tricky part - what NOT to touch - is unit-testable.\n *\n * Two things are deliberately left alone:\n *\n * 1. **Links inside code.** `wikilinkOccurrencesInSource` already drops them: a\n * `[[Physics]]` inside a fence is text the author wrote, not a reference.\n *\n * 2. **Nested occurrences.** `[[[[Physics]] Quantum]]` references the [[Scoped Concept]]\n * \"[[Physics]] Quantum\", which is a *different concept* from \"Physics\" and was not\n * renamed. Rewriting the inner scope would silently re-point the link at a scoped concept\n * that does not exist. Renaming a scope is a cascade over every concept beneath it, and\n * that is out of scope here - the top-level `[[Physics]]` links move, the scoped ones do\n * not, and the rename dialog's count reflects only what actually moves.\n */\n\nimport { wikilinkOccurrencesInSource } from './source'\n\nexport interface WikilinkRewrite {\n text: string\n /** How many occurrences were rewritten. */\n count: number\n}\n\n/** Absolute [start, end) offsets of an occurrence's inner text (between `[[` and `]]`). */\ninterface Target {\n from: number\n to: number\n}\n\n/**\n * Replace every top-level, non-code `[[from]]` in `source` with `[[to]]`. Matching is\n * case-insensitive on the trimmed concept, mirroring [[Concept]] identity.\n */\nexport function rewriteWikilinkTarget(source: string, from: string, to: string): WikilinkRewrite {\n const wanted = from.trim().toLowerCase()\n if (wanted === '') return { text: source, count: 0 }\n\n const occurrences = wikilinkOccurrencesInSource(source)\n if (occurrences.length === 0) return { text: source, count: 0 }\n\n // Absolute offsets per line, so nesting can be judged and edits applied to the whole source.\n const lineStarts: number[] = []\n let at = 0\n for (const line of source.split('\\n')) {\n lineStarts.push(at)\n at += line.length + 1\n }\n\n const spans = occurrences.map((o) => ({\n occurrence: o,\n start: lineStarts[o.line] + o.matchStart,\n end: lineStarts[o.line] + o.matchEnd,\n }))\n\n const targets: Target[] = []\n for (const span of spans) {\n if (span.occurrence.concept.trim().toLowerCase() !== wanted) continue\n // Nested inside another wikilink ⇒ it is a scope, not a reference to this concept.\n const nested = spans.some((other) => other !== span && other.start < span.start && other.end > span.end)\n if (nested) continue\n targets.push({ from: span.start + 2, to: span.end - 2 })\n }\n if (targets.length === 0) return { text: source, count: 0 }\n\n // Apply back-to-front so earlier offsets stay valid.\n targets.sort((a, b) => b.from - a.from)\n let text = source\n for (const target of targets) {\n text = text.slice(0, target.from) + to + text.slice(target.to)\n }\n return { text, count: targets.length }\n}\n\n/** Whether `source` references `concept` at the top level - the rename dialog's count. */\nexport function countWikilinkTargets(source: string, concept: string): number {\n return rewriteWikilinkTarget(source, concept, concept).count\n}\n\n/**\n * Replace every `[[from]]` with `[[to]]` **at any nesting depth** - the cascade rule\n * (ADR 0038 §2), and the deliberate inverse of {@link rewriteWikilinkTarget}.\n *\n * A [[Scoped Concept]]'s name *contains* its [[Scope]]'s name, so rewriting the inner link is\n * what renames the scope. `[[[[Physics]] Quantum]] Field Theory` is corrected through its\n * innermost occurrence and comes out right, which is why nothing here recurses and no depth\n * is special-cased.\n *\n * Used for two different things that turn out to be the same operation: rewriting a concept\n * STRING (`[[Physics]] Quantum` → `[[Physical Science]] Quantum`) and rewriting document\n * BODIES, where a scoped reference `[[[[Physics]] Quantum]]` picks up the new scope for free.\n *\n * Links inside code are still skipped - `wikilinkOccurrencesInSource` drops them.\n */\nexport function rewriteWikilinkScope(source: string, from: string, to: string): WikilinkRewrite {\n const splices = wikilinkScopeSplices(source, from, to)\n if (splices.length === 0) return { text: source, count: 0 }\n // Back-to-front so earlier offsets stay valid.\n let text = source\n for (let i = splices.length - 1; i >= 0; i--) {\n const splice = splices[i]\n text = text.slice(0, splice.from) + splice.insert + text.slice(splice.to)\n }\n return { text, count: splices.length }\n}\n\n/** One replacement inside a source: `[from, to)` in the ORIGINAL offsets, and what goes there. */\nexport interface TextSplice {\n from: number\n to: number\n insert: string\n}\n\n/**\n * The per-occurrence splices that {@link rewriteWikilinkScope} applies, in ascending order of\n * position and in the source's original offsets: every `[[from]]` at any depth becomes\n * `[[to]]`, links inside code left alone.\n *\n * Exposed because a rewrite across documents is applied as these splices, never as a\n * whole-text replace (ADR 0066): on a Server Backend each is a small CRDT delete plus insert\n * that merges with concurrent typing elsewhere in the document, and on a Filesystem Backend\n * each goes through the open buffer so the editor showing it is updated in place.\n *\n * Nested targets never overlap a matching OUTER target: an outer link whose concept is exactly\n * `from` cannot also contain a link with that same concept (a concept does not nest inside\n * itself), so the splices are disjoint and may be applied back-to-front without remapping.\n */\nexport function wikilinkScopeSplices(source: string, from: string, to: string): TextSplice[] {\n const wanted = from.trim().toLowerCase()\n if (wanted === '') return []\n\n const occurrences = wikilinkOccurrencesInSource(source)\n if (occurrences.length === 0) return []\n\n const lineStarts: number[] = []\n let at = 0\n for (const line of source.split('\\n')) {\n lineStarts.push(at)\n at += line.length + 1\n }\n\n const splices: TextSplice[] = []\n for (const occurrence of occurrences) {\n if (occurrence.concept.trim().toLowerCase() !== wanted) continue\n const start = lineStarts[occurrence.line] + occurrence.matchStart\n const end = lineStarts[occurrence.line] + occurrence.matchEnd\n splices.push({ from: start + 2, to: end - 2, insert: to })\n }\n return splices.sort((a, b) => a.from - b.from)\n}\n\n/**\n * True when `concept` is scoped - at any depth - by `scope`. This is what makes a document\n * part of a rename's cascade set.\n */\nexport function isScopedBy(concept: string, scope: string): boolean {\n return rewriteWikilinkScope(concept, scope, scope).count > 0\n}\n\n/**\n * The concepts a rename of `scope` drags with it: everything scoped by it, at any depth,\n * paired with the name each becomes.\n *\n * Concepts with no document behind them are included by the caller's input if it supplies\n * them - they need no rename, but recognising them keeps the impact count honest.\n */\nexport function cascadeFor(\n concepts: readonly string[],\n from: string,\n to: string,\n): { from: string; to: string }[] {\n const out: { from: string; to: string }[] = []\n for (const concept of concepts) {\n const rewritten = rewriteWikilinkScope(concept, from, to)\n if (rewritten.count > 0) out.push({ from: concept, to: rewritten.text })\n }\n return out\n}\n","/**\n * The `etherpk-cipher` fence as it appears in document *text* — finding one, reading what it holds\n * without the key, and producing the edits that write one.\n *\n * Nothing here decrypts. Every function is pure string and byte work over document lines, so the\n * editor, the storage layer, the index guard and the import converter can all ask the same\n * questions of the same source without holding a Protection Key between them.\n *\n * Fence pairing is delegated to {@link fencedBlocks} — the column-scoped rules every other fence\n * in the app already obeys (ADR 0018, ADR 0020). A cipher fence is not a second notion of \"in a\n * block\": an unterminated one is invisible here exactly as it is everywhere else.\n */\nimport {\n protectedEnvelopeFingerprint,\n protectedEnvelopeWrittenAt,\n resolveLastWriteWins,\n unarmourProtected,\n} from '$lib/crypto'\nimport { frontmatterSpan } from '$lib/storage/fs/frontmatter-span'\n\nimport { fenceLineInfo, fencedBlocks } from '../fenced-code'\nimport { CIPHER_FENCE_INFO } from './fence-info'\n\nexport { CIPHER_FENCE_INFO }\n\nconst DEFAULT_TICKS = '```'\n\nexport interface CipherFence {\n /** 0-based line index of the opening fence. */\n start: number\n /** 0-based line index of the closing fence. */\n end: number\n /**\n * Character offset of the opening fence's first character, and just past the closing fence's\n * last. Carried here so the editor's read-only guard, the widget placement and the service's\n * classification all measure the same way - two offset computations over one fence is how a\n * guard silently starts guarding the wrong range.\n */\n from: number\n to: number\n /** The column the fence and its body are clamped to. */\n fenceColumn: number\n /** Every envelope the body holds — more than one after two offline rewrites merged. */\n envelopes: Uint8Array[]\n /** The last-write-wins survivor, or null when the body holds nothing readable. */\n winner: Uint8Array | null\n /** The winner's Protection Key fingerprint — whose key this needs. */\n fingerprint: Uint8Array | null\n /** The winner's write timestamp. */\n writtenAt: number | null\n /**\n * The body holds losing envelopes that a key holder should collapse away. ADR 0028 discards\n * them; ADR 0059 records that under projected editing the loser can be your own work.\n */\n needsCollapse: boolean\n}\n\n/** Every complete cipher fence in `lines`, in document order. */\nexport function cipherFences(lines: readonly string[]): CipherFence[] {\n const out: CipherFence[] = []\n // Offset of each line's first character, so a line index becomes a character offset.\n const lineStarts: number[] = [0]\n for (let i = 0; i < lines.length; i++) lineStarts.push(lineStarts[i] + lines[i].length + 1)\n\n for (const block of fencedBlocks(lines)) {\n const opener = fenceLineInfo(lines[block.start])\n if (!opener || opener.info !== CIPHER_FENCE_INFO) continue\n const body = lines\n .slice(block.start + 1, block.end)\n .map((line) => (line.length >= block.fenceColumn ? line.slice(block.fenceColumn) : line.trimStart()))\n .join('\\n')\n const envelopes = unarmourProtected(body)\n const winner = resolveLastWriteWins(envelopes)\n out.push({\n start: block.start,\n end: block.end,\n from: lineStarts[block.start],\n // The closer's last character, NOT the newline after it — so an insertion on the line\n // below the fence is outside the range and stays editable.\n to: lineStarts[block.end] + lines[block.end].length,\n fenceColumn: block.fenceColumn,\n envelopes,\n winner,\n fingerprint: winner ? protectedEnvelopeFingerprint(winner) : null,\n writtenAt: winner ? protectedEnvelopeWrittenAt(winner) : null,\n needsCollapse: envelopes.length > 1,\n })\n }\n return out\n}\n\nexport type DocumentProtectionKind =\n /**\n * Not a Protected Document. Includes a document with a cipher fence somewhere in it that is\n * not the whole body: protection is whole-document only (ADR 0060), so such a fence is\n * ordinary markdown — a code block that happens to hold base64 — and means nothing here.\n */\n | 'none'\n /** The whole body is one cipher fence — a [[Protected Document]]. */\n | 'document'\n\nexport interface DocumentProtection {\n kind: DocumentProtectionKind\n fences: CipherFence[]\n /** The distinct Protection Key fingerprints the document's fences need. */\n fingerprints: Uint8Array[]\n}\n\n/**\n * Classify a document's text: a Protected Document, or not. Only a fence that is the entire body\n * below any Frontmatter counts (ADR 0060); `fences` still lists every fence found, because the\n * service's per-fence classification and the index's exclusion both want them.\n */\nexport function documentProtection(text: string): DocumentProtection {\n // Measured from the WHOLE document, not from below the Frontmatter. `cipherFences` and\n // `protectedRanges` both use whole-document line indices, and a second index base here would\n // silently mis-locate every fence in a document that has Frontmatter.\n const lines = text.split('\\n')\n const fences = cipherFences(lines)\n if (fences.length === 0) return { kind: 'none', fences, fingerprints: [] }\n\n const fingerprints: Uint8Array[] = []\n const seen = new Set<string>()\n for (const f of fences) {\n if (!f.fingerprint) continue\n const key = String(f.fingerprint)\n if (seen.has(key)) continue\n seen.add(key)\n fingerprints.push(f.fingerprint)\n }\n\n // Document-protected means the fence IS the body: one fence, opening on the first non-blank\n // line below any Frontmatter and closing on the last. Anything else is not protection.\n const only = fences.length === 1 ? fences[0] : null\n const bodyStart = bodyStartLine(text)\n const first = firstNonBlank(lines, bodyStart)\n const last = lastNonBlank(lines)\n const wholeBody = only !== null && only.start === first && only.end === last\n return { kind: wholeBody ? 'document' : 'none', fences, fingerprints }\n}\n\n/** The first line index below any Frontmatter — where a document's body begins. */\nfunction bodyStartLine(text: string): number {\n return frontmatterSpan(text)?.lines ?? 0\n}\n\nfunction firstNonBlank(lines: readonly string[], from: number): number {\n for (let i = from; i < lines.length; i++) if (lines[i].trim() !== '') return i\n return -1\n}\n\nfunction lastNonBlank(lines: readonly string[]): number {\n for (let i = lines.length - 1; i >= 0; i--) if (lines[i].trim() !== '') return i\n return -1\n}\n\n/**\n * A document's body — everything below any Frontmatter. Protection replaces exactly this, leaving\n * the Frontmatter in the clear so the concept name stays visible and wikilink-resolvable.\n */\nexport function documentBody(text: string): string {\n return bodyLines(text).lines.join('\\n')\n}\n\n/** A document split into its frontmatter prefix (kept verbatim) and its body lines. */\nfunction bodyLines(text: string): { prefix: string; lines: string[] } {\n const span = frontmatterSpan(text)\n if (!span) return { prefix: '', lines: text.split('\\n') }\n return { prefix: text.slice(0, span.end), lines: text.slice(span.end).split('\\n') }\n}\n\n/**\n * Turn a document into a [[Protected Document]]: the body becomes one fence holding `armoured`,\n * and any Frontmatter is kept in the clear above it so the concept name stays visible and\n * wikilink-resolvable (ADR 0057 / DESIGN.md).\n */\nexport function protectDocumentText(text: string, armoured: string): string {\n const { prefix } = bodyLines(text)\n return prefix + fenceLines(armoured, '', DEFAULT_TICKS).join('\\n')\n}\n\n/** The three lines of a fence: opener, one armoured body line, closer. */\nfunction fenceLines(armoured: string, indent: string, ticks: string): string[] {\n return [`${indent}${ticks}${CIPHER_FENCE_INFO}`, `${indent}${armoured}`, `${indent}${ticks}`]\n}\n\n/**\n * The Frontmatter a document carries, verbatim, closing delimiter and newline included — the part\n * of a [[Protected Document]] that stays in the clear. `''` when there is none.\n */\nexport function frontmatterPrefix(text: string): string {\n return bodyLines(text).prefix\n}\n\n/** Restore a [[Protected Document]]'s plaintext body, keeping its Frontmatter. */\nexport function unprotectDocumentText(text: string, plaintext: string): string {\n return bodyLines(text).prefix + plaintext\n}\n\nexport interface LineEdit {\n /** First line index replaced, inclusive. */\n fromLine: number\n /** Last line index replaced, inclusive. */\n toLine: number\n /** The replacement text — one delete plus one insert, which is what ADR 0028 requires. */\n text: string\n}\n\n/**\n * Replace a fence's **body** with one armoured envelope, leaving the opener and closer untouched.\n * This is the write that ADR 0028 demands be a single delete plus a single insert: character-level\n * CRDT edits inside ciphertext would let a stray character make the envelope undecryptable.\n */\nexport function replaceCipherFenceBody(fence: CipherFence, armoured: string): LineEdit {\n const pad = ' '.repeat(fence.fenceColumn)\n return { fromLine: fence.start + 1, toLine: fence.end - 1, text: pad + armoured }\n}\n\n\n","/**\n * [[Merge]] (CONTEXT.md): combining two [[Document]]s that have come to share one\n * [[Concept]].\n *\n * Pure, and shared on purpose. It arises in three places - renaming a page onto a name\n * already taken, a cascading scope rename that lands two scoped concepts on the same name\n * (ADR 0038 §4), and later [[Import|import-into-existing]], which must reuse this rather\n * than growing a second merge.\n *\n * Nothing is discarded. Bodies are joined and [[Alias]]es union, so every name either\n * document answered to still resolves and the result is something the user can tidy rather\n * than something they must reconstruct.\n */\n\nimport { conceptKey } from './fs/identity'\n\nexport interface MergeInput {\n /** The document that survives - its frontmatter (beyond aliases) is the one kept. */\n body: string\n aliases: string[]\n}\n\nexport interface MergeResult {\n body: string\n aliases: string[]\n}\n\n/**\n * Join the two documents.\n *\n * The bodies are separated by a blank line and **nothing else** - no marker saying a merge\n * happened (a deliberate choice: the result reads as one document). A blank line is not a\n * marker, it is what stops the last line of one body running into the first line of the\n * other and silently changing the markdown.\n *\n * Aliases union, deduped by [[Concept]] identity, survivor's order first.\n */\nexport function mergeDocuments(survivor: MergeInput, absorbed: MergeInput): MergeResult {\n const a = survivor.body.replace(/\\s+$/, '')\n const b = absorbed.body.replace(/^\\s+/, '')\n const body = a === '' ? b : b === '' ? a : `${a}\\n\\n${b}`\n\n const seen = new Set<string>()\n const aliases: string[] = []\n for (const alias of [...survivor.aliases, ...absorbed.aliases]) {\n const trimmed = alias.trim()\n if (trimmed === '') continue\n const key = conceptKey(trimmed)\n if (seen.has(key)) continue\n seen.add(key)\n aliases.push(trimmed)\n }\n\n return { body, aliases }\n}\n","/**\n * The shared vocabulary of a [[Rename]] (ADR 0037, 0038, 0064), used by both\n * [[Storage Backend]]s.\n *\n * A rename acts on the [[Concept]]; a page is a passenger. The decision the two options\n * encode: renaming a page leaves every `[[Old Name]]` in the graph pointing at a concept with\n * no page behind it, and neither fix is right often enough to impose. Aliasing touches one\n * document and honours \"the source is always canonical\"; rewriting makes the prose read\n * correctly but is N concurrent document edits with no transaction around them on a synced\n * graph. A [[Pageless Concept]] has nothing to alias, so for it there is no choice: renaming\n * one IS the rewrite (ADR 0064).\n */\n\nimport { isJournalConcept } from '$lib/document/journal-concept'\n\n/** How the rename deals with existing inbound links. */\nexport type RenameLinkStrategy =\n /** Add the old name to the page's [[Alias]]es; every existing link keeps resolving. */\n | 'alias'\n /** Rewrite top-level `[[old]]` occurrences across the graph to the new name. */\n | 'rewrite'\n\nexport interface RenameOptions {\n strategy: RenameLinkStrategy\n}\n\nexport interface RenameResult {\n /** The page's new concept. */\n concept: string\n /** Documents whose text was rewritten (`0` for the alias strategy). */\n rewritten: number\n /** Scoped concepts dragged along by the cascade. */\n cascaded: number\n /** Steps that landed on a taken name and so merged. */\n merged: number\n}\n\n/**\n * Why a rename was refused, as a user-facing message, or null when it may proceed.\n * Shared so both backends refuse identically.\n *\n * A [[Journal Entry]] is refused because its concept *is* its date and there is exactly one\n * per day; renaming it would break that invariant rather than express anything. A\n * journal-shaped [[Pageless Concept]] (a day nobody has written yet) is refused for the same\n * reason: its name is its day whether or not the entry exists.\n *\n * A collision is NOT a refusal (ADR 0038 §4): it is a [[Merge]], confirmed in the dialog.\n */\nexport function renameRefusal(options: {\n from: string\n to: string\n /** The document's kind, or null when the concept has no document (ADR 0064). */\n kind: 'journal' | 'page' | null\n}): string | null {\n const to = options.to.trim()\n if (to === '') return 'A page needs a non-empty name.'\n if (options.kind === 'journal' || (options.kind === null && isJournalConcept(options.from))) {\n return 'A journal entry cannot be renamed - its name is its date, and there is exactly one per day.'\n }\n return null\n}\n\n/** One concept the rename moves: its current name and the name it takes. */\nexport interface RenameStep {\n from: string\n to: string\n /**\n * Whether `from` has a document. A step without one - a [[Pageless Concept]], directly\n * or in the cascade - moves only its links; there is no identity to write (ADR 0064).\n */\n hasDocument: boolean\n /**\n * True when `to` is already held by a different document AND `from` has one, so this\n * step is a [[Merge]] rather than a plain rename. A Merge joins two documents; with one\n * there is nothing to join.\n */\n merges: boolean\n /**\n * True when `from` has no document and `to` is held by one: the links are simply\n * redirected to the page that already has the name. Nothing is at risk, which is why the\n * dialog reports it neutrally rather than as a Merge (ADR 0064 §2).\n */\n redirects: boolean\n /**\n * The concept of the document this step ends up in. Equal to `to` unless `to` is another\n * document's [[Alias]]: a name is taken by whatever document answers to it, by title or by\n * alias, and a Merge joins into that document under ITS title - the alias already resolves\n * there, so nothing else needs to change for the asked-for name to work (ADR 0038 §4).\n */\n into: string\n}\n\n/**\n * Everything a rename will do, computed before anything is written (ADR 0038 §1).\n *\n * The dialog renders this so the user confirms the real blast radius - renaming one page and\n * silently retitling twelve others is precisely the surprise the informed-consent principle\n * exists to prevent - and the appliers consume the same plan, so preview and effect cannot\n * drift apart.\n */\nexport interface RenamePlan {\n /** The concept the user actually named. */\n direct: RenameStep\n /**\n * Scoped concepts dragged along, at any depth. Ordered deepest-first so a document is\n * never renamed onto a name a later step is about to vacate.\n */\n cascade: RenameStep[]\n /** Documents whose body text references the old name (the rewrite arm's cost). */\n referencingDocuments: number\n /** Refusal message, or null. When set, nothing else here should be acted on. */\n refusal: string | null\n}\n\n/** Every step, direct first - what an applier iterates. */\nexport function renameSteps(plan: RenamePlan): RenameStep[] {\n return [plan.direct, ...plan.cascade]\n}\n\n/** How many steps merge - what the dialog warns about. */\nexport function mergeCount(plan: RenamePlan): number {\n return renameSteps(plan).filter((s) => s.merges).length\n}\n","/**\n * Building a {@link RenamePlan} from the graph's concepts - pure, so the whole cascade is\n * unit-testable without either backend.\n *\n * Both stores compute their plan here and then apply it, so the impact the dialog shows and\n * the work that actually happens come from one source.\n */\n\nimport { cascadeFor } from '$lib/document/wikilink/rename'\nimport { conceptKey } from './fs/identity'\nimport { type RenamePlan, type RenameStep, renameRefusal, renameSteps } from './rename'\n\nexport interface PlanRenameInput {\n from: string\n to: string\n /** Every concept in the graph that has a document behind it. */\n concepts: readonly string[]\n /** Every alias in the graph, with the concept of the document it names. */\n aliases?: readonly { name: string; concept: string }[]\n /** The kind of the document being renamed, or null when the concept has none (ADR 0064). */\n kind: 'journal' | 'page' | null\n /** Documents whose BODY references `from` at the top level. */\n referencingDocuments: number\n}\n\nexport function planRename(input: PlanRenameInput): RenamePlan {\n const from = input.from.trim()\n const to = input.to.trim()\n\n const refusal = renameRefusal({ from, to, kind: input.kind })\n const hasDocument = input.kind !== null\n const direct: RenameStep = { from, to, hasDocument, merges: false, redirects: false, into: to }\n if (refusal) {\n return { direct, cascade: [], referencingDocuments: input.referencingDocuments, refusal }\n }\n\n // Which document answers to a name - by title, or by alias - so a step can tell whether\n // its target is already taken, and by whom.\n const existing = new Map(input.concepts.map((c) => [conceptKey(c), c]))\n for (const alias of input.aliases ?? []) {\n if (!existing.has(conceptKey(alias.name))) existing.set(conceptKey(alias.name), alias.concept)\n }\n\n const cascade = cascadeFor(input.concepts, from, to)\n // Deepest first: a shallower concept must never be renamed onto a name that a deeper\n // step is still about to vacate.\n .sort((a, b) => depthOf(b.from) - depthOf(a.from))\n // The cascade is computed over documented concepts, so every step here has one.\n .map<RenameStep>((step) => stepFor(step.from, step.to, true, existing))\n\n return {\n direct: stepFor(from, to, hasDocument, existing),\n cascade,\n referencingDocuments: input.referencingDocuments,\n refusal: null,\n }\n}\n\n/**\n * A [[Protected Document]] never merges (ADR 0062): not into another document, and nothing into\n * it. Joining bodies would put a cipher fence beside readable text - which is \"not the entire\n * body\", so the result is no longer protected, shows the ciphertext as an ordinary code block,\n * and on a Filesystem Backend has just written the other document's plaintext into a file that\n * used to hold only ciphertext. Refused, not worked around: there is no join that keeps both\n * documents' guarantees.\n *\n * Both stores call this after planning, with their own way of reading a concept's stored text.\n * Only the endpoints of merging steps are asked about, so a plan with no collision - the common\n * case, and the one previewed on every keystroke in the dialog - reads nothing.\n */\nexport async function refuseProtectedMerges(\n plan: RenamePlan,\n isProtected: (concept: string) => Promise<boolean> | boolean,\n): Promise<RenamePlan> {\n if (plan.refusal) return plan\n for (const step of renameSteps(plan)) {\n if (!step.merges) continue\n if (await isProtected(step.into)) {\n return {\n ...plan,\n refusal: `“${step.into}” is a protected document, so nothing can be merged into it. Choose a different name.`,\n }\n }\n if (await isProtected(step.from)) {\n return {\n ...plan,\n refusal: `“${step.from}” is a protected document, so it cannot be merged into “${step.to}”. Choose a name that is not already taken.`,\n }\n }\n }\n return plan\n}\n\n/**\n * A step merges when a DIFFERENT document already answers to the target name - by title or by\n * alias - and the source has a document to join to it. Renaming onto yourself (a pure\n * re-casing, or onto one of your own aliases) is neither. A source with no document landing\n * on a taken name redirects: its links come to point at the page that already answers to it\n * (ADR 0064 §2). `into` is that page's own title.\n */\nfunction stepFor(from: string, to: string, hasDocument: boolean, existing: Map<string, string>): RenameStep {\n const holder = existing.get(conceptKey(to))\n const taken = holder !== undefined && conceptKey(holder) !== conceptKey(from)\n return {\n from,\n to,\n hasDocument,\n merges: taken && hasDocument,\n redirects: taken && !hasDocument,\n into: taken ? holder : to,\n }\n}\n\n/** Nesting depth of a concept - `[[[[A]] B]] C` is deeper than `[[A]] B`. */\nfunction depthOf(concept: string): number {\n let depth = 0\n let max = 0\n for (let i = 0; i < concept.length - 1; i++) {\n if (concept[i] === '[' && concept[i + 1] === '[') {\n depth += 1\n max = Math.max(max, depth)\n i += 1\n } else if (concept[i] === ']' && concept[i + 1] === ']') {\n depth -= 1\n i += 1\n }\n }\n return max\n}\n","/**\n * ServerDocumentStore (plan Phase 3 Task 6): the DocumentStore seam over the E2EE sync\n * engine. Mirrors FilesystemDocumentStore's surface so every workspace consumer (tree,\n * backlink index, journal, DocumentView) is unchanged — but text lives in a Yjs Y.Text\n * per document, and identity/title/date live ONLY in the encrypted graph-root registry\n * (ADR 0024), never on the server as plaintext.\n */\nimport * as Y from 'yjs'\nimport type { Awareness } from 'y-protocols/awareness'\nimport { isJournalConcept } from '$lib/document/journal-concept'\nimport { normaliseAliases, withFrontmatterIdentity } from '$lib/document/frontmatter/identity'\nimport { parseFrontmatter } from '$lib/storage/fs/frontmatter'\nimport { frontmatterSpan } from '$lib/storage/fs/frontmatter-span'\nimport { conceptKey } from '$lib/storage/fs/identity'\nimport type { DocumentEntry } from '$lib/storage/fs/scan'\nimport {\n type ChangeOrigin,\n DocumentNotFoundError,\n DocumentSyncDegradedError,\n type EditorDocument,\n type TextChange,\n} from '$lib/document/types'\nimport type { IndexDocSnapshot } from '$lib/storage/fs/filesystem-store'\nimport type {\n IndexChangeCheckpoint,\n IndexSnapshotOptions,\n StoreChangeListener,\n} from '$lib/document/backlinks/live-index'\nimport { createBreather } from '$lib/activity/breathe'\nimport type { GraphSync, RegistryEntry } from '$lib/sync/graph-sync'\nimport { REMOTE, SUPPRESSED, type SyncHealth } from '$lib/sync/doc-sync'\nimport { type TextSplice, countWikilinkTargets, wikilinkScopeSplices } from '$lib/document/wikilink/rename'\nimport { documentProtection } from '$lib/document/protection/cipher-fence'\nimport { mergeDocuments } from '../merge'\nimport { planRename, refuseProtectedMerges } from '../rename-plan'\nimport { type RenameOptions, type RenamePlan, type RenameResult, mergeCount, renameSteps } from '../rename'\n\nexport interface ServerDocumentStoreOptions {\n /** Bounded wait (ms) for an initial relay connection in scan(). */\n readyTimeoutMs?: number\n /**\n * A document deleted elsewhere was brought back by a local edit (ADR 0039 §4). The\n * workspace tells the editor, because a document silently returning is inexplicable to\n * whoever deleted it.\n */\n onResurrected?: (concept: string) => void\n}\n\n/**\n * One document's identity keyed by the id that survives a rename. The [[Local Mirror]] needs the\n * id rather than the concept: a concurrent creation race can leave two registry entries with the\n * same title or date, and a concept alone can neither tell them apart nor keep their files stable.\n */\nexport interface DocumentIdentity {\n docId: string\n kind: RegistryEntry['kind']\n concept: string\n aliases: string[]\n}\n\n/** A document's full text, and whether it can be trusted as the document's settled content. */\nexport interface DocumentText {\n text: string\n /**\n * False when the text may not be the document's real content - its history has not arrived,\n * its key is missing, or its ciphertext will not open. A consumer that writes somewhere\n * durable must skip it rather than record an empty document.\n */\n settled: boolean\n}\n\n/** Structural mirror of FilesystemDocumentStore (documented in document/types.ts). */\nexport interface ServerDocumentStore {\n open(target: string): EditorDocument\n /**\n * Resolves when a document has seeded from the Local Cache and, for a cold empty seed,\n * applied its first relay page. This is enough to present content; full historical\n * reconciliation continues independently to its terminal page.\n */\n whenReady(target: string): Promise<void>\n /** The collab buffer for a target — DocumentView mounts y-codemirror over it. */\n getYText(target: string): Y.Text | undefined\n /** The per-doc presence handle for a target — remote cursors (ADR 0024). */\n getAwareness(target: string): Awareness | undefined\n /** Retain relay content and presence while a real view consumes this document. */\n retainDocument(target: string): () => void\n scan(): Promise<void>\n listDocuments(): DocumentEntry[]\n /** Every document's identity, keyed by document id ({@link DocumentIdentity}). */\n listIdentities(): DocumentIdentity[]\n /**\n * Full materialized text for documents nobody has open, in bounded batches seeded from the\n * [[Local Cache]] and retired again - never through {@link open}, which starts a live editing\n * engine per document and keeps it for the session.\n *\n * An empty seed is a cache miss as often as it is an empty document, so those documents (and\n * only those) get a bounded relay catch-up before their emptiness is believed. Whatever is\n * still unconfirmed comes back with `settled: false`.\n */\n readTexts(\n docIds: readonly string[],\n options?: { timeoutMs?: number; onProgress?: (done: number, total: number) => void },\n ): Promise<Map<string, DocumentText>>\n /**\n * Resolves true once the encrypted registry has been confirmed against the server this\n * session. Anything that deletes on the strength of \"not in the registry\" has to wait for\n * this: offline, the registry can be a partial cached one.\n */\n confirmRegistry(timeoutMs?: number): Promise<boolean>\n onDocumentsChanged(listener: () => void): () => void\n onDocumentRemoved(listener: (target: string) => void): () => void\n onChange(listener: StoreChangeListener): () => void\n snapshotForIndex(): Promise<IndexDocSnapshot[]>\n streamForIndex(options?: IndexSnapshotOptions): Promise<{\n total: number\n batches: AsyncIterable<readonly IndexDocSnapshot[]>\n }>\n /** Start bounded relay catch-up for every indexed document after a warm reopen. */\n catchUpPersistedIndex(): Promise<void>\n /** Durable Local Cache boundaries not yet represented by a committed index operation. */\n pendingIndexChanges(): Promise<IndexChangeCheckpoint>\n /** One document's index snapshot, for a listener that knows which one changed. */\n snapshotDocument(concept: string): IndexDocSnapshot | null\n reconcile(): Promise<void>\n resolveConflict(target: string, choice: 'keep-mine' | 'take-disk'): Promise<void>\n /**\n * Create the [[Journal Entry]] for `date` (a `YYYY-MM-DD` day), seeded with `body`.\n * The day is the identity here too - it is the registry entry, not a title (ADR 0056).\n */\n createJournal(date: string, body?: string): Promise<string>\n /** `body` seeds the new document's content, so a promoting [[Draft]] needs no second write (ADR 0050). */\n createPage(title: string, body?: string): Promise<string>\n /** What a rename would do, before anything is written (ADR 0038 §1) — the dialog's preview. */\n planRename(from: string, to: string, referencingDocuments?: number): Promise<RenamePlan>\n /** Apply a rename: the document, its scoped concepts, and any merges (ADR 0037, 0038). */\n renamePage(from: string, to: string, options: RenameOptions): Promise<RenameResult>\n /** Delete a document. Immediate and irreversible (ADR 0039). */\n deleteDocument(concept: string): Promise<void>\n /**\n * Drop deleted content from a document's Local Cache row. After protecting a document the\n * row otherwise keeps the pre-protection body for the document's lifetime, readable from\n * the profile directory with no key. The live document collects on its own (the write\n * runs under an untracked origin); the row is what has to be told.\n */\n compactDocument(target: string): Promise<void>\n /**\n * Upload a consolidated encrypted [[Snapshot]] of a document to the relay now, instead of\n * waiting for the idle trigger, and read it back to verify it (ADR 0025, amended). Nothing\n * in the app calls this on a timer; it is the manual and e2e handle on compaction.\n */\n uploadSnapshot(target: string): Promise<void>\n /**\n * Set a document's aliases (ADR 0061): the registry entry, then the block if the text carries\n * one. Normalised: trimmed, unique, never the document's own name.\n */\n setAliases(target: string, aliases: readonly string[]): Promise<void>\n dispose(): Promise<void>\n}\n\nfunction conceptOf(entry: RegistryEntry): string {\n return entry.kind === 'journal' ? (entry.date ?? '') : (entry.title ?? '')\n}\n\n/**\n * Whether `promise` settled within `timeoutMs`. A sync milestone that never arrives (offline, a\n * silent server) is a bounded wait and a `false`, never a hang and never an unhandled rejection:\n * the caller decides what an unreached milestone means.\n */\nasync function reachedWithin(promise: Promise<unknown>, timeoutMs: number): Promise<boolean> {\n let timer: ReturnType<typeof setTimeout> | undefined\n try {\n return await Promise.race([\n promise.then(\n () => true,\n () => false,\n ),\n new Promise<boolean>((resolve) => {\n timer = setTimeout(() => resolve(false), timeoutMs)\n }),\n ])\n } finally {\n if (timer) clearTimeout(timer)\n }\n}\n\n/** Health that says the bytes are not this document's, so neither text nor emptiness is real. */\nfunction contentBlocked(health: SyncHealth): boolean {\n return health === 'key-unavailable' || health === 'ciphertext-corrupt'\n}\n\nexport function createServerDocumentStore(\n graph: GraphSync,\n options: ServerDocumentStoreOptions,\n): ServerDocumentStore {\n const registry = graph.registry()\n /** Engines created between yields while snapshotting — small enough to keep frames free. */\n const ENGINE_BATCH = 100\n /** Longest a cold-empty document holds its loading state waiting for first catchup. */\n const COLD_CONTENT_TIMEOUT_MS = 15000\n const breathe = createBreather()\n\n const changeListeners = new Set<StoreChangeListener>()\n const docsChangedListeners = new Set<() => void>()\n const removedListeners = new Set<(target: string) => void>()\n const resurrectionClaims = new Map<string, RegistryEntry>()\n let disposed = false\n /** Whether the encrypted registry has been read to its terminal relay page this session. */\n let registryConfirmed = false\n\n interface RegistryIdentity {\n kind: RegistryEntry['kind']\n concept: string\n aliases: string[]\n }\n\n function currentRegistryIdentities(): Map<string, RegistryIdentity> {\n const out = new Map<string, RegistryIdentity>()\n registry.forEach((entry, docId) => {\n out.set(docId, {\n kind: entry.kind,\n concept: conceptOf(entry),\n // Alias order has no meaning in lookup or in the derived index. Normalising\n // here prevents a CRDT replay with reordered aliases from becoming an\n // unnamed full-index replacement.\n aliases: [...(entry.aliases ?? [])].sort((left, right) =>\n left.localeCompare(right),\n ),\n })\n })\n return out\n }\n\n function sameIdentity(\n before: RegistryIdentity | undefined,\n after: RegistryIdentity | undefined,\n ): boolean {\n if (!before || !after) return before === after\n return (\n before.kind === after.kind &&\n before.concept === after.concept &&\n before.aliases.length === after.aliases.length &&\n before.aliases.every((alias, index) => alias === after.aliases[index])\n )\n }\n\n // key → concept, so a removal can report the CONCEPT (what a View is keyed by), not the\n // lower-cased key. The filesystem store reports the concept; consumers should not have to\n // know which backend they are talking to.\n let known = currentConcepts()\n let docIdsByConcept = currentDocIds()\n let knownRegistry = currentRegistryIdentities()\n\n function currentConcepts(): Map<string, string> {\n const out = new Map<string, string>()\n registry.forEach((entry) => {\n const concept = conceptOf(entry)\n out.set(conceptKey(concept), concept)\n })\n return out\n }\n\n function currentDocIds(): Map<string, string> {\n const out = new Map<string, string>()\n registry.forEach((entry, docId) => {\n out.set(conceptKey(conceptOf(entry)), docId)\n for (const alias of entry.aliases ?? []) out.set(conceptKey(alias), docId)\n })\n return out\n }\n\n // Registry membership → onDocumentsChanged (+ removals). A pure addition names the\n // exact concept so the index can ingest one document. Deletes, renames and mixed\n // transactions still require a replacement because they can remove several identities.\n const unobserveRegistry = (() => {\n const handler = (event: Y.YMapEvent<RegistryEntry>) => {\n const nextRegistry = currentRegistryIdentities()\n const changedDocIds = [...new Set([...knownRegistry.keys(), ...nextRegistry.keys()])]\n .filter((docId) => !sameIdentity(knownRegistry.get(docId), nextRegistry.get(docId)))\n // Yjs can replay a different CRDT struct which resolves to the same JSON value.\n // On graph reopen that is restored state, not a rename or membership change.\n // Emitting an unnamed index change here made every new tab walk the full graph.\n if (changedDocIds.length === 0) return\n\n const additions: string[] = []\n let additionsOnly = true\n for (const docId of changedDocIds) {\n const before = knownRegistry.get(docId)\n const after = nextRegistry.get(docId)\n if (before || !after) {\n additionsOnly = false\n continue\n }\n additions.push(after.concept)\n }\n const next = currentConcepts()\n for (const [key, concept] of known) {\n if (!next.has(key)) removedListeners.forEach((l) => l(concept))\n }\n known = next\n docIdsByConcept = currentDocIds()\n knownRegistry = nextRegistry\n docsChangedListeners.forEach((l) => l())\n if (additionsOnly) {\n for (const concept of additions) {\n changeListeners.forEach((listener) => listener({ concept }))\n }\n } else {\n changeListeners.forEach((listener) => listener())\n }\n for (const [docId, change] of event.changes.keys) {\n if (change.action !== 'delete') continue\n const claimed = resurrectionClaims.get(docId)\n if (!claimed) continue\n queueMicrotask(() => {\n if (registry.has(docId)) return\n registry.set(docId, claimed)\n resurrectionClaims.delete(docId)\n options.onResurrected?.(conceptOf(claimed))\n })\n }\n }\n registry.observe(handler)\n return () => registry.unobserve(handler)\n })()\n\n // Document *content* changes (Y.Text edits, local or remote) are also onChange — this is\n // what keeps the derived index and the Local Mirror current on edits, not just on\n // add/remove. (The registry itself is one of these docs; its handler above still runs.)\n //\n // The changed document is NAMED. Discarding it here is what made one keystroke mean\n // \"everything changed\", and cost a full re-index of the graph (~3.9s at 2431 documents).\n // A listener that can act on one document should; one that cannot ignores the argument.\n const unobserveDocUpdates = graph.onDocUpdate((docId) => {\n // The root document contains registry and graph metadata. Its registry observer above\n // has better information, while metadata does not affect the document index at all.\n // Forwarding the root update as unnamed turned every page creation into a full rebuild.\n if (graph.docSync(docId).doc === graph.rootDoc) return\n const entry = entryFor(docId)\n const change = entry ? { concept: conceptOf(entry) } : undefined\n changeListeners.forEach((l) => l(change))\n })\n\n /** Find the registry docId whose concept matches `target` (case-insensitive). */\n function docIdFor(target: string): string | undefined {\n return docIdsByConcept.get(conceptKey(target))\n }\n\n /**\n * What the [[Derived Index]] derives over: the body below any [[Frontmatter]], as a Filesystem\n * Backend hands it (ADR 0061). The block is opaque to the index everywhere, so a wikilink\n * written up there makes no backlink on either backend - and line numbers it records stay\n * body-relative, which is what the reveal offset expects.\n */\n function bodyForIndex(text: string): string {\n return parseFrontmatter(text).body\n }\n\n /**\n * Bring a block the text carries in line with the registry (ADR 0061). Part of the action that\n * changed the registry - a rename, an alias change - and never a reaction to one arriving: if\n * every member's device rewrote the block on seeing the registry move, the CRDT would merge N\n * identical concurrent edits into duplicated text. Adds no block to a document without one. A\n * journal's title is its date, so only its aliases are mirrored.\n */\n function writeBackIdentity(docId: string): void {\n const entry = registry.get(docId)\n if (!entry) return\n const ytext = graph.docSync(docId).doc.getText('content')\n const text = ytext.toString()\n const next = withFrontmatterIdentity(text, {\n ...(entry.kind === 'page' ? { title: conceptOf(entry) } : {}),\n aliases: entry.aliases ?? [],\n })\n if (next === text) return\n // Only the block is replaced: the body is verbatim in `next`, and a whole-document rewrite\n // would be a whole-document edit for every other member to receive.\n const before = frontmatterSpan(text)?.end ?? 0\n const after = frontmatterSpan(next)?.end ?? 0\n ytext.doc!.transact(() => {\n if (before > 0) ytext.delete(0, before)\n ytext.insert(0, next.slice(0, after))\n }, STORE)\n }\n\n function entryFor(docId: string): RegistryEntry | undefined {\n return registry.get(docId)\n }\n\n /**\n * An edit outranks a delete (ADR 0039 §4), watched on the Y.Text rather than on\n * `applyChange`.\n *\n * On this backend the editor binds the Y.Text **directly** (yCollab), so `applyChange` is\n * never called for a Server-backed document - the resurrection check living only there\n * made the rule unreachable in the app while passing its unit tests. Reading the local\n * edit off the CRDT catches every way a document can be typed into.\n *\n * `registry` lives in the ROOT document and the Y.Text in the document's own, so writing\n * one from the other's observer is not re-entrant.\n */\n const resurrectors = new Map<string, () => void>()\n\n /**\n * The origin the store stamps on its OWN structural writes, so they are never mistaken for\n * a user's edit. Without it the deleter resurrected its own delete: `deleteDocument` drops\n * the registry entry and then clears the Y.Text, and that clear is a local content change\n * to a document with no entry - which is precisely the shape of a resurrection.\n */\n const STORE = Symbol('etherpk-store-write')\n\n function watchForResurrection(docId: string, openedAs: RegistryEntry | undefined): void {\n if (!openedAs || resurrectors.has(docId)) return\n const ytext = graph.docSync(docId).doc.getText('content')\n const handler = (_e: Y.YTextEvent, tx: Y.Transaction) => {\n if (tx.origin === REMOTE) return // someone else's edit asserts nothing on our behalf\n if (tx.origin === STORE) return // our own delete/merge, not a user asserting anything\n if (tx.origin === SUPPRESSED) return // ordered lifecycle clear, not an edit\n const lifecycle = graph.docSync(docId).lifecycle()\n if (lifecycle === 'deleted' || lifecycle === 'resurrecting') {\n resurrectionClaims.set(docId, openedAs)\n }\n if (registry.has(docId)) return\n registry.set(docId, openedAs)\n resurrectionClaims.delete(docId)\n options.onResurrected?.(conceptOf(openedAs))\n }\n ytext.observe(handler)\n resurrectors.set(docId, () => ytext.unobserve(handler))\n }\n\n return {\n open(target: string): EditorDocument {\n const docId = docIdFor(target)\n if (!docId) throw new DocumentNotFoundError(target)\n const ytext = graph.docSync(docId).doc.getText('content')\n // The entry as it was when opened, so a resurrection restores THIS document -\n // a journal comes back a journal, keeping its aliases - rather than a bare page.\n const openedAs = registry.get(docId)\n watchForResurrection(docId, openedAs)\n return {\n id: target,\n getText: () => ytext.toString(),\n applyChange(change: TextChange, origin: ChangeOrigin = 'editor') {\n // An edit outranks a delete (ADR 0039 §4): if the entry has gone while\n // this document was open, typing in it asserts that it should exist.\n if (openedAs && !registry.has(docId)) {\n registry.set(docId, openedAs)\n resurrectionClaims.delete(docId)\n options.onResurrected?.(conceptOf(openedAs))\n }\n // Local edit → mutate the Y.Text (NOT REMOTE) so the engine sees it as\n // local and syncs it. Never notifies our subscribers. An EXTERNAL write -\n // protecting the document is one - goes under the store's own origin, which\n // the collaborative editor's undo manager does not track: under the default\n // origin it would pin the deleted plaintext against garbage collection, and\n // every state this document is ever serialised to would carry the body it\n // was protected to hide.\n ytext.doc!.transact(\n () => {\n if (change.to > change.from) ytext.delete(change.from, change.to - change.from)\n if (change.insert) ytext.insert(change.from, change.insert)\n },\n origin === 'external' ? STORE : undefined,\n )\n },\n subscribe(listener: (text: string) => void) {\n // External/remote edits only: fire on REMOTE-origin changes.\n const handler = (_e: Y.YTextEvent, tx: Y.Transaction) => {\n if (tx.origin === REMOTE) listener(ytext.toString())\n }\n ytext.observe(handler)\n return () => ytext.unobserve(handler)\n },\n }\n },\n getYText(target: string): Y.Text | undefined {\n const docId = docIdFor(target)\n return docId ? graph.docSync(docId).doc.getText('content') : undefined\n },\n async whenReady(target: string): Promise<void> {\n const docId = docIdFor(target)\n if (!docId) return\n await graph.whenReady(docId)\n // A cold cache seeds EMPTY — a miss, not content — while the real text is\n // still in flight over the wire. Resolving here dropped the loading overlay\n // onto 4.5s of blank editor (measured live, 2400-doc cold open). Hold until\n // the first catch-up page has applied; capped so an offline or silent server\n // leaves an editable empty document rather than an eternal spinner.\n if (graph.docSync(docId).doc.getText('content').length === 0 && graph.isConnected()) {\n await Promise.race([\n graph.firstCatchupPageDoc(docId),\n new Promise((resolve) => setTimeout(resolve, COLD_CONTENT_TIMEOUT_MS)),\n ])\n if (graph.docSync(docId).doc.getText('content').length === 0) {\n // Caught up (or timed out) and STILL empty: either a genuinely empty\n // document, or its history never arrived / never decrypted — the\n // doc-sync skip counter warning says which. Name it either way.\n console.warn(`[sync] \"${target}\" has no content after catchup — empty doc, or its history was skipped`)\n }\n }\n const health = graph.docSync(docId).health()\n // Only states that genuinely block content fail the open. A sequence gap or a\n // stale generation is a routine self-healing moment. The engine has already\n // requested catch-up when it sets them, and surfacing it as \"opened without\n // its cached content\" put a false failure banner on every open document while\n // busy peers kept the relay head moving (live, 2026-07-30).\n if (health === 'key-unavailable' || health === 'ciphertext-corrupt') {\n throw new DocumentSyncDegradedError(target, health)\n }\n },\n getAwareness(target: string): Awareness | undefined {\n const docId = docIdFor(target)\n return docId ? graph.docSync(docId).awareness : undefined\n },\n retainDocument(target: string): () => void {\n const docId = docIdFor(target)\n return docId ? graph.retainDoc(docId) : () => {}\n },\n async scan(): Promise<void> {\n // Seed the cached registry first. A cold offline open remains bounded, but once\n // connected we must consume the ROOT doc's terminal relay page before anything\n // identity-sensitive reads it. The first page can be a partial registry.\n await graph.ready()\n const reachedRelay =\n graph.isConnected() ||\n (await new Promise<boolean>((resolve) => {\n let settled = false\n const finish = (connected: boolean) => {\n if (settled) return\n settled = true\n clearTimeout(timer)\n resolve(connected)\n }\n const timer = setTimeout(\n () => finish(false),\n Math.max(0, options.readyTimeoutMs ?? 2000),\n )\n void graph.connected().then(() => finish(true))\n }))\n if (reachedRelay) {\n await graph.rootCaughtUp()\n registryConfirmed = true\n }\n known = currentConcepts()\n docIdsByConcept = currentDocIds()\n knownRegistry = currentRegistryIdentities()\n },\n listDocuments(): DocumentEntry[] {\n const journals: DocumentEntry[] = []\n const pages: DocumentEntry[] = []\n registry.forEach((entry, docId) => {\n const concept = conceptOf(entry)\n const doc: DocumentEntry = {\n kind: entry.kind,\n concept,\n key: conceptKey(concept),\n // Server docs are not files; fabricate fs-shaped fields the tree ignores.\n subdir: entry.kind === 'journal' ? 'journals' : 'pages',\n fileName: docId,\n aliases: entry.aliases ?? [],\n lastModified: 0,\n // No file behind a server document; the Filesystem Backend's (mtime, size) fast path\n // needs the field on every DocumentEntry, and zero is as honest as the mtime above.\n size: 0,\n }\n ;(entry.kind === 'journal' ? journals : pages).push(doc)\n })\n journals.sort((a, b) => b.concept.localeCompare(a.concept)) // date-desc\n pages.sort((a, b) => a.concept.localeCompare(b.concept)) // alpha\n return [...journals, ...pages]\n },\n listIdentities(): DocumentIdentity[] {\n const out: DocumentIdentity[] = []\n registry.forEach((entry, docId) => {\n out.push({\n docId,\n kind: entry.kind,\n concept: conceptOf(entry),\n aliases: [...(entry.aliases ?? [])],\n })\n })\n return out\n },\n async readTexts(docIds, readOptions): Promise<Map<string, DocumentText>> {\n const out = new Map<string, DocumentText>()\n const timeoutMs = readOptions?.timeoutMs ?? COLD_CONTENT_TIMEOUT_MS\n const empty: string[] = []\n // Reported against the whole request, not the batch: a caller showing this to a\n // person is answering \"how much of my graph is read\", not \"how far into batch 14\".\n const report = () => readOptions?.onProgress?.(out.size, docIds.length)\n const read = (docId: string): { text: string; blocked: boolean } => ({\n text: graph.docSync(docId).doc.getText('content').toString(),\n blocked: contentBlocked(graph.docSync(docId).health()),\n })\n\n // Cache-only batches, retired as they are consumed: the cost of a mirror pass must\n // not scale with live engines the way opening every document does.\n for (let start = 0; start < docIds.length; start += ENGINE_BATCH) {\n if (disposed) break\n const batch = docIds.slice(start, start + ENGINE_BATCH)\n try {\n await graph.seedDocsFromCache(batch)\n for (const docId of batch) {\n const { text, blocked } = read(docId)\n out.set(docId, { text, settled: !blocked && text.length > 0 })\n if (!blocked && text.length === 0) empty.push(docId)\n }\n } finally {\n graph.retireDocs(batch)\n }\n report()\n await breathe()\n }\n\n // An empty seed is a cache miss as often as it is an empty document - the whole of\n // the mirror's \"every page arrived without its body\" bug. Only these pay for a relay\n // catch-up, so a warm graph pays for none.\n if (!graph.isConnected()) return out\n for (let start = 0; start < empty.length; start += ENGINE_BATCH) {\n if (disposed) break\n const batch = empty.slice(start, start + ENGINE_BATCH)\n try {\n await graph.readyDocs(batch)\n await Promise.all(\n batch.map(async (docId) => {\n const reached = await reachedWithin(graph.caughtUpDoc(docId), timeoutMs)\n const { text, blocked } = read(docId)\n out.set(docId, { text, settled: !blocked && (reached || text.length > 0) })\n }),\n )\n } finally {\n graph.retireDocs(batch)\n }\n await breathe()\n }\n return out\n },\n async confirmRegistry(timeoutMs = 5000): Promise<boolean> {\n if (registryConfirmed) return true\n if (!graph.isConnected()) return false\n if (await reachedWithin(graph.rootCaughtUp(), timeoutMs)) registryConfirmed = true\n return registryConfirmed\n },\n onDocumentsChanged(listener) {\n docsChangedListeners.add(listener)\n return () => docsChangedListeners.delete(listener)\n },\n onDocumentRemoved(listener) {\n removedListeners.add(listener)\n return () => removedListeners.delete(listener)\n },\n onChange(listener) {\n changeListeners.add(listener)\n return () => changeListeners.delete(listener)\n },\n async snapshotForIndex(options?: IndexSnapshotOptions): Promise<IndexDocSnapshot[]> {\n const snapshots: IndexDocSnapshot[] = []\n const stream = await this.streamForIndex(options)\n for await (const batch of stream.batches) snapshots.push(...batch)\n return snapshots\n },\n async streamForIndex(options?: IndexSnapshotOptions) {\n const entries: Array<[string, RegistryEntry]> = []\n registry.forEach((entry, docId) => entries.push([docId, entry]))\n return {\n total: entries.length,\n batches: (async function* () {\n for (let start = 0; start < entries.length; start += ENGINE_BATCH) {\n const batch = entries.slice(start, start + ENGINE_BATCH)\n const docIds = batch.map(([docId]) => docId)\n try {\n const docs = await graph.seedDocsFromCache(docIds)\n const snapshots = batch.map(([_docId, entry], index) => ({\n concept: conceptOf(entry),\n kind: entry.kind,\n aliases: entry.aliases ?? [],\n text: bodyForIndex(docs[index].getText('content').toString()),\n }))\n options?.onProgress?.({\n phase: 'loading',\n done: Math.min(start + batch.length, entries.length),\n total: entries.length,\n })\n yield snapshots\n } finally {\n graph.retireDocs(docIds)\n }\n await breathe()\n }\n })(),\n }\n },\n async catchUpPersistedIndex() {\n const docIds: string[] = []\n registry.forEach((_entry, docId) => docIds.push(docId))\n // One bounded metadata request per 512 documents replaces one catch-up request\n // per document. A warm 2,400-document graph therefore asks the relay five cheap\n // questions and only materialises documents whose sequence actually changed.\n const staleDocIds = await graph.docsNeedingCatchup(docIds)\n for (let start = 0; start < staleDocIds.length; start += ENGINE_BATCH) {\n if (disposed) return\n const batch = staleDocIds.slice(start, start + ENGINE_BATCH)\n try {\n await graph.readyDocs(batch)\n // Catch-up is serialized by GraphSync. Waiting for each terminal page\n // keeps the graph-scoped cross-tab lock until the complete history has\n // advanced the shared cache, while bounding live Y.Docs to one batch.\n // If the browser starts offline, these promises stay pending and the\n // batch remains alive until the socket eventually opens. Graph disposal\n // releases the waits, so leaving an offline workspace cannot leak it.\n await Promise.all(batch.map((docId) => graph.caughtUpDoc(docId)))\n } finally {\n graph.retireDocs(batch)\n }\n if (disposed) return\n await breathe()\n }\n },\n async pendingIndexChanges() {\n const checkpoint = await graph.pendingIndexChanges()\n const root = new Y.Doc()\n try {\n if (checkpoint.rootUpdate) Y.applyUpdate(root, checkpoint.rootUpdate)\n } catch (error) {\n root.destroy()\n throw error\n }\n const checkpointRegistry = root.getMap<RegistryEntry>('registry')\n const entries: Array<[string, RegistryEntry]> = []\n checkpointRegistry.forEach((entry, docId) => entries.push([docId, entry]))\n root.destroy()\n const entriesByDocId = new Map(entries)\n\n const snapshot = async (\n docId: string,\n entry: RegistryEntry,\n ): Promise<IndexDocSnapshot> => {\n const update = await checkpoint.readDocument(docId)\n const doc = new Y.Doc()\n try {\n if (update) Y.applyUpdate(doc, update)\n return {\n concept: conceptOf(entry),\n kind: entry.kind,\n aliases: entry.aliases ?? [],\n text: bodyForIndex(doc.getText('content').toString()),\n }\n } finally {\n doc.destroy()\n }\n }\n\n const checkpointStream: NonNullable<IndexChangeCheckpoint['streamForIndex']> =\n async (snapshotOptions) => ({\n total: entries.length,\n batches: (async function* () {\n for (let start = 0; start < entries.length; start += ENGINE_BATCH) {\n const batch = entries.slice(start, start + ENGINE_BATCH)\n const snapshots = await Promise.all(\n batch.map(([docId, entry]) => snapshot(docId, entry)),\n )\n snapshotOptions?.onProgress?.({\n phase: 'loading',\n done: Math.min(start + batch.length, entries.length),\n total: entries.length,\n })\n yield snapshots\n await breathe()\n }\n })(),\n })\n\n const concepts = new Map<string, string>()\n const documents: IndexDocSnapshot[] = []\n let requiresFullReplacement = false\n for (const docId of checkpoint.docIds) {\n const entry = entriesByDocId.get(docId)\n if (!entry) {\n // The root document owns graph identity, aliases and membership. A\n // removed content document is also absent here. Either shape can remove\n // index rows, so a targeted upsert is insufficient.\n requiresFullReplacement = true\n break\n }\n const concept = conceptOf(entry)\n concepts.set(conceptKey(concept), concept)\n documents.push(await snapshot(docId, entry))\n }\n return {\n changes: requiresFullReplacement\n ? null\n : [...concepts.values()].map((concept) => ({ concept })),\n documents,\n streamForIndex: checkpointStream,\n acknowledge: checkpoint.acknowledge,\n }\n },\n snapshotDocument(concept: string): IndexDocSnapshot | null {\n const docId = docIdFor(concept)\n const entry = docId ? registry.get(docId) : undefined\n if (!docId || !entry) return null\n return {\n concept: conceptOf(entry),\n kind: entry.kind,\n aliases: entry.aliases ?? [],\n text: bodyForIndex(graph.docSync(docId).doc.getText('content').toString()),\n }\n },\n\n async compactDocument(target: string): Promise<void> {\n const docId = docIdFor(target)\n if (docId) await graph.compactDoc(docId)\n },\n\n async uploadSnapshot(target: string): Promise<void> {\n const docId = docIdFor(target)\n if (docId) await graph.uploadSnapshot(docId)\n },\n\n async setAliases(target: string, aliases: readonly string[]): Promise<void> {\n const docId = docIdFor(target)\n const entry = docId ? registry.get(docId) : undefined\n if (!docId || !entry) throw new DocumentNotFoundError(target)\n const next = normaliseAliases(aliases, conceptOf(entry))\n const updated: RegistryEntry = { ...entry }\n if (next.length > 0) updated.aliases = next\n else delete updated.aliases\n registry.set(docId, updated)\n writeBackIdentity(docId)\n },\n // CRDTs merge — there is nothing to reconcile or resolve.\n async reconcile() {},\n async resolveConflict() {},\n async createJournal(date: string, body = ''): Promise<string> {\n const day = date.trim()\n if (!isJournalConcept(day)) throw new Error(`\"${date}\" is not a calendar day.`)\n if (docIdFor(day)) throw new Error(`A document for \"${day}\" already exists`)\n const docId = crypto.randomUUID()\n registry.set(docId, { kind: 'journal', date: day } satisfies RegistryEntry)\n // Seeded in the same breath as the registry entry, exactly as createPage does, so a\n // promoting [[Draft]] never writes twice (ADR 0050).\n if (body !== '') {\n const ytext = graph.docSync(docId).doc.getText('content')\n ytext.doc!.transact(() => ytext.insert(0, body))\n }\n return day\n },\n async createPage(title: string, body = ''): Promise<string> {\n if (docIdFor(title)) throw new Error(`A page for \"${title}\" already exists`)\n const docId = crypto.randomUUID()\n registry.set(docId, { kind: 'page', title } satisfies RegistryEntry)\n // Seed the content in the same breath as the registry entry, so a promoting\n // [[Draft]] never has to write twice (ADR 0050). A local-origin insert, exactly\n // as a keystroke would be, so the sync engine carries it like any other edit.\n if (body !== '') {\n const ytext = graph.docSync(docId).doc.getText('content')\n ytext.doc!.transact(() => ytext.insert(0, body))\n }\n return title\n },\n\n async planRename(from: string, to: string, referencingDocuments?: number): Promise<RenamePlan> {\n // No entry is not a refusal: a [[Pageless Concept]] renames by rewriting its links,\n // and the plan says so through a null kind (ADR 0064).\n const docId = docIdFor(from)\n const entry = docId ? registry.get(docId) : undefined\n\n // Counting here materialises every Y.Doc in the graph. The caller usually has the\n // figure already (the backlink index knows it); the slow path stays for those that\n // do not.\n let counted = 0\n const concepts: string[] = []\n const aliases: { name: string; concept: string }[] = []\n registry.forEach((e, id) => {\n concepts.push(conceptOf(e))\n for (const alias of e.aliases ?? []) aliases.push({ name: alias, concept: conceptOf(e) })\n if (referencingDocuments !== undefined) return\n const text = graph.docSync(id).doc.getText('content').toString()\n if (countWikilinkTargets(text, from) > 0) counted += 1\n })\n const plan = planRename({\n from,\n to,\n kind: entry?.kind ?? null,\n concepts,\n aliases,\n referencingDocuments: referencingDocuments ?? counted,\n })\n // A Protected Document never merges (ADR 0062). Read from the live document, which\n // is what a merge would join.\n return refuseProtectedMerges(plan, (concept) => {\n const id = docIdFor(concept)\n if (!id) return false\n return documentProtection(graph.docSync(id).doc.getText('content').toString()).kind === 'document'\n })\n },\n\n /**\n * Apply a rename plan (ADR 0038). A server document's identity lives ONLY in the\n * encrypted registry (ADR 0024), so a rename is a registry write - there is no\n * frontmatter here, which is why the alias strategy keeps the old name in the registry\n * entry rather than in the document body - and a [[Merge]] is a content write into the\n * surviving doc plus dropping the absorbed entry. A step with no document behind it\n * (a [[Pageless Concept]], ADR 0064) is only the body pass below.\n */\n async renamePage(from: string, to: string, options: RenameOptions): Promise<RenameResult> {\n // Re-plan rather than trust the dialog's preview - the graph may have moved. Only\n // the preview reads the reference count, so the apply path does not pay for it.\n const plan = await this.planRename(from, to, 0)\n if (plan.refusal) throw new Error(plan.refusal)\n\n for (const step of renameSteps(plan)) {\n const docId = docIdFor(step.from)\n if (!docId) continue // a pageless concept; only its links move (ADR 0064)\n const entry = registry.get(docId)\n if (!entry) continue\n\n let aliases = Array.isArray(entry.aliases) ? entry.aliases : []\n if (options.strategy === 'alias' && !aliases.some((a) => conceptKey(a) === conceptKey(step.from))) {\n // Every renamed document keeps its old name, cascaded ones included.\n aliases = [...aliases, step.from]\n }\n\n // The document that answers to the name - by title or alias (docIdFor resolves\n // both) - is the survivor, and keeps its own title (`step.into`).\n const targetId = step.merges ? docIdFor(step.into) : undefined\n if (targetId && targetId !== docId) {\n const survivor = graph.docSync(targetId).doc.getText('content')\n const absorbed = graph.docSync(docId).doc.getText('content')\n const survivorEntry = registry.get(targetId)\n // The absorbed document's block, if it has one, is identity the registry\n // already carries - its aliases union below, its title is the name being\n // vacated. As body text it would sit as a second `---` block inside the\n // survivor, so only what is below it is joined (as on a Filesystem Backend).\n const absorbedText = absorbed.toString()\n const absorbedBody = absorbedText.slice(frontmatterSpan(absorbedText)?.end ?? 0)\n const merged = mergeDocuments(\n { body: survivor.toString(), aliases: survivorEntry?.aliases ?? [] },\n { body: absorbedBody, aliases },\n )\n survivor.doc!.transact(() => {\n survivor.delete(0, survivor.length)\n survivor.insert(0, merged.body)\n }, STORE)\n registry.set(targetId, {\n ...(survivorEntry ?? { kind: entry.kind }),\n title: step.into,\n ...(merged.aliases.length > 0 ? { aliases: merged.aliases } : {}),\n })\n // The absorbed document's entry goes; its content is now in the survivor.\n registry.delete(docId)\n writeBackIdentity(targetId)\n continue\n }\n\n // A page renamed onto one of its own aliases: the alias becomes the title and\n // is no longer an alias.\n const next: RegistryEntry = { ...entry, title: step.to }\n const kept = normaliseAliases(aliases, step.to)\n if (kept.length > 0) next.aliases = kept\n else delete next.aliases\n registry.set(docId, next)\n writeBackIdentity(docId)\n }\n\n // Bodies last, in one pass: the cascade rule fixes plain and scoped references\n // together. Each document is its own CRDT transaction — there is none spanning them\n // — and within a document the rewrite is a set of per-occurrence splices, never a\n // whole-text replace (ADR 0066): a delete-all would not cover anything typed\n // concurrently by a Player or another device, which then survived and was re-seated\n // at the start of the document. Applied back-to-front so earlier offsets stay valid.\n let rewritten = 0\n if (options.strategy === 'rewrite') {\n const edits: { id: string; splices: TextSplice[] }[] = []\n registry.forEach((_e, id) => {\n const ytext = graph.docSync(id).doc.getText('content')\n // The body only, as on a Filesystem Backend: a block's title is the\n // write-back's to set, and anything else up there is not a reference.\n const text = ytext.toString()\n const prefix = frontmatterSpan(text)?.end ?? 0\n const splices = wikilinkScopeSplices(text.slice(prefix), plan.direct.from, plan.direct.to).map(\n (s) => ({ ...s, from: s.from + prefix, to: s.to + prefix }),\n )\n if (splices.length > 0) edits.push({ id, splices })\n })\n for (const edit of edits) {\n const ytext = graph.docSync(edit.id).doc.getText('content')\n ytext.doc!.transact(() => {\n for (let i = edit.splices.length - 1; i >= 0; i--) {\n const splice = edit.splices[i]\n ytext.delete(splice.from, splice.to - splice.from)\n ytext.insert(splice.from, splice.insert)\n }\n }, STORE)\n rewritten += 1\n }\n }\n\n return { concept: plan.direct.into, rewritten, cascaded: plan.cascade.length, merged: mergeCount(plan) }\n },\n\n /**\n * Delete a document (ADR 0039): the registry entry goes AND the stored content is\n * removed server-side, so [[Storage Footprint]] drops immediately rather than waiting\n * on compaction.\n */\n async deleteDocument(concept: string): Promise<void> {\n const docId = docIdFor(concept)\n if (!docId) return // already gone; deleting twice is not an error\n // The ordered content lifecycle transition is durable before the encrypted root\n // registry changes. A delayed old-generation append can no longer recreate rows.\n await graph.deleteDoc(docId)\n // Dropping the entry is what fires onDocumentRemoved. The registry is encrypted\n // client content, so it remains a separate convergent operation.\n registry.delete(docId)\n },\n\n async dispose() {\n disposed = true\n unobserveRegistry()\n unobserveDocUpdates()\n for (const stop of resurrectors.values()) stop()\n resurrectors.clear()\n await graph.flushAll()\n graph.dispose()\n },\n }\n}\n","/**\n * On-disk persistence for the two things the [[Headless Client]] otherwise rebuilds on every\n * launch: the [[Local Cache]] and the [[Derived Index]]. Neither store is re-implemented here;\n * both keep running exactly as they do in the browser, and this module only carries their bytes\n * across process lifetimes.\n *\n * - The Local Cache runs over `fake-indexeddb`, an in-memory IndexedDB. Its rows for one graph\n * (`docs`, `outbox`, `index-dirty` of the `etherpk-sync` database) are read back through the\n * ordinary IndexedDB API and written to one file; on launch they are put back into the fresh\n * in-memory database before the sync engine opens it. The cache's own merge rules, dirty\n * tokens and watermarks are therefore untouched - a restored row is the row that was there.\n * - The index is SQLite in WASM memory. sqlite-wasm exports a database as its file bytes\n * (`sqlite3_js_db_export`) and imports them into its virtual filesystem\n * (`sqlite3_js_posix_create_file`), so a launch opens the index the last run committed and the\n * worker core takes its warm path: only documents whose watermark moved are caught up.\n *\n * **Invalidation is the engines' own.** Nothing here decides what is stale. On launch the sync\n * engine compares every cached document's `(generation, lifecycle, lastSeq)` with the relay in\n * bounded metadata requests and catches up only what moved; a deletion arrives as a lifecycle\n * change; unacknowledged local edits replay from the outbox; the index re-ingests exactly the\n * documents whose `index-dirty` tokens the cache still holds. A schema change is a wholesale\n * discard by design (ADR 0015): the file names carry the cache's `DB_VERSION` and the index's\n * `INDEX_SCHEMA_VERSION`, so a newer build simply does not find the old files, and the core's own\n * `PRAGMA user_version` check discards a stale index it is handed.\n *\n * **Ordering.** A snapshot writes the cache first and the index second. The index must never be\n * behind what the cache says it has acknowledged, and an acknowledgement is only ever written\n * after the ingest it confirms, so an index captured after the cache always covers every\n * acknowledged token in it; a write that lands between the two captures leaves its dirty token\n * in the cache snapshot and is simply re-ingested next launch.\n *\n * What is on disk is plaintext Yjs state and plaintext index rows, the same as the browser\n * profile holds in IndexedDB and OPFS: the trust class the user chose for this machine. A\n * protected document's cache row is collected the moment it reads as protected (the same\n * `collectRowWhen` the workspace passes), and the index never held one. `logout` deletes the lot.\n */\n\nimport { deserialize, serialize } from 'node:v8'\nimport { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises'\nimport { homedir } from 'node:os'\nimport { join } from 'node:path'\n\nimport { INDEX_SCHEMA_VERSION, createSchema, isUsableIndex, type SqlDb } from '$lib/document/index-db'\nimport { wrapOo1Db } from '$lib/document/index-db-sqlite'\nimport type { IndexDbHost } from '$lib/document/index-worker/core'\n\n/** The Local Cache's IndexedDB database and stores, as `sync/local-cache.ts` names them. */\nconst CACHE_DB = 'etherpk-sync'\nconst CACHE_DB_VERSION = 3\nconst CACHE_STORES = ['docs', 'outbox', 'index-dirty'] as const\ntype CacheStore = (typeof CACHE_STORES)[number]\n\ninterface CacheSnapshot {\n version: number\n graphId: string\n rows: Record<CacheStore, unknown[]>\n}\n\n/**\n * `~/.cache/etherpk/mcp/<server host>/<graph id>/`, or under `ETHERPK_MCP_CACHE_DIR`. The host's\n * port separator becomes `_`: a colon is not a path character on Windows.\n */\nexport function graphCacheDir(env: NodeJS.ProcessEnv, serverBaseUrl: string, graphId: string): string {\n const base = env.ETHERPK_MCP_CACHE_DIR?.trim() || join(env.XDG_CACHE_HOME?.trim() || join(homedir(), '.cache'), 'etherpk', 'mcp')\n const host = new URL(serverBaseUrl).host.replace(/[^A-Za-z0-9.-]/g, '_')\n return join(base, host, graphId)\n}\n\n/** The root every graph's cache dir sits under, for `logout` to remove. */\nexport function cacheRoot(env: NodeJS.ProcessEnv): string {\n return env.ETHERPK_MCP_CACHE_DIR?.trim() || join(env.XDG_CACHE_HOME?.trim() || join(homedir(), '.cache'), 'etherpk', 'mcp')\n}\n\nexport function cacheFile(dir: string): string {\n return join(dir, `local-cache.v${CACHE_DB_VERSION}.bin`)\n}\n\nexport function indexFile(dir: string): string {\n return join(dir, `index.v${INDEX_SCHEMA_VERSION}.sqlite`)\n}\n\nfunction request<T>(req: IDBRequest<T>): Promise<T> {\n return new Promise((resolve, reject) => {\n req.onsuccess = () => resolve(req.result)\n req.onerror = () => reject(req.error)\n })\n}\n\nfunction done(tx: IDBTransaction): Promise<void> {\n return new Promise((resolve, reject) => {\n tx.oncomplete = () => resolve()\n tx.onerror = () => reject(tx.error)\n tx.onabort = () => reject(tx.error)\n })\n}\n\n/** Open the cache database the sync engine already created; never creates or upgrades it. */\nasync function openCacheDb(): Promise<IDBDatabase> {\n const db = await request(indexedDB.open(CACHE_DB, CACHE_DB_VERSION))\n for (const store of CACHE_STORES) {\n if (!db.objectStoreNames.contains(store)) {\n db.close()\n throw new Error(`Local Cache database has no \"${store}\" store; open the graph cache before restoring it.`)\n }\n }\n return db\n}\n\n/** Every row of every cache store that belongs to `graphId`, as one serialisable snapshot. */\nexport async function captureLocalCache(graphId: string): Promise<CacheSnapshot> {\n const db = await openCacheDb()\n try {\n const tx = db.transaction([...CACHE_STORES], 'readonly')\n const rows = {} as Record<CacheStore, unknown[]>\n for (const store of CACHE_STORES) {\n const all = (await request(tx.objectStore(store).getAll())) as Array<{ graphId?: string }>\n rows[store] = all.filter((row) => row.graphId === graphId)\n }\n await done(tx)\n return { version: CACHE_DB_VERSION, graphId, rows }\n } finally {\n db.close()\n }\n}\n\n/** Put a snapshot's rows back. Existing rows for the same keys are replaced. */\nexport async function restoreLocalCache(snapshot: CacheSnapshot): Promise<number> {\n const db = await openCacheDb()\n try {\n const tx = db.transaction([...CACHE_STORES], 'readwrite')\n let count = 0\n for (const store of CACHE_STORES) {\n for (const row of snapshot.rows[store]) {\n tx.objectStore(store).put(row)\n count++\n }\n }\n await done(tx)\n return count\n } finally {\n db.close()\n }\n}\n\nasync function writeAtomically(path: string, bytes: Uint8Array): Promise<void> {\n const temp = `${path}.${process.pid}.tmp`\n await writeFile(temp, bytes, { mode: 0o600 })\n await rename(temp, path)\n}\n\nasync function exists(path: string): Promise<boolean> {\n try {\n await stat(path)\n return true\n } catch {\n return false\n }\n}\n\n/** Save the graph's cache rows to disk. */\nexport async function persistLocalCache(dir: string, graphId: string): Promise<void> {\n const snapshot = await captureLocalCache(graphId)\n await mkdir(dir, { recursive: true, mode: 0o700 })\n await writeAtomically(cacheFile(dir), serialize(snapshot))\n}\n\n/**\n * Load a saved cache into the in-memory database, if one exists for this build's cache version.\n * A file that will not parse or belongs to another graph is ignored: the cache is rebuildable.\n */\nexport async function loadLocalCache(dir: string, graphId: string): Promise<number> {\n const path = cacheFile(dir)\n if (!(await exists(path))) return 0\n try {\n const snapshot = deserialize(await readFile(path)) as CacheSnapshot\n if (snapshot.version !== CACHE_DB_VERSION || snapshot.graphId !== graphId) return 0\n return await restoreLocalCache(snapshot)\n } catch (error) {\n console.error(`etherpk-mcp: ignoring an unreadable cache file ${path}: ${error instanceof Error ? error.message : String(error)}`)\n return 0\n }\n}\n\n/**\n * An {@link IndexDbHost} over sqlite-wasm's memory with a file on disk behind it. `open` imports\n * the saved database when there is one and reports `persisted: true`, which is what puts the\n * index core on its warm path; `discard` deletes the file and hands back an empty database.\n * {@link NodeIndexHost.export} writes the live database's bytes for the next launch.\n */\nexport interface NodeIndexHost extends IndexDbHost {\n /** Write the open database to disk; a no-op before `open`. */\n export(): Promise<void>\n}\n\n// The oo1 API typed loosely at this one boundary, as `index-db-sqlite.ts` does.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype Sqlite3 = any\n\nexport function nodeIndexHost(dir: string): NodeIndexHost {\n let sqlite3: Sqlite3 | undefined\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let live: { oo1: any; db: SqlDb } | undefined\n const path = indexFile(dir)\n /** sqlite-wasm's own virtual filesystem path; one per process is enough. */\n const vfsName = `/etherpk-index-${process.pid}.sqlite`\n\n async function init(): Promise<Sqlite3> {\n if (!sqlite3) {\n const { default: sqlite3InitModule } = await import('@sqlite.org/sqlite-wasm')\n sqlite3 = await sqlite3InitModule()\n }\n return sqlite3\n }\n\n function openFresh(s: Sqlite3): { oo1: Sqlite3; db: SqlDb } {\n const oo1 = new s.oo1.DB(':memory:')\n const db = wrapOo1Db(oo1)\n createSchema(db)\n return { oo1, db }\n }\n\n async function openSaved(s: Sqlite3): Promise<{ oo1: Sqlite3; db: SqlDb } | null> {\n if (!(await exists(path))) return null\n try {\n const bytes = await readFile(path)\n s.capi.sqlite3_js_posix_create_file(vfsName, new Uint8Array(bytes))\n const oo1 = new s.oo1.DB(vfsName)\n const db = wrapOo1Db(oo1)\n if (!isUsableIndex(db)) {\n // The core would discard it anyway; do it here so the file goes too.\n db.close()\n await rm(path, { force: true })\n return null\n }\n return { oo1, db }\n } catch (error) {\n console.error(`etherpk-mcp: ignoring an unreadable index file ${path}: ${error instanceof Error ? error.message : String(error)}`)\n await rm(path, { force: true })\n return null\n }\n }\n\n return {\n async open() {\n const s = await init()\n const saved = await openSaved(s)\n live = saved ?? openFresh(s)\n return { db: live.db, persisted: true }\n },\n async discard() {\n const s = await init()\n await rm(path, { force: true })\n live = openFresh(s)\n return { db: live.db, persisted: true }\n },\n async export() {\n if (!live || !sqlite3) return\n const bytes = sqlite3.capi.sqlite3_js_db_export(live.oo1.pointer) as Uint8Array\n await mkdir(dir, { recursive: true, mode: 0o700 })\n await writeAtomically(path, bytes)\n },\n }\n}\n\n/** Remove every persisted graph under the cache root (logout). */\nexport async function removeCacheRoot(env: NodeJS.ProcessEnv): Promise<void> {\n await rm(cacheRoot(env), { recursive: true, force: true })\n}\n","/**\n * One synced graph, open in a process with no editor: the client's own sync engine, server\n * document store and derived index, wired exactly as `GraphWorkspace.svelte` wires them for a\n * tab, minus the tab (ADR 0072). The [[Headless Client]] holds the Graph Key and does the\n * decrypting; the [[Sync Server]] stays as blind to this device as to any other.\n *\n * Two browser seams are swapped here and nowhere else:\n *\n * - The [[Local Cache]] is IndexedDB in the browser. Node has none, so `fake-indexeddb` provides\n * the same API in memory and `openGraphCache` runs unchanged - the store's tests already run it\n * this way. With a `persistDir`, its rows are restored from the last run before the engine\n * opens it and snapshotted after writes (`persistence.ts`); without one it is rebuilt from the\n * relay on every open, which is the test tier's mode.\n * - The [[Derived Index]] runs in-process over sqlite-wasm in memory rather than in a shared\n * worker over OPFS; with a `persistDir` the database bytes are imported on open and exported\n * with the cache, so the index core takes its warm path.\n */\n\nimport 'fake-indexeddb/auto'\n\nimport type { GraphKeyring } from '$lib/crypto'\nimport { createRemoteGraphIndex, type RemoteGraphIndex } from '$lib/document/index-worker/client'\nimport { inlineTransport, memoryDbHost } from '$lib/document/index-worker/transport'\nimport { createServerDocumentStore, type ServerDocumentStore } from '$lib/storage/server/server-document-store'\nimport { createGraphSync, type GraphSync, type TransportSocket } from '$lib/sync/graph-sync'\nimport { openGraphCache } from '$lib/sync/local-cache'\nimport { PRESENCE_PALETTE } from '$lib/sync/presence-identity'\nimport type { SyncTokenSource } from '$lib/sync/sync-token'\nimport { documentProtection } from '$lib/document/protection/cipher-fence'\n\nimport { nodeTransport } from './node-transport'\nimport { loadLocalCache, nodeIndexHost, persistLocalCache, type NodeIndexHost } from './persistence'\n\nexport interface HeadlessGraphDeps {\n graphId: string\n rootDocId: string\n keyring: GraphKeyring\n relayUrl: string\n token: SyncTokenSource\n /** How this device announces itself in collaborators' editors: *Agent on <host>*. */\n presenceName: string\n /** Swap point for tests (the loopback relay); production uses the Node WebSocket. */\n connect?: (url: string) => TransportSocket\n /** Bounded wait for the first relay connection; a serve that cannot reach the relay fails loudly. */\n readyTimeoutMs?: number\n /**\n * Where the Local Cache and the index are kept between launches (`persistence.ts`). Absent,\n * both live in memory and are rebuilt from the relay on every open - the test tier's mode.\n */\n persistDir?: string\n /** How long after a write before the snapshot is taken; the shutdown path never waits. */\n persistDebounceMs?: number\n onError?: (error: Error) => void\n}\n\nexport interface HeadlessGraph {\n readonly graphId: string\n readonly sync: GraphSync\n readonly store: ServerDocumentStore\n readonly index: RemoteGraphIndex\n /** Flush every pending local edit and wait for the relay's acknowledgement. */\n settle(): Promise<{ settled: boolean; outstanding: number }>\n /** Write the cache and the index to `persistDir` now; a no-op without one. */\n persist(): Promise<void>\n dispose(): Promise<void>\n}\n\n/** Open the graph, scan its registry and build the index; resolves once tools can answer. */\nexport async function openHeadlessGraph(deps: HeadlessGraphDeps): Promise<HeadlessGraph> {\n const cache = await openGraphCache(deps.graphId)\n // The saved rows go into the (fresh, in-memory) database the line above just created, before\n // the engine seeds from it: what it then reads is what the last run left.\n if (deps.persistDir) await loadLocalCache(deps.persistDir, deps.graphId)\n const indexHost: NodeIndexHost | undefined = deps.persistDir ? nodeIndexHost(deps.persistDir) : undefined\n const sync = createGraphSync({\n graphId: deps.graphId,\n rootDocId: deps.rootDocId,\n keyring: deps.keyring,\n relayUrl: deps.relayUrl,\n token: deps.token,\n cache,\n connect: deps.connect ?? nodeTransport,\n // Same rule as the workspace: a document that reads as protected has its cache row\n // collected so the body it was protected to hide does not linger in the clear.\n collectRowWhen: (doc) => documentProtection(doc.getText('content').toString()).kind === 'document',\n // The agent's caret colour is whichever palette entry the session allocator leaves it;\n // the name is what members read.\n presence: { name: deps.presenceName, ...PRESENCE_PALETTE[0] },\n onError: deps.onError,\n })\n const store = createServerDocumentStore(sync, { readyTimeoutMs: deps.readyTimeoutMs })\n try {\n await store.scan()\n const index = createRemoteGraphIndex(store, inlineTransport(indexHost ?? memoryDbHost()), { graphId: deps.graphId })\n await index.prepare()\n await index.refresh()\n\n // One snapshot at a time, cache before index (see persistence.ts → Ordering), and never\n // on the tool's own critical path: a write schedules one, shutdown takes one.\n let persisting: Promise<void> = Promise.resolve()\n let timer: ReturnType<typeof setTimeout> | undefined\n const persist = () => {\n if (!deps.persistDir || !indexHost) return persisting\n const dir = deps.persistDir\n persisting = persisting\n .then(() => persistLocalCache(dir, deps.graphId))\n .then(() => indexHost.export())\n .catch((error: unknown) => deps.onError?.(error instanceof Error ? error : new Error(String(error))))\n return persisting\n }\n const schedulePersist = () => {\n if (!deps.persistDir) return\n clearTimeout(timer)\n timer = setTimeout(() => void persist(), deps.persistDebounceMs ?? 5_000)\n }\n // Remote edits arriving while serving count too, or a quiet agent would leave a stale file.\n const unsubscribe = sync.onDocUpdate(schedulePersist)\n\n return {\n graphId: deps.graphId,\n sync,\n store,\n index,\n async settle() {\n await sync.flushAll()\n const result = await sync.awaitAcked({ stallMs: 10_000 })\n schedulePersist()\n return result\n },\n persist,\n async dispose() {\n clearTimeout(timer)\n unsubscribe()\n await persist()\n index.dispose()\n await store.dispose()\n cache.dispose()\n },\n }\n } catch (error) {\n await store.dispose().catch(() => {})\n cache.dispose()\n throw error\n }\n}\n","/**\n * Device approval (ADR 0026 flows): unlock a NEW device from an already-unlocked one, so\n * the Recovery Code stays in the drawer for genuine recovery. The new device generates an\n * ephemeral X25519 pair and shows a SAS derived from its public key; an unlocked device\n * shows the SAS it derives from the key it RECEIVED, the user compares the two screens,\n * and on approve the vault key travels sealed to the ephemeral key. The server brokers\n * blind - a substituted key would change the SAS, and the sealed box it carries opens only\n * on the requesting device.\n *\n * Pure over an injected SyncApi so it unit-tests against a fetch stub.\n */\nimport {\n contextAad,\n deviceApprovalSas,\n fromBase64Url,\n generateIdentityKeyPair,\n openSealed,\n openVault,\n reencryptVault,\n sealToPublicKey,\n toBase64Url,\n} from '$lib/crypto'\nimport type { SyncApi } from './sync-api'\n\n/** AAD binds the sealed vault key to its approval - a blob cannot be replayed across requests. */\nconst approvalAad = (approvalId: string) => contextAad('device-approval', `approval:${approvalId}`)\n\nexport interface DeviceApprovalRequest {\n id: string\n /** The code THIS device displays; the approver must see the same one. */\n sas: string\n ephemeralPrivateKey: Uint8Array\n}\n\n/** New device: register a request and derive the SAS to display. */\nexport async function beginDeviceApproval(api: SyncApi): Promise<DeviceApprovalRequest> {\n const pair = generateIdentityKeyPair() // ephemeral X25519 - same curve as identities\n const { id } = await api.createDeviceApproval(toBase64Url(pair.publicKey))\n return { id, sas: await deviceApprovalSas(pair.publicKey), ephemeralPrivateKey: pair.privateKey }\n}\n\nexport type ApprovalPoll =\n | { state: 'waiting' }\n | { state: 'rejected' }\n | { state: 'expired' }\n | { state: 'unlocked'; deviceKey: Uint8Array }\n\n/**\n * New device: poll for the outcome. On 'unlocked' the sealed blob has been claimed (the\n * server clears it), opened with the ephemeral key, and VERIFIED against the account vault\n * - the returned deviceKey provably decrypts this account's keys.\n */\nexport async function pollDeviceApproval(api: SyncApi, request: DeviceApprovalRequest): Promise<ApprovalPoll> {\n const res = await api.pollDeviceApproval(request.id)\n if (res.status === 'pending') return { state: 'waiting' }\n if (res.status === 'rejected') return { state: 'rejected' }\n if (res.status === 'expired') return { state: 'expired' }\n if (res.status === 'sealed' && res.sealedVaultKey) {\n const deviceKey = await openSealed(request.ephemeralPrivateKey, fromBase64Url(res.sealedVaultKey), approvalAad(request.id))\n const vault = await api.getVault()\n if (!vault) throw new Error('No vault on this account')\n await openVault(fromBase64Url(vault.vault), deviceKey) // throws if the sealed key is wrong\n return { state: 'unlocked', deviceKey }\n }\n // 'claimed' with no blob means a previous poll consumed it and something went wrong after.\n throw new Error(`Approval is in an unexpected state (${res.status}) - start again`)\n}\n\nexport interface PendingDeviceApproval {\n id: string\n ephemeralPublicKey: string\n createdAt: string\n}\n\n/** Approver: the SAS to show for a pending request - derived from the RECEIVED key. */\nexport function approvalSas(approval: PendingDeviceApproval): Promise<string> {\n return deviceApprovalSas(fromBase64Url(approval.ephemeralPublicKey))\n}\n\n/**\n * Approver: seal this account's vault key to the request's ephemeral key. `heldKey` is\n * whatever this device caches (wrap key or vault key - openVault takes either). A legacy\n * vault is upgraded to v2 first, minting the vault key there is to seal. Returns the vault\n * key so the caller can re-cache it (self-healing, and required after an upgrade).\n */\nexport async function approveDevice(api: SyncApi, approval: PendingDeviceApproval, heldKey: Uint8Array): Promise<Uint8Array> {\n const stored = await api.getVault()\n if (!stored) throw new Error('No vault on this account')\n const envelope = fromBase64Url(stored.vault)\n const opened = await openVault(envelope, heldKey)\n let vaultKey = opened.vaultKey\n if (opened.legacy) {\n const upgraded = await reencryptVault(opened.vault, envelope, opened)\n await api.putVault(toBase64Url(upgraded.envelope), stored.version)\n vaultKey = upgraded.vaultKey\n }\n const sealed = await sealToPublicKey(fromBase64Url(approval.ephemeralPublicKey), vaultKey, approvalAad(approval.id))\n await api.sealDeviceApproval(approval.id, toBase64Url(sealed))\n return vaultKey\n}\n\n/** Approver rejects / requestor cancels. */\nexport async function rejectDeviceApproval(api: SyncApi, approvalId: string): Promise<void> {\n await api.cancelDeviceApproval(approvalId)\n}\n","/**\n * How the [[Headless Client]] gets the vault key the first time, as a device of the account\n * (ADR 0072). Two routes, both already served by the Sync Server for every new device:\n *\n * - **Device Approval** (ADR 0034), the default: this process registers an ephemeral key and\n * prints the SAS; the user confirms the same code in any unlocked EtherPK tab, which seals the\n * vault key to the ephemeral key; the poll claims it and verifies it opens the vault. No secret\n * passes through the terminal.\n * - **Recovery Code**, behind a flag, for a box with no tab to hand: the code derives the wrap\n * key, which opens the vault; what is cached is the vault key, never the code.\n *\n * Pure over an injected clock and output so the flow is testable without a terminal.\n */\n\nimport { deriveVaultWrapKey, fromBase64Url, normalizeRecoveryCode, openVault } from '$lib/crypto'\nimport { beginDeviceApproval, pollDeviceApproval } from '$lib/sync/device-approval'\nimport type { SyncApi } from '$lib/sync/sync-api'\n\nexport interface LoginIo {\n /** A line for the user: the SAS, progress, the outcome. Never a secret. */\n say(line: string): void\n sleep(ms: number): Promise<void>\n /**\n * Aborts the wait: the user chose another route or pressed Ctrl-C. The pending approval is\n * cancelled server-side and {@link ApprovalAbandoned} is thrown, so the caller can tell a\n * deliberate switch from a failure.\n */\n signal?: AbortSignal\n /** Where the EtherPK Client lives, to name in the instruction; null when the Server did not say. */\n clientUrl?: string | null\n}\n\n/** The user left the approval wait on purpose (the signal fired); nothing went wrong. */\nexport class ApprovalAbandoned extends Error {\n constructor() {\n super('Approval abandoned.')\n this.name = 'ApprovalAbandoned'\n }\n}\n\n/** Approval rows expire server-side after ten minutes; poll a little longer and then give up. */\nconst APPROVAL_TIMEOUT_MS = 11 * 60_000\nconst APPROVAL_POLL_MS = 2_000\n\nexport async function unlockByDeviceApproval(api: SyncApi, io: LoginIo): Promise<Uint8Array> {\n const request = await beginDeviceApproval(api)\n const where = io.clientUrl ? `open EtherPK at ${io.clientUrl}` : 'open EtherPK in a browser'\n io.say('')\n io.say(`To approve this device, ${where} (any page - it need not be a note) signed in to this account`)\n io.say('with its graphs unlocked. A prompt will show a code; confirm it matches this one:')\n io.say('')\n io.say(` ${request.sas}`)\n io.say('')\n io.say('Waiting (up to ten minutes)…')\n const abandon = async () => {\n await api.cancelDeviceApproval(request.id).catch(() => {})\n throw new ApprovalAbandoned()\n }\n const deadline = Date.now() + APPROVAL_TIMEOUT_MS\n while (Date.now() < deadline) {\n if (io.signal?.aborted) await abandon()\n const outcome = await pollDeviceApproval(api, request)\n if (outcome.state === 'unlocked') return outcome.deviceKey\n if (outcome.state === 'rejected') throw new Error('The approval was rejected in EtherPK.')\n if (outcome.state === 'expired') throw new Error('The approval expired before it was confirmed. Run login again.')\n await io.sleep(APPROVAL_POLL_MS)\n if (io.signal?.aborted) await abandon()\n }\n throw new Error('The approval was not confirmed in time. Run login again.')\n}\n\n/** Recovery Code → wrap key → open the vault; what comes back is the vault key to cache. */\nexport async function unlockByRecoveryCode(api: SyncApi, code: string): Promise<Uint8Array> {\n const wrapKey = await deriveVaultWrapKey(normalizeRecoveryCode(code))\n const stored = await api.getVault()\n if (!stored) throw new Error('This account has no encryption keys yet; there is nothing for a Recovery Code to open.')\n return (await openVault(fromBase64Url(stored.vault), wrapKey)).vaultKey\n}\n","/**\n * The eight tools an agent gets, as plain functions over an open {@link HeadlessGraph}. The MCP\n * layer (`mcp-server.ts`) only serialises these; everything an agent can and cannot do is\n * decided here, and tested here over the loopback relay without a server or a transport.\n *\n * The rules (ADR 0072, [[2026-09-15 Headless Client And MCP]]):\n *\n * - A [[Protected Document]] is listed by name and nothing else. Reading refuses; every write\n * refuses, frontmatter included. The Headless Client never holds a Protection Key, so this is\n * a courtesy to the agent rather than the guarantee - the guarantee is that there is nothing\n * here to serve.\n * - Edits are anchored: `edit_document` replaces one exact, unique occurrence and lands as one\n * `applyChange` range, which the sync engine carries as one `Y.Text` change that merges with\n * whoever is typing elsewhere in the page. There is no whole-page replace.\n * - Every write passes through the paste / import indent normaliser, so an agent that writes\n * four-space bullets lands on the grid like everyone else.\n * - Every result is bounded: pages are paged, a document read is capped and says so.\n */\n\nimport { todayISO } from '$lib/document/calendar/month-grid-core'\nimport { normaliseIndentUnit } from '$lib/document/indent-unit'\nimport { isJournalConcept } from '$lib/document/journal-concept'\nimport { documentProtection } from '$lib/document/protection/cipher-fence'\nimport { conceptKey } from '$lib/storage/fs/identity'\nimport { OPEN_TASK_STATUSES, TASK_PRIORITY_FILTERS, TASK_STATUSES, type TaskDueWindow, type TaskPriorityFilter, type TaskStatus } from '$lib/document/index-db'\nimport type { DocumentIdentity } from '$lib/storage/server/server-document-store'\n\nimport type { HeadlessGraph } from './headless-graph'\n\n/** Sizes an agent's context can absorb; the caller can page or narrow, never exceed them. */\nexport const LIST_PAGE_LIMIT = 200\nexport const SEARCH_PAGE_LIMIT = 50\nexport const TASK_PAGE_LIMIT = 200\n/** UTF-16 units of document text returned before `truncated` is set. */\nexport const READ_TEXT_CAP = 200_000\n\nexport type ToolErrorCode =\n | 'not_found'\n | 'protected_document'\n | 'already_exists'\n | 'ambiguous_match'\n | 'no_match'\n | 'invalid_argument'\n | 'not_settled'\n\n/** A refusal the agent can act on: a stable code first, a sentence second. */\nexport class ToolError extends Error {\n constructor(\n readonly code: ToolErrorCode,\n message: string,\n ) {\n super(message)\n this.name = 'ToolError'\n }\n}\n\nexport interface DocumentSummary {\n concept: string\n kind: 'page' | 'journal'\n aliases: string[]\n /** Present only when true, so an ordinary document's shape is unchanged. */\n protected?: true\n}\n\nexport interface ListDocumentsArgs {\n kind?: 'page' | 'journal'\n offset?: number\n limit?: number\n}\n\nexport interface ReadDocumentResult {\n concept: string\n kind: 'page' | 'journal'\n aliases: string[]\n text: string\n /** True when `text` was cut at {@link READ_TEXT_CAP}. */\n truncated: boolean\n}\n\nexport interface SearchArgs {\n query: string\n offset?: number\n limit?: number\n}\n\nexport interface TasksArgs {\n /** Tasks about this concept (its own line, an ancestor block, or the document), or every task. */\n concept?: string\n /** Default: every unfinished state. */\n statuses?: TaskStatus[]\n priorities?: Array<1 | 2 | 3 | 'none'>\n due?: TaskDueWindow\n offset?: number\n limit?: number\n}\n\nexport interface EditDocumentArgs {\n concept: string\n /** Exact text to replace; must occur exactly once. */\n old: string\n new: string\n}\n\nexport interface AppendDocumentArgs {\n /** A page's name, a journal day `YYYY-MM-DD`, or `today`. */\n concept: string\n text: string\n}\n\nexport interface CreatePageArgs {\n title: string\n text?: string\n}\n\n/** How `concept`, `today` and aliases resolve to one registry entry, case-insensitively. */\nfunction resolveIdentity(graph: HeadlessGraph, target: string): DocumentIdentity | null {\n const wanted = conceptKey(target === 'today' ? todayISO() : target.trim())\n for (const identity of graph.store.listIdentities()) {\n if (conceptKey(identity.concept) === wanted) return identity\n if (identity.aliases.some((alias) => conceptKey(alias) === wanted)) return identity\n }\n return null\n}\n\nfunction requireIdentity(graph: HeadlessGraph, target: string): DocumentIdentity {\n const identity = resolveIdentity(graph, target)\n if (!identity) throw new ToolError('not_found', `No document is named \"${target}\".`)\n return identity\n}\n\n/**\n * The document's current text from a live handle, after its relay catch-up has reached its\n * terminal page. A fresh process has an empty Local Cache, so a cache-seeded read answers \"\"\n * for every document until the relay has spoken; a tool must never believe that emptiness.\n */\nasync function liveText(graph: HeadlessGraph, identity: DocumentIdentity): Promise<string> {\n await graph.store.whenReady(identity.concept)\n const caughtUp = graph.sync.caughtUpDoc(identity.docId).then(() => 'ok' as const)\n const timeout = new Promise<'timeout'>((resolve) => setTimeout(() => resolve('timeout'), CATCH_UP_TIMEOUT_MS))\n await Promise.race([caughtUp, timeout])\n return graph.store.open(identity.concept).getText()\n}\n\n/** How long a read waits for the relay before answering with what it has. */\nconst CATCH_UP_TIMEOUT_MS = 15_000\n\nfunction refuseIfProtected(concept: string, text: string): void {\n if (documentProtection(text).kind === 'document') {\n throw new ToolError(\n 'protected_document',\n `\"${concept}\" is a protected document. Its content is encrypted under a key this client never holds, so it cannot be read or changed here; the user can unlock it in EtherPK.`,\n )\n }\n}\n\nfunction page<T>(items: readonly T[], offset: number, limit: number): { items: T[]; hasMore: boolean } {\n return { items: items.slice(offset, offset + limit), hasMore: items.length > offset + limit }\n}\n\nfunction bounded(value: number | undefined, fallback: number, max: number): number {\n if (value === undefined) return fallback\n if (!Number.isInteger(value) || value < 0) throw new ToolError('invalid_argument', 'offset and limit must be non-negative integers.')\n return Math.min(value, max)\n}\n\n/** After a write: push it and wait for the relay, so the agent's next read anywhere sees it. */\nasync function settle(graph: HeadlessGraph): Promise<void> {\n const result = await graph.settle()\n if (!result.settled) {\n throw new ToolError(\n 'not_settled',\n `The edit is saved locally but the Sync Server has not acknowledged it (${result.outstanding} outstanding). It will be delivered when the connection recovers.`,\n )\n }\n}\n\nexport function listDocuments(graph: HeadlessGraph, args: ListDocumentsArgs = {}) {\n const offset = bounded(args.offset, 0, Number.MAX_SAFE_INTEGER)\n const limit = bounded(args.limit, LIST_PAGE_LIMIT, LIST_PAGE_LIMIT)\n const protectedKeys = new Set(\n graph.index\n .allConcepts()\n .filter((candidate) => candidate.protected && (candidate.kind === 'page' || candidate.kind === 'journal'))\n .map((candidate) => candidate.key),\n )\n const all: DocumentSummary[] = graph.store\n .listDocuments()\n .filter((doc) => !args.kind || doc.kind === args.kind)\n .map((doc) => ({\n concept: doc.concept,\n kind: doc.kind,\n aliases: doc.aliases,\n ...(protectedKeys.has(doc.key) ? { protected: true as const } : {}),\n }))\n const paged = page(all, offset, limit)\n return { documents: paged.items, total: all.length, hasMore: paged.hasMore }\n}\n\nexport async function readDocument(graph: HeadlessGraph, concept: string): Promise<ReadDocumentResult> {\n const identity = requireIdentity(graph, concept)\n const text = await liveText(graph, identity)\n refuseIfProtected(identity.concept, text)\n return {\n concept: identity.concept,\n kind: identity.kind,\n aliases: identity.aliases,\n text: text.length > READ_TEXT_CAP ? text.slice(0, READ_TEXT_CAP) : text,\n truncated: text.length > READ_TEXT_CAP,\n }\n}\n\nexport async function search(graph: HeadlessGraph, args: SearchArgs) {\n const query = args.query.trim()\n if (query === '') throw new ToolError('invalid_argument', 'query must not be empty.')\n const offset = bounded(args.offset, 0, Number.MAX_SAFE_INTEGER)\n const limit = bounded(args.limit, 20, SEARCH_PAGE_LIMIT)\n const [result, count] = await Promise.all([\n graph.index.searchText(query, offset, limit),\n graph.index.searchTextCount(query),\n ])\n return {\n results: result.groups.map((group) => ({\n concept: group.concept,\n kind: group.kind,\n matches: group.matches,\n hits: group.hits.map((hit) => ({\n line: hit.line,\n breadcrumb: hit.breadcrumb,\n snippet: hit.snippet.map((segment) => segment.text).join(''),\n })),\n })),\n total: count.total,\n totalCapped: count.capped,\n hasMore: result.hasMore,\n }\n}\n\nexport async function backlinks(graph: HeadlessGraph, concept: string) {\n const target = concept.trim() === 'today' ? todayISO() : concept.trim()\n if (target === '') throw new ToolError('invalid_argument', 'concept must not be empty.')\n const groups = await graph.index.backlinks(target)\n return {\n concept: target,\n sources: groups.map((group) => ({\n concept: group.sourceConcept,\n kind: group.sourceKind,\n references: group.refs.map((ref) => ({\n line: ref.line,\n breadcrumb: ref.breadcrumb,\n text:\n ref.kind === 'block'\n ? ref.subtree.map((node) => node.label).join('\\n')\n : (ref.context?.text ?? ''),\n })),\n })),\n }\n}\n\nexport async function tasks(graph: HeadlessGraph, args: TasksArgs = {}) {\n const offset = bounded(args.offset, 0, Number.MAX_SAFE_INTEGER)\n const limit = bounded(args.limit, 100, TASK_PAGE_LIMIT)\n const statuses = args.statuses ?? OPEN_TASK_STATUSES\n for (const status of statuses) {\n if (!TASK_STATUSES.includes(status)) throw new ToolError('invalid_argument', `Unknown task status \"${status}\".`)\n }\n const priorities: TaskPriorityFilter[] = args.priorities\n ? args.priorities.map((priority) => (priority === 'none' ? null : priority))\n : [...TASK_PRIORITY_FILTERS]\n const result = await graph.index.tasks(\n {\n concept: args.concept?.trim() ? (args.concept.trim() === 'today' ? todayISO() : args.concept.trim()) : null,\n statuses,\n priorities,\n due: args.due ?? 'any',\n groupBy: 'document',\n today: todayISO(),\n },\n offset,\n limit,\n )\n return {\n tasks: result.hits.map((hit) => ({\n concept: hit.concept,\n kind: hit.kind,\n line: hit.line,\n text: hit.text,\n done: hit.done,\n priority: hit.priority,\n waiting: hit.waiting,\n doing: hit.doing,\n cancelled: hit.cancelled,\n due: hit.due,\n scheduled: hit.scheduled,\n completion: hit.completion,\n breadcrumb: hit.breadcrumb,\n })),\n total: result.total,\n hasMore: result.hasMore,\n }\n}\n\nexport async function editDocument(graph: HeadlessGraph, args: EditDocumentArgs) {\n if (args.old === '') throw new ToolError('invalid_argument', 'old must not be empty.')\n const identity = requireIdentity(graph, args.concept)\n const text = await liveText(graph, identity)\n refuseIfProtected(identity.concept, text)\n const first = text.indexOf(args.old)\n if (first === -1) {\n throw new ToolError('no_match', `\"${identity.concept}\" does not contain the text to replace. Read the document again; it may have changed.`)\n }\n if (text.indexOf(args.old, first + 1) !== -1) {\n throw new ToolError('ambiguous_match', `The text to replace occurs more than once in \"${identity.concept}\". Include more surrounding text so it matches exactly once.`)\n }\n // Only the replacement is normalised: the surrounding document is someone else's to keep,\n // and a normaliser pass over the whole text would be a whole-page write in disguise.\n const insert = normaliseIndentUnit(args.new)\n graph.store.open(identity.concept).applyChange({ from: first, to: first + args.old.length, insert }, 'external')\n await settle(graph)\n return { concept: identity.concept, replaced: args.old.length, inserted: insert.length }\n}\n\nexport async function appendDocument(graph: HeadlessGraph, args: AppendDocumentArgs) {\n const text = normaliseIndentUnit(args.text)\n if (text.trim() === '') throw new ToolError('invalid_argument', 'text must not be empty.')\n const target = args.concept.trim() === 'today' ? todayISO() : args.concept.trim()\n const existing = resolveIdentity(graph, target)\n if (!existing) {\n if (!isJournalConcept(target)) {\n throw new ToolError('not_found', `No document is named \"${target}\". Use create_page to start a new page.`)\n }\n // A journal entry is created by its first content, exactly as the first keystroke does.\n await graph.store.createJournal(target, text.endsWith('\\n') ? text : `${text}\\n`)\n await settle(graph)\n return { concept: target, created: true }\n }\n const current = await liveText(graph, existing)\n refuseIfProtected(existing.concept, current)\n const separator = current === '' || current.endsWith('\\n') ? '' : '\\n'\n const insert = `${separator}${text}${text.endsWith('\\n') ? '' : '\\n'}`\n graph.store.open(existing.concept).applyChange({ from: current.length, to: current.length, insert }, 'external')\n await settle(graph)\n return { concept: existing.concept, created: false }\n}\n\nexport async function createPage(graph: HeadlessGraph, args: CreatePageArgs) {\n const title = args.title.trim()\n if (title === '') throw new ToolError('invalid_argument', 'title must not be empty.')\n if (isJournalConcept(title)) {\n throw new ToolError('invalid_argument', `\"${title}\" is a calendar day, so it names a journal entry; use append_document to write to it.`)\n }\n if (resolveIdentity(graph, title)) {\n throw new ToolError('already_exists', `A document already answers to \"${title}\". Use edit_document or append_document instead.`)\n }\n // No frontmatter block: on a Server Backend identity is the encrypted registry (ADR 0024),\n // and the app seeds a new page with its body alone - a block the text carries is a mirror the\n // store writes back when the registry changes (ADR 0061), never the source of the name.\n await graph.store.createPage(title, normaliseIndentUnit(args.text ?? ''))\n await settle(graph)\n return { concept: title, created: true }\n}\n","/**\n * The MCP face of the tools: names, descriptions and argument schemas an agent reads, mapped\n * onto `tools.ts`. Nothing is decided here. A {@link ToolError} becomes an `isError` result\n * carrying its code and sentence, so the agent can act on the refusal instead of retrying it.\n */\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { z } from 'zod'\n\nimport type { HeadlessGraph } from './headless-graph'\nimport {\n LIST_PAGE_LIMIT,\n SEARCH_PAGE_LIMIT,\n TASK_PAGE_LIMIT,\n ToolError,\n appendDocument,\n backlinks,\n createPage,\n editDocument,\n listDocuments,\n readDocument,\n search,\n tasks,\n} from './tools'\n\nexport interface McpServerInfo {\n graphName: string\n version: string\n}\n\ntype ToolResult = { content: Array<{ type: 'text'; text: string }>; isError?: boolean }\n\nfunction ok(value: unknown): ToolResult {\n return { content: [{ type: 'text', text: JSON.stringify(value, null, 2) }] }\n}\n\nfunction failed(error: unknown): ToolResult {\n const body =\n error instanceof ToolError\n ? { error: error.code, message: error.message }\n : { error: 'internal', message: error instanceof Error ? error.message : String(error) }\n return { content: [{ type: 'text', text: JSON.stringify(body) }], isError: true }\n}\n\nasync function run(work: () => Promise<unknown> | unknown): Promise<ToolResult> {\n try {\n return ok(await work())\n } catch (error) {\n return failed(error)\n }\n}\n\nconst concept = z.string().min(1).describe('A page title, one of its aliases, a journal day as YYYY-MM-DD, or \"today\".')\nconst offset = z.number().int().nonnegative().optional().describe('Skip this many results (paging).')\n\nexport function createMcpServer(graph: HeadlessGraph, info: McpServerInfo): McpServer {\n const server = new McpServer(\n { name: 'etherpk', version: info.version },\n {\n instructions: [\n `You are connected to the EtherPK knowledge graph \"${info.graphName}\": daily journal entries and titled pages in plain markdown, linked with [[wikilinks]].`,\n 'Concept names are case-insensitive. A protected document is listed with protected: true and cannot be read or written here; tell the user to unlock it in EtherPK if a task needs it.',\n 'Bullets are \"- \" with two-space nesting; tasks are \"- [ ]\" / \"- [x]\" with task tags (#P1 #P2 #P3, #W #D #C, #D-YYYY-MM-DD due, #S-YYYY-MM-DD scheduled) right after the checkbox.',\n 'Edit with edit_document (an exact, unique old→new replacement) or append_document; read first, then edit, and never paste a whole page back.',\n 'The user documentation is at https://docs.etherpk.com.',\n ].join('\\n'),\n },\n )\n\n server.registerTool(\n 'list_documents',\n {\n title: 'List documents',\n description: `Every page and journal entry in the graph with its aliases; protected documents carry protected: true and have no readable body. Paged, at most ${LIST_PAGE_LIMIT} per call.`,\n inputSchema: {\n kind: z.enum(['page', 'journal']).optional().describe('Only pages, or only journal entries.'),\n offset,\n limit: z.number().int().positive().max(LIST_PAGE_LIMIT).optional(),\n },\n },\n async (args) => run(() => listDocuments(graph, args)),\n )\n\n server.registerTool(\n 'read_document',\n {\n title: 'Read a document',\n description: 'The full markdown of one document. Refuses a protected document with error \"protected_document\". Long documents are cut and marked truncated: true.',\n inputSchema: { concept },\n },\n async (args) => run(() => readDocument(graph, args.concept)),\n )\n\n server.registerTool(\n 'search',\n {\n title: 'Search text',\n description: `Full-text search over every unprotected document: words match by prefix, \"quoted phrases\" match exactly. Returns documents most-matches-first with the matching lines. At most ${SEARCH_PAGE_LIMIT} documents per call.`,\n inputSchema: {\n query: z.string().min(1),\n offset,\n limit: z.number().int().positive().max(SEARCH_PAGE_LIMIT).optional(),\n },\n },\n async (args) => run(() => search(graph, args)),\n )\n\n server.registerTool(\n 'backlinks',\n {\n title: 'Backlinks',\n description: 'Every document that links to a concept with [[Concept]], with the linking block or paragraph. Aliases are pooled: linking an alias counts.',\n inputSchema: { concept },\n },\n async (args) => run(() => backlinks(graph, args.concept)),\n )\n\n server.registerTool(\n 'tasks',\n {\n title: 'Tasks',\n description: `Tasks (\"- [ ]\" bullets) across the graph with their tags. Defaults to every unfinished task. A concept filter matches tasks written on that page, linking it, or nested under a block or heading that links it. At most ${TASK_PAGE_LIMIT} per call.`,\n inputSchema: {\n concept: concept.optional(),\n statuses: z.array(z.enum(['open', 'doing', 'waiting', 'done', 'cancelled'])).optional(),\n priorities: z.array(z.union([z.literal(1), z.literal(2), z.literal(3), z.literal('none')])).optional(),\n due: z.enum(['any', 'overdue', 'today', 'next7']).optional(),\n offset,\n limit: z.number().int().positive().max(TASK_PAGE_LIMIT).optional(),\n },\n },\n async (args) => run(() => tasks(graph, args)),\n )\n\n server.registerTool(\n 'edit_document',\n {\n title: 'Edit a document',\n description: 'Replace one exact occurrence of \"old\" with \"new\" in a document. \"old\" must appear exactly once (include surrounding lines to disambiguate); the change is applied as one edit so it merges with anyone typing elsewhere in the page. Refused on a protected document.',\n inputSchema: {\n concept,\n old: z.string().min(1).describe('The exact text to replace, as read_document returned it.'),\n new: z.string().describe('The replacement text. Empty deletes the old text.'),\n },\n },\n async (args) => run(() => editDocument(graph, args)),\n )\n\n server.registerTool(\n 'append_document',\n {\n title: 'Append to a document',\n description: 'Add text at the end of a page or a journal entry, on a new line. A journal day that has no entry yet is created (\"today\" is accepted). A page that does not exist is not created: use create_page.',\n inputSchema: {\n concept,\n text: z.string().min(1),\n },\n },\n async (args) => run(() => appendDocument(graph, args)),\n )\n\n server.registerTool(\n 'create_page',\n {\n title: 'Create a page',\n description: 'Create a new page with the given title and optional markdown body. Refuses a title that already names a page or an alias, or that is a calendar day (use append_document for journal entries).',\n inputSchema: {\n title: z.string().min(1),\n text: z.string().optional(),\n },\n },\n async (args) => run(() => createPage(graph, args)),\n )\n\n return server\n}\n","/**\n * `etherpk-mcp`: the [[Headless Client]]'s command line (ADR 0072).\n *\n * etherpk-mcp login --server <url> [--pat <token>] [--recovery-code]\n * etherpk-mcp graphs\n * etherpk-mcp serve --graph <id or name>\n * etherpk-mcp logout\n *\n * `serve` speaks MCP over stdio, so everything for the human goes to stderr; stdout belongs\n * to the agent. `login` and `graphs` are interactive and print to stdout.\n */\n\nimport { createInterface } from 'node:readline/promises'\nimport { hostname } from 'node:os'\nimport { parseArgs } from 'node:util'\nimport { unlink } from 'node:fs/promises'\n\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'\n\n// Inlined by Vite at build time, so the bundle carries the version and needs no file at runtime.\nimport pkg from '../package.json'\n\nimport { fromBase64Url, toBase64Url } from '$lib/crypto'\n\nimport { connectAccount, openAccountVault, resolveGraphById } from './account'\nimport { defaultConfigPath, readConfig, writeConfig, type HeadlessConfig } from './config'\nimport { readGraphName } from './graph-names'\nimport { openHeadlessGraph } from './headless-graph'\nimport { ApprovalAbandoned, unlockByDeviceApproval, unlockByRecoveryCode } from './login'\nimport { createMcpServer } from './mcp-server'\nimport { graphCacheDir, removeCacheRoot } from './persistence'\n\nconst VERSION: string = pkg.version\n\n/**\n * How the user invokes this program, so every hint is one they can paste. Through npx - the way\n * the Agents tab, the README and the docs all say - the bin is never on their PATH, and an npx\n * run executes out of npm's `_npx` cache; a global install (`npm install -g`) runs from\n * anywhere else and does have `etherpk-mcp` on the PATH, so it gets the short spelling.\n */\nconst CMD = /[\\\\/]_npx[\\\\/]/.test(process.argv[1] ?? '') ? 'npx @appsoftwareltd/etherpk-mcp' : 'etherpk-mcp'\n\nconst USAGE = `etherpk-mcp - EtherPK Headless Client (an MCP server over one synced graph)\n\n ${CMD} login --server <url> [--pat <token>] [--recovery-code]\n Sign this machine in as a device of your account. Prompts for a Personal Access\n Token (an account-wide one, from the Sync Server portal at <url>/account/tokens)\n unless --pat or ETHERPK_PAT is given, then unlocks your keys by Device Approval:\n open EtherPK in a browser signed in to the account with its graphs unlocked and\n confirm the code shown. Press r while waiting, or pass --recovery-code, to type\n your Recovery Code instead (or ETHERPK_RECOVERY_CODE, for a scripted setup).\n ${CMD} graphs\n List the synced graphs this account can reach, by name and id.\n ${CMD} serve --graph <id or name>\n Serve one graph to an agent over stdio. For Claude Code:\n claude mcp add etherpk -- npx @appsoftwareltd/etherpk-mcp serve --graph <id>\n ${CMD} logout\n Forget the token, keys and cached graphs on this machine.\n\nThe config file is ${defaultConfigPath()} (override with ETHERPK_MCP_CONFIG); cached graphs live\nunder ~/.cache/etherpk/mcp (override with ETHERPK_MCP_CACHE_DIR).\nDocs: https://docs.etherpk.com/using-ai-agents-with-your-notes\n`\n\nfunction fail(message: string): never {\n console.error(message)\n process.exit(1)\n}\n\nasync function ask(question: string, { secret = false } = {}): Promise<string> {\n if (!process.stdin.isTTY) fail(`${question} - no terminal to ask on; pass it as an option.`)\n if (!secret) {\n const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: true })\n try {\n return (await rl.question(question)).trim()\n } finally {\n rl.close()\n }\n }\n // A secret is read in raw mode so the terminal never echoes it: readline has no echo-off,\n // and a token or Recovery Code in scrollback is exactly what this command exists to avoid.\n process.stderr.write(question)\n return new Promise<string>((resolve, reject) => {\n const stdin = process.stdin\n let typed = ''\n const finish = (outcome: () => void) => {\n stdin.setRawMode(false)\n stdin.pause()\n stdin.off('data', onData)\n process.stderr.write('\\n')\n outcome()\n }\n const onData = (chunk: Buffer) => {\n for (const char of chunk.toString('utf8')) {\n if (char === '\\u0003') return finish(() => reject(new Error('Cancelled.')))\n if (char === '\\r' || char === '\\n') return finish(() => resolve(typed.trim()))\n if (char === '\\u007f' || char === '\\b') typed = typed.slice(0, -1)\n else typed += char\n }\n }\n stdin.setRawMode(true)\n stdin.resume()\n stdin.on('data', onData)\n })\n}\n\nasync function requireConfig(path: string): Promise<HeadlessConfig> {\n const config = await readConfig(path)\n if (!config) fail(`Not logged in on this machine. Run: ${CMD} login --server <url>`)\n return config\n}\n\nasync function login(args: { server?: string; pat?: string; 'recovery-code'?: boolean }): Promise<void> {\n const path = defaultConfigPath()\n const server = (args.server ?? (await readConfig(path))?.server ?? (await ask('Sync Server URL: '))).replace(/\\/$/, '')\n if (!/^https?:\\/\\//.test(server)) fail('The server must be an http(s) URL.')\n const pat = args.pat ?? process.env.ETHERPK_PAT ?? (await ask(`Personal Access Token (account-wide, from ${server}/account/tokens): `, { secret: true }))\n if (!pat) fail('A Personal Access Token is required.')\n\n const account = await connectAccount({ server, pat })\n console.log(`Signed in to ${server} as ${account.principal.email ?? account.principal.name ?? account.principal.id}.`)\n\n // ETHERPK_RECOVERY_CODE serves a box set up by a script, where there is no terminal to type\n // into and no tab to approve from; the variable is read once and never written anywhere.\n const byRecoveryCode = async () =>\n unlockByRecoveryCode(account.api, process.env.ETHERPK_RECOVERY_CODE ?? (await ask('Recovery Code: ', { secret: true })))\n const vaultKey = args['recovery-code'] ? await byRecoveryCode() : await approveOrFallBack(account, byRecoveryCode)\n await writeConfig(path, { server, pat, vaultKey: toBase64Url(vaultKey) })\n console.log(`Keys unlocked and cached in ${path} (owner-only). Anyone who can read your files on this machine can read this account, as with a signed-in browser.`)\n\n await listGraphs({ server, pat, vaultKey: toBase64Url(vaultKey) })\n}\n\n/**\n * Device Approval, with the Recovery Code one keypress away: a user who has no unlocked EtherPK\n * to hand should not have to Ctrl-C and re-read the help to find `--recovery-code`. On a\n * terminal, `r` during the wait abandons the approval (cancelled server-side) and asks for the\n * code instead; without a terminal the wait runs to its outcome.\n */\nasync function approveOrFallBack(\n account: Awaited<ReturnType<typeof connectAccount>>,\n byRecoveryCode: () => Promise<Uint8Array>,\n): Promise<Uint8Array> {\n const abort = new AbortController()\n const io = {\n say: (line: string) => console.log(line),\n sleep: (ms: number) => new Promise<void>((r) => setTimeout(r, ms)),\n signal: abort.signal,\n clientUrl: account.clientUrl,\n }\n const stdin = process.stdin\n const interactive = stdin.isTTY === true\n const onKey = (chunk: Buffer) => {\n const key = chunk.toString('utf8')\n if (key === 'r' || key === 'R') abort.abort()\n if (key === '\\u0003') {\n console.log('')\n process.exit(130)\n }\n }\n if (interactive) {\n console.log('(Press r to type your Recovery Code instead.)')\n stdin.setRawMode(true)\n stdin.resume()\n stdin.on('data', onKey)\n }\n // The key listener comes off BEFORE the Recovery Code prompt runs: that prompt takes the\n // terminal into raw mode itself, and a finally that reset it afterwards ate the code.\n let abandoned = false\n try {\n return await unlockByDeviceApproval(account.api, io)\n } catch (error) {\n if (!(error instanceof ApprovalAbandoned)) throw error\n abandoned = true\n } finally {\n if (interactive) {\n stdin.off('data', onKey)\n stdin.setRawMode(false)\n stdin.pause()\n }\n }\n if (!abandoned) throw new Error('unreachable')\n console.log('Approval cancelled; unlocking with your Recovery Code instead.')\n return byRecoveryCode()\n}\n\nasync function listGraphs(config: HeadlessConfig): Promise<void> {\n if (!config.vaultKey) fail(`Keys are not unlocked on this machine. Run: ${CMD} login`)\n const account = await connectAccount(config)\n const vault = await openAccountVault(account.api, fromBase64Url(config.vaultKey))\n const graphs = await account.api.listGraphs()\n if (graphs.length === 0) {\n console.log('No synced graphs are reachable with this token.')\n return\n }\n console.log('Synced graphs:')\n for (const record of graphs) {\n const keyring = vault.keyrings.find((entry) => entry.graphId === record.id)\n const name = keyring\n ? await readGraphName({ graphId: record.id, rootDocId: record.rootDocId, keyring, relayUrl: account.relayUrl, token: account.tokenFor(record.id) })\n : null\n const label = keyring ? (name ?? '(unnamed)') : '(no key on this account yet - open it in EtherPK first)'\n console.log(` ${record.id} ${label} [${record.role}]`)\n }\n console.log('')\n console.log(`Serve one to an agent with: ${CMD} serve --graph <id>`)\n console.log(`For Claude Code: claude mcp add etherpk -- ${CMD} serve --graph <id>`)\n}\n\nasync function serve(args: { graph?: string }): Promise<void> {\n const wanted = args.graph?.trim()\n if (!wanted) fail('serve needs --graph <id or name>.')\n const config = await requireConfig(defaultConfigPath())\n if (!config.vaultKey) fail(`Keys are not unlocked on this machine. Run: ${CMD} login`)\n const account = await connectAccount(config)\n const vault = await openAccountVault(account.api, fromBase64Url(config.vaultKey))\n const graphs = await account.api.listGraphs()\n\n // By id first; else by name, which means reading every reachable graph's name.\n let graphId = graphs.find((graph) => graph.id === wanted)?.id\n let graphName: string | null = null\n if (!graphId) {\n for (const record of graphs) {\n const keyring = vault.keyrings.find((entry) => entry.graphId === record.id)\n if (!keyring) continue\n const name = await readGraphName({ graphId: record.id, rootDocId: record.rootDocId, keyring, relayUrl: account.relayUrl, token: account.tokenFor(record.id) })\n if (name && name.toLowerCase() === wanted.toLowerCase()) {\n graphId = record.id\n graphName = name\n break\n }\n }\n }\n if (!graphId) fail(`No synced graph is named or identified by \"${wanted}\". Run: ${CMD} graphs`)\n const { record, keyring } = resolveGraphById(graphs, vault, graphId)\n\n console.error(`etherpk-mcp: opening graph ${graphId} on ${account.serverBaseUrl}…`)\n const graph = await openHeadlessGraph({\n graphId,\n rootDocId: record.rootDocId,\n keyring,\n relayUrl: account.relayUrl,\n token: account.tokenFor(graphId),\n presenceName: `Agent on ${hostname()}`,\n readyTimeoutMs: 20_000,\n // The cache and index survive between launches, so a restart catches up rather than\n // rebuilding (persistence.ts); logout removes them with the keys.\n persistDir: graphCacheDir(process.env, account.serverBaseUrl, graphId),\n onError: (error) => console.error(`etherpk-mcp: ${error.message}`),\n })\n graphName ??= graph.sync.getMeta().name ?? graphId\n const server = createMcpServer(graph, { graphName, version: VERSION })\n const transport = new StdioServerTransport()\n const shutdown = async () => {\n await graph.settle().catch(() => {})\n await graph.dispose().catch(() => {})\n process.exit(0)\n }\n process.on('SIGINT', () => void shutdown())\n process.on('SIGTERM', () => void shutdown())\n transport.onclose = () => void shutdown()\n await server.connect(transport)\n console.error(`etherpk-mcp: serving \"${graphName}\" over stdio as \"Agent on ${hostname()}\".`)\n}\n\nasync function logout(): Promise<void> {\n const path = defaultConfigPath()\n await unlink(path).catch((error: NodeJS.ErrnoException) => {\n if (error.code !== 'ENOENT') throw error\n })\n // The cached graphs are plaintext; they go with the keys that made them readable.\n await removeCacheRoot(process.env)\n console.log(`Forgot the token and keys in ${path} and the cached graphs. Revoke the Personal Access Token in the Sync Server portal too if this machine is not yours to keep.`)\n}\n\nasync function main(): Promise<void> {\n const { values, positionals } = parseArgs({\n args: process.argv.slice(2),\n allowPositionals: true,\n options: {\n server: { type: 'string' },\n pat: { type: 'string' },\n 'recovery-code': { type: 'boolean' },\n graph: { type: 'string' },\n help: { type: 'boolean', short: 'h' },\n version: { type: 'boolean', short: 'v' },\n },\n })\n if (values.version) {\n console.log(VERSION)\n return\n }\n const command = positionals[0]\n if (values.help || !command) {\n console.log(USAGE)\n return\n }\n switch (command) {\n case 'login':\n return login(values)\n case 'graphs':\n return listGraphs(await requireConfig(defaultConfigPath()))\n case 'serve':\n return serve(values)\n case 'logout':\n return logout()\n default:\n fail(`Unknown command \"${command}\".\\n\\n${USAGE}`)\n }\n}\n\nmain().catch((error: unknown) => {\n fail(`etherpk-mcp: ${error instanceof Error ? error.message : String(error)}`)\n})\n"],"mappings":"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"}
1
+ {"version":3,"file":"main.js","names":[],"sources":["../package.json","../../client/src/lib/crypto/bytes.ts","../../client/src/lib/crypto/recovery-code.ts","../../client/src/lib/crypto/device-sas.ts","../../client/src/lib/crypto/envelope.ts","../../client/src/lib/crypto/identity.ts","../../client/src/lib/crypto/keyring.ts","../../client/src/lib/crypto/protection.ts","../../client/src/lib/crypto/protection-key.ts","../../client/src/lib/crypto/sealed.ts","../../client/src/lib/crypto/vault.ts","../../client/src/lib/sync/sync-api.ts","../../client/src/lib/sync/sync-config.ts","../../client/src/lib/sync/sync-token.ts","../src/account.ts","../src/config.ts","../../client/src/lib/diagnostics/performance.ts","../../../packages/shared/src/managed-service.ts","../../../packages/shared/src/sync-protocol.ts","../../client/src/lib/sync/state-vector.ts","../../client/src/lib/sync/doc-sync.ts","../../client/src/lib/sync/messages.ts","../../client/src/lib/sync/presence-identity.ts","../../client/src/lib/sync/presence-session.ts","../../client/src/lib/sync/graph-sync.ts","../../client/src/lib/storage/idb-connection.ts","../../client/src/lib/sync/local-cache.ts","../src/node-transport.ts","../src/graph-names.ts","../../client/src/lib/document/wikilink/model.ts","../../client/src/lib/document/wikilink/parser.ts","../../client/src/lib/document/wikilink/code-ranges.ts","../../client/src/lib/document/wikilink/source.ts","../../client/src/lib/document/backlinks/backlink-index.ts","../../client/src/lib/document/index-db-sqlite.ts","../../client/src/lib/storage/fs/frontmatter-span.ts","../../client/src/lib/document/fenced-code.ts","../../client/src/lib/document/indent-unit.ts","../../client/src/lib/document/block-model.ts","../../client/src/lib/document/protection/fence-info.ts","../../client/src/lib/document/task-tags.ts","../../client/src/lib/document/index-derive.ts","../../client/src/lib/document/search-query.ts","../../client/src/lib/document/index-db.ts","../../client/src/lib/document/index-worker/core.ts","../../client/src/lib/document/index-worker/client.ts","../../client/src/lib/document/index-worker/transport.ts","../../client/src/lib/document/calendar/month-grid-core.ts","../../client/src/lib/document/journal-concept.ts","../../client/src/lib/storage/fs/frontmatter.ts","../../client/src/lib/storage/fs/identity.ts","../../client/src/lib/document/frontmatter/identity.ts","../../client/src/lib/document/types.ts","../../client/src/lib/activity/breathe.ts","../../client/src/lib/document/wikilink/rename.ts","../../client/src/lib/document/protection/cipher-fence.ts","../../client/src/lib/storage/merge.ts","../../client/src/lib/storage/rename.ts","../../client/src/lib/storage/rename-plan.ts","../../client/src/lib/storage/server/server-document-store.ts","../src/persistence.ts","../src/headless-graph.ts","../../client/src/lib/sync/device-approval.ts","../src/login.ts","../src/tools.ts","../src/mcp-server.ts","../src/main.ts"],"sourcesContent":["{\n \"name\": \"@appsoftwareltd/etherpk-mcp\",\n \"version\": \"0.3.0\",\n \"license\": \"Elastic-2.0\",\n \"description\": \"EtherPK Headless Client: an MCP server over a synced knowledge graph, run beside the agent on the user's own machine.\",\n \"type\": \"module\",\n \"private\": false,\n \"bin\": {\n \"etherpk-mcp\": \"./bin/etherpk-mcp.js\"\n },\n \"files\": [\n \"bin\",\n \"dist\"\n ],\n \"scripts\": {\n \"build\": \"vite build\",\n \"check\": \"tsc -p tsconfig.json --noEmit\",\n \"test\": \"vitest run\",\n \"test:watch\": \"vitest\",\n \"prepack\": \"pnpm build\"\n },\n \"dependencies\": {\n \"@lezer/markdown\": \"^1.6.4\",\n \"@modelcontextprotocol/sdk\": \"^1.30.0\",\n \"@noble/curves\": \"^2.2.0\",\n \"@noble/hashes\": \"^2.2.0\",\n \"@sqlite.org/sqlite-wasm\": \"3.53.0-build1\",\n \"fake-indexeddb\": \"^6.2.5\",\n \"lib0\": \"^0.2.117\",\n \"y-protocols\": \"^1.0.7\",\n \"yaml\": \"^2.9.0\",\n \"yjs\": \"^13.6.31\",\n \"zod\": \"^4.3.6\"\n },\n \"devDependencies\": {\n \"@appsoftwareltd/etherpk-shared\": \"workspace:*\",\n \"@codemirror/view\": \"^6.43.0\",\n \"@types/node\": \"^25.6.0\",\n \"typescript\": \"^6.0.2\",\n \"vite\": \"^8.0.8\",\n \"vitest\": \"^4.1.4\"\n },\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"homepage\": \"https://docs.etherpk.com/using-ai-agents-with-your-notes\",\n \"repository\": {\n \"type\": \"git\",\n \"url\": \"https://github.com/appsoftwareltd/etherpk\",\n \"directory\": \"apps/mcp\"\n },\n \"keywords\": [\n \"etherpk\",\n \"mcp\",\n \"model-context-protocol\",\n \"knowledge-graph\",\n \"notes\",\n \"agent\"\n ],\n \"engines\": {\n \"node\": \">=22\"\n }\n}","/**\n * Byte-array and encoding primitives for the crypto core. No cryptography here.\n * lib/crypto must stay self-contained (no imports from outside lib/crypto).\n */\nexport function utf8(text: string): Uint8Array {\n return new TextEncoder().encode(text)\n}\n\nexport function concatBytes(...parts: Uint8Array[]): Uint8Array {\n const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0))\n let offset = 0\n for (const p of parts) {\n out.set(p, offset)\n offset += p.length\n }\n return out\n}\n\n/** Constant-time-shaped comparison (length leak is fine; contents don't short-circuit). */\nexport function bytesEqual(a: Uint8Array, b: Uint8Array): boolean {\n if (a.length !== b.length) return false\n let diff = 0\n for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i]\n return diff === 0\n}\n\nexport function randomBytes(n: number): Uint8Array {\n const out = new Uint8Array(n)\n crypto.getRandomValues(out)\n return out\n}\n\nexport function toBase64Url(bytes: Uint8Array): string {\n let raw = ''\n for (const b of bytes) raw += String.fromCharCode(b)\n return btoa(raw).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '')\n}\n\nexport function fromBase64Url(text: string): Uint8Array {\n const raw = atob(text.replaceAll('-', '+').replaceAll('_', '/'))\n const out = new Uint8Array(raw.length)\n for (let i = 0; i < raw.length; i++) out[i] = raw.charCodeAt(i)\n return out\n}\n","/**\n * The Recovery Code (ADR 0026): 128 bits of generated entropy the user saves once.\n * Crockford base32 (no I, L, O, U) so it survives handwriting and re-typing;\n * normalization maps the confusables back. HKDF only — the code is high-entropy,\n * so no password-hardening KDF is needed anywhere in the system.\n */\nimport { randomBytes, utf8 } from './bytes'\n\nconst ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ'\nconst PREFIX = 'EPK1'\nconst BODY_LENGTH = 26 // 16 bytes → ceil(128/5) = 26 base32 chars\n\nexport class RecoveryCodeError extends Error {}\n\n/** e.g. EPK1-4R9TN-0WXA2-JH6MK-P3VZE-C58DQF */\nexport function generateRecoveryCode(): string {\n return formatKit(encodeBase32(randomBytes(16)))\n}\n\nfunction formatKit(body: string): string {\n return `${PREFIX}-${body.slice(0, 5)}-${body.slice(5, 10)}-${body.slice(10, 15)}-${body.slice(15, 20)}-${body.slice(20)}`\n}\n\n/** Crockford base32 (no I, L, O, U) — shared with the device-approval SAS, which is\n * human-compared across two screens and needs the same confusable-resistance. */\nexport function encodeCrockford32(bytes: Uint8Array): string {\n return encodeBase32(bytes)\n}\n\nfunction encodeBase32(bytes: Uint8Array): string {\n let bits = 0\n let value = 0\n let out = ''\n for (const b of bytes) {\n value = (value << 8) | b\n bits += 8\n while (bits >= 5) {\n out += ALPHABET[(value >>> (bits - 5)) & 31]\n bits -= 5\n }\n }\n if (bits > 0) out += ALPHABET[(value << (5 - bits)) & 31]\n return out\n}\n\nfunction decodeBase32(chars: string): Uint8Array {\n let bits = 0\n let value = 0\n const out: number[] = []\n for (const c of chars) {\n const index = ALPHABET.indexOf(c)\n if (index < 0) throw new RecoveryCodeError(`invalid recovery code character \"${c}\"`)\n value = (value << 5) | index\n bits += 5\n if (bits >= 8) {\n out.push((value >>> (bits - 8)) & 255)\n bits -= 8\n }\n }\n return new Uint8Array(out)\n}\n\n/** Uppercase, strip spaces/dashes, map O→0 and I/L→1, verify prefix + length; return canonical form. */\nexport function normalizeRecoveryCode(input: string): string {\n const cleaned = input\n .toUpperCase()\n .replace(/[\\s-]/g, '')\n .replaceAll('O', '0')\n .replaceAll('I', '1')\n .replaceAll('L', '1')\n if (!cleaned.startsWith(PREFIX)) throw new RecoveryCodeError('not a Recovery Code (missing EPK1 prefix)')\n const body = cleaned.slice(PREFIX.length)\n if (body.length !== BODY_LENGTH) {\n throw new RecoveryCodeError(`expected ${BODY_LENGTH} characters after the prefix, got ${body.length}`)\n }\n return formatKit(body)\n}\n\nexport function recoveryCodeToBytes(code: string): Uint8Array {\n const body = normalizeRecoveryCode(code).slice(PREFIX.length + 1).replaceAll('-', '')\n return decodeBase32(body)\n}\n\n/** The vault wrap key: HKDF-SHA-256 over the code's bytes. */\nexport async function deriveVaultWrapKey(code: string): Promise<Uint8Array> {\n const ikm = await crypto.subtle.importKey('raw', recoveryCodeToBytes(code) as BufferSource, 'HKDF', false, [\n 'deriveBits',\n ])\n const bits = await crypto.subtle.deriveBits(\n {\n name: 'HKDF',\n hash: 'SHA-256',\n salt: utf8('etherpk-vault-v1') as BufferSource,\n info: utf8('vault-wrap') as BufferSource,\n },\n ikm,\n 256,\n )\n return new Uint8Array(bits)\n}\n","/**\n * The device-approval SAS (Short Authentication String): a short code both devices derive\n * from the requesting device's ephemeral public key and display for the user to compare.\n * The server brokers that key, so a malicious server could substitute its own — but then\n * the two screens derive DIFFERENT codes and the user refuses. One shot: a mismatch means\n * starting over with a fresh key, so 40 displayed bits leave no room to search for a\n * matching substitute. Same trust move as the invite fingerprint (identity.ts).\n */\nimport { concatBytes, utf8 } from './bytes'\nimport { encodeCrockford32 } from './recovery-code'\n\nconst SAS_CONTEXT = utf8('etherpk/device-approval-sas/v1')\n\n/** e.g. \"7Q4M-KX2A\" — first 40 bits of SHA-256(context || ephemeral public key). */\nexport async function deviceApprovalSas(ephemeralPublicKey: Uint8Array): Promise<string> {\n const digest = new Uint8Array(\n await crypto.subtle.digest('SHA-256', concatBytes(SAS_CONTEXT, ephemeralPublicKey) as BufferSource),\n )\n const code = encodeCrockford32(digest.subarray(0, 5))\n return `${code.slice(0, 4)}-${code.slice(4, 8)}`\n}\n","/**\n * The one binary envelope for symmetric (epoch-keyed) content — updates, snapshots,\n * the vault, asset chunks. Self-delimiting layout, version 1:\n *\n * [0] version = 1\n * [1] kind = 1 (symmetric; sealed boxes are kind 2 — sealed.ts)\n * [2..5] epochId, u32 little-endian (0 where epochs don't apply, e.g. the vault)\n * [6..17] AES-GCM nonce (12 bytes, fresh random per seal)\n * [18..] ciphertext + GCM tag\n *\n * AAD binds the envelope to its context — (graph, doc, kind) — so a compromised server\n * cannot splice a valid blob into another document (design doc → Cryptography).\n */\nimport { concatBytes, randomBytes, utf8 } from './bytes'\n\nexport const ENVELOPE_VERSION = 1\nexport const KIND_SYMMETRIC = 1\nconst HEADER_LENGTH = 18\n\nexport class EnvelopeError extends Error {}\n\n/** Canonical AAD builder: contextAad('update', 'graph:<id>', 'doc:<id>'). */\nexport function contextAad(...parts: string[]): Uint8Array {\n return utf8(`etherpk:v1|${parts.join('|')}`)\n}\n\nasync function importAesKey(key: Uint8Array, usage: KeyUsage): Promise<CryptoKey> {\n return crypto.subtle.importKey('raw', key as BufferSource, 'AES-GCM', false, [usage])\n}\n\nexport async function sealSymmetric(opts: {\n key: Uint8Array\n epochId: number\n plaintext: Uint8Array\n aad: Uint8Array\n}): Promise<Uint8Array> {\n const nonce = randomBytes(12)\n const aesKey = await importAesKey(opts.key, 'encrypt')\n const ciphertext = new Uint8Array(\n await crypto.subtle.encrypt(\n // TS 6 + DOM lib: algorithm byte fields need BufferSource casts too, or check fails\n { name: 'AES-GCM', iv: nonce as BufferSource, additionalData: opts.aad as BufferSource },\n aesKey,\n opts.plaintext as BufferSource,\n ),\n )\n const header = new Uint8Array(HEADER_LENGTH)\n header[0] = ENVELOPE_VERSION\n header[1] = KIND_SYMMETRIC\n new DataView(header.buffer).setUint32(2, opts.epochId, true)\n header.set(nonce, 6)\n return concatBytes(header, ciphertext)\n}\n\n/** Read the epoch id without decrypting (the client picks the keyring entry from it). */\nexport function envelopeEpochId(envelope: Uint8Array): number {\n if (envelope.length < HEADER_LENGTH) throw new EnvelopeError('envelope too short')\n if (envelope[0] !== ENVELOPE_VERSION) throw new EnvelopeError(`unknown envelope version ${envelope[0]}`)\n if (envelope[1] !== KIND_SYMMETRIC) throw new EnvelopeError(`unexpected envelope kind ${envelope[1]}`)\n return new DataView(envelope.buffer, envelope.byteOffset).getUint32(2, true)\n}\n\nexport async function openSymmetric(opts: {\n keyForEpoch: (epochId: number) => Uint8Array | undefined\n envelope: Uint8Array\n aad: Uint8Array\n}): Promise<{ plaintext: Uint8Array; epochId: number }> {\n const epochId = envelopeEpochId(opts.envelope)\n const key = opts.keyForEpoch(epochId)\n if (!key) throw new EnvelopeError(`no key for epoch ${epochId}`)\n const aesKey = await importAesKey(key, 'decrypt')\n try {\n const plaintext = new Uint8Array(\n await crypto.subtle.decrypt(\n {\n name: 'AES-GCM',\n iv: opts.envelope.subarray(6, 18) as BufferSource,\n additionalData: opts.aad as BufferSource,\n },\n aesKey,\n opts.envelope.subarray(HEADER_LENGTH) as BufferSource,\n ),\n )\n return { plaintext, epochId }\n } catch {\n throw new EnvelopeError('envelope authentication failed')\n }\n}\n","/**\n * The account identity keypair (X25519) and its display fingerprint (ADR 0026).\n * @noble/curves is pure JS (no WASM) and clamps private scalars internally.\n */\nimport { x25519 } from '@noble/curves/ed25519.js' // extensioned subpath — required by @noble/curves v2's exports map\nimport { randomBytes } from './bytes'\n\nexport interface IdentityKeyPair {\n publicKey: Uint8Array\n privateKey: Uint8Array\n}\n\nexport function generateIdentityKeyPair(): IdentityKeyPair {\n const privateKey = randomBytes(32)\n return { privateKey, publicKey: x25519.getPublicKey(privateKey) }\n}\n\n/**\n * SHA-256 of the public key, first 128 bits as 8 groups of 4 hex chars —\n * the out-of-band verification string two users compare (ADR 0026, TOFU).\n */\nexport async function fingerprint(publicKey: Uint8Array): Promise<string> {\n const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', publicKey as BufferSource))\n const hex = [...digest.subarray(0, 16)].map((b) => b.toString(16).padStart(2, '0')).join('')\n return hex.toUpperCase().match(/.{4}/g)!.join(' ')\n}\n","/**\n * The Graph Key as it actually exists: an ordered list of epoch keys (ADR 0026).\n * The last entry is the current epoch; older epochs decrypt not-yet-compacted history.\n * Immutable: bumpEpoch returns a new keyring (removing a Player = bump + re-seal).\n */\nimport { fromBase64Url, randomBytes, toBase64Url, utf8 } from './bytes'\n\nexport interface EpochKey {\n epochId: number\n key: Uint8Array\n}\n\nexport interface GraphKeyring {\n graphId: string\n /** Ascending by epochId; last is current. */\n epochs: EpochKey[]\n}\n\nexport function createGraphKeyring(graphId: string): GraphKeyring {\n return { graphId, epochs: [{ epochId: 1, key: randomBytes(32) }] }\n}\n\nexport function bumpEpoch(keyring: GraphKeyring): GraphKeyring {\n const nextId = currentEpoch(keyring).epochId + 1\n return { ...keyring, epochs: [...keyring.epochs, { epochId: nextId, key: randomBytes(32) }] }\n}\n\nexport function currentEpoch(keyring: GraphKeyring): EpochKey {\n return keyring.epochs[keyring.epochs.length - 1]\n}\n\nexport function keyForEpoch(keyring: GraphKeyring, epochId: number): Uint8Array | undefined {\n return keyring.epochs.find((e) => e.epochId === epochId)?.key\n}\n\nexport interface KeyringJson {\n graphId: string\n epochs: Array<{ epochId: number; key: string }>\n}\n\nexport function keyringsToJson(keyrings: GraphKeyring[]): KeyringJson[] {\n return keyrings.map((k) => ({\n graphId: k.graphId,\n epochs: k.epochs.map((e) => ({ epochId: e.epochId, key: toBase64Url(e.key) })),\n }))\n}\n\nexport function keyringsFromJson(json: KeyringJson[]): GraphKeyring[] {\n return json.map((k) => ({\n graphId: k.graphId,\n epochs: k.epochs.map((e) => ({ epochId: e.epochId, key: fromBase64Url(e.key) })),\n }))\n}\n\n/** Plaintext bytes — only ever stored *inside* a vault or sealed box, never bare. */\nexport function serializeKeyrings(keyrings: GraphKeyring[]): Uint8Array {\n return utf8(JSON.stringify(keyringsToJson(keyrings)))\n}\n\nexport function deserializeKeyrings(bytes: Uint8Array): GraphKeyring[] {\n return keyringsFromJson(JSON.parse(new TextDecoder().decode(bytes)) as KeyringJson[])\n}\n","/**\n * The `etherpk-cipher` envelope: a [[Protected Document]]'s ciphertext,\n * as it sits inline in the fence (ADR 0028) — the same bytes on a Filesystem Backend, in an\n * Export, in a Local Mirror, in the Local Cache and on the Sync Server.\n *\n * ADR 0028 requires the envelope be **self-delimiting** and **carry a write timestamp**, because\n * two offline rewrites merge as two intact envelopes concatenated inside one fence and every\n * client must split them and resolve last-write-wins identically, without the key.\n *\n * Layout, version 1 (all multi-byte fields big-endian):\n *\n * [0] version = 1 (the envelope family — see envelope.ts)\n * [1] kind = 4 (1 symmetric, 2 sealed, 3 vault, 4 protected)\n * [2..9] writtenAt, u64 ms since the epoch\n * [10..17] key fingerprint, 8 bytes (non-secret — see {@link keyFingerprint})\n * [18..21] u32 byte length of the inner envelope\n * [22..] inner symmetric envelope, sealed under the Protection Key\n *\n * The header is **not** merely a prefix: `writtenAt` and the fingerprint are bound into the inner\n * envelope's AAD. A Player cannot read a protected fence but can still write to the `Y.Text`\n * holding it, so an unauthenticated timestamp would let them bump a stale envelope to win\n * last-write-wins. Editing either field now makes the envelope fail to open.\n *\n * The length prefix is u32, not u16: a Protected Document's whole body is one envelope and a page\n * of notes very easily exceeds 64 KiB.\n */\nimport { bytesEqual, concatBytes, fromBase64Url, toBase64Url, utf8 } from './bytes'\nimport { ENVELOPE_VERSION, contextAad, openSymmetric, sealSymmetric } from './envelope'\n\nexport const KIND_PROTECTED = 4\n/** version + kind + writtenAt + fingerprint + inner length. */\nexport const PROTECTION_HEADER_LENGTH = 22\nexport const FINGERPRINT_LENGTH = 8\n\nexport class ProtectionError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'ProtectionError'\n }\n}\n\n/**\n * The non-secret fingerprint of a Protection Key. Keys are personal (ADR 0057), so two members of\n * one shared graph hold different keys in it; without the fingerprint a client could not tell\n * *yours, locked* from *another member's, permanently unreadable*, and would offer an unlock\n * prompt no passphrase of yours would satisfy.\n *\n * Truncated to 8 bytes — 64 bits is far past collision range for the handful of keys a graph ever\n * sees, and it keeps the header small enough that a short Protected Document stays small.\n */\nexport async function keyFingerprint(key: Uint8Array): Promise<Uint8Array> {\n const digest = await crypto.subtle.digest(\n 'SHA-256',\n concatBytes(utf8('etherpk:protection-fingerprint:v1'), key) as BufferSource,\n )\n return new Uint8Array(digest).subarray(0, FINGERPRINT_LENGTH)\n}\n\n/** AAD binding the inner envelope to the header fields, so neither can be edited in place. */\nfunction protectionAad(writtenAt: number, fingerprint: Uint8Array): Uint8Array {\n return contextAad('protected', String(writtenAt), toBase64Url(fingerprint))\n}\n\nfunction headerView(envelope: Uint8Array): DataView {\n if (envelope.length < PROTECTION_HEADER_LENGTH) throw new ProtectionError('protected envelope too short')\n if (envelope[0] !== ENVELOPE_VERSION) throw new ProtectionError(`unknown envelope version ${envelope[0]}`)\n if (envelope[1] !== KIND_PROTECTED) throw new ProtectionError(`unexpected envelope kind ${envelope[1]}`)\n return new DataView(envelope.buffer, envelope.byteOffset, envelope.byteLength)\n}\n\n/** The write timestamp, readable without the key — this is what last-write-wins sorts on. */\nexport function protectedEnvelopeWrittenAt(envelope: Uint8Array): number {\n return Number(headerView(envelope).getBigUint64(2, false))\n}\n\n/** The Protection Key fingerprint, readable without the key. */\nexport function protectedEnvelopeFingerprint(envelope: Uint8Array): Uint8Array {\n headerView(envelope)\n return envelope.subarray(10, 10 + FINGERPRINT_LENGTH)\n}\n\n/** Total byte length of the envelope starting at offset 0, or throws if it is not whole. */\nfunction envelopeLength(envelope: Uint8Array): number {\n const innerLength = headerView(envelope).getUint32(18, false)\n const total = PROTECTION_HEADER_LENGTH + innerLength\n if (envelope.length < total) throw new ProtectionError('protected envelope truncated')\n return total\n}\n\nexport async function sealProtected(opts: {\n key: Uint8Array\n fingerprint: Uint8Array\n plaintext: string\n writtenAt: number\n}): Promise<Uint8Array> {\n const inner = await sealSymmetric({\n key: opts.key,\n epochId: 0,\n plaintext: utf8(opts.plaintext),\n aad: protectionAad(opts.writtenAt, opts.fingerprint),\n })\n const header = new Uint8Array(PROTECTION_HEADER_LENGTH)\n const view = new DataView(header.buffer)\n header[0] = ENVELOPE_VERSION\n header[1] = KIND_PROTECTED\n view.setBigUint64(2, BigInt(opts.writtenAt), false)\n header.set(opts.fingerprint.subarray(0, FINGERPRINT_LENGTH), 10)\n view.setUint32(18, inner.length, false)\n return concatBytes(header, inner)\n}\n\nexport async function openProtected(opts: {\n key: Uint8Array\n envelope: Uint8Array\n}): Promise<{ plaintext: string; writtenAt: number }> {\n const writtenAt = protectedEnvelopeWrittenAt(opts.envelope)\n const fingerprint = protectedEnvelopeFingerprint(opts.envelope)\n const inner = opts.envelope.subarray(PROTECTION_HEADER_LENGTH, envelopeLength(opts.envelope))\n try {\n const { plaintext } = await openSymmetric({\n keyForEpoch: () => opts.key,\n envelope: inner,\n aad: protectionAad(writtenAt, fingerprint),\n })\n return { plaintext: new TextDecoder().decode(plaintext), writtenAt }\n } catch {\n // One message for a wrong key and for a tampered header alike: the caller cannot act on\n // the difference, and distinguishing them would confirm a guessed key to an attacker.\n throw new ProtectionError('protected content could not be opened with this key')\n }\n}\n\n/**\n * Split a fence body that holds several envelopes end to end. Yjs never interleaves within one\n * insert, so an offline rewrite on each of two devices merges as two intact envelopes side by\n * side (ADR 0028) — this is what recovers them. Trailing bytes that are not a whole envelope are\n * dropped rather than throwing: a partially-typed or hand-mangled fence must still show the\n * envelopes it does contain.\n */\nexport function splitProtectedEnvelopes(bytes: Uint8Array): Uint8Array[] {\n const out: Uint8Array[] = []\n let offset = 0\n while (offset < bytes.length) {\n const rest = bytes.subarray(offset)\n let length: number\n try {\n length = envelopeLength(rest)\n } catch {\n break\n }\n out.push(rest.subarray(0, length))\n offset += length\n }\n return out\n}\n\n/**\n * ADR 0028's resolution: the newest envelope wins and the losers are discarded. Ties break on the\n * envelope bytes so every client — including one that cannot read any of them — converges on the\n * same winner without coordinating.\n *\n * The losers are genuinely dropped. ADR 0059 records this as an accepted risk: with the projected\n * editing model, two of your own unlocked devices can race, and the loser's work goes with it.\n */\nexport function resolveLastWriteWins(envelopes: readonly Uint8Array[]): Uint8Array | null {\n let winner: Uint8Array | null = null\n let winnerAt = -1\n for (const envelope of envelopes) {\n let writtenAt: number\n try {\n writtenAt = protectedEnvelopeWrittenAt(envelope)\n } catch {\n continue\n }\n if (writtenAt > winnerAt || (writtenAt === winnerAt && winner && compareBytes(envelope, winner) > 0)) {\n winner = envelope\n winnerAt = writtenAt\n }\n }\n return winner\n}\n\nfunction compareBytes(a: Uint8Array, b: Uint8Array): number {\n if (bytesEqual(a, b)) return 0\n const shared = Math.min(a.length, b.length)\n for (let i = 0; i < shared; i++) if (a[i] !== b[i]) return a[i] < b[i] ? -1 : 1\n return a.length < b.length ? -1 : 1\n}\n\n/**\n * Armour an envelope for the fence body. base64url has no backtick, so armoured content can never\n * be mistaken for a closing fence, and no newline, so one envelope is one line — which keeps a\n * Filesystem Backend graph's git diffs to a single changed line per write.\n */\nexport function armourProtected(envelope: Uint8Array): string {\n return toBase64Url(envelope)\n}\n\n/**\n * Read every envelope out of a fence body. Handles both shapes a merge can produce: one envelope\n * per line (the normal case), and several concatenated within one line. Blank and undecodable\n * lines are skipped — a hand-edited fence must still yield what it holds.\n */\nexport function unarmourProtected(body: string): Uint8Array[] {\n const out: Uint8Array[] = []\n for (const line of body.split('\\n')) {\n const trimmed = line.trim()\n if (!trimmed) continue\n try {\n out.push(...splitProtectedEnvelopes(fromBase64Url(trimmed)))\n } catch {\n continue\n }\n }\n return out\n}\n","/**\n * The Protection Key and its two wraps (ADR 0057).\n *\n * The key itself is random and is **never stored**. What a graph stores is a\n * {@link ProtectionRecord}: the non-secret fingerprint, the Argon2id cost parameters, and the key\n * wrapped under a key derived from the [[Protection Passphrase]]. That is the portable root — it\n * travels with the graph, so any device that knows the passphrase can reach the content.\n *\n * A second, optional wrap exists per device: {@link DeviceProtectionWrap}, keyed by a WebAuthn\n * passkey's PRF secret and held in device-local IndexedDB, **never synced and never exported**.\n * This is what keeps a synced passkey's provider outside the trust boundary — iCloud Keychain or\n * Google Password Manager replicate the PRF secret, but the blob it unwraps exists only on the\n * device that made it.\n *\n * Why Argon2id for one and plain HKDF for the other: a passphrase is human-chosen and attacked\n * offline (the wrapped blob travels in an Export), so it must be stretched by something\n * memory-hard. A PRF secret is 32 bytes of authenticator-derived entropy with nothing to guess,\n * which is the same reason `recovery-code.ts` correctly uses bare HKDF for a Recovery Code.\n */\nimport { argon2idAsync } from '@noble/hashes/argon2.js'\n\nimport { fromBase64Url, randomBytes, toBase64Url, utf8 } from './bytes'\nimport { contextAad, openSymmetric, sealSymmetric } from './envelope'\nimport { keyFingerprint } from './protection'\n\nexport const PROTECTION_RECORD_VERSION = 1\nconst PROTECTION_KEY_LENGTH = 32\nconst SALT_LENGTH = 16\nconst PASSPHRASE_AAD = contextAad('protection-key', 'passphrase')\nconst DEVICE_AAD = contextAad('protection-key', 'device')\n\n/**\n * OWASP's Argon2id minimum. RFC 9106's first-choice profile (2 GiB) and second-choice profile\n * (64 MiB, t=3) are both out of reach for a pure-JS implementation in a browser — 64 MiB / t=3\n * measures ~2.4s on a desktop and several times that on a low-end phone, which would make the\n * idle lock punishing enough that people stop protecting documents.\n *\n * Stored per record, so raising them later costs nothing: an old record keeps opening with its\n * own parameters, and the next passphrase change re-wraps under the new ones.\n */\nexport const DEFAULT_KDF_PARAMS = { m: 19456, t: 2, p: 1 } as const\n\nexport interface ProtectionKdfParams {\n /** Argon2id memory cost in KiB. */\n m: number\n /** Argon2id time cost (iterations). */\n t: number\n /** Argon2id parallelism. */\n p: number\n /** base64url, unique per record — two graphs sharing a passphrase must not share a key. */\n salt: string\n}\n\n/** What a graph stores so a passphrase can reach its Protection Key. Holds no secret. */\nexport interface ProtectionRecord {\n v: typeof PROTECTION_RECORD_VERSION\n /** base64url of the key fingerprint — matches the `fp` in every fence written under it. */\n fingerprint: string\n kdf: ProtectionKdfParams\n /** base64url of the passphrase-wrapped Protection Key. */\n wrapped: string\n}\n\n/** A device-local wrap under a passkey's PRF secret. Never synced, never exported. */\nexport interface DeviceProtectionWrap {\n v: typeof PROTECTION_RECORD_VERSION\n fingerprint: string\n /** The WebAuthn credential whose PRF output unwraps this. */\n credentialId: string\n wrapped: string\n}\n\nexport class ProtectionPassphraseError extends Error {\n constructor(message = 'that passphrase does not unlock this graph') {\n super(message)\n this.name = 'ProtectionPassphraseError'\n }\n}\n\n/** Bounds a record's Argon2id parameters must sit within to be used at all, in KiB and rounds. */\nexport const KDF_BOUNDS = { m: [8 * 1024, 1024 * 1024], t: [1, 32], p: [1, 16], saltBytes: [16, 64] } as const\n\n/**\n * A record's parameters come from a file - `etherpk/protection.json` in an imported folder, or\n * a vault entry - and an unbounded record could name a memory cost that pins the tab, or one so\n * small that the passphrase wrap is cheap to attack. Outside these bounds the record is treated\n * as unreadable, never as instructions.\n */\nexport function kdfParamsSane(kdf: { m: number; t: number; p: number; salt: string }): boolean {\n const within = (value: number, [low, high]: readonly [number, number]) =>\n Number.isInteger(value) && value >= low && value <= high\n if (!within(kdf.m, KDF_BOUNDS.m) || !within(kdf.t, KDF_BOUNDS.t) || !within(kdf.p, KDF_BOUNDS.p)) return false\n try {\n return within(fromBase64Url(kdf.salt).length, KDF_BOUNDS.saltBytes)\n } catch {\n return false\n }\n}\n\nexport function newKdfParams(): ProtectionKdfParams {\n return { ...DEFAULT_KDF_PARAMS, salt: toBase64Url(randomBytes(SALT_LENGTH)) }\n}\n\nexport function createProtectionKey(): Uint8Array {\n return randomBytes(PROTECTION_KEY_LENGTH)\n}\n\n/**\n * Stretch a passphrase into a wrapping key. `asyncTick` lets the worker yield between Argon2id\n * blocks so a slow unlock cannot freeze the thread it runs on.\n */\nasync function deriveWrapKey(passphrase: string, kdf: ProtectionKdfParams): Promise<Uint8Array> {\n return argon2idAsync(utf8(passphrase), fromBase64Url(kdf.salt), {\n t: kdf.t,\n m: kdf.m,\n p: kdf.p,\n dkLen: PROTECTION_KEY_LENGTH,\n asyncTick: 20,\n })\n}\n\nasync function wrap(key: Uint8Array, wrapKey: Uint8Array, aad: Uint8Array): Promise<string> {\n return toBase64Url(await sealSymmetric({ key: wrapKey, epochId: 0, plaintext: key, aad }))\n}\n\nasync function unwrap(wrapped: string, wrapKey: Uint8Array, aad: Uint8Array): Promise<Uint8Array> {\n try {\n const { plaintext } = await openSymmetric({\n keyForEpoch: () => wrapKey,\n envelope: fromBase64Url(wrapped),\n aad,\n })\n return plaintext\n } catch {\n throw new ProtectionPassphraseError()\n }\n}\n\n/**\n * Mint a protection record. Pass `existingKey` to re-root an existing key under a new passphrase —\n * that is how a device holding only a passkey wrap recovers from a forgotten passphrase without\n * re-encrypting a single document.\n */\nexport async function createProtectionRecord(\n passphrase: string,\n kdf: ProtectionKdfParams = newKdfParams(),\n existingKey?: Uint8Array,\n): Promise<{ record: ProtectionRecord; key: Uint8Array }> {\n const key = existingKey ?? createProtectionKey()\n const wrapKey = await deriveWrapKey(passphrase, kdf)\n return {\n key,\n record: {\n v: PROTECTION_RECORD_VERSION,\n fingerprint: toBase64Url(await keyFingerprint(key)),\n kdf,\n wrapped: await wrap(key, wrapKey, PASSPHRASE_AAD),\n },\n }\n}\n\nexport async function unlockProtectionRecord(record: ProtectionRecord, passphrase: string): Promise<Uint8Array> {\n return unwrap(record.wrapped, await deriveWrapKey(passphrase, record.kdf), PASSPHRASE_AAD)\n}\n\n/**\n * Re-wrap under a new passphrase. The Protection Key is unchanged, so no protected document is\n * touched and every fence already written stays readable — the same shape as regenerating a\n * Recovery Code. A fresh salt and fresh parameters are minted so the old blob gives no help\n * against the new one.\n */\nexport async function changeProtectionPassphrase(\n record: ProtectionRecord,\n current: string,\n next: string,\n kdf: ProtectionKdfParams = newKdfParams(),\n): Promise<ProtectionRecord> {\n const key = await unlockProtectionRecord(record, current)\n return (await createProtectionRecord(next, kdf, key)).record\n}\n\nexport async function deviceWrapFromPrf(\n key: Uint8Array,\n prfSecret: Uint8Array,\n credentialId: string,\n): Promise<DeviceProtectionWrap> {\n return {\n v: PROTECTION_RECORD_VERSION,\n fingerprint: toBase64Url(await keyFingerprint(key)),\n credentialId,\n wrapped: await wrap(key, await derivePrfWrapKey(prfSecret), DEVICE_AAD),\n }\n}\n\nexport async function unlockWithDeviceWrap(wrapRecord: DeviceProtectionWrap, prfSecret: Uint8Array): Promise<Uint8Array> {\n return unwrap(wrapRecord.wrapped, await derivePrfWrapKey(prfSecret), DEVICE_AAD)\n}\n\n/** HKDF over the PRF secret — no stretching needed, it is already high-entropy. */\nasync function derivePrfWrapKey(prfSecret: Uint8Array): Promise<Uint8Array> {\n const ikm = await crypto.subtle.importKey('raw', prfSecret as BufferSource, 'HKDF', false, ['deriveBits'])\n const bits = await crypto.subtle.deriveBits(\n {\n name: 'HKDF',\n hash: 'SHA-256',\n salt: utf8('etherpk-protection-device-v1') as BufferSource,\n info: utf8('protection-device-wrap') as BufferSource,\n },\n ikm,\n PROTECTION_KEY_LENGTH * 8,\n )\n return new Uint8Array(bits)\n}\n\nexport function serialiseProtectionRecord(record: ProtectionRecord): string {\n return JSON.stringify(record)\n}\n\n/**\n * Why stored text is not a usable record. Two kinds, because they want different advice: a\n * `damaged` record wants restoring from a backup, a `newer-version` one wants this build updated -\n * telling someone to restore a backup over a perfectly good record would be the very mistake the\n * distinction exists to prevent.\n */\nexport type ProtectionRecordProblem = 'damaged' | 'newer-version'\n\n/** Stored text, or a vault entry, that is not a record this build can use. Never \"no record\". */\nexport class ProtectionRecordFormatError extends Error {\n constructor(\n readonly problem: ProtectionRecordProblem,\n detail: string,\n ) {\n super(detail)\n this.name = 'ProtectionRecordFormatError'\n }\n}\n\n/**\n * Parse the JSON a store holds. Anything that is not a usable record throws - a mangled file, an\n * empty one, a record from a newer build, parameters outside {@link KDF_BOUNDS} - and is never\n * read as \"no record\" (ASTRA F13). This used to answer null for all of those, the same answer as\n * for an absent record, and the stores passed it on: enabling protection then wrote a fresh key\n * over a record it could not read, orphaning every fence sealed under the old one, which no\n * passphrase or Recovery Code brings back (ADR 0057). Whether any bytes existed at all is the\n * store's call, not the parser's.\n */\nexport function parseProtectionRecord(text: string): ProtectionRecord {\n let parsed: unknown\n try {\n parsed = JSON.parse(text)\n } catch (error) {\n throw new ProtectionRecordFormatError('damaged', `not JSON: ${(error as Error).message}`)\n }\n return validateProtectionRecord(parsed)\n}\n\n/**\n * The same check over a value that is already parsed - a vault entry, which arrives as an object\n * inside the decrypted vault rather than as text of its own. Only the fields a record has are\n * kept, so whatever else a stored copy carried does not travel on.\n */\nexport function validateProtectionRecord(value: unknown): ProtectionRecord {\n const damaged = (detail: string) => new ProtectionRecordFormatError('damaged', detail)\n if (typeof value !== 'object' || value === null || Array.isArray(value)) throw damaged('not a record object')\n const raw = value as Record<string, unknown>\n if (raw.v !== PROTECTION_RECORD_VERSION) {\n // A whole number above ours was written by a build after this one. Anything else was\n // never written by any build, so it is damage rather than the future.\n if (typeof raw.v === 'number' && Number.isInteger(raw.v) && raw.v > PROTECTION_RECORD_VERSION) {\n throw new ProtectionRecordFormatError(\n 'newer-version',\n `record version ${raw.v}; this build reads version ${PROTECTION_RECORD_VERSION}`,\n )\n }\n throw damaged(`record version ${String(raw.v)} is not ${PROTECTION_RECORD_VERSION}`)\n }\n if (typeof raw.fingerprint !== 'string' || typeof raw.wrapped !== 'string') throw damaged('fingerprint or wrapped key missing')\n const kdf = raw.kdf as Partial<ProtectionKdfParams> | null | undefined\n if (!kdf || typeof kdf !== 'object') throw damaged('KDF parameters missing')\n if (typeof kdf.m !== 'number' || typeof kdf.t !== 'number' || typeof kdf.p !== 'number' || typeof kdf.salt !== 'string') {\n throw damaged('KDF parameters missing')\n }\n const params: ProtectionKdfParams = { m: kdf.m, t: kdf.t, p: kdf.p, salt: kdf.salt }\n if (!kdfParamsSane(params)) throw damaged('KDF parameters outside the bounds this build will run')\n return { v: PROTECTION_RECORD_VERSION, fingerprint: raw.fingerprint, wrapped: raw.wrapped, kdf: params }\n}\n","/**\n * Sealed box: encrypt *to* an X25519 public key so only its holder can open it —\n * how a graph keyring travels to a Player and how the vault key reaches an approved\n * device (ADR 0026). ECIES construction:\n *\n * ephemeral X25519 pair → ECDH(ephemeral, recipient)\n * → HKDF-SHA-256(shared, salt = ephPub || recipientPub, info = 'etherpk/sealed/v1')\n * → AES-256-GCM\n *\n * Layout v1: [0] version=1 | [1] kind=2 | [2..33] ephemeral pub | [34..45] nonce | [46..] ct+tag\n */\nimport { x25519 } from '@noble/curves/ed25519.js' // extensioned subpath — required by @noble/curves v2's exports map\nimport { concatBytes, randomBytes, utf8 } from './bytes'\nimport { ENVELOPE_VERSION, EnvelopeError } from './envelope'\n\nexport const KIND_SEALED = 2\nconst HEADER_LENGTH = 46\n\nasync function deriveSealKey(\n shared: Uint8Array,\n ephemeralPublic: Uint8Array,\n recipientPublic: Uint8Array,\n): Promise<CryptoKey> {\n const ikm = await crypto.subtle.importKey('raw', shared as BufferSource, 'HKDF', false, ['deriveKey'])\n return crypto.subtle.deriveKey(\n {\n name: 'HKDF',\n hash: 'SHA-256',\n salt: concatBytes(ephemeralPublic, recipientPublic) as BufferSource,\n info: utf8('etherpk/sealed/v1') as BufferSource,\n },\n ikm,\n { name: 'AES-GCM', length: 256 },\n false,\n ['encrypt', 'decrypt'],\n )\n}\n\nexport async function sealToPublicKey(\n recipientPublicKey: Uint8Array,\n plaintext: Uint8Array,\n aad: Uint8Array,\n): Promise<Uint8Array> {\n const ephemeralPrivate = randomBytes(32)\n const ephemeralPublic = x25519.getPublicKey(ephemeralPrivate)\n const shared = x25519.getSharedSecret(ephemeralPrivate, recipientPublicKey)\n const key = await deriveSealKey(shared, ephemeralPublic, recipientPublicKey)\n const nonce = randomBytes(12)\n const ciphertext = new Uint8Array(\n await crypto.subtle.encrypt(\n { name: 'AES-GCM', iv: nonce as BufferSource, additionalData: aad as BufferSource },\n key,\n plaintext as BufferSource,\n ),\n )\n const header = new Uint8Array(HEADER_LENGTH)\n header[0] = ENVELOPE_VERSION\n header[1] = KIND_SEALED\n header.set(ephemeralPublic, 2)\n header.set(nonce, 34)\n return concatBytes(header, ciphertext)\n}\n\nexport async function openSealed(\n recipientPrivateKey: Uint8Array,\n envelope: Uint8Array,\n aad: Uint8Array,\n): Promise<Uint8Array> {\n if (envelope.length < HEADER_LENGTH + 16) throw new EnvelopeError('sealed envelope too short')\n if (envelope[0] !== ENVELOPE_VERSION) throw new EnvelopeError(`unknown envelope version ${envelope[0]}`)\n if (envelope[1] !== KIND_SEALED) throw new EnvelopeError(`unexpected envelope kind ${envelope[1]}`)\n const ephemeralPublic = envelope.subarray(2, 34)\n const shared = x25519.getSharedSecret(recipientPrivateKey, ephemeralPublic)\n const recipientPublic = x25519.getPublicKey(recipientPrivateKey)\n const key = await deriveSealKey(shared, ephemeralPublic, recipientPublic)\n try {\n return new Uint8Array(\n await crypto.subtle.decrypt(\n {\n name: 'AES-GCM',\n iv: envelope.subarray(34, 46) as BufferSource,\n additionalData: aad as BufferSource,\n },\n key,\n envelope.subarray(HEADER_LENGTH) as BufferSource,\n ),\n )\n } catch {\n throw new EnvelopeError('sealed envelope authentication failed')\n }\n}\n","/**\n * The key vault (ADR 0026): everything a device needs — the identity keypair and\n * every graph keyring — as one envelope. Stored server-side as an opaque blob;\n * losing local storage is harmless.\n *\n * Format v2 (device approval): the vault content is encrypted under a random VAULT KEY,\n * and the vault key travels alongside, wrapped under the Recovery-Code-derived wrap key:\n *\n * [0] version = 1 (envelope family)\n * [1] kind = 3 (vault container)\n * [2..3] u16 BE — byte length of the wrapped-vault-key envelope\n * [4..] wrapped vault key (symmetric envelope under the WRAP key, aad 'vault-key')\n * [rest] vault content (symmetric envelope under the VAULT key, aad 'vault')\n *\n * The indirection is what device approval seals to a new device (the vault key, never the\n * code), and it makes Regenerate Recovery Code a re-WRAP: the vault key — the thing every\n * unlocked device caches — survives, so other devices stay unlocked.\n *\n * Legacy (phase 1) blobs are the content envelope directly under the wrap key; they still\n * open, and any write upgrades them to v2 with a freshly minted vault key.\n */\nimport { fromBase64Url, randomBytes, toBase64Url, utf8 } from './bytes'\nimport { ENVELOPE_VERSION, EnvelopeError, KIND_SYMMETRIC, contextAad, openSymmetric, sealSymmetric } from './envelope'\nimport { type GraphKeyring, deserializeKeyrings, serializeKeyrings } from './keyring'\nimport type { ProtectionRecord } from './protection-key'\n\nexport interface KeyVault {\n identityPrivateKey: Uint8Array\n identityPublicKey: Uint8Array\n keyrings: GraphKeyring[]\n /**\n * Protection records by graph id (ADR 0057), for graphs on a Server Backend. Personal, never\n * shared with Players — which is why they live here and not in the graph's encrypted *shared*\n * metadata (ADR 0031), where every Player would hold the blob and could attack the passphrase\n * offline. A Filesystem Backend graph has no vault and keeps its record in `etherpk/` instead.\n *\n * Nesting is deliberate and load-bearing: the record holds only a passphrase-wrapped key, so\n * reaching the vault — which a cached wrap key in `localStorage` does — still does not reach\n * protected content.\n */\n protection?: Record<string, ProtectionRecord>\n}\n\nexport const KIND_VAULT_V2 = 3\nconst VAULT_AAD = contextAad('vault')\nconst VAULT_KEY_AAD = contextAad('vault-key')\nconst CONTAINER_HEADER = 4\n\ninterface VaultJson {\n identityPrivateKey: string\n identityPublicKey: string\n keyrings: string\n /** Absent in every vault written before protection existed — read as \"no protected graphs\". */\n protection?: Record<string, ProtectionRecord>\n}\n\nexport interface EncryptedVault {\n envelope: Uint8Array\n /** The key the device caches — it opens the vault content directly. */\n vaultKey: Uint8Array\n}\n\nexport interface OpenedVault {\n vault: KeyVault\n /** The key that decrypts the vault content — cache THIS on the device. */\n vaultKey: Uint8Array\n /** True for a phase-1 blob (no indirection); the opening key was the wrap key itself. */\n legacy: boolean\n}\n\nasync function sealContent(vault: KeyVault, vaultKey: Uint8Array): Promise<Uint8Array> {\n const json: VaultJson = {\n identityPrivateKey: toBase64Url(vault.identityPrivateKey),\n identityPublicKey: toBase64Url(vault.identityPublicKey),\n keyrings: new TextDecoder().decode(serializeKeyrings(vault.keyrings)),\n // Omitted entirely when empty, so a vault holding no protected graph is byte-identical to\n // one written before the feature existed.\n ...(vault.protection && Object.keys(vault.protection).length > 0 ? { protection: vault.protection } : {}),\n }\n return sealSymmetric({ key: vaultKey, epochId: 0, plaintext: utf8(JSON.stringify(json)), aad: VAULT_AAD })\n}\n\nasync function openContent(envelope: Uint8Array, vaultKey: Uint8Array): Promise<KeyVault> {\n const { plaintext } = await openSymmetric({ keyForEpoch: () => vaultKey, envelope, aad: VAULT_AAD })\n const json = JSON.parse(new TextDecoder().decode(plaintext)) as VaultJson\n return {\n identityPrivateKey: fromBase64Url(json.identityPrivateKey),\n identityPublicKey: fromBase64Url(json.identityPublicKey),\n keyrings: deserializeKeyrings(utf8(json.keyrings)),\n ...(json.protection ? { protection: json.protection } : {}),\n }\n}\n\nfunction buildContainer(wrapped: Uint8Array, content: Uint8Array): Uint8Array {\n const out = new Uint8Array(CONTAINER_HEADER + wrapped.length + content.length)\n out[0] = ENVELOPE_VERSION\n out[1] = KIND_VAULT_V2\n new DataView(out.buffer).setUint16(2, wrapped.length, false)\n out.set(wrapped, CONTAINER_HEADER)\n out.set(content, CONTAINER_HEADER + wrapped.length)\n return out\n}\n\nfunction splitContainer(envelope: Uint8Array): { wrapped: Uint8Array; content: Uint8Array } {\n if (envelope.length < CONTAINER_HEADER) throw new EnvelopeError('vault container too short')\n const wrappedLength = new DataView(envelope.buffer, envelope.byteOffset).getUint16(2, false)\n if (envelope.length < CONTAINER_HEADER + wrappedLength) throw new EnvelopeError('vault container truncated')\n return {\n wrapped: envelope.subarray(CONTAINER_HEADER, CONTAINER_HEADER + wrappedLength),\n content: envelope.subarray(CONTAINER_HEADER + wrappedLength),\n }\n}\n\n/**\n * Encrypt a vault under `wrapKey` (v2). Pass `vaultKey` to preserve an existing vault key —\n * a Recovery Code regenerate re-wraps without invalidating other devices' cached keys; omit\n * it to mint a fresh one (new account, or upgrading a legacy blob).\n */\nexport async function encryptVault(vault: KeyVault, wrapKey: Uint8Array, vaultKey?: Uint8Array): Promise<EncryptedVault> {\n const key = vaultKey ?? randomBytes(32)\n const wrapped = await sealSymmetric({ key: wrapKey, epochId: 0, plaintext: key, aad: VAULT_KEY_AAD })\n return { envelope: buildContainer(wrapped, await sealContent(vault, key)), vaultKey: key }\n}\n\n/**\n * Open a vault with whatever key the device holds: the Recovery-Code-derived wrap key\n * (legacy or v2), or the vault key itself (v2 — the device-approval / cached case).\n */\nexport async function openVault(envelope: Uint8Array, key: Uint8Array): Promise<OpenedVault> {\n if (envelope.length >= 2 && envelope[0] === ENVELOPE_VERSION && envelope[1] === KIND_VAULT_V2) {\n const { wrapped, content } = splitContainer(envelope)\n // Only the UNWRAP is speculative: it answers \"is the held key the wrap key or the\n // vault key?\". Opening the content is not, so it stays outside the try. Wrapping both\n // meant a corrupt vault - unwrap succeeded, content failed - fell through to the\n // fallback and surfaced as \"envelope authentication failed\" against the wrong key,\n // pointing the user at the wrong cause (quality audit S17).\n let vaultKey: Uint8Array\n try {\n // The held key as the WRAP key: unwrap the vault key first.\n vaultKey = (await openSymmetric({ keyForEpoch: () => key, envelope: wrapped, aad: VAULT_KEY_AAD })).plaintext\n } catch {\n // The held key as the VAULT key: open the content directly under it.\n return { vault: await openContent(content, key), vaultKey: key, legacy: false }\n }\n return { vault: await openContent(content, vaultKey), vaultKey, legacy: false }\n }\n if (envelope.length >= 2 && envelope[1] === KIND_SYMMETRIC) {\n // Phase-1 blob: content directly under the wrap key — no separate vault key exists.\n return { vault: await openContent(envelope, key), vaultKey: key, legacy: true }\n }\n throw new EnvelopeError(`unexpected vault envelope kind ${envelope[1] ?? 'none'}`)\n}\n\n/**\n * Re-encrypt UPDATED CONTENT against the envelope it was read from. A v2 envelope keeps its\n * wrapped-key segment verbatim (content writers usually hold only the vault key, never the\n * wrap key); a legacy envelope is upgraded to v2 — there the opening key WAS the wrap key,\n * so a fresh vault key is minted under it. Cache the returned vaultKey.\n */\nexport async function reencryptVault(\n vault: KeyVault,\n previousEnvelope: Uint8Array,\n opened: Pick<OpenedVault, 'vaultKey' | 'legacy'>,\n): Promise<EncryptedVault> {\n if (opened.legacy) return encryptVault(vault, opened.vaultKey)\n const { wrapped } = splitContainer(previousEnvelope)\n return { envelope: buildContainer(wrapped, await sealContent(vault, opened.vaultKey)), vaultKey: opened.vaultKey }\n}\n","import type { SyncAccountSummary } from '@appsoftwareltd/etherpk-shared'\n\n/**\n * The client's REST bridge to the sync server (plan Phase 4). Authenticates with a\n * Personal Access Token (bearer) — no cookies, so it works cross-origin. A PAT authenticates\n * but cannot decrypt (ADR 0026); every graph/vault/identity payload it fetches is ciphertext.\n *\n * Pure over an injected `fetch` + base URL so it unit-tests without a network.\n */\nexport interface SyncApiDeps {\n baseUrl: string\n token: string | (() => Promise<string>)\n fetch?: typeof fetch\n}\n\nexport interface GraphStorageFigures {\n docBytes: number\n assetBytes: number\n}\n\nexport interface ServerGraphRecord {\n id: string\n rootDocId: string\n role: string\n /** Storage Footprint (ADR 0033); absent from older servers. */\n storage?: GraphStorageFigures\n}\n\n/** The quota-anticipating rollup over graphs the user owns (ADR 0033). */\nexport interface OwnedStorageTotals extends GraphStorageFigures {\n graphs: number\n}\n\nexport class SyncApiError extends Error {\n constructor(\n message: string,\n readonly status: number,\n readonly code?: string,\n readonly retryable = false,\n ) {\n super(message)\n }\n}\n\nexport function createSyncApi(deps: SyncApiDeps) {\n const f = deps.fetch ?? fetch\n const base = deps.baseUrl.replace(/\\/$/, '')\n\n async function call<T>(path: string, init?: RequestInit): Promise<T> {\n const token = typeof deps.token === 'string' ? deps.token : await deps.token()\n const res = await f(`${base}${path}`, {\n ...init,\n headers: {\n Authorization: `Bearer ${token}`,\n ...(init?.body ? { 'Content-Type': 'application/json' } : {}),\n ...init?.headers,\n },\n })\n if (!res.ok) {\n const body = (await res.json().catch(() => null)) as {\n error?: { message?: string } | 'quota_denied'\n code?: string\n retryable?: boolean\n } | null\n const message = typeof body?.error === 'object'\n ? body.error.message\n : body?.code\n throw new SyncApiError(message ?? `HTTP ${res.status}`, res.status, body?.code, body?.retryable === true)\n }\n return (await res.json()) as T\n }\n\n return {\n /** Resolve the authenticated service-local account before loading account-scoped data. */\n me: () => call<SyncAccountSummary>('/api/v1/sync/me'),\n createGraph: () => call<ServerGraphRecord>('/api/v1/sync/graphs', { method: 'POST' }),\n listGraphs: () => call<{ graphs: ServerGraphRecord[] }>('/api/v1/sync/graphs').then((r) => r.graphs),\n /** Graphs plus the owned-storage rollup (one request - same endpoint as listGraphs). */\n graphsOverview: () =>\n call<{ graphs: ServerGraphRecord[]; ownedStorage: OwnedStorageTotals }>('/api/v1/sync/graphs'),\n graphStorage: (graphId: string) =>\n call<GraphStorageFigures>(`/api/v1/sync/graphs/${graphId}/storage`),\n mintSyncToken: (graphId: string) =>\n call<{ token: string }>('/api/v1/sync/token', {\n method: 'POST',\n body: JSON.stringify({ graphId }),\n }).then((r) => r.token),\n getVault: () =>\n call<{ vault: string; version: number }>('/api/v1/sync/vault').catch((e: unknown) => {\n if (e instanceof SyncApiError && e.status === 404) return null\n throw e\n }),\n putVault: (vault: string, expectedVersion: number) =>\n call<{ version: number }>('/api/v1/sync/vault', {\n method: 'PUT',\n body: JSON.stringify({ vault, expectedVersion }),\n }).then((r) => r.version),\n getIdentity: (userId?: string) =>\n call<{ userId: string; publicKey: string }>(\n `/api/v1/sync/identity${userId ? `?userId=${encodeURIComponent(userId)}` : ''}`,\n ).catch((e: unknown) => {\n if (e instanceof SyncApiError && e.status === 404) return null\n throw e\n }),\n putIdentity: (publicKey: string) =>\n call<{ ok: true }>('/api/v1/sync/identity', {\n method: 'PUT',\n body: JSON.stringify({ publicKey }),\n }),\n getIdentityByEmail: (email: string) =>\n call<{ userId: string; publicKey: string }>(`/api/v1/sync/identity?email=${encodeURIComponent(email)}`).catch(\n (e: unknown) => {\n if (e instanceof SyncApiError && (e.status === 404 || e.status === 409)) return null\n throw e\n },\n ),\n createInvite: (graphId: string, inviteeEmail: string, sealedKeyring: string) =>\n call<{ id: string }>('/api/v1/sync/invites', {\n method: 'POST',\n body: JSON.stringify({ graphId, inviteeEmail, sealedKeyring }),\n }),\n listInvites: () =>\n call<{ invites: Array<{ id: string; graphId: string; rootDocId: string; sealedKeyring: string }> }>(\n '/api/v1/sync/invites',\n ).then((r) => r.invites),\n acceptInvite: (id: string) =>\n call<{ graphId: string }>(`/api/v1/sync/invites/${id}/accept`, { method: 'POST' }).then((r) => r.graphId),\n graphMembers: (graphId: string) =>\n call<{ members: Array<{ userId: string; email: string; role: string }> }>(\n `/api/v1/sync/graphs/${graphId}/members`,\n ).then((r) => r.members),\n leaveGraph: (graphId: string) =>\n call<{ ok: true }>(`/api/v1/sync/graphs/${graphId}/leave`, { method: 'POST' }),\n deleteGraph: (graphId: string) =>\n call<{ ok: true }>(`/api/v1/sync/graphs/${graphId}`, { method: 'DELETE' }),\n transferOwnership: (graphId: string, newOwnerUserId: string) =>\n call<{ ok: true }>(`/api/v1/sync/graphs/${graphId}/transfer`, {\n method: 'POST',\n body: JSON.stringify({ newOwnerUserId }),\n }),\n resetPreview: () =>\n call<{ graphs: Array<{ graphId: string; otherMembers: Array<{ userId: string; email: string; role: string }>; solo: boolean }> }>(\n '/api/v1/sync/reset',\n ).then((r) => r.graphs),\n resetAccount: () =>\n call<{ ownedGraphsDeleted: number; membershipsDropped: number; hadVault: boolean }>('/api/v1/sync/reset', {\n method: 'POST',\n }),\n // Device approval (ADR 0026 flows) — the server brokers blind: an ephemeral public\n // key out, a sealed vault key back.\n createDeviceApproval: (ephemeralPublicKey: string) =>\n call<{ id: string }>('/api/v1/sync/device-approvals', {\n method: 'POST',\n body: JSON.stringify({ ephemeralPublicKey }),\n }),\n listDeviceApprovals: () =>\n call<{ approvals: Array<{ id: string; ephemeralPublicKey: string; createdAt: string }> }>(\n '/api/v1/sync/device-approvals',\n ).then((r) => r.approvals),\n pollDeviceApproval: (id: string) =>\n call<{ status: string; sealedVaultKey?: string }>(`/api/v1/sync/device-approvals/${id}`),\n sealDeviceApproval: (id: string, sealedVaultKey: string) =>\n call<{ ok: true }>(`/api/v1/sync/device-approvals/${id}`, {\n method: 'POST',\n body: JSON.stringify({ sealedVaultKey }),\n }),\n cancelDeviceApproval: (id: string) =>\n call<{ ok: true }>(`/api/v1/sync/device-approvals/${id}`, { method: 'DELETE' }),\n }\n}\n\nexport type SyncApi = ReturnType<typeof createSyncApi>\n","/**\n * Client-side sync configuration (plan Phase 4): the sync server's base URL + the user's\n * connection choice. Managed authentication never stores bearer credentials here; custom\n * deployments keep their PAT as explicit device-local configuration.\n */\nexport const SYNC_CONFIG_STORAGE_KEY = 'etherpk:sync-config'\nexport const SYNC_CONFIG_CHANGED_EVENT = 'etherpk:sync-config-changed'\n\nexport interface ManagedSyncConfig {\n mode: 'managed'\n}\n\nexport interface CustomSyncConfig {\n mode: 'custom'\n /** e.g. https://app.etherpk.example (the sync server origin). */\n serverBaseUrl: string\n /** Personal Access Token (epk_pat_…). */\n token: string\n}\n\nexport type SyncConfig = ManagedSyncConfig | CustomSyncConfig\n\nexport function readSyncConfig(): SyncConfig | null {\n if (typeof localStorage === 'undefined') return null\n const raw = localStorage.getItem(SYNC_CONFIG_STORAGE_KEY)\n if (!raw) return null\n try {\n const parsed = JSON.parse(raw) as Record<string, unknown>\n if (parsed.mode === 'managed') return { mode: 'managed' }\n if (parsed.mode === 'custom' && typeof parsed.serverBaseUrl === 'string' && typeof parsed.token === 'string') {\n return { mode: 'custom', serverBaseUrl: parsed.serverBaseUrl, token: parsed.token }\n }\n } catch {\n /* fall through */\n }\n return null\n}\n\nexport function writeSyncConfig(config: SyncConfig): void {\n if (typeof localStorage === 'undefined') return\n localStorage.setItem(SYNC_CONFIG_STORAGE_KEY, JSON.stringify(config))\n notifySyncConfigChanged()\n}\n\nexport function clearSyncConfig(): void {\n if (typeof localStorage === 'undefined') return\n localStorage.removeItem(SYNC_CONFIG_STORAGE_KEY)\n notifySyncConfigChanged()\n}\n\n/**\n * The browser `storage` event only reaches other tabs. This same-tab event keeps persistent\n * application chrome in step when Graphs saves or removes the device's Sync connection.\n */\nfunction notifySyncConfigChanged(): void {\n if (typeof window !== 'undefined') {\n window.dispatchEvent(new Event(SYNC_CONFIG_CHANGED_EVENT))\n }\n}\n\n/** Derive the `ws(s)://…/sync` relay URL from the server base URL. */\nexport function relayUrlFrom(serverBaseUrl: string): string {\n const base = serverBaseUrl.replace(/\\/$/, '')\n return base.replace(/^http/, 'ws') + '/sync'\n}\n","/**\n * Sync tokens are short-lived (15 minutes, server-side) and authenticate one graph\n * (ADR 0026). Anything that OUTLIVES one token - an import, an open graph session, a\n * WebSocket reconnect - must therefore ask for a token at the moment it needs one rather\n * than capture a string when it is constructed.\n *\n * Capturing one is what killed a live import: the asset uploader held the token minted at\n * the start of the run, uploaded 4592 assets over exactly fifteen minutes, and then every\n * `begin` call 401'd. A caller that holds a `SyncTokenSource` cannot make that mistake.\n *\n * The token AUTHENTICATES only - it is not a Graph Key and decrypts nothing - so re-minting\n * is cheap and carries no key material.\n */\n\nimport { fromBase64Url } from '$lib/crypto'\n\n/**\n * Supplies a currently-valid sync token, re-minting when the held one is near expiry.\n *\n * `force` discards the held token first. It exists for the one case the expiry maths cannot\n * cover: the server rejecting a token this source still believes is live, which means the\n * two clocks disagree by more than the refresh margin. A caller that sees a 401 asks again\n * with `force` and retries, rather than presenting the same dead token until it gives up.\n */\nexport type SyncTokenSource = (opts?: { force?: boolean }) => Promise<string>\n\n/**\n * Re-mint once a quarter of the token's life remains. A fixed minute of headroom is not\n * enough: `exp` is stamped by the SERVER's clock and compared against the BROWSER's, and a\n * client running a few minutes slow would hand out a token the server has already retired.\n * An over-eager mint costs one small request.\n */\nconst REFRESH_AT_FRACTION = 0.25\nconst MIN_MARGIN_MS = 60_000\n\n/** Assumed life of a token whose `exp` cannot be read (the dev/e2e gate injects its own). */\nconst OPAQUE_TOKEN_LIFETIME_MS = 5 * 60_000\n\n/**\n * Read `exp` out of a sync token WITHOUT verifying it. The server is the only verifier;\n * the client reads the claim solely to know when to ask for a replacement, so a forged or\n * malformed token costs nothing here - it simply refreshes on a conservative default.\n */\nexport function syncTokenExpiry(token: string): number | null {\n const dot = token.lastIndexOf('.')\n if (dot < 0) return null\n try {\n const json = new TextDecoder().decode(fromBase64Url(token.slice(0, dot)))\n const exp = (JSON.parse(json) as { exp?: unknown }).exp\n return typeof exp === 'number' ? exp : null\n } catch {\n return null\n }\n}\n\n/**\n * A token that never refreshes. For the dev/e2e gate, which injects a token in the URL and\n * has no API to mint from, and for one-shot sessions that cannot outlive a single token.\n */\nexport function fixedSyncToken(token: string): SyncTokenSource {\n return () => Promise.resolve(token)\n}\n\n/**\n * A refreshing source over `mint` (normally `api.mintSyncToken(graphId)`). Concurrent\n * callers share one in-flight mint: the asset uploader runs six uploads at once, and six\n * simultaneous expiries must cost one round trip, not six.\n */\nexport function createSyncTokenSource(\n mint: () => Promise<string>,\n opts?: { now?: () => number },\n): SyncTokenSource {\n const now = opts?.now ?? (() => Date.now())\n let held: { token: string; refreshAt: number } | undefined\n let inFlight: Promise<string> | undefined\n\n return async (opts) => {\n if (opts?.force) held = undefined\n if (held && now() < held.refreshAt) return held.token\n // A forced call joins a mint already running: six uploads rejected at once must cost\n // one new token, not six.\n if (!inFlight) {\n inFlight = mint()\n .then((token) => {\n const issuedAt = now()\n const expiresAt = syncTokenExpiry(token) ?? issuedAt + OPAQUE_TOKEN_LIFETIME_MS\n const lifetime = Math.max(0, expiresAt - issuedAt)\n const margin = Math.max(MIN_MARGIN_MS, lifetime * REFRESH_AT_FRACTION)\n held = { token, refreshAt: expiresAt - margin }\n return token\n })\n .finally(() => {\n inFlight = undefined\n })\n }\n return inFlight\n }\n}\n","/**\n * The account side of the [[Headless Client]]: the same REST bridge and vault the browser uses,\n * driven from a config file instead of a session. A [[Personal Access Token]] authenticates; the\n * vault key decrypts. Neither is minted here - `login.ts` obtains the second, the portal the first.\n */\n\nimport { fromBase64Url, openVault, type GraphKeyring, type KeyVault } from '$lib/crypto'\nimport { createSyncApi, type ServerGraphRecord, type SyncApi } from '$lib/sync/sync-api'\nimport { relayUrlFrom } from '$lib/sync/sync-config'\nimport { createSyncTokenSource, type SyncTokenSource } from '$lib/sync/sync-token'\n\nimport type { ServerCredentials } from './config'\n\nexport interface HeadlessAccount {\n readonly api: SyncApi\n readonly serverBaseUrl: string\n readonly relayUrl: string\n /** Who the PAT resolves to, for the login summary. */\n readonly principal: { id: string; email: string | null; name: string | null }\n /** The EtherPK Client for this deployment, when the Server declares one (older ones do not). */\n readonly clientUrl: string | null\n /** A token source for one graph, re-minted on every reconnect (a session outlives a token). */\n tokenFor(graphId: string): SyncTokenSource\n}\n\nexport function createHeadlessAccount(config: Pick<ServerCredentials, 'syncServer' | 'pat'>): Omit<HeadlessAccount, 'principal' | 'clientUrl'> {\n const api = createSyncApi({ baseUrl: config.syncServer, token: config.pat })\n return {\n api,\n serverBaseUrl: config.syncServer,\n relayUrl: relayUrlFrom(config.syncServer),\n tokenFor: (graphId) => createSyncTokenSource(() => api.mintSyncToken(graphId)),\n }\n}\n\n/** Resolve the PAT to its principal; a graph-scoped or revoked token fails here, in words. */\nexport async function connectAccount(config: Pick<ServerCredentials, 'syncServer' | 'pat'>): Promise<HeadlessAccount> {\n const account = createHeadlessAccount(config)\n const me = await account.api.me()\n return {\n ...account,\n principal: { id: me.principal.id, email: me.principal.email, name: me.principal.name },\n clientUrl: me.clientUrl ?? null,\n }\n}\n\n/**\n * Open the account vault with the cached vault key. Never writes: a device that only reads the\n * vault cannot create keyrings, mint a Recovery Code or upgrade a blob, which is right for a\n * process that exists to serve one graph.\n */\nexport async function openAccountVault(api: SyncApi, vaultKey: Uint8Array): Promise<KeyVault> {\n const stored = await api.getVault()\n if (!stored) {\n throw new Error('This account has no encryption keys yet. Open EtherPK, create or accept a synced graph, and save the Recovery Code first.')\n }\n return (await openVault(fromBase64Url(stored.vault), vaultKey)).vault\n}\n\nexport interface ResolvedGraph {\n record: ServerGraphRecord\n keyring: GraphKeyring\n}\n\n/**\n * The graph the user named, by id, from the graphs the PAT can see and the keyrings the vault\n * holds. A graph the account is a member of but has no keyring for is an invite not yet\n * accepted in EtherPK - said so, rather than reported as \"not found\".\n */\nexport function resolveGraphById(graphs: ServerGraphRecord[], vault: KeyVault, graphId: string): ResolvedGraph {\n const record = graphs.find((graph) => graph.id === graphId)\n if (!record) {\n throw new Error(`No synced graph with id ${graphId} is reachable with this token. Run \\`etherpk-mcp graphs\\` to list them.`)\n }\n const keyring = vault.keyrings.find((entry) => entry.graphId === graphId)\n if (!keyring) {\n throw new Error(`This account holds no key for graph ${graphId}. Open the graph in EtherPK (accept its invite if there is one) so the key lands in your vault, then try again.`)\n }\n return { record, keyring }\n}\n","/**\n * What `login` leaves behind and `serve` reads: for each Sync Server this machine is signed in\n * to, keyed by origin, the account-wide [[Personal Access Token]] and the vault key that opens\n * the account's keys here. One file holds every login (ADR 0075), so a dev instance beside a\n * production one, or a self-hosted server beside the managed service, need no second file and\n * no environment variable to keep them apart; a command names its server with `--sync-server`\n * and may leave it out while only one is signed in.\n *\n * The file is the same trust class as the browser's `localStorage` cache of the same key\n * (DESIGN.md → Key storage between sessions): whoever can read this user's files can read the\n * accounts. It is written `0600` in the user's config directory, never anywhere a project\n * checkout could pick it up, and `ETHERPK_MCP_CONFIG` overrides the path for tests and for a\n * box that keeps its secrets elsewhere. An OS keychain would be the next step (ADR 0072).\n */\n\nimport { chmod, mkdir, readFile, writeFile } from 'node:fs/promises'\nimport { homedir } from 'node:os'\nimport { dirname, join } from 'node:path'\n\n/** One Sync Server's login. */\nexport interface ServerLogin {\n /** The account-wide PAT (a graph-scoped one cannot reach the vault). */\n pat: string\n /** The vault key, base64url. Absent until `login` has unlocked the account. */\n vaultKey?: string\n}\n\n/** Every login on this machine, keyed by Sync Server origin (`https://…`, no trailing slash). */\nexport interface HeadlessConfig {\n servers: Record<string, ServerLogin>\n}\n\n/** A login together with the server it belongs to: what every command works from. */\nexport interface ServerCredentials extends ServerLogin {\n syncServer: string\n}\n\nexport function defaultConfigPath(env: NodeJS.ProcessEnv = process.env): string {\n const override = env.ETHERPK_MCP_CONFIG?.trim()\n if (override) return override\n const base = env.XDG_CONFIG_HOME?.trim() || join(homedir(), '.config')\n return join(base, 'etherpk', 'mcp.json')\n}\n\nexport function emptyConfig(): HeadlessConfig {\n return { servers: {} }\n}\n\n/** The origin form every key and `--sync-server` value is reduced to before comparison. */\nexport function normaliseSyncServer(value: string): string {\n return value.trim().replace(/\\/+$/, '')\n}\n\n/**\n * Tolerant of a hand-edited file: keeps the entries it knows, refuses the rest. A file in the\n * single-login shape written before 0.3.0 reads as null, so its commands say \"not logged in\"\n * and one `login` rewrites it; the token in it was never lost, only its shape.\n */\nexport function parseConfig(raw: string): HeadlessConfig | null {\n let parsed: unknown\n try {\n parsed = JSON.parse(raw)\n } catch {\n return null\n }\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) return null\n const servers = (parsed as Record<string, unknown>).servers\n if (typeof servers !== 'object' || servers === null || Array.isArray(servers)) return null\n const out = emptyConfig()\n for (const [key, entry] of Object.entries(servers as Record<string, unknown>)) {\n if (typeof entry !== 'object' || entry === null) return null\n const login = entry as Record<string, unknown>\n if (typeof login.pat !== 'string' || login.pat === '') return null\n const syncServer = normaliseSyncServer(key)\n if (!/^https?:\\/\\//.test(syncServer)) return null\n const kept: ServerLogin = { pat: login.pat }\n if (typeof login.vaultKey === 'string' && login.vaultKey !== '') kept.vaultKey = login.vaultKey\n out.servers[syncServer] = kept\n }\n return out\n}\n\nexport function serialiseConfig(config: HeadlessConfig): string {\n return `${JSON.stringify(config, null, 2)}\\n`\n}\n\nexport async function readConfig(path: string): Promise<HeadlessConfig | null> {\n try {\n return parseConfig(await readFile(path, 'utf8'))\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return emptyConfig()\n throw error\n }\n}\n\n/** Write with owner-only permissions, creating the directory; the mode is set even on overwrite. */\nexport async function writeConfig(path: string, config: HeadlessConfig): Promise<void> {\n await mkdir(dirname(path), { recursive: true, mode: 0o700 })\n await writeFile(path, serialiseConfig(config), { mode: 0o600 })\n await chmod(path, 0o600)\n}\n\n/** The logins as a list, in file order, each carrying its server. */\nexport function listLogins(config: HeadlessConfig): ServerCredentials[] {\n return Object.entries(config.servers).map(([syncServer, login]) => ({ syncServer, ...login }))\n}\n\nexport type ServerSelection =\n | { ok: true; credentials: ServerCredentials }\n | { ok: false; reason: 'none' }\n | { ok: false; reason: 'unknown'; syncServer: string; known: string[] }\n | { ok: false; reason: 'ambiguous'; known: string[] }\n\n/**\n * Which login a command means. Named, it must exist; unnamed, it is the only one there is.\n * With two or more logins and no name the command refuses rather than guess: the graphs of\n * one server are not the graphs of another, and a guess would serve the wrong one silently.\n */\nexport function selectServer(config: HeadlessConfig, wanted?: string): ServerSelection {\n const known = Object.keys(config.servers)\n if (wanted !== undefined && wanted.trim() !== '') {\n const syncServer = normaliseSyncServer(wanted)\n const login = config.servers[syncServer]\n return login ? { ok: true, credentials: { syncServer, ...login } } : { ok: false, reason: 'unknown', syncServer, known }\n }\n if (known.length === 0) return { ok: false, reason: 'none' }\n if (known.length > 1) return { ok: false, reason: 'ambiguous', known }\n const syncServer = known[0]\n return { ok: true, credentials: { syncServer, ...config.servers[syncServer] } }\n}\n","/**\n * Content-free performance diagnostics for graph and editor hot paths.\n *\n * Recording is opt-in so normal sessions do not retain timing data. Callers use stable\n * operation names and numeric dimensions only. Concepts, note text, key material and\n * ciphertext must never be passed as labels or detail.\n */\nexport interface PerformanceMetric {\n name: string\n durationMs: number\n at: number\n dimensions: Readonly<Record<string, number>>\n}\n\nexport interface PerformanceRecorder {\n measure<T>(\n name: string,\n dimensions: Readonly<Record<string, number>>,\n operation: () => T,\n ): T\n measureAsync<T>(\n name: string,\n dimensions: Readonly<Record<string, number>>,\n operation: () => Promise<T>,\n ): Promise<T>\n mark(name: string, dimensions?: Readonly<Record<string, number>>): void\n snapshot(): PerformanceMetric[]\n clear(): void\n}\n\nfunction assertSafeName(name: string): void {\n if (!/^[a-z0-9][a-z0-9._-]*$/u.test(name)) {\n throw new Error('Performance metric names may contain only lower-case stable identifiers')\n }\n}\n\nexport function createPerformanceRecorder(options: {\n enabled: boolean\n now?: () => number\n capacity?: number\n}): PerformanceRecorder {\n const now = options.now ?? (() => performance.now())\n const capacity = options.capacity ?? 2_000\n const metrics: PerformanceMetric[] = []\n\n function record(\n name: string,\n startedAt: number,\n dimensions: Readonly<Record<string, number>>,\n ): void {\n if (!options.enabled) return\n assertSafeName(name)\n const endedAt = now()\n metrics.push({\n name,\n durationMs: Math.max(0, endedAt - startedAt),\n at: endedAt,\n dimensions: { ...dimensions },\n })\n if (metrics.length > capacity) metrics.splice(0, metrics.length - capacity)\n\n if (typeof performance !== 'undefined' && performance.mark && performance.measure) {\n const start = `etherpk.${name}.start`\n const end = `etherpk.${name}.end`\n performance.mark(start, { startTime: startedAt })\n performance.mark(end, { startTime: endedAt })\n performance.measure(`etherpk.${name}`, start, end)\n performance.clearMarks(start)\n performance.clearMarks(end)\n }\n }\n\n return {\n measure(name, dimensions, operation) {\n if (!options.enabled) return operation()\n assertSafeName(name)\n const startedAt = now()\n try {\n return operation()\n } finally {\n record(name, startedAt, dimensions)\n }\n },\n async measureAsync(name, dimensions, operation) {\n if (!options.enabled) return operation()\n assertSafeName(name)\n const startedAt = now()\n try {\n return await operation()\n } finally {\n record(name, startedAt, dimensions)\n }\n },\n mark(name, dimensions = {}) {\n if (!options.enabled) return\n assertSafeName(name)\n const at = now()\n metrics.push({ name, durationMs: 0, at, dimensions: { ...dimensions } })\n if (metrics.length > capacity) metrics.splice(0, metrics.length - capacity)\n performance.mark(`etherpk.${name}`)\n },\n snapshot: () => metrics.map((metric) => ({ ...metric, dimensions: { ...metric.dimensions } })),\n clear: () => {\n metrics.length = 0\n },\n }\n}\n\nexport const performanceRecorder = createPerformanceRecorder({\n enabled: typeof window !== 'undefined' && import.meta.env.DEV,\n})\n","import { z } from 'zod'\n\nexport const MANAGED_SYNC_AUDIENCE = 'urn:etherpk:managed-sync'\nexport const MANAGED_SYNC_SCOPE = 'sync'\nexport const ENTITLEMENT_AUDIENCE = 'urn:etherpk:sync-entitlements'\nexport const ENTITLEMENT_SERVICE = 'managed-sync'\nexport const ENTITLEMENT_READ_SCOPE = 'entitlements:read'\nexport const MANAGED_USAGE_SCOPE = 'managed-usage:read'\nexport const ENTITLEMENT_SUBJECT_CLAIM = 'https://etherpk.com/claims/entitlement-subject'\n\n/** The `billing-account:<id>` prefix every entitlement subject carries. */\nexport const ENTITLEMENT_SUBJECT_PREFIX = 'billing-account:'\n\n/**\n * Version-agnostic, like `isUuid` on the Server. The subject was matched by four different\n * hand-rolled patterns that happened to agree only because billing account ids are v4 today:\n * one accepted v1 to v5, another v1 to v8, another any 36 characters of hex and dashes\n * (quality audit B4). If an id ever became a uuidv7 - which is what better-auth already mints\n * - the strictest of them would have silently stopped matching, and the Stripe webhook's\n * metadata fallback would have answered 409 \"Unknown Stripe customer\" for a real subscription.\n * One pattern, here, next to the claim it belongs to.\n */\nexport const ENTITLEMENT_SUBJECT_PATTERN =\n /^billing-account:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i\n\n/** Is this a well-formed `billing-account:<uuid>` subject? */\nexport function isEntitlementSubject(value: unknown): value is string {\n return typeof value === 'string' && ENTITLEMENT_SUBJECT_PATTERN.test(value)\n}\n\nconst httpsUrl = z.url().refine((value) => {\n const url = new URL(value)\n return url.protocol === 'https:' || url.hostname === 'localhost' || url.hostname === '127.0.0.1'\n}, 'URL must use HTTPS except on localhost')\n\nconst authCapabilitySchema = z.discriminatedUnion('mode', [\n z.strictObject({\n mode: z.literal('standalone'),\n portalUrl: httpsUrl,\n pat: z.boolean(),\n }),\n z.strictObject({\n mode: z.literal('oidc'),\n portalUrl: httpsUrl,\n issuer: httpsUrl,\n pat: z.boolean(),\n }),\n])\n\nexport const syncServerCapabilitiesSchema = z.strictObject({\n syncApiVersion: z.literal(1),\n auth: authCapabilitySchema,\n})\n\nexport type SyncServerCapabilities = z.infer<typeof syncServerCapabilitiesSchema>\n\nexport const entitlementLimitsSchema = z.strictObject({\n ownedGraphs: z.int().nonnegative(),\n ownedStorageBytes: z.int().nonnegative(),\n playersPerGraph: z.int().nonnegative(),\n assetBytes: z.int().nonnegative(),\n assetChunks: z.int().nonnegative(),\n})\n\nexport type EntitlementLimits = z.infer<typeof entitlementLimitsSchema>\n\nexport const serviceEntitlementSchema = z.strictObject({\n eventId: z.uuid(),\n issuer: httpsUrl,\n audience: z.literal(ENTITLEMENT_AUDIENCE),\n subject: z.string().regex(ENTITLEMENT_SUBJECT_PATTERN),\n service: z.literal(ENTITLEMENT_SERVICE),\n revision: z.int().positive(),\n status: z.enum(['active', 'grace', 'read_only', 'suspended', 'identity_disabled']),\n plan: z.string().min(1).max(64),\n limits: entitlementLimitsSchema,\n effectiveAt: z.iso.datetime({ offset: true }),\n expiresAt: z.iso.datetime({ offset: true }),\n}).refine(\n ({ effectiveAt, expiresAt }) => Date.parse(expiresAt) > Date.parse(effectiveAt),\n { message: 'expiresAt must be later than effectiveAt', path: ['expiresAt'] },\n)\n\nexport type ServiceEntitlement = z.infer<typeof serviceEntitlementSchema>\n\n/** Content-free aggregate usage which the Sync data plane may expose to Corporate. */\nexport const managedUsageSchema = z.strictObject({\n subject: z.string().regex(ENTITLEMENT_SUBJECT_PATTERN),\n service: z.literal(ENTITLEMENT_SERVICE),\n measuredAt: z.iso.datetime({ offset: true }),\n ownedGraphs: z.int().nonnegative(),\n ownedStorageBytes: z.int().nonnegative(),\n})\n\nexport type ManagedUsage = z.infer<typeof managedUsageSchema>\n\nconst syncAuthenticationSchema = z.discriminatedUnion('mode', [\n z.strictObject({\n mode: z.literal('standalone'),\n method: z.enum(['session', 'pat']),\n }),\n z.strictObject({\n mode: z.literal('managed'),\n method: z.enum(['oidc', 'pat']),\n }),\n])\n\n/**\n * The authenticated, provider-neutral account view exposed by a Sync Server.\n * Provider subjects and entitlement subjects stay server-side because callers only\n * need the stable service-local Principal, display profile, limits, and usage.\n */\nexport const syncAccountSummarySchema = z.strictObject({\n principal: z.strictObject({\n id: z.uuid(),\n email: z.email().nullable(),\n name: z.string().min(1).nullable(),\n image: httpsUrl.nullable(),\n }),\n authentication: syncAuthenticationSchema,\n /**\n * Where the EtherPK Client for this deployment lives (`CLIENT_PUBLIC_URL`), so a device that\n * knows only the Sync Server - the Headless Client asking to be approved - can name the app\n * the user must open. Optional: an older Server does not send it.\n */\n clientUrl: httpsUrl.optional(),\n entitlement: z.strictObject({\n plan: z.string().min(1).max(64),\n status: serviceEntitlementSchema.shape.status,\n limits: entitlementLimitsSchema,\n usage: z.strictObject({\n ownedGraphs: z.int().nonnegative(),\n ownedStorageBytes: z.int().nonnegative(),\n }),\n }),\n})\n\nexport type SyncAccountSummary = z.infer<typeof syncAccountSummarySchema>\n\nexport const quotaErrorCodeSchema = z.enum([\n 'entitlement_inactive',\n 'owned_graph_limit',\n 'owned_storage_limit',\n 'players_per_graph_limit',\n 'asset_size_limit',\n 'asset_chunk_limit',\n 'ownership_transfer_limit',\n])\n\nexport type QuotaErrorCode = z.infer<typeof quotaErrorCodeSchema>\n\nexport const quotaErrorResponseSchema = z.strictObject({\n error: z.literal('quota_denied'),\n code: quotaErrorCodeSchema,\n ownerPrincipalId: z.uuid().optional(),\n retryable: z.literal(true),\n})\n\nexport type QuotaErrorResponse = z.infer<typeof quotaErrorResponseSchema>\n","import { z } from 'zod'\nimport { quotaErrorCodeSchema } from './managed-service'\n\nexport const SYNC_PROTOCOL_VERSION = 2 as const\n\nexport interface SyncProtocolLimits {\n maxMessageBytes: number\n maxEnvelopeBytes: number\n maxSubscriptionDocIds: number\n maxCatchupRows: number\n maxCatchupBytes: number\n malformedMessageBudget: number\n /**\n * A presence envelope (one encrypted awareness update, a few hundred bytes in practice)\n * is bounded separately from content. The relay keeps the latest one per connection and\n * document and replays it to every late subscriber with no database work in the way, so\n * sharing the content ceiling turned one upload into an outbound amplifier (ASTRA F11).\n */\n maxPresenceEnvelopeBytes: number\n /**\n * Document subscriptions one connection may hold at once. `maxSubscriptionDocIds` bounds a\n * single message; this bounds the cumulative set, which used to grow without limit.\n */\n maxSubscriptionsPerConnection: number\n /**\n * Outbound bytes the relay lets queue for one socket before it gives up on the reader.\n * The client resubscribes and catches up from durable storage on reconnect, so dropping a\n * reader that stops draining loses nothing except its live feed.\n */\n maxOutboundBufferedBytes: number\n /**\n * Sockets one user may hold open on one graph. Every browser tab holds its own socket, so\n * this is well above ordinary duplicate-tab use and only bites a script.\n */\n maxSocketsPerUserGraph: number\n}\n\nexport const SYNC_PROTOCOL_LIMITS: Readonly<SyncProtocolLimits> = Object.freeze({\n maxMessageBytes: 12 * 1024 * 1024,\n maxEnvelopeBytes: 8 * 1024 * 1024,\n maxSubscriptionDocIds: 512,\n maxCatchupRows: 256,\n maxCatchupBytes: 8 * 1024 * 1024,\n malformedMessageBudget: 3,\n maxPresenceEnvelopeBytes: 16 * 1024,\n maxSubscriptionsPerConnection: 4096,\n maxOutboundBufferedBytes: 64 * 1024 * 1024,\n maxSocketsPerUserGraph: 10,\n})\n\nexport type SyncProtocolLimitOverrides = Partial<SyncProtocolLimits>\n\nexport const SYNC_ERROR_CODES = [\n 'unsupported_version',\n 'invalid_message',\n 'oversized_message',\n 'stale_generation',\n 'missing_sequence',\n 'membership_revoked',\n 'outbox_conflict',\n 'quota_denied',\n /** A subscribe that would take the connection past `maxSubscriptionsPerConnection`; nothing was subscribed. */\n 'subscription_limit',\n /** A snapshot read-back named a (generation, throughSeq) the relay does not hold. */\n 'snapshot_missing',\n 'internal_error',\n] as const\n\nexport type SyncErrorCode = (typeof SYNC_ERROR_CODES)[number]\n\nconst safeInteger = z.number().int().nonnegative().safe()\nconst generation = z.number().int().positive().safe()\nconst uuid = z.uuid()\nconst base64Url = z\n .string()\n .min(1)\n .regex(/^[A-Za-z0-9_-]+$/)\n .refine((value) => value.length % 4 !== 1, 'Invalid unpadded base64url length')\n\n/**\n * The presence envelope's tighter bound, restated in the schema so a message that somehow\n * reaches `safeParse` without the preflight is still refused. The preflight normally answers\n * first, with the more specific `oversized_message` code.\n */\nfunction presenceEnvelopeSchema(limits: SyncProtocolLimits) {\n return base64Url.refine(\n (value) => (decodedBase64UrlBytes(value) ?? 0) <= limits.maxPresenceEnvelopeBytes,\n 'Presence envelope exceeds the configured byte limit',\n )\n}\n\nfunction clientMessageSchema(limits: SyncProtocolLimits) {\n const version = z.literal(SYNC_PROTOCOL_VERSION)\n const presenceEnvelope = presenceEnvelopeSchema(limits)\n const subscribe = z.union([\n z.object({ v: version, type: z.literal('subscribe'), all: z.literal(true) }).strict(),\n z\n .object({\n v: version,\n type: z.literal('subscribe'),\n docIds: z.array(uuid).max(limits.maxSubscriptionDocIds),\n })\n .strict(),\n ])\n\n return z.discriminatedUnion('type', [\n // Zod cannot discriminate the two subscribe shapes itself, so validate their union\n // as one schema in a final outer union below.\n z\n .object({\n v: version,\n type: z.literal('unsubscribe'),\n docIds: z.array(uuid).max(limits.maxSubscriptionDocIds),\n })\n .strict(),\n z\n .object({\n v: version,\n type: z.literal('watermarks'),\n requestId: uuid,\n docIds: z.array(uuid).min(1).max(limits.maxSubscriptionDocIds),\n })\n .strict(),\n z\n .object({\n v: version,\n type: z.literal('append'),\n docId: uuid,\n outboxId: uuid,\n generation,\n epochId: safeInteger,\n envelope: base64Url,\n })\n .strict(),\n z\n .object({\n v: version,\n type: z.literal('delete'),\n docId: uuid,\n outboxId: uuid,\n generation,\n })\n .strict(),\n z\n .object({\n v: version,\n type: z.literal('resurrect'),\n docId: uuid,\n outboxId: uuid,\n generation,\n epochId: safeInteger,\n envelope: base64Url,\n })\n .strict(),\n z\n .object({\n v: version,\n type: z.literal('catchup'),\n requestId: uuid,\n docId: uuid,\n generation,\n afterSeq: safeInteger,\n priority: z.enum(['foreground', 'background']),\n maxRows: z.number().int().positive().max(limits.maxCatchupRows),\n maxBytes: z.number().int().positive().max(limits.maxCatchupBytes),\n })\n .strict(),\n z\n .object({\n v: version,\n type: z.literal('snapshot_put'),\n docId: uuid,\n generation,\n throughSeq: safeInteger,\n epochId: safeInteger,\n envelope: base64Url,\n })\n .strict(),\n // Read-back verification (ADR 0025, amended 2026-09-12): the compacting client asks\n // for the snapshot it just stored, decodes it, and only then declares it verified.\n // The relay prunes history only behind a verified snapshot.\n z\n .object({\n v: version,\n type: z.literal('snapshot_get'),\n docId: uuid,\n generation,\n throughSeq: safeInteger,\n })\n .strict(),\n z\n .object({\n v: version,\n type: z.literal('snapshot_verified'),\n docId: uuid,\n generation,\n throughSeq: safeInteger,\n })\n .strict(),\n z.object({ v: version, type: z.literal('presence'), docId: uuid, envelope: presenceEnvelope }).strict(),\n z.object({ v: version, type: z.literal('ack_confirm'), outboxId: uuid }).strict(),\n ]).or(subscribe)\n}\n\nfunction serverMessageSchema(limits: SyncProtocolLimits) {\n const version = z.literal(SYNC_PROTOCOL_VERSION)\n const presenceEnvelope = presenceEnvelopeSchema(limits)\n const update = z\n .object({\n seq: safeInteger,\n epochId: safeInteger,\n envelope: base64Url,\n })\n .strict()\n const snapshot = z\n .object({\n throughSeq: safeInteger,\n epochId: safeInteger,\n envelope: base64Url,\n })\n .strict()\n\n return z.discriminatedUnion('type', [\n z\n .object({\n v: version,\n type: z.literal('ack'),\n outboxId: uuid,\n generation,\n state: z.enum(['active', 'deleted']),\n seq: safeInteger,\n })\n .strict(),\n z.object({ v: version, type: z.literal('update'), docId: uuid, generation }).extend(update.shape).strict(),\n /** The relay stored a snapshot_put; the client may now read it back. */\n z\n .object({\n v: version,\n type: z.literal('snapshot_ack'),\n docId: uuid,\n generation,\n throughSeq: safeInteger,\n })\n .strict(),\n /** The stored snapshot, served back by exact identity for verification. */\n z\n .object({\n v: version,\n type: z.literal('snapshot_data'),\n docId: uuid,\n generation,\n throughSeq: safeInteger,\n epochId: safeInteger,\n envelope: base64Url,\n })\n .strict(),\n z\n .object({\n v: version,\n type: z.literal('catchup_batch'),\n /** Omitted only for unsolicited delete lifecycle broadcasts. */\n requestId: uuid.optional(),\n docId: uuid,\n generation,\n state: z.enum(['active', 'deleted']),\n throughSeq: safeInteger,\n hasMore: z.boolean(),\n updates: z.array(update).max(limits.maxCatchupRows),\n snapshot: snapshot.optional(),\n })\n .strict(),\n z.object({ v: version, type: z.literal('presence'), docId: uuid, envelope: presenceEnvelope }).strict(),\n z\n .object({\n v: version,\n type: z.literal('watermarks'),\n requestId: uuid,\n documents: z\n .array(\n z\n .object({\n docId: uuid,\n generation,\n state: z.enum(['active', 'deleted']),\n lastSeq: safeInteger,\n })\n .strict(),\n )\n .max(limits.maxSubscriptionDocIds),\n })\n .strict(),\n z\n .object({\n v: version,\n type: z.literal('error'),\n code: z.enum(SYNC_ERROR_CODES),\n message: z.string().min(1).max(256),\n docId: uuid.optional(),\n currentGeneration: generation.optional(),\n quotaCode: quotaErrorCodeSchema.optional(),\n retryable: z.boolean().optional(),\n })\n .strict(),\n ])\n}\n\n// Exported because the types below are derived from them with `typeof`, and a module-private\n// binding used only in type position reads as dead code to every tool that looks.\nexport const defaultClientMessageSchema = clientMessageSchema(SYNC_PROTOCOL_LIMITS)\nexport const defaultServerMessageSchema = serverMessageSchema(SYNC_PROTOCOL_LIMITS)\n\n/** The exact JSON message accepted from a sync client. */\nexport type SyncClientMessage = z.infer<typeof defaultClientMessageSchema>\n/** The exact JSON message accepted from a sync server. */\nexport type SyncServerMessage = z.infer<typeof defaultServerMessageSchema>\nexport type SyncUpdate = Extract<SyncServerMessage, { type: 'catchup_batch' }>['updates'][number]\n\nexport type SyncProtocolParseResult<T> =\n | { ok: true; value: T }\n | { ok: false; code: 'unsupported_version' | 'invalid_message' | 'oversized_message'; message: string }\n\nfunction byteLength(value: string): number {\n return new TextEncoder().encode(value).byteLength\n}\n\n/** Exact decoded size for valid, unpadded base64url without allocating decoded bytes. */\nexport function decodedBase64UrlBytes(value: string): number | null {\n if (!/^[A-Za-z0-9_-]+$/.test(value) || value.length % 4 === 1) return null\n return Math.floor((value.length * 3) / 4)\n}\n\nexport function validateBase64UrlEnvelope(\n value: unknown,\n limits: Pick<SyncProtocolLimits, 'maxEnvelopeBytes'> = SYNC_PROTOCOL_LIMITS,\n): SyncProtocolParseResult<string> {\n if (typeof value !== 'string' || value.length === 0) {\n return { ok: false, code: 'invalid_message', message: 'Envelope is not valid base64url' }\n }\n const bytes = decodedBase64UrlBytes(value)\n if (bytes === null) return { ok: false, code: 'invalid_message', message: 'Envelope is not valid base64url' }\n if (bytes > limits.maxEnvelopeBytes) {\n return { ok: false, code: 'oversized_message', message: 'Envelope exceeds the configured byte limit' }\n }\n return { ok: true, value }\n}\n\nfunction envelopePreflight(\n parsed: Record<string, unknown>,\n limits: SyncProtocolLimits,\n): SyncProtocolParseResult<true> {\n const envelopes: unknown[] = []\n if ('envelope' in parsed) envelopes.push(parsed.envelope)\n if (parsed.type === 'catchup_batch') {\n if (Array.isArray(parsed.updates)) {\n for (const update of parsed.updates) {\n if (typeof update === 'object' && update !== null) {\n envelopes.push((update as Record<string, unknown>).envelope)\n }\n }\n }\n if (typeof parsed.snapshot === 'object' && parsed.snapshot !== null) {\n envelopes.push((parsed.snapshot as Record<string, unknown>).envelope)\n }\n }\n\n // Presence is bounded on its own, in both directions: the relay parses it inbound and the\n // client parses the relay's broadcast and late-join replay of the very same envelope.\n const envelopeLimit =\n parsed.type === 'presence' ? { maxEnvelopeBytes: limits.maxPresenceEnvelopeBytes } : limits\n let catchupBytes = 0\n for (const envelope of envelopes) {\n const result = validateBase64UrlEnvelope(envelope, envelopeLimit)\n if (!result.ok) return result\n catchupBytes += decodedBase64UrlBytes(result.value) ?? 0\n }\n if (parsed.type === 'catchup_batch' && catchupBytes > limits.maxCatchupBytes) {\n return { ok: false, code: 'oversized_message', message: 'Catch-up page exceeds the configured byte limit' }\n }\n return { ok: true, value: true }\n}\n\nfunction parseMessage<T>(\n raw: string,\n schema: z.ZodType<T>,\n limits: SyncProtocolLimits,\n): SyncProtocolParseResult<T> {\n if (byteLength(raw) > limits.maxMessageBytes) {\n return { ok: false, code: 'oversized_message', message: 'Message exceeds the configured byte limit' }\n }\n\n let parsed: unknown\n try {\n parsed = JSON.parse(raw)\n } catch {\n return { ok: false, code: 'invalid_message', message: 'Message is not valid JSON' }\n }\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n return { ok: false, code: 'invalid_message', message: 'Message must be a JSON object' }\n }\n const record = parsed as Record<string, unknown>\n if (record.v !== SYNC_PROTOCOL_VERSION) {\n return { ok: false, code: 'unsupported_version', message: 'Unsupported sync protocol version' }\n }\n const envelopeResult = envelopePreflight(record, limits)\n if (!envelopeResult.ok) return envelopeResult\n\n const result = schema.safeParse(parsed)\n if (!result.success) {\n return { ok: false, code: 'invalid_message', message: 'Message does not match the sync protocol' }\n }\n return { ok: true, value: result.data }\n}\n\nexport interface SyncProtocol {\n readonly limits: SyncProtocolLimits\n parseClientMessage(raw: string): SyncProtocolParseResult<SyncClientMessage>\n parseServerMessage(raw: string): SyncProtocolParseResult<SyncServerMessage>\n serializeClientMessage(message: SyncClientMessage): string\n serializeServerMessage(message: SyncServerMessage): string\n}\n\nexport function createSyncProtocol(overrides: SyncProtocolLimitOverrides = {}): SyncProtocol {\n const limits = Object.freeze({ ...SYNC_PROTOCOL_LIMITS, ...overrides })\n const clientSchema = clientMessageSchema(limits)\n const serverSchema = serverMessageSchema(limits)\n\n const parseClient = (raw: string) => parseMessage(raw, clientSchema, limits)\n const parseServer = (raw: string) => parseMessage(raw, serverSchema, limits)\n\n function serialize<T>(message: T): string {\n const raw = JSON.stringify(message)\n if (byteLength(raw) > limits.maxMessageBytes) {\n throw new Error('oversized_message: Message exceeds the configured byte limit')\n }\n return raw\n }\n\n return {\n limits,\n parseClientMessage: parseClient,\n parseServerMessage: parseServer,\n serializeClientMessage: (message) => serialize(message),\n serializeServerMessage: (message) => serialize(message),\n }\n}\n\nconst defaultProtocol = createSyncProtocol()\n\nexport const parseClientMessage = defaultProtocol.parseClientMessage\nexport const parseServerMessage = defaultProtocol.parseServerMessage\nexport const serializeClientMessage = defaultProtocol.serializeClientMessage\nexport const serializeServerMessage = defaultProtocol.serializeServerMessage\n","import * as encoding from 'lib0/encoding'\nimport * as Y from 'yjs'\n\n/**\n * A Yjs state vector is a set of per-client clocks. Relay acknowledgement and remote-update\n * order are independent, so replacing one vector with another can forget already-confirmed\n * structs. The union is the component-wise maximum.\n */\nexport function mergeStateVectors(...vectors: readonly Uint8Array[]): Uint8Array {\n const clocks = new Map<number, number>()\n for (const vector of vectors) {\n if (vector.byteLength === 0) continue\n for (const [client, clock] of Y.decodeStateVector(vector)) {\n clocks.set(client, Math.max(clocks.get(client) ?? 0, clock))\n }\n }\n\n const encoder = encoding.createEncoder()\n encoding.writeVarUint(encoder, clocks.size)\n // Stable ordering is not required by Yjs, but deterministic bytes make cache diagnostics\n // and tests much easier to reason about.\n for (const [client, clock] of [...clocks].sort(([a], [b]) => a - b)) {\n encoding.writeVarUint(encoder, client)\n encoding.writeVarUint(encoder, clock)\n }\n return encoding.toUint8Array(encoder)\n}\n\n/** True when every client clock in `required` is present in `actual` at least as far. */\nexport function stateVectorCovers(actual: Uint8Array, required: Uint8Array): boolean {\n const actualClocks = Y.decodeStateVector(actual)\n for (const [client, clock] of Y.decodeStateVector(required)) {\n if ((actualClocks.get(client) ?? 0) < clock) return false\n }\n return true\n}\n","/**\n * One encrypted Yjs document sync engine. Local state is written to IndexedDB before\n * transmission, and at most one durable append per document is in flight. A reconnect\n * therefore retries the same outbox ID and ciphertext instead of manufacturing a new\n * operation from volatile state.\n */\nimport * as Y from 'yjs'\nimport { Awareness, applyAwarenessUpdate, encodeAwarenessUpdate, removeAwarenessStates } from 'y-protocols/awareness'\nimport { contextAad, envelopeEpochId, openSymmetric, sealSymmetric, toBase64Url, fromBase64Url } from '$lib/crypto'\nimport type { GraphKeyring } from '$lib/crypto'\nimport { currentEpoch, keyForEpoch } from '$lib/crypto'\nimport { SYNC_PROTOCOL_LIMITS } from '@appsoftwareltd/etherpk-shared'\nimport type { RelayClientMessage, RelayServerMessage } from './messages'\nimport { mergeStateVectors } from './state-vector'\n\n/** Transaction origin marking a change as relay-originated and never independently re-sent. */\nexport const REMOTE = Symbol('etherpk-remote')\n/** Local IndexedDB hydration is restored state, not a new edit or remote delta. */\nexport const CACHE_SEED = Symbol('etherpk-cache-seed')\n/** Structural lifecycle changes which must never be mistaken for a user's edit. */\nexport const SUPPRESSED = Symbol('etherpk-suppressed')\n\nexport interface DocCacheState {\n update: Uint8Array\n lastSeq: number\n lastSyncedStateVector: Uint8Array\n generation?: number\n lifecycle?: 'active' | 'deleted'\n /**\n * Persistence-boundary tokens for local edits present in `update` but not yet covered\n * by a durable outbox operation. A set is required because several tabs can save the\n * same document independently before any one of them reaches its append debounce.\n */\n dirtyTokens?: readonly string[]\n}\n\nexport interface DocCacheSaveOptions {\n /** Add one local-edit boundary without clearing boundaries written by another tab. */\n dirtyToken?: string\n /** Replace this session's preceding boundary atomically with `dirtyToken`. */\n supersededDirtyToken?: string\n /**\n * Whether this save observed an effective Yjs update. Local Cache uses this O(1) signal\n * for ordinary documents; root documents still compare their registry projection.\n */\n indexContentChanged?: boolean\n}\n\nexport interface DocCacheEnqueueOptions {\n /** Clear only boundaries represented by this exact encrypted operation. */\n settledDirtyTokens?: readonly string[]\n}\n\ninterface NewOutboxBase {\n outboxId: string\n generation: number\n createdAt: number\n}\n\nexport interface NewAppendOperation extends NewOutboxBase {\n kind: 'append' | 'resurrect'\n epochId: number\n envelope: string\n stateVector: Uint8Array\n}\n\nexport interface NewDeleteOperation extends NewOutboxBase {\n kind: 'delete'\n}\n\nexport type NewOutboxOperation = NewAppendOperation | NewDeleteOperation\n\nexport type DurableOutboxOperation = NewOutboxOperation & {\n graphId: string\n docId: string\n attemptCount: number\n lastAttemptAt: number | null\n}\n\nexport interface DocCache {\n load(): Promise<DocCacheState | null>\n save(state: DocCacheState, options?: DocCacheSaveOptions): Promise<void>\n /** Store the current document and its exact encrypted operation in one transaction. */\n enqueue(\n operation: NewOutboxOperation,\n state: DocCacheState,\n options?: DocCacheEnqueueOptions,\n ): Promise<void>\n pending(): Promise<DurableOutboxOperation[]>\n /**\n * Rewrite the row's update as a garbage-collected state: the same clocks, the same\n * information about what exists, and nothing about what was deleted. `mergeUpdates`\n * never collects, so a document's original body stays in the unencrypted row for the\n * life of the document otherwise - readable from the profile directory with no key of any\n * kind once the document has been protected (ADR 0057's third adversary).\n */\n compact(): Promise<void>\n markAttempt(outboxId: string, attemptedAt: number): Promise<void>\n /** Advance state and remove the matching operation in one transaction. */\n commitAcknowledgement(\n outboxId: string,\n generation: number,\n seq: number,\n update: Uint8Array,\n lastSyncedStateVector: Uint8Array,\n ): Promise<boolean>\n commitDeleteAcknowledgement(outboxId: string, generation: number): Promise<boolean>\n discard(outboxId: string): Promise<void>\n purge(): Promise<void>\n}\n\nexport interface DocSyncDeps {\n docId: string\n graphId: string\n keyring: GraphKeyring\n send(message: RelayClientMessage): void\n persist: DocCache\n debounceMs?: number\n now?: () => number\n generation?: number\n newOutboxId?: () => string\n /** Cache and transport failures are user-visible diagnostics, not swallowed promises. */\n onError?: (error: Error) => void\n /** Lets the graph release an unretained engine once its durable work has settled. */\n onIdle?: () => void\n /**\n * When true of the document, its cache row is rewritten as a garbage-collected state\n * after the next persist, once per stretch of being true. For a [[Protected Document]]:\n * the row otherwise keeps the pre-protection body merged in for ever, on every device that\n * receives the protect, not only the one that did it.\n */\n collectRowWhen?: (doc: Y.Doc) => boolean\n /** Foreground views overtake background hydration at the graph scheduler. */\n catchupPriority?: () => 'foreground' | 'background'\n /**\n * When an idle document uploads a consolidated snapshot (ADR 0025, amended 2026-09-12):\n * once the unsnapshotted tail reaches `minUpdates` rows, or holds `minBytes` more than\n * the document's own encoded state, after `idleMs` without a local edit.\n */\n compaction?: {\n minUpdates?: number\n minBytes?: number\n idleMs?: number\n }\n}\n\nexport interface DocSync {\n readonly doc: Y.Doc\n readonly awareness: Awareness\n ready(): Promise<void>\n receive(message: RelayServerMessage): Promise<void>\n receivePresence(envelope: string): Promise<void>\n /** Resolves once this engine's first relay catch-up page has been applied and persisted. */\n firstCatchupPage(): Promise<void>\n /** Resolves only when the current catch-up cycle reaches a terminal relay page. */\n caughtUp(): Promise<void>\n resync(): void\n compact(): Promise<void>\n flush(): Promise<void>\n delete(): Promise<void>\n lifecycle(): 'seeding' | 'active' | 'deleting' | 'deleted' | 'resurrecting'\n health(): SyncHealth\n staleGeneration(currentGeneration: number): Promise<void>\n /** Relay an awareness tombstone before the graph drops this document subscription. */\n clearPresence(): Promise<void>\n /**\n * Re-send the current local awareness state. A subscribe carries no presence, and a\n * state set while the socket was closed (or before a reconnect) was dropped — this is\n * the client half of the late-join handshake: advertise right after subscribing so the\n * relay can store the envelope and replay it to whoever subscribes next.\n */\n advertisePresence(): Promise<void>\n /** True only when destroying the engine cannot strand a locally queued operation. */\n isIdle(): boolean\n destroy(): void\n}\n\nexport type SyncHealth = 'healthy' | 'key-unavailable' | 'ciphertext-corrupt' | 'generation-stale' | 'sequence-gap'\n\ninterface SyncCompletion {\n readonly promise: Promise<void>\n readonly settled: boolean\n resolve(): void\n reject(error: Error): void\n}\n\nfunction newSyncCompletion(): SyncCompletion {\n let resolvePromise!: () => void\n let rejectPromise!: (error: Error) => void\n let settled = false\n const promise = new Promise<void>((resolve, reject) => {\n resolvePromise = resolve\n rejectPromise = reject\n })\n // A document can be used without both readiness milestones being observed. Attach a\n // rejection handler here so a cache failure is still available to callers without\n // becoming an unrelated global unhandled-rejection event.\n void promise.catch(() => undefined)\n return {\n promise,\n get settled() {\n return settled\n },\n resolve() {\n if (settled) return\n settled = true\n resolvePromise()\n },\n reject(error) {\n if (settled) return\n settled = true\n rejectPromise(error)\n },\n }\n}\n\nfunction asError(error: unknown): Error {\n return error instanceof Error ? error : new Error(String(error))\n}\n\nexport function createDocSync(deps: DocSyncDeps): DocSync {\n const { docId, graphId, keyring, send, persist } = deps\n const debounceMs = deps.debounceMs ?? 300\n const now = deps.now ?? Date.now\n const generation = deps.generation ?? 1\n const newOutboxId = deps.newOutboxId ?? (() => crypto.randomUUID())\n const aad = contextAad('update', `graph:${graphId}`, `doc:${docId}`)\n const presenceAad = contextAad('presence', `graph:${graphId}`, `doc:${docId}`)\n const doc = new Y.Doc()\n const awareness = new Awareness(doc)\n\n // Presentation and reconciliation have deliberately different readiness boundaries.\n // A cold editor can render after one page, but cache/index reconciliation must retain\n // its cross-tab lock until every continuation page in the current cycle has landed.\n const firstCatchupPage = newSyncCompletion()\n let catchupCompletion = newSyncCompletion()\n let catchupActive = false\n let lastSeq = 0\n let documentGeneration = generation\n let lifecycleState: ReturnType<DocSync['lifecycle']> = 'seeding'\n let syncHealth: SyncHealth = 'healthy'\n let lastSyncedStateVector = Y.encodeStateVector(doc)\n /**\n * The document state (state vector AND delete set) the relay is known to hold, advanced\n * at acknowledgement boundaries. Session-local: it seeds from the cached document, whose\n * deletions are either relay-originated or replayed by the durable outbox. Snapshot\n * equality is the drain's \"nothing to send\" test. State vectors alone cannot see\n * deletions, and diff length cannot ignore them.\n */\n let lastSyncedSnapshot: Y.Snapshot | undefined\n /** Boundary snapshots captured when each in-flight operation's diff was encoded. */\n const boundarySnapshots = new Map<string, Y.Snapshot>()\n let durableQueue: DurableOutboxOperation[] = []\n let debounceTimer: ReturnType<typeof setTimeout> | undefined\n let drainPromise: Promise<void> | undefined\n const futureUpdates = new Map<number, { envelope: string; generation: number }>()\n let persistenceTail = Promise.resolve()\n let lastPersistenceError: Error | undefined\n let seeding = false\n /** Incremented only when Yjs says an update changed this live document. */\n let indexMutationVersion = 0\n /** A local edit landed before the cache seed finished, so the live doc is ahead of it. */\n let localEditsBeforeSeed = false\n /** Dirty boundaries loaded from a prior tab/session and not yet moved into the outbox. */\n const recoveredDirtyTokens = new Set<string>()\n const dirtySessionId = crypto.randomUUID()\n let dirtyBoundary = 0\n let activeDirtyToken: string | undefined\n let destroyed = false\n let updatesSinceSnapshot = 0\n let bytesSinceSnapshot = 0\n let compactionTimer: ReturnType<typeof setTimeout> | undefined\n const compactionPolicy = {\n minUpdates: deps.compaction?.minUpdates ?? 32,\n minBytes: deps.compaction?.minBytes ?? 4 * 1024,\n idleMs: deps.compaction?.idleMs ?? 30_000,\n }\n /** The snapshot uploaded and not yet read back, with the exact state it encoded. */\n let pendingSnapshot: { generation: number; throughSeq: number; state: Y.Snapshot } | undefined\n /** A read-back failed: the next idle uploads a fresh snapshot whatever the tail size. */\n let snapshotRetryWanted = false\n\n function requestCatchup(afterSeq: number): void {\n if (!catchupActive) {\n // The first cycle owns the completion created with the engine, so callers may\n // safely start waiting before the socket opens. Every later resync receives a\n // fresh pending completion instead of reusing an already-resolved promise.\n if (catchupCompletion.settled) catchupCompletion = newSyncCompletion()\n catchupActive = true\n }\n send({\n type: 'catchup',\n requestId: newOutboxId(),\n docId,\n generation: documentGeneration,\n afterSeq,\n priority: deps.catchupPriority?.() ?? 'background',\n maxRows: SYNC_PROTOCOL_LIMITS.maxCatchupRows,\n maxBytes: SYNC_PROTOCOL_LIMITS.maxCatchupBytes,\n })\n }\n\n function completeCatchup(): void {\n firstCatchupPage.resolve()\n catchupActive = false\n catchupCompletion.resolve()\n }\n\n function failCatchup(error: unknown): Error {\n const reported = asError(error)\n firstCatchupPage.reject(reported)\n catchupActive = false\n catchupCompletion.reject(reported)\n return reported\n }\n\n const encrypt = (update: Uint8Array) =>\n sealSymmetric({\n key: currentEpoch(keyring).key,\n epochId: currentEpoch(keyring).epochId,\n plaintext: update,\n aad,\n })\n\n function stateSnapshot(): DocCacheState {\n return {\n update: Y.encodeStateAsUpdate(doc),\n lastSeq,\n lastSyncedStateVector,\n generation: documentGeneration,\n lifecycle: lifecycleState === 'deleted' ? 'deleted' : 'active',\n }\n }\n\n /**\n * Serialising cache work prevents an older background save from landing after an\n * acknowledgement transaction and regressing its watermark or state vector.\n */\n function persistTask<T>(work: () => Promise<T>): Promise<T> {\n const result = persistenceTail.then(work)\n persistenceTail = result.then(\n () => {\n lastPersistenceError = undefined\n },\n (error: unknown) => {\n lastPersistenceError = asError(error)\n // A write still in flight when the workspace was torn down rejects here, and\n // used to surface as a \"could not save\" notice over whatever opened next.\n if (!destroyed) deps.onError?.(lastPersistenceError)\n },\n )\n return result\n }\n\n function detached(work: Promise<unknown>): void {\n void work.catch((error: unknown) => {\n const reported = asError(error)\n if (!destroyed && reported !== lastPersistenceError) deps.onError?.(reported)\n })\n }\n\n function saveCurrent(options?: DocCacheSaveOptions): Promise<void> {\n const state = stateSnapshot()\n return persistTask(() => persist.save(state, options)).then(collectRowIfWanted)\n }\n\n /**\n * Collect the cache row after a persist that made the document read as wanted - chained\n * on the persistence tail, so it runs over the row that already holds the persisted state.\n * Once per stretch: a document that stops reading as wanted and later reads so again is\n * collected again.\n */\n let rowCollected = false\n function collectRowIfWanted(): Promise<void> {\n if (!deps.collectRowWhen) return Promise.resolve()\n if (!deps.collectRowWhen(doc)) {\n rowCollected = false\n return Promise.resolve()\n }\n if (rowCollected) return Promise.resolve()\n rowCollected = true\n return persistTask(() => persist.compact())\n }\n\n async function transmit(operation: DurableOutboxOperation): Promise<void> {\n await persistTask(() => persist.markAttempt(operation.outboxId, now()))\n if (operation.kind === 'delete') {\n send({\n type: 'delete',\n docId,\n outboxId: operation.outboxId,\n generation: operation.generation,\n })\n return\n }\n send({\n type: operation.kind,\n docId,\n outboxId: operation.outboxId,\n generation: operation.generation,\n epochId: operation.epochId,\n envelope: operation.envelope,\n })\n }\n\n async function drainCurrentDiff(): Promise<void> {\n await persistenceTail\n if (destroyed || durableQueue.length > 0) return\n // \"Nothing to send\" must be judged by SNAPSHOT equality (state vector + delete\n // set), never by the encoded diff's length: Yjs writes the document's ENTIRE\n // delete set into every state-vector difference, so a document with any deletion\n // history always produces a non-empty diff. With the ack handler draining again\n // after each acknowledgement, that byte-length test self-looped one client into\n // ~40 appends per second per document (live, 2026-07-30: three documents at\n // sequence 660,000+, every open degraded, the index churning in every tab).\n const currentSnapshot = Y.snapshot(doc)\n if (lastSyncedSnapshot && Y.equalSnapshots(currentSnapshot, lastSyncedSnapshot)) return\n const diff = Y.encodeStateAsUpdate(doc, lastSyncedStateVector)\n // Yjs encodes an empty state-vector difference as two bytes.\n if (diff.length <= 2) return\n // Capture the boundary synchronously with the diff. Encryption yields to the event\n // loop; a later keystroke must not be included in this operation's acknowledged\n // vector when it is absent from this operation's plaintext.\n const operationStateVector = Y.encodeStateVector(doc)\n const settledDirtyTokens = [...recoveredDirtyTokens]\n if (activeDirtyToken) settledDirtyTokens.push(activeDirtyToken)\n // Rotate before encryption yields. A local edit made while encryption is running\n // receives a new token and cannot be cleared by this older operation boundary.\n activeDirtyToken = undefined\n\n let operation: NewOutboxOperation\n try {\n const epoch = currentEpoch(keyring)\n const kind = lifecycleState === 'deleted' ? 'resurrect' : 'append'\n // Resurrection starts a fresh server generation, so it must be independently\n // materialisable. A state-vector difference can depend on structs deleted with\n // the old generation and would decode as an empty document on another client.\n const plaintext = kind === 'resurrect' ? Y.encodeStateAsUpdate(doc) : diff\n operation = {\n outboxId: newOutboxId(),\n kind,\n generation: documentGeneration,\n epochId: epoch.epochId,\n envelope: toBase64Url(await encrypt(plaintext)),\n stateVector: operationStateVector,\n createdAt: now(),\n }\n const state = stateSnapshot()\n await persistTask(() =>\n persist.enqueue(operation, state, { settledDirtyTokens }),\n )\n await collectRowIfWanted()\n } catch (error) {\n // The cache still carries these tokens. Retain them in memory too so a retry in\n // this session can settle the same durable boundaries without another edit.\n for (const token of settledDirtyTokens) recoveredDirtyTokens.add(token)\n throw error\n }\n for (const token of settledDirtyTokens) recoveredDirtyTokens.delete(token)\n boundarySnapshots.set(operation.outboxId, currentSnapshot)\n const durable: DurableOutboxOperation = {\n graphId,\n docId,\n ...operation,\n attemptCount: 0,\n lastAttemptAt: null,\n }\n durableQueue.push(durable)\n if (operation.kind === 'resurrect') lifecycleState = 'resurrecting'\n await transmit(durable)\n }\n\n function requestDrain(): Promise<void> {\n if (!drainPromise) {\n drainPromise = drainCurrentDiff().finally(() => {\n drainPromise = undefined\n deps.onIdle?.()\n })\n }\n return drainPromise\n }\n\n function scheduleDrain(): void {\n if (debounceTimer) clearTimeout(debounceTimer)\n debounceTimer = setTimeout(() => {\n debounceTimer = undefined\n detached(requestDrain())\n }, debounceMs)\n }\n\n /**\n * Whether the unsnapshotted tail is worth collapsing (ADR 0025, amended 2026-09-12).\n * Either the tail has reached the row floor - a cold load replays that many envelopes -\n * or the log holds at least `minBytes` more than the document's own encoded state, which\n * is what pruning behind a snapshot would actually free. Judged only when the idle timer\n * fires, because the state size is an O(document) encode. A failed read-back forces the\n * next attempt regardless.\n */\n function compactionDue(): boolean {\n if (snapshotRetryWanted) return true\n if (updatesSinceSnapshot === 0) return false\n if (updatesSinceSnapshot >= compactionPolicy.minUpdates) return true\n return bytesSinceSnapshot - Y.encodeStateAsUpdate(doc).byteLength >= compactionPolicy.minBytes\n }\n\n function scheduleAutomaticCompaction(): void {\n if (destroyed || (updatesSinceSnapshot === 0 && !snapshotRetryWanted)) return\n if (compactionTimer) clearTimeout(compactionTimer)\n compactionTimer = setTimeout(() => {\n compactionTimer = undefined\n if (!compactionDue()) return\n detached(\n uploadSnapshot().then((uploaded) => {\n if (!uploaded) scheduleAutomaticCompaction()\n }),\n )\n }, compactionPolicy.idleMs)\n }\n\n const onUpdate = (_update: Uint8Array, origin: unknown) => {\n if (destroyed || seeding) return\n indexMutationVersion += 1\n if (lifecycleState === 'seeding' && origin !== REMOTE) localEditsBeforeSeed = true\n if (compactionTimer) {\n clearTimeout(compactionTimer)\n compactionTimer = undefined\n }\n // Persist local edits immediately, including edits made while an operation is in\n // flight. Relay handlers persist once after an update or whole catch-up page has\n // applied; saving here as well rewrites the complete merged document for every\n // envelope in that page.\n if (origin !== REMOTE) {\n const supersededDirtyToken = activeDirtyToken\n activeDirtyToken = `${dirtySessionId}:${++dirtyBoundary}`\n detached(\n saveCurrent({\n dirtyToken: activeDirtyToken,\n supersededDirtyToken,\n indexContentChanged: true,\n }),\n )\n }\n if (origin !== REMOTE && origin !== SUPPRESSED) scheduleDrain()\n }\n doc.on('update', onUpdate)\n\n let suppressAutomaticAwareness = false\n\n async function sendAwareness(changed: number[]): Promise<void> {\n const update = encodeAwarenessUpdate(awareness, changed)\n const envelope = toBase64Url(\n await sealSymmetric({\n key: currentEpoch(keyring).key,\n epochId: currentEpoch(keyring).epochId,\n plaintext: update,\n aad: presenceAad,\n }),\n )\n send({ type: 'presence', docId, envelope })\n }\n\n const onAwareness = (\n { added, updated, removed }: { added: number[]; updated: number[]; removed: number[] },\n origin: unknown,\n ) => {\n if (destroyed || origin === REMOTE || suppressAutomaticAwareness) return\n const changed = [...added, ...updated, ...removed]\n detached(sendAwareness(changed))\n }\n awareness.on('update', onAwareness)\n\n let skippedUpdates = 0\n\n async function applyEnvelope(envelope: string, seq: number, incomingGeneration = documentGeneration): Promise<boolean> {\n if (incomingGeneration !== documentGeneration) {\n if (\n lifecycleState === 'deleted' &&\n incomingGeneration === documentGeneration + 1 &&\n seq === 1\n ) {\n // A peer's explicit resurrection is broadcast as the complete first update\n // of the new generation.\n documentGeneration = incomingGeneration\n lifecycleState = 'active'\n lastSeq = 0\n } else {\n syncHealth = 'generation-stale'\n return false\n }\n }\n if (seq <= lastSeq) return true\n if (seq !== lastSeq + 1) {\n syncHealth = 'sequence-gap'\n if (futureUpdates.size < 256) {\n futureUpdates.set(seq, { envelope, generation: incomingGeneration })\n }\n requestCatchup(lastSeq)\n return false\n }\n try {\n const bytes = fromBase64Url(envelope)\n const epochId = envelopeEpochId(bytes)\n if (!keyForEpoch(keyring, epochId)) {\n syncHealth = 'key-unavailable'\n return false\n }\n const { plaintext } = await openSymmetric({\n keyForEpoch: (id) => keyForEpoch(keyring, id),\n envelope: bytes,\n aad,\n })\n Y.applyUpdate(doc, plaintext, REMOTE)\n // A relay update is server-confirmed state. Retain it in the baseline even when\n // a local operation is in flight, otherwise its later acknowledgement replaces\n // this knowledge and requestDrain() relays the peer's update back as a new append.\n lastSyncedStateVector = mergeStateVectors(\n lastSyncedStateVector,\n Y.encodeStateVectorFromUpdate(plaintext),\n )\n // With no local work in any stage (queued, draining, or debounced), the doc is\n // exactly baseline-plus-relay-updates: all relay-known, delete sets included.\n if (durableQueue.length === 0 && !debounceTimer && !drainPromise) {\n lastSyncedSnapshot = Y.snapshot(doc)\n }\n } catch (error) {\n syncHealth = 'ciphertext-corrupt'\n skippedUpdates += 1\n if (skippedUpdates <= 3 || skippedUpdates % 50 === 0) {\n console.warn(`[sync] update seq ${seq} for doc ${docId} skipped (${skippedUpdates} so far):`, error)\n }\n return false\n }\n if (seq > lastSeq) lastSeq = seq\n updatesSinceSnapshot += 1\n bytesSinceSnapshot += fromBase64Url(envelope).byteLength\n scheduleAutomaticCompaction()\n syncHealth = 'healthy'\n const next = futureUpdates.get(lastSeq + 1)\n if (next) {\n futureUpdates.delete(lastSeq + 1)\n await applyEnvelope(next.envelope, lastSeq + 1, next.generation)\n }\n return true\n }\n\n async function uploadSnapshot(): Promise<boolean> {\n if (\n lastSeq === 0 ||\n durableQueue.length > 0 ||\n syncHealth !== 'healthy' ||\n lifecycleState !== 'active'\n ) {\n return false\n }\n // Capture the encoded state and its Y.Snapshot together, before encryption yields:\n // the read-back is compared against exactly what was encoded, not against whatever\n // the document holds by the time the relay answers.\n const consolidated = Y.encodeStateAsUpdate(doc)\n const captured = Y.snapshot(doc)\n const throughSeq = lastSeq\n const snapshotGeneration = documentGeneration\n const epochId = currentEpoch(keyring).epochId\n const envelope = toBase64Url(await encrypt(consolidated))\n if (destroyed) return false\n pendingSnapshot = { generation: snapshotGeneration, throughSeq, state: captured }\n snapshotRetryWanted = false\n send({\n type: 'snapshot_put',\n docId,\n generation: snapshotGeneration,\n throughSeq,\n epochId,\n envelope,\n })\n updatesSinceSnapshot = 0\n bytesSinceSnapshot = 0\n return true\n }\n\n /**\n * The relay stored the snapshot. Read it straight back: the blind server cannot check a\n * snapshot, so the client that produced it proves the stored bytes decode to the state\n * it captured, and only then may the server prune behind it (ADR 0025, amended\n * 2026-09-12). Anything naming a snapshot other than the one in flight is ignored.\n */\n function onSnapshotAck(message: { generation: number; throughSeq: number }): void {\n const pending = pendingSnapshot\n if (!pending || pending.generation !== message.generation || pending.throughSeq !== message.throughSeq) return\n send({ type: 'snapshot_get', docId, generation: pending.generation, throughSeq: pending.throughSeq })\n }\n\n async function onSnapshotData(message: { generation: number; throughSeq: number; envelope: string }): Promise<void> {\n const pending = pendingSnapshot\n if (!pending || pending.generation !== message.generation || pending.throughSeq !== message.throughSeq) return\n pendingSnapshot = undefined\n let verified = false\n const scratch = new Y.Doc()\n try {\n const { plaintext } = await openSymmetric({\n keyForEpoch: (id) => keyForEpoch(keyring, id),\n envelope: fromBase64Url(message.envelope),\n aad,\n })\n Y.applyUpdate(scratch, plaintext)\n // State vector AND delete set, the same equality the drain uses: a snapshot that\n // dropped a deletion would still match on state vector alone.\n verified = Y.equalSnapshots(Y.snapshot(scratch), pending.state)\n } catch {\n verified = false\n } finally {\n scratch.destroy()\n }\n if (destroyed) return\n if (!verified) {\n // The relay keeps the log, so nothing is lost; a fresh snapshot goes up at the\n // next idle and gets its own read-back.\n console.warn(`[sync] snapshot ${pending.throughSeq} for doc ${docId} did not read back as stored; left unverified`)\n snapshotRetryWanted = true\n scheduleAutomaticCompaction()\n return\n }\n send({ type: 'snapshot_verified', docId, generation: pending.generation, throughSeq: pending.throughSeq })\n }\n\n return {\n doc,\n awareness,\n async receivePresence(envelope: string) {\n try {\n const { plaintext } = await openSymmetric({\n keyForEpoch: (id) => keyForEpoch(keyring, id),\n envelope: fromBase64Url(envelope),\n aad: presenceAad,\n })\n applyAwarenessUpdate(awareness, plaintext, REMOTE)\n } catch {\n // Presence is ephemeral. An unknown epoch is safely ignored.\n }\n },\n async compact() {\n await uploadSnapshot()\n },\n async ready() {\n const [cached, pending] = await Promise.all([persist.load(), persist.pending()])\n seeding = true\n try {\n if (cached) {\n Y.applyUpdate(doc, cached.update, CACHE_SEED)\n lastSeq = cached.lastSeq\n lastSyncedStateVector = cached.lastSyncedStateVector\n documentGeneration = cached.generation ?? generation\n lifecycleState = cached.lifecycle ?? 'active'\n for (const token of cached.dirtyTokens ?? []) recoveredDirtyTokens.add(token)\n }\n durableQueue = pending.sort(\n (a, b) => a.createdAt - b.createdAt || a.outboxId.localeCompare(b.outboxId),\n )\n } finally {\n seeding = false\n }\n // A cached document can be ahead of a replayed outbox operation. For example,\n // an edit made while the first operation is in flight is saved immediately but\n // cannot enter the outbox until that operation is acknowledged. Treating the\n // whole cached document as relay-known after restart would silently strand that\n // later edit. Without a session-local operation boundary, leave the snapshot\n // unknown: the acknowledged state vector will still suppress known structs, and\n // at worst a deletion set is sent once more before the new boundary is captured.\n const hasPendingContent = durableQueue.some((operation) => operation.kind !== 'delete')\n if (hasPendingContent || recoveredDirtyTokens.size > 0) {\n lastSyncedSnapshot = undefined\n } else if (localEditsBeforeSeed) {\n // A local edit can also land while the cache read is in flight. Rebuild the\n // baseline from cached bytes so that live-only work remains drainable.\n const baseline = new Y.Doc()\n if (cached) Y.applyUpdate(baseline, cached.update)\n lastSyncedSnapshot = Y.snapshot(baseline)\n baseline.destroy()\n } else {\n lastSyncedSnapshot = Y.snapshot(doc)\n }\n if (lifecycleState === 'seeding') lifecycleState = 'active'\n },\n async receive(message) {\n switch (message.type) {\n case 'ack': {\n const operation = durableQueue[0]\n if (!operation || operation.outboxId !== message.outboxId) return\n let committed: boolean\n if (operation.kind === 'delete') {\n committed = await persistTask(() =>\n persist.commitDeleteAcknowledgement(message.outboxId, message.generation),\n )\n } else {\n // An ack proves that this local operation occupies `message.seq`; it\n // does not prove that this tab has applied every earlier relay update.\n // Only move the catch-up watermark when the ack is the next contiguous\n // sequence. Otherwise catch up from the last sequence actually known.\n const acknowledgedSeq =\n message.seq === lastSeq + 1 ? message.seq : lastSeq\n const acknowledgedStateVector = mergeStateVectors(\n lastSyncedStateVector,\n operation.stateVector,\n )\n committed = await persistTask(() =>\n persist.commitAcknowledgement(\n message.outboxId,\n message.generation,\n acknowledgedSeq,\n Y.encodeStateAsUpdate(doc),\n acknowledgedStateVector,\n ),\n )\n }\n if (!committed) return\n documentGeneration = message.generation\n // The relay now holds everything this operation's diff represented,\n // including its delete set. A replayed outbox operation has no captured\n // boundary; leaving the last snapshot unchanged is conservative and\n // costs at most one redundant, idempotent append.\n const boundary = boundarySnapshots.get(message.outboxId)\n boundarySnapshots.delete(message.outboxId)\n if (boundary && operation.kind !== 'delete') lastSyncedSnapshot = boundary\n if (operation.kind === 'delete') {\n lastSeq = 0\n lastSyncedStateVector = Y.encodeStateVector(new Y.Doc())\n lifecycleState = 'deleted'\n } else {\n if (message.seq === lastSeq + 1) {\n lastSeq = message.seq\n } else if (message.seq > lastSeq + 1) {\n syncHealth = 'sequence-gap'\n requestCatchup(lastSeq)\n }\n lastSyncedStateVector = mergeStateVectors(\n lastSyncedStateVector,\n operation.stateVector,\n )\n lifecycleState = 'active'\n updatesSinceSnapshot += 1\n bytesSinceSnapshot += fromBase64Url(operation.envelope).byteLength\n scheduleAutomaticCompaction()\n }\n durableQueue.shift()\n send({ type: 'ack_confirm', outboxId: message.outboxId })\n if (durableQueue[0]) await transmit(durableQueue[0])\n else if (operation.kind !== 'delete') await requestDrain()\n break\n }\n case 'update': {\n const beforeUpdate = indexMutationVersion\n if (\n await applyEnvelope(\n message.envelope,\n message.seq,\n message.generation,\n )\n ) {\n await saveCurrent({\n indexContentChanged: indexMutationVersion !== beforeUpdate,\n })\n }\n break\n }\n case 'catchup_batch': {\n try {\n const beforeBatch = indexMutationVersion\n if (message.generation < documentGeneration) break\n if (message.state === 'deleted') {\n documentGeneration = message.generation\n lifecycleState = 'deleted'\n lastSeq = 0\n seeding = true\n try {\n const content = doc.getText('content')\n if (content.length > 0) {\n doc.transact(() => content.delete(0, content.length), SUPPRESSED)\n }\n } finally {\n seeding = false\n }\n await persist.purge()\n durableQueue = []\n boundarySnapshots.clear()\n recoveredDirtyTokens.clear()\n activeDirtyToken = undefined\n // The relay declared this generation deleted; the emptied document\n // is the shared baseline. Without this, the delete-set left by the\n // content clear reads as unsent local work and a reconnect drain\n // would resurrect the document.\n lastSyncedSnapshot = Y.snapshot(doc)\n futureUpdates.clear()\n completeCatchup()\n break\n }\n documentGeneration = message.generation\n lifecycleState = 'active'\n if (message.snapshot) {\n // A snapshot establishes its own contiguous watermark, and it\n // covers everything before it: an update held for a gap the\n // snapshot has closed is stale, and the tail counters start again.\n lastSeq = message.snapshot.throughSeq - 1\n for (const seq of [...futureUpdates.keys()]) {\n if (seq <= message.snapshot.throughSeq) futureUpdates.delete(seq)\n }\n await applyEnvelope(message.snapshot.envelope, message.snapshot.throughSeq)\n updatesSinceSnapshot = 0\n bytesSinceSnapshot = 0\n }\n for (const update of message.updates) {\n await applyEnvelope(update.envelope, update.seq)\n }\n await saveCurrent({\n indexContentChanged: indexMutationVersion !== beforeBatch,\n })\n if (message.hasMore) {\n firstCatchupPage.resolve()\n requestCatchup(lastSeq)\n } else {\n completeCatchup()\n }\n } catch (error) {\n throw failCatchup(error)\n }\n break\n }\n case 'snapshot_ack':\n onSnapshotAck(message)\n break\n case 'snapshot_data':\n await onSnapshotData(message)\n break\n default:\n break\n }\n },\n firstCatchupPage: () => firstCatchupPage.promise,\n caughtUp: () => catchupCompletion.promise,\n resync() {\n requestCatchup(lastSeq)\n if (durableQueue[0]) detached(transmit(durableQueue[0]))\n else detached(requestDrain())\n },\n async flush() {\n if (debounceTimer) {\n clearTimeout(debounceTimer)\n debounceTimer = undefined\n }\n await persistenceTail\n await requestDrain()\n if (lastPersistenceError) throw lastPersistenceError\n },\n async delete() {\n if (lifecycleState === 'deleted' || lifecycleState === 'deleting') return\n await persistenceTail\n lifecycleState = 'deleting'\n if (debounceTimer) clearTimeout(debounceTimer)\n const content = doc.getText('content')\n seeding = true\n try {\n if (content.length > 0) doc.transact(() => content.delete(0, content.length), SUPPRESSED)\n } finally {\n seeding = false\n }\n for (const operation of durableQueue.splice(0)) await persist.discard(operation.outboxId)\n boundarySnapshots.clear()\n // The cleared document is the baseline while the delete settles; only a real\n // edit (resurrection) should ever drain from here.\n lastSyncedSnapshot = Y.snapshot(doc)\n const operation: NewDeleteOperation = {\n outboxId: newOutboxId(),\n kind: 'delete',\n generation: documentGeneration,\n createdAt: now(),\n }\n const settledDirtyTokens = [...recoveredDirtyTokens]\n if (activeDirtyToken) settledDirtyTokens.push(activeDirtyToken)\n activeDirtyToken = undefined\n try {\n await persistTask(() =>\n persist.enqueue(operation, stateSnapshot(), { settledDirtyTokens }),\n )\n } catch (error) {\n for (const token of settledDirtyTokens) recoveredDirtyTokens.add(token)\n throw error\n }\n for (const token of settledDirtyTokens) recoveredDirtyTokens.delete(token)\n durableQueue.push({\n graphId,\n docId,\n ...operation,\n attemptCount: 0,\n lastAttemptAt: null,\n })\n await transmit(durableQueue[0])\n },\n lifecycle: () => lifecycleState,\n health: () => syncHealth,\n async staleGeneration(currentGeneration) {\n syncHealth = 'generation-stale'\n for (const operation of durableQueue.splice(0)) await persist.discard(operation.outboxId)\n boundarySnapshots.clear()\n documentGeneration = currentGeneration\n lastSeq = 0\n requestCatchup(0)\n },\n async advertisePresence() {\n if (destroyed || awareness.getLocalState() === null) return\n await sendAwareness([doc.clientID])\n },\n async clearPresence() {\n if (destroyed || awareness.getLocalState() === null) return\n suppressAutomaticAwareness = true\n try {\n awareness.setLocalState(null)\n } finally {\n suppressAutomaticAwareness = false\n }\n await sendAwareness([doc.clientID])\n },\n isIdle: () => durableQueue.length === 0 && !debounceTimer && !drainPromise,\n destroy() {\n destroyed = true\n boundarySnapshots.clear()\n // A background persisted-index catch-up may be waiting while the browser is\n // offline. Ending the engine also ends that wait, so workspace disposal cannot\n // retain a detached graph session indefinitely.\n firstCatchupPage.resolve()\n completeCatchup()\n if (debounceTimer) clearTimeout(debounceTimer)\n if (compactionTimer) clearTimeout(compactionTimer)\n doc.off('update', onUpdate)\n awareness.off('update', onAwareness)\n removeAwarenessStates(awareness, [doc.clientID], REMOTE)\n awareness.destroy()\n doc.destroy()\n },\n }\n}\n","/**\n * Application-facing sync messages derived from the shared wire contract. The `v` field is\n * added and checked only at the serialisation boundary so the document engine does not repeat\n * a transport constant on every operation.\n */\nimport {\n SYNC_PROTOCOL_VERSION,\n parseServerMessage as parseWireServerMessage,\n serializeClientMessage as serializeWireClientMessage,\n type SyncClientMessage,\n type SyncServerMessage,\n} from '@appsoftwareltd/etherpk-shared'\n\ntype WithoutVersion<T> = T extends { v: typeof SYNC_PROTOCOL_VERSION } ? Omit<T, 'v'> : never\n\nexport type RelayClientMessage = WithoutVersion<SyncClientMessage>\nexport type RelayServerMessage = WithoutVersion<SyncServerMessage>\nexport type RelayUpdate = Extract<RelayServerMessage, { type: 'catchup_batch' }>['updates'][number]\n\n/** Returns null for anything malformed, oversized or from an unsupported protocol version. */\nexport function parseServerMessage(raw: string): RelayServerMessage | null {\n const result = parseWireServerMessage(raw)\n if (!result.ok) return null\n const { v: _version, ...message } = result.value\n return message as RelayServerMessage\n}\n\nexport function serializeClientMessage(message: RelayClientMessage): string {\n return serializeWireClientMessage({ v: SYNC_PROTOCOL_VERSION, ...message } as SyncClientMessage)\n}\n","/**\n * Who a caret belongs to. y-codemirror renders every remote cursor from the awareness\n * `user` field — nothing in the app ever SET that field, so every collaborator was an\n * identical default-blue \"Anonymous\" bar (live, 2026-07-28: read as \"presence is broken\").\n *\n * The sync layer has no account identity to draw on (the client authenticates with a PAT;\n * the portal session lives on another origin), so the identity is per-device: a friendly\n * pseudonym and a stable colour, minted once and kept in localStorage. Replacing this with\n * the real account name when an identity endpoint exists only means changing `mint()`.\n *\n * The colour here is only this device's PREFERENCE. Tabs share localStorage, and eight\n * palette entries collide across devices, so the per-session allocator in\n * presence-session.ts owns the colour actually advertised: it keeps the preference when it\n * is free and deterministically moves to a free palette entry when an earlier live session\n * in the same graph already shows it.\n */\n\nconst KEY = 'etherpk.presence-identity'\n\nexport interface PresenceIdentity {\n name: string\n color: string\n /** Selection-highlight tint; y-codemirror falls back to color + '33' without one. */\n colorLight: string\n}\n\n/**\n * Pastels: soft enough not to shout beside the prose, saturated enough that a 2px caret bar\n * still reads on a white surface, and light enough that dark label text works on all of them\n * in both themes. The selection tint is the same colour at 40% alpha (`66`) — the old 20%\n * tint of a primary was already faint; a pastel at 20% disappears on white.\n *\n * The same eight are offered as the ready-made toolbar colours in Graph Settings (ADR 0071):\n * one palette, so a graph's bar and a member's caret speak the same language. The label is\n * for a swatch there; presence itself never shows it (`label`, not `name`: an entry is\n * spread into a {@link PresenceIdentity}, whose `name` is the pseudonym).\n */\nexport const PRESENCE_PALETTE: ReadonlyArray<{ label: string; color: string; colorLight: string }> = [\n { label: 'Sky', color: '#7dd3fc', colorLight: '#7dd3fc66' },\n { label: 'Amber', color: '#fcd34d', colorLight: '#fcd34d66' },\n { label: 'Mint', color: '#6ee7b7', colorLight: '#6ee7b766' },\n { label: 'Salmon', color: '#fca5a5', colorLight: '#fca5a566' },\n { label: 'Lavender', color: '#c4b5fd', colorLight: '#c4b5fd66' },\n { label: 'Pink', color: '#f9a8d4', colorLight: '#f9a8d466' },\n { label: 'Peach', color: '#fdba74', colorLight: '#fdba7466' },\n { label: 'Lime', color: '#bef264', colorLight: '#bef26466' },\n]\n\nconst ADJECTIVES = ['Amber', 'Bold', 'Calm', 'Deft', 'Eager', 'Fleet', 'Keen', 'Quiet', 'Swift', 'Vivid']\nconst ANIMALS = ['Fox', 'Owl', 'Wren', 'Hare', 'Lynx', 'Newt', 'Ibis', 'Mole', 'Swan', 'Toad']\n\nfunction mint(): PresenceIdentity {\n const pick = (list: readonly string[]) => list[Math.floor(Math.random() * list.length)]\n const paletteEntry = PRESENCE_PALETTE[Math.floor(Math.random() * PRESENCE_PALETTE.length)]\n return { name: `${pick(ADJECTIVES)} ${pick(ANIMALS)}`, ...paletteEntry }\n}\n\n/** This device's presence identity, minted on first use and stable thereafter. */\nexport function presenceIdentity(): PresenceIdentity {\n try {\n const stored = localStorage.getItem(KEY)\n if (stored) {\n const parsed = JSON.parse(stored) as Partial<PresenceIdentity>\n if (typeof parsed.name === 'string' && typeof parsed.color === 'string' && typeof parsed.colorLight === 'string') {\n // Only a palette colour is advertised: the allocator in presence-session.ts can\n // only de-duplicate against palette entries, so a colour from an earlier palette\n // (or a hand-edited one) is re-minted while the name stays the device's.\n if (PRESENCE_PALETTE.some((entry) => entry.color === parsed.color)) return parsed as PresenceIdentity\n return remember({ ...mint(), name: parsed.name })\n }\n }\n } catch {\n // Unreadable storage or corrupt JSON — mint a fresh one below.\n }\n return remember(mint())\n}\n\nfunction remember(identity: PresenceIdentity): PresenceIdentity {\n try {\n localStorage.setItem(KEY, JSON.stringify(identity))\n } catch {\n // Private mode: a per-session identity is still better than \"Anonymous\".\n }\n return identity\n}\n","/**\n * One live client session's presence identity, applied to every retained document's\n * awareness (live, 2026-07-30: four collaborators all rendered blue, and a client could\n * vanish from its peers entirely).\n *\n * Two separate faults are owned here:\n *\n * - Colour uniqueness. The stored device identity is only a preference — same-profile tabs\n * share it, and independent eight-entry random picks collide across devices. Each session\n * advertises `since` (arrival time) and `seat` (a random total-order tiebreak) alongside\n * its colour. When a session sees its own colour on a peer that arrived earlier, the later\n * session deterministically moves to the first palette colour no visible collaborator is\n * using. Earlier sessions never move, so colours are stable within a session, and the rule\n * converges because movement always goes to a free colour under a total order. With more\n * visible collaborators than palette entries, uniqueness is impossible; the session then\n * keeps its colour rather than flap.\n *\n * - Advertisement liveness. y-protocols' `setLocalStateField` is a NO-OP once the local\n * state has been cleared (`setLocalState(null)` — exactly what releasing a document does),\n * and the awareness renewal timer also skips null states. Re-applying identity through\n * `setLocalState` resurrects a cleared state, so a re-retained document advertises again\n * instead of leaving this client permanently invisible.\n */\nimport type { Awareness } from 'y-protocols/awareness'\nimport { PRESENCE_PALETTE, type PresenceIdentity } from './presence-identity'\n\n/** The awareness `user` field this session advertises. y-codemirror reads name/colour. */\nexport interface PresenceUser extends PresenceIdentity {\n /** Session arrival time; on a colour collision the LATER session moves. */\n since: number\n /** Random total-order tiebreak for sessions arriving in the same millisecond. */\n seat: string\n}\n\n/** What a peer's advertised `user` field may carry (absent fields ⇒ an older client). */\ninterface PeerUser {\n color?: unknown\n since?: unknown\n seat?: unknown\n}\n\nexport interface PresenceSession {\n /** The identity currently advertised (colour may differ from the preference). */\n user(): PresenceUser\n /**\n * Apply this session's identity to a document's awareness and keep its colour\n * collision-free against that document's visible collaborators. Returns a detach\n * which stops observing; it does NOT clear the awareness state (the doc engine's\n * clearPresence owns the tombstone).\n */\n attach(awareness: Awareness): () => void\n}\n\nexport function createPresenceSession(\n identity: PresenceIdentity,\n options: { now?: () => number; seat?: string } = {},\n): PresenceSession {\n const since = (options.now ?? Date.now)()\n const seat = options.seat ?? crypto.randomUUID()\n let color = identity.color\n let colorLight = identity.colorLight ?? `${identity.color}33`\n const attached = new Set<Awareness>()\n let evaluateQueued = false\n\n const user = (): PresenceUser => ({ name: identity.name, color, colorLight, since, seat })\n\n function apply(awareness: Awareness): void {\n // Never setLocalStateField: it silently no-ops on a cleared (null) state, which is\n // precisely the state a released-then-re-retained document is in.\n const state = awareness.getLocalState()\n awareness.setLocalState({ ...(state ?? {}), user: user() })\n }\n\n /** A peer arrived earlier when its (since, seat) sorts below ours; it keeps the colour. */\n function outranks(peer: PeerUser): boolean {\n const peerSince = typeof peer.since === 'number' ? peer.since : Number.NEGATIVE_INFINITY\n if (peerSince !== since) return peerSince < since\n const peerSeat = typeof peer.seat === 'string' ? peer.seat : ''\n return peerSeat < seat\n }\n\n function visiblePeers(): PeerUser[] {\n const peers: PeerUser[] = []\n for (const awareness of attached) {\n for (const [clientId, state] of awareness.getStates()) {\n if (clientId === awareness.clientID) continue\n const peer = (state as { user?: PeerUser }).user\n if (peer && typeof peer.color === 'string') peers.push(peer)\n }\n }\n return peers\n }\n\n function evaluate(): void {\n const peers = visiblePeers()\n const collision = peers.some((peer) => peer.color === color && outranks(peer))\n if (!collision) return\n const inUse = new Set(peers.map((peer) => peer.color))\n const free = PRESENCE_PALETTE.find((entry) => !inUse.has(entry.color))\n // Every palette entry visible ⇒ uniqueness is impossible; keep the colour steady.\n if (!free || free.color === color) return\n color = free.color\n colorLight = free.colorLight\n for (const awareness of attached) {\n if (awareness.getLocalState() !== null) apply(awareness)\n }\n }\n\n function scheduleEvaluate(): void {\n if (evaluateQueued) return\n evaluateQueued = true\n queueMicrotask(() => {\n evaluateQueued = false\n evaluate()\n })\n }\n\n return {\n user,\n attach(awareness) {\n attached.add(awareness)\n apply(awareness)\n const onChange = () => scheduleEvaluate()\n awareness.on('change', onChange)\n scheduleEvaluate()\n return () => {\n awareness.off('change', onChange)\n attached.delete(awareness)\n }\n },\n }\n}\n","/**\n * Graph-level sync (plan Phase 3 Task 5): one WebSocket per open graph, multiplexing all\n * documents. Owns per-doc engines (created lazily), the reconnect loop, and the graph-root\n * document — a hidden Y.Doc whose `registry` Y.Map (docId → {kind,title?,date?}) and `meta`\n * Y.Map (graph name, settings) are how a blind server's clients discover the doc set.\n *\n * The transport is injected (a `connect(url)` factory) so this unit-tests without real sockets.\n */\nimport * as Y from 'yjs'\nimport type { GraphKeyring } from '$lib/crypto'\nimport { performanceRecorder } from '$lib/diagnostics/performance'\nimport { SYNC_PROTOCOL_LIMITS } from '@appsoftwareltd/etherpk-shared'\nimport { CACHE_SEED, createDocSync, type DocSync } from './doc-sync'\nimport type { GraphCache } from './local-cache'\nimport { parseServerMessage, serializeClientMessage, type RelayClientMessage } from './messages'\nimport { createPresenceSession, type PresenceSession } from './presence-session'\nimport type { SyncTokenSource } from './sync-token'\n\nexport interface TransportSocket {\n send(data: string): void\n close(): void\n onOpen(cb: () => void): void\n onMessage(cb: (data: string) => void): void\n onClose(cb: () => void): void\n}\n\nexport interface GraphSyncDeps {\n graphId: string\n rootDocId: string\n keyring: GraphKeyring\n relayUrl: string\n /**\n * Asked for a token on every connect, NOT captured once: a session outlives its\n * token's 15 minutes, and a reconnect carrying an expired one is rejected forever.\n */\n token: SyncTokenSource\n cache: GraphCache\n connect(url: string): TransportSocket\n debounceMs?: number\n /** Called whenever the registry Y.Map changes (membership of the doc set). */\n onRegistryChange?: () => void\n /** Passed to every document engine - see `DocSyncDeps.collectRowWhen`. */\n collectRowWhen?: (doc: Y.Doc) => boolean\n /**\n * This device's PREFERRED identity for the awareness `user` field — what y-codemirror\n * renders a remote caret's colour and name from. Absent ⇒ collaborators all show as\n * the library's default-blue \"Anonymous\". The per-session allocator owns the colour\n * actually advertised (presence-session.ts): the preference is kept when free and\n * deterministically moved when an earlier live session already shows it.\n */\n presence?: { name: string; color: string; colorLight?: string }\n /** Surfaces cache quota and transport failures to the owning workspace. */\n onError?: (error: Error) => void\n}\n\nexport interface RegistryEntry {\n kind: 'journal' | 'page'\n title?: string\n date?: string\n /**\n * Alternative names resolving to this document ([[Alias]]). A server document has no\n * frontmatter - identity lives only here (ADR 0024) - so this is where a rename's\n * \"keep the old name working\" arm puts the old name (ADR 0037).\n */\n aliases?: string[]\n}\n\n/**\n * The root doc's `meta` Y.Map: the [[Graph Name]] (identity metadata — one canonical,\n * any-member-writable name) and the shared [[Graph Settings]] object. Both are content:\n * encrypted like everything else, so the blind server never sees them.\n */\nexport interface GraphMeta {\n name?: string\n /** GraphSettings-shaped; consumers sanitize (storage/fs/graph-settings). */\n settings?: Record<string, unknown>\n}\n\nexport interface GraphSync {\n readonly rootDoc: Y.Doc\n docSync(docId: string): DocSync\n retainDoc(docId: string): () => void\n /** Queue an ordered, durable lifecycle delete before the encrypted registry changes. */\n deleteDoc(docId: string): Promise<void>\n /** Drop deleted content from a document's cache row - after protecting it (ADR 0057). */\n compactDoc(docId: string): Promise<void>\n /**\n * Upload a consolidated encrypted snapshot of a document to the relay now, rather than\n * waiting for the idle trigger; the engine reads it back and verifies it (ADR 0025).\n */\n uploadSnapshot(docId: string): Promise<void>\n registry(): Y.Map<RegistryEntry>\n /** Snapshot of the root doc's `meta` map (graph name + shared settings). */\n getMeta(): GraphMeta\n setMetaName(name: string): void\n setMetaSettings(settings: Record<string, unknown>): void\n /** Fires on any `meta` change (local or remote). Returns an unsubscribe. */\n onMetaChange(listener: () => void): () => void\n /** Resolves once the socket has opened (first connect). Short-lived flows need this\n * before flushing — a flush into a never-opened socket dies with dispose. */\n connected(): Promise<void>\n /** Resolves once the ROOT doc's current catch-up reaches its terminal relay page. */\n rootCaughtUp(): Promise<void>\n /** Fires whenever ANY document's content (Y.Doc) changes — local or remote. The docId is\n * the graph-root id for registry changes, else the content doc id. */\n onDocUpdate(listener: (docId: string) => void): () => void\n ready(): Promise<void>\n /**\n * Resolves when ONE doc's engine has seeded from the [[Local Cache]] (creating the\n * engine if needed). This is what lets an opened document show \"loading\" instead of\n * sitting silently empty while the background materialisation walk is still running.\n */\n whenReady(docId: string): Promise<void>\n /**\n * Hydrate a cold-index batch from the Local Cache without starting relay catch-up.\n * The caller must later promote stale documents through normal synchronized readiness.\n */\n seedDocsFromCache(docIds: readonly string[]): Promise<readonly Y.Doc[]>\n /** Seed only this batch, without re-awaiting every engine created earlier. */\n readyDocs(docIds: readonly string[]): Promise<void>\n /** Release background-only engines once their batch consumer has read them. */\n retireDocs(docIds: readonly string[]): void\n /** Resolves after ONE doc's first relay page is applied, for presentation readiness. */\n firstCatchupPageDoc(docId: string): Promise<void>\n /** Resolves at the terminal page of ONE doc's current catch-up cycle. */\n caughtUpDoc(docId: string): Promise<void>\n /**\n * Compare the Local Cache with one bounded relay metadata request per batch. Warm indexes\n * only materialise documents whose server watermark actually moved while this browser\n * was away.\n */\n docsNeedingCatchup(docIds: readonly string[]): Promise<string[]>\n /**\n * Cache rows which changed after the last explicit index commit. The acknowledgement\n * closure clears only the exact tokens captured by this call.\n */\n pendingIndexChanges(): Promise<GraphIndexCheckpoint>\n /** Whether the relay socket is open right now — gate anything that waits on the server. */\n isConnected(): boolean\n /**\n * Encrypt and send every pending update. On a large [[Import]] this is where the minutes\n * go, so it reports as each document's engine finishes - without it the Activity sat at\n * \"0 of 2401\" until the first acknowledgement arrived, looking hung.\n */\n flushAll(options?: { onProgress?: (flushed: number, total: number) => void }): Promise<void>\n /**\n * Wait until the server has acknowledged every operation in the durable outbox.\n *\n * `flushAll()` only gets bytes as far as `socket.send`; it never waited for the relay.\n * Anything that must not claim \"the server has it\" - an [[Import]] declaring itself\n * done, before the socket is disposed - has to wait here instead.\n *\n * Resolves `settled: false` when no ack has arrived for `stallMs`: the caller decides\n * what a stall means. It is NOT an error - unacked updates are already in the\n * durable outbox and replay on the next open, so a stall is a weaker claim, not a loss.\n */\n awaitAcked(options?: AwaitAckedOptions): Promise<AckResult>\n diagnostics(): { activeEngines: number; retainedDocuments: number }\n dispose(): void\n}\n\nexport interface GraphIndexCheckpoint {\n docIds: readonly string[]\n /** Root bytes merged from durable cache and this tab's live CRDT, for identity mapping. */\n rootUpdate: Uint8Array | null\n /**\n * Read content at least as new as the captured cache token. A dirty missing row is a\n * deletion tombstone and deliberately does not fall back to a stale live engine.\n */\n readDocument(docId: string): Promise<Uint8Array | null>\n acknowledge(): Promise<void>\n}\n\nexport interface AwaitAckedOptions {\n /** Called with the number of appends still outstanding, whenever that number falls. */\n onProgress?: (outstanding: number) => void\n signal?: AbortSignal\n /** Give up after this long with no ack at all. Default 30s. */\n stallMs?: number\n}\n\nexport interface AckResult {\n /** True when everything was acked; false when the wait gave up on a stall. */\n settled: boolean\n /** Appends still unacked (0 when `settled`). */\n outstanding: number\n}\n\n/** Reconnect after a dropped socket (simple; backoff tuned later). */\nconst RECONNECT_MS = 50\n/** Retry after a token mint failed - a server round trip, so slower than a reconnect. */\nconst TOKEN_RETRY_MS = 2_000\n\n/** Internal signal: a watermark request is safe to repeat on the next socket generation. */\nclass WatermarkConnectionInterruptedError extends Error {}\n\nexport function createGraphSync(deps: GraphSyncDeps): GraphSync {\n const engines = new Map<string, DocSync>()\n const retained = new Map<string, number>([[deps.rootDocId, 1]])\n const retiring = new Set<string>()\n const presenceSession: PresenceSession | undefined = deps.presence\n ? createPresenceSession({\n name: deps.presence.name,\n color: deps.presence.color,\n colorLight: deps.presence.colorLight ?? `${deps.presence.color}33`,\n })\n : undefined\n const presenceDetach = new Map<string, () => void>()\n const docUpdateListeners = new Set<(docId: string) => void>()\n // ack carries only outboxId (no docId); remember which doc issued each append so an\n // ack routes back to the right engine.\n const ackRoute = new Map<string, string>()\n let socket: TransportSocket | undefined\n let open = false\n let disposed = false\n const reportError = (error: unknown) => {\n if (!disposed) deps.onError?.(error instanceof Error ? error : new Error(String(error)))\n }\n const outboundSnapshots: string[] = []\n type CatchupRequest = Extract<RelayClientMessage, { type: 'catchup' }>\n const foregroundCatchups: CatchupRequest[] = []\n const backgroundCatchups: CatchupRequest[] = []\n let catchupInFlight: CatchupRequest | undefined\n let resolveFirstOpen: (() => void) | undefined\n const firstOpen = new Promise<void>((resolve) => {\n resolveFirstOpen = resolve\n })\n const connectionWaiters = new Set<{\n resolve: () => void\n reject: (error: Error) => void\n }>()\n type DocumentWatermark = {\n docId: string\n generation: number\n state: 'active' | 'deleted'\n lastSeq: number\n }\n const watermarkRequests = new Map<\n string,\n {\n resolve: (documents: DocumentWatermark[]) => void\n reject: (error: Error) => void\n timer: ReturnType<typeof setTimeout>\n }\n >()\n\n /**\n * `connected()` is deliberately a first-open milestone. Watermark reconciliation can\n * start hours later, so it needs the socket that is live NOW rather than that settled\n * historical promise.\n */\n function waitForCurrentConnection(): Promise<void> {\n if (open && socket) return Promise.resolve()\n if (disposed) return Promise.reject(new Error('the graph sync session was disposed'))\n return new Promise<void>((resolve, reject) => {\n connectionWaiters.add({ resolve, reject })\n })\n }\n\n async function requestWatermarks(docIds: readonly string[]): Promise<DocumentWatermark[]> {\n while (!disposed) {\n await waitForCurrentConnection()\n const activeSocket = socket\n // A close can land between the waiter resolving and this continuation running.\n if (!open || !activeSocket) continue\n\n try {\n return await new Promise<DocumentWatermark[]>((resolve, reject) => {\n const requestId = crypto.randomUUID()\n const timer = setTimeout(() => {\n watermarkRequests.delete(requestId)\n reject(new Error('the sync relay did not answer a watermark check'))\n }, 15_000)\n watermarkRequests.set(requestId, { resolve, reject, timer })\n activeSocket.send(\n serializeClientMessage({\n type: 'watermarks',\n requestId,\n docIds: [...docIds],\n }),\n )\n })\n } catch (error) {\n // The request is read-only and idempotent. A socket generation ending while\n // it is in flight means \"try on the reconnect\", not a failed reconciliation.\n if (error instanceof WatermarkConnectionInterruptedError && !disposed) continue\n throw error\n }\n }\n throw new Error('the graph sync session was disposed')\n }\n\n /**\n * Documents the CURRENT socket has subscribed to. The relay keeps and forwards presence\n * only for a subscribed document (ASTRA F11), and this mirrors that rule at the source:\n * y-protocols renews every engine's awareness state every fifteen seconds, and an engine\n * created for a background walk starts with an empty `{}` state, so without the gate every\n * unretained engine encrypted and sent presence the relay would only drop. Cleared with\n * the socket; the reconnect's subscribe rebuilds it before anything is advertised.\n */\n const subscribed = new Set<string>()\n\n function sendNow(message: RelayClientMessage): void {\n if (message.type === 'presence' && !subscribed.has(message.docId)) return\n const data = serializeClientMessage(message)\n if (open && socket) {\n socket.send(data)\n if (message.type === 'subscribe' && 'docIds' in message) for (const docId of message.docIds) subscribed.add(docId)\n if (message.type === 'unsubscribe') for (const docId of message.docIds) subscribed.delete(docId)\n } else if (message.type === 'snapshot_put') outboundSnapshots.push(data)\n // Appends are already durable and replay on open. Catch-up is regenerated from the\n // current watermark. Presence is ephemeral, and a lost ack confirmation merely\n // leaves a harmless server receipt, so none belongs in a volatile socket queue.\n }\n\n /**\n * Subscribe to every retained document on a freshly opened socket, in batches the relay\n * accepts. One message used to carry the whole set, and above the protocol's per-message\n * limit the relay rejects it as invalid, which left a tab with many open views silently\n * without live updates for anything after a reconnect (ASTRA F11).\n */\n function subscribeRetained(): void {\n const docIds = [...retained.keys()]\n const batchSize = SYNC_PROTOCOL_LIMITS.maxSubscriptionDocIds\n for (let start = 0; start < docIds.length; start += batchSize) {\n sendNow({ type: 'subscribe', docIds: docIds.slice(start, start + batchSize) })\n }\n }\n\n function pumpCatchups(): void {\n if (!open || !socket || catchupInFlight) return\n catchupInFlight = foregroundCatchups.shift() ?? backgroundCatchups.shift()\n if (catchupInFlight) socket.send(serializeClientMessage(catchupInFlight))\n }\n\n function queueCatchup(message: CatchupRequest): void {\n if (!open) return\n if (\n catchupInFlight?.docId === message.docId &&\n catchupInFlight.generation === message.generation &&\n catchupInFlight.afterSeq === message.afterSeq\n ) {\n return\n }\n // A foreground open supersedes an unsent background request for the same document.\n const removeQueued = (queue: CatchupRequest[]) => {\n const index = queue.findIndex((candidate) => candidate.docId === message.docId)\n if (index >= 0) queue.splice(index, 1)\n }\n removeQueued(foregroundCatchups)\n removeQueued(backgroundCatchups)\n const queue = message.priority === 'foreground' ? foregroundCatchups : backgroundCatchups\n queue.push(message)\n pumpCatchups()\n }\n\n function finishCatchup(requestId?: string): void {\n if (!requestId || catchupInFlight?.requestId !== requestId) return\n const completedDocId = catchupInFlight.docId\n catchupInFlight = undefined\n pumpCatchups()\n retireEngineIfIdle(completedDocId)\n }\n\n function rawSend(message: RelayClientMessage): void {\n if (message.type === 'catchup') {\n queueCatchup(message)\n return\n }\n sendNow(message)\n }\n\n // Cache seeding and relay synchronization are separate operations. A cold index needs\n // bounded, cache-only batches; foreground documents and post-commit reconciliation\n // explicitly promote an engine into synchronization.\n const readied = new Map<string, Promise<void>>()\n const syncEnabled = new Set<string>()\n\n function activate(docId: string, e: DocSync): void {\n readied.set(docId, e.ready())\n }\n\n function enableSync(docId: string, e: DocSync): void {\n if (syncEnabled.has(docId)) return\n syncEnabled.add(docId)\n // A lazily-created engine, including one first used by a cache-only index walk,\n // must catch up when a real consumer promotes it. Keep the cache failure visible\n // through `readied`; this detached branch only reports it without an unhandled\n // rejection while no caller is awaiting yet.\n void (readied.get(docId) ?? Promise.resolve())\n .then(() => {\n if (!disposed && open && engines.get(docId) === e) e.resync()\n })\n .catch(reportError)\n }\n\n /**\n * Identity is applied on RETENTION, not engine creation: background index and\n * materialisation walks create engines for documents nobody is editing, and an engine\n * can outlive a release/retain cycle. The session's apply also resurrects a state\n * cleared by clearPresence — setLocalStateField would silently no-op on it, leaving\n * this client permanently invisible after re-opening a document (live, 2026-07-30).\n */\n function attachPresence(docId: string, e: DocSync): void {\n if (!presenceSession || docId === deps.rootDocId || presenceDetach.has(docId)) return\n presenceDetach.set(docId, presenceSession.attach(e.awareness))\n }\n\n function detachPresence(docId: string): void {\n presenceDetach.get(docId)?.()\n presenceDetach.delete(docId)\n }\n\n function retireEngineIfIdle(docId: string): void {\n if (docId === deps.rootDocId || retained.has(docId) || !retiring.has(docId)) return\n if (\n catchupInFlight?.docId === docId ||\n foregroundCatchups.some((request) => request.docId === docId) ||\n backgroundCatchups.some((request) => request.docId === docId)\n ) {\n return\n }\n const candidate = engines.get(docId)\n if (!candidate || !candidate.isIdle()) return\n retiring.delete(docId)\n detachPresence(docId)\n candidate.destroy()\n engines.delete(docId)\n readied.delete(docId)\n syncEnabled.delete(docId)\n performanceRecorder.mark('sync.engine.count', {\n active: engines.size,\n subscriptions: retained.size,\n })\n }\n\n function engine(docId: string, synchronize = true): DocSync {\n let e = engines.get(docId)\n if (!e) {\n e = createDocSync({\n docId,\n graphId: deps.graphId,\n keyring: deps.keyring,\n send: (message) => {\n if (\n message.type === 'append' ||\n message.type === 'delete' ||\n message.type === 'resurrect'\n ) {\n ackRoute.set(message.outboxId, docId)\n }\n rawSend(message)\n },\n persist: deps.cache.docCache(\n docId,\n docId === deps.rootDocId ? 'root' : 'document',\n ),\n debounceMs: deps.debounceMs,\n collectRowWhen: docId === deps.rootDocId ? undefined : deps.collectRowWhen,\n // The disposed-guarded reporter, not the raw callback: an engine's late\n // rejection after dispose must not reach the next workspace's notice.\n onError: reportError,\n onIdle: () => queueMicrotask(() => retireEngineIfIdle(docId)),\n catchupPriority: () => {\n return retained.has(docId) ? 'foreground' : 'background'\n },\n })\n engines.set(docId, e)\n performanceRecorder.mark('sync.engine.count', {\n active: engines.size,\n subscriptions: retained.size,\n })\n // Name this device's caret before any cursor is advertised, so a collaborator\n // never sees the default-blue \"Anonymous\" flash into a named one.\n if (retained.has(docId)) attachPresence(docId, e)\n // Any content change (local edit or remote update) notifies onDocUpdate, so the\n // store's onChange (index, mirror) fires on edits, not only on registry changes.\n e.doc.on('update', (_update, origin) => {\n // Rehydrating the already-indexed IndexedDB snapshot on every hard refresh\n // is not a content change. Genuine relay updates use REMOTE and still flow.\n if (origin === CACHE_SEED) return\n for (const l of docUpdateListeners) l(docId)\n })\n activate(docId, e)\n }\n if (synchronize) enableSync(docId, e)\n return e\n }\n\n async function indexDocumentUpdate(\n docId: string,\n missingCacheMeansDeleted: boolean,\n ): Promise<Uint8Array | null> {\n const cached = await deps.cache.docCache(docId).load()\n if (cached?.lifecycle === 'deleted' || (!cached && missingCacheMeansDeleted)) {\n return null\n }\n const live = engines.get(docId)\n if (!cached && !live) return null\n\n // A tab can be older than a cache boundary written by a peer, while a local edit can\n // be newer than its own asynchronous cache save. CRDT-merging both views gives the\n // index a snapshot at least as new as the captured token without creating a relay\n // subscription solely to read derived data.\n const merged = new Y.Doc()\n try {\n if (cached) Y.applyUpdate(merged, cached.update, CACHE_SEED)\n if (live) Y.applyUpdate(merged, Y.encodeStateAsUpdate(live.doc), CACHE_SEED)\n return Y.encodeStateAsUpdate(merged)\n } finally {\n merged.destroy()\n }\n }\n\n // The root document is just another synced doc.\n const root = engine(deps.rootDocId)\n const registryMap = root.doc.getMap<RegistryEntry>('registry')\n if (deps.onRegistryChange) registryMap.observe(() => deps.onRegistryChange?.())\n const metaMap = root.doc.getMap<unknown>('meta')\n\n function handleMessage(raw: string): void {\n const message = parseServerMessage(raw)\n if (!message) return\n if (message.type === 'catchup_batch') {\n performanceRecorder.mark('sync.catchup.batch', {\n rows: message.updates.length,\n bytes: new TextEncoder().encode(raw).byteLength,\n continuation: message.hasMore ? 1 : 0,\n })\n }\n if (message.type === 'error') {\n if (\n message.code === 'stale_generation' &&\n message.docId &&\n message.currentGeneration !== undefined\n ) {\n if (catchupInFlight?.docId === message.docId) {\n catchupInFlight = undefined\n }\n void engine(message.docId).staleGeneration(message.currentGeneration)\n pumpCatchups()\n }\n return\n }\n if (message.type === 'watermarks') {\n const pending = watermarkRequests.get(message.requestId)\n if (!pending) return\n clearTimeout(pending.timer)\n watermarkRequests.delete(message.requestId)\n pending.resolve(message.documents)\n return\n }\n const report = reportError\n if (message.type === 'presence') {\n if (!retained.has(message.docId)) return\n void engine(message.docId).receivePresence(message.envelope).catch(report)\n return\n }\n if (message.type === 'ack') {\n const docId = ackRoute.get(message.outboxId)\n if (docId) {\n void engine(docId)\n .receive(message)\n .then(() => ackRoute.delete(message.outboxId))\n .catch(report)\n }\n return\n }\n if (message.type === 'catchup_batch') {\n void engine(message.docId)\n .receive(message)\n .catch(report)\n .finally(() => finishCatchup(message.requestId))\n return\n }\n void engine(message.docId).receive(message).catch(report)\n }\n\n /**\n * A token is fetched for EVERY connect, never captured: the source re-mints when the\n * held token nears expiry, so a socket that drops an hour in still reconnects. Sends\n * Encrypted appends issued while the token is in flight remain in IndexedDB and replay\n * on open. Only rebuildable snapshot uploads use a volatile queue.\n */\n function connect(): void {\n if (disposed) return\n void deps.token().then(\n (token) => {\n if (disposed) return\n const s = deps.connect(`${deps.relayUrl}?token=${encodeURIComponent(token)}`)\n socket = s\n s.onOpen(() => {\n if (disposed || socket !== s) return\n open = true\n resolveFirstOpen?.()\n for (const waiter of connectionWaiters) waiter.resolve()\n connectionWaiters.clear()\n subscribeRetained()\n for (const data of outboundSnapshots.splice(0)) s.send(data)\n // A local awareness state set while the socket was closed was dropped\n // (presence never queues). Advertise every retained document now so the\n // relay can store the envelope and replay it to late subscribers.\n for (const docId of retained.keys()) {\n if (docId === deps.rootDocId) continue\n const e = engines.get(docId)\n if (e) void e.advertisePresence().catch(reportError)\n }\n // Resync only engines promoted into synchronization. Cache-only cold-index\n // batches must not turn one quick derivation into thousands of hidden\n // downloads before post-commit reconciliation acquires its graph lock.\n for (const [docId, e] of engines) {\n if (!syncEnabled.has(docId)) continue\n void (readied.get(docId) ?? Promise.resolve())\n .then(() => e.resync())\n .catch(reportError)\n }\n })\n s.onMessage(handleMessage)\n s.onClose(() => {\n if (socket !== s) return\n open = false\n socket = undefined\n // Routes and subscriptions belong to one socket generation. Durable\n // IndexedDB rows, not these entries, determine outstanding work and are\n // replayed below; the next open resubscribes every retained document.\n subscribed.clear()\n ackRoute.clear()\n catchupInFlight = undefined\n foregroundCatchups.length = 0\n backgroundCatchups.length = 0\n for (const pending of watermarkRequests.values()) {\n clearTimeout(pending.timer)\n pending.reject(new WatermarkConnectionInterruptedError())\n }\n watermarkRequests.clear()\n if (!disposed) setTimeout(connect, RECONNECT_MS)\n })\n },\n // No token, no socket - normally offline, or a server that is down. Back off\n // further than a dropped socket does: minting is a server round trip.\n () => {\n if (!disposed) setTimeout(connect, TOKEN_RETRY_MS)\n },\n )\n }\n connect()\n\n return {\n rootDoc: root.doc,\n docSync: (docId) => engine(docId),\n retainDoc(docId) {\n const count = retained.get(docId) ?? 0\n retained.set(docId, count + 1)\n retiring.delete(docId)\n performanceRecorder.mark('sync.subscription.count', {\n subscriptions: retained.size,\n active: engines.size,\n })\n const retainedEngine = engine(docId)\n // The engine may predate retention (background index walk) or have been\n // released and re-retained — both must (re)apply the session identity here.\n attachPresence(docId, retainedEngine)\n if (count === 0 && open) {\n rawSend({ type: 'subscribe', docIds: [docId] })\n // The relay stores the latest presence envelope per subscriber; advertising\n // right after subscribe makes this client immediately visible to the room\n // and replayable to whoever subscribes next.\n void retainedEngine.advertisePresence().catch(reportError)\n void (readied.get(docId) ?? Promise.resolve()).then(() => retainedEngine.resync())\n }\n let released = false\n return () => {\n if (released || docId === deps.rootDocId) return\n released = true\n const next = (retained.get(docId) ?? 1) - 1\n if (next > 0) {\n retained.set(docId, next)\n return\n }\n retained.delete(docId)\n retiring.add(docId)\n performanceRecorder.mark('sync.subscription.count', {\n subscriptions: retained.size,\n active: engines.size,\n })\n // Awareness removal is an encrypted, ephemeral message. Preserve its wire\n // ordering before unsubscribe so peers do not retain a stale collaborator.\n detachPresence(docId)\n void retainedEngine\n .clearPresence()\n .catch(reportError)\n .finally(() => {\n if (disposed) return\n if (retained.has(docId)) {\n // Re-retained while the tombstone was in flight: re-apply the\n // session identity (this resurrects the cleared state) and\n // re-advertise, or this client stays invisible in this doc.\n attachPresence(docId, retainedEngine)\n if (open) void retainedEngine.advertisePresence().catch(reportError)\n return\n }\n rawSend({ type: 'unsubscribe', docIds: [docId] })\n retireEngineIfIdle(docId)\n })\n }\n },\n deleteDoc: (docId) => engine(docId).delete(),\n compactDoc: (docId) => deps.cache.docCache(docId).compact(),\n uploadSnapshot: (docId) => engine(docId).compact(),\n registry: () => registryMap,\n getMeta() {\n const name = metaMap.get('name')\n const settings = metaMap.get('settings')\n return {\n ...(typeof name === 'string' && name !== '' ? { name } : {}),\n ...(typeof settings === 'object' && settings !== null ? { settings: settings as Record<string, unknown> } : {}),\n }\n },\n setMetaName(name) {\n metaMap.set('name', name)\n },\n setMetaSettings(settings) {\n metaMap.set('settings', settings)\n },\n onMetaChange(listener) {\n metaMap.observe(listener)\n return () => metaMap.unobserve(listener)\n },\n connected: () => firstOpen,\n rootCaughtUp: () => root.caughtUp(),\n onDocUpdate(listener) {\n docUpdateListeners.add(listener)\n return () => docUpdateListeners.delete(listener)\n },\n async ready() {\n // Durable operations must replay even when their document is not otherwise\n // opened during this session.\n for (const docId of await deps.cache.pendingDocIds()) engine(docId)\n await Promise.all([...readied.values()])\n },\n async whenReady(docId) {\n engine(docId) // creating on demand also queues its cache read NOW, not at walk order\n await readied.get(docId)\n },\n async seedDocsFromCache(docIds) {\n const batch = docIds.map((docId) => engine(docId, false))\n await Promise.all(\n docIds.map((docId) => readied.get(docId) ?? Promise.resolve()),\n )\n return batch.map((entry) => entry.doc)\n },\n async readyDocs(docIds) {\n for (const docId of docIds) engine(docId)\n await Promise.all(\n docIds.map((docId) => readied.get(docId) ?? Promise.resolve()),\n )\n },\n retireDocs(docIds) {\n for (const docId of docIds) {\n if (docId === deps.rootDocId || retained.has(docId)) continue\n retiring.add(docId)\n retireEngineIfIdle(docId)\n }\n },\n firstCatchupPageDoc: (docId) => engine(docId).firstCatchupPage(),\n caughtUpDoc: (docId) => engine(docId).caughtUp(),\n async docsNeedingCatchup(docIds) {\n const changed: string[] = []\n for (\n let start = 0;\n start < docIds.length;\n start += SYNC_PROTOCOL_LIMITS.maxSubscriptionDocIds\n ) {\n const batch = docIds.slice(\n start,\n start + SYNC_PROTOCOL_LIMITS.maxSubscriptionDocIds,\n )\n const local = await deps.cache.watermarks(batch)\n const remote = await requestWatermarks(batch)\n for (const watermark of remote) {\n const cached = local.get(watermark.docId)\n if (\n (!cached &&\n (watermark.generation !== 1 ||\n watermark.state !== 'active' ||\n watermark.lastSeq !== 0)) ||\n (cached &&\n (cached.generation !== watermark.generation ||\n cached.lifecycle !== watermark.state ||\n cached.lastSeq !== watermark.lastSeq))\n ) {\n changed.push(watermark.docId)\n }\n }\n }\n return changed\n },\n async pendingIndexChanges() {\n const changes = await deps.cache.pendingIndexChanges()\n const dirtyDocIds = new Set(changes.map((change) => change.docId))\n return {\n docIds: changes.map((change) => change.docId),\n rootUpdate: await indexDocumentUpdate(\n deps.rootDocId,\n dirtyDocIds.has(deps.rootDocId),\n ),\n readDocument: (docId) =>\n indexDocumentUpdate(docId, dirtyDocIds.has(docId)),\n acknowledge: () => deps.cache.acknowledgeIndexChanges(changes),\n }\n },\n isConnected: () => open,\n async flushAll({ onProgress } = {}) {\n const all = [...engines.values()]\n let flushed = 0\n await Promise.all(all.map((e) => e.flush().then(() => onProgress?.(++flushed, all.length))))\n },\n async awaitAcked({ onProgress, signal, stallMs = 30_000 } = {}) {\n const initial = await deps.cache.countPending()\n if (initial === 0) return { settled: true, outstanding: 0 }\n onProgress?.(initial)\n\n return new Promise<AckResult>((resolve) => {\n let last = initial\n let lastProgressAt = Date.now()\n const POLL_MS = 100\n let timer: ReturnType<typeof setTimeout> | undefined\n const finish = (result: AckResult) => {\n if (timer) clearTimeout(timer)\n resolve(result)\n }\n const poll = async () => {\n try {\n const current = await deps.cache.countPending()\n if (current < last) {\n last = current\n lastProgressAt = Date.now()\n onProgress?.(current)\n }\n if (current === 0) return finish({ settled: true, outstanding: 0 })\n if (signal?.aborted) return finish({ settled: false, outstanding: current })\n if (Date.now() - lastProgressAt >= stallMs) {\n return finish({ settled: false, outstanding: current })\n }\n timer = setTimeout(poll, POLL_MS)\n } catch (error) {\n reportError(error)\n finish({ settled: false, outstanding: last })\n }\n }\n timer = setTimeout(poll, POLL_MS)\n })\n },\n diagnostics: () => ({\n activeEngines: engines.size,\n retainedDocuments: retained.size,\n }),\n dispose() {\n disposed = true\n socket?.close()\n for (const detach of presenceDetach.values()) detach()\n presenceDetach.clear()\n for (const e of engines.values()) e.destroy()\n engines.clear()\n retiring.clear()\n syncEnabled.clear()\n foregroundCatchups.length = 0\n backgroundCatchups.length = 0\n catchupInFlight = undefined\n for (const pending of watermarkRequests.values()) {\n clearTimeout(pending.timer)\n pending.reject(new Error('the graph sync session was disposed'))\n }\n watermarkRequests.clear()\n for (const waiter of connectionWaiters) {\n waiter.reject(new Error('the graph sync session was disposed'))\n }\n connectionWaiters.clear()\n },\n }\n}\n\n/** The real browser WebSocket adapter (not used in unit tests). */\nexport function browserTransport(url: string): TransportSocket {\n const ws = new WebSocket(url)\n return {\n send: (data) => ws.send(data),\n close: () => ws.close(),\n onOpen: (cb) => ws.addEventListener('open', () => cb()),\n onMessage: (cb) => ws.addEventListener('message', (e) => cb(String((e as MessageEvent).data))),\n onClose: (cb) => ws.addEventListener('close', () => cb()),\n }\n}\n","/**\n * A raw IndexedDB connection that survives the browser closing it underneath the page.\n *\n * Browsers close a connection without the page asking when they evict or reclaim storage,\n * when a discarded tab is restored (common on Android), when the storage process crashes,\n * and when another tab opens a newer database version. From then on every `transaction()`\n * on the old handle throws `InvalidStateError: The database connection is closing`. Before\n * 2026-09-01 the sync Local Cache held one such handle for the life of the workspace, so a\n * page that stayed open kept failing every save until a reload, and the keystrokes typed in\n * between reached neither the cache nor the relay.\n *\n * This wrapper keeps the \"one connection per cache\" shape but notices the close (via the\n * `close` event, the `versionchange` event, or the synchronous throw) and reopens on the\n * next use. A reopen that fails is reported to the caller once rather than retried in a\n * loop, and `dispose()` closes for good.\n */\nexport interface ReopenableConnection {\n /** The live database, opening a fresh connection first if the last one was closed. */\n current(): Promise<IDBDatabase>\n /**\n * `current().transaction(stores, mode)`, retried once through a reopen when the handle\n * turns out to be closed. Call sites place their first request synchronously after\n * awaiting this, exactly as they would after a bare `db.transaction()`.\n */\n transaction(stores: string | string[], mode: IDBTransactionMode): Promise<IDBTransaction>\n /** Close for good. Later calls reject with {@link ConnectionDisposedError} rather than reopen. */\n dispose(): void\n}\n\nexport class ConnectionDisposedError extends Error {\n constructor() {\n super('The IndexedDB connection has been disposed')\n this.name = 'ConnectionDisposedError'\n }\n}\n\n/**\n * The error `IDBDatabase.transaction()` throws once the connection has closed. Every engine\n * uses the `InvalidStateError` name for it; only the message wording differs.\n */\nexport function isClosedConnectionError(error: unknown): boolean {\n return error instanceof Error && error.name === 'InvalidStateError'\n}\n\nexport async function createReopenableConnection(\n open: () => Promise<IDBDatabase>,\n): Promise<ReopenableConnection> {\n let live: IDBDatabase | null = null\n let opening: Promise<IDBDatabase> | null = null\n let disposed = false\n\n const forget = (handle: IDBDatabase) => {\n if (live === handle) live = null\n }\n\n const attach = (handle: IDBDatabase): IDBDatabase => {\n live = handle\n // Fired when the browser terminates the connection abnormally. It is NOT fired for\n // the page's own close(), so the synchronous-throw path below still matters.\n handle.onclose = () => forget(handle)\n // A newer version is being opened elsewhere, typically a deploy picked up by another\n // tab. Releasing the handle lets that upgrade proceed instead of blocking it; the\n // next use reopens at whatever version the browser now holds, and a VersionError\n // there surfaces as a \"reload this tab\" rather than a silently stuck upgrade.\n handle.onversionchange = () => {\n handle.close()\n forget(handle)\n }\n return handle\n }\n\n attach(await open())\n\n async function current(): Promise<IDBDatabase> {\n if (disposed) throw new ConnectionDisposedError()\n if (live) return live\n if (!opening) {\n // One reopen at a time, and a failed one is not memoised: the next caller tries\n // afresh, which is what makes a transient VersionError recoverable after the\n // other tab's upgrade completes.\n opening = open()\n .then(attach)\n .finally(() => {\n opening = null\n })\n }\n return opening\n }\n\n return {\n current,\n async transaction(stores, mode) {\n const first = await current()\n try {\n return first.transaction(stores, mode)\n } catch (error) {\n if (!isClosedConnectionError(error)) throw error\n // Closed without a `close` event reaching us yet. Drop the handle and go\n // through exactly one reopen; a second failure is the caller's to report.\n forget(first)\n return (await current()).transaction(stores, mode)\n }\n },\n dispose() {\n disposed = true\n live?.close()\n live = null\n },\n }\n}\n","/**\n * Browser-private sync storage. A document row is normally a rebuildable Yjs cache. A\n * dirty boundary temporarily makes its newer bytes recoverable user work until an exact\n * encrypted operation is committed to the outbox before the matching socket send.\n */\nimport type { DocCache, DocCacheState, DurableOutboxOperation, NewOutboxOperation } from './doc-sync'\nimport * as Y from 'yjs'\nimport { createReopenableConnection } from '$lib/storage/idb-connection'\nimport { mergeStateVectors, stateVectorCovers } from './state-vector'\n\nconst DB_NAME = 'etherpk-sync'\nconst DB_VERSION = 3\nconst DOC_STORE = 'docs'\nconst OUTBOX_STORE = 'outbox'\nconst OUTBOX_GRAPH_INDEX = 'by_graph'\nconst OUTBOX_GRAPH_DOC_INDEX = 'by_graph_doc'\nconst INDEX_DIRTY_STORE = 'index-dirty'\nconst INDEX_DIRTY_GRAPH_INDEX = 'by_graph'\n\ninterface DocRow extends DocCacheState {\n key: string\n graphId: string\n docId: string\n}\n\ntype OutboxRow = DurableOutboxOperation & { key: string }\n\ninterface IndexDirtyRow {\n key: string\n graphId: string\n docId: string\n token: string\n}\n\n/** One exact Local Cache persistence boundary which has not yet reached the index. */\nexport interface IndexDirtyChange {\n docId: string\n token: string\n}\n\nexport interface GraphCache {\n /** Root writes compare registry identity; ordinary documents conservatively dirty on change. */\n docCache(docId: string, indexRole?: 'document' | 'root'): DocCache\n listDocIds(): Promise<string[]>\n watermarks(\n docIds: readonly string[],\n ): Promise<\n Map<\n string,\n {\n generation: number\n lifecycle: 'active' | 'deleted'\n lastSeq: number\n }\n >\n >\n pendingDocIds(): Promise<string[]>\n countPending(): Promise<number>\n /** Index-visible cache changes which have not crossed an explicit index commit. */\n pendingIndexChanges(): Promise<IndexDirtyChange[]>\n /** Clear only the exact boundaries represented by a committed worker operation. */\n acknowledgeIndexChanges(changes: readonly IndexDirtyChange[]): Promise<void>\n dispose(): void\n}\n\nfunction openDb(): Promise<IDBDatabase> {\n return new Promise((resolve, reject) => {\n let settled = false\n const request = indexedDB.open(DB_NAME, DB_VERSION)\n request.onupgradeneeded = (event) => {\n const db = request.result\n if (!db.objectStoreNames.contains(DOC_STORE)) {\n db.createObjectStore(DOC_STORE, { keyPath: 'key' })\n }\n if (!db.objectStoreNames.contains(OUTBOX_STORE)) {\n const outbox = db.createObjectStore(OUTBOX_STORE, { keyPath: 'key' })\n outbox.createIndex(OUTBOX_GRAPH_INDEX, 'graphId')\n outbox.createIndex(OUTBOX_GRAPH_DOC_INDEX, ['graphId', 'docId'])\n }\n if (!db.objectStoreNames.contains(INDEX_DIRTY_STORE)) {\n const dirty = db.createObjectStore(INDEX_DIRTY_STORE, { keyPath: 'key' })\n dirty.createIndex(INDEX_DIRTY_GRAPH_INDEX, 'graphId')\n }\n if (event.oldVersion > 0 && event.oldVersion < 3) {\n // A legacy cache may already be ahead of its separately-persisted index.\n // Backfill every document as dirty once; a root row naturally forces the\n // first post-upgrade open through a safe full replacement.\n const upgrade = request.transaction\n const docs = upgrade?.objectStore(DOC_STORE)\n const dirty = upgrade?.objectStore(INDEX_DIRTY_STORE)\n if (docs && dirty) {\n docs.openCursor().onsuccess = (cursorEvent) => {\n const cursor = (cursorEvent.target as IDBRequest<IDBCursorWithValue | null>)\n .result\n if (!cursor) return\n const row = cursor.value as Partial<DocRow>\n if (typeof row.graphId === 'string' && typeof row.docId === 'string') {\n dirty.put({\n key: `${row.graphId}/${row.docId}`,\n graphId: row.graphId,\n docId: row.docId,\n token: crypto.randomUUID(),\n } satisfies IndexDirtyRow)\n }\n cursor.continue()\n }\n }\n }\n }\n request.onsuccess = () => {\n if (settled) {\n request.result.close()\n return\n }\n settled = true\n // Version-change and abnormal-close handling belong to the connection wrapper in\n // openGraphCache, which reopens on the next use; a one-shot caller such as\n // deleteGraphCache closes its own handle in a finally.\n resolve(request.result)\n }\n request.onerror = () => {\n if (settled) return\n settled = true\n reject(request.error)\n }\n request.onblocked = () => {\n if (settled) return\n settled = true\n reject(new Error('The sync cache upgrade is blocked by another open tab'))\n }\n })\n}\n\nfunction requestResult<T>(request: IDBRequest<T>): Promise<T> {\n return new Promise((resolve, reject) => {\n request.onsuccess = () => resolve(request.result)\n request.onerror = () => reject(request.error)\n })\n}\n\nfunction transactionDone(transaction: IDBTransaction): Promise<void> {\n return new Promise((resolve, reject) => {\n transaction.oncomplete = () => resolve()\n transaction.onabort = () => reject(transaction.error ?? new Error('IndexedDB transaction aborted'))\n transaction.onerror = () => reject(transaction.error ?? new Error('IndexedDB transaction failed'))\n })\n}\n\nfunction graphRange(graphId: string): IDBKeyRange {\n return IDBKeyRange.only(graphId)\n}\n\nfunction graphDocRange(graphId: string, docId: string): IDBKeyRange {\n return IDBKeyRange.only([graphId, docId])\n}\n\nexport async function openGraphCache(graphId: string): Promise<GraphCache> {\n // One connection per cache, but one the browser cannot kill for the rest of the\n // workspace's life: an eviction, a discarded-and-restored tab or another tab's schema\n // upgrade closes the handle, and the next operation reopens it (idb-connection.ts).\n const connection = await createReopenableConnection(openDb)\n const docKey = (docId: string) => `${graphId}/${docId}`\n const outboxKey = (outboxId: string) => `${graphId}/${outboxId}`\n const indexDirtyKey = (docId: string) => `${graphId}/${docId}`\n\n function markIndexDirty(transaction: IDBTransaction, docId: string): void {\n transaction.objectStore(INDEX_DIRTY_STORE).put({\n key: indexDirtyKey(docId),\n graphId,\n docId,\n // Replacing the token makes acknowledgement compare-and-delete. If another tab\n // writes while the worker is ingesting, that later boundary remains durable.\n token: crypto.randomUUID(),\n } satisfies IndexDirtyRow)\n }\n\n function stateRow(docId: string, state: DocCacheState): DocRow {\n return { key: docKey(docId), graphId, docId, ...state }\n }\n\n function withDirtyTokens(state: DocCacheState, tokens: Iterable<string>): DocCacheState {\n const { dirtyTokens: _previous, ...rest } = state\n const dirtyTokens = [...new Set(tokens)].sort()\n return dirtyTokens.length > 0 ? { ...rest, dirtyTokens } : rest\n }\n\n function mergeState(existing: DocRow | undefined, incoming: DocCacheState): DocCacheState {\n if (!existing) return withDirtyTokens(incoming, incoming.dirtyTokens ?? [])\n const existingGeneration = existing.generation ?? 1\n const incomingGeneration = incoming.generation ?? 1\n if (incomingGeneration < existingGeneration) {\n return existing\n }\n if (incomingGeneration > existingGeneration) {\n return withDirtyTokens(incoming, incoming.dirtyTokens ?? [])\n }\n let update: Uint8Array\n try {\n update = Y.mergeUpdates([existing.update, incoming.update])\n } catch {\n // The cache is rebuildable. A damaged legacy row must not prevent a fresh,\n // valid server state from replacing it.\n update = incoming.update\n }\n let lastSyncedStateVector: Uint8Array\n try {\n lastSyncedStateVector = mergeStateVectors(\n existing.lastSyncedStateVector,\n incoming.lastSyncedStateVector,\n )\n } catch {\n lastSyncedStateVector = incoming.lastSyncedStateVector\n }\n return withDirtyTokens(\n {\n update,\n lastSeq: Math.max(existing.lastSeq, incoming.lastSeq),\n lastSyncedStateVector,\n generation: incomingGeneration,\n lifecycle:\n existing.lifecycle === 'deleted' || incoming.lifecycle === 'deleted'\n ? 'deleted'\n : 'active',\n },\n [...(existing.dirtyTokens ?? []), ...(incoming.dirtyTokens ?? [])],\n )\n }\n\n function sameYjsState(left: Uint8Array, right: Uint8Array): boolean {\n const equalBytes = (a: Uint8Array, b: Uint8Array) =>\n a.byteLength === b.byteLength && a.every((value, index) => value === b[index])\n if (equalBytes(left, right)) return true\n try {\n // mergeUpdates canonicalises struct and delete-set ordering without materialising\n // a Y.Doc. This distinguishes a real CRDT change from a save which only advanced\n // sequence or confirmed-state metadata.\n return equalBytes(Y.mergeUpdates([left]), Y.mergeUpdates([right]))\n } catch {\n // Damaged legacy rows are rebuildable. Treat uncertainty as changed so the index\n // is conservatively repaired alongside the fresh valid cache write.\n return false\n }\n }\n\n function stableJson(value: unknown): string {\n if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`\n if (value !== null && typeof value === 'object') {\n return `{${Object.entries(value)\n .filter(([, child]) => child !== undefined)\n .sort(([left], [right]) => left.localeCompare(right))\n .map(([key, child]) => `${JSON.stringify(key)}:${stableJson(child)}`)\n .join(',')}}`\n }\n return JSON.stringify(value) ?? String(value)\n }\n\n /**\n * Root metadata shares a Y.Doc with registry identity but does not feed the document index.\n * Only root writes pay to compare that projection. Ordinary content changes stay on the\n * cheap structural path and conservatively journal any real Yjs change.\n */\n function sameIndexedState(\n left: Uint8Array,\n right: Uint8Array,\n indexRole: 'document' | 'root',\n ): boolean {\n if (sameYjsState(left, right)) return true\n if (indexRole === 'document') return false\n\n const projection = (update: Uint8Array): string | undefined => {\n const doc = new Y.Doc()\n try {\n Y.applyUpdate(doc, update)\n const identities: unknown[][] = []\n doc.getMap<Record<string, unknown>>('registry').forEach((entry, docId) => {\n const kind = entry?.kind\n const concept = kind === 'journal' ? (entry?.date ?? '') : (entry?.title ?? '')\n const aliases = Array.isArray(entry?.aliases)\n ? [...entry.aliases].sort((leftAlias, rightAlias) =>\n stableJson(leftAlias).localeCompare(stableJson(rightAlias)),\n )\n : (entry?.aliases ?? [])\n identities.push([docId, kind, concept, aliases])\n })\n identities.sort(([leftId], [rightId]) =>\n String(leftId).localeCompare(String(rightId)),\n )\n return stableJson(identities)\n } catch {\n return undefined\n } finally {\n doc.destroy()\n }\n }\n const leftProjection = projection(left)\n return leftProjection !== undefined && leftProjection === projection(right)\n }\n\n return {\n docCache(docId: string, indexRole = 'document'): DocCache {\n return {\n async load() {\n const transaction = await connection.transaction(DOC_STORE, 'readonly')\n const row = await requestResult(\n transaction.objectStore(DOC_STORE).get(docKey(docId)) as IDBRequest<DocRow | undefined>,\n )\n if (!row) return null\n return {\n update: row.update,\n lastSeq: row.lastSeq,\n lastSyncedStateVector: row.lastSyncedStateVector,\n ...(row.generation === undefined ? {} : { generation: row.generation }),\n ...(row.lifecycle === undefined ? {} : { lifecycle: row.lifecycle }),\n ...(row.dirtyTokens === undefined ? {} : { dirtyTokens: row.dirtyTokens }),\n }\n },\n async save(state, options) {\n const transaction = await connection.transaction(\n [DOC_STORE, INDEX_DIRTY_STORE],\n 'readwrite',\n )\n const store = transaction.objectStore(DOC_STORE)\n const existing = await requestResult(\n store.get(docKey(docId)) as IDBRequest<DocRow | undefined>,\n )\n const merged = mergeState(existing, state)\n const dirtyTokens = [...(merged.dirtyTokens ?? [])]\n if (options?.supersededDirtyToken) {\n const index = dirtyTokens.indexOf(options.supersededDirtyToken)\n if (index >= 0) dirtyTokens.splice(index, 1)\n }\n if (options?.dirtyToken) dirtyTokens.push(options.dirtyToken)\n store.put(stateRow(docId, withDirtyTokens(merged, dirtyTokens)))\n const indexChanged =\n !existing ||\n (indexRole === 'document' &&\n options?.indexContentChanged !== undefined\n ? options.indexContentChanged\n : !sameIndexedState(existing.update, merged.update, indexRole)) ||\n (existing.lifecycle ?? 'active') !== (merged.lifecycle ?? 'active')\n if (indexChanged) markIndexDirty(transaction, docId)\n await transactionDone(transaction)\n },\n async enqueue(operation: NewOutboxOperation, state: DocCacheState, options) {\n const transaction = await connection.transaction(\n [DOC_STORE, OUTBOX_STORE, INDEX_DIRTY_STORE],\n 'readwrite',\n )\n const docs = transaction.objectStore(DOC_STORE)\n const existing = await requestResult(\n docs.get(docKey(docId)) as IDBRequest<DocRow | undefined>,\n )\n const merged = mergeState(existing, state)\n const settled = new Set(options?.settledDirtyTokens ?? [])\n docs.put(\n stateRow(\n docId,\n withDirtyTokens(\n merged,\n (merged.dirtyTokens ?? []).filter((token) => !settled.has(token)),\n ),\n ),\n )\n const row: OutboxRow = {\n key: outboxKey(operation.outboxId),\n graphId,\n docId,\n ...operation,\n attemptCount: 0,\n lastAttemptAt: null,\n }\n transaction.objectStore(OUTBOX_STORE).add(row)\n const indexChanged =\n operation.kind === 'delete' ||\n !existing ||\n !sameIndexedState(existing.update, merged.update, indexRole) ||\n (existing.lifecycle ?? 'active') !== (merged.lifecycle ?? 'active')\n if (indexChanged) markIndexDirty(transaction, docId)\n await transactionDone(transaction)\n },\n async pending() {\n const transaction = await connection.transaction(OUTBOX_STORE, 'readonly')\n const rows = await requestResult(\n transaction\n .objectStore(OUTBOX_STORE)\n .index(OUTBOX_GRAPH_DOC_INDEX)\n .getAll(graphDocRange(graphId, docId)) as IDBRequest<OutboxRow[]>,\n )\n return rows\n .sort((a, b) => a.createdAt - b.createdAt || a.outboxId.localeCompare(b.outboxId))\n .map(({ key: _key, ...operation }) => operation)\n },\n async markAttempt(outboxId, attemptedAt) {\n const transaction = await connection.transaction(OUTBOX_STORE, 'readwrite')\n const store = transaction.objectStore(OUTBOX_STORE)\n const key = outboxKey(outboxId)\n const row = await requestResult(store.get(key) as IDBRequest<OutboxRow | undefined>)\n if (row) {\n row.attemptCount += 1\n row.lastAttemptAt = attemptedAt\n store.put(row)\n }\n await transactionDone(transaction)\n },\n async commitAcknowledgement(\n outboxId,\n generation,\n seq,\n update,\n lastSyncedStateVector,\n ) {\n const transaction = await connection.transaction(\n [DOC_STORE, OUTBOX_STORE, INDEX_DIRTY_STORE],\n 'readwrite',\n )\n const docs = transaction.objectStore(DOC_STORE)\n const outbox = transaction.objectStore(OUTBOX_STORE)\n const row = await requestResult(\n outbox.get(outboxKey(outboxId)) as IDBRequest<OutboxRow | undefined>,\n )\n const existing = await requestResult(\n docs.get(docKey(docId)) as IDBRequest<DocRow | undefined>,\n )\n if (!row || row.docId !== docId || row.kind === 'delete') {\n // Another tab can receive and durably commit the same idempotent ack\n // first. The exact outbox row is then gone, while the shared confirmed\n // state vector proves whether that operation's Yjs structs are settled.\n // This remains valid when its sequence could not yet advance because a\n // preceding update from another connection is still missing.\n let settled = false\n try {\n settled =\n !row &&\n existing !== undefined &&\n (existing.generation ?? 1) === generation &&\n stateVectorCovers(\n existing.lastSyncedStateVector,\n lastSyncedStateVector,\n )\n } catch {\n // A malformed legacy vector is rebuildable and cannot prove an ack.\n }\n await transactionDone(transaction)\n return settled\n }\n const acknowledged: DocCacheState = {\n update,\n lastSeq: seq,\n lastSyncedStateVector,\n generation,\n lifecycle: 'active',\n }\n let merged: DocCacheState\n if (\n row.kind === 'resurrect' &&\n existing &&\n generation > (existing.generation ?? 1)\n ) {\n // The relay confirms only the resurrection operation's captured\n // boundary. Another tab, or this tab after encryption yielded, may\n // already have added structs beyond that boundary to the shared old-\n // generation row. Promote those unconfirmed structs with the ack and\n // retain their recovery tokens instead of replacing the whole row.\n let promotedUpdate = update\n try {\n const unconfirmed = Y.diffUpdate(existing.update, row.stateVector)\n promotedUpdate = Y.mergeUpdates([update, unconfirmed])\n } catch {\n // A damaged cache is rebuildable. Keep the valid acknowledged\n // snapshot while preserving its dirty tokens for a visible retry.\n }\n merged = withDirtyTokens(\n { ...acknowledged, update: promotedUpdate },\n existing.dirtyTokens ?? [],\n )\n } else {\n merged = mergeState(existing, acknowledged)\n }\n docs.put(stateRow(docId, merged))\n // A resurrection acknowledgement is the moment the cached lifecycle\n // becomes active. An older checkpoint may still be indexing the deleted\n // snapshot, so rotate its token in this same transaction. The older\n // compare-and-delete can then clear only its own boundary.\n const indexChanged =\n !existing ||\n (existing.generation ?? 1) !== (merged.generation ?? 1) ||\n (existing.lifecycle ?? 'active') !== (merged.lifecycle ?? 'active')\n if (indexChanged) markIndexDirty(transaction, docId)\n outbox.delete(row.key)\n await transactionDone(transaction)\n return true\n },\n async commitDeleteAcknowledgement(outboxId, generation) {\n const transaction = await connection.transaction(\n [DOC_STORE, OUTBOX_STORE, INDEX_DIRTY_STORE],\n 'readwrite',\n )\n const outbox = transaction.objectStore(OUTBOX_STORE)\n const row = await requestResult(\n outbox.get(outboxKey(outboxId)) as IDBRequest<OutboxRow | undefined>,\n )\n if (!row || row.docId !== docId || row.kind !== 'delete') {\n transaction.abort()\n try {\n await transactionDone(transaction)\n } catch {\n // An explicit abort is expected for an unknown acknowledgement.\n }\n return false\n }\n transaction.objectStore(DOC_STORE).delete(docKey(docId))\n outbox.delete(row.key)\n markIndexDirty(transaction, docId)\n await transactionDone(transaction)\n void generation\n return true\n },\n async discard(outboxId) {\n const transaction = await connection.transaction(OUTBOX_STORE, 'readwrite')\n transaction.objectStore(OUTBOX_STORE).delete(outboxKey(outboxId))\n await transactionDone(transaction)\n },\n async compact() {\n const transaction = await connection.transaction(DOC_STORE, 'readwrite')\n const store = transaction.objectStore(DOC_STORE)\n const existing = await requestResult(store.get(docKey(docId)) as IDBRequest<DocRow | undefined>)\n if (!existing) return\n // Applying the row into a fresh document collects its deleted items at the\n // end of that transaction (nothing pins them there), and encoding the\n // result is the same state without their content. Built from the row, not\n // from a live engine, so another tab's saves since are never overwritten.\n const collected = new Y.Doc()\n try {\n Y.applyUpdate(collected, existing.update)\n store.put({ ...existing, update: Y.encodeStateAsUpdate(collected) })\n } finally {\n collected.destroy()\n }\n await transactionDone(transaction)\n },\n async purge() {\n const transaction = await connection.transaction(\n [DOC_STORE, OUTBOX_STORE, INDEX_DIRTY_STORE],\n 'readwrite',\n )\n transaction.objectStore(DOC_STORE).delete(docKey(docId))\n transaction\n .objectStore(OUTBOX_STORE)\n .index(OUTBOX_GRAPH_DOC_INDEX)\n .openKeyCursor(graphDocRange(graphId, docId)).onsuccess = (event) => {\n const cursor = (event.target as IDBRequest<IDBCursor | null>).result\n if (!cursor) return\n transaction.objectStore(OUTBOX_STORE).delete(cursor.primaryKey)\n cursor.continue()\n }\n markIndexDirty(transaction, docId)\n await transactionDone(transaction)\n },\n }\n },\n async listDocIds() {\n const transaction = await connection.transaction(DOC_STORE, 'readonly')\n const keys = await requestResult(transaction.objectStore(DOC_STORE).getAllKeys() as IDBRequest<string[]>)\n const prefix = `${graphId}/`\n return keys.filter((key) => key.startsWith(prefix)).map((key) => key.slice(prefix.length))\n },\n async watermarks(docIds) {\n const transaction = await connection.transaction(DOC_STORE, 'readonly')\n const store = transaction.objectStore(DOC_STORE)\n const rows = await Promise.all(\n docIds.map((docId) =>\n requestResult(\n store.get(docKey(docId)) as IDBRequest<DocRow | undefined>,\n ),\n ),\n )\n return new Map(\n rows.flatMap((row, index) =>\n row\n ? [\n [\n docIds[index],\n {\n generation: row.generation ?? 1,\n lifecycle: row.lifecycle ?? 'active',\n lastSeq: row.lastSeq,\n },\n ] as const,\n ]\n : [],\n ),\n )\n },\n async pendingDocIds() {\n const transaction = await connection.transaction(OUTBOX_STORE, 'readonly')\n const rows = await requestResult(\n transaction.objectStore(OUTBOX_STORE).index(OUTBOX_GRAPH_INDEX).getAll(graphRange(graphId)) as IDBRequest<\n OutboxRow[]\n >,\n )\n return [...new Set(rows.map((row) => row.docId))].sort()\n },\n async countPending() {\n const transaction = await connection.transaction(OUTBOX_STORE, 'readonly')\n return requestResult(\n transaction.objectStore(OUTBOX_STORE).index(OUTBOX_GRAPH_INDEX).count(graphRange(graphId)),\n )\n },\n async pendingIndexChanges() {\n const transaction = await connection.transaction(INDEX_DIRTY_STORE, 'readonly')\n const rows = await requestResult(\n transaction\n .objectStore(INDEX_DIRTY_STORE)\n .index(INDEX_DIRTY_GRAPH_INDEX)\n .getAll(graphRange(graphId)) as IDBRequest<IndexDirtyRow[]>,\n )\n return rows\n .sort((a, b) => a.docId.localeCompare(b.docId))\n .map(({ docId, token }) => ({ docId, token }))\n },\n async acknowledgeIndexChanges(changes) {\n if (changes.length === 0) return\n const transaction = await connection.transaction(INDEX_DIRTY_STORE, 'readwrite')\n const store = transaction.objectStore(INDEX_DIRTY_STORE)\n // Queue every read while the transaction is synchronously active. The token\n // comparison below is the IndexedDB equivalent of compare-and-delete.\n const rows = await Promise.all(\n changes.map((change) =>\n requestResult(\n store.get(indexDirtyKey(change.docId)) as IDBRequest<\n IndexDirtyRow | undefined\n >,\n ),\n ),\n )\n rows.forEach((row, index) => {\n if (row?.token === changes[index]?.token) store.delete(row.key)\n })\n await transactionDone(transaction)\n },\n dispose() {\n connection.dispose()\n },\n }\n}\n\n/**\n * Remove both rebuildable state and recoverable unsent ciphertext after the caller has\n * explicitly forgotten, left, reset or deleted the graph.\n */\nexport async function deleteGraphCache(graphId: string): Promise<void> {\n const db = await openDb()\n try {\n const transaction = db.transaction(\n [DOC_STORE, OUTBOX_STORE, INDEX_DIRTY_STORE],\n 'readwrite',\n )\n const docPrefix = IDBKeyRange.bound(`${graphId}/`, `${graphId}/\\uffff`)\n transaction.objectStore(DOC_STORE).delete(docPrefix)\n transaction\n .objectStore(OUTBOX_STORE)\n .index(OUTBOX_GRAPH_INDEX)\n .openKeyCursor(graphRange(graphId)).onsuccess = (event) => {\n const cursor = (event.target as IDBRequest<IDBCursor | null>).result\n if (!cursor) return\n transaction.objectStore(OUTBOX_STORE).delete(cursor.primaryKey)\n cursor.continue()\n }\n transaction\n .objectStore(INDEX_DIRTY_STORE)\n .index(INDEX_DIRTY_GRAPH_INDEX)\n .openKeyCursor(graphRange(graphId)).onsuccess = (event) => {\n const cursor = (event.target as IDBRequest<IDBCursor | null>).result\n if (!cursor) return\n transaction.objectStore(INDEX_DIRTY_STORE).delete(cursor.primaryKey)\n cursor.continue()\n }\n await transactionDone(transaction)\n } finally {\n db.close()\n }\n}\n","/**\n * The relay socket for Node: the client's `TransportSocket` over the global `WebSocket` Node 22+\n * ships, so `graph-sync.ts` runs unchanged. The browser's `browserTransport` is the same five\n * lines over the same API; it is not imported here only because the client's module lives\n * beside code that reads `window`.\n */\n\nimport type { TransportSocket } from '$lib/sync/graph-sync'\n\nexport function nodeTransport(url: string): TransportSocket {\n const ws = new WebSocket(url)\n return {\n send: (data) => ws.send(data),\n close: () => ws.close(),\n onOpen: (cb) => ws.addEventListener('open', () => cb()),\n onMessage: (cb) => ws.addEventListener('message', (e) => cb(String((e as MessageEvent).data))),\n onClose: (cb) => ws.addEventListener('close', () => cb()),\n }\n}\n","/**\n * A synced graph's name lives in its root document's encrypted `meta` map (ADR 0031), so the\n * Sync Server's graph list carries ids only. Naming a graph on the command line, or listing\n * graphs by name, means opening each root document far enough to read the map - a bounded\n * relay catch-up on one small document per graph, then gone.\n */\n\nimport 'fake-indexeddb/auto'\n\nimport type { GraphKeyring } from '$lib/crypto'\nimport { createGraphSync, type TransportSocket } from '$lib/sync/graph-sync'\nimport { openGraphCache } from '$lib/sync/local-cache'\nimport type { SyncTokenSource } from '$lib/sync/sync-token'\n\nimport { nodeTransport } from './node-transport'\n\nexport interface GraphNameDeps {\n graphId: string\n rootDocId: string\n keyring: GraphKeyring\n relayUrl: string\n token: SyncTokenSource\n connect?: (url: string) => TransportSocket\n timeoutMs?: number\n}\n\n/** The graph's name, or null when it has none yet or the relay could not be reached in time. */\nexport async function readGraphName(deps: GraphNameDeps): Promise<string | null> {\n const cache = await openGraphCache(`${deps.graphId}-name`)\n const sync = createGraphSync({\n graphId: deps.graphId,\n rootDocId: deps.rootDocId,\n keyring: deps.keyring,\n relayUrl: deps.relayUrl,\n token: deps.token,\n cache,\n connect: deps.connect ?? nodeTransport,\n })\n try {\n const timeout = new Promise<'timeout'>((resolve) => setTimeout(() => resolve('timeout'), deps.timeoutMs ?? 10_000))\n const outcome = await Promise.race([sync.rootCaughtUp().then(() => 'ok' as const), timeout])\n if (outcome === 'timeout') return null\n return sync.getMeta().name ?? null\n } finally {\n sync.dispose()\n cache.dispose()\n }\n}\n","/**\n * The wikilink model: the pure data the parser yields and the segment queries the\n * editor decoration and publish renderer consume. No DOM, no CodeMirror, no storage.\n *\n * A wikilink yields only its **concept** (its identity); the on-disk file name and\n * the publish Slug are derived elsewhere (see `derive.ts`). See\n * docs/docs/technical/Wikilinks.md and ADR 0011.\n */\n\n/** One parsed wikilink (at any nesting level). Offsets are relative to the parsed string. */\nexport interface Wikilink {\n /** Full matched text, including the outer `[[` `]]`. */\n text: string\n /** Concept name: outer brackets stripped, inner brackets retained (scoped concepts). */\n concept: string\n /** Inclusive offset of the leading `[`. */\n start: number\n /** Inclusive offset of the trailing `]`. */\n end: number\n}\n\n/** A non-overlapping run of characters whose innermost covering wikilink is `wikilink`. */\nexport interface WikilinkSegment {\n /** Inclusive start offset. */\n start: number\n /** Exclusive end offset. */\n end: number\n wikilink: Wikilink\n}\n\n/** The concept for a matched `[[…]]` text: strip exactly the outer `[[` and `]]`. */\nexport function conceptOf(text: string): string {\n return text.slice(2, -2)\n}\n\n/**\n * The innermost (smallest) wikilink covering an offset, or undefined if none.\n * Maps a cursor/click position to the link it should act on.\n */\nexport function innermostWikilinkAt(links: Wikilink[], offset: number): Wikilink | undefined {\n let best: Wikilink | undefined\n for (const link of links) {\n if (offset < link.start || offset > link.end) continue\n if (!best || link.end - link.start < best.end - best.start) best = link\n }\n return best\n}\n\n/**\n * Split the covered span into non-overlapping, contiguous segments, each owned by the\n * innermost wikilink at that position. Gaps between sibling links (plain text) produce\n * no segment. End offsets are exclusive.\n */\nexport function wikilinkSegments(links: Wikilink[]): WikilinkSegment[] {\n if (links.length === 0) return []\n let min = Infinity\n let max = -Infinity\n for (const l of links) {\n if (l.start < min) min = l.start\n if (l.end > max) max = l.end\n }\n\n const segments: WikilinkSegment[] = []\n let current: WikilinkSegment | undefined\n for (let offset = min; offset <= max; offset++) {\n const inner = innermostWikilinkAt(links, offset)\n if (!inner) {\n current = undefined // a gap between siblings breaks the run\n continue\n }\n if (current && current.wikilink === inner) {\n current.end = offset + 1\n } else {\n current = { start: offset, end: offset + 1, wikilink: inner }\n segments.push(current)\n }\n }\n return segments\n}\n","/**\n * The wikilink parser: turns a string into all wikilinks at every nesting level,\n * in document order. Pure; offsets are relative to the input. Treats `[[` as an\n * open token and `]]` as a close token and matches them with a stack, so\n * `[[[[Physics]] Quantum Mechanics]]` yields the outer link and the inner one.\n *\n * Malformed input degrades gracefully: an unterminated `[[` or a stray `]]`\n * simply produces no link (see ADR 0011 — tolerance, not a feature).\n */\n\nimport { conceptOf, type Wikilink } from './model'\n\nexport function parseWikilinks(input: string): Wikilink[] {\n const links: Wikilink[] = []\n const openStarts: number[] = []\n let i = 0\n while (i < input.length) {\n if (input[i] === '[' && input[i + 1] === '[') {\n openStarts.push(i)\n i += 2\n continue\n }\n if (input[i] === ']' && input[i + 1] === ']') {\n const start = openStarts.pop()\n if (start !== undefined) {\n const end = i + 1 // inclusive offset of the second ']'\n const text = input.slice(start, end + 1)\n links.push({ text, concept: conceptOf(text), start, end })\n }\n i += 2\n continue\n }\n i += 1\n }\n // Document order. (Starts are always distinct; the secondary key is a stable tiebreak.)\n links.sort((a, b) => a.start - b.start || b.end - a.end)\n return links\n}\n","/**\n * Code-region detection for wikilink suppression. Rather than hand-roll a fence\n * scanner, we use `@lezer/markdown` (the framework-agnostic parser CodeMirror's\n * lang-markdown wraps) to report the ranges of code nodes; the wikilink scanner\n * then ignores any segment overlapping one. Pure and headless — the same rule\n * serves the editor, the index, and publish (ADR 0011, docs/.../Wikilinks.md).\n */\n\nimport { parser } from '@lezer/markdown'\n\n/** Node types whose ranges are code and must not contain wikilinks. */\nconst CODE_NODES = new Set([\n 'FencedCode', // ``` … ``` and ~~~ … ~~~ (incl. fences)\n 'CodeBlock', // indented code\n 'InlineCode', // `…` (incl. backticks)\n 'HTMLBlock',\n 'CommentBlock',\n 'Comment',\n])\n\nexport interface CodeRange {\n from: number\n to: number\n}\n\n/** The code ranges in `source`, as absolute `[from, to)` offsets. */\nexport function codeRanges(source: string): CodeRange[] {\n const tree = parser.parse(source)\n const ranges: CodeRange[] = []\n tree.iterate({\n enter: (node) => {\n if (CODE_NODES.has(node.name)) ranges.push({ from: node.from, to: node.to })\n },\n })\n return ranges\n}\n\n/** Whether the half-open range `[from, to)` overlaps any code range. */\nexport function isInCode(ranges: readonly CodeRange[], from: number, to: number): boolean {\n return ranges.some((r) => from < r.to && to > r.from)\n}\n","/**\n * Document-level wikilink parsing: the integration point the editor decoration,\n * the backlink index, and publish all use. Parses each line (wikilinks never span\n * lines), maps offsets to absolute document positions, and drops any segment that\n * falls inside a code range (`@lezer/markdown`). Pure.\n */\n\nimport { codeRanges, isInCode, type CodeRange } from './code-ranges'\nimport { type WikilinkSegment, wikilinkSegments } from './model'\nimport { parseWikilinks } from './parser'\n\n/** One wikilink occurrence located in a document — what the backlink index consumes. */\nexport interface WikilinkOccurrence {\n /** The link's concept (outer brackets stripped; inner brackets kept for scoped concepts). */\n concept: string\n /** 0-based line the link appears on. */\n line: number\n /** The full text of that line (the backlink context). */\n lineText: string\n /** Column of the opening `[` within the line. */\n matchStart: number\n /** Column just past the closing `]` within the line (exclusive). */\n matchEnd: number\n}\n\n/**\n * Every wikilink occurrence in `source` — including links nested inside others —\n * with code-suppressed links dropped. Unlike {@link wikilinkSegmentsInSource}\n * (which merges into rendering segments), this yields one entry per `[[…]]` so the\n * backlink index can record each reference. Pure.\n */\nexport function wikilinkOccurrencesInSource(source: string): WikilinkOccurrence[] {\n const ranges = codeRanges(source)\n const out: WikilinkOccurrence[] = []\n const lines = source.split('\\n')\n let lineStart = 0\n for (let line = 0; line < lines.length; line++) {\n const lineText = lines[line]\n for (const wl of parseWikilinks(lineText)) {\n const absStart = wl.start + lineStart\n const absEnd = wl.end + lineStart\n if (isInCode(ranges, absStart, absEnd)) continue\n out.push({\n concept: wl.concept,\n line,\n lineText,\n matchStart: wl.start,\n matchEnd: wl.end + 1, // wl.end is the inclusive trailing ']'\n })\n }\n lineStart += lineText.length + 1 // +1 for the consumed newline\n }\n return out\n}\n\n/**\n * All wikilink segments in `source`, in absolute offsets, with links inside code\n * (fences / inline code) suppressed. Segments are returned in ascending start order.\n */\nexport function wikilinkSegmentsInSource(\n source: string,\n ranges: readonly CodeRange[] = codeRanges(source),\n): WikilinkSegment[] {\n const out: WikilinkSegment[] = []\n let lineStart = 0\n for (const line of source.split('\\n')) {\n for (const seg of wikilinkSegments(parseWikilinks(line))) {\n const start = seg.start + lineStart\n const end = seg.end + lineStart\n if (isInCode(ranges, start, end)) continue\n const wl = seg.wikilink\n out.push({\n start,\n end,\n wikilink: { ...wl, start: wl.start + lineStart, end: wl.end + lineStart },\n })\n }\n lineStart += line.length + 1 // +1 for the consumed newline\n }\n return out\n}\n","/**\n * The pure backlink (linked-references) index — Logseq-style. Built from a snapshot\n * of every document's body, it answers \"which documents reference this concept?\",\n * folding aliases into the canonical concept and matching case-insensitively\n * (ADR 0011). Browser-private and fully rebuildable from the markdown (DESIGN.md →\n * Derived state is browser-private), so a simple in-memory build is the source of\n * truth; a SQLite-WASM/OPFS persistence layer is a later optimisation.\n *\n * Inspired by the AS Notes index (concept→sources, alias-folding, case-insensitive\n * filename match). EtherPK keeps the line of source as the reference context; the\n * AS Notes outline-chain enrichment (grouping references by their indentation\n * ancestry) is a noted follow-on.\n */\n\nimport type { DocumentKind } from '$lib/storage'\n\nimport { wikilinkOccurrencesInSource } from '../wikilink'\n\n/** Case-insensitive concept identity key (mirrors storage identity / ADR 0011). */\nexport function conceptKey(concept: string): string {\n return concept.toLowerCase()\n}\n\n/** One document fed into the index: identity, aliases, and the body to scan. */\nexport interface IndexDoc {\n concept: string\n kind: DocumentKind\n /** Alternate names declared in frontmatter (`aliases:`). */\n aliases: string[]\n /** The document body (frontmatter stripped) to scan for wikilinks. */\n text: string\n}\n\n/** One reference to a concept, located in a source document. */\nexport interface BacklinkRef {\n sourceConcept: string\n sourceKind: DocumentKind\n /** 0-based line in the source body. */\n line: number\n /** The source line (the reference's context). */\n lineText: string\n /** Column range of the `[[…]]` within `lineText` (for highlighting). */\n matchStart: number\n matchEnd: number\n}\n\n/** References to one source document, in line order. */\nexport interface BacklinkGroup {\n sourceConcept: string\n sourceKind: DocumentKind\n refs: BacklinkRef[]\n}\n\nexport interface BacklinkIndex {\n /** conceptKey(link concept) → every reference to it. */\n byTarget: Map<string, BacklinkRef[]>\n /** aliasKey → canonical conceptKey. */\n aliasToCanonical: Map<string, string>\n /** Every key that resolves to a real document (canonical concept or alias). */\n existing: Set<string>\n /** key → a display concept name (canonical casing as authored). */\n display: Map<string, string>\n}\n\nexport function buildBacklinkIndex(docs: Iterable<IndexDoc>): BacklinkIndex {\n const byTarget = new Map<string, BacklinkRef[]>()\n const aliasToCanonical = new Map<string, string>()\n const existing = new Set<string>()\n const display = new Map<string, string>()\n\n // 1) Register identities + aliases first, so resolution sees the whole graph.\n for (const doc of docs) {\n const ck = conceptKey(doc.concept)\n existing.add(ck)\n display.set(ck, doc.concept)\n for (const alias of doc.aliases) {\n const ak = conceptKey(alias)\n existing.add(ak)\n aliasToCanonical.set(ak, ck)\n if (!display.has(ak)) display.set(ak, alias)\n }\n }\n\n // 2) Record every wikilink occurrence as a backlink against its concept key.\n for (const doc of docs) {\n for (const occ of wikilinkOccurrencesInSource(doc.text)) {\n const tk = conceptKey(occ.concept)\n const ref: BacklinkRef = {\n sourceConcept: doc.concept,\n sourceKind: doc.kind,\n line: occ.line,\n lineText: occ.lineText,\n matchStart: occ.matchStart,\n matchEnd: occ.matchEnd,\n }\n const list = byTarget.get(tk)\n if (list) list.push(ref)\n else byTarget.set(tk, [ref])\n }\n }\n\n return { byTarget, aliasToCanonical, existing, display }\n}\n\n/** True when the concept resolves to a real document (by canonical name or alias). */\nexport function conceptExists(index: BacklinkIndex, concept: string): boolean {\n return index.existing.has(conceptKey(concept))\n}\n\n/** The canonical concept key for a concept (following an alias to its page). */\nexport function canonicalKey(index: BacklinkIndex, concept: string): string {\n const key = conceptKey(concept)\n return index.aliasToCanonical.get(key) ?? key\n}\n\n/** Every key that targets the same canonical concept (the canonical key + its aliases). */\nfunction namesForCanonical(index: BacklinkIndex, canonical: string): Set<string> {\n const names = new Set<string>([canonical])\n for (const [alias, c] of index.aliasToCanonical) if (c === canonical) names.add(alias)\n return names\n}\n\n/**\n * All references to a concept — pooling the concept's own name and any of its\n * aliases (Logseq/AS Notes alias-folding), case-insensitively. Returned grouped by\n * source document: journals first (most-recent date first), then pages\n * alphabetically; references within a document in line order.\n */\nexport function backlinksFor(index: BacklinkIndex, concept: string): BacklinkGroup[] {\n const canonical = canonicalKey(index, concept)\n const names = namesForCanonical(index, canonical)\n\n const refs: BacklinkRef[] = []\n for (const name of names) refs.push(...(index.byTarget.get(name) ?? []))\n\n // Group by source document.\n const groups = new Map<string, BacklinkGroup>()\n for (const ref of refs) {\n let group = groups.get(ref.sourceConcept)\n if (!group) {\n group = { sourceConcept: ref.sourceConcept, sourceKind: ref.sourceKind, refs: [] }\n groups.set(ref.sourceConcept, group)\n }\n group.refs.push(ref)\n }\n for (const group of groups.values()) {\n group.refs.sort((a, b) => a.line - b.line || a.matchStart - b.matchStart)\n }\n\n return [...groups.values()].sort(compareGroups)\n}\n\n/** Journals first (date desc), then pages (concept asc, case-insensitive). */\nfunction compareGroups(a: BacklinkGroup, b: BacklinkGroup): number {\n if (a.sourceKind !== b.sourceKind) return a.sourceKind === 'journal' ? -1 : 1\n if (a.sourceKind === 'journal') return b.sourceConcept.localeCompare(a.sourceConcept)\n return conceptKey(a.sourceConcept).localeCompare(conceptKey(b.sourceConcept))\n}\n\n/** Total reference count across all groups (for the view's summary). */\nexport function backlinkCount(groups: BacklinkGroup[]): number {\n return groups.reduce((n, g) => n + g.refs.length, 0)\n}\n","/**\n * Adapter from the `@sqlite.org/sqlite-wasm` oo1 DB to the engine-agnostic\n * {@link SqlDb} the index query layer uses. Shared by node tests (in-memory) and the\n * browser runtime (OPFS). Keeping the SQL surface tiny is what lets the query layer\n * (index-db.ts) be node-tested without a browser.\n */\n\nimport { createSchema, type SqlDb } from './index-db'\n\n/**\n * The oo1 `Database` (and `:memory:` constructor) typed loosely at this single\n * boundary — the package's overloaded `exec` signatures are awkward to satisfy\n * structurally, and this adapter is the only place that touches them.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype Oo1Db = any\n\n/**\n * Open a fresh in-memory SQLite DB with the index schema. sqlite-wasm is loaded\n * dynamically so it is pulled into the bundle only when a graph actually opens (not\n * by every module that transitively imports the document barrel).\n *\n * NOTE: this is the **main-thread, in-memory** runtime — rebuilt from documents each\n * session, exactly like the in-memory index it replaces. OPFS/worker *persistence*\n * (ADR 0015) is the remaining runtime step; the schema, derivation, and queries are\n * identical regardless of where the DB physically lives.\n */\nexport async function openInMemorySqlDb(): Promise<SqlDb> {\n const { default: sqlite3InitModule } = await import('@sqlite.org/sqlite-wasm')\n const sqlite3 = await sqlite3InitModule()\n const db = wrapOo1Db(new sqlite3.oo1.DB(':memory:') as Oo1Db)\n createSchema(db)\n return db\n}\n\nexport function wrapOo1Db(db: Oo1Db): SqlDb {\n let closed = false\n // Overwrite freed pages with zeros. A document's rows leave the index when it is protected,\n // but without this the pages they sat on keep the text until reused - readable from the\n // OPFS file by anyone with the profile directory, which is the adversary protection is for.\n db.exec('PRAGMA secure_delete = ON')\n return {\n exec(sql) {\n db.exec(sql)\n },\n run(sql, params = []) {\n db.exec({ sql, bind: params as unknown[] })\n },\n all<T = Record<string, unknown>>(sql: string, params: unknown[] = []) {\n const resultRows: unknown[] = []\n db.exec({ sql, bind: params, rowMode: 'object', resultRows })\n return resultRows as T[]\n },\n close() {\n if (closed) return\n closed = true\n db.close()\n },\n }\n}\n","/**\n * The one rule for what counts as [[Frontmatter]] (CONTEXT.md).\n *\n * There were three, and they disagreed. `parseFrontmatter` and `frontmatterLineOffset` closed on\n * `---` only and treated an unterminated opener as no frontmatter; the editor's analysis also\n * accepted `...`, tolerated trailing whitespace on a delimiter, and treated an unterminated\n * opener as frontmatter running to the END of the document. A file closed with `...` was\n * therefore frontmatter to the editor's completion guards and plain body to storage - its\n * `title:` never read, so the document's identity silently came from its file name instead.\n *\n * The rule, stated once:\n *\n * - A line of exactly `---` at offset 0, trailing spaces or tabs allowed.\n * - Closed by the next line of exactly `---`, same allowance. **Only** `---`: `...` is a YAML\n * document terminator, but no tool this format meets writes it (Jekyll, Obsidian, Logseq,\n * AS Notes), and accepting it suppressed editor completions across whole documents on the\n * strength of a terminator nobody types.\n * - An **unterminated** opener is not Frontmatter. It is plain text until the document balances -\n * the same rule `editor-analysis.ts` applies to a freshly typed ``` fence, and for the same\n * reason: otherwise typing `---` restyles everything below it while you are still typing.\n * - CRLF tolerated throughout.\n *\n * Pure string work, no parser and no editor: the storage parse, the editor's analysis and the\n * reveal offset all measure from this, so they cannot drift apart again.\n */\n\n/** Where a document's Frontmatter sits. Offsets are into the whole document text. */\nexport interface FrontmatterSpan {\n /** Offset just past the closing delimiter's line terminator (or the end of the text). */\n end: number\n /** Lines the block occupies, both delimiters included — what a body-relative offset needs. */\n lines: number\n /** The YAML between the delimiters, terminators excluded. */\n body: string\n /** Offsets of {@link body} within the document. */\n bodyFrom: number\n bodyTo: number\n}\n\n/** A line that is exactly a `---` delimiter (trailing spaces and tabs allowed). */\nconst DELIMITER = /^---[ \\t]*\\r?$/\n\n/** Whether `line` is a delimiter line: exactly `---`, trailing spaces or tabs allowed. */\nexport function isFrontmatterDelimiter(line: string): boolean {\n return DELIMITER.test(line)\n}\n\n/** The Frontmatter of `text`, or `null` when it has none — including an unterminated opener. */\nexport function frontmatterSpan(text: string): FrontmatterSpan | null {\n const firstBreak = text.indexOf('\\n')\n // A document that is only `---` has no closing delimiter, so it is not Frontmatter.\n if (firstBreak === -1 || !DELIMITER.test(text.slice(0, firstBreak))) return null\n\n const bodyFrom = firstBreak + 1\n let lineStart = bodyFrom\n let lines = 1\n while (lineStart <= text.length) {\n const nextBreak = text.indexOf('\\n', lineStart)\n const lineEnd = nextBreak === -1 ? text.length : nextBreak\n lines += 1\n if (DELIMITER.test(text.slice(lineStart, lineEnd))) {\n // The body stops before the newline that begins the closing delimiter's line - and\n // before that line's `\\r` under CRLF, which belongs to the terminator, not the YAML.\n let bodyTo = Math.max(bodyFrom, lineStart - 1)\n if (bodyTo > bodyFrom && text[bodyTo - 1] === '\\r') bodyTo -= 1\n return {\n end: nextBreak === -1 ? text.length : nextBreak + 1,\n lines,\n body: text.slice(bodyFrom, bodyTo),\n bodyFrom,\n bodyTo,\n }\n }\n if (nextBreak === -1) break\n lineStart = nextBreak + 1\n }\n return null // unterminated: plain text until the document balances\n}\n\n/**\n * How many lines a document's [[Frontmatter]] occupies - opener and closer included - given the\n * document already split into lines, or 0 when it has none. The same rule as {@link frontmatterSpan}\n * (a lone `---` is not a block; an unterminated one is not a block), for the line-shaped consumers\n * in the editor that treat the block as opaque: the outliner's scans, the bullet dots, the guides,\n * the clamp. Those already hold `lines`, and re-joining them per keystroke to ask the text form\n * would be the only O(n) step in an otherwise line-local pass.\n */\nexport function frontmatterLines(lines: readonly string[]): number {\n if (lines.length < 2 || !DELIMITER.test(lines[0])) return 0\n for (let i = 1; i < lines.length; i++) {\n if (DELIMITER.test(lines[i])) return i + 1\n }\n return 0\n}\n","/**\n * Pure logic for [[Fenced Code Block]]s (ADR 0018, ADR 0020). No CodeMirror imports — the\n * syntax-tree / view glue lives in `view/outliner-context.ts` and `view/fence-guard.ts`.\n *\n * A fence is a run of three or more backticks; the run length is mirrored on completion so\n * a 4-backtick fence balances with 4 (lets a 3-backtick block sit inside one) — and ONLY with\n * 4: a closer is exactly the opener's length, never merely \"at least\" it as CommonMark allows,\n * so a longer opener below can never be taken as a shorter block's closer (ADR 0020, amended\n * 2026-09-12). The optional info-string after the opener selects the language.\n */\n\n/** A clean opener line is exactly a backtick run (3+) and an optional info-string. */\nconst FENCE_OPENER = /^(`{3,})\\s*([\\w+#.-]*)\\s*$/\n/** A fence line (opener or closer): a bare backtick/tilde run. */\nconst FENCE_LINE = /^(`{3,}|~{3,})\\s*$/\n/** A backtick fence line, tolerant of a leading plain-bullet marker (`- `) so `- ``` ` is a fence.\n * Backticks only (this slice) — tildes are left untouched, so they are not auto-completed. */\nconst FENCE_LINE_INFO = /^(\\s*(?:-\\s)?)(`{3,})\\s*(\\S*)\\s*$/\n\nexport interface FenceInfo {\n /** Column where the backtick/tilde run starts (after any indent + bullet marker) — the clamp edge. */\n col: number\n /** The full fence run, e.g. ` ``` ` or ` ```` `. */\n run: string\n /** The fence character: `` ` `` or `~`. */\n char: string\n /** Length of the run (3+). */\n len: number\n /** The info-string after the run; `''` for a bare fence (a closer is always bare). */\n info: string\n /** The fence sits on a bullet line (`- ``` `) — so it OPENS a block (form-1) and can never be a\n * closer: a sibling bullet's opener directly below must not be mistaken for this one's closer. */\n bullet: boolean\n}\n\n/** Parse a line as a fence line (opener or closer), tolerant of a leading `- ` bullet. Null if not. */\nexport function fenceLineInfo(line: string): FenceInfo | null {\n const m = FENCE_LINE_INFO.exec(line)\n if (!m) return null\n return { col: m[1].length, run: m[2], char: m[2][0], len: m[2].length, info: m[3], bullet: m[1].includes('-') }\n}\n\n/** Whether `lines[a]` would close an open fence `b` (same char, run of the SAME length, no info, and not\n * itself a bullet-line opener). */\nfunction closes(a: FenceInfo, open: FenceInfo): boolean {\n return a.info === '' && !a.bullet && a.char === open.char && a.len === open.len\n}\n\nexport interface FencedBlockRange {\n /** 0-based line index of the opening fence. */\n start: number\n /** 0-based line index of the closing fence. */\n end: number\n /** Column where the fence/content sits (the clamp edge). */\n fenceColumn: number\n}\n\n/**\n * The **complete** fenced blocks (opener paired with a closer) by line index — **balance-aware** and\n * **outliner-block-scoped**, so styling never bleeds past where a block actually ends. An unterminated\n * opener is omitted entirely (its region stays unstyled until a closer completes it).\n *\n * Pairing (a fence run is 3+ backticks; tildes are not handled this slice):\n * - A **closer** is a BARE fence (no info-string) at the SAME column as its opener and of the SAME\n * length. Each closer is consumed once — it can't also close a fence higher up. Exact length\n * (not CommonMark's \"at least\") is what keeps a ```` block below a stray ``` intact: the app\n * mirrors lengths on completion, so nothing legitimate is lost, and a longer opener is never eaten.\n * - A fence with an **info-string** is unambiguously an OPENER, never a closer: an unterminated opener\n * therefore can't swallow the start of an already-completed block below it (the reported bug).\n * - Pairing is **column-scoped** (one outliner block). A deeper fence (greater column) opens a nested\n * block that completes independently; a non-blank line that **dedents below** an open fence's column\n * leaves that block, so the fence can no longer be closed (its closer must live in the same block);\n * a same-column fence that is LONGER than an unterminated opener — bare or not — or an equal-length\n * info opener *replaces* it instead of nesting: a longer run can never be a shorter block's content\n * (CommonMark would close the block with it), so it is the next block's opener, and the shorter\n * opener above it was a stray. That is what keeps a ```` block whole beneath a dangling ```.\n * - A shorter same-column fence is literal content (e.g. a ``` block inside a ```` one). To hold a\n * fence as content, the outer fence must be the longer one — the CommonMark convention.\n *\n * `closerTolerance` (default 0 — strict) lets a bare closer sit up to N columns RIGHT of its opener\n * and still pair. Only the source hard-guard uses it (with CommonMark's 3), to recognise a closer\n * that a raw edit/paste nudged right so it can snap it back; every other consumer stays col-exact.\n */\nexport function fencedBlocks(lines: readonly string[], closerTolerance = 0): FencedBlockRange[] {\n const blocks: FencedBlockRange[] = []\n const stack: { info: FenceInfo; index: number }[] = []\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i]\n if (line.trim() !== '') {\n // A non-blank line below an open fence's column has left that fence's block — abandon it.\n const indent = line.length - line.trimStart().length\n while (stack.length && stack[stack.length - 1].info.col > indent) stack.pop()\n }\n const f = fenceLineInfo(line)\n if (!f) continue\n const top = stack.length ? stack[stack.length - 1] : null\n if (top && f.col >= top.info.col && f.col - top.info.col <= closerTolerance && closes(f, top.info)) {\n stack.pop()\n blocks.push({ start: top.index, end: i, fenceColumn: top.info.col })\n } else if (!top || f.col > top.info.col) {\n stack.push({ info: f, index: i }) // a fresh opener, or a nested block in a deeper column\n } else if (f.col === top.info.col && (f.len > top.info.len || (f.info !== '' && f.len === top.info.len))) {\n stack.pop() // an unterminated opener can't contain a longer fence, or an info opener, at its column\n stack.push({ info: f, index: i })\n }\n // else: a shorter same-column fence, or a bare fence that closes nothing — literal content, ignore.\n }\n return blocks\n}\n\n/** A fence line with its backtick/tilde run defanged (indent and bullet marker kept), so a re-scan\n * pairs the other fences as if this one were plain text. */\nexport function defangFence(line: string): string {\n return line.replace(/[`~]+/, 'x')\n}\n\n/**\n * Whether the fence line at `index` is an **unterminated opener** — the one shape Enter completes.\n * This is fence completion's balance check. A fence a complete block already accounts for — its\n * opener, whether the block is empty or holds content, its closer, or a shorter fence that is its\n * content — is never completed again: doing so grew a second closer under the opener and orphaned the\n * real one (found live: Enter at the end of a content-bearing block's opener). Pairing is the\n * column-scoped scan ({@link fencedBlocks}), so stray or orphaned fences elsewhere in the document\n * never make a fresh opener look paired.\n *\n * One refinement for the moment of typing. Pairing is first-with-next, so an opener typed above an\n * existing block swallows it: a prose opener takes the block's bare opener as its closer, and a bullet\n * opener (which no bullet fence can close) runs on to the block's own closer. Setting the candidate\n * aside and re-pairing tells the two apart by TALLY, counting complete blocks from the candidate down:\n * a genuine opener's block dissolves, so the tally drops — even when its orphaned closer then heads a\n * block of its own by taking the next equal-length opener below, that re-pairing runs one fence\n * short — whereas a fresh opener that had swallowed the block below leaves\n * the tally intact, because setting it aside merely hands that block its own opener back. Asking\n * instead whether any block *starts inside* the candidate's conflated the two (live, 2026-09-11: Enter\n * on the opener of a complete block grew a second closer whenever an equal block followed).\n * Blocks starting above the candidate are left out of the tally: a stray opener up there adopting the\n * candidate's closer says nothing about the candidate. `pending` — the analysis's freshly typed,\n * still unclosed fence — is set aside the same way, as every other fence consumer does, and is itself\n * unterminated by definition.\n */\nexport function isUnterminatedOpener(lines: readonly string[], index: number, pending: number | null = null): boolean {\n if (!fenceLineInfo(lines[index])) return false\n if (pending === index) return true\n const scanned = pending === null ? lines : lines.map((l, i) => (i === pending ? defangFence(l) : l))\n const paired = fencedBlocks(scanned)\n const block = paired.find((b) => b.start <= index && index <= b.end)\n if (!block) return true // pairs with nothing\n if (block.start !== index) return false // its closer, or a shorter fence that is content\n const without = fencedBlocks(scanned.map((l, i) => (i === index ? defangFence(l) : l)))\n const tally = (blocks: readonly FencedBlockRange[], from: number) => blocks.filter((b) => b.start >= from).length\n return tally(without, index + 1) >= tally(paired, index)\n}\n\n/** Parse a line as a fence opener; null if it is not a clean opener. Leading indent is ignored. */\nexport function parseFenceOpener(line: string): { ticks: string; info: string } | null {\n const m = FENCE_OPENER.exec(line.trimStart())\n if (!m) return null\n return { ticks: m[1], info: m[2] }\n}\n\nexport interface FenceCompletion {\n /** Text to insert at the end of the opener line (which already holds the indent + ticks). */\n insert: string\n /** Caret position as a character offset into {@link insert}. */\n caretOffset: number\n}\n\n/**\n * Build the text that completes a fence on Enter: the (optional) default info-string to finish\n * the opener line, an empty middle line, and a balanced closer — all at `indent` (the content\n * column for a bullet, `''` for prose). Assumes the caret is at the end of the opener line.\n */\nexport function buildFenceCompletion(opts: {\n indent: string\n ticks: string\n info: string\n defaultLang: string\n}): FenceCompletion {\n const { indent, ticks, info, defaultLang } = opts\n const head = info === '' ? defaultLang : '' // finish the opener line with the default lang when bare\n const insert = `${head}\\n${indent}\\n${indent}${ticks}`\n return { insert, caretOffset: `${head}\\n${indent}`.length }\n}\n\n/**\n * Normalise a fenced block's lines so it stays valid markdown (ADR 0020 hard guard): pad any\n * under-indented content line up to `fenceColumn`, and clamp + rebalance the **closing** fence\n * (the last line, when it is a bare fence) to `fenceColumn` + the opener's tick count. The opener\n * (line 0) is left untouched, as are blank lines and any fence-looking *content* lines (so a\n * 3-backtick block can live inside a 4-backtick one).\n */\nexport function normaliseFenceLines(lines: string[], fenceColumn: number, openerTicks: string): string[] {\n const pad = ' '.repeat(fenceColumn)\n const last = lines.length - 1\n return lines.map((line, i) => {\n if (i === 0) return line\n const trimmed = line.trimStart()\n if (i === last && FENCE_LINE.test(trimmed)) return pad + openerTicks\n if (trimmed === '') return line\n const indent = line.length - trimmed.length\n return indent < fenceColumn ? pad + trimmed : line\n })\n}\n","/**\n * The **Indent Unit** (CONTEXT.md; ADR 0067): one level of [[Block]] nesting is two spaces in the\n * markdown source, for every knowledge graph. There is no per-graph setting - [[Import]] and paste\n * normalise foreign text onto the grid, the editor writes it, and text on any other grid (a\n * four-space vault, a tab-indented Logseq page, a file edited in another tool) is read\n * **structurally**: a bullet is a child of the nearest open bullet with a strictly smaller indent\n * and a sibling of one with an equal indent. That one walk is shared by the normaliser, the block\n * model and the editor's per-line depth, so import, index and rendering can never disagree about\n * what is a child of what.\n *\n * Only `-` bullets are nodes here, as everywhere in EtherPK. Fenced code and [[Frontmatter]] are\n * opaque: a `- item` inside them is code or YAML, and a fenced block travels whole with the bullet\n * that owns it.\n *\n * Two measures of leading whitespace, one per side of the boundary. The **normaliser** measures\n * **columns**, a tab advancing to the next multiple of four as CommonMark reads it — that is what\n * makes a tab-indented Logseq page or a mixed vault come out right. The **editor and the block\n * model** measure **characters**, because everything else in the editor does (the guides, the\n * keymap's branch and sibling primitives, the caret clamp, a fence's column): one measure inside\n * the editor is what keeps the clamp, the guides and the keys reading the same tree from the same\n * text, and text that has been through the boundary holds no tabs, so the two measures agree on\n * it. A tab-indented file that reaches the editor unnormalised is read as a consistent\n * one-character grid.\n */\n\nimport { frontmatterLines } from '$lib/storage/fs/frontmatter-span'\n\nimport { type FencedBlockRange, fencedBlocks } from './fenced-code'\n\n/** Spaces per nesting level in the markdown source. */\nexport const INDENT_UNIT = 2\n\n/** The Indent Unit as text - what Tab inserts. */\nexport const INDENT = ' '.repeat(INDENT_UNIT)\n\n/** Width of the `- ` marker, which sets a bullet's content column (`indent + MARKER_WIDTH`). */\nconst MARKER_WIDTH = 2\n\n/** A tab advances to the next multiple of this column, as CommonMark reads it. */\nconst TAB_STOP = 4\n\n/** Whether the line is a `- ` bullet or task, tolerant of leading whitespace of either kind. */\nfunction isBullet(line: string): boolean {\n return /^[ \\t]*-\\s/.test(line)\n}\n\n/** A heading line, tolerant of leading whitespace — a node in its own right, never a continuation. */\nfunction isHeading(line: string): boolean {\n return /^[ \\t]*#{1,6}\\s/.test(line)\n}\n\n/** Whether the line is whitespace-only (an empty line included). */\nfunction isBlank(line: string): boolean {\n return line.trim() === ''\n}\n\n/** The leading spaces and tabs of a line. */\nfunction leading(line: string): string {\n return /^[ \\t]*/.exec(line)![0]\n}\n\n/** The width of a line's leading whitespace in columns, tabs advancing to the next tab stop. */\nexport function indentColumns(line: string): number {\n let col = 0\n for (const ch of leading(line)) col = ch === '\\t' ? col + TAB_STOP - (col % TAB_STOP) : col + 1\n return col\n}\n\n/** The width of a line's leading whitespace in characters — the editor's measure (module comment). */\nfunction indentChars(line: string): number {\n return leading(line).length\n}\n\n/** How a walk measures leading whitespace. */\ntype Measure = (line: string) => number\n\n/** One line's place in the outline, as the walk reads it. */\nexport interface OutlineLine {\n /**\n * Nesting depth of the line's node, or of the node that owns it. A top-level bullet or prose\n * line is 0; a continuation line, a fenced code line and an indented soft line take their\n * owner's depth. Lines outside any node (a bare blank line, frontmatter) are 0.\n */\n depth: number\n /** 0-based index of the **bullet** whose block this line belongs to (a bullet owns itself), or -1. */\n owner: number\n}\n\ninterface Node {\n /** The raw indent this node was read at, in columns. */\n raw: number\n /** The indent it is written at once normalised. */\n written: number\n depth: number\n /** Line index of the node's bullet, or -1 for a prose node. */\n bullet: number\n}\n\ninterface Visit {\n depth: number\n owner: number\n /** Columns to shift the line (and, for a fenced block, every line of it) onto the grid. */\n delta: number\n /** Lines this visit covers: a fenced block's whole extent from its opener, else 1. */\n span: number\n}\n\n/**\n * The structural walk under both {@link outlineLines} and {@link normaliseIndentUnit}: visits each\n * line in order with the node stack resolved, reporting the line's owner (the nearest open bullet\n * whose block the line is inside) and the delta that would put it on the grid.\n *\n * Nodes are bullets and **unowned** non-blank lines (prose, headings): a deeper plain line nests\n * under the plain line before it, as the block model has always read it, but a plain line never\n * owns a fenced block or a soft line - only a bullet does. A non-bullet, non-heading line that sits\n * at or past the nearest open bullet's content column is that bullet's [[Continuation Line]] and is\n * not a node; a heading is always a node, as the outliner's boundary rules read it.\n * A blank line pops what it is shallower than and belongs to what remains, so an indented soft line\n * stays inside its block while a flush-left empty line closes every open branch (indent decides,\n * ADR 0021). Fenced blocks are visited as one unit at the opener; frontmatter lines are visited as\n * depth-0 lines owned by nothing.\n */\nfunction walk(lines: readonly string[], blocks: readonly FencedBlockRange[], measure: Measure, visit: (index: number, info: Visit) => void): void {\n const starts = new Map<number, FencedBlockRange>()\n for (const b of blocks) starts.set(b.start, b)\n const stack: Node[] = []\n const top = (): Node | null => (stack.length ? stack[stack.length - 1] : null)\n const popTo = (indent: number) => {\n while (stack.length && stack[stack.length - 1].raw >= indent) stack.pop()\n }\n /** The nearest open bullet on the stack, or null when only prose nodes (or nothing) remain. */\n const owningBullet = (): Node | null => {\n for (let k = stack.length - 1; k >= 0; k--) if (stack[k].bullet >= 0) return stack[k]\n return null\n }\n const first = frontmatterLines(lines)\n for (let i = 0; i < first; i++) visit(i, { depth: 0, owner: -1, delta: 0, span: 1 })\n for (let i = first; i < lines.length; i++) {\n const line = lines[i]\n const block = starts.get(i)\n const span = block ? block.end - i + 1 : 1\n const indent = measure(line)\n popTo(indent)\n if (isBlank(line) && !block) {\n // Indent decides: what this blank is not shallower than owns it.\n const owner = owningBullet()\n visit(i, { depth: top()?.depth ?? 0, owner: owner ? owner.bullet : -1, delta: owner ? owner.written - owner.raw : 0, span })\n continue\n }\n if (isBullet(line)) {\n const parent = top()\n const node: Node = { raw: indent, written: parent ? parent.written + INDENT_UNIT : 0, depth: stack.length, bullet: i }\n stack.push(node)\n visit(i, { depth: node.depth, owner: i, delta: node.written - node.raw, span })\n i += span - 1 // a form-1 opener's fence lines travel with the bullet\n continue\n }\n const parent = top()\n if (parent && parent.bullet >= 0 && indent >= parent.raw + MARKER_WIDTH && !isHeading(line)) {\n // A continuation line (a soft line, a form-2 fenced block) inside the bullet's block. A heading\n // is never one: the outliner reads it as a boundary wherever it sits, and so does this walk.\n visit(i, { depth: parent.depth, owner: parent.bullet, delta: parent.written - parent.raw, span })\n i += span - 1\n continue\n }\n if (block) {\n // A fenced block owned by no bullet (top-level code, code under a prose line) is verbatim\n // text, not a node, and is never moved.\n visit(i, { depth: parent ? parent.depth + 1 : 0, owner: -1, delta: 0, span })\n i += span - 1\n continue\n }\n // A prose node: written where it was read - a plain line's indent is never rewritten, since\n // at four columns outside a list it is an indented code block in CommonMark.\n const node: Node = { raw: indent, written: indent, depth: stack.length, bullet: -1 }\n stack.push(node)\n visit(i, { depth: node.depth, owner: -1, delta: 0, span })\n }\n}\n\n/**\n * Per-line depth and owner for every line of the document ({@link OutlineLine}), measured in\n * characters as the editor and the block model measure (module comment).\n */\nexport function outlineLines(lines: readonly string[], blocks: readonly FencedBlockRange[] = fencedBlocks(lines)): OutlineLine[] {\n const out = new Array<OutlineLine>(lines.length)\n walk(lines, blocks, indentChars, (i, { depth, owner, span }) => {\n for (let k = i; k < i + span; k++) out[k] = { depth, owner }\n })\n return out\n}\n\n/**\n * Rewrite a line's leading whitespace: the part of it lying within the first `within` columns is\n * re-measured (a tab advancing to its stop) and written as spaces, shifted by `delta` and floored\n * at column 0; whatever leading whitespace lies beyond `within` is kept verbatim. Every line\n * outside a fence passes `Infinity`: its whole indent becomes columns. Inside a fenced block\n * `within` is the fence column, so the structural indent up to the fence is put in spaces (a\n * fence pairs only with a closer at its own column, and a moved block must keep pairing) while a\n * tab in the code itself, past the fence column, stays a tab.\n */\nfunction relead(line: string, within: number, delta: number): string {\n let col = 0\n let chars = 0\n for (const ch of leading(line)) {\n if (col >= within) break\n col = ch === '\\t' ? col + TAB_STOP - (col % TAB_STOP) : col + 1\n chars++\n }\n return ' '.repeat(Math.max(0, col + delta)) + line.slice(chars)\n}\n\n/**\n * The document with every bullet on the Indent Unit grid: each bullet one unit under its parent\n * (a top-level bullet at column 0, over-nesting collapsed), its continuation lines, soft lines and\n * fenced block moved by the same amount so their indentation beyond the content column is kept\n * (an indented code block inside a bullet stays code). Tabs in leading whitespace become columns\n * (inside a fenced block only up to the fence column: a tab in the code is code); a line owned\n * by no bullet keeps its indent otherwise. Frontmatter is never touched. A document already on\n * the grid comes back byte-identical, so callers may compare by reference.\n */\nexport function normaliseIndentUnit(text: string): string {\n const lines = text.split('\\n')\n const out = lines.slice()\n const frontmatter = frontmatterLines(lines)\n let changed = false\n walk(lines, fencedBlocks(lines), indentColumns, (i, { delta, span }) => {\n if (i < frontmatter) return // YAML: every indent is metadata\n const within = span > 1 ? indentColumns(lines[i]) : Infinity\n for (let k = i; k < i + span; k++) {\n const next = relead(out[k], within, delta)\n if (next !== out[k]) {\n out[k] = next\n changed = true\n }\n }\n })\n return changed ? out.join('\\n') : text\n}\n","/**\n * The unified derived-block model (ADR 0016): blocks inferred from the markdown\n * source, never stored. One tree from three interlocking signals —\n *\n * 1. **Heading level** (`#` > `##` > `###`) is the outer spine: everything between\n * a heading and the next equal-or-higher heading nests beneath it.\n * 2. **Bullet / plain-line indentation** nests further within a section: a line\n * is one deeper than the nearest open line with a smaller indent (the Indent\n * Unit walk, ADR 0067), so a four-space or tab-indented file reads the same\n * tree as a two-space one.\n * 3. **Continuation lines** — non-bullet lines indented to a bullet's content\n * column belong to the *same* block (the soft-newline-within-a-block).\n *\n * A block has a kind (heading / paragraph / bullet / task). Blocks have no\n * persistent identity: a block is its source range in the current parse.\n *\n * Pure and DOM-free; the editor (keyboard ops, fold) and the derived index consume\n * it. See Dual Mode Editor.md.\n */\n\nimport { outlineLines } from './indent-unit'\n\nexport type BlockKind = 'heading' | 'paragraph' | 'bullet' | 'task'\n\nexport interface Block {\n type: BlockKind\n /**\n * Structural nesting depth (0 = top level), read from the indentation by the\n * Indent Unit walk. Headings are recorded at their depth too (usually 0); their\n * *hierarchy* comes from {@link level}, not this.\n */\n depth: number\n /** Heading rank: the `#` count (1–6). Only set for `heading` blocks. */\n level?: number\n /** Completion state. Only set for `task` blocks. */\n done?: boolean\n /** Source text — own lines (incl. continuation lines), indentation trimmed, joined by `\\n`. */\n text: string\n /** 0-based inclusive source line range of this block's *own* lines (incl. continuations). */\n startLine: number\n endLine: number\n children: Block[]\n}\n\nfunction indentOf(line: string): number {\n return line.length - line.trimStart().length\n}\n\nfunction headingLevel(line: string): number | undefined {\n const m = /^(#{1,6})\\s/.exec(line.trimStart())\n return m ? m[1].length : undefined\n}\n\nfunction isBullet(line: string): boolean {\n return /^-\\s/.test(line.trimStart())\n}\n\n/** A bullet whose content is `[ ]` / `[x]` is a task; returns its done state. */\nfunction taskDone(line: string): boolean | undefined {\n const m = /^-\\s+\\[([ xX])\\]/.exec(line.trimStart())\n if (!m) return undefined\n return m[1].toLowerCase() === 'x'\n}\n\ntype Flat = Omit<Block, 'children'>\n\n/**\n * Parse markdown into a tree of derived blocks (ADR 0016).\n *\n * {@link Block.depth} is **structural** (ADR 0067): a line is one deeper than the nearest open\n * line with a strictly smaller indent, whatever grid the text is on — the same walk the editor\n * renders from (`indent-unit.ts`), so the index and the editor never disagree about a four-space\n * or tab-indented document. The content column for continuation lines stays marker-based\n * (`indent + 2`).\n */\nexport function parseBlocks(markdown: string): Block[] {\n const lines = markdown.split('\\n')\n const outline = outlineLines(lines)\n\n // 1) Flat blocks in document order.\n const flat: Flat[] = []\n let i = 0\n while (i < lines.length) {\n if (lines[i].trim() === '') {\n i++\n continue\n }\n const depth = outline[i].depth\n const level = headingLevel(lines[i])\n\n if (level !== undefined) {\n flat.push({\n type: 'heading',\n depth,\n level,\n text: lines[i].trimStart(),\n startLine: i,\n endLine: i,\n })\n i++\n continue\n }\n\n if (isBullet(lines[i])) {\n // A bullet/task: gather continuation lines — non-blank, non-heading,\n // non-bullet lines indented to (or past) the marker's content column.\n const contentCol = indentOf(lines[i]) + 2 // `- `\n const done = taskDone(lines[i])\n const start = i\n const buf: string[] = [lines[i].trimStart()]\n i++\n while (\n i < lines.length &&\n lines[i].trim() !== '' &&\n headingLevel(lines[i]) === undefined &&\n !isBullet(lines[i]) &&\n indentOf(lines[i]) >= contentCol\n ) {\n buf.push(lines[i].trimStart())\n i++\n }\n flat.push({\n type: done === undefined ? 'bullet' : 'task',\n depth,\n ...(done === undefined ? {} : { done }),\n text: buf.join('\\n'),\n startLine: start,\n endLine: i - 1,\n })\n continue\n }\n\n // A paragraph: contiguous non-blank, non-heading, non-bullet lines at the\n // same indentation.\n const start = i\n const buf: string[] = [lines[i].trimStart()]\n i++\n while (\n i < lines.length &&\n lines[i].trim() !== '' &&\n headingLevel(lines[i]) === undefined &&\n !isBullet(lines[i]) &&\n indentOf(lines[i]) === indentOf(lines[start])\n ) {\n buf.push(lines[i].trimStart())\n i++\n }\n flat.push({ type: 'paragraph', depth, text: buf.join('\\n'), startLine: start, endLine: i - 1 })\n }\n\n // 2) Build the tree (ADR 0016 stack rule):\n // - a heading pops every open non-heading block and every heading of >= its level;\n // - a non-heading pops every open non-heading block of >= its indentation depth\n // (never a heading — it nests under the enclosing section).\n const roots: Block[] = []\n const stack: Block[] = []\n for (const f of flat) {\n const block: Block = { ...f, children: [] }\n if (block.type === 'heading') {\n const level = block.level!\n while (\n stack.length > 0 &&\n (stack[stack.length - 1].type !== 'heading' || stack[stack.length - 1].level! >= level)\n ) {\n stack.pop()\n }\n } else {\n while (\n stack.length > 0 &&\n stack[stack.length - 1].type !== 'heading' &&\n stack[stack.length - 1].depth >= block.depth\n ) {\n stack.pop()\n }\n }\n if (stack.length === 0) roots.push(block)\n else stack[stack.length - 1].children.push(block)\n stack.push(block)\n }\n return roots\n}\n","/**\n * The one name for the protected fence, and the one way to recognise one in raw text.\n *\n * Deliberately dependency-free. The [[Derived Index]] worker must be able to ask \"is this a\n * protected fence?\" without pulling in the crypto stack (Argon2id, the envelope, the key record),\n * and the answer must be the same string the editor and the storage layer use — a second regex\n * somewhere else is how a guard silently stops guarding.\n */\n\n/** The info-string that marks a fence as protected content. */\nexport const CIPHER_FENCE_INFO = 'etherpk-cipher'\n\n/**\n * Whether any line of `text` opens a cipher fence. Multiline and deliberately generous: an index\n * unit that contains a cipher fence *anywhere* is excluded whole, rather than the index trying to\n * reason about where the fence starts and stops.\n */\nexport function containsCipherFence(text: string): boolean {\n return CIPHER_FENCE_OPENER.test(text)\n}\n\n// Tolerates a leading bullet marker (`- ```etherpk-cipher`), matching fenceLineInfo's own rule,\n// and anything after the info-string, so a fence written by a newer client still counts.\nconst CIPHER_FENCE_OPENER = new RegExp(`^\\\\s*(?:-\\\\s)?\\`{3,}\\\\s*${CIPHER_FENCE_INFO}\\\\b`, 'mu')\n","/**\n * The [[Task Tag]] grammar (ADR 0032), as one pure parser.\n *\n * A [[Task]]'s metadata is a **contiguous leading run of hash-tags immediately after the\n * checkbox**, and nothing else on the line is metadata. Parsing therefore stops dead at the\n * first token that is not a tag - which is the whole point of the grammar: `#hashtags` in\n * prose stay prose, so a task can talk about `#P1 incidents` without claiming to be one.\n *\n * Lives here rather than inside the index derivation because three consumers need the same\n * answer: the derivation (which stores the parsed values as columns), the [[Tasks View]]\n * (which strips the run for display), and - when they land - the editor's tag rendering and\n * kanban [[Lane]]s. ADR 0032 is explicit that those should parse this grammar rather than\n * invent a parallel one.\n *\n * Pure and DOM-free.\n */\n\n/** Priority is a three-level scale; ADR 0032 dropped the below-normal levels at import. */\nexport type TaskPriority = 1 | 2 | 3\n\nexport interface TaskTags {\n priority: TaskPriority | null\n waiting: boolean\n /** `#D` — doing (in progress). One of ADR 0032's three minted extensions; dated `#D-` is due. */\n doing: boolean\n /** `#C` — minted; a cancelled task is *checked*, so the tag carries the truth. */\n cancelled: boolean\n /** `YYYY-MM-DD`, or null. Kept as a string: it sorts and compares correctly as one. */\n due: string | null\n completion: string | null\n /** `#S-` — minted, from Logseq's SCHEDULED. */\n scheduled: string | null\n /** The text with the leading tag run removed — what a task list displays. */\n text: string\n}\n\nconst EMPTY: Omit<TaskTags, 'text'> = {\n priority: null,\n waiting: false,\n doing: false,\n cancelled: false,\n due: null,\n completion: null,\n scheduled: null,\n}\n\n/**\n * Tags are **case-sensitive** (ADR 0032), so `#p1` and `#d` are prose, not metadata.\n * Dates are shape-matched, not calendar-validated: `#D-2026-13-45` is still a date-shaped\n * tag. Shape is what makes string ordering equal date ordering, and a wrong date is the\n * user's to see and fix, not ours to silently drop.\n */\nconst DATE = /^(\\d{4}-\\d{2}-\\d{2})$/\n\n/** Where the checkbox ends on a raw task line, or -1 when the line is not a task. */\nconst CHECKBOX = /^\\s*-\\s+\\[([ xX])\\]\\s?/\n\ntype TagKind = 'priority' | 'waiting' | 'doing' | 'cancelled' | 'due' | 'completion' | 'scheduled'\n\n/** Read one token as a tag, or null if it is not one (which ends the run). */\nfunction readTag(token: string): { kind: TagKind; value: string } | null {\n if (token === '#W') return { kind: 'waiting', value: '' }\n // A bare letter is a state; the same letter with a date (matched below) is a date. So\n // `#D` is doing where `#D-YYYY-MM-DD` is due, and `#C` is cancelled where `#C-YYYY-MM-DD`\n // is completion. The words the two were minted as (`#doing`, `#cancelled`) are NOT read:\n // ADR 0032 re-minted them before any production graph carried them.\n if (token === '#D') return { kind: 'doing', value: '' }\n if (token === '#C') return { kind: 'cancelled', value: '' }\n if (token === '#P1' || token === '#P2' || token === '#P3') {\n return { kind: 'priority', value: token.slice(2) }\n }\n const dated = /^#([DCS])-(.+)$/.exec(token)\n if (dated && DATE.test(dated[2])) {\n const kind = dated[1] === 'D' ? 'due' : dated[1] === 'C' ? 'completion' : 'scheduled'\n return { kind, value: dated[2] }\n }\n return null\n}\n\n/**\n * Parse the leading tag run of a task's text — the label, i.e. the line with its `- [ ]`\n * marker already stripped. {@link parseTaskLine} handles a raw source line instead.\n *\n * **First of a kind wins** (ADR 0032): a second `#P2` after a `#P1` is still a tag, so it\n * does not end the run, but it is ignored. That way a stray duplicate costs the user the\n * tag, never the rest of their metadata.\n */\nexport function parseTaskTags(label: string): TaskTags {\n const tags = { ...EMPTY }\n // Tokens are whitespace-separated; `rest` tracks where the run ended in the ORIGINAL\n // string so the returned text keeps its own spacing rather than a re-joined guess.\n let rest = label\n for (;;) {\n const match = /^(\\s*)(\\S+)/.exec(rest)\n if (!match) break\n const tag = readTag(match[2])\n if (!tag) break\n switch (tag.kind) {\n case 'priority':\n if (tags.priority === null) tags.priority = Number(tag.value) as TaskPriority\n break\n case 'waiting':\n tags.waiting = true\n break\n case 'doing':\n tags.doing = true\n break\n case 'cancelled':\n tags.cancelled = true\n break\n case 'due':\n if (tags.due === null) tags.due = tag.value\n break\n case 'completion':\n if (tags.completion === null) tags.completion = tag.value\n break\n case 'scheduled':\n if (tags.scheduled === null) tags.scheduled = tag.value\n break\n }\n rest = rest.slice(match[0].length)\n }\n return { ...tags, text: rest.trimStart() }\n}\n\n/** A raw source line's task state, or null when the line is not a [[Task]]. */\nexport interface ParsedTaskLine extends TaskTags {\n done: boolean\n}\n\n/**\n * Parse a whole source line as a task. Returns null for anything that is not a task line,\n * which is what makes this usable as the [[Tasks View]]'s stale-line guard: a line that no\n * longer parses as a task is proof the index has drifted from the document.\n */\nexport function parseTaskLine(line: string): ParsedTaskLine | null {\n const marker = CHECKBOX.exec(line)\n if (!marker) return null\n return { done: marker[1].toLowerCase() === 'x', ...parseTaskTags(line.slice(marker[0].length)) }\n}\n\n/**\n * Flip a task line's checkbox, preserving everything else about the line byte for byte —\n * indentation, marker spacing, tag run and text. Returns null when the line is not a task.\n */\nexport function toggleTaskLine(line: string, done: boolean): string | null {\n const marker = /^(\\s*-\\s+\\[)([ xX])(\\])/.exec(line)\n if (!marker) return null\n return line.slice(0, marker[1].length) + (done ? 'x' : ' ') + line.slice(marker[1].length + 1)\n}\n\n/** A change to make to a [[Task]]'s tag run, as the `#` helper issues them. */\nexport type TaskTagCommand =\n | { kind: 'priority'; value: TaskPriority }\n | { kind: 'waiting' }\n | { kind: 'doing' }\n | { kind: 'cancelled' }\n | { kind: 'due' | 'scheduled' | 'completion'; value: string }\n\n/** The order tags are written back in. Fixed, so the same set always reads the same way. */\nconst TAG_ORDER = ['priority', 'waiting', 'doing', 'cancelled', 'scheduled', 'due', 'completion'] as const\n\nfunction serialiseTags(tags: TaskTags): string[] {\n const out: string[] = []\n for (const kind of TAG_ORDER) {\n if (kind === 'priority' && tags.priority !== null) out.push(`#P${tags.priority}`)\n else if (kind === 'waiting' && tags.waiting) out.push('#W')\n else if (kind === 'doing' && tags.doing) out.push('#D')\n else if (kind === 'cancelled' && tags.cancelled) out.push('#C')\n else if (kind === 'scheduled' && tags.scheduled !== null) out.push(`#S-${tags.scheduled}`)\n else if (kind === 'due' && tags.due !== null) out.push(`#D-${tags.due}`)\n else if (kind === 'completion' && tags.completion !== null) out.push(`#C-${tags.completion}`)\n }\n return out\n}\n\n/**\n * Apply one tag change to a task line, returning the rewritten line (null if not a task).\n *\n * The semantics are AS Notes': issuing a **state** tag that is already set removes it, so the\n * same menu row both sets and clears — which is what makes the helper usable without a\n * separate \"remove\" list. Issuing a *different* priority replaces the current one, because\n * only the first of a kind counts (ADR 0032) and leaving both would silently ignore one.\n * A **date** tag always replaces: picking a date means that date.\n *\n * The run is rewritten in a fixed order rather than appended to, so a task's metadata reads\n * the same way whatever order it was set in. That does reorder tags the user typed by hand —\n * accepted deliberately: the run is an unordered set by definition (first of a kind wins),\n * and a predictable rendering is worth more than preserving an order that carries no meaning.\n */\nexport function applyTaskTag(line: string, command: TaskTagCommand): string | null {\n const marker = /^(\\s*)(-\\s+\\[[ xX]\\])\\s?/.exec(line)\n if (!marker) return null\n const [, indent, checkbox] = marker\n const tags = parseTaskTags(line.slice(marker[0].length))\n\n switch (command.kind) {\n case 'priority':\n tags.priority = tags.priority === command.value ? null : command.value\n break\n case 'waiting':\n tags.waiting = !tags.waiting\n break\n case 'doing':\n tags.doing = !tags.doing\n break\n case 'cancelled':\n tags.cancelled = !tags.cancelled\n break\n default:\n tags[command.kind] = command.value\n }\n\n // Exactly one space between the checkbox, each tag, and the text — and no trailing\n // space when a task has tags but no text yet, which is how it looks mid-typing.\n return [indent + checkbox, ...serialiseTags(tags), tags.text].filter((part) => part !== '').join(' ')\n}\n","/**\n * The pure index-derivation layer (Dual Mode Editor.md → The derived index store).\n * Turns one document's text into the rows the SQLite index stores: blocks (with\n * parent/order/kind/range), wikilink occurrences (tagged with their containing\n * block, for outline-chain backlinks), and tasks.\n *\n * This is the *shared* derivation — the same pure code the server runs over the same\n * content, which is what makes the client and server indexes compatible by\n * construction (ADR 0015). Pure and DOM-free.\n */\n\nimport { conceptKey } from './backlinks/backlink-index'\nimport { type Block, parseBlocks } from './block-model'\nimport { fencedBlocks } from './fenced-code'\nimport { containsCipherFence } from './protection/fence-info'\nimport { parseTaskTags, type TaskPriority } from './task-tags'\nimport { wikilinkOccurrencesInSource } from './wikilink'\n\nexport interface BlockRow {\n /** Stable only within this document's derivation (0-based DFS order). */\n localId: number\n parentId: number | null\n /** Sibling order under the parent. */\n ord: number\n kind: Block['type']\n depth: number\n done?: boolean\n startLine: number\n endLine: number\n text: string\n /** Display label — first line, marker stripped (the outline-chain breadcrumb text). */\n label: string\n}\n\nexport interface LinkRow {\n concept: string\n line: number\n lineText: string\n matchStart: number\n matchEnd: number\n /** The block whose own lines contain this link, or null if outside any block. */\n blockLocalId: number | null\n}\n\nexport interface TaskRow {\n blockLocalId: number\n done: boolean\n /** The block label — the leading [[Task Tag]] run INCLUDED, exactly as authored. */\n text: string\n line: number\n priority: TaskPriority | null\n waiting: boolean\n doing: boolean\n cancelled: boolean\n due: string | null\n completion: string | null\n scheduled: string | null\n}\n\n/**\n * One [[Task Concept]] a task answers to (ADR 0051), from a wikilink on the task's own line\n * or on any ancestor [[Block]]. The document's own concept — the virtual root of that chain —\n * is added by the index when it writes these, because derivation sees text and not identity.\n */\nexport interface TaskConceptRow {\n blockLocalId: number\n /** As written, case preserved; the index keys it. */\n concept: string\n}\n\nexport interface DerivedRows {\n blocks: BlockRow[]\n links: LinkRow[]\n tasks: TaskRow[]\n taskConcepts: TaskConceptRow[]\n}\n\n/**\n * A predicate over source line numbers: is this line inside a [[Fenced Code Block]]?\n *\n * The [[Block]] model does not treat fences as opaque, so `- [ ] x` written inside a fence\n * parses as a [[Task]] like any other bullet. That was invisible while nothing listed tasks;\n * the [[Tasks View]] makes it visible, as phantom tasks lifted out of code examples. Two\n * things follow from excluding them here rather than in the block model: the editor's outliner\n * behaviour is untouched (a fence's lines still belong to their block, which is what the\n * keyboard layer expects), and a [[Protected Document]] - an `etherpk-cipher` fence - can never\n * contribute a task, whatever it holds. That last one is a rule, not an optimisation: the\n * [[Derived Index]] is plaintext at rest and outlives the session.\n *\n * \"Inside a fence\" is the EDITOR's answer, not a second one: the column-scoped pairing of\n * `fencedBlocks` (Editor Content Rules → \"Inside a block\"), so a task is indexed exactly when\n * the editor shows a task. This scan used to have rules of its own, and two live bugs on\n * 2026-09-11 each hid a task the author could plainly see: a fence opened on a bullet line\n * (`- ``` `) was read as text, and a stray unterminated fence was run on to the next\n * top-level block. Under the shared scan a fence on a bullet line opens a block and never\n * closes one, an unterminated opener is no fence at all (its region is plain text in the\n * editor too), and a non-blank line dedenting below a fence's column abandons that fence.\n *\n * The one exception is a [[Protected Document]]'s `etherpk-cipher` opener with no closer:\n * that still runs to the end of the document. Nothing beneath it can be trusted as plaintext,\n * and the index is plaintext at rest, so the conservative reading is the only safe one.\n */\nfunction fencedLines(text: string): (line: number) => boolean {\n const lines = text.split('\\n')\n const spans: Array<[number, number]> = fencedBlocks(lines).map((b) => [b.start, b.end])\n const inside = (line: number) => spans.some(([from, to]) => line >= from && line <= to)\n for (let index = 0; index < lines.length; index++) {\n if (inside(index)) continue\n if (containsCipherFence(lines[index])) {\n spans.push([index, Number.MAX_SAFE_INTEGER])\n break\n }\n }\n if (spans.length === 0) return () => false\n return inside\n}\n\n/** First line of a block's text with its bullet/heading marker stripped. */\nexport function blockLabel(block: Block): string {\n const first = block.text.split('\\n', 1)[0]\n if (block.type === 'heading') return first.replace(/^#{1,6}\\s+/, '')\n return bulletLabel(first)\n}\n\n/**\n * The label of a bullet or [[Task]] line — its marker and checkbox stripped, indentation\n * ignored. Exported because the [[Tasks View]]'s write-back guard has to ask \"is the line in\n * the document still the task the index recorded?\", and the only honest way to answer is with\n * the very function that produced the recorded label. Two regexes that agree today would not\n * stay agreed.\n */\nexport function bulletLabel(line: string): string {\n return line.trimStart().replace(/^-\\s+(\\[[ xX]\\]\\s+)?/, '')\n}\n\n/** Flatten the block tree into rows in document (DFS) order, recording parentage. */\nfunction flatten(roots: Block[]): BlockRow[] {\n const rows: BlockRow[] = []\n const walk = (block: Block, parentId: number | null, ord: number) => {\n const localId = rows.length\n rows.push({\n localId,\n parentId,\n ord,\n kind: block.type,\n depth: block.depth,\n ...(block.done === undefined ? {} : { done: block.done }),\n startLine: block.startLine,\n endLine: block.endLine,\n text: block.text,\n label: blockLabel(block),\n })\n block.children.forEach((child, i) => walk(child, localId, i))\n }\n roots.forEach((root, i) => walk(root, null, i))\n return rows\n}\n\n/** Derive the index rows for a single document's text. Pure. */\nexport function deriveDoc(text: string): DerivedRows {\n const blocks = flatten(parseBlocks(text))\n\n // Map each source line to the deepest block whose own range covers it.\n const lineToBlock = new Map<number, number>()\n for (const b of blocks) {\n for (let line = b.startLine; line <= b.endLine; line++) lineToBlock.set(line, b.localId)\n }\n\n const links: LinkRow[] = wikilinkOccurrencesInSource(text).map((occ) => ({\n concept: occ.concept,\n line: occ.line,\n lineText: occ.lineText,\n matchStart: occ.matchStart,\n matchEnd: occ.matchEnd,\n blockLocalId: lineToBlock.get(occ.line) ?? null,\n }))\n\n // Scanning for fences costs a regex per line of the document, so it is worth deciding\n // whether there is anything to exclude first. Most documents hold no tasks at all, and on\n // a graph of a few thousand this runs for every one of them on a rebuild.\n const candidateTasks = blocks.filter((b) => b.kind === 'task')\n const fenced = candidateTasks.length === 0 ? () => false : fencedLines(text)\n const taskBlocks = candidateTasks.filter((b) => !fenced(b.startLine))\n const tasks: TaskRow[] = taskBlocks.map((b) => {\n const tags = parseTaskTags(b.label)\n return {\n blockLocalId: b.localId,\n done: b.done ?? false,\n text: b.label,\n line: b.startLine,\n priority: tags.priority,\n waiting: tags.waiting,\n doing: tags.doing,\n cancelled: tags.cancelled,\n due: tags.due,\n completion: tags.completion,\n scheduled: tags.scheduled,\n }\n })\n\n return { blocks, links, tasks, taskConcepts: deriveTaskConcepts(blocks, links, taskBlocks) }\n}\n\n/**\n * The [[Task Concept]] rows for one document (ADR 0051): for each task, every concept\n * wikilinked on its own block or on ANY ancestor, up to the root.\n *\n * Headings are ancestors here because they are ancestors in the [[Block]] tree — the heading\n * spine is the same tree the [[Backlink]] breadcrumb walks. That is what makes\n * `# Work on [[Acme]]` claim every task in the section, which is the intended, load-bearing\n * consequence rather than a leak.\n */\nfunction deriveTaskConcepts(blocks: BlockRow[], links: LinkRow[], taskBlocks: BlockRow[]): TaskConceptRow[] {\n if (taskBlocks.length === 0) return []\n const byBlock = new Map<number, string[]>()\n for (const link of links) {\n if (link.blockLocalId === null) continue\n const existing = byBlock.get(link.blockLocalId)\n if (existing) existing.push(link.concept)\n else byBlock.set(link.blockLocalId, [link.concept])\n }\n // Nothing is linked anywhere, so every task's only Task Concept is its document's —\n // which the index adds. Skipping the walk keeps the common journal page free.\n if (byBlock.size === 0) return []\n\n const rows: TaskConceptRow[] = []\n for (const task of taskBlocks) {\n const seen = new Set<string>()\n for (let id: number | null = task.localId; id !== null; id = blocks[id]?.parentId ?? null) {\n for (const concept of byBlock.get(id) ?? []) {\n const key = conceptKey(concept)\n if (seen.has(key)) continue\n seen.add(key)\n rows.push({ blockLocalId: task.localId, concept })\n }\n }\n }\n return rows\n}\n\n/** The ancestor labels of a block, root-first (the outline-chain breadcrumb). */\nexport function ancestorChain(blocks: BlockRow[], localId: number | null): string[] {\n const chain: string[] = []\n let id = localId\n while (id !== null) {\n const b = blocks[id]\n if (!b) break\n chain.unshift(b.label)\n id = b.parentId\n }\n return chain\n}\n","/**\n * Turning what a user typed into an FTS5 `MATCH` expression — and turning what comes back\n * into renderable segments.\n *\n * Pure, so it unit-tests without a database.\n *\n * The governing rule: **user input never reaches the FTS parser as syntax.** FTS5 has its own\n * grammar (`\"` `*` `-` `^` `:` `AND` `OR` `NOT` `NEAR`), so `C++`, `don't` and `orphan - asset`\n * would be a syntax error or, worse, a silent negation. In a live-as-you-type box a parse\n * error is not an edge case — it is the normal state halfway through typing a query. So we\n * tokenize ourselves and quote every term. No operator syntax is exposed, ever; structured\n * querying has a home already in [[Query]].\n */\n\n/** Below this a query is not specific enough to be worth a round trip. Counts TYPED\n * characters, not tokenized ones: `C++` is three characters a user deliberately typed, and\n * telling them it is too short would be nonsense. */\nexport const MIN_TEXT_QUERY_LENGTH = 2\n\n/**\n * A final term shorter than this is matched EXACTLY rather than as a prefix.\n *\n * This, not the length gate, is what stops a query matching most of the graph: `c*` matches\n * every word beginning with c, while `c` matches only a standalone `c` token — which is\n * exactly what someone searching `C++` meant.\n */\nconst MIN_PREFIX_LENGTH = 2\n\n/**\n * Characters FTS5's `snippet()` wraps matches in. Control characters, because they cannot\n * occur in markdown anyone types — a printable sentinel could appear in a note and would then\n * be rendered as a highlight that is not one.\n */\nexport const MATCH_OPEN = '\\u0002'\nexport const MATCH_CLOSE = '\\u0003'\n\n/** One run of snippet text, flagged as matched or not. Rendered as a text node or a `<mark>`. */\nexport interface SearchSegment {\n text: string\n match: boolean\n}\n\ninterface ParsedTerm {\n text: string\n /** A `\"quoted phrase\"` is taken literally and never gets a prefix `*`. */\n phrase: boolean\n}\n\n/**\n * Split input into terms: `\"quoted phrases\"` survive whole, everything else is words, and all\n * punctuation is discarded. An unclosed quote is treated as if closed at the end, so a query\n * stays valid while the user is still typing it.\n */\nexport function parseSearchTerms(input: string): ParsedTerm[] {\n const terms: ParsedTerm[] = []\n let index = 0\n while (index < input.length) {\n const char = input[index]\n if (char === '\"') {\n const end = input.indexOf('\"', index + 1)\n const raw = end === -1 ? input.slice(index + 1) : input.slice(index + 1, end)\n const words = wordsIn(raw)\n if (words.length > 0) terms.push({ text: words.join(' '), phrase: true })\n index = end === -1 ? input.length : end + 1\n continue\n }\n // A run up to the next quote, tokenized into words.\n const nextQuote = input.indexOf('\"', index)\n const chunk = nextQuote === -1 ? input.slice(index) : input.slice(index, nextQuote)\n for (const word of wordsIn(chunk)) terms.push({ text: word, phrase: false })\n index = nextQuote === -1 ? input.length : nextQuote\n }\n return terms\n}\n\n/**\n * The words in a run of text, as `unicode61` would tokenize them: letters and digits, with\n * everything else acting as a separator. Keeping this in step with the tokenizer is what makes\n * `[[Physics]]` findable by typing `physics` — the brackets are separators on both sides.\n */\nfunction wordsIn(text: string): string[] {\n return text.match(/[\\p{L}\\p{N}]+/gu) ?? []\n}\n\n/**\n * The `MATCH` expression for `input`, or `null` when there is nothing to search for.\n *\n * Terms are ANDed (FTS5's default), each one quoted so its content can never be read as\n * syntax. The LAST term gets a prefix `*` unless it was a quoted phrase, which is what makes\n * results narrow as you type: `orph` finds *orphaned* before you have finished the word.\n */\nexport function buildFtsMatch(input: string): string | null {\n const terms = parseSearchTerms(input)\n if (terms.length === 0) return null\n return terms\n .map((term, i) => {\n const quoted = `\"${term.text.replace(/\"/g, '\"\"')}\"`\n const isLast = i === terms.length - 1\n const prefixable = !term.phrase && term.text.length >= MIN_PREFIX_LENGTH\n return isLast && prefixable ? `${quoted}*` : quoted\n })\n .join(' ')\n}\n\n/** True when a query is worth sending to the text index at all. */\nexport function isSearchableTextQuery(input: string): boolean {\n if (parseSearchTerms(input).length === 0) return false\n return input.trim().length >= MIN_TEXT_QUERY_LENGTH\n}\n\n/**\n * Split a `snippet()` result on its sentinels into renderable segments.\n *\n * Returned as data, never as HTML: the text is the user's own notes, and this is the one\n * place in Search where an injection would land.\n */\nexport function snippetSegments(snippet: string): SearchSegment[] {\n const segments: SearchSegment[] = []\n let rest = snippet\n while (rest.length > 0) {\n const open = rest.indexOf(MATCH_OPEN)\n if (open === -1) {\n segments.push({ text: rest, match: false })\n break\n }\n if (open > 0) segments.push({ text: rest.slice(0, open), match: false })\n const close = rest.indexOf(MATCH_CLOSE, open + 1)\n if (close === -1) {\n // Unbalanced (a truncated snippet): keep the remainder as plain text rather than\n // dropping it — showing the words matters more than showing them highlighted.\n segments.push({ text: rest.slice(open + 1), match: false })\n break\n }\n segments.push({ text: rest.slice(open + 1, close), match: true })\n rest = rest.slice(close + 1)\n }\n return segments.filter((s) => s.text !== '')\n}\n","/**\n * The SQLite schema + query layer for the derived index (ADR 0015,\n * Dual Mode Editor.md). Engine-agnostic: it talks to a tiny {@link SqlDb} interface\n * so the same code runs over an in-memory DB in node tests and a sqlite-wasm/OPFS DB\n * in the browser. Derived and rebuildable — `ingest` clears and repopulates.\n *\n * Absorbs the in-memory backlink index: it answers `backlinksFor` (now enriched with\n * the outline-chain breadcrumb) and `existingConceptKeys`. Concept identity is\n * case-insensitive (ADR 0011).\n */\n\nimport type { DocumentKind } from '$lib/storage'\n\nimport { conceptKey } from './backlinks'\nimport { type BlockRow, deriveDoc, type TaskConceptRow, type TaskRow } from './index-derive'\nimport { containsCipherFence } from './protection/fence-info'\n\n/** One document fed into the index (same shape the in-memory index consumed). */\nexport interface IndexDoc {\n concept: string\n kind: DocumentKind\n aliases: string[]\n text: string\n}\n\n/** One node of a block reference's rendered subtree (the matched block + descendants). */\nexport interface RefSubtreeNode {\n label: string\n /** Depth relative to the matched block (0 = the matched block itself). */\n depth: number\n /** True for the block that actually holds the wikilink. */\n isMatch: boolean\n}\n\n/** A prose reference's surrounding text, truncated to 1000 chars centred on the match. */\nexport interface RefContext {\n text: string\n matchStart: number\n matchEnd: number\n truncated: boolean\n}\n\n/**\n * One reference, rendered two ways (Dual Mode Editor.md → references). A **block**\n * reference (wikilink in a bullet/task) carries the matched block + its descendant\n * subtree (Logseq 0.10.15 behaviour). A **prose** reference (wikilink in a paragraph\n * or heading) carries the surrounding paragraph text. Both carry a breadcrumb:\n * full ancestry for block refs, the heading tree for prose refs.\n */\nexport interface DbBacklinkRef {\n sourceConcept: string\n sourceKind: DocumentKind\n line: number\n kind: 'block' | 'prose'\n /** Ancestor labels, root-first. Empty when the reference is direct on the page. */\n breadcrumb: string[]\n /** Block refs: the matched block + descendants. Empty for prose refs. */\n subtree: RefSubtreeNode[]\n /** Prose refs: the surrounding (possibly truncated) text. Null for block refs. */\n context: RefContext | null\n}\n\nexport interface DbBacklinkGroup {\n sourceConcept: string\n sourceKind: DocumentKind\n refs: DbBacklinkRef[]\n}\n\nconst MAX_CONTEXT = 1000\n\n/** The minimal SQL surface the index needs — implemented over sqlite-wasm. */\nexport interface SqlDb {\n exec(sql: string): void\n run(sql: string, params?: unknown[]): void\n all<T = Record<string, unknown>>(sql: string, params?: unknown[]): T[]\n /** Release the underlying SQLite handle. Safe to call more than once. */\n close(): void\n}\n\n/**\n * Bumped whenever the schema OR the derivation changes (ADR 0015's `PRAGMA user_version`\n * pattern, ADR 0041 §5). A persisted index built by an older parser is not merely old, it is\n * WRONG - it holds the output of derivation logic that no longer exists - so a mismatch means\n * discard and rebuild. Changing `index-derive.ts` without bumping this is the trap.\n */\nexport const INDEX_SCHEMA_VERSION = 8\n\nconst SCHEMA = `\nCREATE TABLE IF NOT EXISTS index_metadata (\n key TEXT PRIMARY KEY, value INTEGER NOT NULL);\n-- protected: the stored text holds a cipher fence (fence-info.ts). Kept on the page row so a\n-- listing can wear a padlock without opening the document - the one cheap answer the tab has.\nCREATE TABLE IF NOT EXISTS pages (\n id INTEGER PRIMARY KEY, concept TEXT NOT NULL, concept_key TEXT NOT NULL, kind TEXT NOT NULL,\n text_hash TEXT NOT NULL DEFAULT '', generation INTEGER NOT NULL DEFAULT 1,\n protected INTEGER NOT NULL DEFAULT 0);\nCREATE TABLE IF NOT EXISTS aliases (\n page_id INTEGER NOT NULL, alias_key TEXT NOT NULL, display TEXT NOT NULL);\nCREATE TABLE IF NOT EXISTS blocks (\n page_id INTEGER NOT NULL, local_id INTEGER NOT NULL, parent_local_id INTEGER,\n ord INTEGER NOT NULL, kind TEXT NOT NULL, depth INTEGER NOT NULL, done INTEGER,\n start_line INTEGER NOT NULL, end_line INTEGER NOT NULL, label TEXT NOT NULL, text TEXT NOT NULL);\nCREATE TABLE IF NOT EXISTS links (\n page_id INTEGER NOT NULL, concept TEXT NOT NULL, concept_key TEXT NOT NULL,\n line INTEGER NOT NULL, line_text TEXT NOT NULL, match_start INTEGER NOT NULL,\n match_end INTEGER NOT NULL, block_local_id INTEGER);\n-- The [[Task Tag]] run (ADR 0032) parsed into columns, so the [[Tasks View]] can filter,\n-- order and PAGE in SQL. Keeping the run as text would make LIMIT return the wrong rows.\nCREATE TABLE IF NOT EXISTS tasks (\n page_id INTEGER NOT NULL, block_local_id INTEGER NOT NULL, done INTEGER NOT NULL,\n text TEXT NOT NULL, line INTEGER NOT NULL,\n priority INTEGER, waiting INTEGER NOT NULL DEFAULT 0, doing INTEGER NOT NULL DEFAULT 0,\n cancelled INTEGER NOT NULL DEFAULT 0, due TEXT, completion TEXT, scheduled TEXT);\n-- [[Task Concept]] (ADR 0051): one row per (task, concept it answers to). Concepts are stored\n-- AS WRITTEN and pooled over [[Alias]]es at query time, exactly as backlinksFor does — baking\n-- the canonical name in would go stale the moment a page is renamed.\nCREATE TABLE IF NOT EXISTS task_concepts (\n page_id INTEGER NOT NULL, block_local_id INTEGER NOT NULL, concept_key TEXT NOT NULL);\nCREATE INDEX IF NOT EXISTS links_concept_key ON links(concept_key);\nCREATE INDEX IF NOT EXISTS blocks_page ON blocks(page_id);\n-- Re-indexing ONE document deletes its rows by page_id and finds its page by concept_key;\n-- without these every edit would scan the whole table (ingestOne).\nCREATE INDEX IF NOT EXISTS pages_concept_key ON pages(concept_key);\nCREATE INDEX IF NOT EXISTS pages_generation ON pages(generation);\nCREATE INDEX IF NOT EXISTS aliases_alias_key ON aliases(alias_key);\nCREATE INDEX IF NOT EXISTS aliases_page ON aliases(page_id);\nCREATE INDEX IF NOT EXISTS links_page ON links(page_id);\nCREATE INDEX IF NOT EXISTS tasks_page ON tasks(page_id);\n-- The Name Filter's whole selectivity: without it, filtering to a concept scans every task\n-- row in the graph. Mirrors links_concept_key, and like it stays put during a bulk rebuild.\nCREATE INDEX IF NOT EXISTS task_concepts_key ON task_concepts(concept_key);\n-- Search's text index. One row per block, so a document's rank can be \"how many blocks\n-- mention this\" - the ordering the results actually display. See the block_fts notes above\n-- createSchema for why unicode61 rather than trigram, and why the rowid encodes the block.\nCREATE VIRTUAL TABLE IF NOT EXISTS block_fts USING fts5(text, tokenize = 'unicode61');\n`\n\n/**\n * [[Search]]'s text index (`block_fts` in {@link SCHEMA}) holds one row per [[Block]], so a\n * document can be ranked by how many of its blocks mention the query - the ordering the\n * results actually display.\n *\n * `unicode61` (word tokens), NOT `trigram`: trigram gives true substring matching but needs\n * >=3 characters, ranks close to meaninglessly under bm25, and runs 3-4x the text in an OPFS\n * store ADR 0041 already worries about. The accepted loss is that `graph` does not find\n * `subgraph`; the win is that the final term can carry an implicit `*`, so results narrow as\n * the user types.\n *\n * The rowid ENCODES the block: `page_id * BLOCK_FTS_STRIDE + local_id`. That is what makes\n * retiring a page a primary-key range delete rather than a scan of the whole text index -\n * an FTS5 column cannot be indexed for that, and `ingestOne` runs on every keystroke.\n *\n * Blocks per page in that encoding: 2^20 - far past any real document (a block is a line or a\n * bullet), and `page_id * STRIDE` stays inside `Number.MAX_SAFE_INTEGER`. 2^20 - far past any real document\n */\nexport const BLOCK_FTS_STRIDE = 1_048_576\n\n/** The rowid range covering every text-index row for one page: `[from, to)`. */\nfunction blockFtsRange(pageId: number): [number, number] {\n return [pageId * BLOCK_FTS_STRIDE, (pageId + 1) * BLOCK_FTS_STRIDE]\n}\n\nexport function createSchema(db: SqlDb): void {\n db.exec(SCHEMA)\n db.run(\"INSERT OR IGNORE INTO index_metadata (key, value) VALUES ('active_generation', 1)\")\n db.run(\"INSERT OR IGNORE INTO index_metadata (key, value) VALUES ('revision', 0)\")\n db.exec(`PRAGMA user_version = ${INDEX_SCHEMA_VERSION}`)\n}\n\nexport function activeIndexGeneration(db: SqlDb): number {\n return (\n db.all<{ value: number }>(\n \"SELECT value FROM index_metadata WHERE key = 'active_generation'\",\n )[0]?.value ?? 1\n )\n}\n\nexport function indexRevision(db: SqlDb): number {\n return db.all<{ value: number }>(\"SELECT value FROM index_metadata WHERE key = 'revision'\")[0]\n ?.value ?? 0\n}\n\nexport function advanceIndexRevision(db: SqlDb): number {\n db.run(\"UPDATE index_metadata SET value = value + 1 WHERE key = 'revision'\")\n return indexRevision(db)\n}\n\n/**\n * Ready an existing database for use, or report that it cannot be trusted.\n *\n * Returns false when the file was written by a different schema or derivation version — the\n * caller discards it and rebuilds. Never migrates: the index is derived and rebuildable, so\n * rebuilding is always both available and cheaper than being careful (ADR 0015).\n */\nexport function isUsableIndex(db: SqlDb): boolean {\n try {\n const rows = db.all<{ user_version: number }>('PRAGMA user_version')\n return rows[0]?.user_version === INDEX_SCHEMA_VERSION\n } catch {\n return false\n }\n}\n\n/** Every indexed document's content hash, for deciding what actually needs re-deriving. */\nexport function documentHashes(db: SqlDb): Map<string, string> {\n const rows = db.all<{ concept_key: string; text_hash: string }>(\n 'SELECT concept_key, text_hash FROM pages WHERE generation = ?',\n [activeIndexGeneration(db)],\n )\n return new Map(rows.map((r) => [r.concept_key, r.text_hash]))\n}\n\nexport function documentHash(db: SqlDb, key: string): string | undefined {\n return db.all<{ text_hash: string }>(\n 'SELECT text_hash FROM pages WHERE generation = ? AND concept_key = ? LIMIT 1',\n [activeIndexGeneration(db), key],\n )[0]?.text_hash\n}\n\n/**\n * cyrb53 — a fast, non-cryptographic 53-bit string hash. Not a security boundary: it decides\n * whether a document's derived rows are still current. A collision leaves one page stale until\n * its next edit, which a never-authoritative store survives (ADR 0041 §5).\n */\nexport function hashText(text: string): string {\n let h1 = 0xdeadbeef\n let h2 = 0x41c6ce57\n for (let i = 0; i < text.length; i++) {\n const ch = text.charCodeAt(i)\n h1 = Math.imul(h1 ^ ch, 2654435761)\n h2 = Math.imul(h2 ^ ch, 1597334677)\n }\n h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909)\n h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909)\n return (4294967296 * (2097151 & h2) + (h1 >>> 0)).toString(36)\n}\n\n/**\n * Whether a [[Block]] contributes to [[Search]]'s text index.\n *\n * A [[Protected Document]] contributes **nothing** - not its plaintext and not its ciphertext.\n * This is a permanent rule, not a limitation waiting to be lifted (CONTEXT.md): the\n * [[Derived Index]] is plaintext at rest in OPFS and outlives the session, so indexing\n * decrypted cipher content would leave it on disk in a queryable file, surviving after the\n * key is forgotten - which defeats the whole feature. Indexing the ciphertext instead would\n * be merely useless: base64 matches nothing a human types, and it bloats the index.\n *\n * This is also what makes the lock cheap (ADR 0058): there is nothing here to evict when the\n * key goes, because protected content was never written here in the first place.\n */\nfunction isTextSearchable(block: BlockRow): boolean {\n return !containsCipherFence(block.text)\n}\n\n/** Write one document's derived rows under an already-established page id. */\nfunction insertDerived(db: SqlDb, pageId: number, doc: IndexDoc): void {\n for (const alias of doc.aliases) {\n db.run('INSERT INTO aliases (page_id, alias_key, display) VALUES (?,?,?)', [pageId, conceptKey(alias), alias])\n }\n const { blocks, links, tasks, taskConcepts } = deriveDoc(doc.text)\n for (const b of blocks) {\n db.run(\n 'INSERT INTO blocks (page_id, local_id, parent_local_id, ord, kind, depth, done, start_line, end_line, label, text) VALUES (?,?,?,?,?,?,?,?,?,?,?)',\n [pageId, b.localId, b.parentId, b.ord, b.kind, b.depth, b.done == null ? null : b.done ? 1 : 0, b.startLine, b.endLine, b.label, b.text],\n )\n if (isTextSearchable(b) && b.localId < BLOCK_FTS_STRIDE) {\n db.run('INSERT INTO block_fts (rowid, text) VALUES (?,?)', [\n pageId * BLOCK_FTS_STRIDE + b.localId,\n b.text,\n ])\n }\n }\n for (const l of links) {\n db.run(\n 'INSERT INTO links (page_id, concept, concept_key, line, line_text, match_start, match_end, block_local_id) VALUES (?,?,?,?,?,?,?,?)',\n [pageId, l.concept, conceptKey(l.concept), l.line, l.lineText, l.matchStart, l.matchEnd, l.blockLocalId],\n )\n }\n for (const t of tasks) {\n db.run(\n `INSERT INTO tasks (page_id, block_local_id, done, text, line,\n priority, waiting, doing, cancelled, due, completion, scheduled)\n VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`,\n [\n pageId,\n t.blockLocalId,\n t.done ? 1 : 0,\n t.text,\n t.line,\n t.priority,\n t.waiting ? 1 : 0,\n t.doing ? 1 : 0,\n t.cancelled ? 1 : 0,\n t.due,\n t.completion,\n t.scheduled,\n ],\n )\n }\n insertTaskConcepts(db, pageId, doc, tasks, taskConcepts)\n}\n\n/**\n * Write one document's [[Task Concept]] rows (ADR 0051).\n *\n * Derivation supplies the wikilinked half; the document's own concept is added HERE, as the\n * virtual root of every task's ancestry chain, because derivation reads text and never learns\n * the document's identity. Without that row, filtering to a project page returns nothing for\n * the tasks written directly on it — the case the \"Use <active tab>\" shortcut exists for.\n *\n * Only the document's own concept goes in, never its aliases: the query resolves a filtered\n * name to its page and matches on every name that page answers to, so an alias row here would\n * be redundant now and stale after a rename.\n */\nfunction insertTaskConcepts(\n db: SqlDb,\n pageId: number,\n doc: IndexDoc,\n tasks: readonly TaskRow[],\n taskConcepts: readonly TaskConceptRow[],\n): void {\n if (tasks.length === 0) return\n const ownKey = conceptKey(doc.concept)\n const written = new Set<string>()\n const write = (blockLocalId: number, key: string) => {\n const seen = `${blockLocalId} ${key}`\n if (written.has(seen)) return\n written.add(seen)\n db.run('INSERT INTO task_concepts (page_id, block_local_id, concept_key) VALUES (?,?,?)', [\n pageId,\n blockLocalId,\n key,\n ])\n }\n for (const t of tasks) write(t.blockLocalId, ownKey)\n for (const tc of taskConcepts) write(tc.blockLocalId, conceptKey(tc.concept))\n}\n\n/**\n * Run `work` as ONE transaction. Without this every INSERT is its own implicit transaction,\n * which is most of what made a full rebuild cost seconds rather than milliseconds.\n */\nfunction inTransaction(db: SqlDb, work: () => void): void {\n db.exec('BEGIN')\n try {\n work()\n db.exec('COMMIT')\n } catch (err) {\n db.exec('ROLLBACK')\n throw err\n }\n}\n\n/**\n * Indexes that exist to make a ONE-document re-index cheap (`ingestOne`). During a bulk\n * rebuild they are pure cost — every one of ~124k inserts maintains them — so the full path\n * drops and rebuilds them around the load, which SQLite does far faster in one pass.\n */\nconst MAINTENANCE_INDEXES = [\n 'pages_concept_key',\n 'aliases_alias_key',\n 'aliases_page',\n 'links_page',\n 'tasks_page',\n 'task_concepts_page',\n]\n\nconst CREATE_MAINTENANCE_INDEXES = `\nCREATE INDEX IF NOT EXISTS pages_concept_key ON pages(concept_key);\nCREATE INDEX IF NOT EXISTS aliases_alias_key ON aliases(alias_key);\nCREATE INDEX IF NOT EXISTS aliases_page ON aliases(page_id);\nCREATE INDEX IF NOT EXISTS links_page ON links(page_id);\nCREATE INDEX IF NOT EXISTS tasks_page ON tasks(page_id);\nCREATE INDEX IF NOT EXISTS task_concepts_page ON task_concepts(page_id);`\n\nfunction clearForRebuild(db: SqlDb): void {\n db.exec(\n 'DELETE FROM pages; DELETE FROM aliases; DELETE FROM blocks; DELETE FROM links; DELETE FROM tasks; DELETE FROM task_concepts; DELETE FROM block_fts;',\n )\n for (const name of MAINTENANCE_INDEXES) db.exec(`DROP INDEX IF EXISTS ${name}`)\n}\n\n/** SQLite has no boolean: the `protected` column as stored. */\nfunction protectedFlag(doc: IndexDoc): number {\n return containsCipherFence(doc.text) ? 1 : 0\n}\n\nfunction insertPage(db: SqlDb, pageId: number, generation: number, doc: IndexDoc): void {\n db.run(\n 'INSERT INTO pages (id, concept, concept_key, kind, text_hash, generation, protected) VALUES (?,?,?,?,?,?,?)',\n [pageId, doc.concept, conceptKey(doc.concept), doc.kind, hashText(doc.text), generation, protectedFlag(doc)],\n )\n insertDerived(db, pageId, doc)\n}\n\n/** Rebuild the whole index from the documents (derived; never authoritative). */\nexport function ingest(db: SqlDb, docs: Iterable<IndexDoc>): void {\n inTransaction(db, () => {\n clearForRebuild(db)\n const generation = activeIndexGeneration(db)\n let pageId = 0\n for (const doc of docs) insertPage(db, ++pageId, generation, doc)\n db.exec(CREATE_MAINTENANCE_INDEXES)\n })\n}\n\n/** Documents per transaction in a progressive rebuild — small enough to yield often. */\nconst REBUILD_BATCH = 100\n\nexport interface ProgressiveIngestOptions {\n /** Yield to the event loop; see `activity/breathe`. Without one this still batches, but never repaints. */\n breathe?: () => Promise<void> | void\n onProgress?: (done: number, total: number) => void\n}\n\n/**\n * The same full rebuild, in slices that hand the thread back between them.\n *\n * A 2431-document graph takes seconds to index however fast each insert is, and doing it in\n * one synchronous call means a frozen tab and a loading screen that cannot paint. The index\n * is derived and rebuildable, so a partially-built index mid-rebuild is not a correctness\n * problem — it is simply incomplete, which is what the progress the caller renders says.\n */\nexport async function ingestProgressively(\n db: SqlDb,\n docs: readonly IndexDoc[],\n options: ProgressiveIngestOptions = {},\n): Promise<void> {\n inTransaction(db, () => clearForRebuild(db))\n const generation = activeIndexGeneration(db)\n options.onProgress?.(0, docs.length)\n for (let start = 0; start < docs.length; start += REBUILD_BATCH) {\n const batch = docs.slice(start, start + REBUILD_BATCH)\n inTransaction(db, () => {\n batch.forEach((doc, n) => insertPage(db, start + n + 1, generation, doc))\n })\n options.onProgress?.(Math.min(start + REBUILD_BATCH, docs.length), docs.length)\n await options.breathe?.()\n }\n inTransaction(db, () => db.exec(CREATE_MAINTENANCE_INDEXES))\n}\n\nfunction deleteGeneration(db: SqlDb, generation: number): void {\n // Generation retirement is one relation-sized operation per table. Deleting four\n // child relations one page at a time made a 2,400-page commit execute roughly 9,600\n // synchronous wasm calls in one uninterruptible worker turn, so newly attached tabs\n // could wait many seconds even though their persisted index was already usable.\n // The text index first, and by ROWID: its rowids are derived from `blocks`, so once the\n // block rows are gone there is nothing left to compute them from, and the only remaining\n // way to find them would be a scan of the whole text index.\n db.run(\n `DELETE FROM block_fts WHERE rowid IN (\n SELECT b.page_id * ${BLOCK_FTS_STRIDE} + b.local_id FROM blocks b\n JOIN pages p ON p.id = b.page_id WHERE p.generation = ?)`,\n [generation],\n )\n for (const table of ['aliases', 'blocks', 'links', 'tasks', 'task_concepts']) {\n db.run(\n `DELETE FROM ${table} WHERE page_id IN (SELECT id FROM pages WHERE generation = ?)`,\n [generation],\n )\n }\n db.run('DELETE FROM pages WHERE generation = ?', [generation])\n}\n\n/** Begin an invisible rebuild generation and remove debris from any interrupted predecessor. */\nexport function beginIndexRebuild(db: SqlDb): number {\n const active = activeIndexGeneration(db)\n inTransaction(db, () => {\n const inactive = db\n .all<{ generation: number }>(\n 'SELECT DISTINCT generation FROM pages WHERE generation <> ?',\n [active],\n )\n .map((row) => row.generation)\n for (const generation of inactive) deleteGeneration(db, generation)\n })\n return (\n db.all<{ generation: number }>(\n 'SELECT MAX(generation) AS generation FROM pages',\n )[0]?.generation ?? active\n ) + 1\n}\n\n/** Ingest a rebuild chunk immediately without exposing it to active queries. */\nexport function ingestIndexRebuildChunk(\n db: SqlDb,\n generation: number,\n docs: readonly IndexDoc[],\n): void {\n inTransaction(db, () => {\n let pageId =\n db.all<{ id: number }>('SELECT COALESCE(MAX(id), 0) AS id FROM pages')[0]?.id ?? 0\n for (const doc of docs) insertPage(db, ++pageId, generation, doc)\n })\n}\n\n/** Atomically expose a complete staged generation, then reclaim the previous one. */\nexport function commitIndexRebuild(db: SqlDb, generation: number): number {\n return inTransactionWithResult(db, () => {\n db.run(\"UPDATE index_metadata SET value = ? WHERE key = 'active_generation'\", [\n generation,\n ])\n const revision = advanceIndexRevision(db)\n const obsolete = db\n .all<{ generation: number }>(\n 'SELECT DISTINCT generation FROM pages WHERE generation <> ?',\n [generation],\n )\n .map((row) => row.generation)\n for (const oldGeneration of obsolete) deleteGeneration(db, oldGeneration)\n return revision\n })\n}\n\nexport function abortIndexRebuild(db: SqlDb, generation: number): void {\n inTransaction(db, () => deleteGeneration(db, generation))\n}\n\nfunction inTransactionWithResult<T>(db: SqlDb, work: () => T): T {\n db.exec('BEGIN')\n try {\n const result = work()\n db.exec('COMMIT')\n return result\n } catch (error) {\n db.exec('ROLLBACK')\n throw error\n }\n}\n\n/**\n * Re-index ONE document, leaving every other page's rows untouched.\n *\n * This is what an edit costs. Rebuilding the whole graph for one keystroke was measured at\n * ~3.9s of blocked main thread on a 2431-document graph; the derived rows of one document\n * are a few dozen inserts.\n *\n * The document's own rows are replaced wholesale rather than diffed, deliberately: block\n * `local_id`s come from parse ORDER, so inserting a single line renumbers every block after\n * it. There is no stable block identity to diff against, and at ~40 blocks a page the\n * replacement is not worth avoiding. Nothing outside this page is touched.\n */\nexport function ingestOne(db: SqlDb, doc: IndexDoc): void {\n inTransaction(db, () => {\n const key = conceptKey(doc.concept)\n const generation = activeIndexGeneration(db)\n const found = db.all<{ id: number }>(\n 'SELECT id FROM pages WHERE generation = ? AND concept_key = ?',\n [generation, key],\n )\n let pageId: number\n if (found.length > 0) {\n pageId = found[0].id\n db.run('DELETE FROM aliases WHERE page_id = ?', [pageId])\n const [ftsFrom, ftsTo] = blockFtsRange(pageId)\n // A primary-key range delete, which is why the rowid encodes the page: this runs\n // on every keystroke, and a scan of the text index would not survive that.\n db.run('DELETE FROM block_fts WHERE rowid >= ? AND rowid < ?', [ftsFrom, ftsTo])\n db.run('DELETE FROM blocks WHERE page_id = ?', [pageId])\n db.run('DELETE FROM links WHERE page_id = ?', [pageId])\n db.run('DELETE FROM tasks WHERE page_id = ?', [pageId])\n db.run('DELETE FROM task_concepts WHERE page_id = ?', [pageId])\n db.run('UPDATE pages SET concept = ?, kind = ?, text_hash = ?, protected = ? WHERE id = ?', [\n doc.concept,\n doc.kind,\n hashText(doc.text),\n protectedFlag(doc),\n pageId,\n ])\n } else {\n const max = db.all<{ n: number }>('SELECT COALESCE(MAX(id), 0) AS n FROM pages')\n pageId = (max[0]?.n ?? 0) + 1\n db.run(\n 'INSERT INTO pages (id, concept, concept_key, kind, text_hash, generation, protected) VALUES (?,?,?,?,?,?,?)',\n [pageId, doc.concept, key, doc.kind, hashText(doc.text), generation, protectedFlag(doc)],\n )\n }\n insertDerived(db, pageId, doc)\n })\n}\n\n/** Every concept key that resolves to a real document (canonical name or alias). */\nexport function existingConceptKeys(db: SqlDb): string[] {\n const generation = activeIndexGeneration(db)\n const pages = db.all<{ k: string }>(\n 'SELECT concept_key AS k FROM pages WHERE generation = ?',\n [generation],\n )\n const aliases = db.all<{ k: string }>(\n 'SELECT a.alias_key AS k FROM aliases a JOIN pages p ON p.id = a.page_id WHERE p.generation = ?',\n [generation],\n )\n return [...pages, ...aliases].map((r) => r.k)\n}\n\n/** What a completion candidate resolves to. Pages/journals/aliases exist; `pageless` does not. */\nexport type ConceptCandidateKind = 'page' | 'journal' | 'alias' | 'pageless'\n\n/** One row the wikilink-completion popover can offer (Dual Mode Editor.md → Wikilink completion). */\nexport interface ConceptCandidate {\n /** Case-preserved name shown and inserted (the alias's own name for aliases). */\n display: string\n /** Case-insensitive identity key (ADR 0011). */\n key: string\n kind: ConceptCandidateKind\n /** Aliases only: the canonical page's display name (what it resolves to). */\n canonical?: string\n /**\n * Pageless only: how many wikilink occurrences reference this concept. It is what\n * distinguishes a pageless row from a draft row in [[Quick Find]] and [[Search]] - the\n * one thing a \"make this up\" row can never show (ADR 0050).\n */\n references?: number\n /**\n * The document this resolves to is a [[Protected Document]] - its stored text holds a cipher\n * fence. Set on the page's own row AND on each of its aliases, since an alias row opens that\n * same document. Present only when true, so an unprotected candidate's shape is unchanged.\n *\n * This is what lets a listing - [[Search]]'s Names, the Sidebar's Favourites and Recents -\n * wear the padlock without opening anything: `open()` retains a sync engine on a Server\n * Backend, and the tab renderer already learned the hard way not to call it from a repaint.\n * It says nothing about the lock STATE; that is per graph and read from the session.\n */\n protected?: true\n}\n\n/**\n * Every concept the wikilink-completion popover can offer, as one flat snapshot:\n * existing pages and journals, each alias (carrying its canonical page), and\n * `pageless` concepts — referenced by some wikilink but with no page yet (\"Not yet\n * created\"). Scoped concepts (display contains a nested `[[…]]`) are included — a real\n * page named e.g. `This is a [[Mini Inside]] tab` must be linkable; the popover only\n * hides them when completing *inside* a nested bracket (Dual Mode Editor.md → Wikilink\n * completion). Built fresh after each ingest by the live index, never queried per keystroke.\n */\nexport function conceptCandidates(db: SqlDb): ConceptCandidate[] {\n const generation = activeIndexGeneration(db)\n const pages = db.all<{ concept: string; key: string; kind: DocumentKind; protected: number }>(\n 'SELECT concept, concept_key AS key, kind, protected FROM pages WHERE generation = ? ORDER BY concept_key',\n [generation],\n )\n const aliases = db.all<{ display: string; key: string; canonical: string; protected: number }>(\n 'SELECT a.display AS display, a.alias_key AS key, p.concept AS canonical, p.protected AS protected FROM aliases a JOIN pages p ON p.id = a.page_id WHERE p.generation = ? ORDER BY a.alias_key',\n [generation],\n )\n // SQLite was previously asked to run two correlated anti-joins for every link row. On a\n // fully hydrated 2,400-document graph that made each persisted-index open spend several\n // seconds rebuilding this same cache. We already have the complete page and alias sets\n // above, so group link targets once and perform the inexpensive membership test in JS.\n //\n // The display is the MAJORITY casing among the concept's wikilink instances, not an\n // arbitrary one: a [[Draft]] shows this name from the moment it opens and promotes with\n // it, so it decides the page's canonical name (ADR 0050). Ties break on BINARY collation,\n // which puts the Title-Cased variant first - the right default for a page title.\n const referenced = db.all<{ display: string; key: string; references: number }>(\n `SELECT key, display, refs AS \"references\" FROM (\n SELECT l.concept_key AS key, l.concept AS display,\n SUM(COUNT(*)) OVER (PARTITION BY l.concept_key) AS refs,\n ROW_NUMBER() OVER (\n PARTITION BY l.concept_key ORDER BY COUNT(*) DESC, l.concept ASC\n ) AS rn\n FROM links l JOIN pages source ON source.id = l.page_id\n WHERE source.generation = ?\n GROUP BY l.concept_key, l.concept\n ) WHERE rn = 1 ORDER BY key`,\n [generation],\n )\n const existing = new Set([...pages.map((page) => page.key), ...aliases.map((alias) => alias.key)])\n const out: ConceptCandidate[] = []\n for (const page of pages) {\n out.push(withProtected({ display: page.concept, key: page.key, kind: page.kind }, page.protected))\n }\n for (const alias of aliases) {\n out.push(\n withProtected(\n { display: alias.display, key: alias.key, kind: 'alias', canonical: alias.canonical },\n alias.protected,\n ),\n )\n }\n for (const link of referenced) {\n if (!existing.has(link.key)) {\n out.push({\n display: link.display,\n key: link.key,\n kind: 'pageless',\n references: link.references,\n })\n }\n }\n return out\n}\n\n/** Attach the padlock flag only when set, so unprotected candidates keep their exact shape. */\nfunction withProtected(candidate: ConceptCandidate, flag: number): ConceptCandidate {\n return flag ? { ...candidate, protected: true } : candidate\n}\n\n/** Resolve only the candidate keys affected by one document ingest. */\nexport function conceptCandidatesForKeys(\n db: SqlDb,\n keys: Iterable<string>,\n): Map<string, ConceptCandidate> {\n const generation = activeIndexGeneration(db)\n const result = new Map<string, ConceptCandidate>()\n for (const key of new Set(keys)) {\n const page = db.all<{ concept: string; kind: DocumentKind; protected: number }>(\n 'SELECT concept, kind, protected FROM pages WHERE generation=? AND concept_key=? LIMIT 1',\n [generation, key],\n )[0]\n if (page) {\n result.set(key, withProtected({ display: page.concept, key, kind: page.kind }, page.protected))\n continue\n }\n const alias = db.all<{ display: string; canonical: string; protected: number }>(\n `SELECT a.display, p.concept AS canonical, p.protected AS protected\n FROM aliases a JOIN pages p ON p.id=a.page_id\n WHERE p.generation=? AND a.alias_key=? LIMIT 1`,\n [generation, key],\n )[0]\n if (alias) {\n result.set(\n key,\n withProtected(\n { display: alias.display, key, kind: 'alias', canonical: alias.canonical },\n alias.protected,\n ),\n )\n continue\n }\n // Majority casing and occurrence count, exactly as `conceptCandidates` derives them\n // for the full snapshot — the two must agree or a candidate would change name when\n // one document is re-ingested.\n const pageless = db.all<{ display: string; references: number }>(\n `SELECT display, refs AS \"references\" FROM (\n SELECT l.concept AS display,\n SUM(COUNT(*)) OVER () AS refs,\n ROW_NUMBER() OVER (ORDER BY COUNT(*) DESC, l.concept ASC) AS rn\n FROM links l JOIN pages p ON p.id=l.page_id\n WHERE p.generation=? AND l.concept_key=?\n GROUP BY l.concept\n ) WHERE rn = 1`,\n [generation, key],\n )[0]\n if (pageless) {\n result.set(key, {\n display: pageless.display,\n key,\n kind: 'pageless',\n references: pageless.references,\n })\n }\n }\n return result\n}\n\nexport interface IndexedDocumentFacts {\n aliases: string[]\n linkTargets: string[]\n}\n\nexport function indexedDocumentFacts(db: SqlDb, key: string): IndexedDocumentFacts {\n const generation = activeIndexGeneration(db)\n const page = db.all<{ id: number }>(\n 'SELECT id FROM pages WHERE generation=? AND concept_key=? LIMIT 1',\n [generation, key],\n )[0]\n if (!page) return { aliases: [], linkTargets: [] }\n return {\n aliases: db\n .all<{ key: string }>('SELECT alias_key AS key FROM aliases WHERE page_id=?', [\n page.id,\n ])\n .map((row) => row.key),\n linkTargets: db\n .all<{ key: string }>(\n 'SELECT DISTINCT concept_key AS key FROM links WHERE page_id=?',\n [page.id],\n )\n .map((row) => row.key),\n }\n}\n\nexport function indexDocumentFactKeys(doc: IndexDoc): IndexedDocumentFacts {\n return {\n aliases: doc.aliases.map(conceptKey),\n linkTargets: [\n ...new Set(deriveDoc(doc.text).links.map((link) => conceptKey(link.concept))),\n ],\n }\n}\n\nexport function conceptExists(db: SqlDb, concept: string): boolean {\n const key = conceptKey(concept)\n const generation = activeIndexGeneration(db)\n const hit = db.all<{ n: number }>(\n `SELECT (\n EXISTS(SELECT 1 FROM pages WHERE generation=? AND concept_key=?)\n OR EXISTS(\n SELECT 1 FROM aliases a JOIN pages p ON p.id=a.page_id\n WHERE p.generation=? AND a.alias_key=?\n )\n ) AS n`,\n [generation, key, generation, key],\n )\n return !!hit[0]?.n\n}\n\n/** The canonical page id a concept resolves to (by its own name or an alias), or null. */\nfunction canonicalPageId(db: SqlDb, key: string): number | null {\n const generation = activeIndexGeneration(db)\n const direct = db.all<{ id: number }>(\n 'SELECT id FROM pages WHERE generation=? AND concept_key=? LIMIT 1',\n [generation, key],\n )\n if (direct[0]) return direct[0].id\n const viaAlias = db.all<{ id: number }>(\n `SELECT a.page_id AS id FROM aliases a JOIN pages p ON p.id=a.page_id\n WHERE p.generation=? AND a.alias_key=? LIMIT 1`,\n [generation, key],\n )\n return viaAlias[0]?.id ?? null\n}\n\n/** All concept keys that target the same page (its own name + aliases). */\nfunction namesForPage(db: SqlDb, pageId: number): string[] {\n const page = db.all<{ k: string }>('SELECT concept_key AS k FROM pages WHERE id=?', [pageId])\n const aliases = db.all<{ k: string }>('SELECT alias_key AS k FROM aliases WHERE page_id=?', [pageId])\n return [...page.map((r) => r.k), ...aliases.map((r) => r.k)]\n}\n\ninterface LinkHit {\n page_id: number\n sourceConcept: string\n sourceKind: DocumentKind\n line: number\n line_text: string\n match_start: number\n match_end: number\n block_local_id: number | null\n}\n\n/** Ancestor labels of a block, root-first. `headingsOnly` keeps just the heading tree. */\nfunction ancestorLabels(blocks: BlockRow[], id: number, headingsOnly: boolean): string[] {\n const out: string[] = []\n let cur = blocks[id]?.parentId ?? null\n while (cur !== null) {\n const b = blocks[cur]\n if (!b) break\n if (!headingsOnly || b.kind === 'heading') out.unshift(b.label)\n cur = b.parentId\n }\n return out\n}\n\n/** The matched block plus its descendant subtree (Logseq block-reference behaviour). */\nfunction collectSubtree(blocks: BlockRow[], rootId: number): RefSubtreeNode[] {\n const childrenByParent = new Map<number, BlockRow[]>()\n for (const block of blocks) {\n if (!block || block.parentId === null) continue\n const children = childrenByParent.get(block.parentId) ?? []\n children.push(block)\n childrenByParent.set(block.parentId, children)\n }\n for (const children of childrenByParent.values()) {\n children.sort((a, b) => a.ord - b.ord)\n }\n const out: RefSubtreeNode[] = []\n const walk = (id: number, depth: number) => {\n const b = blocks[id]\n if (!b) return\n out.push({ label: b.label, depth, isMatch: id === rootId })\n for (const child of childrenByParent.get(id) ?? []) walk(child.localId, depth + 1)\n }\n walk(rootId, 0)\n return out\n}\n\n/** Truncate `text` to MAX_CONTEXT chars centred on the match, with ellipses. */\nfunction truncateContext(text: string, matchStart: number, matchEnd: number): RefContext {\n if (text.length <= MAX_CONTEXT) return { text, matchStart, matchEnd, truncated: false }\n const mid = Math.floor((matchStart + matchEnd) / 2)\n let start = Math.max(0, mid - Math.floor(MAX_CONTEXT / 2))\n const end = Math.min(text.length, start + MAX_CONTEXT)\n start = Math.max(0, end - MAX_CONTEXT)\n const prefix = start > 0 ? '…' : ''\n const suffix = end < text.length ? '…' : ''\n const shift = prefix.length - start\n return {\n text: prefix + text.slice(start, end) + suffix,\n matchStart: Math.max(0, matchStart + shift),\n matchEnd: Math.max(0, matchEnd + shift),\n truncated: true,\n }\n}\n\n/** Build a reference: a block subtree for bullets/tasks, prose context otherwise. */\nfunction buildRef(h: LinkHit, blocks: BlockRow[]): DbBacklinkRef {\n const base = { sourceConcept: h.sourceConcept, sourceKind: h.sourceKind, line: h.line }\n const block = h.block_local_id != null ? blocks[h.block_local_id] : undefined\n\n if (block && (block.kind === 'bullet' || block.kind === 'task')) {\n return {\n ...base,\n kind: 'block',\n breadcrumb: ancestorLabels(blocks, block.localId, false),\n subtree: collectSubtree(blocks, block.localId),\n context: null,\n }\n }\n\n // Prose (paragraph / heading / no block): the surrounding text + the heading tree.\n const source = block?.text ?? h.line_text\n const matchText = h.line_text.slice(h.match_start, h.match_end)\n const offset = source.indexOf(matchText)\n const ms = offset >= 0 ? offset : 0\n const me = offset >= 0 ? offset + matchText.length : 0\n return {\n ...base,\n kind: 'prose',\n breadcrumb: block ? ancestorLabels(blocks, block.localId, true) : [],\n subtree: [],\n context: truncateContext(source, ms, me),\n }\n}\n\n/**\n * All references to a concept, pooled across its own name and aliases\n * (case-insensitive), grouped by source document (journals newest-first, then pages).\n * Each ref renders as a Logseq-style block subtree (bullets) or prose context\n * (paragraphs/headings) — see {@link DbBacklinkRef}. Replaces the in-memory index.\n */\nexport function backlinksFor(db: SqlDb, concept: string): DbBacklinkGroup[] {\n const key = conceptKey(concept)\n const pageId = canonicalPageId(db, key)\n const names = pageId === null ? [key] : namesForPage(db, pageId)\n if (names.length === 0) return []\n\n const placeholders = names.map(() => '?').join(',')\n const generation = activeIndexGeneration(db)\n const hits = db.all<LinkHit>(\n `SELECT l.page_id, p.concept AS sourceConcept, p.kind AS sourceKind, l.line, l.line_text,\n l.match_start, l.match_end, l.block_local_id\n FROM links l JOIN pages p ON p.id = l.page_id\n WHERE p.generation=? AND l.concept_key IN (${placeholders})`,\n [generation, ...names],\n )\n\n // Fetch full block rows for the involved pages once (ancestry + subtree + text).\n const blocksByPage = new Map<number, BlockRow[]>()\n for (const pid of new Set(hits.map((h) => h.page_id))) {\n const rows = db.all<{\n local_id: number\n parent_local_id: number | null\n ord: number\n kind: BlockRow['kind']\n depth: number\n label: string\n text: string\n }>(\n 'SELECT local_id, parent_local_id, ord, kind, depth, label, text FROM blocks WHERE page_id=? ORDER BY local_id',\n [pid],\n )\n const blocks: BlockRow[] = []\n for (const r of rows) {\n blocks[r.local_id] = {\n localId: r.local_id,\n parentId: r.parent_local_id,\n ord: r.ord,\n kind: r.kind,\n depth: r.depth,\n startLine: 0,\n endLine: 0,\n text: r.text,\n label: r.label,\n }\n }\n blocksByPage.set(pid, blocks)\n }\n\n const groups = new Map<string, DbBacklinkGroup>()\n for (const h of hits) {\n let group = groups.get(h.sourceConcept)\n if (!group) {\n group = { sourceConcept: h.sourceConcept, sourceKind: h.sourceKind, refs: [] }\n groups.set(h.sourceConcept, group)\n }\n group.refs.push(buildRef(h, blocksByPage.get(h.page_id) ?? []))\n }\n for (const g of groups.values()) g.refs.sort((a, b) => a.line - b.line)\n return [...groups.values()].sort(compareGroups)\n}\n\n/** Journals first (date desc), then pages (concept asc, case-insensitive). */\nfunction compareGroups(a: DbBacklinkGroup, b: DbBacklinkGroup): number {\n if (a.sourceKind !== b.sourceKind) return a.sourceKind === 'journal' ? -1 : 1\n if (a.sourceKind === 'journal') return b.sourceConcept.localeCompare(a.sourceConcept)\n return conceptKey(a.sourceConcept).localeCompare(conceptKey(b.sourceConcept))\n}\n\n// ---- [[Search]]: the text group ------------------------------------------------------\n\nimport {\n buildFtsMatch,\n MATCH_CLOSE,\n MATCH_OPEN,\n snippetSegments,\n type SearchSegment,\n} from './search-query'\n\n/** Matching documents counted beyond this are reported as \"1000+\" rather than tallied. */\nexport const SEARCH_COUNT_CAP = 1000\n\n/** Matching blocks shown per document before the \"+N more\" expander. */\nexport const SEARCH_HITS_PER_DOCUMENT = 3\n\n/** Roughly two lines of context around a match, in tokens. */\nconst SNIPPET_TOKENS = 24\n\n/** One matching [[Block]] — a locator, not something to read in place. */\nexport interface SearchBlockHit {\n /** 0-based source line, for opening the document at this block. */\n line: number\n /** Ancestor labels, root-first. A bare bullet is often meaningless without them. */\n breadcrumb: string[]\n /** The snippet, pre-split into plain and matched runs. Never HTML. */\n snippet: SearchSegment[]\n}\n\n/** One document's matches, which is what a Text result row actually is. */\nexport interface SearchDocumentGroup {\n concept: string\n kind: DocumentKind\n /** Every matching block in this document, not just the ones listed in `hits`. */\n matches: number\n hits: SearchBlockHit[]\n}\n\nexport interface SearchTextPage {\n groups: SearchDocumentGroup[]\n /** True when a further page exists — from fetching one row beyond the limit. */\n hasMore: boolean\n}\n\n/**\n * Documents whose text matches `query`, most-mentions-first, one page at a time.\n *\n * Ordering is **matching-block count**, with `bm25()` only as a tie-break. Ranking by best\n * block instead would be the obvious choice and is wrong here: the FTS rows are blocks, so\n * bm25 scores how much a *block* is about the term, which rewards short ones - a one-line\n * `see [[Orphaned Asset]]` outranks the page that explains orphaned assets. Summing block\n * scores merely reintroduces the long-document bias bm25 exists to remove. Counting is cruder,\n * robust, and explainable in one sentence - and it orders by a number the row displays.\n */\nexport function searchText(\n db: SqlDb,\n query: string,\n offset: number,\n limit: number,\n): SearchTextPage {\n const match = buildFtsMatch(query)\n if (match === null) return { groups: [], hasMore: false }\n const generation = activeIndexGeneration(db)\n\n // One extra row decides `hasMore` without a second COUNT.\n const ranked = db.all<{ page_id: number; matches: number }>(\n // MATERIALIZED is load-bearing, not a hint: an FTS5 auxiliary function like bm25()\n // is only legal where the virtual table is queried directly, and the planner would\n // otherwise flatten this subquery into the join and reject it outright.\n `WITH hits AS MATERIALIZED (\n SELECT rowid / ${BLOCK_FTS_STRIDE} AS page_id, bm25(block_fts) AS score\n FROM block_fts WHERE block_fts MATCH ?\n )\n SELECT h.page_id AS page_id, COUNT(*) AS matches, MIN(h.score) AS best\n FROM hits h\n JOIN pages p ON p.id = h.page_id AND p.generation = ?\n GROUP BY h.page_id\n ORDER BY matches DESC, best ASC, h.page_id ASC\n LIMIT ? OFFSET ?`,\n [match, generation, limit + 1, offset],\n )\n const hasMore = ranked.length > limit\n const page = hasMore ? ranked.slice(0, limit) : ranked\n if (page.length === 0) return { groups: [], hasMore: false }\n\n const ids = page.map((row) => row.page_id)\n const placeholders = ids.map(() => '?').join(',')\n const pages = new Map(\n db\n .all<{ id: number; concept: string; kind: DocumentKind }>(\n `SELECT id, concept, kind FROM pages WHERE id IN (${placeholders})`,\n ids,\n )\n .map((row) => [row.id, row]),\n )\n // Ancestry for the whole page of documents in one read; the breadcrumb is walked in JS.\n const blocks = db.all<{\n page_id: number\n local_id: number\n parent_local_id: number | null\n label: string\n start_line: number\n }>(\n `SELECT page_id, local_id, parent_local_id, label, start_line FROM blocks\n WHERE page_id IN (${placeholders})`,\n ids,\n )\n const byPage = new Map<number, Map<number, (typeof blocks)[number]>>()\n for (const block of blocks) {\n let map = byPage.get(block.page_id)\n if (!map) byPage.set(block.page_id, (map = new Map()))\n map.set(block.local_id, block)\n }\n\n const groups: SearchDocumentGroup[] = []\n for (const row of page) {\n const meta = pages.get(row.page_id)\n if (!meta) continue\n const [from, to] = blockFtsRange(row.page_id)\n // Document order, not relevance order: a list of locators reads top-to-bottom.\n const hits = db.all<{ rowid: number; snippet: string }>(\n `SELECT rowid, snippet(block_fts, 0, ?, ?, '…', ${SNIPPET_TOKENS}) AS snippet\n FROM block_fts\n WHERE block_fts MATCH ? AND rowid >= ? AND rowid < ?\n ORDER BY rowid LIMIT ?`,\n [MATCH_OPEN, MATCH_CLOSE, match, from, to, SEARCH_HITS_PER_DOCUMENT],\n )\n const blockMap = byPage.get(row.page_id)\n groups.push({\n concept: meta.concept,\n kind: meta.kind,\n matches: row.matches,\n hits: hits.map((hit) => {\n const localId = hit.rowid - from\n const block = blockMap?.get(localId)\n return {\n line: block?.start_line ?? 0,\n breadcrumb: blockMap ? searchBreadcrumb(blockMap, localId) : [],\n snippet: snippetSegments(hit.snippet),\n }\n }),\n })\n }\n return { groups, hasMore }\n}\n\n/**\n * Ancestor labels of a block, root-first, from the flat map `searchText` already loaded.\n * Distinct from the same-named helper above, which walks the in-memory derivation's rows.\n */\nfunction searchBreadcrumb(\n blocks: Map<number, { parent_local_id: number | null; label: string }>,\n localId: number,\n): string[] {\n const chain: string[] = []\n let parent = blocks.get(localId)?.parent_local_id ?? null\n // Bounded by the map size: a corrupt parent cycle must not hang the worker.\n for (let guard = 0; parent !== null && guard <= blocks.size; guard++) {\n const block = blocks.get(parent)\n if (!block) break\n chain.unshift(block.label)\n parent = block.parent_local_id\n }\n return chain\n}\n\n/**\n * How many documents match, capped at {@link SEARCH_COUNT_CAP}.\n *\n * Capped rather than exact because a prefix query - which is every query, mid-typing - can\n * match most of the graph, and this runs beside the rows it must never delay.\n */\nexport function searchTextCount(db: SqlDb, query: string): { total: number; capped: boolean } {\n const match = buildFtsMatch(query)\n if (match === null) return { total: 0, capped: false }\n const rows = db.all<{ n: number }>(\n `SELECT COUNT(*) AS n FROM (\n SELECT m.page_id FROM (\n SELECT DISTINCT rowid / ${BLOCK_FTS_STRIDE} AS page_id FROM block_fts\n WHERE block_fts MATCH ?\n ) m\n JOIN pages p ON p.id = m.page_id AND p.generation = ?\n LIMIT ?)`,\n [match, activeIndexGeneration(db), SEARCH_COUNT_CAP + 1],\n )\n const n = rows[0]?.n ?? 0\n return n > SEARCH_COUNT_CAP ? { total: SEARCH_COUNT_CAP, capped: true } : { total: n, capped: false }\n}\n\n// ── Asset usage ─────────────────────────────────────────────────────────────\n\n/** One document that holds at least one [[Asset Reference]] to the asset asked about. */\nexport interface AssetUsageDocument {\n concept: string\n kind: DocumentKind\n /** How many references this document holds — two images of one asset count as two. */\n references: number\n}\n\nexport interface AssetUsage {\n /** References across the whole graph, including the one the caller is standing on. */\n references: number\n /** The documents holding them, in concept order so the dialog reads the same every time. */\n documents: AssetUsageDocument[]\n}\n\n/** Escape a literal for a LIKE pattern, so an asset name can never behave as a wildcard. */\nfunction likeLiteral(needle: string): string {\n return needle.replace(/[\\\\%_]/g, (c) => `\\\\${c}`)\n}\n\n/** Occurrences of `needle` in `text`, non-overlapping. */\nfunction occurrences(text: string, needle: string): number {\n let n = 0\n for (let at = text.indexOf(needle); at !== -1; at = text.indexOf(needle, at + needle.length)) n += 1\n return n\n}\n\n/**\n * Which documents reference an [[Asset]], and how many times — the safety gate in front of a\n * permanent delete (ADR 0054). `needles` are the forms the reference can be written in: the\n * asset's identity (its on-disk name on a [[Filesystem Backend]], its id on a [[Server\n * Backend]]) and, when it differs, the percent-encoded form.\n *\n * Deliberately a **substring scan of every block's source**, not a search over `block_fts`:\n * the text index tokenises, so it cannot be trusted to match a punctuated file name, and a\n * missed match here would destroy bytes another document still needs. The same conservative\n * rule the [[Orphaned Asset]] scanners use — a mention anywhere counts, fences and prose\n * alike — for the same reason: over-counting refuses a delete, under-counting loses data.\n *\n * `blocks` carries no index on `text`, so this is a scan. That is the right shape: on the\n * largest graph this was built against it is ~85k rows inside the index worker, tens of\n * milliseconds, and it runs once per trash click rather than per keystroke.\n */\nexport function assetUsage(db: SqlDb, needles: readonly string[]): AssetUsage {\n const distinct = [...new Set(needles)].filter((n) => n.length > 0)\n if (distinct.length === 0) return { references: 0, documents: [] }\n const where = distinct.map(() => `b.text LIKE ? ESCAPE '\\\\'`).join(' OR ')\n const rows = db.all<{ concept: string; kind: DocumentKind; text: string }>(\n `SELECT p.concept AS concept, p.kind AS kind, b.text AS text\n FROM blocks b\n JOIN pages p ON p.id = b.page_id AND p.generation = ?\n WHERE ${where}`,\n [activeIndexGeneration(db), ...distinct.map((n) => `%${likeLiteral(n)}%`)],\n )\n\n // Count in JS rather than SQL: one block can hold two references to the same asset, and\n // \"used in more than one place\" has to see both.\n const byConcept = new Map<string, AssetUsageDocument>()\n let references = 0\n for (const row of rows) {\n const n = distinct.reduce((sum, needle) => sum + occurrences(row.text, needle), 0)\n if (n === 0) continue\n references += n\n const existing = byConcept.get(row.concept)\n if (existing) existing.references += n\n else byConcept.set(row.concept, { concept: row.concept, kind: row.kind, references: n })\n }\n return {\n references,\n documents: [...byConcept.values()].sort((a, b) => a.concept.localeCompare(b.concept)),\n }\n}\n\n// ── The Tasks View ──────────────────────────────────────────────────────────\n\n/**\n * The five [[Task]] states, as a partition. They are mutually exclusive by construction, so\n * a task is in exactly one and the Status chips can be a plain multi-select:\n *\n * waiting open and `#W` doing open, `#D`, not `#W`\n * open open and neither done checked, not `#C`\n * cancelled checked and `#C`\n *\n * `cancelled` earns a state of its own because ADR 0032 makes a cancelled task *checked* -\n * without one it would hide inside `done` with no way to ask for it or exclude it.\n */\nexport type TaskStatus = 'open' | 'doing' | 'waiting' | 'done' | 'cancelled'\n\nexport const TASK_STATUSES: readonly TaskStatus[] = ['open', 'doing', 'waiting', 'done', 'cancelled']\n\n/** The states a task that is not finished can be in - the Tasks View's default selection. */\nexport const OPEN_TASK_STATUSES: readonly TaskStatus[] = ['open', 'doing', 'waiting']\n\n/** A priority chip. `null` is the real, selectable \"no priority\" bucket, not \"unset\". */\nexport type TaskPriorityFilter = 1 | 2 | 3 | null\n\nexport const TASK_PRIORITY_FILTERS: readonly TaskPriorityFilter[] = [1, 2, 3, null]\n\nexport type TaskDueWindow = 'any' | 'overdue' | 'today' | 'next7'\n\nexport type TaskGroupBy = 'document' | 'priority' | 'due'\n\nexport interface TaskQuery {\n /** The [[Name Filter]]'s selection, or null for every task in the graph. */\n concept: string | null\n statuses: readonly TaskStatus[]\n priorities: readonly TaskPriorityFilter[]\n due: TaskDueWindow\n groupBy: TaskGroupBy\n /**\n * Today as `YYYY-MM-DD`, supplied by the caller rather than read from the clock here.\n * The query then stays a pure function of its inputs - which is what makes the date\n * windows testable, and what keeps the worker from having an opinion about the user's\n * timezone, which only the tab knows.\n */\n today: string\n}\n\n/** One task, as the [[Tasks View]] renders it. */\nexport interface TaskHit {\n /** Identity for the ticked-in-this-session set, and the write-back coordinates. */\n pageId: number\n blockLocalId: number\n /** The source document's concept - the ViewRef target to open, and the Document group. */\n concept: string\n kind: DocumentKind\n /** 0-based source line, for `revealLine` and for the write-back's stale-line guard. */\n line: number\n done: boolean\n /** The block label with its [[Task Tag]] run still on it, exactly as authored. */\n text: string\n priority: number | null\n waiting: boolean\n doing: boolean\n cancelled: boolean\n due: string | null\n completion: string | null\n scheduled: string | null\n /** Ancestor labels, root-first - why this task matched, when it matched via an ancestor. */\n breadcrumb: string[]\n}\n\nexport interface TaskPage {\n hits: TaskHit[]\n /** True when a further page exists - from fetching one row beyond the limit. */\n hasMore: boolean\n}\n\n/** `YYYY-MM-DD` for a local date, `days` from it. Local, because \"today\" is the user's. */\nexport function taskDateKey(date: Date, days = 0): string {\n const shifted = new Date(date.getFullYear(), date.getMonth(), date.getDate() + days)\n const month = String(shifted.getMonth() + 1).padStart(2, '0')\n const day = String(shifted.getDate()).padStart(2, '0')\n return `${shifted.getFullYear()}-${month}-${day}`\n}\n\n/** The SQL predicate for one status, over the `tasks` columns. */\nconst STATUS_SQL: Record<TaskStatus, string> = {\n open: '(t.done = 0 AND t.waiting = 0 AND t.doing = 0)',\n doing: '(t.done = 0 AND t.doing = 1 AND t.waiting = 0)',\n waiting: '(t.done = 0 AND t.waiting = 1)',\n done: '(t.done = 1 AND t.cancelled = 0)',\n cancelled: '(t.done = 1 AND t.cancelled = 1)',\n}\n\n/**\n * Ordering per grouping. The FIRST key must be the group key, because the view splits an\n * already-ordered page into groups rather than re-sorting - which is what lets a group\n * straddle a page boundary and simply continue.\n *\n * Within a group the order is due-date then priority (AS Notes' behaviour), with NULLs last\n * both times: an undated task is not urgent, and SQLite would otherwise sort NULL first.\n * `page_id, block_local_id` finally makes the order total, so paging cannot repeat or skip.\n */\nconst WITHIN_GROUP = '(t.due IS NULL), t.due ASC, (t.priority IS NULL), t.priority ASC'\nconst ORDER_SQL: Record<TaskGroupBy, string> = {\n document: `p.concept_key ASC, ${WITHIN_GROUP}`,\n priority: `(t.priority IS NULL), t.priority ASC, (t.due IS NULL), t.due ASC`,\n due: `(t.due IS NULL), t.due ASC, (t.priority IS NULL), t.priority ASC`,\n}\n\ninterface TaskRowResult {\n page_id: number\n concept: string\n kind: DocumentKind\n block_local_id: number\n line: number\n done: number\n text: string\n priority: number | null\n waiting: number\n doing: number\n cancelled: number\n due: string | null\n completion: string | null\n scheduled: string | null\n}\n\n\n/**\n * The WHERE clause and parameters shared by {@link tasksMatching} and its count, so the two\n * can never drift into answering different questions about the same filter.\n *\n * Returns null when the filter cannot match anything - an empty Status or Priority selection\n * (a genuine \"show me nothing\", not a missing value), or a [[Name Filter]] naming a concept\n * with no page and no links.\n */\nfunction taskPredicate(db: SqlDb, query: TaskQuery): { where: string; params: unknown[] } | null {\n if (query.statuses.length === 0 || query.priorities.length === 0) return null\n\n const where: string[] = ['p.generation = ?']\n const params: unknown[] = [activeIndexGeneration(db)]\n\n if (query.concept !== null) {\n // The [[Name Filter]] resolves to a page and matches EVERY name that page answers to\n // (its own plus [[Alias]]es), the pooling backlinksFor performs - so filtering by an\n // alias finds tasks written against the canonical name and vice versa. A name with no\n // page is a [[Pageless Concept]] and matches only itself, which is right: it has none.\n const key = conceptKey(query.concept)\n const pageId = canonicalPageId(db, key)\n const names = pageId === null ? [key] : namesForPage(db, pageId)\n if (names.length === 0) return null\n where.push(\n `EXISTS (SELECT 1 FROM task_concepts tc\n WHERE tc.page_id = t.page_id AND tc.block_local_id = t.block_local_id\n AND tc.concept_key IN (${names.map(() => '?').join(',')}))`,\n )\n params.push(...names)\n }\n\n if (query.statuses.length < TASK_STATUSES.length) {\n where.push(`(${query.statuses.map((s) => STATUS_SQL[s]).join(' OR ')})`)\n }\n\n if (query.priorities.length < TASK_PRIORITY_FILTERS.length) {\n const levels = query.priorities.filter((p): p is 1 | 2 | 3 => p !== null)\n const clauses: string[] = []\n if (levels.length > 0) {\n clauses.push(`t.priority IN (${levels.map(() => '?').join(',')})`)\n params.push(...levels)\n }\n if (query.priorities.includes(null)) clauses.push('t.priority IS NULL')\n where.push(`(${clauses.join(' OR ')})`)\n }\n\n if (query.due !== 'any') {\n const today = query.today\n if (query.due === 'overdue') {\n where.push('(t.due IS NOT NULL AND t.due < ?)')\n params.push(today)\n } else if (query.due === 'today') {\n where.push('t.due = ?')\n params.push(today)\n } else {\n where.push('(t.due IS NOT NULL AND t.due >= ? AND t.due <= ?)')\n params.push(today, addDays(today, 7))\n }\n }\n\n return { where: where.join(' AND '), params }\n}\n\ninterface TaskRowResult {\n page_id: number\n concept: string\n kind: DocumentKind\n block_local_id: number\n line: number\n done: number\n text: string\n priority: number | null\n waiting: number\n doing: number\n cancelled: number\n due: string | null\n completion: string | null\n scheduled: string | null\n}\n\n/**\n * One page of [[Task]]s matching a [[Tasks View]] query, ordered so the caller groups by\n * simply walking the rows in order.\n *\n * Every row carries its ancestor breadcrumb, which the [[Tasks View]] is not decorating with:\n * under ADR 0051 a task can match through a distant ancestor - a heading, most visibly - so\n * without the chain a legitimately matching row looks like a bug.\n */\nexport function tasksMatching(db: SqlDb, query: TaskQuery, offset: number, limit: number): TaskPage {\n const predicate = taskPredicate(db, query)\n if (predicate === null) return { hits: [], hasMore: false }\n\n // One extra row decides `hasMore` without a second COUNT, as searchText does.\n const rows = db.all<TaskRowResult>(\n `SELECT t.page_id, p.concept, p.kind, t.block_local_id, t.line, t.done, t.text,\n t.priority, t.waiting, t.doing, t.cancelled, t.due, t.completion, t.scheduled\n FROM tasks t JOIN pages p ON p.id = t.page_id\n WHERE ${predicate.where}\n ORDER BY ${ORDER_SQL[query.groupBy]}, t.page_id ASC, t.block_local_id ASC\n LIMIT ? OFFSET ?`,\n [...predicate.params, limit + 1, offset],\n )\n const hasMore = rows.length > limit\n const page = hasMore ? rows.slice(0, limit) : rows\n if (page.length === 0) return { hits: [], hasMore: false }\n\n const breadcrumbs = breadcrumbsFor(db, page)\n return {\n hits: page.map((row) => ({\n ...toHit(row),\n breadcrumb: breadcrumbs.get(row.page_id)?.get(row.block_local_id) ?? [],\n })),\n hasMore,\n }\n}\n\n/**\n * Ancestor labels for a whole page of tasks: one read for every document involved, then the\n * chains walked in JS. The same shape searchText uses, and for the same reason - a recursive\n * SQL walk per row would be one query per task.\n */\nfunction breadcrumbsFor(db: SqlDb, rows: readonly TaskRowResult[]): Map<number, Map<number, string[]>> {\n const ids = [...new Set(rows.map((row) => row.page_id))]\n const placeholders = ids.map(() => '?').join(',')\n const blocks = db.all<{\n page_id: number\n local_id: number\n parent_local_id: number | null\n label: string\n }>(\n `SELECT page_id, local_id, parent_local_id, label FROM blocks WHERE page_id IN (${placeholders})`,\n ids,\n )\n const byPage = new Map<number, Map<number, { parent_local_id: number | null; label: string }>>()\n for (const block of blocks) {\n let page = byPage.get(block.page_id)\n if (!page) byPage.set(block.page_id, (page = new Map()))\n page.set(block.local_id, { parent_local_id: block.parent_local_id, label: block.label })\n }\n const out = new Map<number, Map<number, string[]>>()\n for (const row of rows) {\n const page = byPage.get(row.page_id)\n if (!page) continue\n let chains = out.get(row.page_id)\n if (!chains) out.set(row.page_id, (chains = new Map()))\n chains.set(row.block_local_id, searchBreadcrumb(page, row.block_local_id))\n }\n return out\n}\n\n/** How many tasks match. Uncapped: this counts an indexed join, not a text index. */\nexport function tasksMatchingCount(db: SqlDb, query: TaskQuery): number {\n const predicate = taskPredicate(db, query)\n if (predicate === null) return 0\n const rows = db.all<{ n: number }>(\n `SELECT COUNT(*) AS n FROM tasks t JOIN pages p ON p.id = t.page_id WHERE ${predicate.where}`,\n predicate.params,\n )\n return rows[0]?.n ?? 0\n}\n\n/** `YYYY-MM-DD` shifted by whole days, so the caller never leaves string space. */\nfunction addDays(day: string, days: number): string {\n const [year, month, date] = day.split('-').map(Number)\n return taskDateKey(new Date(year, month - 1, date), days)\n}\n\nfunction toHit(row: TaskRowResult): Omit<TaskHit, 'breadcrumb'> {\n return {\n pageId: row.page_id,\n blockLocalId: row.block_local_id,\n concept: row.concept,\n kind: row.kind,\n line: row.line,\n done: row.done !== 0,\n text: row.text,\n priority: row.priority,\n waiting: row.waiting !== 0,\n doing: row.doing !== 0,\n cancelled: row.cancelled !== 0,\n due: row.due,\n completion: row.completion,\n scheduled: row.scheduled,\n }\n}\n","/**\n * The index server, free of any transport (ADR 0041 §2). It owns the SQLite database and\n * answers {@link IndexRequest}s; it does not know whether it is inside a SharedWorker, a\n * dedicated Worker, or running inline in a test.\n *\n * Keeping it transport-free is what lets the whole of the index's behaviour be node-tested\n * against an in-memory database, exactly as ADR 0015 established for the query layer — no\n * browser, no worker, no OPFS.\n */\n\nimport {\n type ConceptCandidate,\n type IndexDoc,\n type SqlDb,\n abortIndexRebuild,\n activeIndexGeneration,\n assetUsage,\n advanceIndexRevision,\n backlinksFor,\n beginIndexRebuild,\n commitIndexRebuild,\n conceptCandidates,\n conceptCandidatesForKeys,\n createSchema,\n documentHash,\n existingConceptKeys,\n hashText,\n indexDocumentFactKeys,\n indexRevision,\n ingestOne,\n indexedDocumentFacts,\n ingestIndexRebuildChunk,\n isUsableIndex,\n searchText,\n searchTextCount,\n tasksMatching,\n tasksMatchingCount,\n} from '../index-db'\nimport { conceptKey } from '../backlinks/backlink-index'\nimport type { IndexDelta, IndexRequest, IndexResponse } from './protocol'\n\n/** How a host supplies a database for a graph. Injected so tests need no OPFS. */\nexport interface IndexDbHost {\n open(graphId: string): Promise<{ db: SqlDb; persisted: boolean; blocked?: 'held' | 'unsupported' }>\n /** Discard a database whose version stamp no longer matches, and hand back a fresh one. */\n discard(graphId: string): Promise<{ db: SqlDb; persisted: boolean; blocked?: 'held' | 'unsupported' }>\n}\n\nexport interface IndexCore {\n handle(request: IndexRequest): Promise<IndexResponse[]>\n dispose(): void\n}\n\nexport function createIndexCore(host: IndexDbHost): IndexCore {\n type SnapshotResponse = Extract<IndexResponse, { type: 'snapshot' }>\n let db: SqlDb | undefined\n let persisted = false\n /** Why persistence is unavailable, when known. Travels with `opened` for the notice. */\n let persistenceBlocked: 'held' | 'unsupported' | undefined\n let openGraphId: string | undefined\n let pendingRebuild:\n | { id: string; generation: number; total: number; done: number }\n | undefined\n /**\n * The exact synchronous-read cache most recently pushed to clients. A shared worker can\n * serve many tabs, and querying every link again for every idempotent `open` made the third\n * and fourth tabs slower than the cold build. SQLite remains authoritative; this mirrors\n * its active generation and is updated by the same commit/delta boundaries clients see.\n */\n let visibleSnapshot: SnapshotResponse | undefined\n\n /**\n * End this core's ownership of the current SQLite handle.\n *\n * The OPFS host may unlink or reopen the same graph immediately afterwards, so merely\n * dropping the JavaScript reference is not enough. Clear the core state even if SQLite\n * reports a cleanup error, then close the underlying oo1 handle.\n */\n function closeActiveDatabase(): void {\n const closing = db\n const interruptedRebuild = pendingRebuild\n db = undefined\n pendingRebuild = undefined\n openGraphId = undefined\n persisted = false\n persistenceBlocked = undefined\n visibleSnapshot = undefined\n if (!closing) return\n\n try {\n if (interruptedRebuild) {\n abortIndexRebuild(closing, interruptedRebuild.generation)\n }\n } finally {\n closing.close()\n }\n }\n\n /** Read the active generation once after opening or atomically replacing it. */\n function readSnapshot(): SnapshotResponse {\n return {\n type: 'snapshot',\n revision: db ? indexRevision(db) : 0,\n existing: db ? existingConceptKeys(db) : [],\n candidates: db ? conceptCandidates(db) : [],\n }\n }\n\n /** Clone the pushed cache for inline transports too, where postMessage does not clone it. */\n function snapshot(rebuildId?: string): SnapshotResponse {\n visibleSnapshot ??= readSnapshot()\n return {\n ...visibleSnapshot,\n existing: [...visibleSnapshot.existing],\n candidates: visibleSnapshot.candidates.map((candidate) => ({ ...candidate })),\n ...(rebuildId ? { rebuildId } : {}),\n }\n }\n\n function replaceSnapshot(rebuildId?: string): SnapshotResponse {\n // If any SQL read fails after a generation commit, a later recovery must retry from\n // that new active generation. It must never fall back to the previous cached revision.\n visibleSnapshot = undefined\n visibleSnapshot = readSnapshot()\n return snapshot(rebuildId)\n }\n\n /** Keep the worker's cache at the same revision as the delta broadcast to every tab. */\n function applyDeltaToSnapshot(\n previous: SnapshotResponse | undefined,\n delta: IndexDelta,\n ): void {\n if (!previous || previous.revision !== delta.revision - 1) return\n const existing = new Set(previous.existing)\n for (const key of delta.existingAdded) existing.add(key)\n for (const key of delta.existingRemoved) existing.delete(key)\n const candidates = new Map(\n previous.candidates.map((candidate) => [candidate.key, candidate]),\n )\n for (const candidate of delta.candidateUpserts) candidates.set(candidate.key, candidate)\n for (const key of delta.candidateRemoved) candidates.delete(key)\n visibleSnapshot = {\n type: 'snapshot',\n revision: delta.revision,\n existing: [...existing],\n candidates: [...candidates.values()],\n }\n }\n\n function pageCount(): number {\n if (!db) return 0\n return (\n db.all<{ n: number }>('SELECT COUNT(*) AS n FROM pages WHERE generation = ?', [\n activeIndexGeneration(db),\n ])[0]?.n ?? 0\n )\n }\n\n const candidateExists = (candidate: ConceptCandidate | undefined) =>\n candidate !== undefined && candidate.kind !== 'pageless'\n\n function deltaFor(\n revision: number,\n before: Map<string, ConceptCandidate>,\n after: Map<string, ConceptCandidate>,\n backlinkTargetsChanged: Set<string>,\n ): IndexDelta {\n const existingAdded: string[] = []\n const existingRemoved: string[] = []\n const candidateUpserts: ConceptCandidate[] = []\n const candidateRemoved: string[] = []\n for (const key of new Set([...before.keys(), ...after.keys()])) {\n const previous = before.get(key)\n const next = after.get(key)\n if (!candidateExists(previous) && candidateExists(next)) existingAdded.push(key)\n if (candidateExists(previous) && !candidateExists(next)) existingRemoved.push(key)\n if (!next) candidateRemoved.push(key)\n else if (JSON.stringify(previous) !== JSON.stringify(next)) candidateUpserts.push(next)\n }\n return {\n revision,\n existingAdded,\n existingRemoved,\n candidateUpserts,\n candidateRemoved,\n backlinkTargetsChanged: [...backlinkTargetsChanged],\n }\n }\n\n /**\n * The stored index is wrecked (SQLite reports corruption). It is derived, so the answer is\n * ADR 0015's: discard it and start again — a fresh, empty database for the same graph, the\n * caller told so it can re-derive. Until this, a corrupt file was permanent: every ingest\n * failed, reads that missed the damage still answered, and the sidebars silently stopped\n * following the documents (live, 2026-09-04: \"the checkbox goes sidebar → document but not\n * document → sidebar\", then a manual rebuild reporting SQLITE_CORRUPT).\n */\n async function discardWreckedDatabase(graphId: string, cause: unknown): Promise<void> {\n console.warn(`[index] persisted index for ${graphId} is corrupt; discarding and re-deriving:`, cause)\n try {\n closeActiveDatabase()\n } catch {\n // A wreck need not even close cleanly; the state is already cleared.\n }\n const fresh = await host.discard(graphId)\n db = fresh.db\n persisted = fresh.persisted\n persistenceBlocked = fresh.blocked\n createSchema(db)\n openGraphId = graphId\n }\n\n async function dispatch(request: IndexRequest): Promise<IndexResponse[]> {\n switch (request.type) {\n case 'open': {\n // Idempotent per graph: with several tabs attached to one worker\n // (ADR 0042), every tab opens on arrival. A second `open` must\n // answer from the live database, not stack another connection\n // onto the same file.\n if (db && openGraphId === request.graphId) {\n return [\n {\n type: 'opened',\n persisted,\n indexedDocuments: pageCount(),\n ...(request.openId !== undefined ? { openId: request.openId } : {}),\n ...(persistenceBlocked ? { persistenceBlocked } : {}),\n },\n snapshot(),\n ]\n }\n // A worker normally serves one graph, but inline transports and tests can\n // switch. Never retain the old graph's SQLite handle while opening another.\n closeActiveDatabase()\n const opened = await host.open(request.graphId)\n // A database written by an older schema or an older DERIVATION is not\n // stale, it is wrong. Never migrate — discard and rebuild (ADR 0015).\n if (!isUsableIndex(opened.db)) {\n // OPFS cannot unlink the stale file until its oo1 database releases it.\n opened.db.close()\n const fresh = await host.discard(request.graphId)\n db = fresh.db\n persisted = fresh.persisted\n persistenceBlocked = fresh.blocked\n createSchema(db)\n } else {\n db = opened.db\n persisted = opened.persisted\n persistenceBlocked = opened.blocked\n }\n openGraphId = request.graphId\n return [\n {\n type: 'opened',\n persisted,\n indexedDocuments: pageCount(),\n ...(request.openId !== undefined ? { openId: request.openId } : {}),\n ...(persistenceBlocked ? { persistenceBlocked } : {}),\n },\n replaceSnapshot(),\n ]\n }\n\n case 'rebuild-begin': {\n if (!db) throw new Error('index not open')\n if (pendingRebuild) abortIndexRebuild(db, pendingRebuild.generation)\n pendingRebuild = {\n id: request.rebuildId,\n generation: beginIndexRebuild(db),\n total: request.total,\n done: 0,\n }\n return []\n }\n\n case 'rebuild-docs': {\n if (!pendingRebuild) throw new Error('rebuild not begun')\n // Another tab may have superseded this rebuild. Its remaining chunks\n // can still be in a MessagePort queue, so stale identity is a no-op,\n // not a protocol failure.\n if (request.rebuildId !== pendingRebuild.id) return []\n if (!db) throw new Error('index not open')\n ingestIndexRebuildChunk(db, pendingRebuild.generation, request.docs)\n pendingRebuild.done += request.docs.length\n return [\n {\n type: 'progress',\n phase: 'indexing',\n done: pendingRebuild.done,\n total: pendingRebuild.total,\n },\n ]\n }\n\n case 'rebuild-commit': {\n if (!db) throw new Error('index not open')\n if (!pendingRebuild) throw new Error('rebuild not begun')\n if (request.rebuildId !== pendingRebuild.id) return []\n const rebuildId = pendingRebuild.id\n commitIndexRebuild(db, pendingRebuild.generation)\n pendingRebuild = undefined\n const committed = replaceSnapshot(rebuildId)\n return [committed]\n }\n\n case 'ingest': {\n if (!db) throw new Error('index not open')\n const changed: IndexDoc[] = []\n for (const doc of request.docs) {\n if (documentHash(db, conceptKey(doc.concept)) !== hashText(doc.text)) {\n changed.push(doc)\n }\n }\n if (changed.length === 0) return []\n\n const affected = new Set<string>()\n const backlinkTargetsChanged = new Set<string>()\n for (const doc of changed) {\n const key = conceptKey(doc.concept)\n const before = indexedDocumentFacts(db, key)\n const after = indexDocumentFactKeys(doc)\n affected.add(key)\n for (const alias of [...before.aliases, ...after.aliases]) affected.add(alias)\n for (const target of [...before.linkTargets, ...after.linkTargets]) {\n affected.add(target)\n backlinkTargetsChanged.add(target)\n }\n }\n const before = conceptCandidatesForKeys(db, affected)\n // `ingestOne` commits each document independently. Invalidate before the\n // first write so a later failure cannot leave same-worker opens reading a\n // cache which predates a partially-applied batch.\n const previousSnapshot = visibleSnapshot\n visibleSnapshot = undefined\n for (const doc of changed) ingestOne(db, doc)\n const after = conceptCandidatesForKeys(db, affected)\n const delta = deltaFor(\n advanceIndexRevision(db),\n before,\n after,\n backlinkTargetsChanged,\n )\n applyDeltaToSnapshot(previousSnapshot, delta)\n return [{ type: 'delta', ...delta }]\n }\n\n case 'snapshot-request':\n return [snapshot()]\n\n case 'barrier':\n // The serial router invokes this only after every earlier request on the\n // connection has completed. It lets the source clear a durable dirty marker\n // only after the corresponding SQLite ingest or generation commit is visible.\n return [{ type: 'barrier', id: request.id }]\n\n case 'backlinks': {\n const groups = db ? backlinksFor(db, request.concept) : []\n return [{ type: 'backlinks', id: request.id, groups }]\n }\n\n case 'search-text': {\n const page = db\n ? searchText(db, request.query, request.offset, request.limit)\n : { groups: [], hasMore: false }\n return [\n {\n type: 'search-text',\n id: request.id,\n groups: page.groups,\n hasMore: page.hasMore,\n },\n ]\n }\n\n case 'search-count': {\n const count = db\n ? searchTextCount(db, request.query)\n : { total: 0, capped: false }\n return [\n {\n type: 'search-count',\n id: request.id,\n total: count.total,\n capped: count.capped,\n },\n ]\n }\n\n case 'tasks': {\n const page = db\n ? tasksMatching(db, request.query, request.offset, request.limit)\n : { hits: [], hasMore: false }\n return [\n {\n type: 'tasks',\n id: request.id,\n hits: page.hits,\n hasMore: page.hasMore,\n total: db ? tasksMatchingCount(db, request.query) : 0,\n },\n ]\n }\n\n case 'asset-usage': {\n // No database means no evidence, and the caller must NOT read that as\n // \"unreferenced\" — it reads it as \"cannot answer\" and refuses the delete.\n const usage = db ? assetUsage(db, request.needles) : { references: 0, documents: [] }\n return [{ type: 'asset-usage', id: request.id, usage }]\n }\n\n case 'close':\n closeActiveDatabase()\n return []\n }\n }\n\n return {\n async handle(request: IndexRequest): Promise<IndexResponse[]> {\n try {\n return await dispatch(request)\n } catch (err) {\n const graphId = openGraphId\n if (!graphId || !isSqliteCorruption(err)) throw err\n await discardWreckedDatabase(graphId, err)\n // The requester learns its request failed AND why, with the prefix the client\n // keys its recovery on; the fresh (empty) snapshot follows so no tab keeps\n // answering \"exists\" from a generation that no longer does.\n const message = `${INDEX_DISCARDED_PREFIX} ${err instanceof Error ? err.message : String(err)}`\n return [\n {\n type: 'error',\n message,\n ...('id' in request ? { id: request.id } : {}),\n ...('rebuildId' in request ? { rebuildId: request.rebuildId } : {}),\n ...(request.type === 'open' && request.openId !== undefined\n ? { openId: request.openId }\n : {}),\n },\n replaceSnapshot(),\n ]\n }\n },\n dispose() {\n closeActiveDatabase()\n },\n }\n}\n\n/**\n * Prefix on the error a wrecked-and-discarded index reports, so the client can tell \"this\n * request failed\" from \"this request failed and the whole index is now empty — re-derive\".\n */\nexport const INDEX_DISCARDED_PREFIX = 'index-discarded:'\n\n/**\n * Whether an error is SQLite saying the file itself is damaged, as opposed to a bad query or\n * a busy lock. sqlite-wasm throws `SQLite3Error` with a `resultCode`; the message form is\n * matched too because the error may have been re-wrapped on its way here.\n */\nexport function isSqliteCorruption(err: unknown): boolean {\n const code = (err as { resultCode?: unknown } | null)?.resultCode\n if (code === SQLITE_CORRUPT || code === SQLITE_NOTADB) return true\n const message = err instanceof Error ? err.message : String(err)\n return /SQLITE_CORRUPT|SQLITE_NOTADB|disk image is malformed|file is not a database/i.test(message)\n}\n\nconst SQLITE_CORRUPT = 11\nconst SQLITE_NOTADB = 26\n\n/** Re-exported so a host can build the doc shape without reaching into index-db. */\nexport type { IndexDoc }\n","/**\n * The tab's half of the [[Derived Index]] (ADR 0041).\n *\n * It owns everything that needs the documents — which change, when to coalesce, and pulling\n * their text out of the store — because the worker cannot read them. It owns nothing about\n * SQL.\n *\n * The two reads the editor makes constantly (`conceptExists` on every keystroke for\n * missing-link styling, `allConcepts` for wikilink completion and Quick Find) are answered\n * from the snapshot the worker pushes after each ingest, so they stay synchronous and never\n * cross a message boundary. `backlinks` is a real round trip; its two callers tolerate it.\n */\n\nimport type {\n AssetUsage,\n ConceptCandidate,\n DbBacklinkGroup,\n IndexDoc,\n SearchDocumentGroup,\n TaskHit,\n TaskQuery,\n} from '../index-db'\nimport { conceptKey } from '../backlinks/backlink-index'\nimport type {\n IndexChangeCheckpoint,\n IndexSource,\n IndexProgress,\n StoreChange,\n} from '../backlinks/live-index'\nimport { INDEX_DISCARDED_PREFIX } from './core'\nimport type { IndexRequest, IndexResponse } from './protocol'\nimport { performanceRecorder } from '$lib/diagnostics/performance'\n\n/**\n * Dirty documents above which replacing the whole index is cheaper than re-indexing each.\n * Bulk import and external reconciliation can report hundreds of named changes together.\n */\nconst INCREMENTAL_LIMIT = 200\n\n/** Documents per rebuild postMessage — bounds each structured clone to a few hundred KB. */\nconst REBUILD_SEND_CHUNK = 150\n\n/** One cold full derivation per graph across every tab in this browser profile. */\nconst REBUILD_LOCK_PREFIX = 'etherpk-index-rebuild:'\n\n/** One persisted-history reconciliation per graph across every tab in this browser profile. */\nconst CATCH_UP_LOCK_PREFIX = 'etherpk-index-catch-up:'\n\n/** A connection to an index server, wherever it runs. */\nexport interface IndexTransport {\n send(request: IndexRequest): void\n onMessage(listener: (response: IndexResponse) => void): void\n close(): void\n}\n\n/**\n * The transport could not get an index `open` accepted or completed. The workspace may retry\n * this specific failure inline; source/rebuild failures are deliberately not this type, because\n * repeating the same graph walk on the main thread only duplicates work and hides the real error.\n */\nexport class IndexTransportOpenError extends Error {\n override name = 'IndexTransportOpenError'\n}\n\nexport interface RemoteGraphIndex {\n /** Synchronous — from the pushed snapshot; safe on every keystroke. */\n conceptExists(concept: string): boolean\n /** Synchronous — likewise pushed, never a query. */\n allConcepts(): readonly ConceptCandidate[]\n /** A round trip to the index server. */\n backlinks(concept: string): Promise<DbBacklinkGroup[]>\n /**\n * [[Search]]'s text group: one page of matching documents. A round trip, like\n * `backlinks` — which is exactly why Search runs it separately from the name group,\n * whose answers come from the synchronous snapshot above and always land first.\n */\n searchText(query: string, offset: number, limit: number): Promise<SearchTextResult>\n /** How many documents match, capped. Issued in parallel; never blocks the rows. */\n searchTextCount(query: string): Promise<SearchCountResult>\n /**\n * Which documents reference an [[Asset]], and how many times. A round trip, issued once\n * when the user asks to delete one — never speculatively. See ADR 0054 for why the count\n * comes from here rather than from a walk of every document.\n */\n assetUsage(needles: readonly string[]): Promise<AssetUsage>\n /**\n * One page of the [[Tasks View]]'s list, with its total. A round trip like `backlinks` —\n * the Tasks View re-issues it when a filter changes or the index updates, never per\n * keystroke: the Name Filter's typeahead reads the synchronous `allConcepts` snapshot.\n */\n tasks(query: TaskQuery, offset: number, limit: number): Promise<TaskPageResult>\n /**\n * Start the transport/worker open without reading the source. `refresh()` consumes this\n * prepared result, so store hydration and a delayed cross-tab attachment can overlap.\n * Failures remain attached to the prepared result and surface from `refresh()`.\n */\n prepare(): Promise<void>\n /**\n * Open on a persisted generation immediately, then let the source catch up genuine\n * changes in the background. A cold index dispatches a staged full stream before it\n * has results to show.\n */\n refresh(): Promise<void>\n /**\n * Discard and re-derive the whole index from the source, on request. The one thing a\n * user can do when they doubt it: the index is never authoritative and always\n * rebuildable (CONTEXT.md → Derived Index), and until this there was no way to invoke that\n * rule short of clearing site data. Streams through a staged generation and swaps at the\n * end, so queries keep answering from the old one meanwhile; resolves once the swap has\n * been confirmed and the snapshot pushed. A rebuild already in flight is awaited, not\n * doubled.\n */\n rebuild(): Promise<void>\n onUpdated(listener: (update: IndexUpdate) => void): () => void\n /** True when this index survives a reload; false means it was rebuilt from scratch. */\n isPersisted(): boolean\n /**\n * Why persistence is unavailable, when known: 'held' means another live context (typically\n * a stale window) holds this graph's pool, which the user can act on; 'unsupported' means\n * this environment cannot persist at all.\n */\n persistenceBlocked(): 'held' | 'unsupported' | undefined\n /** Observe failover between a temporary and persisted worker after the initial open. */\n onPersistenceChanged(listener: (status: IndexPersistenceStatus) => void): () => void\n /**\n * True while the INITIAL build is still deriving the graph (cold open, nothing\n * persisted). Consumers with an empty snapshot can say \"loading\" instead of showing\n * nothing — the wikilink completion's empty popover read as a hang (live, 2026-07-29).\n */\n isBuilding(): boolean\n dispose(): void\n}\n\nexport interface SearchTextResult {\n groups: SearchDocumentGroup[]\n hasMore: boolean\n}\n\nexport interface SearchCountResult {\n total: number\n /** True when there are more matches than were counted — rendered as `1000+`. */\n capped: boolean\n}\n\nexport interface TaskPageResult {\n hits: TaskHit[]\n hasMore: boolean\n /** Every match, not just this page — uncapped, so the header can say \"1–100 of 412\". */\n total: number\n}\n\nexport interface IndexPersistenceStatus {\n persisted: boolean\n blocked?: 'held' | 'unsupported'\n}\n\nexport interface IndexUpdate {\n full: boolean\n changedConceptKeys: ReadonlySet<string>\n backlinkTargetsChanged: ReadonlySet<string>\n}\n\nexport interface RemoteIndexOptions {\n graphId: string\n debounceMs?: number\n /**\n * How long `refresh()` waits for the worker's first answer before giving up. Generous by\n * default — the first answer includes fetching ~900KB of sqlite wasm — but finite, so a\n * worker that cannot start rejects the open instead of hanging it; the caller then falls\n * back to the inline index (ADR 0041: fail closed).\n */\n openTimeoutMs?: number\n /**\n * Idle patience after a ferried port proves the owner worker adopted it, but before the\n * replayed open reaches that worker. Chromium can split those deliveries across long\n * owner-page scheduling gaps. Defaults to 60 seconds.\n */\n adoptedOpenTimeoutMs?: number\n onProgress?: (progress: IndexProgress) => void\n /** A warm-index catch-up failure must be visible without failing the interactive open. */\n onBackgroundError?: (error: Error) => void\n}\n\nexport function createRemoteGraphIndex(\n source: IndexSource,\n transport: IndexTransport,\n options: RemoteIndexOptions,\n): RemoteGraphIndex {\n const debounceMs = options.debounceMs ?? 300\n let existing = new Set<string>()\n let candidates = new Map<string, ConceptCandidate>()\n let revision = 0\n let persisted = false\n let persistenceBlocked: 'held' | 'unsupported' | undefined\n let persistenceKnown = false\n const persistenceChanged = new Set<(status: IndexPersistenceStatus) => void>()\n /**\n * Armed by the shared transport's `transport-changed` marker and cleared by the next\n * honoured open answer. The fresh-empty-worker recovery below fires only while armed,\n * so duplicate open answers, including the compatibility nudge each ferry candidate carries,\n * can never masquerade as a worker replacement (live, 2026-07-31: every such duplicate\n * scheduled another full derivation during bring-up).\n */\n let workerReplaced = false\n /** Set while refresh()'s initial full derivation runs; cleared by its snapshot. */\n let building = false\n const updated = new Set<(update: IndexUpdate) => void>()\n let timer: ReturnType<typeof setTimeout> | undefined\n let rebuildTail = Promise.resolve()\n /** Source events accumulate immediately, but cannot mutate a worker before open completes. */\n let backgroundRebuildsEnabled = false\n let disposed = false\n let activeRebuildId: string | undefined\n let activeRebuildCompletion:\n | {\n promise: Promise<void>\n resolve: () => void\n reject: (error: Error) => void\n }\n | undefined\n let rebuildInvalidated = false\n /** A concurrent worker ingest is not represented in our source-side dirty set. */\n let rebuildRequiresFullRetry = false\n /** A cold generation must exist before its source begins shared history hydration. */\n let coldCatchUpPending = false\n\n /** Concepts whose content changed, or `null` for \"the document set moved — replace all\". */\n let dirty: Set<string> | null = new Set()\n\n let nextId = 1\n let nextOpenId = 1\n const pending = new Map<number, (response: IndexResponse) => void>()\n let onOpened: ((response: Extract<IndexResponse, { type: 'opened' }>) => void) | undefined\n let onOpenAttached: (() => void) | undefined\n /** Stops open-startup patience once the worker confirms that it accepted the request. */\n let onOpenAccepted: ((openId: number) => void) | undefined\n /** A replay onto a replacement worker needs a fresh startup-patience window. */\n let onOpenTransportChanged: (() => void) | undefined\n let awaitedOpenId: number | undefined\n let onOpenFailed: ((error: Error) => void) | undefined\n const rebuildLockAbort = new AbortController()\n /** Any inbound worker message counts as life; see openIndex's activity-based patience. */\n let lastTransportActivity = performance.now()\n let transportGeneration = 0\n type OpenedResponse = Extract<IndexResponse, { type: 'opened' }>\n interface PreparedOpen {\n openId: number\n generation: number\n settled: boolean\n promise: Promise<OpenedResponse>\n }\n let preparedOpen: PreparedOpen | undefined\n\n transport.onMessage((response) => {\n lastTransportActivity = performance.now()\n switch (response.type) {\n case 'snapshot': {\n // Snapshots pushed after the initial rebuild started mean it has content\n // to show (the empty pre-rebuild snapshot arrives WITH 'opened', before\n // `building` is ever set).\n if (!building || response.rebuildId) building = false\n revision = response.revision\n // A full replacement can remove real concepts and pageless candidates.\n // Include both generations so an open editor restyles links which changed\n // from existing to missing, or disappeared from completion entirely.\n const changedConceptKeys = new Set([\n ...existing,\n ...candidates.keys(),\n ...response.existing,\n ...response.candidates.map((candidate) => candidate.key),\n ])\n existing = new Set(response.existing)\n candidates = new Map(response.candidates.map((candidate) => [candidate.key, candidate]))\n for (const listener of updated) {\n listener({\n full: true,\n changedConceptKeys,\n backlinkTargetsChanged: new Set(),\n })\n }\n if (response.rebuildId && activeRebuildId) {\n // A different id means another tab superseded our stream and committed\n // an authoritative full generation. Either way, our stream no longer\n // owns the pending rebuild.\n const shouldRetry = rebuildInvalidated\n const completion = activeRebuildCompletion\n if (rebuildRequiresFullRetry) dirty = null\n activeRebuildId = undefined\n activeRebuildCompletion = undefined\n rebuildInvalidated = false\n rebuildRequiresFullRetry = false\n completion?.resolve()\n // A cold generation is only the cache snapshot. Even when a foreground\n // relay update invalidated it while streaming, post-commit catch-up is\n // the one authoritative retry path. Scheduling another full derivation\n // here duplicated the entire graph before reconciliation had even begun.\n if (coldCatchUpPending && !disposed) {\n coldCatchUpPending = false\n catchUpWarmIndex({ verifyExternalSource: false })\n } else if (shouldRetry && !disposed) {\n queueRebuild()\n }\n }\n return\n }\n case 'delta': {\n if (response.revision !== revision + 1) {\n transport.send({ type: 'snapshot-request' })\n return\n }\n revision = response.revision\n if (activeRebuildId) {\n // Another attached tab changed the worker's visible generation while our\n // staged replacement was being built. The staged commit would overwrite\n // that delta, and its source concept is not necessarily known in this tab.\n rebuildInvalidated = true\n rebuildRequiresFullRetry = true\n }\n for (const key of response.existingAdded) existing.add(key)\n for (const key of response.existingRemoved) existing.delete(key)\n for (const candidate of response.candidateUpserts) {\n candidates.set(candidate.key, candidate)\n }\n for (const key of response.candidateRemoved) candidates.delete(key)\n const changedConceptKeys = new Set([\n ...response.existingAdded,\n ...response.existingRemoved,\n ])\n const update: IndexUpdate = {\n full: false,\n changedConceptKeys,\n backlinkTargetsChanged: new Set(response.backlinkTargetsChanged),\n }\n performanceRecorder.mark('index.delta.received', {\n bytes: new TextEncoder().encode(JSON.stringify(response)).byteLength,\n changed: changedConceptKeys.size,\n })\n for (const listener of updated) listener(update)\n return\n }\n case 'progress':\n options.onProgress?.({ phase: 'indexing', done: response.done, total: response.total })\n return\n case 'attached':\n // The owner's worker has adopted the ferried port. Chromium can still defer\n // the replayed inbound open until a later owner-page scheduling gap, so move\n // from the short \"worker never started\" budget to adoption patience.\n onOpenAttached?.()\n return\n case 'open-accepted':\n // This is stronger than a generic liveness pulse: the open has reached the\n // worker and is now ordered behind any existing core work. A storm-history\n // catch-up can keep that queue busy for minutes, but abandoning it for an\n // inline rebuild would duplicate the whole graph and lose persistence.\n onOpenAccepted?.(response.openId)\n return\n case 'transport-changed':\n workerReplaced = true\n transportGeneration += 1\n if (preparedOpen) {\n if (preparedOpen.settled) {\n // The sharing facade replays its latest correlated open immediately\n // after this marker. Start waiting before that synchronous replay is\n // sent, rather than issuing a duplicate open from refresh later.\n preparedOpen = createPreparedOpen({\n openId: preparedOpen.openId,\n send: false,\n })\n } else {\n // The same in-flight request is replayed. Move its result onto the new\n // generation and restore the timer which open-accepted may have stopped\n // for the worker that just disappeared.\n preparedOpen.generation = transportGeneration\n onOpenTransportChanged?.()\n }\n } else {\n // Cold refresh re-opens directly while holding the rebuild lock rather\n // than through the preparation cache. Its replay needs the same renewed\n // failure detection.\n onOpenTransportChanged?.()\n }\n return\n case 'opened': {\n // Compatibility ferrying can replay opens. Never let a late answer for an\n // older correlated request settle the open currently awaited by this tab.\n if (\n onOpened &&\n response.openId !== undefined &&\n response.openId !== awaitedOpenId\n ) {\n return\n }\n const persistenceDidChange =\n !persistenceKnown ||\n persisted !== response.persisted ||\n persistenceBlocked !== response.persistenceBlocked\n persisted = response.persisted\n persistenceBlocked = response.persistenceBlocked\n persistenceKnown = true\n if (persistenceDidChange) {\n const status = {\n persisted,\n ...(persistenceBlocked ? { blocked: persistenceBlocked } : {}),\n }\n for (const listener of persistenceChanged) listener(status)\n }\n if (onOpened) {\n workerReplaced = false\n onOpened(response)\n } else if (!workerReplaced) {\n // A duplicate answer from the ferry's compatibility nudge, not a\n // replacement. Ignoring it is the whole point of the marker.\n } else if (response.indexedDocuments === 0) {\n // Unsolicited after a real swap, and empty: the shared transport moved\n // onto a fresh worker (owner failover, reclaiming a dead holder's pool,\n // or giving up on an unreachable owner, ADR 0042 §4) whose database\n // holds nothing. Its snapshot has just wiped this tab's concept caches;\n // without a rebuild every wikilink would style as missing from here on\n // (live, 2026-07-28).\n workerReplaced = false\n if (building) {\n // A cold caller has no usable generation and must fall back rather\n // than publish the empty caches from the replacement worker.\n failActiveRebuild(new Error('the index worker was replaced during rebuild'))\n } else {\n // A detached rebuild is opportunistic. Cancel its superseded stream\n // quietly and let the replacement worker's scheduled rebuild take over.\n cancelActiveRebuild()\n }\n rebuildInvalidated = false\n rebuildRequiresFullRetry = false\n scheduleRebuild()\n } else {\n // Unsolicited after a real swap, with content: the replacement pool is\n // adopted as-is, exactly as before the swap markers existed. Known gap,\n // deliberately unchanged here: a takeover of a pool whose last commit\n // predates recent edits serves that older state until the next edit or\n // full refresh reconciles it.\n workerReplaced = false\n }\n return\n }\n default: {\n const id = 'id' in response ? response.id : undefined\n if (id !== undefined) {\n pending.get(id)?.(response)\n pending.delete(id)\n return\n }\n // An error with no request id is the worker itself failing (its script or\n // wasm would not load). If an open is in flight, it must reject, not hang.\n if (response.type === 'error') {\n if (\n onOpenFailed &&\n response.openId !== undefined &&\n response.openId !== awaitedOpenId\n ) {\n return\n }\n // A rebuild stream's chunks and commit are fire-and-forget, so its\n // failures can arrive after the stream has already been failed and a\n // replacement started — a wrecked index does exactly this: the discard\n // fails the stream, then the commit it had already posted reports\n // 'rebuild not begun'. Those must not fail the replacement.\n if (response.rebuildId !== undefined && response.rebuildId !== activeRebuildId) return\n const error = new Error(response.message)\n onOpenFailed?.(error)\n failActiveRebuild(error)\n recoverIfDiscarded(error)\n }\n }\n }\n })\n\n function newRebuildCompletion() {\n let resolve!: () => void\n let reject!: (error: Error) => void\n const promise = new Promise<void>((onResolve, onReject) => {\n resolve = onResolve\n reject = onReject\n })\n // Detached rebuilds keep their rejection handled here as well as at the caller so\n // disposing or replacing a worker cannot produce an unhandled rejection between\n // those two turns of the microtask queue.\n void promise.catch(() => {})\n return { promise, resolve, reject }\n }\n\n function failActiveRebuild(error: Error): void {\n const completion = activeRebuildCompletion\n activeRebuildId = undefined\n activeRebuildCompletion = undefined\n building = false\n completion?.reject(error)\n }\n\n const isIndexDiscarded = (error: Error) => error.message.startsWith(INDEX_DISCARDED_PREFIX)\n\n /**\n * The worker found its stored index corrupt, threw it away and is now serving an EMPTY one\n * (core.ts). Whatever request tripped over it has already failed; what matters is that the\n * graph gets re-derived without anyone asking — the sidebars are answering \"nothing\" until\n * it does. Queued through the ordinary debounce so it lands behind whatever the failure\n * interrupted, and as a full replacement, because there is nothing left to be incremental\n * against.\n */\n function recoverIfDiscarded(error: Error): void {\n if (disposed || !isIndexDiscarded(error)) return\n dirty = null\n queueRebuild()\n }\n\n function cancelActiveRebuild(): void {\n const completion = activeRebuildCompletion\n activeRebuildId = undefined\n activeRebuildCompletion = undefined\n building = false\n completion?.resolve()\n }\n\n function request<T extends IndexResponse>(build: (id: number) => IndexRequest): Promise<T> {\n const id = nextId++\n return new Promise<T>((resolve, reject) => {\n pending.set(id, (response) => {\n if (response.type === 'error') {\n const error = new Error(response.message)\n recoverIfDiscarded(error)\n reject(error)\n } else resolve(response as T)\n })\n transport.send(build(id))\n })\n }\n\n /**\n * The ownership lock in share.ts protects the ONE worker, not the source-side graph walk.\n * Several cold tabs can attach to that worker before its first generation commits, observe\n * the same empty database and each start streaming every document. Serialize that decision\n * separately, then re-open under the lock so followers see the winner's committed result.\n */\n async function withRebuildLock(work: () => Promise<void>): Promise<void> {\n const manager =\n typeof navigator !== 'undefined' &&\n typeof navigator.locks?.request === 'function'\n ? navigator.locks\n : undefined\n if (!manager) {\n await work()\n return\n }\n let entered = false\n try {\n await manager.request(\n REBUILD_LOCK_PREFIX + options.graphId,\n { mode: 'exclusive', signal: rebuildLockAbort.signal },\n async () => {\n entered = true\n if (!disposed) await work()\n },\n )\n } catch (error) {\n if (disposed || rebuildLockAbort.signal.aborted) return\n // A failure inside the protected work is authoritative. Only an unavailable\n // lock service degrades to the old single-tab behaviour.\n if (entered) throw error\n await work()\n }\n }\n\n /**\n * Source catch-up reads and advances shared cache watermarks. Serialising that read/advance\n * pair stops every newly-opened tab from downloading and decrypting the same long server\n * history concurrently. The follower re-checks watermarks only after the winner commits.\n */\n async function withCatchUpLock(work: () => Promise<void>): Promise<void> {\n const manager =\n typeof navigator !== 'undefined' &&\n typeof navigator.locks?.request === 'function'\n ? navigator.locks\n : undefined\n if (!manager) {\n await work()\n return\n }\n let entered = false\n try {\n await manager.request(\n CATCH_UP_LOCK_PREFIX + options.graphId,\n { mode: 'exclusive', signal: rebuildLockAbort.signal },\n async () => {\n entered = true\n if (!disposed) await work()\n },\n )\n } catch (error) {\n if (disposed || rebuildLockAbort.signal.aborted) return\n if (entered) throw error\n await work()\n }\n }\n\n async function openIndex({\n openId = nextOpenId++,\n send = true,\n }: {\n openId?: number\n send?: boolean\n } = {}): Promise<Extract<IndexResponse, { type: 'opened' }>> {\n if (disposed) throw new IndexTransportOpenError('the index was disposed')\n try {\n return await new Promise<Extract<IndexResponse, { type: 'opened' }>>(\n (resolve, reject) => {\n // Startup patience is ACTIVITY-based, not a fixed cliff. Ferry attempts\n // keep it alive until a worker accepts the open. Once `open-accepted`\n // arrives the request is safely ordered in that worker's serial queue,\n // so there is deliberately no client deadline on legitimate work ahead\n // of it. A worker which cannot start never acknowledges and still fails\n // closed. The worker owns a separate terminal safety bound.\n const timeoutMs = options.openTimeoutMs ?? 15_000\n const adoptedTimeoutMs = options.adoptedOpenTimeoutMs ?? 60_000\n let idleTimeoutMs = timeoutMs\n awaitedOpenId = openId\n const cadence = Math.max(25, Math.min(1_000, Math.floor(timeoutMs / 2)))\n let startedAt = performance.now()\n let timer: ReturnType<typeof setInterval> | undefined\n const stopStartupTimer = () => {\n if (timer === undefined) return\n clearInterval(timer)\n timer = undefined\n }\n const startStartupTimer = () => {\n stopStartupTimer()\n startedAt = performance.now()\n timer = setInterval(() => {\n const idleSince = Math.max(startedAt, lastTransportActivity)\n if (performance.now() - idleSince >= idleTimeoutMs) {\n stopStartupTimer()\n reject(new Error('the index worker did not answer'))\n }\n }, cadence)\n }\n startStartupTimer()\n onOpenAccepted = (acceptedOpenId) => {\n if (acceptedOpenId === openId) stopStartupTimer()\n }\n onOpenTransportChanged = () => {\n idleTimeoutMs = timeoutMs\n startStartupTimer()\n }\n onOpenAttached = () => {\n idleTimeoutMs = Math.max(idleTimeoutMs, adoptedTimeoutMs)\n }\n onOpened = (response) => {\n stopStartupTimer()\n resolve(response)\n }\n onOpenFailed = (error) => {\n stopStartupTimer()\n reject(error)\n }\n if (send) transport.send({ type: 'open', graphId: options.graphId, openId })\n },\n ).finally(() => {\n onOpened = undefined\n onOpenAttached = undefined\n onOpenAccepted = undefined\n onOpenTransportChanged = undefined\n onOpenFailed = undefined\n awaitedOpenId = undefined\n })\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n throw new IndexTransportOpenError(message)\n }\n }\n\n function createPreparedOpen({\n openId,\n send,\n }: {\n openId?: number\n send?: boolean\n } = {}): PreparedOpen {\n const correlatedOpenId = openId ?? nextOpenId++\n const pending = openIndex({ openId: correlatedOpenId, send })\n const next: PreparedOpen = {\n openId: correlatedOpenId,\n generation: transportGeneration,\n settled: false,\n promise: pending.then(\n (response) => {\n next.settled = true\n return response\n },\n (error) => {\n next.settled = true\n throw error\n },\n ),\n }\n // GraphWorkspace deliberately starts preparation before awaiting it. Attach a\n // rejection observer now, while preserving the same rejecting promise for refresh.\n void next.promise.catch(() => {})\n return next\n }\n\n function ensurePreparedOpen(): PreparedOpen {\n if (preparedOpen?.generation === transportGeneration) return preparedOpen\n preparedOpen = createPreparedOpen()\n return preparedOpen\n }\n\n async function preparedOpenResponse(consume: boolean): Promise<OpenedResponse> {\n for (;;) {\n const candidate = ensurePreparedOpen()\n let response: OpenedResponse\n try {\n response = await candidate.promise\n } catch (error) {\n if (candidate.generation !== transportGeneration) {\n if (preparedOpen === candidate) preparedOpen = undefined\n continue\n }\n throw error\n }\n if (candidate.generation !== transportGeneration) {\n if (preparedOpen === candidate) preparedOpen = undefined\n continue\n }\n if (consume && preparedOpen === candidate) preparedOpen = undefined\n return response\n }\n }\n\n function catchUpWarmIndex({ verifyExternalSource = true } = {}): void {\n // `dirty` is the monotonic queue of source events observed since subscription.\n // A target preload can emit while a busy shared worker is still answering `open`;\n // clearing here would lose that event after its Local Cache watermark had already\n // advanced. Only `rebuild()` may take and reset the queue. Repeated named changes\n // are harmless because the worker's content hash makes ingest idempotent.\n // The persisted generation is already the browser's durable derived state.\n // A synced source catches documents up from its stored sequence watermarks;\n // only genuine remote changes emit named `onChange` events and cross into the\n // worker. Externally mutable sources without that seam retain the safe full\n // verification path.\n const catchUp = Promise.resolve().then(() =>\n withCatchUpLock(async () => {\n if (source.catchUpPersistedIndex) {\n await source.catchUpPersistedIndex()\n // Cache watermarks and the SQLite index live in different databases. Drain\n // the durable source journal and wait for the worker commit before releasing\n // the cross-tab lock, otherwise a close in this gap can make the stale index\n // look permanently current to the next tab.\n if (timer !== undefined) {\n clearTimeout(timer)\n timer = undefined\n }\n await runRebuild()\n return\n }\n // A follower which just waited for this browser's rebuild lock observed the\n // generation that the winner committed moments ago. Re-reading the same\n // filesystem source immediately would be a second derivation in disguise.\n if (!verifyExternalSource) return\n dirty = null\n await runRebuild()\n }),\n )\n void catchUp.catch((error: unknown) =>\n options.onBackgroundError?.(\n error instanceof Error ? error : new Error(String(error)),\n ),\n )\n }\n\n /** Hand the worker every document. Used when nothing usable is persisted. */\n async function rebuildAll(\n checkpointStream?: IndexChangeCheckpoint['streamForIndex'],\n ): Promise<void> {\n if (disposed) return\n if (activeRebuildId) {\n await activeRebuildCompletion?.promise\n return\n }\n performanceRecorder.mark('index.rebuild.full')\n const rebuildId = crypto.randomUUID()\n activeRebuildId = rebuildId\n const completion = newRebuildCompletion()\n activeRebuildCompletion = completion\n rebuildInvalidated = false\n rebuildRequiresFullRetry = false\n let total: number\n let batches: AsyncIterable<readonly IndexDoc[]>\n try {\n const streamSource = checkpointStream ?? source.streamForIndex\n if (streamSource) {\n const stream = await streamSource({\n onProgress: options.onProgress,\n })\n total = stream.total\n batches = stream.batches\n } else {\n const docs = await source.snapshotForIndex({ onProgress: options.onProgress })\n total = docs.length\n batches = (async function* () {\n for (let start = 0; start < docs.length; start += REBUILD_SEND_CHUNK) {\n yield docs.slice(start, start + REBUILD_SEND_CHUNK)\n }\n })()\n }\n if (disposed || activeRebuildId !== rebuildId) {\n await completion.promise\n return\n }\n transport.send({ type: 'rebuild-begin', rebuildId, total })\n let sent = 0\n for await (const batch of batches) {\n if (disposed || activeRebuildId !== rebuildId) {\n await completion.promise\n return\n }\n if (sent > 0) await new Promise((resolve) => setTimeout(resolve))\n const docs = [...batch]\n transport.send({ type: 'rebuild-docs', rebuildId, docs })\n sent += docs.length\n }\n if (!disposed && activeRebuildId === rebuildId) {\n transport.send({ type: 'rebuild-commit', rebuildId })\n // Posting the commit only puts it in the worker's queue. A cold workspace\n // cannot mount editors until the worker confirms that the generation was\n // atomically swapped and pushes the caches used for wikilink styling.\n await completion.promise\n }\n } catch (error) {\n const reported = error instanceof Error ? error : new Error(String(error))\n if (activeRebuildId === rebuildId) failActiveRebuild(reported)\n throw reported\n }\n }\n\n async function rebuild(): Promise<void> {\n if (disposed) return\n const checkpoint = await source.pendingIndexChanges?.()\n const checkpointDocuments = new Map(\n (checkpoint?.documents ?? []).map((document) => [\n conceptKey(document.concept),\n document,\n ]),\n )\n if (checkpoint?.changes === null) {\n dirty = null\n } else if (checkpoint && dirty !== null) {\n for (const change of checkpoint.changes) dirty.add(change.concept)\n }\n const changed = dirty\n dirty = new Set()\n try {\n const checkpointCoversChanged =\n changed !== null &&\n [...changed].every((concept) =>\n checkpointDocuments.has(conceptKey(concept)),\n )\n if (\n changed === null ||\n (changed.size > INCREMENTAL_LIMIT && !checkpointCoversChanged)\n ) {\n await rebuildAll(checkpoint?.streamForIndex)\n } else if (changed.size > 0) {\n const docs: IndexDoc[] = []\n for (const concept of changed) {\n const doc =\n checkpointDocuments.get(conceptKey(concept)) ??\n (await source.snapshotDocument?.(concept))\n if (doc) docs.push(doc)\n }\n if (docs.length !== changed.size) {\n // A named document disappeared before its snapshot was read. Only a full\n // replacement can remove its old rows safely.\n await rebuildAll(checkpoint?.streamForIndex)\n } else {\n // A cold post-commit hydration can make every cache row dirty at once.\n // Those rows already have exact, cache-bound snapshots, so replacing the\n // just-committed generation would be a duplicate derivation. Send bounded\n // targeted chunks instead and acknowledge them behind one ordered barrier.\n for (let start = 0; start < docs.length; start += REBUILD_SEND_CHUNK) {\n transport.send({\n type: 'ingest',\n docs: docs.slice(start, start + REBUILD_SEND_CHUNK),\n })\n }\n await request<Extract<IndexResponse, { type: 'barrier' }>>((id) => ({\n type: 'barrier',\n id,\n }))\n }\n }\n await checkpoint?.acknowledge()\n } catch (error) {\n // Keep volatile work retryable in this session. A durable checkpoint remains\n // unacknowledged as the cross-restart source of truth as well.\n if (changed === null) dirty = null\n else if (dirty !== null) for (const concept of changed) dirty.add(concept)\n throw error\n }\n }\n\n /** Source events and the terminal catch-up flush share one ordered main-thread pipeline. */\n function runRebuild(): Promise<void> {\n const next = rebuildTail.then(() => rebuild())\n rebuildTail = next.catch(() => undefined)\n return next\n }\n\n function queueRebuild(): void {\n if (timer !== undefined) clearTimeout(timer)\n timer = setTimeout(() => {\n timer = undefined\n void runRebuild().catch((error: unknown) =>\n options.onBackgroundError?.(\n error instanceof Error ? error : new Error(String(error)),\n ),\n )\n }, debounceMs)\n }\n\n function scheduleRebuild(change?: StoreChange): void {\n if (!change || !source.snapshotDocument) dirty = null\n else if (dirty !== null) dirty.add(change.concept)\n if (activeRebuildId) {\n rebuildInvalidated = true\n return\n }\n if (!backgroundRebuildsEnabled) return\n queueRebuild()\n }\n\n const unsubscribe = source.onChange(scheduleRebuild)\n\n return {\n conceptExists: (concept) => existing.has(conceptKey(concept)),\n allConcepts: () =>\n [...candidates.values()].sort((a, b) => a.key.localeCompare(b.key)),\n async backlinks(concept) {\n const response = await request<Extract<IndexResponse, { type: 'backlinks' }>>((id) => ({\n type: 'backlinks',\n id,\n concept,\n }))\n return response.groups\n },\n async assetUsage(needles) {\n const response = await request<Extract<IndexResponse, { type: 'asset-usage' }>>((id) => ({\n type: 'asset-usage',\n id,\n needles: [...needles],\n }))\n return response.usage\n },\n async searchText(query, offset, limit) {\n const response = await request<Extract<IndexResponse, { type: 'search-text' }>>(\n (id) => ({ type: 'search-text', id, query, offset, limit }),\n )\n return { groups: response.groups, hasMore: response.hasMore }\n },\n async searchTextCount(query) {\n const response = await request<Extract<IndexResponse, { type: 'search-count' }>>(\n (id) => ({ type: 'search-count', id, query }),\n )\n return { total: response.total, capped: response.capped }\n },\n async tasks(query, offset, limit) {\n const response = await request<Extract<IndexResponse, { type: 'tasks' }>>((id) => ({\n type: 'tasks',\n id,\n query,\n offset,\n limit,\n }))\n return { hits: response.hits, hasMore: response.hasMore, total: response.total }\n },\n prepare() {\n const preparation = preparedOpenResponse(false).then(() => {})\n // A caller may intentionally fire-and-overlap this with store hydration. The\n // authoritative failure is still returned and later rethrown by refresh.\n void preparation.catch(() => {})\n return preparation\n },\n isPersisted: () => persisted,\n persistenceBlocked: () => persistenceBlocked,\n onPersistenceChanged(listener) {\n persistenceChanged.add(listener)\n return () => persistenceChanged.delete(listener)\n },\n isBuilding: () => building,\n async refresh() {\n const opened = await preparedOpenResponse(true)\n // Something usable was already on disk. Trust it for the interactive open.\n // Subsequent document deltas correct changed rows, and an explicit full refresh\n // streams content through a staged generation before swapping it into view.\n if (opened.indexedDocuments > 0) {\n backgroundRebuildsEnabled = true\n catchUpWarmIndex()\n return\n }\n // Keep every follower on GraphWorkspace's loading surface while the first tab\n // derives and commits the shared generation.\n building = true\n await withRebuildLock(async () => {\n // Another tab may have completed the graph walk while this tab waited.\n // Re-open is idempotent in the shared worker and avoids touching the source.\n const afterLock = await openIndex()\n if (afterLock.indexedDocuments > 0) {\n building = false\n backgroundRebuildsEnabled = true\n catchUpWarmIndex({ verifyExternalSource: false })\n return\n }\n dirty = null\n coldCatchUpPending = true\n await runRebuild()\n backgroundRebuildsEnabled = true\n })\n },\n async rebuild() {\n if (disposed) return\n // A null queue is the full-replacement signal `rebuild()` already honours for an\n // over-limit change set: every document is streamed, the worker inserts each into\n // the staged generation without consulting its stored hash, and the commit swaps\n // the whole thing. Queued on the same ordered tail as source events, so a keystroke\n // landing mid-rebuild is not lost — it is ingested after the swap.\n dirty = null\n try {\n await runRebuild()\n } catch (error) {\n // The rebuild is precisely what a user reaches for when the index is wrecked,\n // and a wreck fails the first attempt by discarding itself. Once over.\n if (!(error instanceof Error) || !isIndexDiscarded(error)) throw error\n dirty = null\n await runRebuild()\n }\n },\n onUpdated(listener) {\n updated.add(listener)\n return () => updated.delete(listener)\n },\n dispose() {\n disposed = true\n onOpenFailed?.(new Error('the index was disposed'))\n preparedOpen = undefined\n rebuildLockAbort.abort()\n // Disposal is an expected workspace lifecycle event, not a background failure.\n cancelActiveRebuild()\n if (timer !== undefined) clearTimeout(timer)\n unsubscribe()\n updated.clear()\n persistenceChanged.clear()\n transport.send({ type: 'close' })\n transport.close()\n },\n }\n}\n","/**\n * Choosing where the index server runs, and talking to it (ADR 0041 §2–§4).\n *\n * Two hosts, one protocol:\n *\n * 1. **Dedicated Worker**: the elected graph owner persists the index to OPFS and serves\n * ferried ports from every other tab. A temporary follower worker can be configured\n * memory-only while a known owner is unreachable (ADR 0042).\n * 2. **Inline** — no Worker at all (node, vitest, a CSP that refuses one). Same core, same\n * messages, on the calling thread.\n *\n * **Why not a SharedWorker**, which ADR 0041 originally chose for cross-tab sharing: measured\n * in Chromium 149, a SharedWorker cannot use the `opfs-sahpool` VFS (\"Missing required OPFS\n * APIs\") and cannot spawn a nested Worker to hold it either (\"Worker is not defined\"). Sharing\n * and persistence are therefore mutually exclusive, and persistence is the half that fixes a\n * multi-second open. See ADR 0041's amendment.\n *\n * Nothing above this file knows which one it got.\n */\n\nimport { createIndexCore, type IndexDbHost } from './core'\nimport { openInMemorySqlDb } from '../index-db-sqlite'\nimport type { IndexResponse } from './protocol'\nimport type { IndexTransport } from './client'\n\n/** In-memory host: no OPFS, so nothing survives the session. */\nexport function memoryDbHost(blocked?: 'held' | 'unsupported'): IndexDbHost {\n return {\n async open() {\n return {\n db: await openInMemorySqlDb(),\n persisted: false,\n ...(blocked ? { blocked } : {}),\n }\n },\n async discard() {\n return {\n db: await openInMemorySqlDb(),\n persisted: false,\n ...(blocked ? { blocked } : {}),\n }\n },\n }\n}\n\n/** Drive the core directly on this thread — the last-resort host, and the one tests use. */\nexport function inlineTransport(host: IndexDbHost = memoryDbHost()): IndexTransport {\n const core = createIndexCore(host)\n const listeners = new Set<(response: IndexResponse) => void>()\n // One request at a time, in order — the same contract the worker's connection router\n // gives the core. Unserialised, a request arriving while the core is mid-way through an\n // async step (discarding a corrupt database, say) saw no database at all.\n let tail: Promise<void> = Promise.resolve()\n return {\n send(request) {\n tail = tail\n .then(() => core.handle(request))\n .then((responses) => {\n for (const response of responses) for (const l of listeners) l(response)\n })\n .catch((err: unknown) => {\n const message = err instanceof Error ? err.message : String(err)\n // Shaped as the connection router shapes it: the failed request's id, and\n // its rebuild stream, so the client can match and discount it.\n for (const l of listeners) {\n l({\n type: 'error',\n message,\n ...('id' in request ? { id: request.id } : {}),\n ...('rebuildId' in request ? { rebuildId: request.rebuildId } : {}),\n ...(request.type === 'open' && request.openId !== undefined\n ? { openId: request.openId }\n : {}),\n })\n }\n })\n },\n onMessage(listener) {\n listeners.add(listener)\n },\n close() {\n listeners.clear()\n core.dispose()\n },\n }\n}\n\n/** Wrap a MessagePort or a Worker in the transport shape. */\nexport function portTransport(port: MessagePort | Worker, stop: () => void): IndexTransport {\n const listeners = new Set<(response: IndexResponse) => void>()\n port.addEventListener('message', (event) => {\n const response = (event as MessageEvent<IndexResponse>).data\n for (const l of listeners) l(response)\n })\n // A worker whose SCRIPT or wasm cannot load fires 'error', never 'message'. Without\n // this, the client awaited 'opened' forever and the graph never finished opening\n // (live, 2026-07-28: a broken service worker rejecting the worker's fetches).\n port.addEventListener('error', (event) => {\n const message = (event as ErrorEvent).message || 'the index worker failed to start'\n for (const l of listeners) l({ type: 'error', message })\n })\n if ('start' in port) port.start()\n return {\n send: (request) => port.postMessage(request),\n onMessage: (listener) => listeners.add(listener),\n close() {\n listeners.clear()\n stop()\n },\n }\n}\n\n/** A dedicated index worker plus the hooks that ferry other tabs' ports into it (ADR 0042). */\nexport interface DedicatedWorkerHost {\n transport: IndexTransport\n adopt(port: MessagePort): void\n /**\n * Give the worker a port the BROKER holds the other end of, so ferried client ports\n * reach the worker without crossing this page's main thread. A wedged owner page must\n * not starve every other tab's attach (live, 2026-07-31).\n */\n adoptBridge(port: MessagePort): void\n}\n\nexport interface DedicatedWorkerHostOptions {\n /** A temporary follower already knows another worker owns OPFS, so it must use memory. */\n persistence?: 'opfs' | 'memory'\n}\n\n/** Spawn the dedicated index worker, or null where Workers do not exist. */\nexport function createDedicatedWorkerHost(\n options: DedicatedWorkerHostOptions = {},\n): DedicatedWorkerHost | null {\n if (typeof Worker === 'undefined') return null\n try {\n const worker = new Worker(new URL('./index-worker.ts', import.meta.url), { type: 'module' })\n if (options.persistence === 'memory') {\n // Worker messages from one sender are ordered. Configure the host before the\n // queued `open` is replayed, avoiding a futile 12-second OPFS contention loop\n // when a live owner is already known to hold this graph's pool.\n worker.postMessage({ type: 'configure-host', persistence: 'memory' })\n }\n return {\n transport: portTransport(worker, () => worker.terminate()),\n adopt: (port) => worker.postMessage({ type: 'adopt-client' }, [port]),\n adoptBridge: (port) => worker.postMessage({ type: 'adopt-bridge' }, [port]),\n }\n } catch {\n return null\n }\n}\n\n/**\n * The best transport this browser can give us. Never throws: every failure step degrades to\n * the next host, because an index that cannot be shared or persisted is a slower app, while\n * no index at all is a broken one.\n */\nexport function createIndexTransport(): IndexTransport {\n return createDedicatedWorkerHost()?.transport ?? inlineTransport()\n}\n\n/** A worker-backed in-memory index for temporary service while another tab owns OPFS. */\nexport function createMemoryIndexTransport(): IndexTransport {\n return (\n createDedicatedWorkerHost({ persistence: 'memory' })?.transport ??\n inlineTransport(memoryDbHost('held'))\n )\n}\n","/**\n * Pure date helpers for the month grid both calendars paint — the [[Journal Calendar]] in the\n * [[Sidebar]] and the Command Menu's pick-a-date popover (Dual Mode Editor.md → *Command Menu*,\n * \"Date Picker\"). No DOM, no CodeMirror: the ISO format a [[Journal Concept]] is written in, a\n * Monday-start month grid, and the twelve-month grid the year view pages by. The renderer that\n * turns these into elements is `month-grid.ts`.\n *\n * Everything here is **local time**. A journal entry belongs to the day its author was living\n * in, not to a UTC instant, so `formatISODate(new Date())` is the one definition of \"today\" the\n * app uses (ADR 0056).\n */\n\n/** A single calendar cell. `iso` is the `YYYY-MM-DD` the day resolves to. */\nexport interface DayCell {\n year: number\n /** 0-based month. */\n month: number\n day: number\n /** False for the leading/trailing days that belong to the adjacent month. */\n inMonth: boolean\n iso: string\n}\n\n/** `YYYY-MM-DD` for a local date — the journal file-name format (fs/identity.ts). */\nexport function formatISODate(date: Date): string {\n const y = date.getFullYear()\n const m = String(date.getMonth() + 1).padStart(2, '0')\n const d = String(date.getDate()).padStart(2, '0')\n return `${y}-${m}-${d}`\n}\n\n/** Today, as the ISO day the user is living in. The app's single definition of \"today\". */\nexport function todayISO(now: Date = new Date()): string {\n return formatISODate(now)\n}\n\n/**\n * Milliseconds from `now` until the next local midnight — when `todayISO()` changes.\n *\n * For anything painted from \"today\" that stays on screen for days: a tab left open overnight\n * keeps the old day's mark until it repaints, so callers schedule one repaint this far ahead.\n * Local, like everything here, so a DST change lands on the calendar day the clock does. Always\n * at least 1ms: a timer set exactly at midnight must not fire in the same day again.\n */\nexport function msUntilNextLocalMidnight(now: Date = new Date()): number {\n const next = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1)\n return Math.max(1, next.getTime() - now.getTime())\n}\n\nconst ISO_SHAPE = /^(\\d{4})-(\\d{2})-(\\d{2})$/\n\n/**\n * The local `Date` a `YYYY-MM-DD` string names, or `null` when it names no day that exists.\n *\n * The shape test alone is not enough: `2026-02-30` and `2026-13-01` match it and are not days.\n * Round-tripping through `Date` catches both, because JS silently rolls an out-of-range field\n * forward (31 February becomes 3 March) and the re-format then disagrees with the input.\n */\nexport function parseISODate(value: string): Date | null {\n const match = ISO_SHAPE.exec(value.trim())\n if (!match) return null\n const [, y, m, d] = match\n const date = new Date(Number(y), Number(m) - 1, Number(d))\n return formatISODate(date) === value.trim() ? date : null\n}\n\n/** True when `value` names a calendar day that actually exists. */\nexport function isCalendarDay(value: string): boolean {\n return parseISODate(value) !== null\n}\n\n/** A new Date `days` after `date` (normalises across month/year boundaries). */\nexport function shiftDate(date: Date, days: number): Date {\n return new Date(date.getFullYear(), date.getMonth(), date.getDate() + days)\n}\n\n/** A new Date `months` after `date`, clamping the day to the target month's length. */\nexport function shiftMonth(date: Date, months: number): Date {\n const target = new Date(date.getFullYear(), date.getMonth() + months, 1)\n const lastDay = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate()\n return new Date(target.getFullYear(), target.getMonth(), Math.min(date.getDate(), lastDay))\n}\n\n/** A new Date `years` after `date`. Clamps 29 February onto a non-leap year's 28th. */\nexport function shiftYear(date: Date, years: number): Date {\n return shiftMonth(date, years * 12)\n}\n\n/** Day-of-week with **Monday = 0** … Sunday = 6 (British week start). */\nfunction mondayIndex(date: Date): number {\n return (date.getDay() + 6) % 7\n}\n\n/**\n * The 42 cells (6 weeks × 7 days, Monday-first) covering `month` of `year` (0-based month),\n * including the leading/trailing adjacent-month days that pad the grid. Each cell carries its\n * own year/month/day, an `inMonth` flag, and its ISO string.\n *\n * Always six rows, even for a month that fits in five: a grid that changed height as the user\n * paged would shunt everything below it in the Sidebar.\n */\nexport function monthGridDays(year: number, month: number): DayCell[] {\n const first = new Date(year, month, 1)\n const start = shiftDate(first, -mondayIndex(first)) // back up to the Monday of week 1\n const cells: DayCell[] = []\n for (let i = 0; i < 42; i++) {\n const d = shiftDate(start, i)\n cells.push({\n year: d.getFullYear(),\n month: d.getMonth(),\n day: d.getDate(),\n inMonth: d.getMonth() === month && d.getFullYear() === year,\n iso: formatISODate(d),\n })\n }\n return cells\n}\n\n/** One cell of the year view: a month of `year`, with the short name it is labelled by. */\nexport interface MonthCell {\n year: number\n /** 0-based month. */\n month: number\n label: string\n}\n\n/**\n * The twelve months of `year`, for the view the month header opens. Paging a month at a time\n * to reach a distant year is not navigation, it is a chore; from here any month of any year is\n * two clicks away.\n */\nexport function yearGridMonths(year: number): MonthCell[] {\n const cells: MonthCell[] = []\n for (let month = 0; month < 12; month++) {\n cells.push({\n year,\n month,\n label: new Date(year, month, 1).toLocaleString(undefined, { month: 'short' }),\n })\n }\n return cells\n}\n","/**\n * What makes a [[Concept]] a [[Journal Concept]] — the one predicate that decides where a\n * promoting [[Draft]] writes, and which concepts refuse to be renamed (ADR 0056).\n *\n * It is deliberately **not** the `\\d{4}-\\d{2}-\\d{2}` shape it grew out of. While the shape only\n * guarded rename, letting `2026-13-45` through cost nothing; now that the same answer decides\n * whether a file lands in `journals/` or `pages/`, a loose test would mint a journal entry for a\n * day no calendar can display or reach again.\n *\n * Pure; no DOM, no I/O.\n */\n\nimport { isCalendarDay } from './calendar/month-grid-core'\n\n/**\n * True when `concept` names a calendar day that exists — a [[Journal Entry]]'s identity.\n *\n * Journal identity is the day itself, so this is case- and alias-free: there is nothing to\n * normalise beyond surrounding whitespace.\n */\nexport function isJournalConcept(concept: string): boolean {\n return isCalendarDay(concept)\n}\n","/**\n * Parse a document's YAML frontmatter. Frontmatter is authoritative for identity\n * (DESIGN.md → On-disk layout); the rest of the storage layer reads `title`,\n * `aliases`, etc. from `data`.\n *\n * A hand-edited file must never crash the scan, so malformed or unterminated\n * frontmatter degrades to \"no frontmatter\" (`data: {}`, `body` = the whole text)\n * rather than throwing.\n */\n\nimport { parse as parseYaml } from 'yaml'\n\nimport { frontmatterSpan } from './frontmatter-span'\n\nexport interface ParsedFrontmatter {\n /** The parsed YAML object, or `{}` when there is none / it is malformed. */\n data: Record<string, unknown>\n /** The document text after the frontmatter block (the whole text if none). */\n body: string\n}\n\nexport function parseFrontmatter(text: string): ParsedFrontmatter {\n // What counts as Frontmatter is decided in one place (frontmatter-span.ts), so the editor's\n // styling, its completion guards and this parse cannot disagree about the same file.\n const span = frontmatterSpan(text)\n if (!span) return { data: {}, body: text }\n\n const body = text.slice(span.end)\n try {\n const parsed = parseYaml(span.body)\n // YAML can parse to a scalar/array/null; only a plain object is frontmatter.\n if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {\n return { data: parsed as Record<string, unknown>, body }\n }\n return { data: {}, body }\n } catch {\n // Malformed YAML in a hand-edited file: treat as no frontmatter.\n return { data: {}, body }\n }\n}\n","/**\n * Concept identity, as fixed by CONTEXT.md → Concept / Alias / Slug:\n *\n * - A page's canonical concept name is its frontmatter `title`; the filename is a\n * derived convenience (and the fallback when `title` is absent). This is why a\n * filesystem-mangled name never corrupts identity (DESIGN.md → On-disk layout).\n * - Identity is **case-insensitive** (`conceptKey`), display is **case-preserving**.\n * - A journal entry is identified by its ISO date (its filename stem).\n *\n * Pure; no DOM, no I/O.\n */\n\nimport type { Subdir } from './directory-adapter'\n\nexport type DocumentKind = 'journal' | 'page'\n\n/** The kind a content subdir holds, or `null` for non-document subdirs. */\nexport function documentKindOf(subdir: Subdir): DocumentKind | null {\n if (subdir === 'journals') return 'journal'\n if (subdir === 'pages') return 'page'\n return null\n}\n\n/** Strip a single trailing `.md` extension. */\nexport function fileStem(fileName: string): string {\n return fileName.replace(/\\.md$/i, '')\n}\n\n/** The case-insensitive identity key for a concept (display casing is preserved elsewhere). */\nexport function conceptKey(concept: string): string {\n return concept.toLowerCase()\n}\n\n/** The display concept name for a page: frontmatter `title` if a non-empty string, else the filename stem. */\nexport function conceptOf(fm: { data: Record<string, unknown> }, fileNameStem: string): string {\n const title = fm.data.title\n if (typeof title === 'string' && title.trim() !== '') return title\n return fileNameStem\n}\n\n/** The concept (ISO date) of a journal entry, derived from its filename. */\nexport function journalConceptOf(fileName: string): string {\n return fileStem(fileName)\n}\n\n/** The aliases declared in frontmatter (`aliases:`), as a string array; `[]` when absent/invalid. */\nexport function aliasesOf(fm: { data: Record<string, unknown> }): string[] {\n const aliases = fm.data.aliases\n if (!Array.isArray(aliases)) return []\n return aliases.filter((a): a is string => typeof a === 'string')\n}\n","/**\n * What [[Frontmatter]] says about a document's identity, and how to rewrite it (ADR 0061).\n *\n * Frontmatter is real text on both backends. On a [[Filesystem Backend]] it is authoritative\n * (ADR 0007); on a [[Server Backend]] the encrypted registry is, and the block is a *proposal*\n * about it. Either way exactly two properties carry identity - `title` and `aliases` - and\n * everything else in the block is kept verbatim for whatever feature defines it. This module is\n * the one place that reads those two keys out of a block and writes them back into one, so the\n * stores, the editor's proposal detection and the mirror cannot disagree about what a block\n * claims.\n *\n * Pure: no store, no editor. The YAML rules (what counts as a block, how it parses) come from\n * `storage/fs`, shared with the scan and the editor's analysis, so there is one rule.\n */\nimport { parse as parseYaml, stringify as stringifyYaml } from 'yaml'\n\nimport { parseFrontmatter } from '$lib/storage/fs/frontmatter'\nimport { frontmatterSpan } from '$lib/storage/fs/frontmatter-span'\nimport { aliasesOf, conceptKey } from '$lib/storage/fs/identity'\n\n/**\n * What an edit to a property does when the editing episode ends:\n * - `confirm`: opens a dialog before anything changes (a rename is a graph-wide operation).\n * - `silent`: applied as typed.\n * - `none`: recognised and documented, but nothing reads it yet.\n */\nexport type FrontmatterPolicy = 'confirm' | 'silent' | 'none'\n\nexport interface FrontmatterProperty {\n key: string\n policy: FrontmatterPolicy\n /** One line for the docs table and any UI that lists what the block can do. */\n summary: string\n}\n\n/**\n * The known properties, in the order their steps run. Silent steps come before confirmed ones,\n * so a rename cascade sees the aliases the user just wrote. A future property that needs its own\n * confirmation gets a row here and a step in `proposal.ts`; the sequencing needs no change.\n */\nexport const FRONTMATTER_PROPERTIES: readonly FrontmatterProperty[] = [\n {\n key: 'aliases',\n policy: 'silent',\n summary: 'Other names the document answers to. Applied when you leave the block.',\n },\n {\n key: 'title',\n policy: 'confirm',\n summary: 'The document’s name. Changing it opens the rename dialog when you leave the block.',\n },\n {\n key: 'public',\n policy: 'none',\n summary: 'Reserved for publishing. Kept in the text; nothing reads it yet.',\n },\n]\n\nexport interface FrontmatterIdentity {\n /** The `title` value, or null when the block has none (or only a blank one). */\n title: string | null\n aliases: string[]\n /** Whether the document carries a balanced block at all. No block is no claim. */\n hasBlock: boolean\n}\n\n/** What the block claims. An unterminated block is not a block, as everywhere else. */\nexport function frontmatterIdentity(text: string): FrontmatterIdentity {\n const hasBlock = frontmatterSpan(text) !== null\n const fm = parseFrontmatter(text)\n return { title: titleOf(fm.data), aliases: aliasesOf(fm), hasBlock }\n}\n\nfunction titleOf(data: Record<string, unknown>): string | null {\n const title = data.title\n return typeof title === 'string' && title.trim() !== '' ? title : null\n}\n\n/**\n * Aliases as identity sees them: trimmed, non-blank, unique case-insensitively (first spelling\n * wins), and never the document's own name - a document is not an alias of itself.\n */\nexport function normaliseAliases(aliases: readonly string[], concept?: string): string[] {\n const seen = new Set<string>(concept === undefined ? [] : [conceptKey(concept)])\n const out: string[] = []\n for (const raw of aliases) {\n const alias = raw.trim()\n if (alias === '') continue\n const key = conceptKey(alias)\n if (seen.has(key)) continue\n seen.add(key)\n out.push(alias)\n }\n return out\n}\n\n/** Set equality by identity key: order and case carry no meaning. */\nexport function sameAliases(a: readonly string[], b: readonly string[]): boolean {\n const left = new Set(a.map(conceptKey))\n const right = new Set(b.map(conceptKey))\n return left.size === right.size && [...left].every((key) => right.has(key))\n}\n\nexport interface IdentityPatch {\n /** A string sets the title; null removes it; undefined leaves it alone. */\n title?: string | null\n /** Empty removes the key; undefined leaves it alone. */\n aliases?: readonly string[]\n}\n\nexport interface WriteOptions {\n /**\n * Add a block to a document that has none. Off by default: a document without a block has\n * made no claim, and a synced document never grows one unprompted (ADR 0061).\n */\n addBlock?: boolean\n}\n\n/**\n * The text with its identity keys rewritten. Other keys keep their values and their order; a\n * new `title` goes first and new `aliases` last, which is how a fresh page's block reads. The\n * body is untouched. Returns the very same string when nothing would change, so callers can\n * write back on every save without reformatting a block that already agrees. A block whose YAML\n * does not parse is left alone: rewriting it would destroy whatever the user was typing.\n */\nexport function withFrontmatterIdentity(text: string, patch: IdentityPatch, options: WriteOptions = {}): string {\n const span = frontmatterSpan(text)\n if (!span) {\n if (!options.addBlock) return text\n const data: Record<string, unknown> = {}\n if (typeof patch.title === 'string' && patch.title.trim() !== '') data.title = patch.title\n const aliases = patch.aliases ? [...patch.aliases] : []\n if (aliases.length > 0) data.aliases = aliases\n if (Object.keys(data).length === 0) return text\n return `---\\n${stringifyYaml(data)}---\\n${text}`\n }\n\n const data = parseBlock(span.body)\n if (data === null) return text\n const currentTitle = titleOf(data)\n const currentAliases = aliasesOf({ data })\n const wantTitle = patch.title === undefined ? currentTitle : patch.title\n const wantAliases = patch.aliases === undefined ? currentAliases : [...patch.aliases]\n if (wantTitle === currentTitle && sameAliases(wantAliases, currentAliases)) return text\n\n const next: Record<string, unknown> = {}\n if (wantTitle !== null && !('title' in data)) next.title = wantTitle\n for (const [key, value] of Object.entries(data)) {\n if (key === 'title') {\n if (wantTitle !== null) next.title = wantTitle\n continue\n }\n if (key === 'aliases') {\n if (wantAliases.length > 0) next.aliases = wantAliases\n continue\n }\n next[key] = value\n }\n if (wantAliases.length > 0 && !('aliases' in data)) next.aliases = wantAliases\n return `---\\n${serialise(next)}---\\n${text.slice(span.end)}`\n}\n\n/**\n * The text with `title` and `aliases` removed from its block, and the block itself removed if\n * nothing else was in it. For an [[Import]] into a synced graph, where identity goes into the\n * registry and the rest of the block stays with the document.\n */\nexport function withoutIdentityKeys(text: string): string {\n const span = frontmatterSpan(text)\n if (!span) return text\n const data = parseBlock(span.body)\n if (data === null) return text\n if (!('title' in data) && !('aliases' in data)) return text\n const rest: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(data)) {\n if (key !== 'title' && key !== 'aliases') rest[key] = value\n }\n if (Object.keys(rest).length === 0) return text.slice(span.end)\n return `---\\n${serialise(rest)}---\\n${text.slice(span.end)}`\n}\n\n/** The block's YAML as a plain object, or null when it is malformed or not an object. */\nfunction parseBlock(yaml: string): Record<string, unknown> | null {\n try {\n const parsed: unknown = parseYaml(yaml)\n if (parsed === null || parsed === undefined) return {}\n if (typeof parsed !== 'object' || Array.isArray(parsed)) return null\n return parsed as Record<string, unknown>\n } catch {\n return null\n }\n}\n\nfunction serialise(data: Record<string, unknown>): string {\n return Object.keys(data).length === 0 ? '' : stringifyYaml(data)\n}\n","/**\n * The backend-agnostic document seam.\n *\n * The editor binds to an {@link EditorDocument}; the backend owns the text —\n * CodeMirror is never the source of truth (see ADR 0010). The Local engine will\n * implement {@link DocumentStore} over a file buffer, the Server engine over a\n * Yjs Y.Text; the harness uses an in-memory store. All three look identical here.\n */\n\nimport type { ViewRef } from '$lib/layout'\n\nexport class DocumentNotFoundError extends Error {\n constructor(readonly target: string) {\n super(`No document for \"${target}\"`)\n this.name = 'DocumentNotFoundError'\n }\n}\n\nexport class DocumentSyncDegradedError extends Error {\n constructor(\n readonly target: string,\n readonly status:\n | 'key-unavailable'\n | 'ciphertext-corrupt'\n | 'generation-stale'\n | 'sequence-gap',\n ) {\n super(`Document \"${target}\" is sync-degraded: ${status}`)\n this.name = 'DocumentSyncDegradedError'\n }\n}\n\n/** A {@link ViewRef} narrowed to documents — `kind` is always `'document'`. */\nexport interface DocumentRef extends ViewRef {\n kind: 'document'\n /** The document's stable id (its concept name / file id). */\n target: string\n}\n\n/** A single text edit, expressed as a range replacement (CodeMirror-shaped). */\nexport interface TextChange {\n /** UTF-16 offset where the replacement starts. */\n from: number\n /** UTF-16 offset where the replacement ends (`from` for a pure insert). */\n to: number\n /** Replacement text (`''` for a pure delete). */\n insert: string\n}\n\n/**\n * Who made an edit — which decides whether the document's subscribers hear about it.\n *\n * `editor` is the default and the historical behaviour: the editor that called\n * {@link EditorDocument.applyChange} already shows the edit, so echoing it back would be a\n * pointless round trip (and, for CodeMirror, a fight with its own transaction).\n *\n * `external` is an edit made by anything that is NOT the editor — the [[Tasks View]] ticking a\n * checkbox, for one. Those MUST notify, or an open editor keeps rendering text the buffer no\n * longer holds, and its next keystroke applies an offset against a document it disagrees with.\n */\nexport type ChangeOrigin = 'editor' | 'external'\n\n/**\n * A live handle to one document's text. The editor reads {@link getText} once to\n * seed itself, pushes local edits via {@link applyChange}, and is pushed *external*\n * changes (git reload, a remote CRDT update) via {@link subscribe}.\n */\nexport interface EditorDocument {\n readonly id: string\n getText(): string\n /** Apply an edit. Defaults to `editor` origin — see {@link ChangeOrigin}. */\n applyChange(change: TextChange, origin?: ChangeOrigin): void\n /**\n * Observe changes that did NOT originate from this editor's own\n * {@link applyChange} call — i.e. external/remote edits the editor must\n * reflect. Returns an unsubscribe function.\n */\n subscribe(listener: (text: string) => void): () => void\n}\n\n/** Opens documents by id. One store backs one knowledge graph. */\nexport interface DocumentStore {\n open(target: string): EditorDocument\n /**\n * Resolves when the document's backing content has actually loaded (e.g. a Server\n * Backend's cache seed). Absent on stores whose `open` returns real text synchronously;\n * the editor shows a loading state only while this is pending.\n */\n whenReady?(target: string): Promise<void>\n}\n","/**\n * The yield-and-cancel checkpoint (plan: 2026-07-27 Import Progress And Activities).\n *\n * A long synchronous loop starves the main thread: Svelte cannot repaint, so a status\n * line jumps from nothing straight to its final value, possibly via the browser's\n * \"page unresponsive\" prompt. `breathe` hands control back to the event loop.\n *\n * It yields on **elapsed time, not item count**. A graph of one-line journals and a graph\n * of 5000-line pages want wildly different batch sizes, and the thing we actually care\n * about is how long a frame has been blocked - so measure that directly.\n *\n * The yield point is also the natural cancel checkpoint, so cancellation rides along at\n * no extra cost (ADR 0035 §3): callers thread one `AbortSignal` and get both.\n */\n\n/** Longest a loop may hold the thread before yielding. ~3 frames: smooth enough to repaint, coarse enough not to dominate. */\nconst SLICE_MS = 50\n\n/** Per-call-site state, so two concurrent loops do not starve each other's budget. */\nexport interface Breather {\n (signal?: AbortSignal): Promise<void>\n}\n\n/**\n * Create a breather. Call `await breathe(signal)` inside a loop: it returns immediately\n * while the current slice has budget left, and yields when it does not. It throws the\n * signal's reason as soon as the signal aborts, whether or not it yields.\n */\nexport function createBreather(now: () => number = () => performance.now()): Breather {\n let lastYield = now()\n return async (signal?: AbortSignal) => {\n signal?.throwIfAborted()\n if (now() - lastYield < SLICE_MS) return\n // setTimeout (a macrotask) rather than queueMicrotask or await Promise.resolve():\n // a microtask does NOT let the browser paint, which is the whole point.\n await new Promise((resolve) => setTimeout(resolve))\n lastYield = now()\n signal?.throwIfAborted()\n }\n}\n","/**\n * Rewriting [[Wikilink]] targets across a document's source, for the rewrite arm of a page\n * rename (ADR 0037). Pure, so the tricky part - what NOT to touch - is unit-testable.\n *\n * Two things are deliberately left alone:\n *\n * 1. **Links inside code.** `wikilinkOccurrencesInSource` already drops them: a\n * `[[Physics]]` inside a fence is text the author wrote, not a reference.\n *\n * 2. **Nested occurrences.** `[[[[Physics]] Quantum]]` references the [[Scoped Concept]]\n * \"[[Physics]] Quantum\", which is a *different concept* from \"Physics\" and was not\n * renamed. Rewriting the inner scope would silently re-point the link at a scoped concept\n * that does not exist. Renaming a scope is a cascade over every concept beneath it, and\n * that is out of scope here - the top-level `[[Physics]]` links move, the scoped ones do\n * not, and the rename dialog's count reflects only what actually moves.\n */\n\nimport { wikilinkOccurrencesInSource } from './source'\n\nexport interface WikilinkRewrite {\n text: string\n /** How many occurrences were rewritten. */\n count: number\n}\n\n/** Absolute [start, end) offsets of an occurrence's inner text (between `[[` and `]]`). */\ninterface Target {\n from: number\n to: number\n}\n\n/**\n * Replace every top-level, non-code `[[from]]` in `source` with `[[to]]`. Matching is\n * case-insensitive on the trimmed concept, mirroring [[Concept]] identity.\n */\nexport function rewriteWikilinkTarget(source: string, from: string, to: string): WikilinkRewrite {\n const wanted = from.trim().toLowerCase()\n if (wanted === '') return { text: source, count: 0 }\n\n const occurrences = wikilinkOccurrencesInSource(source)\n if (occurrences.length === 0) return { text: source, count: 0 }\n\n // Absolute offsets per line, so nesting can be judged and edits applied to the whole source.\n const lineStarts: number[] = []\n let at = 0\n for (const line of source.split('\\n')) {\n lineStarts.push(at)\n at += line.length + 1\n }\n\n const spans = occurrences.map((o) => ({\n occurrence: o,\n start: lineStarts[o.line] + o.matchStart,\n end: lineStarts[o.line] + o.matchEnd,\n }))\n\n const targets: Target[] = []\n for (const span of spans) {\n if (span.occurrence.concept.trim().toLowerCase() !== wanted) continue\n // Nested inside another wikilink ⇒ it is a scope, not a reference to this concept.\n const nested = spans.some((other) => other !== span && other.start < span.start && other.end > span.end)\n if (nested) continue\n targets.push({ from: span.start + 2, to: span.end - 2 })\n }\n if (targets.length === 0) return { text: source, count: 0 }\n\n // Apply back-to-front so earlier offsets stay valid.\n targets.sort((a, b) => b.from - a.from)\n let text = source\n for (const target of targets) {\n text = text.slice(0, target.from) + to + text.slice(target.to)\n }\n return { text, count: targets.length }\n}\n\n/** Whether `source` references `concept` at the top level - the rename dialog's count. */\nexport function countWikilinkTargets(source: string, concept: string): number {\n return rewriteWikilinkTarget(source, concept, concept).count\n}\n\n/**\n * Replace every `[[from]]` with `[[to]]` **at any nesting depth** - the cascade rule\n * (ADR 0038 §2), and the deliberate inverse of {@link rewriteWikilinkTarget}.\n *\n * A [[Scoped Concept]]'s name *contains* its [[Scope]]'s name, so rewriting the inner link is\n * what renames the scope. `[[[[Physics]] Quantum]] Field Theory` is corrected through its\n * innermost occurrence and comes out right, which is why nothing here recurses and no depth\n * is special-cased.\n *\n * Used for two different things that turn out to be the same operation: rewriting a concept\n * STRING (`[[Physics]] Quantum` → `[[Physical Science]] Quantum`) and rewriting document\n * BODIES, where a scoped reference `[[[[Physics]] Quantum]]` picks up the new scope for free.\n *\n * Links inside code are still skipped - `wikilinkOccurrencesInSource` drops them.\n */\nexport function rewriteWikilinkScope(source: string, from: string, to: string): WikilinkRewrite {\n const splices = wikilinkScopeSplices(source, from, to)\n if (splices.length === 0) return { text: source, count: 0 }\n // Back-to-front so earlier offsets stay valid.\n let text = source\n for (let i = splices.length - 1; i >= 0; i--) {\n const splice = splices[i]\n text = text.slice(0, splice.from) + splice.insert + text.slice(splice.to)\n }\n return { text, count: splices.length }\n}\n\n/** One replacement inside a source: `[from, to)` in the ORIGINAL offsets, and what goes there. */\nexport interface TextSplice {\n from: number\n to: number\n insert: string\n}\n\n/**\n * The per-occurrence splices that {@link rewriteWikilinkScope} applies, in ascending order of\n * position and in the source's original offsets: every `[[from]]` at any depth becomes\n * `[[to]]`, links inside code left alone.\n *\n * Exposed because a rewrite across documents is applied as these splices, never as a\n * whole-text replace (ADR 0066): on a Server Backend each is a small CRDT delete plus insert\n * that merges with concurrent typing elsewhere in the document, and on a Filesystem Backend\n * each goes through the open buffer so the editor showing it is updated in place.\n *\n * Nested targets never overlap a matching OUTER target: an outer link whose concept is exactly\n * `from` cannot also contain a link with that same concept (a concept does not nest inside\n * itself), so the splices are disjoint and may be applied back-to-front without remapping.\n */\nexport function wikilinkScopeSplices(source: string, from: string, to: string): TextSplice[] {\n const wanted = from.trim().toLowerCase()\n if (wanted === '') return []\n\n const occurrences = wikilinkOccurrencesInSource(source)\n if (occurrences.length === 0) return []\n\n const lineStarts: number[] = []\n let at = 0\n for (const line of source.split('\\n')) {\n lineStarts.push(at)\n at += line.length + 1\n }\n\n const splices: TextSplice[] = []\n for (const occurrence of occurrences) {\n if (occurrence.concept.trim().toLowerCase() !== wanted) continue\n const start = lineStarts[occurrence.line] + occurrence.matchStart\n const end = lineStarts[occurrence.line] + occurrence.matchEnd\n splices.push({ from: start + 2, to: end - 2, insert: to })\n }\n return splices.sort((a, b) => a.from - b.from)\n}\n\n/**\n * True when `concept` is scoped - at any depth - by `scope`. This is what makes a document\n * part of a rename's cascade set.\n */\nexport function isScopedBy(concept: string, scope: string): boolean {\n return rewriteWikilinkScope(concept, scope, scope).count > 0\n}\n\n/**\n * The concepts a rename of `scope` drags with it: everything scoped by it, at any depth,\n * paired with the name each becomes.\n *\n * Concepts with no document behind them are included by the caller's input if it supplies\n * them - they need no rename, but recognising them keeps the impact count honest.\n */\nexport function cascadeFor(\n concepts: readonly string[],\n from: string,\n to: string,\n): { from: string; to: string }[] {\n const out: { from: string; to: string }[] = []\n for (const concept of concepts) {\n const rewritten = rewriteWikilinkScope(concept, from, to)\n if (rewritten.count > 0) out.push({ from: concept, to: rewritten.text })\n }\n return out\n}\n","/**\n * The `etherpk-cipher` fence as it appears in document *text* — finding one, reading what it holds\n * without the key, and producing the edits that write one.\n *\n * Nothing here decrypts. Every function is pure string and byte work over document lines, so the\n * editor, the storage layer, the index guard and the import converter can all ask the same\n * questions of the same source without holding a Protection Key between them.\n *\n * Fence pairing is delegated to {@link fencedBlocks} — the column-scoped rules every other fence\n * in the app already obeys (ADR 0018, ADR 0020). A cipher fence is not a second notion of \"in a\n * block\": an unterminated one is invisible here exactly as it is everywhere else.\n */\nimport {\n protectedEnvelopeFingerprint,\n protectedEnvelopeWrittenAt,\n resolveLastWriteWins,\n unarmourProtected,\n} from '$lib/crypto'\nimport { frontmatterSpan } from '$lib/storage/fs/frontmatter-span'\n\nimport { fenceLineInfo, fencedBlocks } from '../fenced-code'\nimport { CIPHER_FENCE_INFO } from './fence-info'\n\nexport { CIPHER_FENCE_INFO }\n\nconst DEFAULT_TICKS = '```'\n\nexport interface CipherFence {\n /** 0-based line index of the opening fence. */\n start: number\n /** 0-based line index of the closing fence. */\n end: number\n /**\n * Character offset of the opening fence's first character, and just past the closing fence's\n * last. Carried here so the editor's read-only guard, the widget placement and the service's\n * classification all measure the same way - two offset computations over one fence is how a\n * guard silently starts guarding the wrong range.\n */\n from: number\n to: number\n /** The column the fence and its body are clamped to. */\n fenceColumn: number\n /** Every envelope the body holds — more than one after two offline rewrites merged. */\n envelopes: Uint8Array[]\n /** The last-write-wins survivor, or null when the body holds nothing readable. */\n winner: Uint8Array | null\n /** The winner's Protection Key fingerprint — whose key this needs. */\n fingerprint: Uint8Array | null\n /** The winner's write timestamp. */\n writtenAt: number | null\n /**\n * The body holds losing envelopes that a key holder should collapse away. ADR 0028 discards\n * them; ADR 0059 records that under projected editing the loser can be your own work.\n */\n needsCollapse: boolean\n}\n\n/** Every complete cipher fence in `lines`, in document order. */\nexport function cipherFences(lines: readonly string[]): CipherFence[] {\n const out: CipherFence[] = []\n // Offset of each line's first character, so a line index becomes a character offset.\n const lineStarts: number[] = [0]\n for (let i = 0; i < lines.length; i++) lineStarts.push(lineStarts[i] + lines[i].length + 1)\n\n for (const block of fencedBlocks(lines)) {\n const opener = fenceLineInfo(lines[block.start])\n if (!opener || opener.info !== CIPHER_FENCE_INFO) continue\n const body = lines\n .slice(block.start + 1, block.end)\n .map((line) => (line.length >= block.fenceColumn ? line.slice(block.fenceColumn) : line.trimStart()))\n .join('\\n')\n const envelopes = unarmourProtected(body)\n const winner = resolveLastWriteWins(envelopes)\n out.push({\n start: block.start,\n end: block.end,\n from: lineStarts[block.start],\n // The closer's last character, NOT the newline after it — so an insertion on the line\n // below the fence is outside the range and stays editable.\n to: lineStarts[block.end] + lines[block.end].length,\n fenceColumn: block.fenceColumn,\n envelopes,\n winner,\n fingerprint: winner ? protectedEnvelopeFingerprint(winner) : null,\n writtenAt: winner ? protectedEnvelopeWrittenAt(winner) : null,\n needsCollapse: envelopes.length > 1,\n })\n }\n return out\n}\n\nexport type DocumentProtectionKind =\n /**\n * Not a Protected Document. Includes a document with a cipher fence somewhere in it that is\n * not the whole body: protection is whole-document only (ADR 0060), so such a fence is\n * ordinary markdown — a code block that happens to hold base64 — and means nothing here.\n */\n | 'none'\n /** The whole body is one cipher fence — a [[Protected Document]]. */\n | 'document'\n\nexport interface DocumentProtection {\n kind: DocumentProtectionKind\n fences: CipherFence[]\n /** The distinct Protection Key fingerprints the document's fences need. */\n fingerprints: Uint8Array[]\n}\n\n/**\n * Classify a document's text: a Protected Document, or not. Only a fence that is the entire body\n * below any Frontmatter counts (ADR 0060); `fences` still lists every fence found, because the\n * service's per-fence classification and the index's exclusion both want them.\n */\nexport function documentProtection(text: string): DocumentProtection {\n // Measured from the WHOLE document, not from below the Frontmatter. `cipherFences` and\n // `protectedRanges` both use whole-document line indices, and a second index base here would\n // silently mis-locate every fence in a document that has Frontmatter.\n const lines = text.split('\\n')\n const fences = cipherFences(lines)\n if (fences.length === 0) return { kind: 'none', fences, fingerprints: [] }\n\n const fingerprints: Uint8Array[] = []\n const seen = new Set<string>()\n for (const f of fences) {\n if (!f.fingerprint) continue\n const key = String(f.fingerprint)\n if (seen.has(key)) continue\n seen.add(key)\n fingerprints.push(f.fingerprint)\n }\n\n // Document-protected means the fence IS the body: one fence, opening on the first non-blank\n // line below any Frontmatter and closing on the last. Anything else is not protection.\n const only = fences.length === 1 ? fences[0] : null\n const bodyStart = bodyStartLine(text)\n const first = firstNonBlank(lines, bodyStart)\n const last = lastNonBlank(lines)\n const wholeBody = only !== null && only.start === first && only.end === last\n return { kind: wholeBody ? 'document' : 'none', fences, fingerprints }\n}\n\n/** The first line index below any Frontmatter — where a document's body begins. */\nfunction bodyStartLine(text: string): number {\n return frontmatterSpan(text)?.lines ?? 0\n}\n\nfunction firstNonBlank(lines: readonly string[], from: number): number {\n for (let i = from; i < lines.length; i++) if (lines[i].trim() !== '') return i\n return -1\n}\n\nfunction lastNonBlank(lines: readonly string[]): number {\n for (let i = lines.length - 1; i >= 0; i--) if (lines[i].trim() !== '') return i\n return -1\n}\n\n/**\n * A document's body — everything below any Frontmatter. Protection replaces exactly this, leaving\n * the Frontmatter in the clear so the concept name stays visible and wikilink-resolvable.\n */\nexport function documentBody(text: string): string {\n return bodyLines(text).lines.join('\\n')\n}\n\n/** A document split into its frontmatter prefix (kept verbatim) and its body lines. */\nfunction bodyLines(text: string): { prefix: string; lines: string[] } {\n const span = frontmatterSpan(text)\n if (!span) return { prefix: '', lines: text.split('\\n') }\n return { prefix: text.slice(0, span.end), lines: text.slice(span.end).split('\\n') }\n}\n\n/**\n * Turn a document into a [[Protected Document]]: the body becomes one fence holding `armoured`,\n * and any Frontmatter is kept in the clear above it so the concept name stays visible and\n * wikilink-resolvable (ADR 0057 / DESIGN.md).\n */\nexport function protectDocumentText(text: string, armoured: string): string {\n const { prefix } = bodyLines(text)\n return prefix + fenceLines(armoured, '', DEFAULT_TICKS).join('\\n')\n}\n\n/** The three lines of a fence: opener, one armoured body line, closer. */\nfunction fenceLines(armoured: string, indent: string, ticks: string): string[] {\n return [`${indent}${ticks}${CIPHER_FENCE_INFO}`, `${indent}${armoured}`, `${indent}${ticks}`]\n}\n\n/**\n * The Frontmatter a document carries, verbatim, closing delimiter and newline included — the part\n * of a [[Protected Document]] that stays in the clear. `''` when there is none.\n */\nexport function frontmatterPrefix(text: string): string {\n return bodyLines(text).prefix\n}\n\n/** Restore a [[Protected Document]]'s plaintext body, keeping its Frontmatter. */\nexport function unprotectDocumentText(text: string, plaintext: string): string {\n return bodyLines(text).prefix + plaintext\n}\n\nexport interface LineEdit {\n /** First line index replaced, inclusive. */\n fromLine: number\n /** Last line index replaced, inclusive. */\n toLine: number\n /** The replacement text — one delete plus one insert, which is what ADR 0028 requires. */\n text: string\n}\n\n/**\n * Replace a fence's **body** with one armoured envelope, leaving the opener and closer untouched.\n * This is the write that ADR 0028 demands be a single delete plus a single insert: character-level\n * CRDT edits inside ciphertext would let a stray character make the envelope undecryptable.\n */\nexport function replaceCipherFenceBody(fence: CipherFence, armoured: string): LineEdit {\n const pad = ' '.repeat(fence.fenceColumn)\n return { fromLine: fence.start + 1, toLine: fence.end - 1, text: pad + armoured }\n}\n\n\n","/**\n * [[Merge]] (CONTEXT.md): combining two [[Document]]s that have come to share one\n * [[Concept]].\n *\n * Pure, and shared on purpose. It arises in three places - renaming a page onto a name\n * already taken, a cascading scope rename that lands two scoped concepts on the same name\n * (ADR 0038 §4), and later [[Import|import-into-existing]], which must reuse this rather\n * than growing a second merge.\n *\n * Nothing is discarded. Bodies are joined and [[Alias]]es union, so every name either\n * document answered to still resolves and the result is something the user can tidy rather\n * than something they must reconstruct.\n */\n\nimport { conceptKey } from './fs/identity'\n\nexport interface MergeInput {\n /** The document that survives - its frontmatter (beyond aliases) is the one kept. */\n body: string\n aliases: string[]\n}\n\nexport interface MergeResult {\n body: string\n aliases: string[]\n}\n\n/**\n * Join the two documents.\n *\n * The bodies are separated by a blank line and **nothing else** - no marker saying a merge\n * happened (a deliberate choice: the result reads as one document). A blank line is not a\n * marker, it is what stops the last line of one body running into the first line of the\n * other and silently changing the markdown.\n *\n * Aliases union, deduped by [[Concept]] identity, survivor's order first.\n */\nexport function mergeDocuments(survivor: MergeInput, absorbed: MergeInput): MergeResult {\n const a = survivor.body.replace(/\\s+$/, '')\n const b = absorbed.body.replace(/^\\s+/, '')\n const body = a === '' ? b : b === '' ? a : `${a}\\n\\n${b}`\n\n const seen = new Set<string>()\n const aliases: string[] = []\n for (const alias of [...survivor.aliases, ...absorbed.aliases]) {\n const trimmed = alias.trim()\n if (trimmed === '') continue\n const key = conceptKey(trimmed)\n if (seen.has(key)) continue\n seen.add(key)\n aliases.push(trimmed)\n }\n\n return { body, aliases }\n}\n","/**\n * The shared vocabulary of a [[Rename]] (ADR 0037, 0038, 0064), used by both\n * [[Storage Backend]]s.\n *\n * A rename acts on the [[Concept]]; a page is a passenger. The decision the two options\n * encode: renaming a page leaves every `[[Old Name]]` in the graph pointing at a concept with\n * no page behind it, and neither fix is right often enough to impose. Aliasing touches one\n * document and honours \"the source is always canonical\"; rewriting makes the prose read\n * correctly but is N concurrent document edits with no transaction around them on a synced\n * graph. A [[Pageless Concept]] has nothing to alias, so for it there is no choice: renaming\n * one IS the rewrite (ADR 0064).\n */\n\nimport { isJournalConcept } from '$lib/document/journal-concept'\n\n/** How the rename deals with existing inbound links. */\nexport type RenameLinkStrategy =\n /** Add the old name to the page's [[Alias]]es; every existing link keeps resolving. */\n | 'alias'\n /** Rewrite top-level `[[old]]` occurrences across the graph to the new name. */\n | 'rewrite'\n\nexport interface RenameOptions {\n strategy: RenameLinkStrategy\n}\n\nexport interface RenameResult {\n /** The page's new concept. */\n concept: string\n /** Documents whose text was rewritten (`0` for the alias strategy). */\n rewritten: number\n /** Scoped concepts dragged along by the cascade. */\n cascaded: number\n /** Steps that landed on a taken name and so merged. */\n merged: number\n}\n\n/**\n * Why a rename was refused, as a user-facing message, or null when it may proceed.\n * Shared so both backends refuse identically.\n *\n * A [[Journal Entry]] is refused because its concept *is* its date and there is exactly one\n * per day; renaming it would break that invariant rather than express anything. A\n * journal-shaped [[Pageless Concept]] (a day nobody has written yet) is refused for the same\n * reason: its name is its day whether or not the entry exists.\n *\n * A collision is NOT a refusal (ADR 0038 §4): it is a [[Merge]], confirmed in the dialog.\n */\nexport function renameRefusal(options: {\n from: string\n to: string\n /** The document's kind, or null when the concept has no document (ADR 0064). */\n kind: 'journal' | 'page' | null\n}): string | null {\n const to = options.to.trim()\n if (to === '') return 'A page needs a non-empty name.'\n if (options.kind === 'journal' || (options.kind === null && isJournalConcept(options.from))) {\n return 'A journal entry cannot be renamed - its name is its date, and there is exactly one per day.'\n }\n return null\n}\n\n/** One concept the rename moves: its current name and the name it takes. */\nexport interface RenameStep {\n from: string\n to: string\n /**\n * Whether `from` has a document. A step without one - a [[Pageless Concept]], directly\n * or in the cascade - moves only its links; there is no identity to write (ADR 0064).\n */\n hasDocument: boolean\n /**\n * True when `to` is already held by a different document AND `from` has one, so this\n * step is a [[Merge]] rather than a plain rename. A Merge joins two documents; with one\n * there is nothing to join.\n */\n merges: boolean\n /**\n * True when `from` has no document and `to` is held by one: the links are simply\n * redirected to the page that already has the name. Nothing is at risk, which is why the\n * dialog reports it neutrally rather than as a Merge (ADR 0064 §2).\n */\n redirects: boolean\n /**\n * The concept of the document this step ends up in. Equal to `to` unless `to` is another\n * document's [[Alias]]: a name is taken by whatever document answers to it, by title or by\n * alias, and a Merge joins into that document under ITS title - the alias already resolves\n * there, so nothing else needs to change for the asked-for name to work (ADR 0038 §4).\n */\n into: string\n}\n\n/**\n * Everything a rename will do, computed before anything is written (ADR 0038 §1).\n *\n * The dialog renders this so the user confirms the real blast radius - renaming one page and\n * silently retitling twelve others is precisely the surprise the informed-consent principle\n * exists to prevent - and the appliers consume the same plan, so preview and effect cannot\n * drift apart.\n */\nexport interface RenamePlan {\n /** The concept the user actually named. */\n direct: RenameStep\n /**\n * Scoped concepts dragged along, at any depth. Ordered deepest-first so a document is\n * never renamed onto a name a later step is about to vacate.\n */\n cascade: RenameStep[]\n /** Documents whose body text references the old name (the rewrite arm's cost). */\n referencingDocuments: number\n /** Refusal message, or null. When set, nothing else here should be acted on. */\n refusal: string | null\n}\n\n/** Every step, direct first - what an applier iterates. */\nexport function renameSteps(plan: RenamePlan): RenameStep[] {\n return [plan.direct, ...plan.cascade]\n}\n\n/** How many steps merge - what the dialog warns about. */\nexport function mergeCount(plan: RenamePlan): number {\n return renameSteps(plan).filter((s) => s.merges).length\n}\n","/**\n * Building a {@link RenamePlan} from the graph's concepts - pure, so the whole cascade is\n * unit-testable without either backend.\n *\n * Both stores compute their plan here and then apply it, so the impact the dialog shows and\n * the work that actually happens come from one source.\n */\n\nimport { cascadeFor } from '$lib/document/wikilink/rename'\nimport { conceptKey } from './fs/identity'\nimport { type RenamePlan, type RenameStep, renameRefusal, renameSteps } from './rename'\n\nexport interface PlanRenameInput {\n from: string\n to: string\n /** Every concept in the graph that has a document behind it. */\n concepts: readonly string[]\n /** Every alias in the graph, with the concept of the document it names. */\n aliases?: readonly { name: string; concept: string }[]\n /** The kind of the document being renamed, or null when the concept has none (ADR 0064). */\n kind: 'journal' | 'page' | null\n /** Documents whose BODY references `from` at the top level. */\n referencingDocuments: number\n}\n\nexport function planRename(input: PlanRenameInput): RenamePlan {\n const from = input.from.trim()\n const to = input.to.trim()\n\n const refusal = renameRefusal({ from, to, kind: input.kind })\n const hasDocument = input.kind !== null\n const direct: RenameStep = { from, to, hasDocument, merges: false, redirects: false, into: to }\n if (refusal) {\n return { direct, cascade: [], referencingDocuments: input.referencingDocuments, refusal }\n }\n\n // Which document answers to a name - by title, or by alias - so a step can tell whether\n // its target is already taken, and by whom.\n const existing = new Map(input.concepts.map((c) => [conceptKey(c), c]))\n for (const alias of input.aliases ?? []) {\n if (!existing.has(conceptKey(alias.name))) existing.set(conceptKey(alias.name), alias.concept)\n }\n\n const cascade = cascadeFor(input.concepts, from, to)\n // Deepest first: a shallower concept must never be renamed onto a name that a deeper\n // step is still about to vacate.\n .sort((a, b) => depthOf(b.from) - depthOf(a.from))\n // The cascade is computed over documented concepts, so every step here has one.\n .map<RenameStep>((step) => stepFor(step.from, step.to, true, existing))\n\n return {\n direct: stepFor(from, to, hasDocument, existing),\n cascade,\n referencingDocuments: input.referencingDocuments,\n refusal: null,\n }\n}\n\n/**\n * A [[Protected Document]] never merges (ADR 0062): not into another document, and nothing into\n * it. Joining bodies would put a cipher fence beside readable text - which is \"not the entire\n * body\", so the result is no longer protected, shows the ciphertext as an ordinary code block,\n * and on a Filesystem Backend has just written the other document's plaintext into a file that\n * used to hold only ciphertext. Refused, not worked around: there is no join that keeps both\n * documents' guarantees.\n *\n * Both stores call this after planning, with their own way of reading a concept's stored text.\n * Only the endpoints of merging steps are asked about, so a plan with no collision - the common\n * case, and the one previewed on every keystroke in the dialog - reads nothing.\n */\nexport async function refuseProtectedMerges(\n plan: RenamePlan,\n isProtected: (concept: string) => Promise<boolean> | boolean,\n): Promise<RenamePlan> {\n if (plan.refusal) return plan\n for (const step of renameSteps(plan)) {\n if (!step.merges) continue\n if (await isProtected(step.into)) {\n return {\n ...plan,\n refusal: `“${step.into}” is a protected document, so nothing can be merged into it. Choose a different name.`,\n }\n }\n if (await isProtected(step.from)) {\n return {\n ...plan,\n refusal: `“${step.from}” is a protected document, so it cannot be merged into “${step.to}”. Choose a name that is not already taken.`,\n }\n }\n }\n return plan\n}\n\n/**\n * A step merges when a DIFFERENT document already answers to the target name - by title or by\n * alias - and the source has a document to join to it. Renaming onto yourself (a pure\n * re-casing, or onto one of your own aliases) is neither. A source with no document landing\n * on a taken name redirects: its links come to point at the page that already answers to it\n * (ADR 0064 §2). `into` is that page's own title.\n */\nfunction stepFor(from: string, to: string, hasDocument: boolean, existing: Map<string, string>): RenameStep {\n const holder = existing.get(conceptKey(to))\n const taken = holder !== undefined && conceptKey(holder) !== conceptKey(from)\n return {\n from,\n to,\n hasDocument,\n merges: taken && hasDocument,\n redirects: taken && !hasDocument,\n into: taken ? holder : to,\n }\n}\n\n/** Nesting depth of a concept - `[[[[A]] B]] C` is deeper than `[[A]] B`. */\nfunction depthOf(concept: string): number {\n let depth = 0\n let max = 0\n for (let i = 0; i < concept.length - 1; i++) {\n if (concept[i] === '[' && concept[i + 1] === '[') {\n depth += 1\n max = Math.max(max, depth)\n i += 1\n } else if (concept[i] === ']' && concept[i + 1] === ']') {\n depth -= 1\n i += 1\n }\n }\n return max\n}\n","/**\n * ServerDocumentStore (plan Phase 3 Task 6): the DocumentStore seam over the E2EE sync\n * engine. Mirrors FilesystemDocumentStore's surface so every workspace consumer (tree,\n * backlink index, journal, DocumentView) is unchanged — but text lives in a Yjs Y.Text\n * per document, and identity/title/date live ONLY in the encrypted graph-root registry\n * (ADR 0024), never on the server as plaintext.\n */\nimport * as Y from 'yjs'\nimport type { Awareness } from 'y-protocols/awareness'\nimport { isJournalConcept } from '$lib/document/journal-concept'\nimport { normaliseAliases, withFrontmatterIdentity } from '$lib/document/frontmatter/identity'\nimport { parseFrontmatter } from '$lib/storage/fs/frontmatter'\nimport { frontmatterSpan } from '$lib/storage/fs/frontmatter-span'\nimport { conceptKey } from '$lib/storage/fs/identity'\nimport type { DocumentEntry } from '$lib/storage/fs/scan'\nimport {\n type ChangeOrigin,\n DocumentNotFoundError,\n DocumentSyncDegradedError,\n type EditorDocument,\n type TextChange,\n} from '$lib/document/types'\nimport type { IndexDocSnapshot } from '$lib/storage/fs/filesystem-store'\nimport type {\n IndexChangeCheckpoint,\n IndexSnapshotOptions,\n StoreChangeListener,\n} from '$lib/document/backlinks/live-index'\nimport { createBreather } from '$lib/activity/breathe'\nimport type { GraphSync, RegistryEntry } from '$lib/sync/graph-sync'\nimport { REMOTE, SUPPRESSED, type SyncHealth } from '$lib/sync/doc-sync'\nimport { type TextSplice, countWikilinkTargets, wikilinkScopeSplices } from '$lib/document/wikilink/rename'\nimport { documentProtection } from '$lib/document/protection/cipher-fence'\nimport { mergeDocuments } from '../merge'\nimport { planRename, refuseProtectedMerges } from '../rename-plan'\nimport { type RenameOptions, type RenamePlan, type RenameResult, mergeCount, renameSteps } from '../rename'\n\nexport interface ServerDocumentStoreOptions {\n /** Bounded wait (ms) for an initial relay connection in scan(). */\n readyTimeoutMs?: number\n /**\n * A document deleted elsewhere was brought back by a local edit (ADR 0039 §4). The\n * workspace tells the editor, because a document silently returning is inexplicable to\n * whoever deleted it.\n */\n onResurrected?: (concept: string) => void\n}\n\n/**\n * One document's identity keyed by the id that survives a rename. The [[Local Mirror]] needs the\n * id rather than the concept: a concurrent creation race can leave two registry entries with the\n * same title or date, and a concept alone can neither tell them apart nor keep their files stable.\n */\nexport interface DocumentIdentity {\n docId: string\n kind: RegistryEntry['kind']\n concept: string\n aliases: string[]\n}\n\n/** A document's full text, and whether it can be trusted as the document's settled content. */\nexport interface DocumentText {\n text: string\n /**\n * False when the text may not be the document's real content - its history has not arrived,\n * its key is missing, or its ciphertext will not open. A consumer that writes somewhere\n * durable must skip it rather than record an empty document.\n */\n settled: boolean\n}\n\n/** Structural mirror of FilesystemDocumentStore (documented in document/types.ts). */\nexport interface ServerDocumentStore {\n open(target: string): EditorDocument\n /**\n * Resolves when a document has seeded from the Local Cache and, for a cold empty seed,\n * applied its first relay page. This is enough to present content; full historical\n * reconciliation continues independently to its terminal page.\n */\n whenReady(target: string): Promise<void>\n /** The collab buffer for a target — DocumentView mounts y-codemirror over it. */\n getYText(target: string): Y.Text | undefined\n /** The per-doc presence handle for a target — remote cursors (ADR 0024). */\n getAwareness(target: string): Awareness | undefined\n /** Retain relay content and presence while a real view consumes this document. */\n retainDocument(target: string): () => void\n scan(): Promise<void>\n listDocuments(): DocumentEntry[]\n /** Every document's identity, keyed by document id ({@link DocumentIdentity}). */\n listIdentities(): DocumentIdentity[]\n /**\n * Full materialized text for documents nobody has open, in bounded batches seeded from the\n * [[Local Cache]] and retired again - never through {@link open}, which starts a live editing\n * engine per document and keeps it for the session.\n *\n * An empty seed is a cache miss as often as it is an empty document, so those documents (and\n * only those) get a bounded relay catch-up before their emptiness is believed. Whatever is\n * still unconfirmed comes back with `settled: false`.\n */\n readTexts(\n docIds: readonly string[],\n options?: { timeoutMs?: number; onProgress?: (done: number, total: number) => void },\n ): Promise<Map<string, DocumentText>>\n /**\n * Resolves true once the encrypted registry has been confirmed against the server this\n * session. Anything that deletes on the strength of \"not in the registry\" has to wait for\n * this: offline, the registry can be a partial cached one.\n */\n confirmRegistry(timeoutMs?: number): Promise<boolean>\n onDocumentsChanged(listener: () => void): () => void\n onDocumentRemoved(listener: (target: string) => void): () => void\n onChange(listener: StoreChangeListener): () => void\n snapshotForIndex(): Promise<IndexDocSnapshot[]>\n streamForIndex(options?: IndexSnapshotOptions): Promise<{\n total: number\n batches: AsyncIterable<readonly IndexDocSnapshot[]>\n }>\n /** Start bounded relay catch-up for every indexed document after a warm reopen. */\n catchUpPersistedIndex(): Promise<void>\n /** Durable Local Cache boundaries not yet represented by a committed index operation. */\n pendingIndexChanges(): Promise<IndexChangeCheckpoint>\n /** One document's index snapshot, for a listener that knows which one changed. */\n snapshotDocument(concept: string): IndexDocSnapshot | null\n reconcile(): Promise<void>\n resolveConflict(target: string, choice: 'keep-mine' | 'take-disk'): Promise<void>\n /**\n * Create the [[Journal Entry]] for `date` (a `YYYY-MM-DD` day), seeded with `body`.\n * The day is the identity here too - it is the registry entry, not a title (ADR 0056).\n */\n createJournal(date: string, body?: string): Promise<string>\n /** `body` seeds the new document's content, so a promoting [[Draft]] needs no second write (ADR 0050). */\n createPage(title: string, body?: string): Promise<string>\n /** What a rename would do, before anything is written (ADR 0038 §1) — the dialog's preview. */\n planRename(from: string, to: string, referencingDocuments?: number): Promise<RenamePlan>\n /** Apply a rename: the document, its scoped concepts, and any merges (ADR 0037, 0038). */\n renamePage(from: string, to: string, options: RenameOptions): Promise<RenameResult>\n /** Delete a document. Immediate and irreversible (ADR 0039). */\n deleteDocument(concept: string): Promise<void>\n /**\n * Drop deleted content from a document's Local Cache row. After protecting a document the\n * row otherwise keeps the pre-protection body for the document's lifetime, readable from\n * the profile directory with no key. The live document collects on its own (the write\n * runs under an untracked origin); the row is what has to be told.\n */\n compactDocument(target: string): Promise<void>\n /**\n * Upload a consolidated encrypted [[Snapshot]] of a document to the relay now, instead of\n * waiting for the idle trigger, and read it back to verify it (ADR 0025, amended). Nothing\n * in the app calls this on a timer; it is the manual and e2e handle on compaction.\n */\n uploadSnapshot(target: string): Promise<void>\n /**\n * Set a document's aliases (ADR 0061): the registry entry, then the block if the text carries\n * one. Normalised: trimmed, unique, never the document's own name.\n */\n setAliases(target: string, aliases: readonly string[]): Promise<void>\n dispose(): Promise<void>\n}\n\nfunction conceptOf(entry: RegistryEntry): string {\n return entry.kind === 'journal' ? (entry.date ?? '') : (entry.title ?? '')\n}\n\n/**\n * Whether `promise` settled within `timeoutMs`. A sync milestone that never arrives (offline, a\n * silent server) is a bounded wait and a `false`, never a hang and never an unhandled rejection:\n * the caller decides what an unreached milestone means.\n */\nasync function reachedWithin(promise: Promise<unknown>, timeoutMs: number): Promise<boolean> {\n let timer: ReturnType<typeof setTimeout> | undefined\n try {\n return await Promise.race([\n promise.then(\n () => true,\n () => false,\n ),\n new Promise<boolean>((resolve) => {\n timer = setTimeout(() => resolve(false), timeoutMs)\n }),\n ])\n } finally {\n if (timer) clearTimeout(timer)\n }\n}\n\n/** Health that says the bytes are not this document's, so neither text nor emptiness is real. */\nfunction contentBlocked(health: SyncHealth): boolean {\n return health === 'key-unavailable' || health === 'ciphertext-corrupt'\n}\n\nexport function createServerDocumentStore(\n graph: GraphSync,\n options: ServerDocumentStoreOptions,\n): ServerDocumentStore {\n const registry = graph.registry()\n /** Engines created between yields while snapshotting — small enough to keep frames free. */\n const ENGINE_BATCH = 100\n /** Longest a cold-empty document holds its loading state waiting for first catchup. */\n const COLD_CONTENT_TIMEOUT_MS = 15000\n const breathe = createBreather()\n\n const changeListeners = new Set<StoreChangeListener>()\n const docsChangedListeners = new Set<() => void>()\n const removedListeners = new Set<(target: string) => void>()\n const resurrectionClaims = new Map<string, RegistryEntry>()\n let disposed = false\n /** Whether the encrypted registry has been read to its terminal relay page this session. */\n let registryConfirmed = false\n\n interface RegistryIdentity {\n kind: RegistryEntry['kind']\n concept: string\n aliases: string[]\n }\n\n function currentRegistryIdentities(): Map<string, RegistryIdentity> {\n const out = new Map<string, RegistryIdentity>()\n registry.forEach((entry, docId) => {\n out.set(docId, {\n kind: entry.kind,\n concept: conceptOf(entry),\n // Alias order has no meaning in lookup or in the derived index. Normalising\n // here prevents a CRDT replay with reordered aliases from becoming an\n // unnamed full-index replacement.\n aliases: [...(entry.aliases ?? [])].sort((left, right) =>\n left.localeCompare(right),\n ),\n })\n })\n return out\n }\n\n function sameIdentity(\n before: RegistryIdentity | undefined,\n after: RegistryIdentity | undefined,\n ): boolean {\n if (!before || !after) return before === after\n return (\n before.kind === after.kind &&\n before.concept === after.concept &&\n before.aliases.length === after.aliases.length &&\n before.aliases.every((alias, index) => alias === after.aliases[index])\n )\n }\n\n // key → concept, so a removal can report the CONCEPT (what a View is keyed by), not the\n // lower-cased key. The filesystem store reports the concept; consumers should not have to\n // know which backend they are talking to.\n let known = currentConcepts()\n let docIdsByConcept = currentDocIds()\n let knownRegistry = currentRegistryIdentities()\n\n function currentConcepts(): Map<string, string> {\n const out = new Map<string, string>()\n registry.forEach((entry) => {\n const concept = conceptOf(entry)\n out.set(conceptKey(concept), concept)\n })\n return out\n }\n\n function currentDocIds(): Map<string, string> {\n const out = new Map<string, string>()\n registry.forEach((entry, docId) => {\n out.set(conceptKey(conceptOf(entry)), docId)\n for (const alias of entry.aliases ?? []) out.set(conceptKey(alias), docId)\n })\n return out\n }\n\n // Registry membership → onDocumentsChanged (+ removals). A pure addition names the\n // exact concept so the index can ingest one document. Deletes, renames and mixed\n // transactions still require a replacement because they can remove several identities.\n const unobserveRegistry = (() => {\n const handler = (event: Y.YMapEvent<RegistryEntry>) => {\n const nextRegistry = currentRegistryIdentities()\n const changedDocIds = [...new Set([...knownRegistry.keys(), ...nextRegistry.keys()])]\n .filter((docId) => !sameIdentity(knownRegistry.get(docId), nextRegistry.get(docId)))\n // Yjs can replay a different CRDT struct which resolves to the same JSON value.\n // On graph reopen that is restored state, not a rename or membership change.\n // Emitting an unnamed index change here made every new tab walk the full graph.\n if (changedDocIds.length === 0) return\n\n const additions: string[] = []\n let additionsOnly = true\n for (const docId of changedDocIds) {\n const before = knownRegistry.get(docId)\n const after = nextRegistry.get(docId)\n if (before || !after) {\n additionsOnly = false\n continue\n }\n additions.push(after.concept)\n }\n const next = currentConcepts()\n for (const [key, concept] of known) {\n if (!next.has(key)) removedListeners.forEach((l) => l(concept))\n }\n known = next\n docIdsByConcept = currentDocIds()\n knownRegistry = nextRegistry\n docsChangedListeners.forEach((l) => l())\n if (additionsOnly) {\n for (const concept of additions) {\n changeListeners.forEach((listener) => listener({ concept }))\n }\n } else {\n changeListeners.forEach((listener) => listener())\n }\n for (const [docId, change] of event.changes.keys) {\n if (change.action !== 'delete') continue\n const claimed = resurrectionClaims.get(docId)\n if (!claimed) continue\n queueMicrotask(() => {\n if (registry.has(docId)) return\n registry.set(docId, claimed)\n resurrectionClaims.delete(docId)\n options.onResurrected?.(conceptOf(claimed))\n })\n }\n }\n registry.observe(handler)\n return () => registry.unobserve(handler)\n })()\n\n // Document *content* changes (Y.Text edits, local or remote) are also onChange — this is\n // what keeps the derived index and the Local Mirror current on edits, not just on\n // add/remove. (The registry itself is one of these docs; its handler above still runs.)\n //\n // The changed document is NAMED. Discarding it here is what made one keystroke mean\n // \"everything changed\", and cost a full re-index of the graph (~3.9s at 2431 documents).\n // A listener that can act on one document should; one that cannot ignores the argument.\n const unobserveDocUpdates = graph.onDocUpdate((docId) => {\n // The root document contains registry and graph metadata. Its registry observer above\n // has better information, while metadata does not affect the document index at all.\n // Forwarding the root update as unnamed turned every page creation into a full rebuild.\n if (graph.docSync(docId).doc === graph.rootDoc) return\n const entry = entryFor(docId)\n const change = entry ? { concept: conceptOf(entry) } : undefined\n changeListeners.forEach((l) => l(change))\n })\n\n /** Find the registry docId whose concept matches `target` (case-insensitive). */\n function docIdFor(target: string): string | undefined {\n return docIdsByConcept.get(conceptKey(target))\n }\n\n /**\n * What the [[Derived Index]] derives over: the body below any [[Frontmatter]], as a Filesystem\n * Backend hands it (ADR 0061). The block is opaque to the index everywhere, so a wikilink\n * written up there makes no backlink on either backend - and line numbers it records stay\n * body-relative, which is what the reveal offset expects.\n */\n function bodyForIndex(text: string): string {\n return parseFrontmatter(text).body\n }\n\n /**\n * Bring a block the text carries in line with the registry (ADR 0061). Part of the action that\n * changed the registry - a rename, an alias change - and never a reaction to one arriving: if\n * every member's device rewrote the block on seeing the registry move, the CRDT would merge N\n * identical concurrent edits into duplicated text. Adds no block to a document without one. A\n * journal's title is its date, so only its aliases are mirrored.\n */\n function writeBackIdentity(docId: string): void {\n const entry = registry.get(docId)\n if (!entry) return\n const ytext = graph.docSync(docId).doc.getText('content')\n const text = ytext.toString()\n const next = withFrontmatterIdentity(text, {\n ...(entry.kind === 'page' ? { title: conceptOf(entry) } : {}),\n aliases: entry.aliases ?? [],\n })\n if (next === text) return\n // Only the block is replaced: the body is verbatim in `next`, and a whole-document rewrite\n // would be a whole-document edit for every other member to receive.\n const before = frontmatterSpan(text)?.end ?? 0\n const after = frontmatterSpan(next)?.end ?? 0\n ytext.doc!.transact(() => {\n if (before > 0) ytext.delete(0, before)\n ytext.insert(0, next.slice(0, after))\n }, STORE)\n }\n\n function entryFor(docId: string): RegistryEntry | undefined {\n return registry.get(docId)\n }\n\n /**\n * An edit outranks a delete (ADR 0039 §4), watched on the Y.Text rather than on\n * `applyChange`.\n *\n * On this backend the editor binds the Y.Text **directly** (yCollab), so `applyChange` is\n * never called for a Server-backed document - the resurrection check living only there\n * made the rule unreachable in the app while passing its unit tests. Reading the local\n * edit off the CRDT catches every way a document can be typed into.\n *\n * `registry` lives in the ROOT document and the Y.Text in the document's own, so writing\n * one from the other's observer is not re-entrant.\n */\n const resurrectors = new Map<string, () => void>()\n\n /**\n * The origin the store stamps on its OWN structural writes, so they are never mistaken for\n * a user's edit. Without it the deleter resurrected its own delete: `deleteDocument` drops\n * the registry entry and then clears the Y.Text, and that clear is a local content change\n * to a document with no entry - which is precisely the shape of a resurrection.\n */\n const STORE = Symbol('etherpk-store-write')\n\n function watchForResurrection(docId: string, openedAs: RegistryEntry | undefined): void {\n if (!openedAs || resurrectors.has(docId)) return\n const ytext = graph.docSync(docId).doc.getText('content')\n const handler = (_e: Y.YTextEvent, tx: Y.Transaction) => {\n if (tx.origin === REMOTE) return // someone else's edit asserts nothing on our behalf\n if (tx.origin === STORE) return // our own delete/merge, not a user asserting anything\n if (tx.origin === SUPPRESSED) return // ordered lifecycle clear, not an edit\n const lifecycle = graph.docSync(docId).lifecycle()\n if (lifecycle === 'deleted' || lifecycle === 'resurrecting') {\n resurrectionClaims.set(docId, openedAs)\n }\n if (registry.has(docId)) return\n registry.set(docId, openedAs)\n resurrectionClaims.delete(docId)\n options.onResurrected?.(conceptOf(openedAs))\n }\n ytext.observe(handler)\n resurrectors.set(docId, () => ytext.unobserve(handler))\n }\n\n return {\n open(target: string): EditorDocument {\n const docId = docIdFor(target)\n if (!docId) throw new DocumentNotFoundError(target)\n const ytext = graph.docSync(docId).doc.getText('content')\n // The entry as it was when opened, so a resurrection restores THIS document -\n // a journal comes back a journal, keeping its aliases - rather than a bare page.\n const openedAs = registry.get(docId)\n watchForResurrection(docId, openedAs)\n return {\n id: target,\n getText: () => ytext.toString(),\n applyChange(change: TextChange, origin: ChangeOrigin = 'editor') {\n // An edit outranks a delete (ADR 0039 §4): if the entry has gone while\n // this document was open, typing in it asserts that it should exist.\n if (openedAs && !registry.has(docId)) {\n registry.set(docId, openedAs)\n resurrectionClaims.delete(docId)\n options.onResurrected?.(conceptOf(openedAs))\n }\n // Local edit → mutate the Y.Text (NOT REMOTE) so the engine sees it as\n // local and syncs it. Never notifies our subscribers. An EXTERNAL write -\n // protecting the document is one - goes under the store's own origin, which\n // the collaborative editor's undo manager does not track: under the default\n // origin it would pin the deleted plaintext against garbage collection, and\n // every state this document is ever serialised to would carry the body it\n // was protected to hide.\n ytext.doc!.transact(\n () => {\n if (change.to > change.from) ytext.delete(change.from, change.to - change.from)\n if (change.insert) ytext.insert(change.from, change.insert)\n },\n origin === 'external' ? STORE : undefined,\n )\n },\n subscribe(listener: (text: string) => void) {\n // External/remote edits only: fire on REMOTE-origin changes.\n const handler = (_e: Y.YTextEvent, tx: Y.Transaction) => {\n if (tx.origin === REMOTE) listener(ytext.toString())\n }\n ytext.observe(handler)\n return () => ytext.unobserve(handler)\n },\n }\n },\n getYText(target: string): Y.Text | undefined {\n const docId = docIdFor(target)\n return docId ? graph.docSync(docId).doc.getText('content') : undefined\n },\n async whenReady(target: string): Promise<void> {\n const docId = docIdFor(target)\n if (!docId) return\n await graph.whenReady(docId)\n // A cold cache seeds EMPTY — a miss, not content — while the real text is\n // still in flight over the wire. Resolving here dropped the loading overlay\n // onto 4.5s of blank editor (measured live, 2400-doc cold open). Hold until\n // the first catch-up page has applied; capped so an offline or silent server\n // leaves an editable empty document rather than an eternal spinner.\n if (graph.docSync(docId).doc.getText('content').length === 0 && graph.isConnected()) {\n await Promise.race([\n graph.firstCatchupPageDoc(docId),\n new Promise((resolve) => setTimeout(resolve, COLD_CONTENT_TIMEOUT_MS)),\n ])\n if (graph.docSync(docId).doc.getText('content').length === 0) {\n // Caught up (or timed out) and STILL empty: either a genuinely empty\n // document, or its history never arrived / never decrypted — the\n // doc-sync skip counter warning says which. Name it either way.\n console.warn(`[sync] \"${target}\" has no content after catchup — empty doc, or its history was skipped`)\n }\n }\n const health = graph.docSync(docId).health()\n // Only states that genuinely block content fail the open. A sequence gap or a\n // stale generation is a routine self-healing moment. The engine has already\n // requested catch-up when it sets them, and surfacing it as \"opened without\n // its cached content\" put a false failure banner on every open document while\n // busy peers kept the relay head moving (live, 2026-07-30).\n if (health === 'key-unavailable' || health === 'ciphertext-corrupt') {\n throw new DocumentSyncDegradedError(target, health)\n }\n },\n getAwareness(target: string): Awareness | undefined {\n const docId = docIdFor(target)\n return docId ? graph.docSync(docId).awareness : undefined\n },\n retainDocument(target: string): () => void {\n const docId = docIdFor(target)\n return docId ? graph.retainDoc(docId) : () => {}\n },\n async scan(): Promise<void> {\n // Seed the cached registry first. A cold offline open remains bounded, but once\n // connected we must consume the ROOT doc's terminal relay page before anything\n // identity-sensitive reads it. The first page can be a partial registry.\n await graph.ready()\n const reachedRelay =\n graph.isConnected() ||\n (await new Promise<boolean>((resolve) => {\n let settled = false\n const finish = (connected: boolean) => {\n if (settled) return\n settled = true\n clearTimeout(timer)\n resolve(connected)\n }\n const timer = setTimeout(\n () => finish(false),\n Math.max(0, options.readyTimeoutMs ?? 2000),\n )\n void graph.connected().then(() => finish(true))\n }))\n if (reachedRelay) {\n await graph.rootCaughtUp()\n registryConfirmed = true\n }\n known = currentConcepts()\n docIdsByConcept = currentDocIds()\n knownRegistry = currentRegistryIdentities()\n },\n listDocuments(): DocumentEntry[] {\n const journals: DocumentEntry[] = []\n const pages: DocumentEntry[] = []\n registry.forEach((entry, docId) => {\n const concept = conceptOf(entry)\n const doc: DocumentEntry = {\n kind: entry.kind,\n concept,\n key: conceptKey(concept),\n // Server docs are not files; fabricate fs-shaped fields the tree ignores.\n subdir: entry.kind === 'journal' ? 'journals' : 'pages',\n fileName: docId,\n aliases: entry.aliases ?? [],\n lastModified: 0,\n // No file behind a server document; the Filesystem Backend's (mtime, size) fast path\n // needs the field on every DocumentEntry, and zero is as honest as the mtime above.\n size: 0,\n }\n ;(entry.kind === 'journal' ? journals : pages).push(doc)\n })\n journals.sort((a, b) => b.concept.localeCompare(a.concept)) // date-desc\n pages.sort((a, b) => a.concept.localeCompare(b.concept)) // alpha\n return [...journals, ...pages]\n },\n listIdentities(): DocumentIdentity[] {\n const out: DocumentIdentity[] = []\n registry.forEach((entry, docId) => {\n out.push({\n docId,\n kind: entry.kind,\n concept: conceptOf(entry),\n aliases: [...(entry.aliases ?? [])],\n })\n })\n return out\n },\n async readTexts(docIds, readOptions): Promise<Map<string, DocumentText>> {\n const out = new Map<string, DocumentText>()\n const timeoutMs = readOptions?.timeoutMs ?? COLD_CONTENT_TIMEOUT_MS\n const empty: string[] = []\n // Reported against the whole request, not the batch: a caller showing this to a\n // person is answering \"how much of my graph is read\", not \"how far into batch 14\".\n const report = () => readOptions?.onProgress?.(out.size, docIds.length)\n const read = (docId: string): { text: string; blocked: boolean } => ({\n text: graph.docSync(docId).doc.getText('content').toString(),\n blocked: contentBlocked(graph.docSync(docId).health()),\n })\n\n // Cache-only batches, retired as they are consumed: the cost of a mirror pass must\n // not scale with live engines the way opening every document does.\n for (let start = 0; start < docIds.length; start += ENGINE_BATCH) {\n if (disposed) break\n const batch = docIds.slice(start, start + ENGINE_BATCH)\n try {\n await graph.seedDocsFromCache(batch)\n for (const docId of batch) {\n const { text, blocked } = read(docId)\n out.set(docId, { text, settled: !blocked && text.length > 0 })\n if (!blocked && text.length === 0) empty.push(docId)\n }\n } finally {\n graph.retireDocs(batch)\n }\n report()\n await breathe()\n }\n\n // An empty seed is a cache miss as often as it is an empty document - the whole of\n // the mirror's \"every page arrived without its body\" bug. Only these pay for a relay\n // catch-up, so a warm graph pays for none.\n if (!graph.isConnected()) return out\n for (let start = 0; start < empty.length; start += ENGINE_BATCH) {\n if (disposed) break\n const batch = empty.slice(start, start + ENGINE_BATCH)\n try {\n await graph.readyDocs(batch)\n await Promise.all(\n batch.map(async (docId) => {\n const reached = await reachedWithin(graph.caughtUpDoc(docId), timeoutMs)\n const { text, blocked } = read(docId)\n out.set(docId, { text, settled: !blocked && (reached || text.length > 0) })\n }),\n )\n } finally {\n graph.retireDocs(batch)\n }\n await breathe()\n }\n return out\n },\n async confirmRegistry(timeoutMs = 5000): Promise<boolean> {\n if (registryConfirmed) return true\n if (!graph.isConnected()) return false\n if (await reachedWithin(graph.rootCaughtUp(), timeoutMs)) registryConfirmed = true\n return registryConfirmed\n },\n onDocumentsChanged(listener) {\n docsChangedListeners.add(listener)\n return () => docsChangedListeners.delete(listener)\n },\n onDocumentRemoved(listener) {\n removedListeners.add(listener)\n return () => removedListeners.delete(listener)\n },\n onChange(listener) {\n changeListeners.add(listener)\n return () => changeListeners.delete(listener)\n },\n async snapshotForIndex(options?: IndexSnapshotOptions): Promise<IndexDocSnapshot[]> {\n const snapshots: IndexDocSnapshot[] = []\n const stream = await this.streamForIndex(options)\n for await (const batch of stream.batches) snapshots.push(...batch)\n return snapshots\n },\n async streamForIndex(options?: IndexSnapshotOptions) {\n const entries: Array<[string, RegistryEntry]> = []\n registry.forEach((entry, docId) => entries.push([docId, entry]))\n return {\n total: entries.length,\n batches: (async function* () {\n for (let start = 0; start < entries.length; start += ENGINE_BATCH) {\n const batch = entries.slice(start, start + ENGINE_BATCH)\n const docIds = batch.map(([docId]) => docId)\n try {\n const docs = await graph.seedDocsFromCache(docIds)\n const snapshots = batch.map(([_docId, entry], index) => ({\n concept: conceptOf(entry),\n kind: entry.kind,\n aliases: entry.aliases ?? [],\n text: bodyForIndex(docs[index].getText('content').toString()),\n }))\n options?.onProgress?.({\n phase: 'loading',\n done: Math.min(start + batch.length, entries.length),\n total: entries.length,\n })\n yield snapshots\n } finally {\n graph.retireDocs(docIds)\n }\n await breathe()\n }\n })(),\n }\n },\n async catchUpPersistedIndex() {\n const docIds: string[] = []\n registry.forEach((_entry, docId) => docIds.push(docId))\n // One bounded metadata request per 512 documents replaces one catch-up request\n // per document. A warm 2,400-document graph therefore asks the relay five cheap\n // questions and only materialises documents whose sequence actually changed.\n const staleDocIds = await graph.docsNeedingCatchup(docIds)\n for (let start = 0; start < staleDocIds.length; start += ENGINE_BATCH) {\n if (disposed) return\n const batch = staleDocIds.slice(start, start + ENGINE_BATCH)\n try {\n await graph.readyDocs(batch)\n // Catch-up is serialized by GraphSync. Waiting for each terminal page\n // keeps the graph-scoped cross-tab lock until the complete history has\n // advanced the shared cache, while bounding live Y.Docs to one batch.\n // If the browser starts offline, these promises stay pending and the\n // batch remains alive until the socket eventually opens. Graph disposal\n // releases the waits, so leaving an offline workspace cannot leak it.\n await Promise.all(batch.map((docId) => graph.caughtUpDoc(docId)))\n } finally {\n graph.retireDocs(batch)\n }\n if (disposed) return\n await breathe()\n }\n },\n async pendingIndexChanges() {\n const checkpoint = await graph.pendingIndexChanges()\n const root = new Y.Doc()\n try {\n if (checkpoint.rootUpdate) Y.applyUpdate(root, checkpoint.rootUpdate)\n } catch (error) {\n root.destroy()\n throw error\n }\n const checkpointRegistry = root.getMap<RegistryEntry>('registry')\n const entries: Array<[string, RegistryEntry]> = []\n checkpointRegistry.forEach((entry, docId) => entries.push([docId, entry]))\n root.destroy()\n const entriesByDocId = new Map(entries)\n\n const snapshot = async (\n docId: string,\n entry: RegistryEntry,\n ): Promise<IndexDocSnapshot> => {\n const update = await checkpoint.readDocument(docId)\n const doc = new Y.Doc()\n try {\n if (update) Y.applyUpdate(doc, update)\n return {\n concept: conceptOf(entry),\n kind: entry.kind,\n aliases: entry.aliases ?? [],\n text: bodyForIndex(doc.getText('content').toString()),\n }\n } finally {\n doc.destroy()\n }\n }\n\n const checkpointStream: NonNullable<IndexChangeCheckpoint['streamForIndex']> =\n async (snapshotOptions) => ({\n total: entries.length,\n batches: (async function* () {\n for (let start = 0; start < entries.length; start += ENGINE_BATCH) {\n const batch = entries.slice(start, start + ENGINE_BATCH)\n const snapshots = await Promise.all(\n batch.map(([docId, entry]) => snapshot(docId, entry)),\n )\n snapshotOptions?.onProgress?.({\n phase: 'loading',\n done: Math.min(start + batch.length, entries.length),\n total: entries.length,\n })\n yield snapshots\n await breathe()\n }\n })(),\n })\n\n const concepts = new Map<string, string>()\n const documents: IndexDocSnapshot[] = []\n let requiresFullReplacement = false\n for (const docId of checkpoint.docIds) {\n const entry = entriesByDocId.get(docId)\n if (!entry) {\n // The root document owns graph identity, aliases and membership. A\n // removed content document is also absent here. Either shape can remove\n // index rows, so a targeted upsert is insufficient.\n requiresFullReplacement = true\n break\n }\n const concept = conceptOf(entry)\n concepts.set(conceptKey(concept), concept)\n documents.push(await snapshot(docId, entry))\n }\n return {\n changes: requiresFullReplacement\n ? null\n : [...concepts.values()].map((concept) => ({ concept })),\n documents,\n streamForIndex: checkpointStream,\n acknowledge: checkpoint.acknowledge,\n }\n },\n snapshotDocument(concept: string): IndexDocSnapshot | null {\n const docId = docIdFor(concept)\n const entry = docId ? registry.get(docId) : undefined\n if (!docId || !entry) return null\n return {\n concept: conceptOf(entry),\n kind: entry.kind,\n aliases: entry.aliases ?? [],\n text: bodyForIndex(graph.docSync(docId).doc.getText('content').toString()),\n }\n },\n\n async compactDocument(target: string): Promise<void> {\n const docId = docIdFor(target)\n if (docId) await graph.compactDoc(docId)\n },\n\n async uploadSnapshot(target: string): Promise<void> {\n const docId = docIdFor(target)\n if (docId) await graph.uploadSnapshot(docId)\n },\n\n async setAliases(target: string, aliases: readonly string[]): Promise<void> {\n const docId = docIdFor(target)\n const entry = docId ? registry.get(docId) : undefined\n if (!docId || !entry) throw new DocumentNotFoundError(target)\n const next = normaliseAliases(aliases, conceptOf(entry))\n const updated: RegistryEntry = { ...entry }\n if (next.length > 0) updated.aliases = next\n else delete updated.aliases\n registry.set(docId, updated)\n writeBackIdentity(docId)\n },\n // CRDTs merge — there is nothing to reconcile or resolve.\n async reconcile() {},\n async resolveConflict() {},\n async createJournal(date: string, body = ''): Promise<string> {\n const day = date.trim()\n if (!isJournalConcept(day)) throw new Error(`\"${date}\" is not a calendar day.`)\n if (docIdFor(day)) throw new Error(`A document for \"${day}\" already exists`)\n const docId = crypto.randomUUID()\n registry.set(docId, { kind: 'journal', date: day } satisfies RegistryEntry)\n // Seeded in the same breath as the registry entry, exactly as createPage does, so a\n // promoting [[Draft]] never writes twice (ADR 0050).\n if (body !== '') {\n const ytext = graph.docSync(docId).doc.getText('content')\n ytext.doc!.transact(() => ytext.insert(0, body))\n }\n return day\n },\n async createPage(title: string, body = ''): Promise<string> {\n if (docIdFor(title)) throw new Error(`A page for \"${title}\" already exists`)\n const docId = crypto.randomUUID()\n registry.set(docId, { kind: 'page', title } satisfies RegistryEntry)\n // Seed the content in the same breath as the registry entry, so a promoting\n // [[Draft]] never has to write twice (ADR 0050). A local-origin insert, exactly\n // as a keystroke would be, so the sync engine carries it like any other edit.\n if (body !== '') {\n const ytext = graph.docSync(docId).doc.getText('content')\n ytext.doc!.transact(() => ytext.insert(0, body))\n }\n return title\n },\n\n async planRename(from: string, to: string, referencingDocuments?: number): Promise<RenamePlan> {\n // No entry is not a refusal: a [[Pageless Concept]] renames by rewriting its links,\n // and the plan says so through a null kind (ADR 0064).\n const docId = docIdFor(from)\n const entry = docId ? registry.get(docId) : undefined\n\n // Counting here materialises every Y.Doc in the graph. The caller usually has the\n // figure already (the backlink index knows it); the slow path stays for those that\n // do not.\n let counted = 0\n const concepts: string[] = []\n const aliases: { name: string; concept: string }[] = []\n registry.forEach((e, id) => {\n concepts.push(conceptOf(e))\n for (const alias of e.aliases ?? []) aliases.push({ name: alias, concept: conceptOf(e) })\n if (referencingDocuments !== undefined) return\n const text = graph.docSync(id).doc.getText('content').toString()\n if (countWikilinkTargets(text, from) > 0) counted += 1\n })\n const plan = planRename({\n from,\n to,\n kind: entry?.kind ?? null,\n concepts,\n aliases,\n referencingDocuments: referencingDocuments ?? counted,\n })\n // A Protected Document never merges (ADR 0062). Read from the live document, which\n // is what a merge would join.\n return refuseProtectedMerges(plan, (concept) => {\n const id = docIdFor(concept)\n if (!id) return false\n return documentProtection(graph.docSync(id).doc.getText('content').toString()).kind === 'document'\n })\n },\n\n /**\n * Apply a rename plan (ADR 0038). A server document's identity lives ONLY in the\n * encrypted registry (ADR 0024), so a rename is a registry write - there is no\n * frontmatter here, which is why the alias strategy keeps the old name in the registry\n * entry rather than in the document body - and a [[Merge]] is a content write into the\n * surviving doc plus dropping the absorbed entry. A step with no document behind it\n * (a [[Pageless Concept]], ADR 0064) is only the body pass below.\n */\n async renamePage(from: string, to: string, options: RenameOptions): Promise<RenameResult> {\n // Re-plan rather than trust the dialog's preview - the graph may have moved. Only\n // the preview reads the reference count, so the apply path does not pay for it.\n const plan = await this.planRename(from, to, 0)\n if (plan.refusal) throw new Error(plan.refusal)\n\n for (const step of renameSteps(plan)) {\n const docId = docIdFor(step.from)\n if (!docId) continue // a pageless concept; only its links move (ADR 0064)\n const entry = registry.get(docId)\n if (!entry) continue\n\n let aliases = Array.isArray(entry.aliases) ? entry.aliases : []\n if (options.strategy === 'alias' && !aliases.some((a) => conceptKey(a) === conceptKey(step.from))) {\n // Every renamed document keeps its old name, cascaded ones included.\n aliases = [...aliases, step.from]\n }\n\n // The document that answers to the name - by title or alias (docIdFor resolves\n // both) - is the survivor, and keeps its own title (`step.into`).\n const targetId = step.merges ? docIdFor(step.into) : undefined\n if (targetId && targetId !== docId) {\n const survivor = graph.docSync(targetId).doc.getText('content')\n const absorbed = graph.docSync(docId).doc.getText('content')\n const survivorEntry = registry.get(targetId)\n // The absorbed document's block, if it has one, is identity the registry\n // already carries - its aliases union below, its title is the name being\n // vacated. As body text it would sit as a second `---` block inside the\n // survivor, so only what is below it is joined (as on a Filesystem Backend).\n const absorbedText = absorbed.toString()\n const absorbedBody = absorbedText.slice(frontmatterSpan(absorbedText)?.end ?? 0)\n const merged = mergeDocuments(\n { body: survivor.toString(), aliases: survivorEntry?.aliases ?? [] },\n { body: absorbedBody, aliases },\n )\n survivor.doc!.transact(() => {\n survivor.delete(0, survivor.length)\n survivor.insert(0, merged.body)\n }, STORE)\n registry.set(targetId, {\n ...(survivorEntry ?? { kind: entry.kind }),\n title: step.into,\n ...(merged.aliases.length > 0 ? { aliases: merged.aliases } : {}),\n })\n // The absorbed document's entry goes; its content is now in the survivor.\n registry.delete(docId)\n writeBackIdentity(targetId)\n continue\n }\n\n // A page renamed onto one of its own aliases: the alias becomes the title and\n // is no longer an alias.\n const next: RegistryEntry = { ...entry, title: step.to }\n const kept = normaliseAliases(aliases, step.to)\n if (kept.length > 0) next.aliases = kept\n else delete next.aliases\n registry.set(docId, next)\n writeBackIdentity(docId)\n }\n\n // Bodies last, in one pass: the cascade rule fixes plain and scoped references\n // together. Each document is its own CRDT transaction — there is none spanning them\n // — and within a document the rewrite is a set of per-occurrence splices, never a\n // whole-text replace (ADR 0066): a delete-all would not cover anything typed\n // concurrently by a Player or another device, which then survived and was re-seated\n // at the start of the document. Applied back-to-front so earlier offsets stay valid.\n let rewritten = 0\n if (options.strategy === 'rewrite') {\n const edits: { id: string; splices: TextSplice[] }[] = []\n registry.forEach((_e, id) => {\n const ytext = graph.docSync(id).doc.getText('content')\n // The body only, as on a Filesystem Backend: a block's title is the\n // write-back's to set, and anything else up there is not a reference.\n const text = ytext.toString()\n const prefix = frontmatterSpan(text)?.end ?? 0\n const splices = wikilinkScopeSplices(text.slice(prefix), plan.direct.from, plan.direct.to).map(\n (s) => ({ ...s, from: s.from + prefix, to: s.to + prefix }),\n )\n if (splices.length > 0) edits.push({ id, splices })\n })\n for (const edit of edits) {\n const ytext = graph.docSync(edit.id).doc.getText('content')\n ytext.doc!.transact(() => {\n for (let i = edit.splices.length - 1; i >= 0; i--) {\n const splice = edit.splices[i]\n ytext.delete(splice.from, splice.to - splice.from)\n ytext.insert(splice.from, splice.insert)\n }\n }, STORE)\n rewritten += 1\n }\n }\n\n return { concept: plan.direct.into, rewritten, cascaded: plan.cascade.length, merged: mergeCount(plan) }\n },\n\n /**\n * Delete a document (ADR 0039): the registry entry goes AND the stored content is\n * removed server-side, so [[Storage Footprint]] drops immediately rather than waiting\n * on compaction.\n */\n async deleteDocument(concept: string): Promise<void> {\n const docId = docIdFor(concept)\n if (!docId) return // already gone; deleting twice is not an error\n // The ordered content lifecycle transition is durable before the encrypted root\n // registry changes. A delayed old-generation append can no longer recreate rows.\n await graph.deleteDoc(docId)\n // Dropping the entry is what fires onDocumentRemoved. The registry is encrypted\n // client content, so it remains a separate convergent operation.\n registry.delete(docId)\n },\n\n async dispose() {\n disposed = true\n unobserveRegistry()\n unobserveDocUpdates()\n for (const stop of resurrectors.values()) stop()\n resurrectors.clear()\n await graph.flushAll()\n graph.dispose()\n },\n }\n}\n","/**\n * On-disk persistence for the two things the [[Headless Client]] otherwise rebuilds on every\n * launch: the [[Local Cache]] and the [[Derived Index]]. Neither store is re-implemented here;\n * both keep running exactly as they do in the browser, and this module only carries their bytes\n * across process lifetimes.\n *\n * - The Local Cache runs over `fake-indexeddb`, an in-memory IndexedDB. Its rows for one graph\n * (`docs`, `outbox`, `index-dirty` of the `etherpk-sync` database) are read back through the\n * ordinary IndexedDB API and written to one file; on launch they are put back into the fresh\n * in-memory database before the sync engine opens it. The cache's own merge rules, dirty\n * tokens and watermarks are therefore untouched - a restored row is the row that was there.\n * - The index is SQLite in WASM memory. sqlite-wasm exports a database as its file bytes\n * (`sqlite3_js_db_export`) and imports them into its virtual filesystem\n * (`sqlite3_js_posix_create_file`), so a launch opens the index the last run committed and the\n * worker core takes its warm path: only documents whose watermark moved are caught up.\n *\n * **Invalidation is the engines' own.** Nothing here decides what is stale. On launch the sync\n * engine compares every cached document's `(generation, lifecycle, lastSeq)` with the relay in\n * bounded metadata requests and catches up only what moved; a deletion arrives as a lifecycle\n * change; unacknowledged local edits replay from the outbox; the index re-ingests exactly the\n * documents whose `index-dirty` tokens the cache still holds. A schema change is a wholesale\n * discard by design (ADR 0015): the file names carry the cache's `DB_VERSION` and the index's\n * `INDEX_SCHEMA_VERSION`, so a newer build simply does not find the old files, and the core's own\n * `PRAGMA user_version` check discards a stale index it is handed.\n *\n * **Ordering.** A snapshot writes the cache first and the index second. The index must never be\n * behind what the cache says it has acknowledged, and an acknowledgement is only ever written\n * after the ingest it confirms, so an index captured after the cache always covers every\n * acknowledged token in it; a write that lands between the two captures leaves its dirty token\n * in the cache snapshot and is simply re-ingested next launch.\n *\n * What is on disk is plaintext Yjs state and plaintext index rows, the same as the browser\n * profile holds in IndexedDB and OPFS: the trust class the user chose for this machine. A\n * protected document's cache row is collected the moment it reads as protected (the same\n * `collectRowWhen` the workspace passes), and the index never held one. `logout` deletes the lot.\n */\n\nimport { deserialize, serialize } from 'node:v8'\nimport { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises'\nimport { homedir } from 'node:os'\nimport { dirname, join } from 'node:path'\n\nimport { INDEX_SCHEMA_VERSION, createSchema, isUsableIndex, type SqlDb } from '$lib/document/index-db'\nimport { wrapOo1Db } from '$lib/document/index-db-sqlite'\nimport type { IndexDbHost } from '$lib/document/index-worker/core'\n\n/** The Local Cache's IndexedDB database and stores, as `sync/local-cache.ts` names them. */\nconst CACHE_DB = 'etherpk-sync'\nconst CACHE_DB_VERSION = 3\nconst CACHE_STORES = ['docs', 'outbox', 'index-dirty'] as const\ntype CacheStore = (typeof CACHE_STORES)[number]\n\ninterface CacheSnapshot {\n version: number\n graphId: string\n rows: Record<CacheStore, unknown[]>\n}\n\n/**\n * `~/.cache/etherpk/mcp/<server host>/<graph id>/`, or under `ETHERPK_MCP_CACHE_DIR`. The host's\n * port separator becomes `_`: a colon is not a path character on Windows.\n */\nexport function graphCacheDir(env: NodeJS.ProcessEnv, serverBaseUrl: string, graphId: string): string {\n const base = env.ETHERPK_MCP_CACHE_DIR?.trim() || join(env.XDG_CACHE_HOME?.trim() || join(homedir(), '.cache'), 'etherpk', 'mcp')\n const host = new URL(serverBaseUrl).host.replace(/[^A-Za-z0-9.-]/g, '_')\n return join(base, host, graphId)\n}\n\n/** The root every graph's cache dir sits under, for `logout` to remove. */\nexport function cacheRoot(env: NodeJS.ProcessEnv): string {\n return env.ETHERPK_MCP_CACHE_DIR?.trim() || join(env.XDG_CACHE_HOME?.trim() || join(homedir(), '.cache'), 'etherpk', 'mcp')\n}\n\nexport function cacheFile(dir: string): string {\n return join(dir, `local-cache.v${CACHE_DB_VERSION}.bin`)\n}\n\nexport function indexFile(dir: string): string {\n return join(dir, `index.v${INDEX_SCHEMA_VERSION}.sqlite`)\n}\n\nfunction request<T>(req: IDBRequest<T>): Promise<T> {\n return new Promise((resolve, reject) => {\n req.onsuccess = () => resolve(req.result)\n req.onerror = () => reject(req.error)\n })\n}\n\nfunction done(tx: IDBTransaction): Promise<void> {\n return new Promise((resolve, reject) => {\n tx.oncomplete = () => resolve()\n tx.onerror = () => reject(tx.error)\n tx.onabort = () => reject(tx.error)\n })\n}\n\n/** Open the cache database the sync engine already created; never creates or upgrades it. */\nasync function openCacheDb(): Promise<IDBDatabase> {\n const db = await request(indexedDB.open(CACHE_DB, CACHE_DB_VERSION))\n for (const store of CACHE_STORES) {\n if (!db.objectStoreNames.contains(store)) {\n db.close()\n throw new Error(`Local Cache database has no \"${store}\" store; open the graph cache before restoring it.`)\n }\n }\n return db\n}\n\n/** Every row of every cache store that belongs to `graphId`, as one serialisable snapshot. */\nexport async function captureLocalCache(graphId: string): Promise<CacheSnapshot> {\n const db = await openCacheDb()\n try {\n const tx = db.transaction([...CACHE_STORES], 'readonly')\n const rows = {} as Record<CacheStore, unknown[]>\n for (const store of CACHE_STORES) {\n const all = (await request(tx.objectStore(store).getAll())) as Array<{ graphId?: string }>\n rows[store] = all.filter((row) => row.graphId === graphId)\n }\n await done(tx)\n return { version: CACHE_DB_VERSION, graphId, rows }\n } finally {\n db.close()\n }\n}\n\n/** Put a snapshot's rows back. Existing rows for the same keys are replaced. */\nexport async function restoreLocalCache(snapshot: CacheSnapshot): Promise<number> {\n const db = await openCacheDb()\n try {\n const tx = db.transaction([...CACHE_STORES], 'readwrite')\n let count = 0\n for (const store of CACHE_STORES) {\n for (const row of snapshot.rows[store]) {\n tx.objectStore(store).put(row)\n count++\n }\n }\n await done(tx)\n return count\n } finally {\n db.close()\n }\n}\n\nasync function writeAtomically(path: string, bytes: Uint8Array): Promise<void> {\n const temp = `${path}.${process.pid}.tmp`\n await writeFile(temp, bytes, { mode: 0o600 })\n await rename(temp, path)\n}\n\nasync function exists(path: string): Promise<boolean> {\n try {\n await stat(path)\n return true\n } catch {\n return false\n }\n}\n\n/** Save the graph's cache rows to disk. */\nexport async function persistLocalCache(dir: string, graphId: string): Promise<void> {\n const snapshot = await captureLocalCache(graphId)\n await mkdir(dir, { recursive: true, mode: 0o700 })\n await writeAtomically(cacheFile(dir), serialize(snapshot))\n}\n\n/**\n * Load a saved cache into the in-memory database, if one exists for this build's cache version.\n * A file that will not parse or belongs to another graph is ignored: the cache is rebuildable.\n */\nexport async function loadLocalCache(dir: string, graphId: string): Promise<number> {\n const path = cacheFile(dir)\n if (!(await exists(path))) return 0\n try {\n const snapshot = deserialize(await readFile(path)) as CacheSnapshot\n if (snapshot.version !== CACHE_DB_VERSION || snapshot.graphId !== graphId) return 0\n return await restoreLocalCache(snapshot)\n } catch (error) {\n console.error(`etherpk-mcp: ignoring an unreadable cache file ${path}: ${error instanceof Error ? error.message : String(error)}`)\n return 0\n }\n}\n\n/**\n * An {@link IndexDbHost} over sqlite-wasm's memory with a file on disk behind it. `open` imports\n * the saved database when there is one and reports `persisted: true`, which is what puts the\n * index core on its warm path; `discard` deletes the file and hands back an empty database.\n * {@link NodeIndexHost.export} writes the live database's bytes for the next launch.\n */\nexport interface NodeIndexHost extends IndexDbHost {\n /** Write the open database to disk; a no-op before `open`. */\n export(): Promise<void>\n}\n\n// The oo1 API typed loosely at this one boundary, as `index-db-sqlite.ts` does.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype Sqlite3 = any\n\nexport function nodeIndexHost(dir: string): NodeIndexHost {\n let sqlite3: Sqlite3 | undefined\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let live: { oo1: any; db: SqlDb } | undefined\n const path = indexFile(dir)\n /** sqlite-wasm's own virtual filesystem path; one per process is enough. */\n const vfsName = `/etherpk-index-${process.pid}.sqlite`\n\n async function init(): Promise<Sqlite3> {\n if (!sqlite3) {\n const { default: sqlite3InitModule } = await import('@sqlite.org/sqlite-wasm')\n sqlite3 = await sqlite3InitModule()\n }\n return sqlite3\n }\n\n function openFresh(s: Sqlite3): { oo1: Sqlite3; db: SqlDb } {\n const oo1 = new s.oo1.DB(':memory:')\n const db = wrapOo1Db(oo1)\n createSchema(db)\n return { oo1, db }\n }\n\n async function openSaved(s: Sqlite3): Promise<{ oo1: Sqlite3; db: SqlDb } | null> {\n if (!(await exists(path))) return null\n try {\n const bytes = await readFile(path)\n s.capi.sqlite3_js_posix_create_file(vfsName, new Uint8Array(bytes))\n const oo1 = new s.oo1.DB(vfsName)\n const db = wrapOo1Db(oo1)\n if (!isUsableIndex(db)) {\n // The core would discard it anyway; do it here so the file goes too.\n db.close()\n await rm(path, { force: true })\n return null\n }\n return { oo1, db }\n } catch (error) {\n console.error(`etherpk-mcp: ignoring an unreadable index file ${path}: ${error instanceof Error ? error.message : String(error)}`)\n await rm(path, { force: true })\n return null\n }\n }\n\n return {\n async open() {\n const s = await init()\n const saved = await openSaved(s)\n live = saved ?? openFresh(s)\n return { db: live.db, persisted: true }\n },\n async discard() {\n const s = await init()\n await rm(path, { force: true })\n live = openFresh(s)\n return { db: live.db, persisted: true }\n },\n async export() {\n if (!live || !sqlite3) return\n const bytes = sqlite3.capi.sqlite3_js_db_export(live.oo1.pointer) as Uint8Array\n await mkdir(dir, { recursive: true, mode: 0o700 })\n await writeAtomically(path, bytes)\n },\n }\n}\n\n/** Remove every persisted graph under the cache root (logout of the last server). */\nexport async function removeCacheRoot(env: NodeJS.ProcessEnv): Promise<void> {\n await rm(cacheRoot(env), { recursive: true, force: true })\n}\n\n/** Remove one server's persisted graphs (logout of that server while others stay). */\nexport async function removeServerCache(env: NodeJS.ProcessEnv, serverBaseUrl: string): Promise<void> {\n await rm(dirname(graphCacheDir(env, serverBaseUrl, 'x')), { recursive: true, force: true })\n}\n","/**\n * One synced graph, open in a process with no editor: the client's own sync engine, server\n * document store and derived index, wired exactly as `GraphWorkspace.svelte` wires them for a\n * tab, minus the tab (ADR 0072). The [[Headless Client]] holds the Graph Key and does the\n * decrypting; the [[Sync Server]] stays as blind to this device as to any other.\n *\n * Two browser seams are swapped here and nowhere else:\n *\n * - The [[Local Cache]] is IndexedDB in the browser. Node has none, so `fake-indexeddb` provides\n * the same API in memory and `openGraphCache` runs unchanged - the store's tests already run it\n * this way. With a `persistDir`, its rows are restored from the last run before the engine\n * opens it and snapshotted after writes (`persistence.ts`); without one it is rebuilt from the\n * relay on every open, which is the test tier's mode.\n * - The [[Derived Index]] runs in-process over sqlite-wasm in memory rather than in a shared\n * worker over OPFS; with a `persistDir` the database bytes are imported on open and exported\n * with the cache, so the index core takes its warm path.\n */\n\nimport 'fake-indexeddb/auto'\n\nimport type { GraphKeyring } from '$lib/crypto'\nimport { createRemoteGraphIndex, type RemoteGraphIndex } from '$lib/document/index-worker/client'\nimport { inlineTransport, memoryDbHost } from '$lib/document/index-worker/transport'\nimport { createServerDocumentStore, type ServerDocumentStore } from '$lib/storage/server/server-document-store'\nimport { createGraphSync, type GraphSync, type TransportSocket } from '$lib/sync/graph-sync'\nimport { openGraphCache } from '$lib/sync/local-cache'\nimport { PRESENCE_PALETTE } from '$lib/sync/presence-identity'\nimport type { SyncTokenSource } from '$lib/sync/sync-token'\nimport { documentProtection } from '$lib/document/protection/cipher-fence'\n\nimport { nodeTransport } from './node-transport'\nimport { loadLocalCache, nodeIndexHost, persistLocalCache, type NodeIndexHost } from './persistence'\n\nexport interface HeadlessGraphDeps {\n graphId: string\n rootDocId: string\n keyring: GraphKeyring\n relayUrl: string\n token: SyncTokenSource\n /** How this device announces itself in collaborators' editors: *Agent on <host>*. */\n presenceName: string\n /** Swap point for tests (the loopback relay); production uses the Node WebSocket. */\n connect?: (url: string) => TransportSocket\n /** Bounded wait for the first relay connection; a serve that cannot reach the relay fails loudly. */\n readyTimeoutMs?: number\n /**\n * Where the Local Cache and the index are kept between launches (`persistence.ts`). Absent,\n * both live in memory and are rebuilt from the relay on every open - the test tier's mode.\n */\n persistDir?: string\n /** How long after a write before the snapshot is taken; the shutdown path never waits. */\n persistDebounceMs?: number\n onError?: (error: Error) => void\n}\n\nexport interface HeadlessGraph {\n readonly graphId: string\n readonly sync: GraphSync\n readonly store: ServerDocumentStore\n readonly index: RemoteGraphIndex\n /** Flush every pending local edit and wait for the relay's acknowledgement. */\n settle(): Promise<{ settled: boolean; outstanding: number }>\n /** Write the cache and the index to `persistDir` now; a no-op without one. */\n persist(): Promise<void>\n dispose(): Promise<void>\n}\n\n/** Open the graph, scan its registry and build the index; resolves once tools can answer. */\nexport async function openHeadlessGraph(deps: HeadlessGraphDeps): Promise<HeadlessGraph> {\n const cache = await openGraphCache(deps.graphId)\n // The saved rows go into the (fresh, in-memory) database the line above just created, before\n // the engine seeds from it: what it then reads is what the last run left.\n if (deps.persistDir) await loadLocalCache(deps.persistDir, deps.graphId)\n const indexHost: NodeIndexHost | undefined = deps.persistDir ? nodeIndexHost(deps.persistDir) : undefined\n const sync = createGraphSync({\n graphId: deps.graphId,\n rootDocId: deps.rootDocId,\n keyring: deps.keyring,\n relayUrl: deps.relayUrl,\n token: deps.token,\n cache,\n connect: deps.connect ?? nodeTransport,\n // Same rule as the workspace: a document that reads as protected has its cache row\n // collected so the body it was protected to hide does not linger in the clear.\n collectRowWhen: (doc) => documentProtection(doc.getText('content').toString()).kind === 'document',\n // The agent's caret colour is whichever palette entry the session allocator leaves it;\n // the name is what members read.\n presence: { name: deps.presenceName, ...PRESENCE_PALETTE[0] },\n onError: deps.onError,\n })\n const store = createServerDocumentStore(sync, { readyTimeoutMs: deps.readyTimeoutMs })\n try {\n await store.scan()\n const index = createRemoteGraphIndex(store, inlineTransport(indexHost ?? memoryDbHost()), { graphId: deps.graphId })\n await index.prepare()\n await index.refresh()\n\n // One snapshot at a time, cache before index (see persistence.ts → Ordering), and never\n // on the tool's own critical path: a write schedules one, shutdown takes one.\n let persisting: Promise<void> = Promise.resolve()\n let timer: ReturnType<typeof setTimeout> | undefined\n const persist = () => {\n if (!deps.persistDir || !indexHost) return persisting\n const dir = deps.persistDir\n persisting = persisting\n .then(() => persistLocalCache(dir, deps.graphId))\n .then(() => indexHost.export())\n .catch((error: unknown) => deps.onError?.(error instanceof Error ? error : new Error(String(error))))\n return persisting\n }\n const schedulePersist = () => {\n if (!deps.persistDir) return\n clearTimeout(timer)\n timer = setTimeout(() => void persist(), deps.persistDebounceMs ?? 5_000)\n }\n // Remote edits arriving while serving count too, or a quiet agent would leave a stale file.\n const unsubscribe = sync.onDocUpdate(schedulePersist)\n\n return {\n graphId: deps.graphId,\n sync,\n store,\n index,\n async settle() {\n await sync.flushAll()\n const result = await sync.awaitAcked({ stallMs: 10_000 })\n schedulePersist()\n return result\n },\n persist,\n async dispose() {\n clearTimeout(timer)\n unsubscribe()\n await persist()\n index.dispose()\n await store.dispose()\n cache.dispose()\n },\n }\n } catch (error) {\n await store.dispose().catch(() => {})\n cache.dispose()\n throw error\n }\n}\n","/**\n * Device approval (ADR 0026 flows): unlock a NEW device from an already-unlocked one, so\n * the Recovery Code stays in the drawer for genuine recovery. The new device generates an\n * ephemeral X25519 pair and shows a SAS derived from its public key; an unlocked device\n * shows the SAS it derives from the key it RECEIVED, the user compares the two screens,\n * and on approve the vault key travels sealed to the ephemeral key. The server brokers\n * blind - a substituted key would change the SAS, and the sealed box it carries opens only\n * on the requesting device.\n *\n * Pure over an injected SyncApi so it unit-tests against a fetch stub.\n */\nimport {\n contextAad,\n deviceApprovalSas,\n fromBase64Url,\n generateIdentityKeyPair,\n openSealed,\n openVault,\n reencryptVault,\n sealToPublicKey,\n toBase64Url,\n} from '$lib/crypto'\nimport type { SyncApi } from './sync-api'\n\n/** AAD binds the sealed vault key to its approval - a blob cannot be replayed across requests. */\nconst approvalAad = (approvalId: string) => contextAad('device-approval', `approval:${approvalId}`)\n\nexport interface DeviceApprovalRequest {\n id: string\n /** The code THIS device displays; the approver must see the same one. */\n sas: string\n ephemeralPrivateKey: Uint8Array\n}\n\n/** New device: register a request and derive the SAS to display. */\nexport async function beginDeviceApproval(api: SyncApi): Promise<DeviceApprovalRequest> {\n const pair = generateIdentityKeyPair() // ephemeral X25519 - same curve as identities\n const { id } = await api.createDeviceApproval(toBase64Url(pair.publicKey))\n return { id, sas: await deviceApprovalSas(pair.publicKey), ephemeralPrivateKey: pair.privateKey }\n}\n\nexport type ApprovalPoll =\n | { state: 'waiting' }\n | { state: 'rejected' }\n | { state: 'expired' }\n | { state: 'unlocked'; deviceKey: Uint8Array }\n\n/**\n * New device: poll for the outcome. On 'unlocked' the sealed blob has been claimed (the\n * server clears it), opened with the ephemeral key, and VERIFIED against the account vault\n * - the returned deviceKey provably decrypts this account's keys.\n */\nexport async function pollDeviceApproval(api: SyncApi, request: DeviceApprovalRequest): Promise<ApprovalPoll> {\n const res = await api.pollDeviceApproval(request.id)\n if (res.status === 'pending') return { state: 'waiting' }\n if (res.status === 'rejected') return { state: 'rejected' }\n if (res.status === 'expired') return { state: 'expired' }\n if (res.status === 'sealed' && res.sealedVaultKey) {\n const deviceKey = await openSealed(request.ephemeralPrivateKey, fromBase64Url(res.sealedVaultKey), approvalAad(request.id))\n const vault = await api.getVault()\n if (!vault) throw new Error('No vault on this account')\n await openVault(fromBase64Url(vault.vault), deviceKey) // throws if the sealed key is wrong\n return { state: 'unlocked', deviceKey }\n }\n // 'claimed' with no blob means a previous poll consumed it and something went wrong after.\n throw new Error(`Approval is in an unexpected state (${res.status}) - start again`)\n}\n\nexport interface PendingDeviceApproval {\n id: string\n ephemeralPublicKey: string\n createdAt: string\n}\n\n/** Approver: the SAS to show for a pending request - derived from the RECEIVED key. */\nexport function approvalSas(approval: PendingDeviceApproval): Promise<string> {\n return deviceApprovalSas(fromBase64Url(approval.ephemeralPublicKey))\n}\n\n/**\n * Approver: seal this account's vault key to the request's ephemeral key. `heldKey` is\n * whatever this device caches (wrap key or vault key - openVault takes either). A legacy\n * vault is upgraded to v2 first, minting the vault key there is to seal. Returns the vault\n * key so the caller can re-cache it (self-healing, and required after an upgrade).\n */\nexport async function approveDevice(api: SyncApi, approval: PendingDeviceApproval, heldKey: Uint8Array): Promise<Uint8Array> {\n const stored = await api.getVault()\n if (!stored) throw new Error('No vault on this account')\n const envelope = fromBase64Url(stored.vault)\n const opened = await openVault(envelope, heldKey)\n let vaultKey = opened.vaultKey\n if (opened.legacy) {\n const upgraded = await reencryptVault(opened.vault, envelope, opened)\n await api.putVault(toBase64Url(upgraded.envelope), stored.version)\n vaultKey = upgraded.vaultKey\n }\n const sealed = await sealToPublicKey(fromBase64Url(approval.ephemeralPublicKey), vaultKey, approvalAad(approval.id))\n await api.sealDeviceApproval(approval.id, toBase64Url(sealed))\n return vaultKey\n}\n\n/** Approver rejects / requestor cancels. */\nexport async function rejectDeviceApproval(api: SyncApi, approvalId: string): Promise<void> {\n await api.cancelDeviceApproval(approvalId)\n}\n","/**\n * How the [[Headless Client]] gets the vault key the first time, as a device of the account\n * (ADR 0072). Two routes, both already served by the Sync Server for every new device:\n *\n * - **Device Approval** (ADR 0034), the default: this process registers an ephemeral key and\n * prints the SAS; the user confirms the same code in any unlocked EtherPK tab, which seals the\n * vault key to the ephemeral key; the poll claims it and verifies it opens the vault. No secret\n * passes through the terminal.\n * - **Recovery Code**, behind a flag, for a box with no tab to hand: the code derives the wrap\n * key, which opens the vault; what is cached is the vault key, never the code.\n *\n * Pure over an injected clock and output so the flow is testable without a terminal.\n */\n\nimport { deriveVaultWrapKey, fromBase64Url, normalizeRecoveryCode, openVault } from '$lib/crypto'\nimport { beginDeviceApproval, pollDeviceApproval } from '$lib/sync/device-approval'\nimport type { SyncApi } from '$lib/sync/sync-api'\n\nexport interface LoginIo {\n /** A line for the user: the SAS, progress, the outcome. Never a secret. */\n say(line: string): void\n sleep(ms: number): Promise<void>\n /**\n * Aborts the wait: the user chose another route or pressed Ctrl-C. The pending approval is\n * cancelled server-side and {@link ApprovalAbandoned} is thrown, so the caller can tell a\n * deliberate switch from a failure.\n */\n signal?: AbortSignal\n /** Where the EtherPK Client lives, to name in the instruction; null when the Server did not say. */\n clientUrl?: string | null\n}\n\n/** The user left the approval wait on purpose (the signal fired); nothing went wrong. */\nexport class ApprovalAbandoned extends Error {\n constructor() {\n super('Approval abandoned.')\n this.name = 'ApprovalAbandoned'\n }\n}\n\n/** Approval rows expire server-side after ten minutes; poll a little longer and then give up. */\nconst APPROVAL_TIMEOUT_MS = 11 * 60_000\nconst APPROVAL_POLL_MS = 2_000\n\nexport async function unlockByDeviceApproval(api: SyncApi, io: LoginIo): Promise<Uint8Array> {\n const request = await beginDeviceApproval(api)\n const where = io.clientUrl ? `open EtherPK at ${io.clientUrl}` : 'open EtherPK in a browser'\n io.say('')\n io.say(`To approve this device, ${where} (any page - it need not be a note) signed in to this account`)\n io.say('with its graphs unlocked. A prompt will show a code; confirm it matches this one:')\n io.say('')\n io.say(` ${request.sas}`)\n io.say('')\n io.say('Waiting (up to ten minutes)…')\n const abandon = async () => {\n await api.cancelDeviceApproval(request.id).catch(() => {})\n throw new ApprovalAbandoned()\n }\n const deadline = Date.now() + APPROVAL_TIMEOUT_MS\n while (Date.now() < deadline) {\n if (io.signal?.aborted) await abandon()\n const outcome = await pollDeviceApproval(api, request)\n if (outcome.state === 'unlocked') return outcome.deviceKey\n if (outcome.state === 'rejected') throw new Error('The approval was rejected in EtherPK.')\n if (outcome.state === 'expired') throw new Error('The approval expired before it was confirmed. Run login again.')\n await io.sleep(APPROVAL_POLL_MS)\n if (io.signal?.aborted) await abandon()\n }\n throw new Error('The approval was not confirmed in time. Run login again.')\n}\n\n/** Recovery Code → wrap key → open the vault; what comes back is the vault key to cache. */\nexport async function unlockByRecoveryCode(api: SyncApi, code: string): Promise<Uint8Array> {\n const wrapKey = await deriveVaultWrapKey(normalizeRecoveryCode(code))\n const stored = await api.getVault()\n if (!stored) throw new Error('This account has no encryption keys yet; there is nothing for a Recovery Code to open.')\n return (await openVault(fromBase64Url(stored.vault), wrapKey)).vaultKey\n}\n","/**\n * The eight tools an agent gets, as plain functions over an open {@link HeadlessGraph}. The MCP\n * layer (`mcp-server.ts`) only serialises these; everything an agent can and cannot do is\n * decided here, and tested here over the loopback relay without a server or a transport.\n *\n * The rules (ADR 0072, [[2026-09-15 Headless Client And MCP]]):\n *\n * - A [[Protected Document]] is listed by name and nothing else. Reading refuses; every write\n * refuses, frontmatter included. The Headless Client never holds a Protection Key, so this is\n * a courtesy to the agent rather than the guarantee - the guarantee is that there is nothing\n * here to serve.\n * - Edits are anchored: `edit_document` replaces one exact, unique occurrence and lands as one\n * `applyChange` range, which the sync engine carries as one `Y.Text` change that merges with\n * whoever is typing elsewhere in the page. There is no whole-page replace.\n * - Every write passes through the paste / import indent normaliser, so an agent that writes\n * four-space bullets lands on the grid like everyone else.\n * - Every result is bounded: pages are paged, a document read is capped and says so.\n */\n\nimport { todayISO } from '$lib/document/calendar/month-grid-core'\nimport { normaliseIndentUnit } from '$lib/document/indent-unit'\nimport { isJournalConcept } from '$lib/document/journal-concept'\nimport { documentProtection } from '$lib/document/protection/cipher-fence'\nimport { conceptKey } from '$lib/storage/fs/identity'\nimport { OPEN_TASK_STATUSES, TASK_PRIORITY_FILTERS, TASK_STATUSES, type TaskDueWindow, type TaskPriorityFilter, type TaskStatus } from '$lib/document/index-db'\nimport type { DocumentIdentity } from '$lib/storage/server/server-document-store'\n\nimport type { HeadlessGraph } from './headless-graph'\n\n/** Sizes an agent's context can absorb; the caller can page or narrow, never exceed them. */\nexport const LIST_PAGE_LIMIT = 200\nexport const SEARCH_PAGE_LIMIT = 50\nexport const TASK_PAGE_LIMIT = 200\n/** UTF-16 units of document text returned before `truncated` is set. */\nexport const READ_TEXT_CAP = 200_000\n\nexport type ToolErrorCode =\n | 'not_found'\n | 'protected_document'\n | 'already_exists'\n | 'ambiguous_match'\n | 'no_match'\n | 'invalid_argument'\n | 'not_settled'\n\n/** A refusal the agent can act on: a stable code first, a sentence second. */\nexport class ToolError extends Error {\n constructor(\n readonly code: ToolErrorCode,\n message: string,\n ) {\n super(message)\n this.name = 'ToolError'\n }\n}\n\nexport interface DocumentSummary {\n concept: string\n kind: 'page' | 'journal'\n aliases: string[]\n /** Present only when true, so an ordinary document's shape is unchanged. */\n protected?: true\n}\n\nexport interface ListDocumentsArgs {\n kind?: 'page' | 'journal'\n offset?: number\n limit?: number\n}\n\nexport interface ReadDocumentResult {\n concept: string\n kind: 'page' | 'journal'\n aliases: string[]\n text: string\n /** True when `text` was cut at {@link READ_TEXT_CAP}. */\n truncated: boolean\n}\n\nexport interface SearchArgs {\n query: string\n offset?: number\n limit?: number\n}\n\nexport interface TasksArgs {\n /** Tasks about this concept (its own line, an ancestor block, or the document), or every task. */\n concept?: string\n /** Default: every unfinished state. */\n statuses?: TaskStatus[]\n priorities?: Array<1 | 2 | 3 | 'none'>\n due?: TaskDueWindow\n offset?: number\n limit?: number\n}\n\nexport interface EditDocumentArgs {\n concept: string\n /** Exact text to replace; must occur exactly once. */\n old: string\n new: string\n}\n\nexport interface AppendDocumentArgs {\n /** A page's name, a journal day `YYYY-MM-DD`, or `today`. */\n concept: string\n text: string\n}\n\nexport interface CreatePageArgs {\n title: string\n text?: string\n}\n\n/** How `concept`, `today` and aliases resolve to one registry entry, case-insensitively. */\nfunction resolveIdentity(graph: HeadlessGraph, target: string): DocumentIdentity | null {\n const wanted = conceptKey(target === 'today' ? todayISO() : target.trim())\n for (const identity of graph.store.listIdentities()) {\n if (conceptKey(identity.concept) === wanted) return identity\n if (identity.aliases.some((alias) => conceptKey(alias) === wanted)) return identity\n }\n return null\n}\n\nfunction requireIdentity(graph: HeadlessGraph, target: string): DocumentIdentity {\n const identity = resolveIdentity(graph, target)\n if (!identity) throw new ToolError('not_found', `No document is named \"${target}\".`)\n return identity\n}\n\n/**\n * The document's current text from a live handle, after its relay catch-up has reached its\n * terminal page. A fresh process has an empty Local Cache, so a cache-seeded read answers \"\"\n * for every document until the relay has spoken; a tool must never believe that emptiness.\n */\nasync function liveText(graph: HeadlessGraph, identity: DocumentIdentity): Promise<string> {\n await graph.store.whenReady(identity.concept)\n const caughtUp = graph.sync.caughtUpDoc(identity.docId).then(() => 'ok' as const)\n const timeout = new Promise<'timeout'>((resolve) => setTimeout(() => resolve('timeout'), CATCH_UP_TIMEOUT_MS))\n await Promise.race([caughtUp, timeout])\n return graph.store.open(identity.concept).getText()\n}\n\n/** How long a read waits for the relay before answering with what it has. */\nconst CATCH_UP_TIMEOUT_MS = 15_000\n\nfunction refuseIfProtected(concept: string, text: string): void {\n if (documentProtection(text).kind === 'document') {\n throw new ToolError(\n 'protected_document',\n `\"${concept}\" is a protected document. Its content is encrypted under a key this client never holds, so it cannot be read or changed here; the user can unlock it in EtherPK.`,\n )\n }\n}\n\nfunction page<T>(items: readonly T[], offset: number, limit: number): { items: T[]; hasMore: boolean } {\n return { items: items.slice(offset, offset + limit), hasMore: items.length > offset + limit }\n}\n\nfunction bounded(value: number | undefined, fallback: number, max: number): number {\n if (value === undefined) return fallback\n if (!Number.isInteger(value) || value < 0) throw new ToolError('invalid_argument', 'offset and limit must be non-negative integers.')\n return Math.min(value, max)\n}\n\n/** After a write: push it and wait for the relay, so the agent's next read anywhere sees it. */\nasync function settle(graph: HeadlessGraph): Promise<void> {\n const result = await graph.settle()\n if (!result.settled) {\n throw new ToolError(\n 'not_settled',\n `The edit is saved locally but the Sync Server has not acknowledged it (${result.outstanding} outstanding). It will be delivered when the connection recovers.`,\n )\n }\n}\n\nexport function listDocuments(graph: HeadlessGraph, args: ListDocumentsArgs = {}) {\n const offset = bounded(args.offset, 0, Number.MAX_SAFE_INTEGER)\n const limit = bounded(args.limit, LIST_PAGE_LIMIT, LIST_PAGE_LIMIT)\n const protectedKeys = new Set(\n graph.index\n .allConcepts()\n .filter((candidate) => candidate.protected && (candidate.kind === 'page' || candidate.kind === 'journal'))\n .map((candidate) => candidate.key),\n )\n const all: DocumentSummary[] = graph.store\n .listDocuments()\n .filter((doc) => !args.kind || doc.kind === args.kind)\n .map((doc) => ({\n concept: doc.concept,\n kind: doc.kind,\n aliases: doc.aliases,\n ...(protectedKeys.has(doc.key) ? { protected: true as const } : {}),\n }))\n const paged = page(all, offset, limit)\n return { documents: paged.items, total: all.length, hasMore: paged.hasMore }\n}\n\nexport async function readDocument(graph: HeadlessGraph, concept: string): Promise<ReadDocumentResult> {\n const identity = requireIdentity(graph, concept)\n const text = await liveText(graph, identity)\n refuseIfProtected(identity.concept, text)\n return {\n concept: identity.concept,\n kind: identity.kind,\n aliases: identity.aliases,\n text: text.length > READ_TEXT_CAP ? text.slice(0, READ_TEXT_CAP) : text,\n truncated: text.length > READ_TEXT_CAP,\n }\n}\n\nexport async function search(graph: HeadlessGraph, args: SearchArgs) {\n const query = args.query.trim()\n if (query === '') throw new ToolError('invalid_argument', 'query must not be empty.')\n const offset = bounded(args.offset, 0, Number.MAX_SAFE_INTEGER)\n const limit = bounded(args.limit, 20, SEARCH_PAGE_LIMIT)\n const [result, count] = await Promise.all([\n graph.index.searchText(query, offset, limit),\n graph.index.searchTextCount(query),\n ])\n return {\n results: result.groups.map((group) => ({\n concept: group.concept,\n kind: group.kind,\n matches: group.matches,\n hits: group.hits.map((hit) => ({\n line: hit.line,\n breadcrumb: hit.breadcrumb,\n snippet: hit.snippet.map((segment) => segment.text).join(''),\n })),\n })),\n total: count.total,\n totalCapped: count.capped,\n hasMore: result.hasMore,\n }\n}\n\nexport async function backlinks(graph: HeadlessGraph, concept: string) {\n const target = concept.trim() === 'today' ? todayISO() : concept.trim()\n if (target === '') throw new ToolError('invalid_argument', 'concept must not be empty.')\n const groups = await graph.index.backlinks(target)\n return {\n concept: target,\n sources: groups.map((group) => ({\n concept: group.sourceConcept,\n kind: group.sourceKind,\n references: group.refs.map((ref) => ({\n line: ref.line,\n breadcrumb: ref.breadcrumb,\n text:\n ref.kind === 'block'\n ? ref.subtree.map((node) => node.label).join('\\n')\n : (ref.context?.text ?? ''),\n })),\n })),\n }\n}\n\nexport async function tasks(graph: HeadlessGraph, args: TasksArgs = {}) {\n const offset = bounded(args.offset, 0, Number.MAX_SAFE_INTEGER)\n const limit = bounded(args.limit, 100, TASK_PAGE_LIMIT)\n const statuses = args.statuses ?? OPEN_TASK_STATUSES\n for (const status of statuses) {\n if (!TASK_STATUSES.includes(status)) throw new ToolError('invalid_argument', `Unknown task status \"${status}\".`)\n }\n const priorities: TaskPriorityFilter[] = args.priorities\n ? args.priorities.map((priority) => (priority === 'none' ? null : priority))\n : [...TASK_PRIORITY_FILTERS]\n const result = await graph.index.tasks(\n {\n concept: args.concept?.trim() ? (args.concept.trim() === 'today' ? todayISO() : args.concept.trim()) : null,\n statuses,\n priorities,\n due: args.due ?? 'any',\n groupBy: 'document',\n today: todayISO(),\n },\n offset,\n limit,\n )\n return {\n tasks: result.hits.map((hit) => ({\n concept: hit.concept,\n kind: hit.kind,\n line: hit.line,\n text: hit.text,\n done: hit.done,\n priority: hit.priority,\n waiting: hit.waiting,\n doing: hit.doing,\n cancelled: hit.cancelled,\n due: hit.due,\n scheduled: hit.scheduled,\n completion: hit.completion,\n breadcrumb: hit.breadcrumb,\n })),\n total: result.total,\n hasMore: result.hasMore,\n }\n}\n\nexport async function editDocument(graph: HeadlessGraph, args: EditDocumentArgs) {\n if (args.old === '') throw new ToolError('invalid_argument', 'old must not be empty.')\n const identity = requireIdentity(graph, args.concept)\n const text = await liveText(graph, identity)\n refuseIfProtected(identity.concept, text)\n const first = text.indexOf(args.old)\n if (first === -1) {\n throw new ToolError('no_match', `\"${identity.concept}\" does not contain the text to replace. Read the document again; it may have changed.`)\n }\n if (text.indexOf(args.old, first + 1) !== -1) {\n throw new ToolError('ambiguous_match', `The text to replace occurs more than once in \"${identity.concept}\". Include more surrounding text so it matches exactly once.`)\n }\n // Only the replacement is normalised: the surrounding document is someone else's to keep,\n // and a normaliser pass over the whole text would be a whole-page write in disguise.\n const insert = normaliseIndentUnit(args.new)\n graph.store.open(identity.concept).applyChange({ from: first, to: first + args.old.length, insert }, 'external')\n await settle(graph)\n return { concept: identity.concept, replaced: args.old.length, inserted: insert.length }\n}\n\nexport async function appendDocument(graph: HeadlessGraph, args: AppendDocumentArgs) {\n const text = normaliseIndentUnit(args.text)\n if (text.trim() === '') throw new ToolError('invalid_argument', 'text must not be empty.')\n const target = args.concept.trim() === 'today' ? todayISO() : args.concept.trim()\n const existing = resolveIdentity(graph, target)\n if (!existing) {\n if (!isJournalConcept(target)) {\n throw new ToolError('not_found', `No document is named \"${target}\". Use create_page to start a new page.`)\n }\n // A journal entry is created by its first content, exactly as the first keystroke does.\n await graph.store.createJournal(target, text.endsWith('\\n') ? text : `${text}\\n`)\n await settle(graph)\n return { concept: target, created: true }\n }\n const current = await liveText(graph, existing)\n refuseIfProtected(existing.concept, current)\n const separator = current === '' || current.endsWith('\\n') ? '' : '\\n'\n const insert = `${separator}${text}${text.endsWith('\\n') ? '' : '\\n'}`\n graph.store.open(existing.concept).applyChange({ from: current.length, to: current.length, insert }, 'external')\n await settle(graph)\n return { concept: existing.concept, created: false }\n}\n\nexport async function createPage(graph: HeadlessGraph, args: CreatePageArgs) {\n const title = args.title.trim()\n if (title === '') throw new ToolError('invalid_argument', 'title must not be empty.')\n if (isJournalConcept(title)) {\n throw new ToolError('invalid_argument', `\"${title}\" is a calendar day, so it names a journal entry; use append_document to write to it.`)\n }\n if (resolveIdentity(graph, title)) {\n throw new ToolError('already_exists', `A document already answers to \"${title}\". Use edit_document or append_document instead.`)\n }\n // No frontmatter block: on a Server Backend identity is the encrypted registry (ADR 0024),\n // and the app seeds a new page with its body alone - a block the text carries is a mirror the\n // store writes back when the registry changes (ADR 0061), never the source of the name.\n await graph.store.createPage(title, normaliseIndentUnit(args.text ?? ''))\n await settle(graph)\n return { concept: title, created: true }\n}\n","/**\n * The MCP face of the tools: names, descriptions and argument schemas an agent reads, mapped\n * onto `tools.ts`. Nothing is decided here. A {@link ToolError} becomes an `isError` result\n * carrying its code and sentence, so the agent can act on the refusal instead of retrying it.\n */\n\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { z } from 'zod'\n\nimport type { HeadlessGraph } from './headless-graph'\nimport {\n LIST_PAGE_LIMIT,\n SEARCH_PAGE_LIMIT,\n TASK_PAGE_LIMIT,\n ToolError,\n appendDocument,\n backlinks,\n createPage,\n editDocument,\n listDocuments,\n readDocument,\n search,\n tasks,\n} from './tools'\n\nexport interface McpServerInfo {\n graphName: string\n version: string\n}\n\ntype ToolResult = { content: Array<{ type: 'text'; text: string }>; isError?: boolean }\n\nfunction ok(value: unknown): ToolResult {\n return { content: [{ type: 'text', text: JSON.stringify(value, null, 2) }] }\n}\n\nfunction failed(error: unknown): ToolResult {\n const body =\n error instanceof ToolError\n ? { error: error.code, message: error.message }\n : { error: 'internal', message: error instanceof Error ? error.message : String(error) }\n return { content: [{ type: 'text', text: JSON.stringify(body) }], isError: true }\n}\n\nasync function run(work: () => Promise<unknown> | unknown): Promise<ToolResult> {\n try {\n return ok(await work())\n } catch (error) {\n return failed(error)\n }\n}\n\nconst concept = z.string().min(1).describe('A page title, one of its aliases, a journal day as YYYY-MM-DD, or \"today\".')\nconst offset = z.number().int().nonnegative().optional().describe('Skip this many results (paging).')\n\nexport function createMcpServer(graph: HeadlessGraph, info: McpServerInfo): McpServer {\n const server = new McpServer(\n { name: 'etherpk', version: info.version },\n {\n instructions: [\n `You are connected to the EtherPK knowledge graph \"${info.graphName}\": daily journal entries and titled pages in plain markdown, linked with [[wikilinks]].`,\n 'Concept names are case-insensitive. A protected document is listed with protected: true and cannot be read or written here; tell the user to unlock it in EtherPK if a task needs it.',\n 'Bullets are \"- \" with two-space nesting; tasks are \"- [ ]\" / \"- [x]\" with task tags (#P1 #P2 #P3, #W #D #C, #D-YYYY-MM-DD due, #S-YYYY-MM-DD scheduled) right after the checkbox.',\n 'Edit with edit_document (an exact, unique old→new replacement) or append_document; read first, then edit, and never paste a whole page back.',\n 'The user documentation is at https://docs.etherpk.com.',\n ].join('\\n'),\n },\n )\n\n server.registerTool(\n 'list_documents',\n {\n title: 'List documents',\n description: `Every page and journal entry in the graph with its aliases; protected documents carry protected: true and have no readable body. Paged, at most ${LIST_PAGE_LIMIT} per call.`,\n inputSchema: {\n kind: z.enum(['page', 'journal']).optional().describe('Only pages, or only journal entries.'),\n offset,\n limit: z.number().int().positive().max(LIST_PAGE_LIMIT).optional(),\n },\n },\n async (args) => run(() => listDocuments(graph, args)),\n )\n\n server.registerTool(\n 'read_document',\n {\n title: 'Read a document',\n description: 'The full markdown of one document. Refuses a protected document with error \"protected_document\". Long documents are cut and marked truncated: true.',\n inputSchema: { concept },\n },\n async (args) => run(() => readDocument(graph, args.concept)),\n )\n\n server.registerTool(\n 'search',\n {\n title: 'Search text',\n description: `Full-text search over every unprotected document: words match by prefix, \"quoted phrases\" match exactly. Returns documents most-matches-first with the matching lines. At most ${SEARCH_PAGE_LIMIT} documents per call.`,\n inputSchema: {\n query: z.string().min(1),\n offset,\n limit: z.number().int().positive().max(SEARCH_PAGE_LIMIT).optional(),\n },\n },\n async (args) => run(() => search(graph, args)),\n )\n\n server.registerTool(\n 'backlinks',\n {\n title: 'Backlinks',\n description: 'Every document that links to a concept with [[Concept]], with the linking block or paragraph. Aliases are pooled: linking an alias counts.',\n inputSchema: { concept },\n },\n async (args) => run(() => backlinks(graph, args.concept)),\n )\n\n server.registerTool(\n 'tasks',\n {\n title: 'Tasks',\n description: `Tasks (\"- [ ]\" bullets) across the graph with their tags. Defaults to every unfinished task. A concept filter matches tasks written on that page, linking it, or nested under a block or heading that links it. At most ${TASK_PAGE_LIMIT} per call.`,\n inputSchema: {\n concept: concept.optional(),\n statuses: z.array(z.enum(['open', 'doing', 'waiting', 'done', 'cancelled'])).optional(),\n priorities: z.array(z.union([z.literal(1), z.literal(2), z.literal(3), z.literal('none')])).optional(),\n due: z.enum(['any', 'overdue', 'today', 'next7']).optional(),\n offset,\n limit: z.number().int().positive().max(TASK_PAGE_LIMIT).optional(),\n },\n },\n async (args) => run(() => tasks(graph, args)),\n )\n\n server.registerTool(\n 'edit_document',\n {\n title: 'Edit a document',\n description: 'Replace one exact occurrence of \"old\" with \"new\" in a document. \"old\" must appear exactly once (include surrounding lines to disambiguate); the change is applied as one edit so it merges with anyone typing elsewhere in the page. Refused on a protected document.',\n inputSchema: {\n concept,\n old: z.string().min(1).describe('The exact text to replace, as read_document returned it.'),\n new: z.string().describe('The replacement text. Empty deletes the old text.'),\n },\n },\n async (args) => run(() => editDocument(graph, args)),\n )\n\n server.registerTool(\n 'append_document',\n {\n title: 'Append to a document',\n description: 'Add text at the end of a page or a journal entry, on a new line. A journal day that has no entry yet is created (\"today\" is accepted). A page that does not exist is not created: use create_page.',\n inputSchema: {\n concept,\n text: z.string().min(1),\n },\n },\n async (args) => run(() => appendDocument(graph, args)),\n )\n\n server.registerTool(\n 'create_page',\n {\n title: 'Create a page',\n description: 'Create a new page with the given title and optional markdown body. Refuses a title that already names a page or an alias, or that is a calendar day (use append_document for journal entries).',\n inputSchema: {\n title: z.string().min(1),\n text: z.string().optional(),\n },\n },\n async (args) => run(() => createPage(graph, args)),\n )\n\n return server\n}\n","/**\n * `etherpk-mcp`: the [[Headless Client]]'s command line (ADR 0072).\n *\n * etherpk-mcp login --sync-server <url> [--pat <token>] [--recovery-code]\n * etherpk-mcp graphs [--sync-server <url>]\n * etherpk-mcp serve --graph <id or name> [--sync-server <url>]\n * etherpk-mcp logout [--sync-server <url> | --all]\n *\n * One config file holds a login per Sync Server (ADR 0075). `--sync-server` names the one a\n * command means and may be left out while only one is signed in.\n *\n * `serve` speaks MCP over stdio, so everything for the human goes to stderr; stdout belongs\n * to the agent. `login` and `graphs` are interactive and print to stdout.\n */\n\nimport { createInterface } from 'node:readline/promises'\nimport { hostname } from 'node:os'\nimport { parseArgs } from 'node:util'\nimport { unlink } from 'node:fs/promises'\n\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'\n\n// Inlined by Vite at build time, so the bundle carries the version and needs no file at runtime.\nimport pkg from '../package.json'\n\nimport { fromBase64Url, toBase64Url } from '$lib/crypto'\n\nimport { connectAccount, openAccountVault, resolveGraphById } from './account'\nimport {\n defaultConfigPath,\n emptyConfig,\n listLogins,\n normaliseSyncServer,\n readConfig,\n selectServer,\n writeConfig,\n type HeadlessConfig,\n type ServerCredentials,\n type ServerSelection,\n} from './config'\nimport { readGraphName } from './graph-names'\nimport { openHeadlessGraph } from './headless-graph'\nimport { ApprovalAbandoned, unlockByDeviceApproval, unlockByRecoveryCode } from './login'\nimport { createMcpServer } from './mcp-server'\nimport { graphCacheDir, removeCacheRoot, removeServerCache } from './persistence'\n\nconst VERSION: string = pkg.version\n\n/**\n * How the user invokes this program, so every hint is one they can paste. Through npx - the way\n * the Agents tab, the README and the docs all say - the bin is never on their PATH, and an npx\n * run executes out of npm's `_npx` cache; a global install (`npm install -g`) runs from\n * anywhere else and does have `etherpk-mcp` on the PATH, so it gets the short spelling.\n */\nconst CMD = /[\\\\/]_npx[\\\\/]/.test(process.argv[1] ?? '') ? 'npx @appsoftwareltd/etherpk-mcp' : 'etherpk-mcp'\n\nconst USAGE = `etherpk-mcp - EtherPK Headless Client (an MCP server over one synced graph)\n\n ${CMD} login --sync-server <url> [--pat <token>] [--recovery-code]\n Sign this machine in as a device of your account. Prompts for a Personal Access\n Token (an account-wide one, from the Sync Server portal at <url>/account/tokens)\n unless --pat or ETHERPK_PAT is given, then unlocks your keys by Device Approval:\n open EtherPK in a browser signed in to the account with its graphs unlocked and\n confirm the code shown. Press r while waiting, or pass --recovery-code, to type\n your Recovery Code instead (or ETHERPK_RECOVERY_CODE, for a scripted setup).\n ${CMD} graphs [--sync-server <url>]\n List the synced graphs each signed-in account can reach, by name and id.\n ${CMD} serve --graph <id or name> [--sync-server <url>]\n Serve one graph to an agent over stdio. For Claude Code:\n claude mcp add etherpk -- npx @appsoftwareltd/etherpk-mcp serve --sync-server <url> --graph <id>\n ${CMD} logout [--sync-server <url> | --all]\n Forget that server's token, keys and cached graphs on this machine.\n\nThis machine can be signed in to several Sync Servers at once (a self-hosted one beside the\nmanaged service, say); --sync-server says which one a command means, and can be left out\nwhile only one is signed in. The config file is ${defaultConfigPath()} (override with\nETHERPK_MCP_CONFIG); cached graphs live under ~/.cache/etherpk/mcp (override with\nETHERPK_MCP_CACHE_DIR).\nDocs: https://docs.etherpk.com/using-ai-agents-with-your-notes\n`\n\nfunction fail(message: string): never {\n console.error(message)\n process.exit(1)\n}\n\nasync function ask(question: string, { secret = false } = {}): Promise<string> {\n if (!process.stdin.isTTY) fail(`${question} - no terminal to ask on; pass it as an option.`)\n if (!secret) {\n const rl = createInterface({ input: process.stdin, output: process.stderr, terminal: true })\n try {\n return (await rl.question(question)).trim()\n } finally {\n rl.close()\n }\n }\n // A secret is read in raw mode so the terminal never echoes it: readline has no echo-off,\n // and a token or Recovery Code in scrollback is exactly what this command exists to avoid.\n process.stderr.write(question)\n return new Promise<string>((resolve, reject) => {\n const stdin = process.stdin\n let typed = ''\n const finish = (outcome: () => void) => {\n stdin.setRawMode(false)\n stdin.pause()\n stdin.off('data', onData)\n process.stderr.write('\\n')\n outcome()\n }\n const onData = (chunk: Buffer) => {\n for (const char of chunk.toString('utf8')) {\n if (char === '\\u0003') return finish(() => reject(new Error('Cancelled.')))\n if (char === '\\r' || char === '\\n') return finish(() => resolve(typed.trim()))\n if (char === '\\u007f' || char === '\\b') typed = typed.slice(0, -1)\n else typed += char\n }\n }\n stdin.setRawMode(true)\n stdin.resume()\n stdin.on('data', onData)\n })\n}\n\nasync function loadConfig(path: string): Promise<HeadlessConfig> {\n const config = await readConfig(path)\n if (!config) fail(`${path} is not a config file this version understands. Run: ${CMD} login --sync-server <url> (it will be rewritten; nothing else is affected).`)\n return config\n}\n\n/** The login a command means, or the reason there is none - in words the user can act on. */\nfunction requireServer(config: HeadlessConfig, wanted: string | undefined): ServerCredentials {\n const selection: ServerSelection = selectServer(config, wanted)\n if (selection.ok) return selection.credentials\n switch (selection.reason) {\n case 'none':\n return fail(`Not logged in on this machine. Run: ${CMD} login --sync-server <url>`)\n case 'unknown':\n return fail(`Not logged in to ${selection.syncServer}. Signed in to: ${selection.known.join(', ') || '(none)'}. Run: ${CMD} login --sync-server ${selection.syncServer}`)\n case 'ambiguous':\n return fail(`Signed in to more than one Sync Server here: ${selection.known.join(', ')}. Say which with --sync-server <url>.`)\n }\n}\n\nasync function login(args: { 'sync-server'?: string; pat?: string; 'recovery-code'?: boolean }): Promise<void> {\n const path = defaultConfigPath()\n // A file this version cannot read is replaced, not refused: the old shape held one login,\n // and the user is about to make one.\n const config = (await readConfig(path)) ?? emptyConfig()\n const known = Object.keys(config.servers)\n const syncServer = normaliseSyncServer(args['sync-server'] ?? (known.length === 1 ? known[0] : await ask('Sync Server URL: ')))\n if (!/^https?:\\/\\//.test(syncServer)) fail('The Sync Server must be an http(s) URL.')\n const pat = args.pat ?? process.env.ETHERPK_PAT ?? (await ask(`Personal Access Token (account-wide, from ${syncServer}/account/tokens): `, { secret: true }))\n if (!pat) fail('A Personal Access Token is required.')\n\n const account = await connectAccount({ syncServer, pat })\n console.log(`Signed in to ${syncServer} as ${account.principal.email ?? account.principal.name ?? account.principal.id}.`)\n\n // ETHERPK_RECOVERY_CODE serves a box set up by a script, where there is no terminal to type\n // into and no tab to approve from; the variable is read once and never written anywhere.\n const byRecoveryCode = async () =>\n unlockByRecoveryCode(account.api, process.env.ETHERPK_RECOVERY_CODE ?? (await ask('Recovery Code: ', { secret: true })))\n const vaultKey = args['recovery-code'] ? await byRecoveryCode() : await approveOrFallBack(account, byRecoveryCode)\n config.servers[syncServer] = { pat, vaultKey: toBase64Url(vaultKey) }\n await writeConfig(path, config)\n console.log(`Keys unlocked and cached in ${path} (owner-only). Anyone who can read your files on this machine can read this account, as with a signed-in browser.`)\n const others = Object.keys(config.servers).filter((server) => server !== syncServer)\n if (others.length > 0) console.log(`Also signed in to ${others.join(', ')}; commands now need --sync-server <url> to say which.`)\n\n await listGraphs({ syncServer, pat, vaultKey: toBase64Url(vaultKey) }, others.length > 0)\n}\n\n/**\n * Device Approval, with the Recovery Code one keypress away: a user who has no unlocked EtherPK\n * to hand should not have to Ctrl-C and re-read the help to find `--recovery-code`. On a\n * terminal, `r` during the wait abandons the approval (cancelled server-side) and asks for the\n * code instead; without a terminal the wait runs to its outcome.\n */\nasync function approveOrFallBack(\n account: Awaited<ReturnType<typeof connectAccount>>,\n byRecoveryCode: () => Promise<Uint8Array>,\n): Promise<Uint8Array> {\n const abort = new AbortController()\n const io = {\n say: (line: string) => console.log(line),\n sleep: (ms: number) => new Promise<void>((r) => setTimeout(r, ms)),\n signal: abort.signal,\n clientUrl: account.clientUrl,\n }\n const stdin = process.stdin\n const interactive = stdin.isTTY === true\n const onKey = (chunk: Buffer) => {\n const key = chunk.toString('utf8')\n if (key === 'r' || key === 'R') abort.abort()\n if (key === '\\u0003') {\n console.log('')\n process.exit(130)\n }\n }\n if (interactive) {\n console.log('(Press r to type your Recovery Code instead.)')\n stdin.setRawMode(true)\n stdin.resume()\n stdin.on('data', onKey)\n }\n // The key listener comes off BEFORE the Recovery Code prompt runs: that prompt takes the\n // terminal into raw mode itself, and a finally that reset it afterwards ate the code.\n let abandoned = false\n try {\n return await unlockByDeviceApproval(account.api, io)\n } catch (error) {\n if (!(error instanceof ApprovalAbandoned)) throw error\n abandoned = true\n } finally {\n if (interactive) {\n stdin.off('data', onKey)\n stdin.setRawMode(false)\n stdin.pause()\n }\n }\n if (!abandoned) throw new Error('unreachable')\n console.log('Approval cancelled; unlocking with your Recovery Code instead.')\n return byRecoveryCode()\n}\n\n/**\n * `graphs` with a server named lists that server; unnamed, it lists every signed-in server in\n * turn, because \"what can the agent reach from here\" is the question and it has one answer\n * per login.\n */\nasync function graphsCommand(args: { 'sync-server'?: string }): Promise<void> {\n const config = await loadConfig(defaultConfigPath())\n const logins = args['sync-server'] ? [requireServer(config, args['sync-server'])] : listLogins(config)\n if (logins.length === 0) fail(`Not logged in on this machine. Run: ${CMD} login --sync-server <url>`)\n for (const login of logins) await listGraphs(login, logins.length > 1)\n}\n\nasync function listGraphs(login: ServerCredentials, several: boolean): Promise<void> {\n if (!login.vaultKey) fail(`Keys are not unlocked on this machine for ${login.syncServer}. Run: ${CMD} login --sync-server ${login.syncServer}`)\n const account = await connectAccount(login)\n const vault = await openAccountVault(account.api, fromBase64Url(login.vaultKey))\n const graphs = await account.api.listGraphs()\n const serverFlag = several ? ` --sync-server ${login.syncServer}` : ''\n if (graphs.length === 0) {\n console.log(`No synced graphs are reachable with the token for ${login.syncServer}.`)\n return\n }\n console.log(several ? `Synced graphs on ${login.syncServer}:` : 'Synced graphs:')\n for (const record of graphs) {\n const keyring = vault.keyrings.find((entry) => entry.graphId === record.id)\n const name = keyring\n ? await readGraphName({ graphId: record.id, rootDocId: record.rootDocId, keyring, relayUrl: account.relayUrl, token: account.tokenFor(record.id) })\n : null\n const label = keyring ? (name ?? '(unnamed)') : '(no key on this account yet - open it in EtherPK first)'\n console.log(` ${record.id} ${label} [${record.role}]`)\n }\n console.log('')\n console.log(`Serve one to an agent with: ${CMD} serve${serverFlag} --graph <id>`)\n console.log(`For Claude Code: claude mcp add etherpk -- ${CMD} serve${serverFlag} --graph <id>`)\n}\n\nasync function serve(args: { graph?: string; 'sync-server'?: string }): Promise<void> {\n const wanted = args.graph?.trim()\n if (!wanted) fail('serve needs --graph <id or name>.')\n const login = requireServer(await loadConfig(defaultConfigPath()), args['sync-server'])\n if (!login.vaultKey) fail(`Keys are not unlocked on this machine for ${login.syncServer}. Run: ${CMD} login --sync-server ${login.syncServer}`)\n const account = await connectAccount(login)\n const vault = await openAccountVault(account.api, fromBase64Url(login.vaultKey))\n const graphs = await account.api.listGraphs()\n\n // By id first; else by name, which means reading every reachable graph's name.\n let graphId = graphs.find((graph) => graph.id === wanted)?.id\n let graphName: string | null = null\n if (!graphId) {\n for (const record of graphs) {\n const keyring = vault.keyrings.find((entry) => entry.graphId === record.id)\n if (!keyring) continue\n const name = await readGraphName({ graphId: record.id, rootDocId: record.rootDocId, keyring, relayUrl: account.relayUrl, token: account.tokenFor(record.id) })\n if (name && name.toLowerCase() === wanted.toLowerCase()) {\n graphId = record.id\n graphName = name\n break\n }\n }\n }\n if (!graphId) fail(`No synced graph on ${login.syncServer} is named or identified by \"${wanted}\". Run: ${CMD} graphs`)\n const { record, keyring } = resolveGraphById(graphs, vault, graphId)\n\n console.error(`etherpk-mcp: opening graph ${graphId} on ${account.serverBaseUrl}…`)\n const graph = await openHeadlessGraph({\n graphId,\n rootDocId: record.rootDocId,\n keyring,\n relayUrl: account.relayUrl,\n token: account.tokenFor(graphId),\n presenceName: `Agent on ${hostname()}`,\n readyTimeoutMs: 20_000,\n // The cache and index survive between launches, so a restart catches up rather than\n // rebuilding (persistence.ts); logout removes them with the keys.\n persistDir: graphCacheDir(process.env, account.serverBaseUrl, graphId),\n onError: (error) => console.error(`etherpk-mcp: ${error.message}`),\n })\n graphName ??= graph.sync.getMeta().name ?? graphId\n const server = createMcpServer(graph, { graphName, version: VERSION })\n const transport = new StdioServerTransport()\n const shutdown = async () => {\n await graph.settle().catch(() => {})\n await graph.dispose().catch(() => {})\n process.exit(0)\n }\n process.on('SIGINT', () => void shutdown())\n process.on('SIGTERM', () => void shutdown())\n transport.onclose = () => void shutdown()\n await server.connect(transport)\n console.error(`etherpk-mcp: serving \"${graphName}\" over stdio as \"Agent on ${hostname()}\".`)\n}\n\nasync function logout(args: { 'sync-server'?: string; all?: boolean }): Promise<void> {\n const path = defaultConfigPath()\n const config = await readConfig(path)\n // Everything, or a file this version cannot read: remove the file and every cached graph.\n // The cached graphs are plaintext; they go with the keys that made them readable.\n if (args.all || !config) {\n await unlink(path).catch((error: NodeJS.ErrnoException) => {\n if (error.code !== 'ENOENT') throw error\n })\n await removeCacheRoot(process.env)\n console.log(`Forgot every token and key in ${path} and the cached graphs. Revoke the Personal Access Tokens in each Sync Server portal too if this machine is not yours to keep.`)\n return\n }\n const login = requireServer(config, args['sync-server'])\n delete config.servers[login.syncServer]\n await removeServerCache(process.env, login.syncServer)\n if (Object.keys(config.servers).length === 0) {\n await unlink(path).catch((error: NodeJS.ErrnoException) => {\n if (error.code !== 'ENOENT') throw error\n })\n await removeCacheRoot(process.env)\n } else {\n await writeConfig(path, config)\n }\n console.log(`Forgot the token, keys and cached graphs for ${login.syncServer}. Revoke the Personal Access Token in its portal too if this machine is not yours to keep.`)\n}\n\nasync function main(): Promise<void> {\n const { values, positionals } = parseArgs({\n args: process.argv.slice(2),\n allowPositionals: true,\n options: {\n 'sync-server': { type: 'string' },\n pat: { type: 'string' },\n 'recovery-code': { type: 'boolean' },\n graph: { type: 'string' },\n all: { type: 'boolean' },\n help: { type: 'boolean', short: 'h' },\n version: { type: 'boolean', short: 'v' },\n },\n })\n if (values.version) {\n console.log(VERSION)\n return\n }\n const command = positionals[0]\n if (values.help || !command) {\n console.log(USAGE)\n return\n }\n switch (command) {\n case 'login':\n return login(values)\n case 'graphs':\n return graphsCommand(values)\n case 'serve':\n return serve(values)\n case 'logout':\n return logout(values)\n default:\n fail(`Unknown command \"${command}\".\\n\\n${USAGE}`)\n }\n}\n\nmain().catch((error: unknown) => {\n fail(`etherpk-mcp: ${error instanceof Error ? error.message : String(error)}`)\n})\n"],"mappings":"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"}