From d16817f89e617676be99890922ed1e12eaecf362 Mon Sep 17 00:00:00 2001 From: Seanathon Date: Fri, 26 Jun 2026 19:13:41 -0700 Subject: [PATCH 1/3] chore: add project metadata, typecheck script, allowJs tsconfig, and LICENSE Pre-existing working-tree changes captured as their own commit before the directory reorganization: package.json metadata + typecheck script, @types/better-sqlite3, tsconfig allowJs/checkJs + exclude list, LICENSE, and assorted test/source tweaks. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 1 + LICENSE | 21 +++++++++++++++++++++ README.md | 4 ++++ api/v1.test.ts | 14 +++++++------- capture/url-snapshot.test.ts | 8 ++++---- collections-ui.js | 2 ++ collections-ui.test.ts | 4 ++-- db/archive-backfill.test.ts | 2 +- db/snapshot-asset.ts | 2 +- enrichment/assign.test.ts | 2 +- extension/api-client.test.ts | 10 ++++++---- package-lock.json | 12 ++++++++++++ package.json | 13 +++++++++++++ tsconfig.json | 20 ++++++++++++++++++-- 14 files changed, 93 insertions(+), 22 deletions(-) create mode 100644 LICENSE diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 63595a8..6c90508 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,7 @@ jobs: node-version: 22 cache: npm - run: npm ci + - run: npm run typecheck - run: npm test # Story 11.2: the container image's "test" is build + boot + /healthz + a smoke diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ab6a7a9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Seanathon + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 25ad842..b9e0c46 100644 --- a/README.md +++ b/README.md @@ -143,3 +143,7 @@ asserting `/healthz` and a real in-container screenshot capture. Your data is a plain SQLite file plus a `screenshots/` directory under `DATA_DIR` — copy the directory and walk away. Upgrading the code (a `git pull` / container rebuild) never touches `DATA_DIR`. + +## License + +[MIT](./LICENSE) © Seanathon diff --git a/api/v1.test.ts b/api/v1.test.ts index a081497..fe340f0 100644 --- a/api/v1.test.ts +++ b/api/v1.test.ts @@ -340,7 +340,7 @@ test("13.1: POST /api/v1/items with no boardId lands on the Inbox", async () => }); assert.equal(res.statusCode, 201); const id = JSON.parse(res.body).id; - assert.equal(handle.db.select().from(items).where(eq(items.id, id)).get().boardId, "inbox"); + assert.equal(handle.db.select().from(items).where(eq(items.id, id)).get()!.boardId, "inbox"); } finally { handle.sqlite.close(); fs.rmSync(dir, { recursive: true, force: true }); @@ -364,7 +364,7 @@ test("13.2: POST /api/v1/items {url, title} with no board lands a pending item i assert.equal(res.statusCode, 201); const body = JSON.parse(res.body); assert.equal(body.status, "pending"); - assert.equal(handle.db.select().from(items).where(eq(items.id, body.id)).get().boardId, "inbox"); + assert.equal(handle.db.select().from(items).where(eq(items.id, body.id)).get()!.boardId, "inbox"); } finally { handle.sqlite.close(); fs.rmSync(dir, { recursive: true, force: true }); @@ -521,7 +521,7 @@ test("12.2: PATCH /api/v1/items/:id applies the user-field allowlist", async () body: JSON.stringify({ notes: "hello", favorite: true, status: "done" }), }); assert.equal(res.statusCode, 200); - const row = handle.db.select().from(items).where(eq(items.id, "p1")).get(); + const row = handle.db.select().from(items).where(eq(items.id, "p1")).get()!; assert.equal(row.notes, "hello"); assert.equal(row.favorite, 1); assert.equal(row.status, "ready", "disallowed `status` must be unchanged"); @@ -613,7 +613,7 @@ test("14.2: POST /api/v1/items/assign moves items to the target board (single-FK }); assert.equal(res.statusCode, 200); assert.deepEqual(JSON.parse(res.body).assigned, ["a1"]); - assert.equal(handle.db.select().from(items).where(eq(items.id, "a1")).get().boardId, "library"); + assert.equal(handle.db.select().from(items).where(eq(items.id, "a1")).get()!.boardId, "library"); } finally { handle.sqlite.close(); fs.rmSync(dir, { recursive: true, force: true }); @@ -647,7 +647,7 @@ test("14.2: POST /api/v1/items/assign to an unknown board → 400", async () => body: JSON.stringify({ itemIds: ["a2"], boardId: "no-such-board" }), }); assert.equal(res.statusCode, 400); - assert.equal(handle.db.select().from(items).where(eq(items.id, "a2")).get().boardId, "inbox"); + assert.equal(handle.db.select().from(items).where(eq(items.id, "a2")).get()!.boardId, "inbox"); } finally { handle.sqlite.close(); fs.rmSync(dir, { recursive: true, force: true }); @@ -679,7 +679,7 @@ test("14.3: GET /api/v1/items/:id/suggestion returns null when no provider is co assert.equal(res.statusCode, 200); assert.equal(JSON.parse(res.body).suggestedBoardId, null); // read-only: the item is untouched - assert.equal(handle.db.select().from(items).where(eq(items.id, "s1")).get().boardId, "inbox"); + assert.equal(handle.db.select().from(items).where(eq(items.id, "s1")).get()!.boardId, "inbox"); } finally { handle.sqlite.close(); fs.rmSync(dir, { recursive: true, force: true }); @@ -770,7 +770,7 @@ test("12.2 (NFR-BC): an item from the collections path is visible + mutable via body: JSON.stringify({ notes: "via v1" }), }); assert.equal(patched.statusCode, 200); - assert.equal(handle.db.select().from(items).where(eq(items.id, id)).get().notes, "via v1"); + assert.equal(handle.db.select().from(items).where(eq(items.id, id)).get()!.notes, "via v1"); } finally { handle.sqlite.close(); fs.rmSync(dir, { recursive: true, force: true }); diff --git a/capture/url-snapshot.test.ts b/capture/url-snapshot.test.ts index b51f48a..db0f7cc 100644 --- a/capture/url-snapshot.test.ts +++ b/capture/url-snapshot.test.ts @@ -170,7 +170,7 @@ describe('runSnapshotJob — status-neutral degradation (Story 16.1)', () => { }); const res = await runSnapshotJob(handle, { itemId: 'd1', url: 'https://x', capture, snapshotsDir: join(dir, 'snapshots') }); assert.equal(res.status, 'failed'); - assert.equal(handle.db.select().from(items).where(eq(items.id, 'd1')).get().status, 'done', 'status untouched'); + assert.equal(handle.db.select().from(items).where(eq(items.id, 'd1')).get()!.status, 'done', 'status untouched'); assert.equal(handle.db.select().from(assets).where(eq(assets.itemId, 'd1')).all().length, 0, 'no asset written'); } finally { handle.sqlite.close(); @@ -194,7 +194,7 @@ describe('runSnapshotJob — status-neutral degradation (Story 16.1)', () => { const capture = createUrlSnapshotCapture({ launch: async () => inspectableBrowser().browser, captureHtml: moduleNotFound }); const res = await runSnapshotJob(handle, { itemId: 'd1', url: 'https://x', capture, snapshotsDir: join(dir, 'snapshots') }); assert.equal(res.status, 'failed', 'module-absence is swallowed like any capture failure'); - assert.equal(handle.db.select().from(items).where(eq(items.id, 'd1')).get().status, 'done'); + assert.equal(handle.db.select().from(items).where(eq(items.id, 'd1')).get()!.status, 'done'); assert.equal(handle.db.select().from(assets).where(eq(assets.itemId, 'd1')).all().length, 0); } finally { handle.sqlite.close(); @@ -221,7 +221,7 @@ describe('runSnapshotJob — status-neutral degradation (Story 16.1)', () => { const res = await p; assert.equal(res.status, 'failed'); assert.equal(proc.killed, true, 'the hung browser was SIGKILL-ed on the teardown path'); - assert.equal(handle.db.select().from(items).where(eq(items.id, 'd1')).get().status, 'done'); + assert.equal(handle.db.select().from(items).where(eq(items.id, 'd1')).get()!.status, 'done'); } finally { handle.sqlite.close(); rmSync(dir, { recursive: true, force: true }); @@ -256,7 +256,7 @@ describe('runSnapshotJob — success path through the worker slot (Story 16.1)', assert.ok(row, 'snapshot asset row persisted'); assert.equal(row.kind, 'snapshot'); assert.ok(existsSync(join(dir, 'snapshots', 'ok1.html')), 'snapshot html written'); - assert.equal(handle.db.select().from(items).where(eq(items.id, 'ok1')).get().status, 'done', 'status untouched'); + assert.equal(handle.db.select().from(items).where(eq(items.id, 'ok1')).get()!.status, 'done', 'status untouched'); } finally { handle.sqlite.close(); rmSync(dir, { recursive: true, force: true }); diff --git a/collections-ui.js b/collections-ui.js index 7495c67..5d50b47 100644 --- a/collections-ui.js +++ b/collections-ui.js @@ -69,6 +69,7 @@ export function itemFieldEntries(item, descriptor) { // `[{ key, label, type }]` for each filterable field. NOTE: free-text `q` is NOT a // filter here — full-text search is Story 9.1 (server FTS5); don't reintroduce a // second client text search. +/** @returns {Array<{ key: string, label: string, type: string, values: string[] | null }>} */ export function buildFilters(descriptor) { if (!descriptor || !Array.isArray(descriptor.fields)) return []; return descriptor.fields @@ -295,6 +296,7 @@ export function matchesLibraryFilters(item, { q = "", topic = "", type = "" } = return true; } +/** @returns {Record} topic → occurrence count across all items */ export function topicCounts(items) { const counts = {}; for (const item of items) { diff --git a/collections-ui.test.ts b/collections-ui.test.ts index 02d4260..81fe492 100644 --- a/collections-ui.test.ts +++ b/collections-ui.test.ts @@ -241,8 +241,8 @@ const FILTER_DESCRIPTOR = { view: "list", fields: [ test("buildFilters derives filters from enum/tags fields only (synthetic descriptor)", () => { const filters = buildFilters(FILTER_DESCRIPTOR); assert.deepEqual(filters.map((f) => f.key), ["type", "topics"]); // text/number excluded - assert.equal(filters.find((f) => f.key === "type").type, "enum"); - assert.deepEqual(filters.find((f) => f.key === "type").values, ["article", "video"]); + assert.equal(filters.find((f) => f.key === "type")!.type, "enum"); + assert.deepEqual(filters.find((f) => f.key === "type")!.values, ["article", "video"]); assert.equal(buildFilters(undefined).length, 0); }); diff --git a/db/archive-backfill.test.ts b/db/archive-backfill.test.ts index cfb36ad..f544105 100644 --- a/db/archive-backfill.test.ts +++ b/db/archive-backfill.test.ts @@ -83,7 +83,7 @@ describe('backfillSnapshots (Story 16.3)', () => { assert.ok(handle.db.select().from(assets).where(eq(assets.id, 'k1-snapshot')).get(), 'snapshot added alongside the screenshot'); const shot = handle.db.select().from(assets).where(eq(assets.id, 'k1-shot')).get(); assert.ok(shot && shot.kind === 'screenshot' && shot.hash === 'shot', 'screenshot asset untouched'); - assert.equal((handle.db.select().from(items).where(eq(items.id, 'k1')).get().fields as any).summary, 'keep', 'item fields untouched'); + assert.equal((handle.db.select().from(items).where(eq(items.id, 'k1')).get()!.fields as any).summary, 'keep', 'item fields untouched'); } finally { handle.sqlite.close(); rmSync(dir, { recursive: true, force: true }); diff --git a/db/snapshot-asset.ts b/db/snapshot-asset.ts index 41950db..6c3aba4 100644 --- a/db/snapshot-asset.ts +++ b/db/snapshot-asset.ts @@ -85,7 +85,7 @@ export function writeSnapshotAssetDirect( set: { path: relPath, hash: snapshot.hash, capturedAt: sql`(unixepoch())` }, }) .run(); - return { written: true, asset: { kind: 'snapshot', path: relPath, hash: snapshot.hash } }; + return { written: true, asset: { kind: 'snapshot' as const, path: relPath, hash: snapshot.hash } }; })(); } diff --git a/enrichment/assign.test.ts b/enrichment/assign.test.ts index c598031..f01cd5a 100644 --- a/enrichment/assign.test.ts +++ b/enrichment/assign.test.ts @@ -250,7 +250,7 @@ describe('assignItems archival trigger (Story 16.2)', () => { assert.equal(snaps.length, 1, 'exactly one snapshot enqueued for the promoted item'); assert.deepEqual(snaps[0], { itemId: 'arch1', url: 'https://archive.me/x' }); // the enrichment-WRITTEN takeaway coexists with the snapshot trigger (not clobbered) - const row = handle.db.select().from(items).where(eq(items.id, 'arch1')).get(); + const row = handle.db.select().from(items).where(eq(items.id, 'arch1')).get()!; assert.equal((row.fields as any).summary, 'earned takeaway', 'the earned takeaway the enricher wrote is intact'); assert.equal(row.boardId, LIBRARY_BOARD_ID); } finally { diff --git a/extension/api-client.test.ts b/extension/api-client.test.ts index eece4d4..6327b3c 100644 --- a/extension/api-client.test.ts +++ b/extension/api-client.test.ts @@ -22,7 +22,9 @@ function recordingFetch(response: unknown = {}, status = 200) { json: async () => response, }; }; - return { fetchFn, calls }; + // The client only needs a fetch-shaped callable; cast the minimal stand-in to the + // global fetch type at the injection seam (the test asserts behavior, not types). + return { fetchFn: fetchFn as unknown as typeof fetch, calls }; } // AC 1/5 — save() POSTs the current tab to the authed /api/v1/items with NO board. @@ -101,17 +103,17 @@ test("13.4 (contract): save→Inbox and assign→move work against a real buildS const res = await app.inject({ method: opts.method ?? "GET", url, headers: opts.headers, payload: opts.body }); return { ok: res.statusCode < 400, status: res.statusCode, json: async () => JSON.parse(res.body) }; }; - const client = createBoardClient({ baseUrl: "", token: "test-token", fetch: fetchAdapter }); + const client = createBoardClient({ baseUrl: "", token: "test-token", fetch: fetchAdapter as unknown as typeof fetch }); try { // save() → an Inbox item exists (AC1, → Inbox via the live omitted-board default). const saved = await client.save({ url: "https://ext.example/a", title: "A" }); assert.ok(saved.id, "save returned a created item id"); - assert.equal(handle.db.select().from(items).where(eq(items.id, saved.id)).get().boardId, "inbox"); + assert.equal(handle.db.select().from(items).where(eq(items.id, saved.id)).get()!.boardId, "inbox"); // assign() → board_id actually moved to the target (AC2, the one assign verb). const result = await client.assign(saved.id, "library"); assert.deepEqual(result.assigned, [saved.id], "the live assign endpoint accepted the batch body and moved the item"); - assert.equal(handle.db.select().from(items).where(eq(items.id, saved.id)).get().boardId, "library"); + assert.equal(handle.db.select().from(items).where(eq(items.id, saved.id)).get()!.boardId, "library"); } finally { handle.sqlite.close(); fs.rmSync(dir, { recursive: true, force: true }); diff --git a/package-lock.json b/package-lock.json index 20a8d54..1fce772 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "board", "version": "0.2.0-alpha", + "license": "MIT", "dependencies": { "@fastify/cors": "11.2.0", "@fastify/static": "^8.1.1", @@ -22,6 +23,7 @@ "zod": "3.25.76" }, "devDependencies": { + "@types/better-sqlite3": "^7.6.13", "@types/jsdom": "28.0.3", "@types/turndown": "5.0.6" }, @@ -915,6 +917,16 @@ "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", "license": "MIT" }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/jsdom": { "version": "28.0.3", "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-28.0.3.tgz", diff --git a/package.json b/package.json index dcaa8c5..434665d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,17 @@ { "name": "board", "version": "0.2.0-alpha", + "description": "A lightweight, self-hostable, agent-native curation tool. Node/TypeScript + Fastify + SQLite, built to run on a small LXC.", + "license": "MIT", + "author": "Seanathon ", + "repository": { + "type": "git", + "url": "git+https://github.com/Hayawan/board.git" + }, + "homepage": "https://github.com/Hayawan/board#readme", + "bugs": { + "url": "https://github.com/Hayawan/board/issues" + }, "type": "module", "scripts": { "add": "tsx add.ts", @@ -8,6 +19,7 @@ "start": "node --env-file-if-exists=.env --import tsx server.ts", "import:flat": "tsx db/import-cli.ts", "archive:backfill": "tsx db/archive-backfill-cli.ts", + "typecheck": "tsc --noEmit", "test": "node --import tsx --test --test-concurrency=1 add.test.ts storage.test.ts processors.test.ts processor-library.test.ts library-e2e.test.ts server.test.ts collections-ui.test.ts db/schema.test.ts descriptor/descriptor.test.ts descriptor/render-map.test.ts descriptor/guardrails.test.ts descriptor/inbox-suggest.test.ts db/seed.test.ts db/inbox-seed.test.ts db/queue.test.ts db/worker.test.ts db/status.test.ts db/item-actions.test.ts db/board-actions.test.ts db/search.test.ts db/view.test.ts db/materialize.test.ts db/fts.test.ts db/importer.test.ts db/suggestion-override.test.ts db/export.test.ts db/archive-footprint.test.ts db/archive-backfill.test.ts capture/adapter.test.ts capture/net-guard.test.ts capture/url-screenshot.test.ts capture/url-readable.test.ts capture/manual-upload.test.ts capture/concurrency.test.ts capture/url-snapshot.test.ts enrichment/worker.test.ts enrichment/refetch.test.ts enrichment/pipeline.test.ts enrichment/tier.test.ts enrichment/assign.test.ts enrichment/suggest.test.ts config.test.ts paths.test.ts browser.test.ts server-lock.test.ts api/v1.test.ts capture-clients/bookmarklet.test.ts extension/api-client.test.ts skills/registry.test.ts skills-route.test.ts skills/import-bookmarks.test.ts skills/core-skills.test.ts skills/compose-board.test.ts skills/compose-collection.test.ts skills/generate-fields.test.ts skills/export.test.ts llm/provider.test.ts llm/http-provider.test.ts llm/cli-provider.characterization.test.ts llm/cli-provider.test.ts llm/select-provider.test.ts sse.test.ts" }, "dependencies": { @@ -25,6 +37,7 @@ "zod": "3.25.76" }, "devDependencies": { + "@types/better-sqlite3": "^7.6.13", "@types/jsdom": "28.0.3", "@types/turndown": "5.0.6" }, diff --git a/tsconfig.json b/tsconfig.json index e9e1bd4..291ed60 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,6 +5,22 @@ "moduleResolution": "NodeNext", "strict": true, "esModuleInterop": true, - "skipLibCheck": true - } + "skipLibCheck": true, + // The browser-served helpers (collections-ui.js, descriptor/*.js, extension/ + // api-client.js) have no build step, so they ship as plain .js. allowJs lets tsc + // INFER their types for the .ts tests that import them; checkJs:false means their + // own internals aren't linted (they target the browser, not node). The exclude list + // below drops the .js that use non-DOM globals (service worker `self`/`caches`, + // extension `chrome`, node scripts) — those are run, not type-checked. + "allowJs": true, + "checkJs": false + }, + "exclude": [ + "node_modules", + "scripts", + "get_advice.cjs", + "sw.js", + "extension/popup.js", + "extension/options.js" + ] } From 361e6f5d542dceeab98ef37d665b10acdb36e955 Mon Sep 17 00:00:00 2001 From: Seanathon Date: Fri, 26 Jun 2026 19:25:31 -0700 Subject: [PATCH 2/3] refactor(repo): reorganize into src/ + public/ layout The repo grew from an HTML/JS prototype, leaving ~15 loose backend .ts files at the root next to frontend assets, config JSON, and scattered docs. Move to a conventional layout so server code, browser code, and data are distinguishable at a glance. No runtime behavior change. - src/ all server + browser-shipped code (loose root .ts + api/ capture/ capture-clients/ db/ descriptor/ enrichment/ llm/ skills/, intact) - public/ static shell served as-is (index.html, sw.js, manifest, icons) - docs/ stories/ -> docs/stories, _planning_documents/ -> docs/planning The whole backend tree moved uniformly, so every relative import stayed valid. Only the non-relative couplings were touched: - data reads anchored at repo root (collections.json / taxonomy.json / bookmarks.json / library.json) via path.join(__dirname, "..", ...) so existing local data is preserved in place - server.ts static root -> ../public; two explicit routes serve the browser JS that stays in src/ (/collections-ui.js, /descriptor/render-map.js) - spawn cwd for add.ts kept at repo root (DATA_DIR is cwd-relative) - package.json test script switched to a glob (src/**, extension/**); run-mode paths (Dockerfile CMD, systemd units, README) -> src/server.ts - tsconfig exclude sw.js -> public/sw.js Verified: tsc --noEmit clean; 518 tests across 62 files pass (== baseline); server boots and every served URL returns 200 with the right content-type. Co-Authored-By: Claude Opus 4.8 (1M context) --- Dockerfile | 4 +-- README.md | 2 +- deploy/board-oss.service | 2 +- deploy/proxmoxve/install/board-install.sh | 2 +- .../2026-05-01-bookmark-categories.md | 0 .../1-1-collections-storage-foundation.md | 0 .../1-2-processor-registry-dispatch.md | 0 .../stories}/1-3-library-capture-pipeline.md | 0 .../1-4-library-cli-end-to-end-proof.md | 0 .../stories}/1-5-server-collection-api.md | 0 .../1-6-sidebar-collection-switcher.md | 0 .../stories}/1-7-library-list-view.md | 0 extension/api-client.test.ts | 8 +++--- package.json | 12 ++++----- favicon.png => public/favicon.png | Bin icon-maskable.svg => public/icon-maskable.svg | 0 icon.svg => public/icon.svg | 0 index.html => public/index.html | 0 .../manifest.webmanifest | 0 sw.js => public/sw.js | 0 add.test.ts => src/add.test.ts | 0 add.ts => src/add.ts | 4 +-- {api => src/api}/v1.test.ts | 0 {api => src/api}/v1.ts | 0 browser.test.ts => src/browser.test.ts | 0 browser.ts => src/browser.ts | 0 .../capture-clients}/bookmarklet.test.ts | 0 .../capture-clients}/bookmarklet.ts | 0 {capture => src/capture}/adapter.test.ts | 0 {capture => src/capture}/adapter.ts | 0 {capture => src/capture}/concurrency.test.ts | 0 .../capture}/manual-upload.test.ts | 0 {capture => src/capture}/manual-upload.ts | 0 {capture => src/capture}/net-guard.test.ts | 0 {capture => src/capture}/net-guard.ts | 0 {capture => src/capture}/teardown.ts | 0 {capture => src/capture}/url-readable.test.ts | 0 {capture => src/capture}/url-readable.ts | 0 .../capture}/url-screenshot.test.ts | 0 {capture => src/capture}/url-screenshot.ts | 0 {capture => src/capture}/url-snapshot.test.ts | 0 {capture => src/capture}/url-snapshot.ts | 0 collections-ui.js => src/collections-ui.js | 0 .../collections-ui.test.ts | 0 config.test.ts => src/config.test.ts | 0 config.ts => src/config.ts | 0 .../db}/__fixtures__/bookmarks.sample.json | 0 .../db}/__fixtures__/library.sample.json | 0 {db => src/db}/archive-backfill-cli.ts | 0 {db => src/db}/archive-backfill.test.ts | 0 {db => src/db}/archive-backfill.ts | 0 {db => src/db}/archive-footprint.test.ts | 0 {db => src/db}/archive-footprint.ts | 0 {db => src/db}/board-actions.test.ts | 0 {db => src/db}/board-actions.ts | 0 {db => src/db}/export.test.ts | 0 {db => src/db}/export.ts | 0 {db => src/db}/fts.test.ts | 0 {db => src/db}/hydrate.ts | 0 {db => src/db}/import-cli.ts | 0 {db => src/db}/importer.test.ts | 0 {db => src/db}/importer.ts | 0 {db => src/db}/inbox-seed.test.ts | 0 {db => src/db}/index.ts | 0 {db => src/db}/item-actions.test.ts | 0 {db => src/db}/item-actions.ts | 0 {db => src/db}/materialize.test.ts | 0 {db => src/db}/materialize.ts | 0 {db => src/db}/queue.test.ts | 0 {db => src/db}/queue.ts | 0 {db => src/db}/schema.test.ts | 0 {db => src/db}/schema.ts | 0 {db => src/db}/search-blob.ts | 0 {db => src/db}/search.test.ts | 0 {db => src/db}/search.ts | 0 {db => src/db}/seed.test.ts | 0 {db => src/db}/seed.ts | 0 {db => src/db}/snapshot-asset.ts | 0 {db => src/db}/status.test.ts | 0 {db => src/db}/suggestion-override.test.ts | 0 {db => src/db}/suggestion-override.ts | 0 {db => src/db}/view.test.ts | 0 {db => src/db}/view.ts | 0 {db => src/db}/worker.test.ts | 0 .../descriptor}/descriptor.test.ts | 0 .../descriptor}/guardrails.test.ts | 0 {descriptor => src/descriptor}/guardrails.ts | 0 .../descriptor}/inbox-suggest.js | 0 .../descriptor}/inbox-suggest.test.ts | 0 {descriptor => src/descriptor}/meta-schema.ts | 0 {descriptor => src/descriptor}/render-map.js | 0 .../descriptor}/render-map.test.ts | 0 {descriptor => src/descriptor}/types.ts | 0 {enrichment => src/enrichment}/assign.test.ts | 0 {enrichment => src/enrichment}/assign.ts | 0 .../enrichment}/pipeline.test.ts | 0 {enrichment => src/enrichment}/pipeline.ts | 0 .../enrichment}/refetch.test.ts | 0 {enrichment => src/enrichment}/refetch.ts | 0 .../enrichment}/suggest.test.ts | 0 {enrichment => src/enrichment}/suggest.ts | 0 {enrichment => src/enrichment}/tier.test.ts | 0 {enrichment => src/enrichment}/worker.test.ts | 0 {enrichment => src/enrichment}/worker.ts | 0 .../library-e2e.test.ts | 2 +- .../cli-provider.characterization.test.ts | 0 {llm => src/llm}/cli-provider.test.ts | 0 {llm => src/llm}/cli-provider.ts | 0 {llm => src/llm}/conformance.ts | 0 {llm => src/llm}/http-provider.test.ts | 0 {llm => src/llm}/http-provider.ts | 0 {llm => src/llm}/provider.test.ts | 0 {llm => src/llm}/provider.ts | 0 {llm => src/llm}/select-provider.test.ts | 0 {llm => src/llm}/select-provider.ts | 0 paths.test.ts => src/paths.test.ts | 3 ++- .../processor-library.test.ts | 0 .../processor-library.ts | 0 processors.test.ts => src/processors.test.ts | 0 processors.ts => src/processors.ts | 0 .../server-lock.test.ts | 0 server-lock.ts => src/server-lock.ts | 0 server.test.ts => src/server.test.ts | 2 +- server.ts => src/server.ts | 24 +++++++++++++++--- .../skills-route.test.ts | 0 {skills => src/skills}/add-item.ts | 0 {skills => src/skills}/compose-board.test.ts | 0 {skills => src/skills}/compose-board.ts | 0 .../skills}/compose-collection.test.ts | 0 {skills => src/skills}/compose-collection.ts | 0 {skills => src/skills}/core-skills.test.ts | 0 {skills => src/skills}/create-board.ts | 0 {skills => src/skills}/export.test.ts | 0 {skills => src/skills}/export.ts | 0 .../skills}/generate-fields.test.ts | 0 {skills => src/skills}/generate-fields.ts | 0 .../skills}/import-bookmarks.test.ts | 0 {skills => src/skills}/import-bookmarks.ts | 0 {skills => src/skills}/refetch.ts | 0 {skills => src/skills}/registry.test.ts | 0 {skills => src/skills}/registry.ts | 0 {skills => src/skills}/search.ts | 0 {skills => src/skills}/tag.ts | 0 {skills => src/skills}/types.ts | 0 {skills => src/skills}/upload-asset.ts | 0 sse.test.ts => src/sse.test.ts | 0 sse.ts => src/sse.ts | 0 storage.test.ts => src/storage.test.ts | 4 +-- storage.ts => src/storage.ts | 4 +-- tsconfig.json | 2 +- 150 files changed, 47 insertions(+), 28 deletions(-) rename {_planning_documents => docs/planning}/2026-05-01-bookmark-categories.md (100%) rename {stories => docs/stories}/1-1-collections-storage-foundation.md (100%) rename {stories => docs/stories}/1-2-processor-registry-dispatch.md (100%) rename {stories => docs/stories}/1-3-library-capture-pipeline.md (100%) rename {stories => docs/stories}/1-4-library-cli-end-to-end-proof.md (100%) rename {stories => docs/stories}/1-5-server-collection-api.md (100%) rename {stories => docs/stories}/1-6-sidebar-collection-switcher.md (100%) rename {stories => docs/stories}/1-7-library-list-view.md (100%) rename favicon.png => public/favicon.png (100%) rename icon-maskable.svg => public/icon-maskable.svg (100%) rename icon.svg => public/icon.svg (100%) rename index.html => public/index.html (100%) rename manifest.webmanifest => public/manifest.webmanifest (100%) rename sw.js => public/sw.js (100%) rename add.test.ts => src/add.test.ts (100%) rename add.ts => src/add.ts (99%) rename {api => src/api}/v1.test.ts (100%) rename {api => src/api}/v1.ts (100%) rename browser.test.ts => src/browser.test.ts (100%) rename browser.ts => src/browser.ts (100%) rename {capture-clients => src/capture-clients}/bookmarklet.test.ts (100%) rename {capture-clients => src/capture-clients}/bookmarklet.ts (100%) rename {capture => src/capture}/adapter.test.ts (100%) rename {capture => src/capture}/adapter.ts (100%) rename {capture => src/capture}/concurrency.test.ts (100%) rename {capture => src/capture}/manual-upload.test.ts (100%) rename {capture => src/capture}/manual-upload.ts (100%) rename {capture => src/capture}/net-guard.test.ts (100%) rename {capture => src/capture}/net-guard.ts (100%) rename {capture => src/capture}/teardown.ts (100%) rename {capture => src/capture}/url-readable.test.ts (100%) rename {capture => src/capture}/url-readable.ts (100%) rename {capture => src/capture}/url-screenshot.test.ts (100%) rename {capture => src/capture}/url-screenshot.ts (100%) rename {capture => src/capture}/url-snapshot.test.ts (100%) rename {capture => src/capture}/url-snapshot.ts (100%) rename collections-ui.js => src/collections-ui.js (100%) rename collections-ui.test.ts => src/collections-ui.test.ts (100%) rename config.test.ts => src/config.test.ts (100%) rename config.ts => src/config.ts (100%) rename {db => src/db}/__fixtures__/bookmarks.sample.json (100%) rename {db => src/db}/__fixtures__/library.sample.json (100%) rename {db => src/db}/archive-backfill-cli.ts (100%) rename {db => src/db}/archive-backfill.test.ts (100%) rename {db => src/db}/archive-backfill.ts (100%) rename {db => src/db}/archive-footprint.test.ts (100%) rename {db => src/db}/archive-footprint.ts (100%) rename {db => src/db}/board-actions.test.ts (100%) rename {db => src/db}/board-actions.ts (100%) rename {db => src/db}/export.test.ts (100%) rename {db => src/db}/export.ts (100%) rename {db => src/db}/fts.test.ts (100%) rename {db => src/db}/hydrate.ts (100%) rename {db => src/db}/import-cli.ts (100%) rename {db => src/db}/importer.test.ts (100%) rename {db => src/db}/importer.ts (100%) rename {db => src/db}/inbox-seed.test.ts (100%) rename {db => src/db}/index.ts (100%) rename {db => src/db}/item-actions.test.ts (100%) rename {db => src/db}/item-actions.ts (100%) rename {db => src/db}/materialize.test.ts (100%) rename {db => src/db}/materialize.ts (100%) rename {db => src/db}/queue.test.ts (100%) rename {db => src/db}/queue.ts (100%) rename {db => src/db}/schema.test.ts (100%) rename {db => src/db}/schema.ts (100%) rename {db => src/db}/search-blob.ts (100%) rename {db => src/db}/search.test.ts (100%) rename {db => src/db}/search.ts (100%) rename {db => src/db}/seed.test.ts (100%) rename {db => src/db}/seed.ts (100%) rename {db => src/db}/snapshot-asset.ts (100%) rename {db => src/db}/status.test.ts (100%) rename {db => src/db}/suggestion-override.test.ts (100%) rename {db => src/db}/suggestion-override.ts (100%) rename {db => src/db}/view.test.ts (100%) rename {db => src/db}/view.ts (100%) rename {db => src/db}/worker.test.ts (100%) rename {descriptor => src/descriptor}/descriptor.test.ts (100%) rename {descriptor => src/descriptor}/guardrails.test.ts (100%) rename {descriptor => src/descriptor}/guardrails.ts (100%) rename {descriptor => src/descriptor}/inbox-suggest.js (100%) rename {descriptor => src/descriptor}/inbox-suggest.test.ts (100%) rename {descriptor => src/descriptor}/meta-schema.ts (100%) rename {descriptor => src/descriptor}/render-map.js (100%) rename {descriptor => src/descriptor}/render-map.test.ts (100%) rename {descriptor => src/descriptor}/types.ts (100%) rename {enrichment => src/enrichment}/assign.test.ts (100%) rename {enrichment => src/enrichment}/assign.ts (100%) rename {enrichment => src/enrichment}/pipeline.test.ts (100%) rename {enrichment => src/enrichment}/pipeline.ts (100%) rename {enrichment => src/enrichment}/refetch.test.ts (100%) rename {enrichment => src/enrichment}/refetch.ts (100%) rename {enrichment => src/enrichment}/suggest.test.ts (100%) rename {enrichment => src/enrichment}/suggest.ts (100%) rename {enrichment => src/enrichment}/tier.test.ts (100%) rename {enrichment => src/enrichment}/worker.test.ts (100%) rename {enrichment => src/enrichment}/worker.ts (100%) rename library-e2e.test.ts => src/library-e2e.test.ts (98%) rename {llm => src/llm}/cli-provider.characterization.test.ts (100%) rename {llm => src/llm}/cli-provider.test.ts (100%) rename {llm => src/llm}/cli-provider.ts (100%) rename {llm => src/llm}/conformance.ts (100%) rename {llm => src/llm}/http-provider.test.ts (100%) rename {llm => src/llm}/http-provider.ts (100%) rename {llm => src/llm}/provider.test.ts (100%) rename {llm => src/llm}/provider.ts (100%) rename {llm => src/llm}/select-provider.test.ts (100%) rename {llm => src/llm}/select-provider.ts (100%) rename paths.test.ts => src/paths.test.ts (91%) rename processor-library.test.ts => src/processor-library.test.ts (100%) rename processor-library.ts => src/processor-library.ts (100%) rename processors.test.ts => src/processors.test.ts (100%) rename processors.ts => src/processors.ts (100%) rename server-lock.test.ts => src/server-lock.test.ts (100%) rename server-lock.ts => src/server-lock.ts (100%) rename server.test.ts => src/server.test.ts (99%) rename server.ts => src/server.ts (96%) rename skills-route.test.ts => src/skills-route.test.ts (100%) rename {skills => src/skills}/add-item.ts (100%) rename {skills => src/skills}/compose-board.test.ts (100%) rename {skills => src/skills}/compose-board.ts (100%) rename {skills => src/skills}/compose-collection.test.ts (100%) rename {skills => src/skills}/compose-collection.ts (100%) rename {skills => src/skills}/core-skills.test.ts (100%) rename {skills => src/skills}/create-board.ts (100%) rename {skills => src/skills}/export.test.ts (100%) rename {skills => src/skills}/export.ts (100%) rename {skills => src/skills}/generate-fields.test.ts (100%) rename {skills => src/skills}/generate-fields.ts (100%) rename {skills => src/skills}/import-bookmarks.test.ts (100%) rename {skills => src/skills}/import-bookmarks.ts (100%) rename {skills => src/skills}/refetch.ts (100%) rename {skills => src/skills}/registry.test.ts (100%) rename {skills => src/skills}/registry.ts (100%) rename {skills => src/skills}/search.ts (100%) rename {skills => src/skills}/tag.ts (100%) rename {skills => src/skills}/types.ts (100%) rename {skills => src/skills}/upload-asset.ts (100%) rename sse.test.ts => src/sse.test.ts (100%) rename sse.ts => src/sse.ts (100%) rename storage.test.ts => src/storage.test.ts (95%) rename storage.ts => src/storage.ts (95%) diff --git a/Dockerfile b/Dockerfile index 67d5227..fc549f5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends python3 build-e && rm -rf /var/lib/apt/lists/* COPY package.json package-lock.json ./ # Runtime deps only (tsx + typescript are runtime deps — Story 11.1 — so the app runs -# via `node --import tsx server.ts` with no build step). +# via `node --import tsx src/server.ts` with no build step). RUN npm ci --omit=dev # ---- runtime ---------------------------------------------------------------- @@ -60,4 +60,4 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ # Chromium-in-container uses the no-sandbox launch args already set in Story 6.2 # (--no-sandbox --disable-setuid-sandbox --disable-dev-shm-usage) — no --privileged # / SYS_ADMIN needed. -CMD ["node", "--import", "tsx", "server.ts"] +CMD ["node", "--import", "tsx", "src/server.ts"] diff --git a/README.md b/README.md index b9e0c46..06792e0 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ installs the app to `/opt/board-oss` with a persistent `DATA_DIR` at (`deploy/board-oss.service`), and waits for `/healthz`. Tunables via env: `APP_DIR`, `DATA_DIR`, `PORT`, `APP_USER`. -- **Run mode:** the service runs `node --import tsx server.ts` (no build step; `tsx` +- **Run mode:** the service runs `node --import tsx src/server.ts` (no build step; `tsx` + `typescript` are runtime deps, so `npm ci --omit=dev` keeps them). - **`better-sqlite3`** uses its prebuilt binary on glibc Linux / Node LTS (no compiler needed). If a from-source build is ever required, `apt-get install -y diff --git a/deploy/board-oss.service b/deploy/board-oss.service index 48fee4c..65d8875 100644 --- a/deploy/board-oss.service +++ b/deploy/board-oss.service @@ -11,7 +11,7 @@ WorkingDirectory=/opt/board-oss # Story 11.1: production run mode = tsx at runtime (no build step). tsx + typescript # are runtime dependencies, so `npm ci --omit=dev` keeps them. -ExecStart=/usr/bin/env node --import tsx server.ts +ExecStart=/usr/bin/env node --import tsx src/server.ts # Epic 2 config seam. localhost-only bind (Story 2.4) — auth/TLS is the reverse proxy's # job. DATA_DIR (Story 2.2) is persistent + separate from the code dir, so upgrades / diff --git a/deploy/proxmoxve/install/board-install.sh b/deploy/proxmoxve/install/board-install.sh index 92c7e49..5b27609 100644 --- a/deploy/proxmoxve/install/board-install.sh +++ b/deploy/proxmoxve/install/board-install.sh @@ -54,7 +54,7 @@ After=network.target Type=simple WorkingDirectory=/opt/board EnvironmentFile=/opt/board/.env -ExecStart=/usr/bin/env node --import tsx server.ts +ExecStart=/usr/bin/env node --import tsx src/server.ts Restart=on-failure RestartSec=5 diff --git a/_planning_documents/2026-05-01-bookmark-categories.md b/docs/planning/2026-05-01-bookmark-categories.md similarity index 100% rename from _planning_documents/2026-05-01-bookmark-categories.md rename to docs/planning/2026-05-01-bookmark-categories.md diff --git a/stories/1-1-collections-storage-foundation.md b/docs/stories/1-1-collections-storage-foundation.md similarity index 100% rename from stories/1-1-collections-storage-foundation.md rename to docs/stories/1-1-collections-storage-foundation.md diff --git a/stories/1-2-processor-registry-dispatch.md b/docs/stories/1-2-processor-registry-dispatch.md similarity index 100% rename from stories/1-2-processor-registry-dispatch.md rename to docs/stories/1-2-processor-registry-dispatch.md diff --git a/stories/1-3-library-capture-pipeline.md b/docs/stories/1-3-library-capture-pipeline.md similarity index 100% rename from stories/1-3-library-capture-pipeline.md rename to docs/stories/1-3-library-capture-pipeline.md diff --git a/stories/1-4-library-cli-end-to-end-proof.md b/docs/stories/1-4-library-cli-end-to-end-proof.md similarity index 100% rename from stories/1-4-library-cli-end-to-end-proof.md rename to docs/stories/1-4-library-cli-end-to-end-proof.md diff --git a/stories/1-5-server-collection-api.md b/docs/stories/1-5-server-collection-api.md similarity index 100% rename from stories/1-5-server-collection-api.md rename to docs/stories/1-5-server-collection-api.md diff --git a/stories/1-6-sidebar-collection-switcher.md b/docs/stories/1-6-sidebar-collection-switcher.md similarity index 100% rename from stories/1-6-sidebar-collection-switcher.md rename to docs/stories/1-6-sidebar-collection-switcher.md diff --git a/stories/1-7-library-list-view.md b/docs/stories/1-7-library-list-view.md similarity index 100% rename from stories/1-7-library-list-view.md rename to docs/stories/1-7-library-list-view.md diff --git a/extension/api-client.test.ts b/extension/api-client.test.ts index 6327b3c..b6808a8 100644 --- a/extension/api-client.test.ts +++ b/extension/api-client.test.ts @@ -89,10 +89,10 @@ test("13.4: reviewAction() shows a chip for a suggestion, falls back to manual w // in the Inbox; assign() actually moves board_id. This is what makes the mocks above // trustworthy (e.g. it would catch a wrong assign body shape — the real route 400s). test("13.4 (contract): save→Inbox and assign→move work against a real buildServer", async () => { - const { buildServer } = await import("../server.js"); - const { initDb } = await import("../db/index.js"); - const { seed } = await import("../db/seed.js"); - const { items } = await import("../db/schema.js"); + const { buildServer } = await import("../src/server.js"); + const { initDb } = await import("../src/db/index.js"); + const { seed } = await import("../src/db/seed.js"); + const { items } = await import("../src/db/schema.js"); const { eq } = await import("drizzle-orm"); const dir = fs.mkdtempSync(path.join(os.tmpdir(), "board-oss-ext-")); const handle = initDb(path.join(dir, "c.db")); diff --git a/package.json b/package.json index 434665d..3875ef2 100644 --- a/package.json +++ b/package.json @@ -14,13 +14,13 @@ }, "type": "module", "scripts": { - "add": "tsx add.ts", - "dev": "node --env-file-if-exists=.env --import tsx server.ts", - "start": "node --env-file-if-exists=.env --import tsx server.ts", - "import:flat": "tsx db/import-cli.ts", - "archive:backfill": "tsx db/archive-backfill-cli.ts", + "add": "tsx src/add.ts", + "dev": "node --env-file-if-exists=.env --import tsx src/server.ts", + "start": "node --env-file-if-exists=.env --import tsx src/server.ts", + "import:flat": "tsx src/db/import-cli.ts", + "archive:backfill": "tsx src/db/archive-backfill-cli.ts", "typecheck": "tsc --noEmit", - "test": "node --import tsx --test --test-concurrency=1 add.test.ts storage.test.ts processors.test.ts processor-library.test.ts library-e2e.test.ts server.test.ts collections-ui.test.ts db/schema.test.ts descriptor/descriptor.test.ts descriptor/render-map.test.ts descriptor/guardrails.test.ts descriptor/inbox-suggest.test.ts db/seed.test.ts db/inbox-seed.test.ts db/queue.test.ts db/worker.test.ts db/status.test.ts db/item-actions.test.ts db/board-actions.test.ts db/search.test.ts db/view.test.ts db/materialize.test.ts db/fts.test.ts db/importer.test.ts db/suggestion-override.test.ts db/export.test.ts db/archive-footprint.test.ts db/archive-backfill.test.ts capture/adapter.test.ts capture/net-guard.test.ts capture/url-screenshot.test.ts capture/url-readable.test.ts capture/manual-upload.test.ts capture/concurrency.test.ts capture/url-snapshot.test.ts enrichment/worker.test.ts enrichment/refetch.test.ts enrichment/pipeline.test.ts enrichment/tier.test.ts enrichment/assign.test.ts enrichment/suggest.test.ts config.test.ts paths.test.ts browser.test.ts server-lock.test.ts api/v1.test.ts capture-clients/bookmarklet.test.ts extension/api-client.test.ts skills/registry.test.ts skills-route.test.ts skills/import-bookmarks.test.ts skills/core-skills.test.ts skills/compose-board.test.ts skills/compose-collection.test.ts skills/generate-fields.test.ts skills/export.test.ts llm/provider.test.ts llm/http-provider.test.ts llm/cli-provider.characterization.test.ts llm/cli-provider.test.ts llm/select-provider.test.ts sse.test.ts" + "test": "node --import tsx --test --test-concurrency=1 \"src/**/*.test.ts\" \"extension/**/*.test.ts\"" }, "dependencies": { "@fastify/cors": "11.2.0", diff --git a/favicon.png b/public/favicon.png similarity index 100% rename from favicon.png rename to public/favicon.png diff --git a/icon-maskable.svg b/public/icon-maskable.svg similarity index 100% rename from icon-maskable.svg rename to public/icon-maskable.svg diff --git a/icon.svg b/public/icon.svg similarity index 100% rename from icon.svg rename to public/icon.svg diff --git a/index.html b/public/index.html similarity index 100% rename from index.html rename to public/index.html diff --git a/manifest.webmanifest b/public/manifest.webmanifest similarity index 100% rename from manifest.webmanifest rename to public/manifest.webmanifest diff --git a/sw.js b/public/sw.js similarity index 100% rename from sw.js rename to public/sw.js diff --git a/add.test.ts b/src/add.test.ts similarity index 100% rename from add.test.ts rename to src/add.test.ts diff --git a/add.ts b/src/add.ts similarity index 99% rename from add.ts rename to src/add.ts index 17b0643..f8a05bb 100644 --- a/add.ts +++ b/src/add.ts @@ -12,7 +12,7 @@ import { launchBrowser } from "./browser.js"; import { config } from "./config.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const TAXONOMY_FILE = path.join(__dirname, "taxonomy.json"); +const TAXONOMY_FILE = path.join(__dirname, "..", "taxonomy.json"); // Story 2.2: screenshots live under DATA_DIR (config.screenshotsDir), not the app tree. const SCREENSHOTS_DIR = config.screenshotsDir; @@ -450,7 +450,7 @@ async function analyze( fs.writeFileSync(schemaFile, JSON.stringify(outputSchema)); const { command, args } = buildAnalysisCommand(agent, prompt, processor.schema, processor.systemPrompt, { schemaFile, resultFile }); const result = spawnSync(command, args, { - cwd: __dirname, + cwd: path.join(__dirname, ".."), encoding: "utf-8", maxBuffer: 10 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"], diff --git a/api/v1.test.ts b/src/api/v1.test.ts similarity index 100% rename from api/v1.test.ts rename to src/api/v1.test.ts diff --git a/api/v1.ts b/src/api/v1.ts similarity index 100% rename from api/v1.ts rename to src/api/v1.ts diff --git a/browser.test.ts b/src/browser.test.ts similarity index 100% rename from browser.test.ts rename to src/browser.test.ts diff --git a/browser.ts b/src/browser.ts similarity index 100% rename from browser.ts rename to src/browser.ts diff --git a/capture-clients/bookmarklet.test.ts b/src/capture-clients/bookmarklet.test.ts similarity index 100% rename from capture-clients/bookmarklet.test.ts rename to src/capture-clients/bookmarklet.test.ts diff --git a/capture-clients/bookmarklet.ts b/src/capture-clients/bookmarklet.ts similarity index 100% rename from capture-clients/bookmarklet.ts rename to src/capture-clients/bookmarklet.ts diff --git a/capture/adapter.test.ts b/src/capture/adapter.test.ts similarity index 100% rename from capture/adapter.test.ts rename to src/capture/adapter.test.ts diff --git a/capture/adapter.ts b/src/capture/adapter.ts similarity index 100% rename from capture/adapter.ts rename to src/capture/adapter.ts diff --git a/capture/concurrency.test.ts b/src/capture/concurrency.test.ts similarity index 100% rename from capture/concurrency.test.ts rename to src/capture/concurrency.test.ts diff --git a/capture/manual-upload.test.ts b/src/capture/manual-upload.test.ts similarity index 100% rename from capture/manual-upload.test.ts rename to src/capture/manual-upload.test.ts diff --git a/capture/manual-upload.ts b/src/capture/manual-upload.ts similarity index 100% rename from capture/manual-upload.ts rename to src/capture/manual-upload.ts diff --git a/capture/net-guard.test.ts b/src/capture/net-guard.test.ts similarity index 100% rename from capture/net-guard.test.ts rename to src/capture/net-guard.test.ts diff --git a/capture/net-guard.ts b/src/capture/net-guard.ts similarity index 100% rename from capture/net-guard.ts rename to src/capture/net-guard.ts diff --git a/capture/teardown.ts b/src/capture/teardown.ts similarity index 100% rename from capture/teardown.ts rename to src/capture/teardown.ts diff --git a/capture/url-readable.test.ts b/src/capture/url-readable.test.ts similarity index 100% rename from capture/url-readable.test.ts rename to src/capture/url-readable.test.ts diff --git a/capture/url-readable.ts b/src/capture/url-readable.ts similarity index 100% rename from capture/url-readable.ts rename to src/capture/url-readable.ts diff --git a/capture/url-screenshot.test.ts b/src/capture/url-screenshot.test.ts similarity index 100% rename from capture/url-screenshot.test.ts rename to src/capture/url-screenshot.test.ts diff --git a/capture/url-screenshot.ts b/src/capture/url-screenshot.ts similarity index 100% rename from capture/url-screenshot.ts rename to src/capture/url-screenshot.ts diff --git a/capture/url-snapshot.test.ts b/src/capture/url-snapshot.test.ts similarity index 100% rename from capture/url-snapshot.test.ts rename to src/capture/url-snapshot.test.ts diff --git a/capture/url-snapshot.ts b/src/capture/url-snapshot.ts similarity index 100% rename from capture/url-snapshot.ts rename to src/capture/url-snapshot.ts diff --git a/collections-ui.js b/src/collections-ui.js similarity index 100% rename from collections-ui.js rename to src/collections-ui.js diff --git a/collections-ui.test.ts b/src/collections-ui.test.ts similarity index 100% rename from collections-ui.test.ts rename to src/collections-ui.test.ts diff --git a/config.test.ts b/src/config.test.ts similarity index 100% rename from config.test.ts rename to src/config.test.ts diff --git a/config.ts b/src/config.ts similarity index 100% rename from config.ts rename to src/config.ts diff --git a/db/__fixtures__/bookmarks.sample.json b/src/db/__fixtures__/bookmarks.sample.json similarity index 100% rename from db/__fixtures__/bookmarks.sample.json rename to src/db/__fixtures__/bookmarks.sample.json diff --git a/db/__fixtures__/library.sample.json b/src/db/__fixtures__/library.sample.json similarity index 100% rename from db/__fixtures__/library.sample.json rename to src/db/__fixtures__/library.sample.json diff --git a/db/archive-backfill-cli.ts b/src/db/archive-backfill-cli.ts similarity index 100% rename from db/archive-backfill-cli.ts rename to src/db/archive-backfill-cli.ts diff --git a/db/archive-backfill.test.ts b/src/db/archive-backfill.test.ts similarity index 100% rename from db/archive-backfill.test.ts rename to src/db/archive-backfill.test.ts diff --git a/db/archive-backfill.ts b/src/db/archive-backfill.ts similarity index 100% rename from db/archive-backfill.ts rename to src/db/archive-backfill.ts diff --git a/db/archive-footprint.test.ts b/src/db/archive-footprint.test.ts similarity index 100% rename from db/archive-footprint.test.ts rename to src/db/archive-footprint.test.ts diff --git a/db/archive-footprint.ts b/src/db/archive-footprint.ts similarity index 100% rename from db/archive-footprint.ts rename to src/db/archive-footprint.ts diff --git a/db/board-actions.test.ts b/src/db/board-actions.test.ts similarity index 100% rename from db/board-actions.test.ts rename to src/db/board-actions.test.ts diff --git a/db/board-actions.ts b/src/db/board-actions.ts similarity index 100% rename from db/board-actions.ts rename to src/db/board-actions.ts diff --git a/db/export.test.ts b/src/db/export.test.ts similarity index 100% rename from db/export.test.ts rename to src/db/export.test.ts diff --git a/db/export.ts b/src/db/export.ts similarity index 100% rename from db/export.ts rename to src/db/export.ts diff --git a/db/fts.test.ts b/src/db/fts.test.ts similarity index 100% rename from db/fts.test.ts rename to src/db/fts.test.ts diff --git a/db/hydrate.ts b/src/db/hydrate.ts similarity index 100% rename from db/hydrate.ts rename to src/db/hydrate.ts diff --git a/db/import-cli.ts b/src/db/import-cli.ts similarity index 100% rename from db/import-cli.ts rename to src/db/import-cli.ts diff --git a/db/importer.test.ts b/src/db/importer.test.ts similarity index 100% rename from db/importer.test.ts rename to src/db/importer.test.ts diff --git a/db/importer.ts b/src/db/importer.ts similarity index 100% rename from db/importer.ts rename to src/db/importer.ts diff --git a/db/inbox-seed.test.ts b/src/db/inbox-seed.test.ts similarity index 100% rename from db/inbox-seed.test.ts rename to src/db/inbox-seed.test.ts diff --git a/db/index.ts b/src/db/index.ts similarity index 100% rename from db/index.ts rename to src/db/index.ts diff --git a/db/item-actions.test.ts b/src/db/item-actions.test.ts similarity index 100% rename from db/item-actions.test.ts rename to src/db/item-actions.test.ts diff --git a/db/item-actions.ts b/src/db/item-actions.ts similarity index 100% rename from db/item-actions.ts rename to src/db/item-actions.ts diff --git a/db/materialize.test.ts b/src/db/materialize.test.ts similarity index 100% rename from db/materialize.test.ts rename to src/db/materialize.test.ts diff --git a/db/materialize.ts b/src/db/materialize.ts similarity index 100% rename from db/materialize.ts rename to src/db/materialize.ts diff --git a/db/queue.test.ts b/src/db/queue.test.ts similarity index 100% rename from db/queue.test.ts rename to src/db/queue.test.ts diff --git a/db/queue.ts b/src/db/queue.ts similarity index 100% rename from db/queue.ts rename to src/db/queue.ts diff --git a/db/schema.test.ts b/src/db/schema.test.ts similarity index 100% rename from db/schema.test.ts rename to src/db/schema.test.ts diff --git a/db/schema.ts b/src/db/schema.ts similarity index 100% rename from db/schema.ts rename to src/db/schema.ts diff --git a/db/search-blob.ts b/src/db/search-blob.ts similarity index 100% rename from db/search-blob.ts rename to src/db/search-blob.ts diff --git a/db/search.test.ts b/src/db/search.test.ts similarity index 100% rename from db/search.test.ts rename to src/db/search.test.ts diff --git a/db/search.ts b/src/db/search.ts similarity index 100% rename from db/search.ts rename to src/db/search.ts diff --git a/db/seed.test.ts b/src/db/seed.test.ts similarity index 100% rename from db/seed.test.ts rename to src/db/seed.test.ts diff --git a/db/seed.ts b/src/db/seed.ts similarity index 100% rename from db/seed.ts rename to src/db/seed.ts diff --git a/db/snapshot-asset.ts b/src/db/snapshot-asset.ts similarity index 100% rename from db/snapshot-asset.ts rename to src/db/snapshot-asset.ts diff --git a/db/status.test.ts b/src/db/status.test.ts similarity index 100% rename from db/status.test.ts rename to src/db/status.test.ts diff --git a/db/suggestion-override.test.ts b/src/db/suggestion-override.test.ts similarity index 100% rename from db/suggestion-override.test.ts rename to src/db/suggestion-override.test.ts diff --git a/db/suggestion-override.ts b/src/db/suggestion-override.ts similarity index 100% rename from db/suggestion-override.ts rename to src/db/suggestion-override.ts diff --git a/db/view.test.ts b/src/db/view.test.ts similarity index 100% rename from db/view.test.ts rename to src/db/view.test.ts diff --git a/db/view.ts b/src/db/view.ts similarity index 100% rename from db/view.ts rename to src/db/view.ts diff --git a/db/worker.test.ts b/src/db/worker.test.ts similarity index 100% rename from db/worker.test.ts rename to src/db/worker.test.ts diff --git a/descriptor/descriptor.test.ts b/src/descriptor/descriptor.test.ts similarity index 100% rename from descriptor/descriptor.test.ts rename to src/descriptor/descriptor.test.ts diff --git a/descriptor/guardrails.test.ts b/src/descriptor/guardrails.test.ts similarity index 100% rename from descriptor/guardrails.test.ts rename to src/descriptor/guardrails.test.ts diff --git a/descriptor/guardrails.ts b/src/descriptor/guardrails.ts similarity index 100% rename from descriptor/guardrails.ts rename to src/descriptor/guardrails.ts diff --git a/descriptor/inbox-suggest.js b/src/descriptor/inbox-suggest.js similarity index 100% rename from descriptor/inbox-suggest.js rename to src/descriptor/inbox-suggest.js diff --git a/descriptor/inbox-suggest.test.ts b/src/descriptor/inbox-suggest.test.ts similarity index 100% rename from descriptor/inbox-suggest.test.ts rename to src/descriptor/inbox-suggest.test.ts diff --git a/descriptor/meta-schema.ts b/src/descriptor/meta-schema.ts similarity index 100% rename from descriptor/meta-schema.ts rename to src/descriptor/meta-schema.ts diff --git a/descriptor/render-map.js b/src/descriptor/render-map.js similarity index 100% rename from descriptor/render-map.js rename to src/descriptor/render-map.js diff --git a/descriptor/render-map.test.ts b/src/descriptor/render-map.test.ts similarity index 100% rename from descriptor/render-map.test.ts rename to src/descriptor/render-map.test.ts diff --git a/descriptor/types.ts b/src/descriptor/types.ts similarity index 100% rename from descriptor/types.ts rename to src/descriptor/types.ts diff --git a/enrichment/assign.test.ts b/src/enrichment/assign.test.ts similarity index 100% rename from enrichment/assign.test.ts rename to src/enrichment/assign.test.ts diff --git a/enrichment/assign.ts b/src/enrichment/assign.ts similarity index 100% rename from enrichment/assign.ts rename to src/enrichment/assign.ts diff --git a/enrichment/pipeline.test.ts b/src/enrichment/pipeline.test.ts similarity index 100% rename from enrichment/pipeline.test.ts rename to src/enrichment/pipeline.test.ts diff --git a/enrichment/pipeline.ts b/src/enrichment/pipeline.ts similarity index 100% rename from enrichment/pipeline.ts rename to src/enrichment/pipeline.ts diff --git a/enrichment/refetch.test.ts b/src/enrichment/refetch.test.ts similarity index 100% rename from enrichment/refetch.test.ts rename to src/enrichment/refetch.test.ts diff --git a/enrichment/refetch.ts b/src/enrichment/refetch.ts similarity index 100% rename from enrichment/refetch.ts rename to src/enrichment/refetch.ts diff --git a/enrichment/suggest.test.ts b/src/enrichment/suggest.test.ts similarity index 100% rename from enrichment/suggest.test.ts rename to src/enrichment/suggest.test.ts diff --git a/enrichment/suggest.ts b/src/enrichment/suggest.ts similarity index 100% rename from enrichment/suggest.ts rename to src/enrichment/suggest.ts diff --git a/enrichment/tier.test.ts b/src/enrichment/tier.test.ts similarity index 100% rename from enrichment/tier.test.ts rename to src/enrichment/tier.test.ts diff --git a/enrichment/worker.test.ts b/src/enrichment/worker.test.ts similarity index 100% rename from enrichment/worker.test.ts rename to src/enrichment/worker.test.ts diff --git a/enrichment/worker.ts b/src/enrichment/worker.ts similarity index 100% rename from enrichment/worker.ts rename to src/enrichment/worker.ts diff --git a/library-e2e.test.ts b/src/library-e2e.test.ts similarity index 98% rename from library-e2e.test.ts rename to src/library-e2e.test.ts index 14a84f9..b6f006f 100644 --- a/library-e2e.test.ts +++ b/src/library-e2e.test.ts @@ -8,7 +8,7 @@ import { runAdd } from "./add.js"; import { BOOKMARKS_FILE, getCollection } from "./storage.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const LIBRARY_FILE = path.join(__dirname, getCollection("library").dataFile); +const LIBRARY_FILE = path.join(__dirname, "..", getCollection("library").dataFile); // library.json / bookmarks.json are gitignored personal-capture files (absent in // CI). Snapshot tolerates a missing file (returns null); restore puts the original diff --git a/llm/cli-provider.characterization.test.ts b/src/llm/cli-provider.characterization.test.ts similarity index 100% rename from llm/cli-provider.characterization.test.ts rename to src/llm/cli-provider.characterization.test.ts diff --git a/llm/cli-provider.test.ts b/src/llm/cli-provider.test.ts similarity index 100% rename from llm/cli-provider.test.ts rename to src/llm/cli-provider.test.ts diff --git a/llm/cli-provider.ts b/src/llm/cli-provider.ts similarity index 100% rename from llm/cli-provider.ts rename to src/llm/cli-provider.ts diff --git a/llm/conformance.ts b/src/llm/conformance.ts similarity index 100% rename from llm/conformance.ts rename to src/llm/conformance.ts diff --git a/llm/http-provider.test.ts b/src/llm/http-provider.test.ts similarity index 100% rename from llm/http-provider.test.ts rename to src/llm/http-provider.test.ts diff --git a/llm/http-provider.ts b/src/llm/http-provider.ts similarity index 100% rename from llm/http-provider.ts rename to src/llm/http-provider.ts diff --git a/llm/provider.test.ts b/src/llm/provider.test.ts similarity index 100% rename from llm/provider.test.ts rename to src/llm/provider.test.ts diff --git a/llm/provider.ts b/src/llm/provider.ts similarity index 100% rename from llm/provider.ts rename to src/llm/provider.ts diff --git a/llm/select-provider.test.ts b/src/llm/select-provider.test.ts similarity index 100% rename from llm/select-provider.test.ts rename to src/llm/select-provider.test.ts diff --git a/llm/select-provider.ts b/src/llm/select-provider.ts similarity index 100% rename from llm/select-provider.ts rename to src/llm/select-provider.ts diff --git a/paths.test.ts b/src/paths.test.ts similarity index 91% rename from paths.test.ts rename to src/paths.test.ts index bc4bb3c..166ad9b 100644 --- a/paths.test.ts +++ b/src/paths.test.ts @@ -7,7 +7,8 @@ import { fileURLToPath } from 'node:url'; import { loadConfig, ensureDataDir } from './config.js'; -const repoRoot = dirname(fileURLToPath(import.meta.url)); +// This test lives in src/; the repo root (the "app tree" data must never land in) is one up. +const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..'); describe('DATA_DIR-rooted paths (Story 2.2)', () => { // AC 1 / AC 3 — all data paths derive from DATA_DIR and none lands in the app tree diff --git a/processor-library.test.ts b/src/processor-library.test.ts similarity index 100% rename from processor-library.test.ts rename to src/processor-library.test.ts diff --git a/processor-library.ts b/src/processor-library.ts similarity index 100% rename from processor-library.ts rename to src/processor-library.ts diff --git a/processors.test.ts b/src/processors.test.ts similarity index 100% rename from processors.test.ts rename to src/processors.test.ts diff --git a/processors.ts b/src/processors.ts similarity index 100% rename from processors.ts rename to src/processors.ts diff --git a/server-lock.test.ts b/src/server-lock.test.ts similarity index 100% rename from server-lock.test.ts rename to src/server-lock.test.ts diff --git a/server-lock.ts b/src/server-lock.ts similarity index 100% rename from server-lock.ts rename to src/server-lock.ts diff --git a/server.test.ts b/src/server.test.ts similarity index 99% rename from server.test.ts rename to src/server.test.ts index 2a8c1b8..bb0668b 100644 --- a/server.test.ts +++ b/src/server.test.ts @@ -9,7 +9,7 @@ import { loadConfig } from "./config.js"; import { BOOKMARKS_FILE, getCollection, loadCollection, saveCollection } from "./storage.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const LIBRARY_FILE = path.join(__dirname, getCollection("library").dataFile); +const LIBRARY_FILE = path.join(__dirname, "..", getCollection("library").dataFile); // library.json / bookmarks.json are gitignored personal-capture files (absent in // CI). snapshotFile tolerates a missing file (returns null); restoreFile puts the diff --git a/server.ts b/src/server.ts similarity index 96% rename from server.ts rename to src/server.ts index 684b0a0..4aeba94 100644 --- a/server.ts +++ b/src/server.ts @@ -73,7 +73,7 @@ export function warnIfExposed(opts: ListenOptions, logger: { warn: (m: string) = ); } } -const TAXONOMY_FILE = path.join(__dirname, "taxonomy.json"); +const TAXONOMY_FILE = path.join(__dirname, "..", "taxonomy.json"); interface Bookmark { id: string; @@ -128,7 +128,7 @@ function spawnAddItem( if (opts.instructions) env.BOARD_INSTRUCTIONS = opts.instructions; if (opts.analysisAgent) env.BOARD_ANALYSIS_AGENT = opts.analysisAgent; - const proc = spawn("npx", args, { cwd: __dirname, env }); + const proc = spawn("npx", args, { cwd: path.join(__dirname, ".."), env }); let stderr = ""; proc.stderr.on("data", (d) => { stderr += d.toString(); }); proc.on("close", (code) => { @@ -344,13 +344,31 @@ export async function buildServer(opts: BuildServerOptions = {}) { const app = Fastify({ logger: false, bodyLimit: 20 * 1024 * 1024 }); + // Static shell (index.html, sw.js, manifest, icons) lives in ../public. The + // browser-shipped JS modules stay under src/ (next to their node tests and the + // descriptor module they import) and are served by the two explicit routes below. + const publicDir = path.join(__dirname, "..", "public"); await app.register(fastifyStatic, { - root: __dirname, + root: publicDir, prefix: "/", index: false, serve: true, }); + // The two browser entry points the page fetches live under src/ (no build step), + // outside the public static root. Serve them explicitly with a plain route (same + // pattern as /screenshots/* below) rather than a 2nd @fastify/static instance, + // which would crash on decorateReply double-registration. + const sendJs = (reply: FastifyReply, abs: string) => { + if (!fs.existsSync(abs)) { reply.status(404); return { error: "Not found" }; } + reply.type("text/javascript"); + return reply.send(fs.createReadStream(abs)); + }; + app.get("/collections-ui.js", async (_req, reply) => + sendJs(reply, path.join(__dirname, "collections-ui.js"))); + app.get("/descriptor/render-map.js", async (_req, reply) => + sendJs(reply, path.join(__dirname, "descriptor", "render-map.js"))); + // Screenshots now live OUTSIDE __dirname (under DATA_DIR), so the static root no // longer serves them. Stream them from screenshotsDir at the /screenshots/ prefix // the frontend still requests. A plain route (not a 2nd @fastify/static) avoids diff --git a/skills-route.test.ts b/src/skills-route.test.ts similarity index 100% rename from skills-route.test.ts rename to src/skills-route.test.ts diff --git a/skills/add-item.ts b/src/skills/add-item.ts similarity index 100% rename from skills/add-item.ts rename to src/skills/add-item.ts diff --git a/skills/compose-board.test.ts b/src/skills/compose-board.test.ts similarity index 100% rename from skills/compose-board.test.ts rename to src/skills/compose-board.test.ts diff --git a/skills/compose-board.ts b/src/skills/compose-board.ts similarity index 100% rename from skills/compose-board.ts rename to src/skills/compose-board.ts diff --git a/skills/compose-collection.test.ts b/src/skills/compose-collection.test.ts similarity index 100% rename from skills/compose-collection.test.ts rename to src/skills/compose-collection.test.ts diff --git a/skills/compose-collection.ts b/src/skills/compose-collection.ts similarity index 100% rename from skills/compose-collection.ts rename to src/skills/compose-collection.ts diff --git a/skills/core-skills.test.ts b/src/skills/core-skills.test.ts similarity index 100% rename from skills/core-skills.test.ts rename to src/skills/core-skills.test.ts diff --git a/skills/create-board.ts b/src/skills/create-board.ts similarity index 100% rename from skills/create-board.ts rename to src/skills/create-board.ts diff --git a/skills/export.test.ts b/src/skills/export.test.ts similarity index 100% rename from skills/export.test.ts rename to src/skills/export.test.ts diff --git a/skills/export.ts b/src/skills/export.ts similarity index 100% rename from skills/export.ts rename to src/skills/export.ts diff --git a/skills/generate-fields.test.ts b/src/skills/generate-fields.test.ts similarity index 100% rename from skills/generate-fields.test.ts rename to src/skills/generate-fields.test.ts diff --git a/skills/generate-fields.ts b/src/skills/generate-fields.ts similarity index 100% rename from skills/generate-fields.ts rename to src/skills/generate-fields.ts diff --git a/skills/import-bookmarks.test.ts b/src/skills/import-bookmarks.test.ts similarity index 100% rename from skills/import-bookmarks.test.ts rename to src/skills/import-bookmarks.test.ts diff --git a/skills/import-bookmarks.ts b/src/skills/import-bookmarks.ts similarity index 100% rename from skills/import-bookmarks.ts rename to src/skills/import-bookmarks.ts diff --git a/skills/refetch.ts b/src/skills/refetch.ts similarity index 100% rename from skills/refetch.ts rename to src/skills/refetch.ts diff --git a/skills/registry.test.ts b/src/skills/registry.test.ts similarity index 100% rename from skills/registry.test.ts rename to src/skills/registry.test.ts diff --git a/skills/registry.ts b/src/skills/registry.ts similarity index 100% rename from skills/registry.ts rename to src/skills/registry.ts diff --git a/skills/search.ts b/src/skills/search.ts similarity index 100% rename from skills/search.ts rename to src/skills/search.ts diff --git a/skills/tag.ts b/src/skills/tag.ts similarity index 100% rename from skills/tag.ts rename to src/skills/tag.ts diff --git a/skills/types.ts b/src/skills/types.ts similarity index 100% rename from skills/types.ts rename to src/skills/types.ts diff --git a/skills/upload-asset.ts b/src/skills/upload-asset.ts similarity index 100% rename from skills/upload-asset.ts rename to src/skills/upload-asset.ts diff --git a/sse.test.ts b/src/sse.test.ts similarity index 100% rename from sse.test.ts rename to src/sse.test.ts diff --git a/sse.ts b/src/sse.ts similarity index 100% rename from sse.ts rename to src/sse.ts diff --git a/storage.test.ts b/src/storage.test.ts similarity index 95% rename from storage.test.ts rename to src/storage.test.ts index b6a5d02..b20d7a6 100644 --- a/storage.test.ts +++ b/src/storage.test.ts @@ -14,7 +14,7 @@ import { } from "./storage.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const LIBRARY_FILE = path.join(__dirname, "library.json"); +const LIBRARY_FILE = path.join(__dirname, "..", "library.json"); // library.json is a gitignored personal-capture file (absent in CI). Snapshot // tolerates a missing file (returns null); restore puts the original back, or @@ -94,6 +94,6 @@ test("BOOKMARKS_FILE is the absolute path to bookmarks.json", () => { }); test("BOOKMARKS_FILE matches inspiration collection dataFile path", () => { - const expected = path.join(__dirname, getCollection("inspiration").dataFile); + const expected = path.join(__dirname, "..", getCollection("inspiration").dataFile); assert.equal(BOOKMARKS_FILE, expected); }); diff --git a/storage.ts b/src/storage.ts similarity index 95% rename from storage.ts rename to src/storage.ts index d78fb94..1594595 100644 --- a/storage.ts +++ b/src/storage.ts @@ -69,7 +69,7 @@ let _manifest: CollectionMeta[] | null = null; function loadManifest(): CollectionMeta[] { if (!_manifest) { _manifest = JSON.parse( - fs.readFileSync(path.join(__dirname, "collections.json"), "utf-8") + fs.readFileSync(path.join(__dirname, "..", "collections.json"), "utf-8") ) as CollectionMeta[]; } return _manifest; @@ -86,7 +86,7 @@ export function getCollection(id: string): CollectionMeta { } function resolveDataFile(id: string): string { - return path.join(__dirname, getCollection(id).dataFile); + return path.join(__dirname, "..", getCollection(id).dataFile); } // --- Collection-aware API --- diff --git a/tsconfig.json b/tsconfig.json index 291ed60..15af448 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,7 @@ "node_modules", "scripts", "get_advice.cjs", - "sw.js", + "public/sw.js", "extension/popup.js", "extension/options.js" ] From 837eb422288a665699667bc476fddcf013177af0 Mon Sep 17 00:00:00 2001 From: Seanathon Date: Fri, 26 Jun 2026 21:17:08 -0700 Subject: [PATCH 3/3] feat: dignified empty/degraded board states + Inbox integrity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three related touches so the board never dead-ends and composed boards arrive with taste (PRODUCT.md: 'degrade with dignity', 'empty states are first-class designed moments'). A. Inbox is now a protected system board — DELETE /api/boards/inbox returns 409 instead of cascading, so the app can never reach zero boards. B. Frontend resilience: load() no longer white-screens when /api/collections fails or returns empty. It always renders the switcher chrome (incl. the + affordance) and a board-level fallback (renderBoardsFallback): a calm 'Can't reach the server' + Retry, or a 'No boards yet' invite whose 'Describe a board' CTA opens the existing composer (progressive disclosure). The filter/sort toolbar hides in these states; '⚙' hides with no active board. C. The composer now writes per-board empty-state copy. Added an optional empty_state {head, body} to the board descriptor (backward-compatible, like archive_on_promote); the compose-board prompt asks for it in the board's voice; guardrails trim/cap and drop it if blank; emptyVoice() renders it for composed boards. A 'Mood board' or 'Videos' board now gets a bespoke empty state instead of the generic 'This board is ready.' Verified: tsc clean; 524 tests pass (+6); in-browser — Inbox delete 409, degraded state + Retry recovery, and no-boards → composer all confirmed. Co-Authored-By: Claude Opus 4.8 (1M context) --- public/index.html | 60 +++++++++++++++++++++++++++---- src/collections-ui.js | 45 ++++++++++++++++++++++- src/collections-ui.test.ts | 25 +++++++++++++ src/descriptor/guardrails.test.ts | 17 +++++++++ src/descriptor/guardrails.ts | 16 +++++++++ src/descriptor/types.ts | 7 ++++ src/server.test.ts | 15 ++++++++ src/server.ts | 7 ++++ src/skills/compose-board.ts | 6 ++++ 9 files changed, 190 insertions(+), 8 deletions(-) diff --git a/public/index.html b/public/index.html index 7ca6394..f31115b 100644 --- a/public/index.html +++ b/public/index.html @@ -1680,8 +1680,27 @@ async function load() { const helpers = window.collectionHelpers; const storedCid = localStorage.getItem('board.activeCollection') || 'inspiration'; - const collectionsRes = await fetch('/api/collections'); - collections = await collectionsRes.json(); + // Boards must load before anything else can render. A fetch failure (server down) + // or zero boards used to throw past renderSwitcher and white-screen the app; instead + // render a dignified board-level fallback so the chrome (incl. the + affordance) and + // a way forward always appear (Design Principle 3: degrade with dignity). + try { + const collectionsRes = await fetch('/api/collections'); + if (!collectionsRes.ok) throw new Error('collections ' + collectionsRes.status); + collections = await collectionsRes.json(); + } catch { + renderSwitcher(); + showBoardsFallback({ unavailable: true }); + return; + } + if (!collections.length) { + renderSwitcher(); + showBoardsFallback({ unavailable: false }); + return; + } + // Boards loaded → restore the toolbar row the fallback hides, then render normally. + const toolbar = document.querySelector('.filters')?.closest('.header-row'); + if (toolbar) toolbar.style.display = ''; activeCollection = helpers.resolveActiveCollection(storedCid, collections); const activeCol = collections.find(c => c.id === activeCollection); @@ -1711,6 +1730,30 @@ maybeShowAiNudge(); } + // Render a whole-board fallback (server unreachable, or zero boards) into the main + // area: hide the filter toolbar + clouds, take over grid-view, and wire the variant's + // affordance — Retry re-runs load(), "Describe a board" opens the existing composer + // (progressive disclosure: the empty state is the invite, the modal is the detail). + function showBoardsFallback(opts) { + // Hide the whole filter/sort/view toolbar row (board-scoped + inert here); the + // header-row-left (title, switcher, search, add) stays so the chrome is intact. + const toolbar = document.querySelector('.filters')?.closest('.header-row'); + if (toolbar) toolbar.style.display = 'none'; + const tagCloud = document.getElementById('tag-cloud'); + const libCloud = document.getElementById('library-topic-cloud'); + if (tagCloud) tagCloud.style.display = 'none'; + if (libCloud) libCloud.style.display = 'none'; + const listView = document.getElementById('list-view'); + const gridView = document.getElementById('grid-view'); + listView.innerHTML = ''; listView.style.display = 'none'; + gridView.style.display = ''; + gridView.innerHTML = window.collectionHelpers.renderBoardsFallback(opts); + const retry = gridView.querySelector('[data-boards-retry]'); + if (retry) retry.addEventListener('click', () => load()); + const create = gridView.querySelector('[data-boards-new]'); + if (create) create.addEventListener('click', openComposeModal); + } + // Story 8.6: a peripheral, dismissible "enable AI" nudge — only when no provider is // configured (the Story 4.4 signal via /api/meta) AND not previously dismissed. The // board stays the hero (SM-C2): a small corner card, never a blocking modal/banner. @@ -1762,17 +1805,20 @@ const headerLeft = document.querySelector('.header-row-left'); headerLeft.insertBefore(sw, document.getElementById('search')); } + // Edit (⚙) only makes sense with an active board; the New-board (+) affordance is + // ALWAYS present so a board can be created even from the empty/degraded states. + const hasActive = !!collections.find(c => c.id === activeCollection); sw.innerHTML = collections.map(c => `` ).join('') - // New-board + Edit-board affordances (compose-board / edit-board modals). - + `` + + (hasActive ? `` : '') + ``; sw.querySelectorAll('.coll-btn[data-cid]').forEach(b => b.addEventListener('click', () => setActiveCollection(b.dataset.cid)) ); document.getElementById('new-board-btn').addEventListener('click', openComposeModal); - document.getElementById('edit-board-btn').addEventListener('click', () => openEditBoardModal(activeCollection)); + const editBtn = document.getElementById('edit-board-btn'); + if (editBtn) editBtn.addEventListener('click', () => openEditBoardModal(activeCollection)); } // --- Story 10.1/10.3 + Edit: New board (compose→preview→create) & Edit board (rename/delete) --- @@ -3355,8 +3401,8 @@

Worth knowing

// load() is called by the module script below after collectionHelpers are initialized