@korajs/cli 0.6.0 → 1.0.0-beta.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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/dist/bin.cjs +2582 -202
  3. package/dist/bin.cjs.map +1 -1
  4. package/dist/bin.js +152 -96
  5. package/dist/bin.js.map +1 -1
  6. package/dist/chunk-5NI2FEQL.js +92 -0
  7. package/dist/chunk-5NI2FEQL.js.map +1 -0
  8. package/dist/chunk-6C4BHSRA.js +82 -0
  9. package/dist/chunk-6C4BHSRA.js.map +1 -0
  10. package/dist/{chunk-VLTPEATY.js → chunk-B5YS4STN.js} +9 -96
  11. package/dist/chunk-B5YS4STN.js.map +1 -0
  12. package/dist/{chunk-EEZNRI5W.js → chunk-BWTKRKNJ.js} +13 -5
  13. package/dist/{chunk-EEZNRI5W.js.map → chunk-BWTKRKNJ.js.map} +1 -1
  14. package/dist/{chunk-Q2FBCOQD.js → chunk-EOWLAAIV.js} +5 -3
  15. package/dist/{chunk-Q2FBCOQD.js.map → chunk-EOWLAAIV.js.map} +1 -1
  16. package/dist/create.cjs +8 -2
  17. package/dist/create.cjs.map +1 -1
  18. package/dist/create.js +3 -2
  19. package/dist/create.js.map +1 -1
  20. package/dist/index.js +3 -2
  21. package/dist/lab-manager-KUDII6BT.js +280 -0
  22. package/dist/lab-manager-KUDII6BT.js.map +1 -0
  23. package/dist/schema-loader-GGHECMTM.js +9 -0
  24. package/dist/schema-loader-GGHECMTM.js.map +1 -0
  25. package/dist/spectator-manager-E2LH2P54.js +142 -0
  26. package/dist/spectator-manager-E2LH2P54.js.map +1 -0
  27. package/dist/studio-server-NIFKZI4F.js +1738 -0
  28. package/dist/studio-server-NIFKZI4F.js.map +1 -0
  29. package/package.json +8 -6
  30. package/templates/react-basic/AGENTS.md +87 -0
  31. package/templates/react-sync/AGENTS.md +87 -0
  32. package/templates/react-sync/src/App.tsx +8 -0
  33. package/templates/react-sync/src/index.css +11 -0
  34. package/templates/react-tailwind/AGENTS.md +87 -0
  35. package/templates/react-tailwind-sync/AGENTS.md +87 -0
  36. package/templates/react-tailwind-sync/src/App.tsx +28 -0
  37. package/templates/svelte-basic/AGENTS.md +76 -0
  38. package/templates/svelte-basic/src/App.svelte +27 -31
  39. package/templates/svelte-basic/src/Root.svelte +4 -4
  40. package/templates/svelte-basic/src/modules/todos/useTodos.ts +1 -1
  41. package/templates/svelte-basic/vite.config.ts +1 -1
  42. package/templates/svelte-sync/AGENTS.md +76 -0
  43. package/templates/svelte-sync/src/App.svelte +31 -35
  44. package/templates/svelte-sync/src/Root.svelte +6 -6
  45. package/templates/svelte-sync/src/modules/todos/useTodos.ts +1 -1
  46. package/templates/svelte-tailwind/AGENTS.md +76 -0
  47. package/templates/svelte-tailwind/src/App.svelte +17 -21
  48. package/templates/svelte-tailwind/src/Root.svelte +4 -4
  49. package/templates/svelte-tailwind/src/modules/todos/useTodos.ts +1 -1
  50. package/templates/svelte-tailwind/vite.config.ts +2 -2
  51. package/templates/svelte-tailwind-sync/AGENTS.md +76 -0
  52. package/templates/svelte-tailwind-sync/src/App.svelte +52 -56
  53. package/templates/svelte-tailwind-sync/src/Root.svelte +6 -6
  54. package/templates/svelte-tailwind-sync/src/modules/todos/useTodos.ts +1 -1
  55. package/templates/svelte-tailwind-sync/vite.config.ts +1 -1
  56. package/templates/tauri-react/AGENTS.md +91 -0
  57. package/templates/vue-basic/AGENTS.md +84 -0
  58. package/templates/vue-basic/src/modules/todos/useTodos.ts +1 -1
  59. package/templates/vue-sync/AGENTS.md +84 -0
  60. package/templates/vue-sync/src/main.ts +5 -1
  61. package/templates/vue-sync/src/modules/todos/useTodos.ts +1 -1
  62. package/templates/vue-tailwind/AGENTS.md +84 -0
  63. package/templates/vue-tailwind/src/App.vue +1 -8
  64. package/templates/vue-tailwind/src/modules/todos/useTodos.ts +1 -1
  65. package/templates/vue-tailwind-sync/AGENTS.md +84 -0
  66. package/templates/vue-tailwind-sync/src/modules/todos/useTodos.ts +1 -1
  67. package/dist/chunk-VLTPEATY.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/studio/studio-server.ts","../src/commands/studio/db-reader.ts","../src/commands/studio/studio-replay.ts","../src/commands/studio/studio-ui.ts","../src/commands/studio/studio-ui-app.ts","../src/commands/studio/studio-ui-css.ts"],"sourcesContent":["import type { IncomingMessage, Server, ServerResponse } from 'node:http'\nimport { createServer } from 'node:http'\nimport { StudioDbReader } from './db-reader'\nimport type { LabManager } from './lab-manager'\nimport { buildCausalDag, replayToOperation } from './studio-replay'\nimport { STUDIO_HTML } from './studio-ui'\nimport { STUDIO_APP_JS } from './studio-ui-app'\nimport { STUDIO_CSS } from './studio-ui-css'\n\n/**\n * Kora Studio HTTP server.\n *\n * Two modes share one inspection surface:\n * - FILE mode: read-only against a user database. Strictly GET.\n * - LAB mode: an in-process multi-device sync laboratory. Lab routes accept\n * POST, but only ever touch the Lab's throwaway databases — a user database\n * opened by Studio is never writable, in any mode.\n *\n * Live updates flow over Server-Sent Events: file mode polls SQLite's\n * data_version (cheap, cross-connection), lab mode pushes real device events.\n */\nexport interface StudioServerOptions {\n\tport: number\n\thost?: string\n\t/** File mode: path of the database to inspect. */\n\tdbPath?: string\n\t/** Lab mode: the running lab. */\n\tlab?: LabManager\n\t/** Spectator mode: live read-only replica of a production sync server. */\n\tspectator?: import('./spectator-manager').SpectatorManager\n}\n\nexport interface StudioServer {\n\tserver: Server\n\tport: number\n\turl: string\n\tclose(): Promise<void>\n}\n\ninterface StudioContext {\n\tmode: 'file' | 'lab' | 'spectator'\n\tlab: LabManager | null\n\tspectator: import('./spectator-manager').SpectatorManager | null\n\tmainReader: StudioDbReader | null\n\t/** Lazily-opened read-only readers for lab device DBs. */\n\tdeviceReaders: Map<string, StudioDbReader>\n}\n\nconst SSE_POLL_MS = 700\n\nexport async function startStudioServer(options: StudioServerOptions): Promise<StudioServer> {\n\tconst host = options.host ?? '127.0.0.1'\n\t// The spectator's replica is inspected exactly like a file-mode database.\n\tconst dbPath = options.spectator ? options.spectator.dbPath : options.dbPath\n\tconst context: StudioContext = {\n\t\tmode: options.spectator ? 'spectator' : options.lab ? 'lab' : 'file',\n\t\tlab: options.lab ?? null,\n\t\tspectator: options.spectator ?? null,\n\t\tmainReader: dbPath ? await StudioDbReader.open(dbPath) : null,\n\t\tdeviceReaders: new Map(),\n\t}\n\n\tconst sseClients = new Set<ServerResponse>()\n\tconst broadcast = (event: string, data: unknown): void => {\n\t\tconst payload = `event: ${event}\\ndata: ${JSON.stringify(data)}\\n\\n`\n\t\tfor (const client of sseClients) {\n\t\t\tclient.write(payload)\n\t\t}\n\t}\n\n\t// Spectator mode: forward live client events (op applied, sync, merges).\n\tlet unsubscribeSpectator: (() => void) | null = null\n\tif (context.spectator) {\n\t\tunsubscribeSpectator = context.spectator.onEvent((event) => {\n\t\t\tbroadcast('spectator', event)\n\t\t\tbroadcast('change', { at: event.at })\n\t\t})\n\t}\n\n\t// File mode: poll the DB's data_version and broadcast change ticks.\n\tlet pollTimer: ReturnType<typeof setInterval> | null = null\n\tif (context.mode === 'file' && context.mainReader) {\n\t\tlet lastFingerprint = context.mainReader.fingerprint()\n\t\tpollTimer = setInterval(() => {\n\t\t\ttry {\n\t\t\t\tconst current = context.mainReader?.fingerprint()\n\t\t\t\tif (current !== undefined && current !== lastFingerprint) {\n\t\t\t\t\tlastFingerprint = current\n\t\t\t\t\tbroadcast('change', { at: Date.now() })\n\t\t\t\t}\n\t\t\t} catch {\n\t\t\t\t// DB temporarily busy — next tick will retry.\n\t\t\t}\n\t\t}, SSE_POLL_MS)\n\t}\n\n\t// Lab mode: forward live device events.\n\tlet unsubscribeLab: (() => void) | null = null\n\tif (context.lab) {\n\t\tunsubscribeLab = context.lab.onEvent((event) => {\n\t\t\tbroadcast('lab', event)\n\t\t\tbroadcast('change', { at: event.at })\n\t\t})\n\t}\n\n\tconst server = createServer((req, res) => {\n\t\thandleRequest(context, sseClients, req, res).catch((error: unknown) => {\n\t\t\tconst message = error instanceof Error ? error.message : 'Internal error'\n\t\t\tif (!res.headersSent) {\n\t\t\t\tsendJson(res, 500, { error: message })\n\t\t\t} else {\n\t\t\t\tres.end()\n\t\t\t}\n\t\t})\n\t})\n\n\tawait new Promise<void>((resolve, reject) => {\n\t\tserver.once('error', reject)\n\t\tserver.listen(options.port, host, () => {\n\t\t\tserver.removeListener('error', reject)\n\t\t\tresolve()\n\t\t})\n\t})\n\n\tconst address = server.address()\n\tconst port = typeof address === 'object' && address !== null ? address.port : options.port\n\n\treturn {\n\t\tserver,\n\t\tport,\n\t\turl: `http://${host}:${port}`,\n\t\tclose: async () => {\n\t\t\tif (pollTimer) {\n\t\t\t\tclearInterval(pollTimer)\n\t\t\t}\n\t\t\tunsubscribeSpectator?.()\n\t\t\tunsubscribeLab?.()\n\t\t\tfor (const client of sseClients) {\n\t\t\t\tclient.end()\n\t\t\t}\n\t\t\tsseClients.clear()\n\t\t\tcontext.mainReader?.close()\n\t\t\tfor (const [, reader] of context.deviceReaders) {\n\t\t\t\treader.close()\n\t\t\t}\n\t\t\tcontext.deviceReaders.clear()\n\t\t\tawait new Promise<void>((resolve, reject) => {\n\t\t\t\tserver.close((error) => (error ? reject(error) : resolve()))\n\t\t\t})\n\t\t},\n\t}\n}\n\nasync function resolveReader(context: StudioContext, url: URL): Promise<StudioDbReader> {\n\tconst device = url.searchParams.get('device')\n\tif (device && context.lab) {\n\t\tconst cached = context.deviceReaders.get(device)\n\t\tif (cached) {\n\t\t\treturn cached\n\t\t}\n\t\tconst state = context.lab.deviceState(device)\n\t\tconst reader = await StudioDbReader.open(state.dbPath)\n\t\tcontext.deviceReaders.set(device, reader)\n\t\treturn reader\n\t}\n\tif (context.mainReader) {\n\t\treturn context.mainReader\n\t}\n\t// Lab mode with no device specified: default to the first device.\n\tif (context.lab) {\n\t\tconst first = context.lab.listDevices()[0]\n\t\tif (first) {\n\t\t\treturn resolveReader(context, new URL(`${url.origin}${url.pathname}?device=${first.name}`))\n\t\t}\n\t}\n\tthrow new Error('No database available')\n}\n\nasync function handleRequest(\n\tcontext: StudioContext,\n\tsseClients: Set<ServerResponse>,\n\treq: IncomingMessage,\n\tres: ServerResponse,\n): Promise<void> {\n\tconst url = new URL(req.url ?? '/', 'http://studio.local')\n\tconst path = url.pathname\n\tconst method = req.method ?? 'GET'\n\n\t// ── Static assets ──\n\tif (method === 'GET' && (path === '/' || path === '/index.html')) {\n\t\tres.writeHead(200, { 'content-type': 'text/html; charset=utf-8' })\n\t\tres.end(STUDIO_HTML)\n\t\treturn\n\t}\n\tif (method === 'GET' && path === '/app.js') {\n\t\tres.writeHead(200, { 'content-type': 'text/javascript; charset=utf-8' })\n\t\tres.end(STUDIO_APP_JS)\n\t\treturn\n\t}\n\tif (method === 'GET' && path === '/style.css') {\n\t\tres.writeHead(200, { 'content-type': 'text/css; charset=utf-8' })\n\t\tres.end(STUDIO_CSS)\n\t\treturn\n\t}\n\n\t// ── Live updates (SSE) ──\n\tif (method === 'GET' && path === '/api/events') {\n\t\tres.writeHead(200, {\n\t\t\t'content-type': 'text/event-stream',\n\t\t\t'cache-control': 'no-store',\n\t\t\tconnection: 'keep-alive',\n\t\t})\n\t\tres.write(`event: hello\\ndata: ${JSON.stringify({ mode: context.mode })}\\n\\n`)\n\t\tif (context.lab) {\n\t\t\tfor (const event of context.lab.recentEvents().slice(-100)) {\n\t\t\t\tres.write(`event: lab\\ndata: ${JSON.stringify(event)}\\n\\n`)\n\t\t\t}\n\t\t}\n\t\tif (context.spectator) {\n\t\t\tfor (const event of context.spectator.recentEvents().slice(-100)) {\n\t\t\t\tres.write(`event: spectator\\ndata: ${JSON.stringify(event)}\\n\\n`)\n\t\t\t}\n\t\t}\n\t\tsseClients.add(res)\n\t\treq.on('close', () => {\n\t\t\tsseClients.delete(res)\n\t\t})\n\t\treturn\n\t}\n\n\t// ── Lab routes (POST allowed; lab DBs are throwaway) ──\n\tif (path.startsWith('/api/lab')) {\n\t\tif (!context.lab) {\n\t\t\tsendJson(res, 404, {\n\t\t\t\terror: 'Studio is not running in lab mode. Start with: kora studio --lab',\n\t\t\t})\n\t\t\treturn\n\t\t}\n\t\tawait handleLabRoute(context, context.lab, req, res, url)\n\t\treturn\n\t}\n\n\t// ── Everything else is strictly read-only ──\n\tif (method !== 'GET') {\n\t\tsendJson(res, 405, { error: 'Studio data routes are read-only: GET only.' })\n\t\treturn\n\t}\n\n\tif (path === '/api/mode') {\n\t\tsendJson(res, 200, {\n\t\t\tmode: context.mode,\n\t\t\tdevices: context.lab?.listDevices().map((d) => d.name) ?? [],\n\t\t\t...(context.spectator ? { spectator: context.spectator.status() } : {}),\n\t\t})\n\t\treturn\n\t}\n\n\tif (path === '/api/spectator/status') {\n\t\tif (!context.spectator) {\n\t\t\tsendJson(res, 404, { error: 'Not in spectator mode' })\n\t\t\treturn\n\t\t}\n\t\tsendJson(res, 200, context.spectator.status())\n\t\treturn\n\t}\n\n\tif (path === '/api/overview') {\n\t\tconst reader = await resolveReader(context, url)\n\t\tsendJson(res, 200, reader.overview())\n\t\treturn\n\t}\n\n\tif (path === '/api/audit') {\n\t\tconst reader = await resolveReader(context, url)\n\t\tsendJson(res, 200, { traces: reader.auditTraces(intParam(url, 'limit') ?? 100) })\n\t\treturn\n\t}\n\n\tconst collectionMatch = path.match(\n\t\t/^\\/api\\/collections\\/([a-zA-Z0-9_]+)\\/(records|ops|replay|dag)(?:\\/([^/]+))?(?:\\/(ops))?$/,\n\t)\n\tif (collectionMatch) {\n\t\tconst [, collection, kind, recordId, sub] = collectionMatch\n\t\tif (!collection || !kind) {\n\t\t\tsendJson(res, 400, { error: 'Bad request' })\n\t\t\treturn\n\t\t}\n\t\tconst reader = await resolveReader(context, url)\n\t\tif (!reader.listCollections().includes(collection)) {\n\t\t\tsendJson(res, 404, { error: `Unknown collection \"${collection}\"` })\n\t\t\treturn\n\t\t}\n\n\t\tif (kind === 'replay') {\n\t\t\tconst ops = reader.allOperations(collection)\n\t\t\tconst upTo = url.searchParams.get('upTo')\n\t\t\tsendJson(res, 200, replayToOperation(ops, upTo))\n\t\t\treturn\n\t\t}\n\n\t\tif (kind === 'dag') {\n\t\t\tconst record = url.searchParams.get('record')\n\t\t\tconst limit = intParam(url, 'limit') ?? 200\n\t\t\tlet ops = reader.allOperations(collection)\n\t\t\tif (record) {\n\t\t\t\tops = ops.filter((o) => o.recordId === record)\n\t\t\t} else {\n\t\t\t\tops = ops.slice(-limit)\n\t\t\t}\n\t\t\tsendJson(res, 200, buildCausalDag(ops))\n\t\t\treturn\n\t\t}\n\n\t\tif (kind === 'ops') {\n\t\t\tsendJson(\n\t\t\t\tres,\n\t\t\t\t200,\n\t\t\t\treader.operations(collection, {\n\t\t\t\t\tlimit: intParam(url, 'limit'),\n\t\t\t\t\toffset: intParam(url, 'offset'),\n\t\t\t\t}),\n\t\t\t)\n\t\t\treturn\n\t\t}\n\n\t\tif (recordId && sub === 'ops') {\n\t\t\tsendJson(res, 200, { operations: reader.recordOperations(collection, recordId) })\n\t\t\treturn\n\t\t}\n\n\t\tif (recordId) {\n\t\t\tconst record = reader.record(collection, recordId)\n\t\t\tif (!record) {\n\t\t\t\tsendJson(res, 404, { error: `Record \"${recordId}\" not found` })\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst richtextPreviews = await decodeRichtextFields(\n\t\t\t\treader,\n\t\t\t\tcollection,\n\t\t\t\trecordId,\n\t\t\t\trecord.fields,\n\t\t\t)\n\t\t\tsendJson(res, 200, {\n\t\t\t\trecord,\n\t\t\t\trichtextPreviews,\n\t\t\t\toperations: reader.recordOperations(collection, recordId),\n\t\t\t})\n\t\t\treturn\n\t\t}\n\n\t\tsendJson(\n\t\t\tres,\n\t\t\t200,\n\t\t\treader.records(collection, {\n\t\t\t\tlimit: intParam(url, 'limit'),\n\t\t\t\toffset: intParam(url, 'offset'),\n\t\t\t\tincludeDeleted: url.searchParams.get('includeDeleted') === 'true',\n\t\t\t\tsearch: url.searchParams.get('search') ?? undefined,\n\t\t\t}),\n\t\t)\n\t\treturn\n\t}\n\n\tsendJson(res, 404, { error: `No route for ${path}` })\n}\n\n// ── Lab routes ───────────────────────────────────────────────────────────────\n\nasync function handleLabRoute(\n\tcontext: StudioContext,\n\tlab: LabManager,\n\treq: IncomingMessage,\n\tres: ServerResponse,\n\turl: URL,\n): Promise<void> {\n\tconst path = url.pathname\n\tconst method = req.method ?? 'GET'\n\n\tif (method === 'GET' && path === '/api/lab/state') {\n\t\tconst schema = lab.getSchema()\n\t\tsendJson(res, 200, {\n\t\t\tdevices: lab.listDevices(),\n\t\t\tserverOperations: lab.serverOperationCount(),\n\t\t\tcollections: Object.entries(schema.collections).map(([name, def]) => ({\n\t\t\t\tname,\n\t\t\t\tfields: Object.entries(def.fields).map(([fieldName, descriptor]) => ({\n\t\t\t\t\tname: fieldName,\n\t\t\t\t\tkind: descriptor.kind,\n\t\t\t\t\toptional: !descriptor.required,\n\t\t\t\t\tenumValues: descriptor.enumValues ?? null,\n\t\t\t\t\tdefaultValue: descriptor.defaultValue ?? null,\n\t\t\t\t})),\n\t\t\t})),\n\t\t})\n\t\treturn\n\t}\n\n\tif (method === 'GET' && path === '/api/lab/convergence') {\n\t\tsendJson(res, 200, await lab.convergence())\n\t\treturn\n\t}\n\n\tif (method !== 'POST') {\n\t\tsendJson(res, 405, { error: 'POST required' })\n\t\treturn\n\t}\n\n\tconst body = await readJsonBody(req)\n\n\tif (path === '/api/lab/devices') {\n\t\tconst state = await lab.addDevice(typeof body.name === 'string' ? body.name : undefined)\n\t\tsendJson(res, 200, state)\n\t\treturn\n\t}\n\n\tconst deviceMatch = path.match(\n\t\t/^\\/api\\/lab\\/devices\\/([a-zA-Z0-9_-]+)\\/(connect|disconnect|sync|chaos|insert|update|delete)$/,\n\t)\n\tif (!deviceMatch) {\n\t\tsendJson(res, 404, { error: `No lab route for ${path}` })\n\t\treturn\n\t}\n\tconst [, device, action] = deviceMatch\n\tif (!device || !action) {\n\t\tsendJson(res, 400, { error: 'Bad request' })\n\t\treturn\n\t}\n\n\tswitch (action) {\n\t\tcase 'connect':\n\t\t\tawait lab.connect(device)\n\t\t\tbreak\n\t\tcase 'disconnect': {\n\t\t\tawait lab.disconnect(device)\n\t\t\t// The device's DB file keeps changing while its reader is cached; drop\n\t\t\t// nothing — readers see committed WAL state per query. But a RESET of\n\t\t\t// the lab would invalidate paths; that's handled at close.\n\t\t\tbreak\n\t\t}\n\t\tcase 'sync':\n\t\t\tawait lab.sync(device)\n\t\t\tbreak\n\t\tcase 'chaos':\n\t\t\tsendJson(\n\t\t\t\tres,\n\t\t\t\t200,\n\t\t\t\tlab.setChaos(device, body as Partial<import('./lab-manager').LabChaosConfig>),\n\t\t\t)\n\t\t\treturn\n\t\tcase 'insert': {\n\t\t\tconst record = await lab.insert(\n\t\t\t\tdevice,\n\t\t\t\tString(body.collection ?? ''),\n\t\t\t\t(body.data as Record<string, unknown>) ?? {},\n\t\t\t)\n\t\t\tsendJson(res, 200, { record })\n\t\t\treturn\n\t\t}\n\t\tcase 'update': {\n\t\t\tconst record = await lab.update(\n\t\t\t\tdevice,\n\t\t\t\tString(body.collection ?? ''),\n\t\t\t\tString(body.id ?? ''),\n\t\t\t\t(body.data as Record<string, unknown>) ?? {},\n\t\t\t\t(body.increments as Record<string, number>) ?? undefined,\n\t\t\t)\n\t\t\tsendJson(res, 200, { record })\n\t\t\treturn\n\t\t}\n\t\tcase 'delete':\n\t\t\tawait lab.delete(device, String(body.collection ?? ''), String(body.id ?? ''))\n\t\t\tbreak\n\t}\n\tsendJson(res, 200, { ok: true, device: lab.deviceState(device) })\n}\n\n// ── Helpers ──────────────────────────────────────────────────────────────────\n\nasync function decodeRichtextFields(\n\treader: StudioDbReader,\n\tcollection: string,\n\trecordId: string,\n\tfields: Record<string, unknown>,\n): Promise<Record<string, string>> {\n\tconst previews: Record<string, string> = {}\n\tconst binaryFields = Object.entries(fields).filter(\n\t\t([, value]) => typeof value === 'string' && value.startsWith('<binary '),\n\t)\n\tif (binaryFields.length === 0) {\n\t\treturn previews\n\t}\n\tlet toPlainText: ((state: Uint8Array) => string) | null = null\n\ttry {\n\t\tconst storePkg = await import('@korajs/store')\n\t\ttoPlainText =\n\t\t\t(storePkg as { richtextToPlainText?: (s: Uint8Array) => string }).richtextToPlainText ?? null\n\t} catch {\n\t\treturn previews\n\t}\n\tif (!toPlainText) {\n\t\treturn previews\n\t}\n\tfor (const [field] of binaryFields) {\n\t\ttry {\n\t\t\tconst raw = reader.rawFieldValue(collection, recordId, field)\n\t\t\tif (raw instanceof Uint8Array || Buffer.isBuffer(raw)) {\n\t\t\t\tpreviews[field] = toPlainText(new Uint8Array(raw as Uint8Array))\n\t\t\t}\n\t\t} catch {\n\t\t\t// Not decodable as Yjs state — leave the byte summary.\n\t\t}\n\t}\n\treturn previews\n}\n\nasync function readJsonBody(req: IncomingMessage): Promise<Record<string, unknown>> {\n\tconst chunks: Buffer[] = []\n\tlet size = 0\n\tfor await (const chunk of req) {\n\t\tsize += (chunk as Buffer).length\n\t\tif (size > 256 * 1024) {\n\t\t\tthrow new Error('Request body too large')\n\t\t}\n\t\tchunks.push(chunk as Buffer)\n\t}\n\tif (chunks.length === 0) {\n\t\treturn {}\n\t}\n\ttry {\n\t\treturn JSON.parse(Buffer.concat(chunks).toString('utf8')) as Record<string, unknown>\n\t} catch {\n\t\tthrow new Error('Invalid JSON body')\n\t}\n}\n\nfunction intParam(url: URL, name: string): number | undefined {\n\tconst raw = url.searchParams.get(name)\n\tif (raw === null) {\n\t\treturn undefined\n\t}\n\tconst parsed = Number.parseInt(raw, 10)\n\treturn Number.isNaN(parsed) ? undefined : parsed\n}\n\nfunction sendJson(res: ServerResponse, status: number, body: unknown): void {\n\tres.writeHead(status, {\n\t\t'content-type': 'application/json; charset=utf-8',\n\t\t'cache-control': 'no-store',\n\t})\n\tres.end(JSON.stringify(body))\n}\n","import type BetterSqlite3 from 'better-sqlite3'\n\n/**\n * Read-only access layer for Kora Studio.\n *\n * Opens the SQLite database with `readonly: true` — Studio can NEVER write.\n * Any future editing feature must create real operations through the store\n * pipeline (never raw SQL), or it would corrupt the append-only op log and\n * violate content addressing.\n *\n * No schema file is required: collections are introspected as tables that\n * have a companion `_kora_ops_<name>` operation log table.\n */\n\n/** A parsed serialized HLC version stamp (\"wallTime:logical:nodeId\"). */\nexport interface ParsedVersion {\n\twallTime: number\n\tlogical: number\n\tnodeId: string\n}\n\nexport interface StudioCollectionInfo {\n\tname: string\n\tliveRecords: number\n\ttombstones: number\n\toperations: number\n\tcolumns: string[]\n}\n\nexport interface StudioOverview {\n\tdbPath: string\n\tcollections: StudioCollectionInfo[]\n\tversionVector: Array<{ nodeId: string; sequenceNumber: number }>\n\tmeta: Array<{ key: string; value: string }>\n\tpendingSyncOps: number\n\tauditTraces: number\n}\n\nexport interface StudioRecord {\n\tid: string\n\tfields: Record<string, unknown>\n\tcreatedAt: number\n\tupdatedAt: number\n\tdeleted: boolean\n\tversion: ParsedVersion | null\n\tfieldVersions: Record<string, ParsedVersion>\n}\n\nexport interface StudioOperation {\n\tid: string\n\tnodeId: string\n\ttype: string\n\trecordId: string\n\tdata: Record<string, unknown> | null\n\tpreviousData: Record<string, unknown> | null\n\ttimestamp: ParsedVersion | null\n\tsequenceNumber: number\n\tcausalDeps: string[]\n\tschemaVersion: number\n}\n\nexport interface StudioAuditTrace {\n\tid: string\n\trecordedAt: number\n\teventType: string\n\tcollection: string\n\trecordId: string\n\tfield: string\n\tstrategy: string\n\ttier: number\n\tconstraintName: string | null\n}\n\nconst KORA_TABLE_PREFIX = '_kora_'\nconst OPS_TABLE_PREFIX = '_kora_ops_'\n/** Metadata columns present on every materialized row. */\nconst META_COLUMNS = new Set([\n\t'id',\n\t'_created_at',\n\t'_updated_at',\n\t'_version',\n\t'_field_versions',\n\t'_deleted',\n])\n\n/** Parse a serialized HLC stamp; tolerates legacy/empty values with null. */\nexport function parseVersionStamp(raw: unknown): ParsedVersion | null {\n\tif (typeof raw !== 'string' || raw.length === 0) {\n\t\treturn null\n\t}\n\tconst parts = raw.split(':')\n\tif (parts.length < 3) {\n\t\treturn null\n\t}\n\tconst wallTime = Number.parseInt(parts[0] ?? '', 10)\n\tconst logical = Number.parseInt(parts[1] ?? '', 10)\n\tif (Number.isNaN(wallTime) || Number.isNaN(logical)) {\n\t\treturn null\n\t}\n\treturn { wallTime, logical, nodeId: parts.slice(2).join(':') }\n}\n\nfunction parseFieldVersionsColumn(raw: unknown): Record<string, ParsedVersion> {\n\tif (typeof raw !== 'string' || raw.length === 0) {\n\t\treturn {}\n\t}\n\ttry {\n\t\tconst parsed = JSON.parse(raw) as Record<string, unknown>\n\t\tconst result: Record<string, ParsedVersion> = {}\n\t\tfor (const [field, value] of Object.entries(parsed)) {\n\t\t\tconst version = parseVersionStamp(value)\n\t\t\tif (version) {\n\t\t\t\tresult[field] = version\n\t\t\t}\n\t\t}\n\t\treturn result\n\t} catch {\n\t\treturn {}\n\t}\n}\n\nfunction safeIdentifier(name: string): string {\n\tif (!/^[a-zA-Z0-9_]+$/.test(name)) {\n\t\tthrow new Error(`Unsafe SQL identifier: \"${name}\"`)\n\t}\n\treturn name\n}\n\nfunction parseJsonColumn(raw: unknown): Record<string, unknown> | null {\n\tif (typeof raw !== 'string' || raw.length === 0) {\n\t\treturn null\n\t}\n\ttry {\n\t\treturn JSON.parse(raw) as Record<string, unknown>\n\t} catch {\n\t\treturn null\n\t}\n}\n\nfunction parseJsonArrayColumn(raw: unknown): string[] {\n\tif (typeof raw !== 'string' || raw.length === 0) {\n\t\treturn []\n\t}\n\ttry {\n\t\tconst parsed = JSON.parse(raw) as unknown\n\t\treturn Array.isArray(parsed) ? parsed.map(String) : []\n\t} catch {\n\t\treturn []\n\t}\n}\n\n/**\n * Read-only Studio access to a Kora SQLite database.\n */\nexport class StudioDbReader {\n\tprivate constructor(\n\t\tprivate readonly db: BetterSqlite3.Database,\n\t\tprivate readonly dbPath: string,\n\t) {}\n\n\t/**\n\t * Open a Kora database strictly read-only.\n\t * @throws when the file does not exist or is not a Kora database\n\t */\n\tstatic async open(dbPath: string): Promise<StudioDbReader> {\n\t\t// Dynamic import so better-sqlite3 loads only when Studio runs.\n\t\tlet Database: typeof BetterSqlite3\n\t\ttry {\n\t\t\tDatabase = (await import('better-sqlite3')).default\n\t\t} catch {\n\t\t\tthrow new Error(\n\t\t\t\t'Kora Studio needs the \"better-sqlite3\" package to open database files. Install it in your project: pnpm add -D better-sqlite3',\n\t\t\t)\n\t\t}\n\t\tconst db = new Database(dbPath, { readonly: true, fileMustExist: true })\n\t\tconst reader = new StudioDbReader(db, dbPath)\n\t\tif (reader.listCollections().length === 0 && !reader.hasKoraTables()) {\n\t\t\tdb.close()\n\t\t\tthrow new Error(`\"${dbPath}\" does not look like a Kora database (no _kora_* tables found).`)\n\t\t}\n\t\treturn reader\n\t}\n\n\tclose(): void {\n\t\tthis.db.close()\n\t}\n\n\tprivate hasKoraTables(): boolean {\n\t\tconst row = this.db\n\t\t\t.prepare(\"SELECT COUNT(*) as count FROM sqlite_master WHERE type='table' AND name LIKE ?\")\n\t\t\t.get(`${KORA_TABLE_PREFIX}%`) as { count: number }\n\t\treturn row.count > 0\n\t}\n\n\t/** Collections = tables that have a companion _kora_ops_<name> table. */\n\tlistCollections(): string[] {\n\t\tconst tables = this.db\n\t\t\t.prepare(\"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name\")\n\t\t\t.all() as Array<{ name: string }>\n\t\tconst names = new Set(tables.map((t) => t.name))\n\t\tconst collections: string[] = []\n\t\tfor (const { name } of tables) {\n\t\t\tif (name.startsWith(OPS_TABLE_PREFIX)) {\n\t\t\t\tconst collection = name.slice(OPS_TABLE_PREFIX.length)\n\t\t\t\tif (names.has(collection)) {\n\t\t\t\t\tcollections.push(collection)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn collections.sort()\n\t}\n\n\toverview(): StudioOverview {\n\t\tconst collections: StudioCollectionInfo[] = this.listCollections().map((name) => {\n\t\t\tconst table = safeIdentifier(name)\n\t\t\tconst live = this.db\n\t\t\t\t.prepare(`SELECT COUNT(*) as count FROM ${table} WHERE _deleted = 0`)\n\t\t\t\t.get() as { count: number }\n\t\t\tconst tombstones = this.db\n\t\t\t\t.prepare(`SELECT COUNT(*) as count FROM ${table} WHERE _deleted = 1`)\n\t\t\t\t.get() as { count: number }\n\t\t\tconst operations = this.db\n\t\t\t\t.prepare(`SELECT COUNT(*) as count FROM ${OPS_TABLE_PREFIX}${table}`)\n\t\t\t\t.get() as { count: number }\n\t\t\tconst columnRows = this.db.prepare(`PRAGMA table_info(${table})`).all() as Array<{\n\t\t\t\tname: string\n\t\t\t}>\n\t\t\treturn {\n\t\t\t\tname,\n\t\t\t\tliveRecords: live.count,\n\t\t\t\ttombstones: tombstones.count,\n\t\t\t\toperations: operations.count,\n\t\t\t\tcolumns: columnRows.map((c) => c.name).filter((c) => !META_COLUMNS.has(c)),\n\t\t\t}\n\t\t})\n\n\t\tconst versionVector = this.tableExists('_kora_version_vector')\n\t\t\t? (this.db\n\t\t\t\t\t.prepare(\n\t\t\t\t\t\t'SELECT node_id as nodeId, sequence_number as sequenceNumber FROM _kora_version_vector ORDER BY node_id',\n\t\t\t\t\t)\n\t\t\t\t\t.all() as Array<{ nodeId: string; sequenceNumber: number }>)\n\t\t\t: []\n\n\t\tconst meta = this.tableExists('_kora_meta')\n\t\t\t? (this.db.prepare('SELECT key, value FROM _kora_meta ORDER BY key').all() as Array<{\n\t\t\t\t\tkey: string\n\t\t\t\t\tvalue: string\n\t\t\t\t}>)\n\t\t\t: []\n\n\t\tconst pendingSyncOps = this.tableExists('_kora_sync_queue')\n\t\t\t? (\n\t\t\t\t\tthis.db.prepare('SELECT COUNT(*) as count FROM _kora_sync_queue').get() as {\n\t\t\t\t\t\tcount: number\n\t\t\t\t\t}\n\t\t\t\t).count\n\t\t\t: 0\n\n\t\tconst auditTraces = this.tableExists('_kora_audit_traces')\n\t\t\t? (\n\t\t\t\t\tthis.db.prepare('SELECT COUNT(*) as count FROM _kora_audit_traces').get() as {\n\t\t\t\t\t\tcount: number\n\t\t\t\t\t}\n\t\t\t\t).count\n\t\t\t: 0\n\n\t\treturn { dbPath: this.dbPath, collections, versionVector, meta, pendingSyncOps, auditTraces }\n\t}\n\n\trecords(\n\t\tcollection: string,\n\t\toptions: {\n\t\t\tlimit?: number\n\t\t\toffset?: number\n\t\t\tincludeDeleted?: boolean\n\t\t\tsearch?: string\n\t\t} = {},\n\t): { records: StudioRecord[]; total: number } {\n\t\tconst table = safeIdentifier(collection)\n\t\tconst limit = Math.min(Math.max(options.limit ?? 50, 1), 500)\n\t\tconst offset = Math.max(options.offset ?? 0, 0)\n\n\t\tconst clauses: string[] = []\n\t\tconst params: unknown[] = []\n\t\tif (!options.includeDeleted) {\n\t\t\tclauses.push('_deleted = 0')\n\t\t}\n\t\tif (options.search && options.search.trim().length > 0) {\n\t\t\t// Search across id and every TEXT-affinity column.\n\t\t\tconst textColumns = (\n\t\t\t\tthis.db.prepare(`PRAGMA table_info(${table})`).all() as Array<{\n\t\t\t\t\tname: string\n\t\t\t\t\ttype: string\n\t\t\t\t}>\n\t\t\t).filter((c) => c.type.toUpperCase().includes('TEXT') || c.name === 'id')\n\t\t\tconst term = `%${options.search.trim()}%`\n\t\t\tconst likes = textColumns.map((c) => {\n\t\t\t\tparams.push(term)\n\t\t\t\treturn `${safeIdentifier(c.name)} LIKE ?`\n\t\t\t})\n\t\t\tif (likes.length > 0) {\n\t\t\t\tclauses.push(`(${likes.join(' OR ')})`)\n\t\t\t}\n\t\t}\n\t\tconst where = clauses.length > 0 ? `WHERE ${clauses.join(' AND ')}` : ''\n\n\t\tconst total = (\n\t\t\tthis.db.prepare(`SELECT COUNT(*) as count FROM ${table} ${where}`).get(...params) as {\n\t\t\t\tcount: number\n\t\t\t}\n\t\t).count\n\t\tconst rows = this.db\n\t\t\t.prepare(`SELECT * FROM ${table} ${where} ORDER BY _updated_at DESC LIMIT ? OFFSET ?`)\n\t\t\t.all(...params, limit, offset) as Array<Record<string, unknown>>\n\n\t\treturn { records: rows.map((row) => this.toStudioRecord(row)), total }\n\t}\n\n\t/** Full operation log for one collection (capped), for replay and the DAG. */\n\tallOperations(collection: string, cap = 5000): StudioOperation[] {\n\t\tconst table = safeIdentifier(collection)\n\t\tconst rows = this.db\n\t\t\t.prepare(`SELECT * FROM ${OPS_TABLE_PREFIX}${table} ORDER BY timestamp ASC LIMIT ?`)\n\t\t\t.all(Math.min(Math.max(cap, 1), 20000)) as Array<Record<string, unknown>>\n\t\treturn rows.map((row) => this.toStudioOperation(row))\n\t}\n\n\t/**\n\t * Cheap change fingerprint for live updates. SQLite's `data_version` PRAGMA\n\t * increments whenever ANOTHER connection commits — exactly what a read-only\n\t * watcher needs to know \"something changed, refetch\".\n\t */\n\tfingerprint(): string {\n\t\tconst row = this.db.prepare('PRAGMA data_version').get() as { data_version: number }\n\t\treturn String(row.data_version)\n\t}\n\n\t/** Raw value of a single column on a record (for richtext preview decode). */\n\trawFieldValue(collection: string, recordId: string, field: string): unknown {\n\t\tconst table = safeIdentifier(collection)\n\t\tconst column = safeIdentifier(field)\n\t\tconst row = this.db\n\t\t\t.prepare(`SELECT ${column} as value FROM ${table} WHERE id = ?`)\n\t\t\t.get(recordId) as { value: unknown } | undefined\n\t\treturn row?.value\n\t}\n\n\trecord(collection: string, recordId: string): StudioRecord | null {\n\t\tconst table = safeIdentifier(collection)\n\t\tconst row = this.db.prepare(`SELECT * FROM ${table} WHERE id = ?`).get(recordId) as\n\t\t\t| Record<string, unknown>\n\t\t\t| undefined\n\t\treturn row ? this.toStudioRecord(row) : null\n\t}\n\n\t/** Full operation history for one record, newest first. */\n\trecordOperations(collection: string, recordId: string): StudioOperation[] {\n\t\tconst table = safeIdentifier(collection)\n\t\tconst rows = this.db\n\t\t\t.prepare(\n\t\t\t\t`SELECT * FROM ${OPS_TABLE_PREFIX}${table} WHERE record_id = ? ORDER BY timestamp DESC`,\n\t\t\t)\n\t\t\t.all(recordId) as Array<Record<string, unknown>>\n\t\treturn rows.map((row) => this.toStudioOperation(row))\n\t}\n\n\toperations(\n\t\tcollection: string,\n\t\toptions: { limit?: number; offset?: number } = {},\n\t): { operations: StudioOperation[]; total: number } {\n\t\tconst table = safeIdentifier(collection)\n\t\tconst limit = Math.min(Math.max(options.limit ?? 50, 1), 500)\n\t\tconst offset = Math.max(options.offset ?? 0, 0)\n\t\tconst total = (\n\t\t\tthis.db.prepare(`SELECT COUNT(*) as count FROM ${OPS_TABLE_PREFIX}${table}`).get() as {\n\t\t\t\tcount: number\n\t\t\t}\n\t\t).count\n\t\tconst rows = this.db\n\t\t\t.prepare(`SELECT * FROM ${OPS_TABLE_PREFIX}${table} ORDER BY timestamp DESC LIMIT ? OFFSET ?`)\n\t\t\t.all(limit, offset) as Array<Record<string, unknown>>\n\t\treturn { operations: rows.map((row) => this.toStudioOperation(row)), total }\n\t}\n\n\tauditTraces(limit = 100): StudioAuditTrace[] {\n\t\tif (!this.tableExists('_kora_audit_traces')) {\n\t\t\treturn []\n\t\t}\n\t\tconst rows = this.db\n\t\t\t.prepare(\n\t\t\t\t`SELECT id, recorded_at, event_type, collection, record_id, field, strategy, tier, constraint_name\n\t\t\t\t FROM _kora_audit_traces ORDER BY recorded_at DESC LIMIT ?`,\n\t\t\t)\n\t\t\t.all(Math.min(Math.max(limit, 1), 500)) as Array<Record<string, unknown>>\n\t\treturn rows.map((row) => ({\n\t\t\tid: String(row.id),\n\t\t\trecordedAt: Number(row.recorded_at),\n\t\t\teventType: String(row.event_type),\n\t\t\tcollection: String(row.collection),\n\t\t\trecordId: String(row.record_id),\n\t\t\tfield: String(row.field),\n\t\t\tstrategy: String(row.strategy),\n\t\t\ttier: Number(row.tier),\n\t\t\tconstraintName: row.constraint_name === null ? null : String(row.constraint_name),\n\t\t}))\n\t}\n\n\tprivate tableExists(name: string): boolean {\n\t\tconst row = this.db\n\t\t\t.prepare(\"SELECT COUNT(*) as count FROM sqlite_master WHERE type='table' AND name = ?\")\n\t\t\t.get(name) as { count: number }\n\t\treturn row.count > 0\n\t}\n\n\tprivate toStudioRecord(row: Record<string, unknown>): StudioRecord {\n\t\tconst fields: Record<string, unknown> = {}\n\t\tfor (const [key, value] of Object.entries(row)) {\n\t\t\tif (!META_COLUMNS.has(key)) {\n\t\t\t\t// BLOBs (richtext Yjs state) are summarized, not dumped raw.\n\t\t\t\tfields[key] =\n\t\t\t\t\tvalue instanceof Uint8Array || Buffer.isBuffer(value)\n\t\t\t\t\t\t? `<binary ${(value as Uint8Array).byteLength} bytes>`\n\t\t\t\t\t\t: value\n\t\t\t}\n\t\t}\n\t\treturn {\n\t\t\tid: String(row.id),\n\t\t\tfields,\n\t\t\tcreatedAt: Number(row._created_at),\n\t\t\tupdatedAt: Number(row._updated_at),\n\t\t\tdeleted: row._deleted === 1,\n\t\t\tversion: parseVersionStamp(row._version),\n\t\t\tfieldVersions: parseFieldVersionsColumn(row._field_versions),\n\t\t}\n\t}\n\n\tprivate toStudioOperation(row: Record<string, unknown>): StudioOperation {\n\t\treturn {\n\t\t\tid: String(row.id),\n\t\t\tnodeId: String(row.node_id),\n\t\t\ttype: String(row.type),\n\t\t\trecordId: String(row.record_id),\n\t\t\tdata: parseJsonColumn(row.data),\n\t\t\tpreviousData: parseJsonColumn(row.previous_data),\n\t\t\ttimestamp: parseVersionStamp(row.timestamp),\n\t\t\tsequenceNumber: Number(row.sequence_number),\n\t\t\tcausalDeps: parseJsonArrayColumn(row.causal_deps),\n\t\t\tschemaVersion: Number(row.schema_version),\n\t\t}\n\t}\n}\n","import type { ParsedVersion, StudioOperation } from './db-reader'\n\n/**\n * Time travel and causal-graph helpers for Kora Studio.\n *\n * `replayToOperation` folds the operation log up to (and including) a chosen\n * operation, in HLC order, producing the record set as it existed at that\n * causal cut. Because per-field last-write-wins is deterministic and\n * order-independent, folding in HLC order reproduces exactly the state every\n * in-order device computed at that point — the same principle the store's own\n * materializer uses, applied read-only for inspection.\n */\n\nexport interface ReplayRecord {\n\tid: string\n\tfields: Record<string, unknown>\n\tdeleted: boolean\n\tlastWriterByField: Record<string, string>\n}\n\nexport interface ReplayResult {\n\t/** Records alive (or tombstoned) at the causal cut, keyed by record id. */\n\trecords: ReplayRecord[]\n\t/** How many operations were folded (position of the cut, 1-based). */\n\tappliedCount: number\n\ttotalCount: number\n\t/** The operation at the cut. */\n\tcutOperation: StudioOperation | null\n}\n\nfunction compareVersion(a: ParsedVersion | null, b: ParsedVersion | null): number {\n\tif (!a || !b) {\n\t\treturn a === b ? 0 : a ? 1 : -1\n\t}\n\tif (a.wallTime !== b.wallTime) {\n\t\treturn a.wallTime - b.wallTime\n\t}\n\tif (a.logical !== b.logical) {\n\t\treturn a.logical - b.logical\n\t}\n\treturn a.nodeId < b.nodeId ? -1 : a.nodeId > b.nodeId ? 1 : 0\n}\n\n/** Sort operations into the HLC total order (the canonical replay order). */\nexport function sortByHlc(ops: StudioOperation[]): StudioOperation[] {\n\treturn [...ops].sort((a, b) => compareVersion(a.timestamp, b.timestamp))\n}\n\n/**\n * Fold operations up to and including `upToOpId` (or all when null).\n */\nexport function replayToOperation(ops: StudioOperation[], upToOpId: string | null): ReplayResult {\n\tconst ordered = sortByHlc(ops)\n\tlet cutIndex = ordered.length - 1\n\tif (upToOpId !== null) {\n\t\tconst found = ordered.findIndex((o) => o.id === upToOpId)\n\t\tif (found === -1) {\n\t\t\tthrow new Error(`Operation \"${upToOpId}\" not found in the log`)\n\t\t}\n\t\tcutIndex = found\n\t}\n\n\tconst records = new Map<\n\t\tstring,\n\t\t{ fields: Record<string, unknown>; deleted: boolean; writers: Record<string, string> }\n\t>()\n\n\tfor (let i = 0; i <= cutIndex; i++) {\n\t\tconst operation = ordered[i]\n\t\tif (!operation) {\n\t\t\tcontinue\n\t\t}\n\t\tconst writer = operation.timestamp ? shortNode(operation.timestamp.nodeId) : '?'\n\t\tif (operation.type === 'insert' && operation.data) {\n\t\t\tconst writers: Record<string, string> = {}\n\t\t\tfor (const field of Object.keys(operation.data)) {\n\t\t\t\twriters[field] = writer\n\t\t\t}\n\t\t\trecords.set(operation.recordId, {\n\t\t\t\tfields: { ...operation.data },\n\t\t\t\tdeleted: false,\n\t\t\t\twriters,\n\t\t\t})\n\t\t} else if (operation.type === 'update' && operation.data) {\n\t\t\tconst existing = records.get(operation.recordId)\n\t\t\tif (existing) {\n\t\t\t\tfor (const [field, value] of Object.entries(operation.data)) {\n\t\t\t\t\texisting.fields[field] = value\n\t\t\t\t\texisting.writers[field] = writer\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Orphaned update (insert later in HLC order than the update is\n\t\t\t\t// impossible in HLC order — but a compacted log can lose the insert).\n\t\t\t\tconst writers: Record<string, string> = {}\n\t\t\t\tfor (const field of Object.keys(operation.data)) {\n\t\t\t\t\twriters[field] = writer\n\t\t\t\t}\n\t\t\t\trecords.set(operation.recordId, {\n\t\t\t\t\tfields: { ...operation.data },\n\t\t\t\t\tdeleted: false,\n\t\t\t\t\twriters,\n\t\t\t\t})\n\t\t\t}\n\t\t} else if (operation.type === 'delete') {\n\t\t\tconst existing = records.get(operation.recordId)\n\t\t\tif (existing) {\n\t\t\t\texisting.deleted = true\n\t\t\t} else {\n\t\t\t\trecords.set(operation.recordId, { fields: {}, deleted: true, writers: {} })\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\trecords: [...records.entries()].map(([id, r]) => ({\n\t\t\tid,\n\t\t\tfields: r.fields,\n\t\t\tdeleted: r.deleted,\n\t\t\tlastWriterByField: r.writers,\n\t\t})),\n\t\tappliedCount: cutIndex + 1,\n\t\ttotalCount: ordered.length,\n\t\tcutOperation: ordered[cutIndex] ?? null,\n\t}\n}\n\n// ── Causal DAG ───────────────────────────────────────────────────────────────\n\nexport interface DagNode {\n\tid: string\n\ttype: string\n\trecordId: string\n\tnodeId: string\n\tshortNodeId: string\n\tsequenceNumber: number\n\twallTime: number\n\t/** Column in the layered layout (HLC order). */\n\tx: number\n\t/** Lane index (one lane per originating device). */\n\tlane: number\n\tdataPreview: string\n}\n\nexport interface DagEdge {\n\tfrom: string\n\tto: string\n}\n\nexport interface DagResult {\n\tnodes: DagNode[]\n\tedges: DagEdge[]\n\tlanes: Array<{ nodeId: string; shortNodeId: string }>\n}\n\n/**\n * Build a layered causal graph from operations: x = HLC order, one horizontal\n * lane per originating device, edges = causal dependencies. Concurrency is\n * VISIBLE as operations sharing an x-neighborhood on different lanes with no\n * path between them.\n */\nexport function buildCausalDag(ops: StudioOperation[]): DagResult {\n\tconst ordered = sortByHlc(ops)\n\tconst laneByNode = new Map<string, number>()\n\tconst lanes: Array<{ nodeId: string; shortNodeId: string }> = []\n\n\tfor (const operation of ordered) {\n\t\tif (!laneByNode.has(operation.nodeId)) {\n\t\t\tlaneByNode.set(operation.nodeId, lanes.length)\n\t\t\tlanes.push({ nodeId: operation.nodeId, shortNodeId: shortNode(operation.nodeId) })\n\t\t}\n\t}\n\n\tconst idSet = new Set(ordered.map((o) => o.id))\n\tconst nodes: DagNode[] = ordered.map((operation, index) => ({\n\t\tid: operation.id,\n\t\ttype: operation.type,\n\t\trecordId: operation.recordId,\n\t\tnodeId: operation.nodeId,\n\t\tshortNodeId: shortNode(operation.nodeId),\n\t\tsequenceNumber: operation.sequenceNumber,\n\t\twallTime: operation.timestamp?.wallTime ?? 0,\n\t\tx: index,\n\t\tlane: laneByNode.get(operation.nodeId) ?? 0,\n\t\tdataPreview: operation.data ? JSON.stringify(operation.data).slice(0, 80) : '',\n\t}))\n\n\tconst edges: DagEdge[] = []\n\tfor (const operation of ordered) {\n\t\tfor (const dep of operation.causalDeps) {\n\t\t\tif (idSet.has(dep)) {\n\t\t\t\tedges.push({ from: dep, to: operation.id })\n\t\t\t}\n\t\t}\n\t}\n\n\treturn { nodes, edges, lanes }\n}\n\nfunction shortNode(nodeId: string): string {\n\treturn nodeId.length > 8 ? nodeId.slice(-8) : nodeId\n}\n","/**\n * Kora Studio HTML shell (served at /). The stylesheet and application are\n * served as separate routes so each stays an editable module; everything is\n * still fully self-contained — no CDN, no build step, works offline.\n */\nexport const STUDIO_HTML = `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n<title>Kora Studio</title>\n<link rel=\"stylesheet\" href=\"/style.css\" />\n</head>\n<body>\n<header>\n <div class=\"logo\">kora <span>studio</span></div>\n <div class=\"modebadge\" id=\"modebadge\"></div>\n <div class=\"dbpath mono\" id=\"dbpath\"></div>\n <div class=\"live\" id=\"live\"><div class=\"dot\"></div><span>live</span></div>\n <button id=\"refresh\">Refresh</button>\n</header>\n<div class=\"tabs\" id=\"tabs\"></div>\n<div class=\"layout\">\n <nav class=\"sidebar\" id=\"sidebar\"></nav>\n <main id=\"main\"><div class=\"empty\">Loading…</div></main>\n</div>\n<div class=\"drawer\" id=\"drawer\"></div>\n<div class=\"dag-tip\" id=\"dagtip\"></div>\n<script src=\"/app.js\"></script>\n</body>\n</html>\n`\n","/** Kora Studio application (served at /app.js). Vanilla DOM, zero dependencies. */\nexport const STUDIO_APP_JS = `\n(function () {\n 'use strict';\n\n // ── State ──────────────────────────────────────────────────────────────────\n var state = {\n mode: 'file', // 'file' | 'lab'\n tab: 'data', // data | ops | timetravel | merges | sync | lab\n device: null, // selected lab device for data-ish tabs\n devices: [],\n collection: null,\n page: 0,\n includeDeleted: false,\n search: '',\n overview: null,\n ttPosition: null, // time-travel slider position\n ttPlaying: null,\n labState: null,\n feed: [],\n deviceColors: {},\n live: false\n };\n var PAGE = 50;\n var refreshTimer = null;\n\n // ── Utilities ──────────────────────────────────────────────────────────────\n function qs(sel) { return document.querySelector(sel); }\n function api(path) {\n var sep = path.indexOf('?') === -1 ? '?' : '&';\n var url = state.device ? path + sep + 'device=' + encodeURIComponent(state.device) : path;\n return fetch(url).then(function (r) {\n if (!r.ok) { return r.json().then(function (b) { throw new Error(b.error || r.statusText); }); }\n return r.json();\n });\n }\n function post(path, body) {\n return fetch(path, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body || {})\n }).then(function (r) {\n if (!r.ok) { return r.json().then(function (b) { throw new Error(b.error || r.statusText); }); }\n return r.json();\n });\n }\n function el(tag, attrs, children) {\n var node = document.createElement(tag);\n if (attrs) { Object.keys(attrs).forEach(function (k) {\n if (k === 'class') node.className = attrs[k];\n else if (k === 'text') node.textContent = attrs[k];\n else if (k === 'html') node.innerHTML = attrs[k];\n else if (k.slice(0, 2) === 'on') node.addEventListener(k.slice(2), attrs[k]);\n else node.setAttribute(k, attrs[k]);\n }); }\n (children || []).forEach(function (c) { if (c) node.appendChild(c); });\n return node;\n }\n function fmtTime(ms) {\n if (!ms) return '·';\n var d = new Date(ms);\n return d.toLocaleTimeString() + '.' + ('00' + d.getMilliseconds()).slice(-3);\n }\n function fmtDateTime(ms) {\n if (!ms) return '·';\n var d = new Date(ms);\n return d.toLocaleDateString() + ' ' + d.toLocaleTimeString();\n }\n function shortNode(id) { return id && id.length > 8 ? id.slice(-8) : (id || '·'); }\n function shortId(id) { return id && id.length > 13 ? id.slice(0, 13) + '…' : (id || ''); }\n function fmtVal(v) {\n if (v === null || v === undefined) return 'null';\n if (typeof v === 'object') return JSON.stringify(v);\n return String(v);\n }\n function deviceColor(name) {\n if (!(name in state.deviceColors)) {\n state.deviceColors[name] = 'dev-c' + (Object.keys(state.deviceColors).length % 6);\n }\n return state.deviceColors[name];\n }\n function debounceRefresh() {\n if (refreshTimer) clearTimeout(refreshTimer);\n refreshTimer = setTimeout(function () { render(false); }, 250);\n }\n\n // ── Boot + live events ─────────────────────────────────────────────────────\n fetch('/api/mode').then(function (r) { return r.json(); }).then(function (info) {\n state.mode = info.mode;\n state.devices = info.devices || [];\n if (state.mode === 'lab') { state.tab = 'lab'; state.device = state.devices[0] || null; }\n qs('#modebadge').textContent = state.mode;\n if (info.spectator) {\n qs('#dbpath').textContent = '⇄ ' + info.spectator.url +\n (info.spectator.connected ? ' · connected' : ' · connecting…') +\n ' · ' + info.spectator.operationsReceived + ' ops received';\n }\n connectEvents();\n render(true);\n });\n\n function connectEvents() {\n var source = new EventSource('/api/events');\n source.addEventListener('hello', function () {\n state.live = true; qs('#live').className = 'live on';\n });\n source.addEventListener('change', function () {\n if (state.tab !== 'timetravel') debounceRefresh();\n });\n source.addEventListener('lab', function (event) {\n var data = JSON.parse(event.data);\n state.feed.push(data);\n if (state.feed.length > 400) state.feed.shift();\n appendFeedLine(data);\n });\n source.addEventListener('spectator', function (event) {\n var data = JSON.parse(event.data);\n data.device = 'server';\n state.feed.push(data);\n if (state.feed.length > 400) state.feed.shift();\n appendFeedLine(data);\n // Keep the header status fresh as ops stream in.\n fetch('/api/spectator/status').then(function (r) { return r.json(); }).then(function (s) {\n qs('#dbpath').textContent = '⇄ ' + s.url +\n (s.connected ? ' · connected' : ' · reconnecting…') +\n ' · ' + s.operationsReceived + ' ops received';\n }).catch(function () {});\n });\n source.onerror = function () {\n state.live = false; qs('#live').className = 'live';\n };\n }\n\n // ── Tabs + chrome ──────────────────────────────────────────────────────────\n function renderTabs() {\n var tabs = qs('#tabs');\n tabs.textContent = '';\n var defs = [];\n if (state.mode === 'lab') defs.push(['lab', 'Lab']);\n defs.push(['data', 'Data'], ['ops', 'Operations'], ['timetravel', 'Time Travel'], ['merges', 'Merges'], ['sync', 'Sync']);\n defs.forEach(function (d) {\n tabs.appendChild(el('div', {\n class: 'tab' + (state.tab === d[0] ? ' active' : ''),\n text: d[1],\n onclick: function () { state.tab = d[0]; state.page = 0; closeDrawer(); render(true); }\n }));\n });\n tabs.appendChild(el('div', { class: 'spacer' }));\n if (state.mode === 'lab' && state.tab !== 'lab') {\n var pick = el('select', { onchange: function () { state.device = pick.value; state.page = 0; render(true); } },\n state.devices.map(function (d) {\n var o = el('option', { value: d, text: d });\n if (d === state.device) o.selected = true;\n return o;\n }));\n tabs.appendChild(el('div', { class: 'devicepick' }, [el('span', { text: 'device:' }), pick]));\n }\n }\n\n function renderSidebar() {\n var nav = qs('#sidebar');\n nav.textContent = '';\n if (state.tab === 'lab' || state.tab === 'sync' || state.tab === 'merges') {\n nav.style.display = 'none';\n return;\n }\n nav.style.display = '';\n nav.appendChild(el('h3', { text: 'Collections' }));\n (state.overview ? state.overview.collections : []).forEach(function (c) {\n nav.appendChild(el('div', {\n class: 'item' + (state.collection === c.name ? ' active' : ''),\n onclick: function () { state.collection = c.name; state.page = 0; closeDrawer(); render(false); }\n }, [\n el('span', { text: c.name }),\n el('span', { class: 'count', text: String(c.liveRecords) + (c.tombstones ? ' +' + c.tombstones + '†' : '') })\n ]));\n });\n }\n\n // ── Main render ────────────────────────────────────────────────────────────\n function render(full) {\n renderTabs();\n if (state.tab === 'lab') { renderSidebar(); renderLab(full); return; }\n api('/api/overview').then(function (overview) {\n state.overview = overview;\n qs('#dbpath').textContent = overview.dbPath || '';\n if (!state.collection || !overview.collections.some(function (c) { return c.name === state.collection; })) {\n state.collection = overview.collections.length ? overview.collections[0].name : null;\n }\n renderSidebar();\n var main = qs('#main');\n main.textContent = '';\n if (state.tab === 'data') return renderData(main);\n if (state.tab === 'ops') return renderOps(main);\n if (state.tab === 'timetravel') return renderTimeTravel(main);\n if (state.tab === 'merges') return renderMerges(main);\n if (state.tab === 'sync') return renderSync(main);\n }).catch(function (e) {\n qs('#main').innerHTML = '<div class=\"error\">' + e.message + '</div>';\n });\n }\n\n function toolbar(main, title, extra) {\n main.appendChild(el('div', { class: 'toolbar' }, [el('h2', { text: title })].concat(extra || [])));\n }\n function pager(main, total) {\n var pages = Math.max(1, Math.ceil(total / PAGE));\n main.appendChild(el('div', { class: 'pager' }, [\n el('button', { text: '‹ Prev', onclick: function () { if (state.page > 0) { state.page--; render(false); } } }),\n el('span', { text: 'page ' + (state.page + 1) + ' / ' + pages + ' · ' + total + ' total' }),\n el('button', { text: 'Next ›', onclick: function () { if (state.page < pages - 1) { state.page++; render(false); } } })\n ]));\n }\n\n // ── Data tab ───────────────────────────────────────────────────────────────\n function renderData(main) {\n if (!state.collection) { main.appendChild(el('div', { class: 'empty', text: 'No collections.' })); return; }\n var searchBox = el('input', { type: 'text', placeholder: 'search…', value: state.search });\n searchBox.oninput = function () { state.search = searchBox.value; state.page = 0; debounceRefresh(); };\n var deletedToggle = el('label', {}, [\n (function () { var cb = el('input', { type: 'checkbox' }); cb.checked = state.includeDeleted;\n cb.onchange = function () { state.includeDeleted = cb.checked; state.page = 0; render(false); }; return cb; })(),\n el('span', { text: 'tombstones' })\n ]);\n toolbar(main, state.collection, [searchBox, deletedToggle]);\n\n var q = '/api/collections/' + state.collection + '/records?limit=' + PAGE +\n '&offset=' + (state.page * PAGE) + '&includeDeleted=' + state.includeDeleted +\n (state.search ? '&search=' + encodeURIComponent(state.search) : '');\n api(q).then(function (data) {\n var info = state.overview.collections.filter(function (c) { return c.name === state.collection; })[0];\n var columns = info ? info.columns : [];\n if (!data.records.length) { main.appendChild(el('div', { class: 'empty', text: 'No records.' })); return; }\n var thead = el('tr', {}, [el('th', { text: 'id' })].concat(\n columns.map(function (c) { return el('th', { text: c }); }),\n [el('th', { text: 'updated' })]\n ));\n var tbody = el('tbody', {}, data.records.map(function (r) {\n return el('tr', { class: 'clickable' + (r.deleted ? ' tombstone' : ''), onclick: function () { openRecord(r.id); } },\n [el('td', { class: 'mono', text: shortId(r.id) })].concat(\n columns.map(function (c) { return el('td', { text: fmtVal(r.fields[c]).slice(0, 60) }); }),\n [el('td', { class: 'mono', text: fmtDateTime(r.updatedAt) })]\n ));\n }));\n main.appendChild(el('table', { class: 'grid' }, [el('thead', {}, [thead]), tbody]));\n pager(main, data.total);\n }).catch(showError(main));\n }\n\n // ── Record drawer ──────────────────────────────────────────────────────────\n function openRecord(id) {\n api('/api/collections/' + state.collection + '/records/' + encodeURIComponent(id)).then(function (data) {\n var drawer = qs('#drawer');\n drawer.textContent = '';\n var r = data.record;\n drawer.appendChild(el('h3', {}, [\n el('span', { class: 'mono', text: r.id }),\n el('span', { class: 'close', text: '✕', onclick: closeDrawer })\n ]));\n if (r.deleted) drawer.appendChild(el('span', { class: 'badge delete', text: 'tombstone' }));\n\n drawer.appendChild(el('h4', { text: 'Fields — with last writer per field' }));\n Object.keys(r.fields).forEach(function (f) {\n var fv = r.fieldVersions[f];\n var val = fmtVal(r.fields[f]);\n var preview = data.richtextPreviews && data.richtextPreviews[f];\n drawer.appendChild(el('div', { class: 'fieldrow' }, [\n el('div', { class: 'fname mono', text: f }),\n el('div', { class: 'fval', text: val }),\n preview ? el('div', { class: 'preview', text: '“' + preview + '”' }) : null,\n fv ? el('span', { class: 'chip' }, [\n el('span', { text: 'last writer' }),\n el('b', { class: 'mono', text: shortNode(fv.nodeId) }),\n el('span', { class: 'mono', text: fmtDateTime(fv.wallTime) })\n ]) : el('span', { class: 'chip', text: 'no field version' })\n ]));\n });\n\n drawer.appendChild(el('h4', { text: 'Causal graph for this record' }));\n var dagBox = el('div', { class: 'dagwrap' });\n drawer.appendChild(dagBox);\n api('/api/collections/' + state.collection + '/dag?record=' + encodeURIComponent(id)).then(function (dag) {\n renderDag(dagBox, dag, 500);\n });\n\n drawer.appendChild(el('h4', { text: 'Operation history (newest first)' }));\n if (!data.operations.length) drawer.appendChild(el('div', { class: 'empty', text: 'No operations (compacted?).' }));\n data.operations.forEach(function (o) {\n var body = el('div', { class: 'op ' + o.type }, [\n el('div', {}, [\n el('span', { class: 'badge ' + o.type, text: o.type }),\n el('span', { class: 'meta', text: ' ' + shortNode(o.nodeId) + ' · seq ' + o.sequenceNumber + ' · ' + (o.timestamp ? fmtDateTime(o.timestamp.wallTime) : '·') })\n ])\n ]);\n if (o.data) {\n var pre = el('pre', { class: 'mono' });\n pre.textContent = JSON.stringify(o.data, null, 1);\n body.appendChild(pre);\n }\n drawer.appendChild(body);\n });\n drawer.className = 'drawer open';\n }).catch(function (e) { alertError(e); });\n }\n function closeDrawer() { qs('#drawer').className = 'drawer'; }\n\n // ── Operations tab (DAG + table) ───────────────────────────────────────────\n function renderOps(main) {\n if (!state.collection) { main.appendChild(el('div', { class: 'empty', text: 'No collections.' })); return; }\n toolbar(main, state.collection + ' — operation log');\n var dagBox = el('div', { class: 'dagwrap' });\n main.appendChild(dagBox);\n api('/api/collections/' + state.collection + '/dag?limit=120').then(function (dag) {\n renderDag(dagBox, dag, null);\n });\n\n api('/api/collections/' + state.collection + '/ops?limit=' + PAGE + '&offset=' + (state.page * PAGE)).then(function (data) {\n if (!data.operations.length) { main.appendChild(el('div', { class: 'empty', text: 'Empty (compacted?).' })); return; }\n var thead = el('tr', {}, ['type', 'record', 'node', 'seq', 'time', 'data'].map(function (h) { return el('th', { text: h }); }));\n var tbody = el('tbody', {}, data.operations.map(function (o) {\n return el('tr', { class: 'clickable', onclick: function () { openRecord(o.recordId); } }, [\n el('td', {}, [el('span', { class: 'badge ' + o.type, text: o.type })]),\n el('td', { class: 'mono', text: shortId(o.recordId) }),\n el('td', { class: 'mono', text: shortNode(o.nodeId) }),\n el('td', { class: 'mono', text: String(o.sequenceNumber) }),\n el('td', { class: 'mono', text: o.timestamp ? fmtDateTime(o.timestamp.wallTime) : '·' }),\n el('td', { class: 'mono', text: o.data ? JSON.stringify(o.data).slice(0, 70) : '·' })\n ]);\n }));\n main.appendChild(el('table', { class: 'grid' }, [el('thead', {}, [thead]), tbody]));\n pager(main, data.total);\n }).catch(showError(main));\n }\n\n // ── Causal DAG renderer (SVG) ──────────────────────────────────────────────\n function renderDag(container, dag, maxWidth) {\n container.textContent = '';\n if (!dag.nodes.length) { container.appendChild(el('div', { class: 'empty', text: 'No operations to graph.' })); return; }\n var XSTEP = 46, YSTEP = 44, PADX = 90, PADY = 26, R = 7;\n var width = PADX + dag.nodes.length * XSTEP + 30;\n var height = PADY + dag.lanes.length * YSTEP + 16;\n var svgns = 'http://www.w3.org/2000/svg';\n var svg = document.createElementNS(svgns, 'svg');\n svg.setAttribute('width', String(width));\n svg.setAttribute('height', String(height));\n\n var defs = document.createElementNS(svgns, 'defs');\n defs.innerHTML = '<marker id=\"arrow\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"6\" markerHeight=\"6\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"#3a4356\"/></marker>';\n svg.appendChild(defs);\n\n var colors = { insert: '#48bb78', update: '#4299e1', delete: '#f56565' };\n var pos = {};\n dag.nodes.forEach(function (n) {\n pos[n.id] = { x: PADX + n.x * XSTEP, y: PADY + n.lane * YSTEP + YSTEP / 2 };\n });\n\n dag.lanes.forEach(function (lane, i) {\n var y = PADY + i * YSTEP + YSTEP / 2;\n var line = document.createElementNS(svgns, 'line');\n line.setAttribute('x1', '10'); line.setAttribute('x2', String(width - 10));\n line.setAttribute('y1', String(y)); line.setAttribute('y2', String(y));\n line.setAttribute('class', 'dag-lane-line');\n svg.appendChild(line);\n var label = document.createElementNS(svgns, 'text');\n label.setAttribute('x', '10'); label.setAttribute('y', String(y - 8));\n label.setAttribute('class', 'dag-lane-label');\n label.textContent = lane.shortNodeId;\n svg.appendChild(label);\n });\n\n dag.edges.forEach(function (e) {\n var a = pos[e.from], b = pos[e.to];\n if (!a || !b) return;\n var path = document.createElementNS(svgns, 'path');\n var midX = (a.x + b.x) / 2;\n path.setAttribute('d', 'M ' + a.x + ' ' + a.y + ' C ' + midX + ' ' + a.y + ', ' + midX + ' ' + b.y + ', ' + (b.x - R - 2) + ' ' + b.y);\n path.setAttribute('class', 'dag-edge');\n svg.appendChild(path);\n });\n\n var tip = qs('#dagtip');\n dag.nodes.forEach(function (n) {\n var g = document.createElementNS(svgns, 'g');\n g.setAttribute('class', 'dag-node');\n var c = document.createElementNS(svgns, 'circle');\n var p = pos[n.id];\n c.setAttribute('cx', String(p.x)); c.setAttribute('cy', String(p.y)); c.setAttribute('r', String(R));\n c.setAttribute('fill', '#161a22');\n c.setAttribute('stroke', colors[n.type] || '#8b93a7');\n g.appendChild(c);\n g.addEventListener('mousemove', function (ev) {\n tip.style.display = 'block';\n tip.style.left = (ev.clientX + 14) + 'px';\n tip.style.top = (ev.clientY + 14) + 'px';\n tip.innerHTML = '<b>' + n.type + '</b> by ' + n.shortNodeId + ' · seq ' + n.sequenceNumber +\n '<br/>record ' + shortId(n.recordId) +\n (n.dataPreview ? '<br/><span class=\"mono\">' + escapeHtml(n.dataPreview) + '</span>' : '');\n });\n g.addEventListener('mouseleave', function () { tip.style.display = 'none'; });\n g.addEventListener('click', function () { openRecord(n.recordId); });\n svg.appendChild(g);\n });\n\n if (maxWidth) container.style.maxWidth = maxWidth + 'px';\n container.appendChild(svg);\n container.scrollLeft = width;\n }\n function escapeHtml(s) {\n return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\n }\n\n // ── Time travel tab ────────────────────────────────────────────────────────\n function renderTimeTravel(main) {\n if (!state.collection) { main.appendChild(el('div', { class: 'empty', text: 'No collections.' })); return; }\n toolbar(main, state.collection + ' — time travel');\n api('/api/collections/' + state.collection + '/replay').then(function (full) {\n var total = full.totalCount;\n if (!total) { main.appendChild(el('div', { class: 'empty', text: 'No operations to replay.' })); return; }\n if (state.ttPosition === null || state.ttPosition > total) state.ttPosition = total;\n\n var posLabel = el('span', { class: 'mono', text: '' });\n var slider = el('input', { type: 'range', min: '1', max: String(total), value: String(state.ttPosition) });\n var playBtn = el('button', { text: '▶ replay' });\n var cutBox = el('div', { class: 'tt-cut mono' });\n var stateBox = el('div', {});\n\n function show(position) {\n state.ttPosition = position;\n posLabel.textContent = position + ' / ' + total + ' ops';\n api('/api/collections/' + state.collection + '/replay?upTo=' + encodeURIComponent(opIdAt(position))).then(function (cut) {\n var o = cut.cutOperation;\n cutBox.innerHTML = o\n ? 'cut at <b>' + o.type + '</b> by ' + shortNode(o.nodeId) + ' · ' + (o.timestamp ? fmtDateTime(o.timestamp.wallTime) : '') +\n (o.data ? ' · ' + escapeHtml(JSON.stringify(o.data).slice(0, 90)) : '')\n : '';\n renderReplayState(stateBox, cut);\n });\n }\n // The full replay result orders ops by HLC; fetch the ordered id list once.\n var orderedIds = [];\n api('/api/collections/' + state.collection + '/dag?limit=20000').then(function (dag) {\n orderedIds = dag.nodes.map(function (n) { return n.id; });\n show(Math.min(state.ttPosition, orderedIds.length));\n });\n function opIdAt(position) { return orderedIds[position - 1]; }\n\n slider.oninput = function () { show(Number(slider.value)); };\n playBtn.onclick = function () {\n if (state.ttPlaying) { clearInterval(state.ttPlaying); state.ttPlaying = null; playBtn.textContent = '▶ replay'; return; }\n var pos = 1;\n playBtn.textContent = '⏸ pause';\n state.ttPlaying = setInterval(function () {\n if (pos > orderedIds.length) { clearInterval(state.ttPlaying); state.ttPlaying = null; playBtn.textContent = '▶ replay'; return; }\n slider.value = String(pos);\n show(pos);\n pos++;\n }, 350);\n };\n\n main.appendChild(el('div', { class: 'tt-controls' }, [playBtn, slider, posLabel]));\n main.appendChild(cutBox);\n main.appendChild(stateBox);\n }).catch(showError(main));\n }\n\n function renderReplayState(box, cut) {\n box.textContent = '';\n if (!cut.records.length) { box.appendChild(el('div', { class: 'empty', text: 'No records at this point.' })); return; }\n var fieldSet = {};\n cut.records.forEach(function (r) { Object.keys(r.fields).forEach(function (f) { fieldSet[f] = true; }); });\n var fields = Object.keys(fieldSet);\n var thead = el('tr', {}, [el('th', { text: 'id' })].concat(fields.map(function (f) { return el('th', { text: f }); })));\n var tbody = el('tbody', {}, cut.records.map(function (r) {\n return el('tr', { class: r.deleted ? 'tombstone' : '' },\n [el('td', { class: 'mono', text: shortId(r.id) })].concat(fields.map(function (f) {\n return el('td', { text: r.fields[f] === undefined ? '' : fmtVal(r.fields[f]).slice(0, 40) });\n })));\n }));\n box.appendChild(el('table', { class: 'grid' }, [el('thead', {}, [thead]), tbody]));\n }\n\n // ── Merges tab ─────────────────────────────────────────────────────────────\n function renderMerges(main) {\n toolbar(main, 'Merge audit trail');\n api('/api/audit?limit=200').then(function (data) {\n if (!data.traces.length) { main.appendChild(el('div', { class: 'empty', text: 'No merge audit traces recorded.' })); return; }\n var thead = el('tr', {}, ['when', 'event', 'collection', 'record', 'field', 'strategy', 'tier'].map(function (h) { return el('th', { text: h }); }));\n var tbody = el('tbody', {}, data.traces.map(function (t) {\n return el('tr', {}, [\n el('td', { class: 'mono', text: fmtDateTime(t.recordedAt) }),\n el('td', { text: t.eventType }),\n el('td', { text: t.collection }),\n el('td', { class: 'mono', text: shortId(t.recordId) }),\n el('td', { text: t.field }),\n el('td', { text: t.strategy }),\n el('td', { text: String(t.tier) })\n ]);\n }));\n main.appendChild(el('table', { class: 'grid' }, [el('thead', {}, [thead]), tbody]));\n }).catch(showError(main));\n }\n\n // ── Sync tab ───────────────────────────────────────────────────────────────\n function renderSync(main) {\n toolbar(main, 'Sync state' + (state.device ? ' — ' + state.device : ''));\n var o = state.overview;\n main.appendChild(el('div', { class: 'panel' }, [\n el('h3', { text: 'Version vector — what this store has seen from each node' }),\n el('table', { class: 'grid' }, [\n el('thead', {}, [el('tr', {}, [el('th', { text: 'node' }), el('th', { text: 'max sequence' })])]),\n el('tbody', {}, o.versionVector.map(function (v) {\n return el('tr', {}, [el('td', { class: 'mono', text: v.nodeId }), el('td', { class: 'mono', text: String(v.sequenceNumber) })]);\n }))\n ])\n ]));\n main.appendChild(el('div', { class: 'panel' }, [\n el('h3', { text: 'Outbound queue' }),\n el('div', { text: o.pendingSyncOps + ' operation(s) waiting to sync' })\n ]));\n main.appendChild(el('div', { class: 'panel' }, [\n el('h3', { text: 'Store meta' }),\n el('table', { class: 'grid' }, [\n el('tbody', {}, o.meta.map(function (m) {\n return el('tr', {}, [el('td', { class: 'mono', text: m.key }), el('td', { class: 'mono', text: m.value })]);\n }))\n ])\n ]));\n if (state.mode === 'spectator') {\n var feedBox = el('div', { class: 'lab-feed', style: 'width:100%;max-height:340px' }, [\n el('h3', { text: 'Live events from the server' }),\n el('div', { class: 'feed', id: 'feed' })\n ]);\n main.appendChild(feedBox);\n state.feed.slice(-200).forEach(appendFeedLine);\n }\n }\n\n // ── Lab tab ────────────────────────────────────────────────────────────────\n function renderLab(full) {\n var main = qs('#main');\n qs('#sidebar').style.display = 'none';\n Promise.all([fetch('/api/lab/state').then(function (r) { return r.json(); }),\n fetch('/api/lab/convergence').then(function (r) { return r.json(); })])\n .then(function (results) {\n state.labState = results[0];\n state.devices = results[0].devices.map(function (d) { return d.name; });\n var convergence = results[1];\n main.textContent = '';\n qs('#dbpath').textContent = 'sync laboratory · ' + results[0].serverOperations + ' ops on server';\n\n var conv = el('div', { class: 'convergence ' + (convergence.converged ? 'ok' : 'bad') }, [\n el('div', { text: convergence.converged\n ? '✓ ALL ' + convergence.deviceCount + ' DEVICES CONVERGED'\n : '✗ DEVICES DIVERGED (' + convergence.differences.length + ' difference(s)) — sync to converge' })\n ]);\n if (!convergence.converged) {\n conv.appendChild(el('div', { class: 'diffs', text: convergence.differences.slice(0, 4).join(' · ') }));\n }\n var addBtn = el('button', { class: 'primary', text: '+ add device', onclick: function () {\n post('/api/lab/devices', {}).then(function () { render(true); }).catch(alertError);\n } });\n var syncAllBtn = el('button', { text: 'sync all', onclick: function () {\n var chain = Promise.resolve();\n state.devices.forEach(function (d) { chain = chain.then(function () { return post('/api/lab/devices/' + d + '/sync', {}); }); });\n chain.then(function () { return post('/api/lab/devices/' + state.devices[0] + '/sync', {}); })\n .then(function () { render(true); }).catch(alertError);\n } });\n main.appendChild(el('div', { class: 'lab-top' }, [conv, addBtn, syncAllBtn]));\n\n var devicesBox = el('div', { class: 'lab-devices' });\n results[0].devices.forEach(function (d) { devicesBox.appendChild(deviceCard(d, results[0].collections)); });\n\n var feedBox = el('div', { class: 'lab-feed' }, [\n el('h3', { text: 'Live events' }),\n el('div', { class: 'feed', id: 'feed' })\n ]);\n main.appendChild(el('div', { class: 'lab-grid' }, [devicesBox, feedBox]));\n state.feed.slice(-200).forEach(appendFeedLine);\n })\n .catch(showError(qs('#main')));\n }\n\n function deviceCard(d, collections) {\n var collection = collections[0];\n var card = el('div', { class: 'device-card' + (d.connected ? '' : ' offline') });\n card.appendChild(el('div', { class: 'device-head' }, [\n el('span', { class: 'name ' + deviceColor(d.name), text: d.name }),\n el('span', { class: 'node mono', text: shortNode(d.nodeId) }),\n el('span', { class: 'badge ' + (d.connected ? 'ok' : 'bad'), text: d.connected ? 'online' : 'offline' }),\n d.pendingOperations ? el('span', { class: 'badge neutral', text: d.pendingOperations + ' queued' }) : null,\n el('span', { class: 'spacer' }),\n el('button', { text: d.connected ? 'disconnect' : 'connect', onclick: function () {\n post('/api/lab/devices/' + d.name + '/' + (d.connected ? 'disconnect' : 'connect'), {})\n .then(function () { render(true); }).catch(alertError);\n } }),\n el('button', { text: 'sync', onclick: function () {\n post('/api/lab/devices/' + d.name + '/sync', {}).then(function () { render(true); }).catch(alertError);\n } })\n ]));\n\n var body = el('div', { class: 'device-body' });\n card.appendChild(body);\n\n // Chaos controls\n body.appendChild(el('h5', { text: 'Network chaos (applies on next connect)' }));\n var chaosDefs = [['dropRate', 'drop'], ['duplicateRate', 'duplicate'], ['reorderRate', 'reorder']];\n chaosDefs.forEach(function (cd) {\n var key = cd[0];\n var valueLabel = el('span', { class: 'mono', text: String(d.chaos[key]) });\n var range = el('input', { type: 'range', min: '0', max: '0.5', step: '0.05', value: String(d.chaos[key]) });\n range.oninput = function () { valueLabel.textContent = range.value; };\n range.onchange = function () {\n var body2 = {}; body2[key] = Number(range.value);\n post('/api/lab/devices/' + d.name + '/chaos', body2).catch(alertError);\n };\n body.appendChild(el('div', { class: 'chaos-row' }, [el('span', { text: cd[1] }), range, valueLabel]));\n });\n\n // Records with inline editing\n body.appendChild(el('h5', { text: 'records (' + collection.name + ') — edits happen ON this device' }));\n var recBox = el('div', {});\n body.appendChild(recBox);\n api('/api/collections/' + collection.name + '/records?limit=20&device=' + encodeURIComponent(d.name)).then(function (data) {\n data.records.forEach(function (r) {\n var titleInput = el('input', { type: 'text', value: String(r.fields.title || '') });\n titleInput.onchange = function () {\n post('/api/lab/devices/' + d.name + '/update', { collection: collection.name, id: r.id, data: { title: titleInput.value } }).catch(alertError);\n };\n var doneCb = el('input', { type: 'checkbox', title: 'done' });\n doneCb.checked = !!r.fields.done;\n doneCb.onchange = function () {\n post('/api/lab/devices/' + d.name + '/update', { collection: collection.name, id: r.id, data: { done: doneCb.checked } }).catch(alertError);\n };\n var incBtn = el('button', { text: '+1', title: 'atomic increment — composes across devices', onclick: function () {\n post('/api/lab/devices/' + d.name + '/update', { collection: collection.name, id: r.id, data: {}, increments: { points: 1 } })\n .then(function () { debounceRefresh(); }).catch(alertError);\n } });\n var delBtn = el('button', { class: 'danger', text: '✕', title: 'delete', onclick: function () {\n post('/api/lab/devices/' + d.name + '/delete', { collection: collection.name, id: r.id })\n .then(function () { debounceRefresh(); }).catch(alertError);\n } });\n recBox.appendChild(el('div', { class: 'rec-line' }, [\n doneCb, titleInput,\n el('span', { class: 'pts mono', text: String(r.fields.points === undefined ? '' : r.fields.points) }),\n incBtn, delBtn,\n el('button', { text: '⌕', title: 'inspect', onclick: function () { state.device = d.name; state.collection = collection.name; openRecord(r.id); } })\n ]));\n });\n if (!data.records.length) recBox.appendChild(el('div', { class: 'empty', text: 'no records yet' }));\n });\n\n // Insert form\n var newTitle = el('input', { type: 'text', placeholder: 'new task title…' });\n var insertBtn = el('button', { class: 'primary', text: 'insert', onclick: function () {\n if (!newTitle.value.trim()) return;\n post('/api/lab/devices/' + d.name + '/insert', { collection: collection.name, data: { title: newTitle.value.trim() } })\n .then(function () { newTitle.value = ''; debounceRefresh(); }).catch(alertError);\n } });\n newTitle.addEventListener('keydown', function (e) { if (e.key === 'Enter') insertBtn.click(); });\n body.appendChild(el('div', { class: 'insert-form' }, [newTitle, insertBtn]));\n return card;\n }\n\n function appendFeedLine(event) {\n var feed = qs('#feed');\n if (!feed) return;\n var cls = 'feed-line';\n if (event.type === 'operation:created') cls += ' evt-created';\n if (event.type === 'operation:applied') cls += ' evt-applied';\n if (event.type === 'merge:conflict') cls += ' evt-conflict';\n if (event.type === 'sync:apply-failed') cls += ' evt-failed';\n feed.appendChild(el('div', { class: cls }, [\n el('span', { class: 'mono', text: fmtTime(event.at) }),\n el('span', { class: 'dev ' + deviceColor(event.device), text: event.device }),\n el('span', { class: 'what', text: event.type.replace('operation:', '').replace('sync:', '') + ' · ' + event.summary })\n ]));\n feed.scrollTop = feed.scrollHeight;\n }\n\n // ── Misc ───────────────────────────────────────────────────────────────────\n function showError(main) {\n return function (e) { main.appendChild(el('div', { class: 'error', text: e.message })); };\n }\n function alertError(e) { console.error(e); qs('#dbpath').textContent = 'error: ' + e.message; }\n\n qs('#refresh').onclick = function () { render(true); };\n document.addEventListener('keydown', function (e) { if (e.key === 'Escape') closeDrawer(); });\n})();\n`\n","/** Kora Studio stylesheet (served at /style.css). */\nexport const STUDIO_CSS = `\n:root {\n --bg: #0e1015; --panel: #161a22; --panel-2: #1c212c; --panel-3: #232936;\n --border: #262c3a; --text: #e6e9f0; --muted: #8b93a7;\n --accent: #4fd1c5; --accent-2: #9f7aea;\n --insert: #48bb78; --update: #4299e1; --delete: #f56565; --warn: #ed8936;\n --ok-bg: rgba(72,187,120,.12); --bad-bg: rgba(245,101,101,.12);\n}\n* { box-sizing: border-box; margin: 0; padding: 0; }\nhtml, body { height: 100%; }\nbody {\n background: var(--bg); color: var(--text); overflow: hidden;\n font: 14px/1.5 ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\n}\n.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; }\nbutton {\n background: var(--panel-2); color: var(--text); border: 1px solid var(--border);\n border-radius: 6px; padding: 5px 12px; cursor: pointer; font-size: 13px;\n}\nbutton:hover { border-color: var(--accent); }\nbutton.primary { background: rgba(79,209,197,.12); border-color: var(--accent); color: var(--accent); }\nbutton.danger { color: var(--delete); }\nbutton:disabled { opacity: .4; cursor: default; }\ninput, select {\n background: var(--bg); color: var(--text); border: 1px solid var(--border);\n border-radius: 6px; padding: 4px 8px; font-size: 13px;\n}\ninput:focus, select:focus { outline: none; border-color: var(--accent); }\ninput[type=\"checkbox\"] { width: auto; }\ninput[type=\"range\"] { padding: 0; }\n\nheader {\n display: flex; align-items: center; gap: 14px; padding: 10px 16px;\n background: var(--panel); border-bottom: 1px solid var(--border); height: 48px;\n}\nheader .logo { font-weight: 700; letter-spacing: .3px; white-space: nowrap; }\nheader .logo span { color: var(--accent); }\nheader .modebadge {\n font-size: 10px; text-transform: uppercase; letter-spacing: 1px; padding: 2px 8px;\n border-radius: 10px; background: rgba(159,122,234,.15); color: var(--accent-2);\n}\nheader .dbpath { color: var(--muted); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }\nheader .live { display: flex; align-items: center; gap: 6px; color: var(--muted); font-size: 12px; }\nheader .live .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--muted); }\nheader .live.on .dot { background: var(--insert); box-shadow: 0 0 8px rgba(72,187,120,.8); }\n\n.tabs { display: flex; gap: 2px; padding: 0 16px; background: var(--panel); border-bottom: 1px solid var(--border); }\n.tabs .tab {\n padding: 8px 16px; cursor: pointer; color: var(--muted); font-size: 13px;\n border-bottom: 2px solid transparent; user-select: none;\n}\n.tabs .tab:hover { color: var(--text); }\n.tabs .tab.active { color: var(--accent); border-bottom-color: var(--accent); }\n.tabs .spacer { flex: 1; }\n.tabs .devicepick { display: flex; align-items: center; gap: 6px; padding: 6px 0; color: var(--muted); font-size: 12px; }\n\n.layout { display: flex; height: calc(100vh - 85px); }\nnav.sidebar {\n width: 220px; background: var(--panel); border-right: 1px solid var(--border);\n padding: 12px 0; overflow-y: auto; flex-shrink: 0;\n}\nnav.sidebar h3 { font-size: 11px; text-transform: uppercase; letter-spacing: .8px; color: var(--muted); padding: 8px 16px 4px; }\nnav.sidebar .item {\n display: flex; justify-content: space-between; align-items: center;\n padding: 6px 16px; cursor: pointer; border-left: 2px solid transparent; font-size: 13px;\n}\nnav.sidebar .item:hover { background: var(--panel-2); }\nnav.sidebar .item.active { border-left-color: var(--accent); background: var(--panel-2); }\nnav.sidebar .item .count { color: var(--muted); font-size: 11px; }\nmain { flex: 1; overflow: auto; padding: 16px 20px; }\n\n.toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; flex-wrap: wrap; }\n.toolbar h2 { font-size: 16px; margin-right: auto; }\n.toolbar label { color: var(--muted); font-size: 12px; display: flex; align-items: center; gap: 5px; cursor: pointer; }\n\ntable.grid { width: 100%; border-collapse: collapse; background: var(--panel); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }\ntable.grid th, table.grid td { text-align: left; padding: 7px 10px; border-bottom: 1px solid var(--border); vertical-align: top; }\ntable.grid th { background: var(--panel-2); color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: .5px; position: sticky; top: 0; z-index: 1; }\ntable.grid tbody tr.clickable { cursor: pointer; }\ntable.grid tbody tr.clickable:hover { background: var(--panel-2); }\ntr.tombstone td { color: var(--muted); text-decoration: line-through; }\n\n.badge { display: inline-block; padding: 1px 8px; border-radius: 10px; font-size: 11px; font-weight: 600; }\n.badge.insert { background: rgba(72,187,120,.15); color: var(--insert); }\n.badge.update { background: rgba(66,153,225,.15); color: var(--update); }\n.badge.delete { background: rgba(245,101,101,.15); color: var(--delete); }\n.badge.neutral { background: var(--panel-3); color: var(--muted); }\n.badge.ok { background: var(--ok-bg); color: var(--insert); }\n.badge.bad { background: var(--bad-bg); color: var(--delete); }\n\n.chip {\n display: inline-flex; gap: 6px; align-items: center; background: var(--panel-2);\n border: 1px solid var(--border); border-radius: 6px; padding: 2px 8px; font-size: 11px; color: var(--muted);\n}\n.chip b { color: var(--accent-2); font-weight: 600; }\n\n.pager { display: flex; gap: 8px; align-items: center; margin-top: 10px; color: var(--muted); font-size: 12px; }\n.empty { color: var(--muted); padding: 40px; text-align: center; }\n.error { color: var(--delete); padding: 20px; }\n\n.drawer {\n position: fixed; top: 85px; right: 0; bottom: 0; width: 560px; max-width: 92vw;\n background: var(--panel); border-left: 1px solid var(--border); overflow-y: auto;\n padding: 16px; box-shadow: -12px 0 30px rgba(0,0,0,.45); display: none; z-index: 20;\n}\n.drawer.open { display: block; }\n.drawer h3 { font-size: 14px; margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center; }\n.drawer h4 { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: .6px; margin: 16px 0 8px; }\n.drawer .close { cursor: pointer; color: var(--muted); padding: 4px 8px; }\n.fieldrow { padding: 8px 10px; background: var(--panel-2); border: 1px solid var(--border); border-radius: 8px; margin-bottom: 6px; }\n.fieldrow .fname { color: var(--accent); font-size: 12px; }\n.fieldrow .fval { word-break: break-word; margin: 2px 0 4px; }\n.fieldrow .preview { color: var(--muted); font-style: italic; }\n\n.op { border-left: 2px solid var(--border); margin-left: 6px; padding: 6px 0 6px 14px; position: relative; }\n.op::before { content: \"\"; position: absolute; left: -5px; top: 13px; width: 8px; height: 8px; border-radius: 50%; background: var(--muted); }\n.op.insert::before { background: var(--insert); } .op.update::before { background: var(--update); } .op.delete::before { background: var(--delete); }\n.op .meta { color: var(--muted); font-size: 11px; }\n.op pre { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 6px 8px; margin-top: 4px; overflow-x: auto; }\n\n.panel { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 14px; margin-bottom: 14px; }\n.panel h3 { font-size: 13px; margin-bottom: 8px; color: var(--accent); }\n\n/* DAG */\n.dagwrap { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; overflow-x: auto; margin-bottom: 14px; }\n.dagwrap svg { display: block; }\n.dag-node { cursor: pointer; }\n.dag-node circle { stroke-width: 2; }\n.dag-node text { fill: var(--muted); font-size: 10px; font-family: ui-monospace, Menlo, monospace; }\n.dag-edge { stroke: #3a4356; stroke-width: 1.4; fill: none; marker-end: url(#arrow); }\n.dag-lane-label { fill: var(--accent-2); font-size: 11px; font-family: ui-monospace, Menlo, monospace; }\n.dag-lane-line { stroke: #1d2330; stroke-width: 1; }\n.dag-tip {\n position: fixed; background: var(--panel-3); border: 1px solid var(--border); border-radius: 6px;\n padding: 6px 10px; font-size: 12px; pointer-events: none; z-index: 50; max-width: 380px; display: none;\n}\n\n/* Time travel */\n.tt-controls { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; }\n.tt-controls input[type=\"range\"] { flex: 1; }\n.tt-cut { background: var(--panel-2); border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; margin-bottom: 14px; font-size: 12px; }\n\n/* Lab */\n.lab-top { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; flex-wrap: wrap; }\n.convergence { padding: 8px 14px; border-radius: 8px; font-weight: 600; font-size: 13px; }\n.convergence.ok { background: var(--ok-bg); color: var(--insert); border: 1px solid rgba(72,187,120,.4); }\n.convergence.bad { background: var(--bad-bg); color: var(--delete); border: 1px solid rgba(245,101,101,.4); }\n.convergence .diffs { font-weight: 400; font-size: 11px; margin-top: 4px; color: var(--muted); }\n.lab-grid { display: flex; gap: 14px; align-items: flex-start; }\n.lab-devices { flex: 1; display: grid; grid-template-columns: repeat(auto-fill, minmax(390px, 1fr)); gap: 14px; }\n.device-card { background: var(--panel); border: 1px solid var(--border); border-radius: 10px; overflow: hidden; }\n.device-card.offline { border-color: rgba(245,101,101,.35); }\n.device-head { display: flex; align-items: center; gap: 8px; padding: 10px 12px; background: var(--panel-2); }\n.device-head .name { font-weight: 700; }\n.device-head .node { color: var(--muted); font-size: 11px; }\n.device-head .spacer { flex: 1; }\n.device-body { padding: 10px 12px; }\n.device-body h5 { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: .6px; margin: 10px 0 6px; }\n.chaos-row { display: grid; grid-template-columns: 78px 1fr 44px; gap: 6px; align-items: center; font-size: 11px; color: var(--muted); margin-bottom: 3px; }\n.rec-line { display: flex; align-items: center; gap: 6px; padding: 5px 6px; border-bottom: 1px solid var(--border); font-size: 12px; flex-wrap: wrap; }\n.rec-line:hover { background: var(--panel-2); }\n.rec-line .title { flex: 1; min-width: 120px; }\n.rec-line input[type=\"text\"] { font-size: 12px; padding: 2px 6px; width: 130px; }\n.rec-line .pts { color: var(--accent); min-width: 30px; text-align: right; }\n.insert-form { display: flex; gap: 6px; margin-top: 8px; flex-wrap: wrap; }\n.insert-form input { flex: 1; min-width: 120px; }\n.lab-feed { width: 360px; flex-shrink: 0; background: var(--panel); border: 1px solid var(--border); border-radius: 10px; display: flex; flex-direction: column; max-height: calc(100vh - 200px); }\n.lab-feed h3 { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: .6px; padding: 10px 12px; border-bottom: 1px solid var(--border); }\n.lab-feed .feed { overflow-y: auto; padding: 6px 0; flex: 1; }\n.feed-line { display: flex; gap: 8px; padding: 3px 12px; font-size: 11px; align-items: baseline; }\n.feed-line .dev { min-width: 66px; font-weight: 600; }\n.feed-line .what { color: var(--muted); }\n.feed-line.evt-created .what, .feed-line.evt-applied .what { color: var(--text); }\n.feed-line.evt-conflict .what { color: var(--warn); }\n.feed-line.evt-failed .what { color: var(--delete); font-weight: 700; }\n.dev-c0 { color: #4fd1c5; } .dev-c1 { color: #9f7aea; } .dev-c2 { color: #f6ad55; }\n.dev-c3 { color: #63b3ed; } .dev-c4 { color: #f687b3; } .dev-c5 { color: #68d391; }\n`\n"],"mappings":"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AAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAtB,IAAM,aAAa;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAA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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@korajs/cli",
3
- "version": "0.6.0",
3
+ "version": "1.0.0-beta.0",
4
4
  "description": "Kora.js CLI tooling and project scaffolding",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -36,7 +36,7 @@
36
36
  "conf": "^15.1.0",
37
37
  "esbuild": "^0.28.0",
38
38
  "validate-npm-package-name": "^7.0.2",
39
- "@korajs/core": "0.6.0"
39
+ "@korajs/core": "1.0.0-beta.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/better-sqlite3": "^7.6.13",
@@ -46,10 +46,12 @@
46
46
  "tsup": "^8.3.6",
47
47
  "typescript": "^5.7.3",
48
48
  "vitest": "^3.0.4",
49
- "@korajs/merge": "0.6.0",
50
- "@korajs/store": "0.6.0",
51
- "@korajs/sync": "0.6.0",
52
- "korajs": "0.6.0"
49
+ "@korajs/server": "1.0.0-beta.0",
50
+ "@korajs/test": "1.0.0-beta.0",
51
+ "@korajs/merge": "1.0.0-beta.0",
52
+ "@korajs/store": "1.0.0-beta.0",
53
+ "@korajs/sync": "1.0.0-beta.0",
54
+ "korajs": "1.0.0-beta.0"
53
55
  },
54
56
  "license": "MIT",
55
57
  "scripts": {
@@ -0,0 +1,87 @@
1
+ # AGENTS.md
2
+
3
+ Guidance for AI coding agents working in this project. Humans: this is useful for you too.
4
+
5
+ ## What this project is
6
+
7
+ This is a **Kora.js** application: an offline-first app where all data lives in a local SQLite database (WASM + OPFS in the browser) and optionally syncs across devices through a Kora sync server. Offline is the normal state, not an error state. Full docs: https://korajs.dev and machine-readable index at https://korajs.dev/llms.txt (complete docs in one file: https://korajs.dev/llms-full.txt).
8
+
9
+ ## Golden rules
10
+
11
+ 1. **The schema is the source of truth.** All collections are defined in `src/schema.ts` with `defineSchema` and the `t.*` field builders. To add or change data shapes, edit the schema first; types flow from it automatically. Never hand-write types for records.
12
+ 2. **Never fetch application data over HTTP.** Do not add REST or GraphQL calls for app data, and do not talk to the sync server directly. Read and write through Kora collections only; sync happens automatically in the background.
13
+ 3. **Await readiness before direct collection access.** Outside the UI bindings, `await app.ready` before calling `app.<collection>` methods. The bindings handle this for you inside components.
14
+ 4. **Offline must keep working.** Any feature you add must function with the network off. Never gate a write or a read on connectivity. If you find yourself checking `navigator.onLine` before a data operation, you are doing it wrong.
15
+ 5. **Surface mutation errors.** Fire-and-forget mutate calls swallow errors into the mutation state. Always render the mutation's `error` in the UI or handle the promise from the async variant. Silent failure is the worst failure.
16
+ 6. **Do not touch `src/kora-worker.ts`.** It wires the SQLite WASM binary URL for both dev and production builds. Changing it breaks production builds in ways that only show up after deploy.
17
+ 7. **Do not add a state management library for server or app data.** Kora's reactive queries are the store. Local UI state (form inputs, toggles) can use the framework's normal state tools.
18
+
19
+ ## Data API cheat sheet
20
+
21
+ ```ts
22
+ await app.ready
23
+ const rec = await app.todos.insert({ title: 'x' }) // defaults and .auto() fields applied
24
+ await app.todos.update(rec.id, { completed: true }) // partial update, changed fields only
25
+ await app.todos.delete(rec.id)
26
+ const one = await app.todos.findById(rec.id)
27
+ const unsubscribe = app.todos
28
+ .where({ completed: false })
29
+ .orderBy('createdAt', 'desc')
30
+ .subscribe((rows) => {/* fires immediately, then on every change */})
31
+ ```
32
+
33
+ Schema example (`src/schema.ts`):
34
+
35
+ ```ts
36
+ import { defineSchema, t } from 'korajs'
37
+
38
+ export default defineSchema({
39
+ version: 1,
40
+ collections: {
41
+ todos: {
42
+ fields: {
43
+ title: t.string(),
44
+ completed: t.boolean().default(false),
45
+ tags: t.array(t.string()).default([]),
46
+ priority: t.enum(['low', 'medium', 'high']).default('medium'),
47
+ createdAt: t.timestamp().auto(), // set automatically; never pass it on insert
48
+ },
49
+ indexes: ['completed', 'createdAt'],
50
+ },
51
+ },
52
+ })
53
+ ```
54
+
55
+ If you bump collections in a way that changes shapes, increment `version` and run `npx kora migrate`.
56
+
57
+ ## Project conventions
58
+
59
+ - Feature code lives in `src/modules/<feature>/` with the pattern: `<feature>.schema.ts` (collection definition, imported into `src/schema.ts`), `<feature>.queries.ts` (query builders), `<feature>.mutations.ts` (mutation functions taking a collection accessor).
60
+ - Conflict handling is declarative. Concurrent edits merge automatically (last-write-wins per field, add-wins for arrays). If a field needs domain-specific merging (counters, quantities), add a `resolve` function in the schema rather than writing sync logic.
61
+ - `kora.config.ts` controls the dev environment (ports, sync server, schema watcher).
62
+
63
+ ## Commands
64
+
65
+ - `npm run dev` starts everything: Vite dev server (port 5173), local sync server (port 3001), and the schema watcher.
66
+ - `npm run build` type-checks and builds for production.
67
+ - `npx kora doctor` diagnoses a broken setup.
68
+ - DevTools overlay: press Ctrl+Shift+K (Cmd+Shift+K on macOS) in the running app to inspect operations, merges, and sync status.
69
+
70
+ ## Sync and auth
71
+
72
+ Sync is configured in `src/main.*` via `createApp({ sync: { url, authClient } })`. In dev, the sync URL is derived from the page host and proxied by Vite; in production set `VITE_SYNC_URL`. Auth (if present in this template) uses `@korajs/auth`; the client is created in `src/auth.ts`. Local writes work without sign-in; sync requires the server to accept the connection.
73
+
74
+ ## React bindings
75
+
76
+ ```tsx
77
+ import { useCollection, useMutation, useQuery, useSyncStatus } from '@korajs/react'
78
+
79
+ const todos = useCollection('todos')
80
+ const rows = useQuery(orderedTodos(todos)) // reactive, no loading state for local data
81
+ const addTodo = useMutation((data) => todos.insert(data))
82
+ addTodo.mutate({ title: 'x' }) // fire-and-forget; errors land in addTodo.error
83
+ await addTodo.mutateAsync({ title: 'x' }) // throws on failure; use when you need the error
84
+ const status = useSyncStatus() // status.status, status.pendingOperations
85
+ ```
86
+
87
+ Rules: always render `mutation.error` somewhere. The app must work under React StrictMode (the framework's hooks are StrictMode-safe; keep `<StrictMode>` in `src/main.tsx`). `useQuery` returns data synchronously from the local store, so do not add loading spinners for local reads.
@@ -0,0 +1,87 @@
1
+ # AGENTS.md
2
+
3
+ Guidance for AI coding agents working in this project. Humans: this is useful for you too.
4
+
5
+ ## What this project is
6
+
7
+ This is a **Kora.js** application: an offline-first app where all data lives in a local SQLite database (WASM + OPFS in the browser) and optionally syncs across devices through a Kora sync server. Offline is the normal state, not an error state. Full docs: https://korajs.dev and machine-readable index at https://korajs.dev/llms.txt (complete docs in one file: https://korajs.dev/llms-full.txt).
8
+
9
+ ## Golden rules
10
+
11
+ 1. **The schema is the source of truth.** All collections are defined in `src/schema.ts` with `defineSchema` and the `t.*` field builders. To add or change data shapes, edit the schema first; types flow from it automatically. Never hand-write types for records.
12
+ 2. **Never fetch application data over HTTP.** Do not add REST or GraphQL calls for app data, and do not talk to the sync server directly. Read and write through Kora collections only; sync happens automatically in the background.
13
+ 3. **Await readiness before direct collection access.** Outside the UI bindings, `await app.ready` before calling `app.<collection>` methods. The bindings handle this for you inside components.
14
+ 4. **Offline must keep working.** Any feature you add must function with the network off. Never gate a write or a read on connectivity. If you find yourself checking `navigator.onLine` before a data operation, you are doing it wrong.
15
+ 5. **Surface mutation errors.** Fire-and-forget mutate calls swallow errors into the mutation state. Always render the mutation's `error` in the UI or handle the promise from the async variant. Silent failure is the worst failure.
16
+ 6. **Do not touch `src/kora-worker.ts`.** It wires the SQLite WASM binary URL for both dev and production builds. Changing it breaks production builds in ways that only show up after deploy.
17
+ 7. **Do not add a state management library for server or app data.** Kora's reactive queries are the store. Local UI state (form inputs, toggles) can use the framework's normal state tools.
18
+
19
+ ## Data API cheat sheet
20
+
21
+ ```ts
22
+ await app.ready
23
+ const rec = await app.todos.insert({ title: 'x' }) // defaults and .auto() fields applied
24
+ await app.todos.update(rec.id, { completed: true }) // partial update, changed fields only
25
+ await app.todos.delete(rec.id)
26
+ const one = await app.todos.findById(rec.id)
27
+ const unsubscribe = app.todos
28
+ .where({ completed: false })
29
+ .orderBy('createdAt', 'desc')
30
+ .subscribe((rows) => {/* fires immediately, then on every change */})
31
+ ```
32
+
33
+ Schema example (`src/schema.ts`):
34
+
35
+ ```ts
36
+ import { defineSchema, t } from 'korajs'
37
+
38
+ export default defineSchema({
39
+ version: 1,
40
+ collections: {
41
+ todos: {
42
+ fields: {
43
+ title: t.string(),
44
+ completed: t.boolean().default(false),
45
+ tags: t.array(t.string()).default([]),
46
+ priority: t.enum(['low', 'medium', 'high']).default('medium'),
47
+ createdAt: t.timestamp().auto(), // set automatically; never pass it on insert
48
+ },
49
+ indexes: ['completed', 'createdAt'],
50
+ },
51
+ },
52
+ })
53
+ ```
54
+
55
+ If you bump collections in a way that changes shapes, increment `version` and run `npx kora migrate`.
56
+
57
+ ## Project conventions
58
+
59
+ - Feature code lives in `src/modules/<feature>/` with the pattern: `<feature>.schema.ts` (collection definition, imported into `src/schema.ts`), `<feature>.queries.ts` (query builders), `<feature>.mutations.ts` (mutation functions taking a collection accessor).
60
+ - Conflict handling is declarative. Concurrent edits merge automatically (last-write-wins per field, add-wins for arrays). If a field needs domain-specific merging (counters, quantities), add a `resolve` function in the schema rather than writing sync logic.
61
+ - `kora.config.ts` controls the dev environment (ports, sync server, schema watcher).
62
+
63
+ ## Commands
64
+
65
+ - `npm run dev` starts everything: Vite dev server (port 5173), local sync server (port 3001), and the schema watcher.
66
+ - `npm run build` type-checks and builds for production.
67
+ - `npx kora doctor` diagnoses a broken setup.
68
+ - DevTools overlay: press Ctrl+Shift+K (Cmd+Shift+K on macOS) in the running app to inspect operations, merges, and sync status.
69
+
70
+ ## Sync and auth
71
+
72
+ Sync is configured in `src/main.*` via `createApp({ sync: { url, authClient } })`. In dev, the sync URL is derived from the page host and proxied by Vite; in production set `VITE_SYNC_URL`. Auth (if present in this template) uses `@korajs/auth`; the client is created in `src/auth.ts`. Local writes work without sign-in; sync requires the server to accept the connection.
73
+
74
+ ## React bindings
75
+
76
+ ```tsx
77
+ import { useCollection, useMutation, useQuery, useSyncStatus } from '@korajs/react'
78
+
79
+ const todos = useCollection('todos')
80
+ const rows = useQuery(orderedTodos(todos)) // reactive, no loading state for local data
81
+ const addTodo = useMutation((data) => todos.insert(data))
82
+ addTodo.mutate({ title: 'x' }) // fire-and-forget; errors land in addTodo.error
83
+ await addTodo.mutateAsync({ title: 'x' }) // throws on failure; use when you need the error
84
+ const status = useSyncStatus() // status.status, status.pendingOperations
85
+ ```
86
+
87
+ Rules: always render `mutation.error` somewhere. The app must work under React StrictMode (the framework's hooks are StrictMode-safe; keep `<StrictMode>` in `src/main.tsx`). `useQuery` returns data synchronously from the local store, so do not add loading spinners for local reads.
@@ -52,6 +52,14 @@ export function App() {
52
52
  </div>
53
53
  </div>
54
54
 
55
+ {status.status === 'clock-error' && (
56
+ <div className="clock-error-banner">
57
+ <strong>This device's clock looks wrong.</strong> Your changes are safe on this device,
58
+ but they can't sync until the clock is corrected. Open Settings, find Date &amp; Time, and
59
+ turn on "Set automatically".
60
+ </div>
61
+ )}
62
+
55
63
  {error && <div className="auth-error">{error}</div>}
56
64
 
57
65
  <div className="stats">
@@ -337,3 +337,14 @@ body {
337
337
  font-size: 0.75rem;
338
338
  color: var(--color-text-dim);
339
339
  }
340
+
341
+ .clock-error-banner {
342
+ margin: 0 0 16px;
343
+ padding: 12px 16px;
344
+ border: 1px solid rgba(251, 191, 36, 0.4);
345
+ border-radius: 8px;
346
+ background: rgba(251, 191, 36, 0.1);
347
+ color: #fbbf24;
348
+ font-size: 14px;
349
+ line-height: 1.5;
350
+ }
@@ -0,0 +1,87 @@
1
+ # AGENTS.md
2
+
3
+ Guidance for AI coding agents working in this project. Humans: this is useful for you too.
4
+
5
+ ## What this project is
6
+
7
+ This is a **Kora.js** application: an offline-first app where all data lives in a local SQLite database (WASM + OPFS in the browser) and optionally syncs across devices through a Kora sync server. Offline is the normal state, not an error state. Full docs: https://korajs.dev and machine-readable index at https://korajs.dev/llms.txt (complete docs in one file: https://korajs.dev/llms-full.txt).
8
+
9
+ ## Golden rules
10
+
11
+ 1. **The schema is the source of truth.** All collections are defined in `src/schema.ts` with `defineSchema` and the `t.*` field builders. To add or change data shapes, edit the schema first; types flow from it automatically. Never hand-write types for records.
12
+ 2. **Never fetch application data over HTTP.** Do not add REST or GraphQL calls for app data, and do not talk to the sync server directly. Read and write through Kora collections only; sync happens automatically in the background.
13
+ 3. **Await readiness before direct collection access.** Outside the UI bindings, `await app.ready` before calling `app.<collection>` methods. The bindings handle this for you inside components.
14
+ 4. **Offline must keep working.** Any feature you add must function with the network off. Never gate a write or a read on connectivity. If you find yourself checking `navigator.onLine` before a data operation, you are doing it wrong.
15
+ 5. **Surface mutation errors.** Fire-and-forget mutate calls swallow errors into the mutation state. Always render the mutation's `error` in the UI or handle the promise from the async variant. Silent failure is the worst failure.
16
+ 6. **Do not touch `src/kora-worker.ts`.** It wires the SQLite WASM binary URL for both dev and production builds. Changing it breaks production builds in ways that only show up after deploy.
17
+ 7. **Do not add a state management library for server or app data.** Kora's reactive queries are the store. Local UI state (form inputs, toggles) can use the framework's normal state tools.
18
+
19
+ ## Data API cheat sheet
20
+
21
+ ```ts
22
+ await app.ready
23
+ const rec = await app.todos.insert({ title: 'x' }) // defaults and .auto() fields applied
24
+ await app.todos.update(rec.id, { completed: true }) // partial update, changed fields only
25
+ await app.todos.delete(rec.id)
26
+ const one = await app.todos.findById(rec.id)
27
+ const unsubscribe = app.todos
28
+ .where({ completed: false })
29
+ .orderBy('createdAt', 'desc')
30
+ .subscribe((rows) => {/* fires immediately, then on every change */})
31
+ ```
32
+
33
+ Schema example (`src/schema.ts`):
34
+
35
+ ```ts
36
+ import { defineSchema, t } from 'korajs'
37
+
38
+ export default defineSchema({
39
+ version: 1,
40
+ collections: {
41
+ todos: {
42
+ fields: {
43
+ title: t.string(),
44
+ completed: t.boolean().default(false),
45
+ tags: t.array(t.string()).default([]),
46
+ priority: t.enum(['low', 'medium', 'high']).default('medium'),
47
+ createdAt: t.timestamp().auto(), // set automatically; never pass it on insert
48
+ },
49
+ indexes: ['completed', 'createdAt'],
50
+ },
51
+ },
52
+ })
53
+ ```
54
+
55
+ If you bump collections in a way that changes shapes, increment `version` and run `npx kora migrate`.
56
+
57
+ ## Project conventions
58
+
59
+ - Feature code lives in `src/modules/<feature>/` with the pattern: `<feature>.schema.ts` (collection definition, imported into `src/schema.ts`), `<feature>.queries.ts` (query builders), `<feature>.mutations.ts` (mutation functions taking a collection accessor).
60
+ - Conflict handling is declarative. Concurrent edits merge automatically (last-write-wins per field, add-wins for arrays). If a field needs domain-specific merging (counters, quantities), add a `resolve` function in the schema rather than writing sync logic.
61
+ - `kora.config.ts` controls the dev environment (ports, sync server, schema watcher).
62
+
63
+ ## Commands
64
+
65
+ - `npm run dev` starts everything: Vite dev server (port 5173), local sync server (port 3001), and the schema watcher.
66
+ - `npm run build` type-checks and builds for production.
67
+ - `npx kora doctor` diagnoses a broken setup.
68
+ - DevTools overlay: press Ctrl+Shift+K (Cmd+Shift+K on macOS) in the running app to inspect operations, merges, and sync status.
69
+
70
+ ## Sync and auth
71
+
72
+ Sync is configured in `src/main.*` via `createApp({ sync: { url, authClient } })`. In dev, the sync URL is derived from the page host and proxied by Vite; in production set `VITE_SYNC_URL`. Auth (if present in this template) uses `@korajs/auth`; the client is created in `src/auth.ts`. Local writes work without sign-in; sync requires the server to accept the connection.
73
+
74
+ ## React bindings
75
+
76
+ ```tsx
77
+ import { useCollection, useMutation, useQuery, useSyncStatus } from '@korajs/react'
78
+
79
+ const todos = useCollection('todos')
80
+ const rows = useQuery(orderedTodos(todos)) // reactive, no loading state for local data
81
+ const addTodo = useMutation((data) => todos.insert(data))
82
+ addTodo.mutate({ title: 'x' }) // fire-and-forget; errors land in addTodo.error
83
+ await addTodo.mutateAsync({ title: 'x' }) // throws on failure; use when you need the error
84
+ const status = useSyncStatus() // status.status, status.pendingOperations
85
+ ```
86
+
87
+ Rules: always render `mutation.error` somewhere. The app must work under React StrictMode (the framework's hooks are StrictMode-safe; keep `<StrictMode>` in `src/main.tsx`). `useQuery` returns data synchronously from the local store, so do not add loading spinners for local reads.