diff --git a/.cursor/environment.json b/.cursor/environment.json new file mode 100644 index 00000000..d68ad3fd --- /dev/null +++ b/.cursor/environment.json @@ -0,0 +1,4 @@ +{ + "name": "Dripnex", + "install": "bash .cursor/install.sh" +} diff --git a/.cursor/install.sh b/.cursor/install.sh new file mode 100755 index 00000000..3352de5f --- /dev/null +++ b/.cursor/install.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +# +# Cloud Agent install script for Dripnex. +# +# Non-obvious constraints this script exists to satisfy: +# +# 1. node:sqlite FTS5 — @dripnex/mcp-server uses Node's built-in `node:sqlite` +# (DatabaseSync) and asserts FTS5 is compiled in. The node binary the +# Cursor exec daemon ships (/exec-daemon/node) has FTS5 DISABLED, so a +# plain `node`/`pnpm test` would fail. We select an nvm-managed Node whose +# bundled SQLite has FTS5 enabled and make it the default `node` for every +# shell by symlinking the toolchain into a PATH dir that precedes +# /exec-daemon. +# +# 2. better-sqlite3 (desktop) — apps/desktop's postinstall runs +# `electron-builder install-app-deps`, which rebuilds better-sqlite3 +# against Electron's ABI. That is required for `pnpm dev` and the +# Playwright+Electron e2e suite to launch. (This is why `pnpm test` +# excludes @dripnex/storage-sqlite — see CLAUDE.md.) +# +set -euo pipefail + +cd "$(dirname "$0")/.." +REPO_ROOT="$(pwd)" +echo "==> Dripnex install (repo: $REPO_ROOT)" + +# Dependabot-regenerated lockfiles can resolve GitHub git deps over SSH; CI +# rewrites them to HTTPS so the tarball install works without a deploy key. +git config --global 'url.https://github.com/.insteadOf' 'git@github.com:' || true + +# --- 1. Select an FTS5-capable Node and make it the default ---------------- +has_fts5() { + "$1" -e 'const{DatabaseSync}=require("node:sqlite");const d=new DatabaseSync(":memory:");process.exit(d.prepare("SELECT sqlite_compileoption_used(\x27ENABLE_FTS5\x27) v").get().v===1?0:1)' >/dev/null 2>&1 +} + +export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" +NODE_BIN="" +if [ -s "$NVM_DIR/nvm.sh" ]; then + # shellcheck disable=SC1091 + . "$NVM_DIR/nvm.sh" + # Prefer an already-installed Node >= 22 that has FTS5; otherwise install 22. + for ver in $(nvm ls --no-colors 2>/dev/null | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' | sort -Vr | uniq); do + cand="$NVM_DIR/versions/node/$ver/bin/node" + if [ -x "$cand" ] && has_fts5 "$cand"; then NODE_BIN="$cand"; break; fi + done + if [ -z "$NODE_BIN" ]; then + echo "==> No FTS5-capable Node found; installing Node 22 via nvm" + nvm install 22 >/dev/null + cand="$(nvm which 22 2>/dev/null || true)" + if [ -n "$cand" ] && has_fts5 "$cand"; then NODE_BIN="$cand"; fi + fi +fi + +if [ -z "$NODE_BIN" ]; then + echo "ERROR: could not locate a Node build with node:sqlite FTS5 enabled." >&2 + exit 1 +fi +NODE_BIN_DIR="$(dirname "$NODE_BIN")" +echo "==> Using Node $("$NODE_BIN" -v) (FTS5 enabled) from $NODE_BIN_DIR" + +# Make the FTS5 node the default `node`/`npx` for every shell. +# +# The Cursor daemon's runtime PATH places /exec-daemon (which ships an +# FTS5-less node) AHEAD of the nvm bin, so a plain `node` would be wrong at +# agent runtime. Crucially, the install-time PATH can differ from the agent +# runtime PATH (install may run with nvm already ahead), so we cannot skip +# based on the current ordering — we ALWAYS place shims. We symlink the +# toolchain into every writable PATH dir that precedes /exec-daemon, plus +# /usr/local/cargo/bin (world-writable in the base image and consistently +# ahead of /exec-daemon at runtime). Symlinks live on disk, so they survive +# into environment builds/snapshots and win over /exec-daemon's node. +declare -a SHIM_CANDIDATES=() +IFS=':' read -r -a _path_entries <<< "$PATH" +for d in "${_path_entries[@]}"; do + case "$d" in */exec-daemon*) break ;; esac + SHIM_CANDIDATES+=("$d") +done +SHIM_CANDIDATES+=("/usr/local/cargo/bin") + +_shimmed="" +for d in "${SHIM_CANDIDATES[@]}"; do + [ "$d" = "$NODE_BIN_DIR" ] && continue # never clobber the node bin itself + case " $_shimmed " in *" $d "*) continue ;; esac # dedupe + mkdir -p "$d" 2>/dev/null || sudo mkdir -p "$d" 2>/dev/null || true + [ -d "$d" ] && [ -w "$d" ] || continue + for b in node npm npx corepack pnpm pnpx yarn yarnpkg; do + src="$NODE_BIN_DIR/$b"; dest="$d/$b" + [ -e "$src" ] && [ "$src" != "$dest" ] && ln -sfn "$src" "$dest" 2>/dev/null || true + done + echo "==> Linked node toolchain into $d" + _shimmed="$_shimmed $d" +done + +# Ensure the rest of THIS script uses the FTS5 node too. +export PATH="$NODE_BIN_DIR:$PATH" +hash -r || true + +# --- 2. Install workspace dependencies (runs postinstall scripts) ---------- +# postinstall: lefthook install (git hooks) + electron-builder install-app-deps +# (rebuilds better-sqlite3 for Electron) + downloads the Electron binary. +echo "==> pnpm install --frozen-lockfile" +pnpm install --frozen-lockfile + +# Belt-and-suspenders: ensure the Electron binary is materialized for e2e. +if [ -f apps/desktop/node_modules/electron/install.js ]; then + echo "==> Materializing Electron binary" + ( cd apps/desktop && node node_modules/electron/install.js ) +fi + +# --- 3. Build workspace packages (source-derived; needed by typecheck/e2e) - +echo "==> pnpm build" +pnpm build + +# --- 4. Verify the critical invariant -------------------------------------- +echo "==> Verifying node:sqlite FTS5" +node -e 'const{DatabaseSync}=require("node:sqlite");const v=new DatabaseSync(":memory:").prepare("SELECT sqlite_compileoption_used(\x27ENABLE_FTS5\x27) v").get().v;if(v!==1){console.error("FTS5 missing");process.exit(1)}console.log("node",process.version,"FTS5 OK")' + +echo "==> Install complete." diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml index e5b20356..135d5a6a 100644 --- a/.github/workflows/automerge.yml +++ b/.github/workflows/automerge.yml @@ -59,11 +59,21 @@ jobs: disable-automerge: runs-on: ubuntu-latest permissions: - # Only the auto-merge state changes here; no contents write needed. + # `pull-requests: write` alone returned FORBIDDEN ("Resource not + # accessible by integration") on PR #571. The narrower scope came from + # a least-privilege review; the mutation appears to want contents too. + # If FORBIDDEN comes back with both scopes granted, the cause is a + # repository or org restriction on GITHUB_TOKEN, not this file. + contents: write pull-requests: write + # Only where a queued merge could still land somewhere unprotected: a + # draft, or a base that is neither develop nor main. A PR retargeted onto + # main is a release promotion, and main is protected — there is nothing + # dangerous to undo, so firing there was pure noise. if: >- github.event.pull_request.draft == true || - github.event.pull_request.base.ref != 'develop' + (github.event.pull_request.base.ref != 'develop' && + github.event.pull_request.base.ref != 'main') steps: - name: Disable auto-merge for drafts and non-develop bases env: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 36bf914b..20ee3bc6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -218,23 +218,53 @@ jobs: if: needs.publish.result == 'success' runs-on: ubuntu-latest steps: - - name: Create sync PR + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + + # A `develop <- main` PR cannot merge: develop requires the head branch + # to be up to date, and main is behind develop the moment anything lands + # after the release. Push a branch descended from develop with main + # merged into it instead — that satisfies the rule and, unlike a squash, + # actually makes main an ancestor of develop so the next promotion is + # not stuck at BEHIND. + - name: Open the back-merge PR env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG_NAME: ${{ needs.meta.outputs.tag }} run: | set -euo pipefail - if ! pr_count="$(gh pr list --base develop --head main --repo "$GITHUB_REPOSITORY" --json number --jq 'length')"; then - echo "::error::Unable to check for an existing sync PR." - exit 1 + git fetch origin main develop + + if git merge-base --is-ancestor origin/main origin/develop; then + echo "main is already an ancestor of develop; nothing to sync." + exit 0 fi - if [ "$pr_count" -eq 0 ]; then - gh pr create \ - --base develop \ - --head main \ - --title "chore: sync release $TAG_NAME back to develop" \ - --body "Auto sync of release commit and changelog from $TAG_NAME." \ - --repo "$GITHUB_REPOSITORY" - else - echo "Sync PR already open; nothing to create." + + # The chore/backmerge- prefix keeps this out of the squash + # auto-merge in automerge.yml. Squashing it would defeat the point. + branch="chore/backmerge-main-${TAG_NAME}" + + if git ls-remote --exit-code --heads origin "$branch" >/dev/null 2>&1; then + echo "$branch already exists; leaving the open PR alone." + exit 0 fi + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git checkout -b "$branch" origin/develop + git merge origin/main -m "chore(release): merge main into develop" + git push origin "$branch" + + gh pr create \ + --base develop \ + --head "$branch" \ + --title "chore(release): merge main into develop" \ + --body "Back-merge of $TAG_NAME so main stays an ancestor of develop and the next promotion PR is not stuck at BEHIND. + + Merge this with a **merge commit**. A squash replays the content as a new commit and does not establish ancestry." \ + --repo "$GITHUB_REPOSITORY" + + # Explicitly a merge commit. automerge.yml skips chore/backmerge-*, + # so nothing else will arm this with --squash. + gh pr merge "$branch" --auto --merge --repo "$GITHUB_REPOSITORY" diff --git a/apps/desktop/src/renderer/themes/__tests__/officialThemes.test.ts b/apps/desktop/src/renderer/themes/__tests__/officialThemes.test.ts new file mode 100644 index 00000000..e4df44a1 --- /dev/null +++ b/apps/desktop/src/renderer/themes/__tests__/officialThemes.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest'; +import { validateThemeTokens } from '@dripnex/plugin-api'; +import { OFFICIAL_THEMES } from '../officialThemes'; + +describe('OFFICIAL_THEMES', () => { + it('registers unique ids with only valid tokens', () => { + const ids = OFFICIAL_THEMES.map(theme => theme.id); + expect(new Set(ids).size).toBe(ids.length); + + for (const theme of OFFICIAL_THEMES) { + expect(theme.name.length).toBeGreaterThan(0); + expect(['dark', 'light']).toContain(theme.colorScheme); + const valid = validateThemeTokens(theme.tokens, theme.id); + for (const [token, value] of Object.entries(theme.tokens)) { + expect(valid[token]).toBe(value); + } + expect(valid['--accent-primary']).toBe(theme.tokens['--accent']); + } + }); + + it('includes Harbor Dusk as a dark official palette', () => { + const harbor = OFFICIAL_THEMES.find(theme => theme.id === 'dripnex-harbor-dusk'); + expect(harbor).toMatchObject({ + name: 'Harbor Dusk', + colorScheme: 'dark', + pluginId: 'dripnex', + }); + expect(harbor?.tokens['--bg-base']).toBe('#141c26'); + expect(harbor?.tokens['--accent']).toBe('#5e9a92'); + expect(harbor?.tokens['--cm-link']).toBe('#d4a05a'); + }); +}); diff --git a/apps/desktop/src/renderer/themes/officialThemes.ts b/apps/desktop/src/renderer/themes/officialThemes.ts index eaed6475..5a36cbc7 100644 --- a/apps/desktop/src/renderer/themes/officialThemes.ts +++ b/apps/desktop/src/renderer/themes/officialThemes.ts @@ -464,6 +464,42 @@ export const OFFICIAL_THEMES: ThemeDefinition[] = [ '--status-dropped': '#c44b4b', }, }, + { + id: 'dripnex-harbor-dusk', + name: 'Harbor Dusk', + description: 'Coastal evening. Mist text, muted teal, amber lanterns.', + author: 'Dripnex', + colorScheme: 'dark', + pluginId: 'dripnex', + tokens: { + '--bg-base': '#141c26', + '--bg-surface': '#10161e', + '--bg-elevated': '#1c2633', + '--bg-inset': '#0c1218', + '--bg-hover': 'rgba(205, 214, 222, 0.06)', + '--bg-active': 'rgba(205, 214, 222, 0.1)', + '--text-primary': '#cdd6de', + '--text-secondary': 'rgba(205, 214, 222, 0.74)', + '--text-muted': 'rgba(205, 214, 222, 0.5)', + '--text-faint': 'rgba(205, 214, 222, 0.32)', + '--border': 'rgba(205, 214, 222, 0.1)', + '--border-subtle': 'rgba(205, 214, 222, 0.06)', + '--border-strong': 'rgba(205, 214, 222, 0.16)', + '--accent': '#5e9a92', + '--accent-hover': '#74aea6', + '--accent-muted': 'rgba(94, 154, 146, 0.2)', + '--accent-subtle': 'rgba(94, 154, 146, 0.1)', + '--glass-bg': 'rgba(20, 28, 38, 0.9)', + '--glass-border': 'rgba(205, 214, 222, 0.08)', + '--glass-bg-menu': 'rgba(28, 38, 51, 0.95)', + '--glass-border-menu': 'rgba(205, 214, 222, 0.08)', + '--status-active': '#5e9a92', + '--status-on-hold': '#d4a05a', + '--status-completed': '#7aad8a', + '--status-dropped': '#c46b6b', + '--cm-link': '#d4a05a', + }, + }, ]; export function registerOfficialThemes(): void { diff --git a/docs/dripnex-dependency-map.md b/docs/dripnex-dependency-map.md new file mode 100644 index 00000000..2d0fbf1d --- /dev/null +++ b/docs/dripnex-dependency-map.md @@ -0,0 +1,153 @@ +# Dripnex — Dependency Map & Critical Path + +> **Status:** Living planning doc derived from the GitHub issue tree (E1–E18, 116 issues). +> The issues remain the source of truth; this is the cross-cutting view they can't show: +> the dependency DAG, the critical path, spike gates, and a 2-person parallelization plan. + +## TL;DR + +- **Three keystones:** **E1** (foundation — blocks everything), **E9** (knowledge substrate — + blocks all of Q3), **E10** (the MCP write-loop — _the_ value of the pivot). +- **The differentiator ships in Q3.** Q1–Q2 are prelude (stabilize + platform + rename). + There is no demonstrable "dripnex moment" until E9→E10→E12 land mid-year. +- **Longest dependency chain = the backend split** (E5→E8→E14→E16). It's also the highest + scope/capacity risk — see D3 in the reconciliation doc. +- **Two spike gates** decide Q3 subsystems: graph WebGL (#318→E11) and sqlite-vec (#319→E13). + +--- + +## 1. Epic dependency DAG + +```mermaid +graph TD + subgraph Q1["Q1 — Stabilize + Platform foundations"] + E1["E1 v0.15.4 patch
(stabilize + e2e signal)"] + E2["E2 Spike week
graph · sqlite-vec · Tauri"] + E3["E3 Plugin API v1"] + E4["E4 First satellites"] + E5["E5 Backend contracts"] + end + subgraph Q2["Q2 — Extraction + Rename + Split 1"] + E6["E6 Built-in extraction"] + E7["E7 Rename train
(highest risk)"] + E8["E8 Service split 1
auth + sync"] + E9["E9 Knowledge layer
frontmatter + link index"] + end + subgraph Q3["Q3 — AI notetaker core"] + E10["E10 MCP write surface"] + E11["E11 Graph view"] + E12["E12 Palette + Connect w/ Claude"] + E13["E13 Semantic layer"] + E14["E14 Service split 2"] + end + subgraph Q4["Q4 — Ecosystem + polish"] + E15["E15 UX hygiene"] + E16["E16 Plugin ecosystem"] + E17["E17 QA program"] + E18["E18 Positioning & launch"] + end + + E1 --> E3 & E5 & E9 & E7 + E3 --> E4 --> E6 + E3 --> E6 + E5 --> E8 --> E14 + E2 -. graph spike .-> E11 + E2 -. sqlite-vec spike .-> E13 + E9 --> E10 & E11 & E13 + E10 --> E12 + E7 -. deep-link .-> E12 + E7 --> E18 + E11 --> E18 + E12 --> E18 + E3 --> E16 + E6 --> E16 + E14 -- plugin-registry --> E16 + E8 & E14 --> E17 + E16 --> E18 +``` + +> **Note:** E1 is drawn feeding the four chain-heads it actually unblocks; in practice its +> e2e-signal task (#309) gates _everything_ — nothing should merge on a red e2e suite. + +--- + +## 2. The dependency chains, ranked + +| Chain | Depth | What it delivers | Risk | +| --------------------------------- | ----- | -------------------------------------------------- | -------------------------------------------------------- | +| **Backend:** E1→E5→E8→E14→E16→E18 | 6 | Microservices + marketplace + launch | ⚠️ Longest; biggest capacity cost (D3) | +| **Value loop:** E1→E9→E10→E12→E18 | 5 | The agent write-loop — the pivot's reason to exist | 🎯 Demo-critical | +| **Platform:** E1→E3→E4→E6→E16→E18 | 6 | Plugin API → satellites → marketplace | Steady, well-sequenced | +| **Graph:** E2⇢E11, E9→E11→E18 | — | The differentiator UI | Gated on spike (#318) | +| **Semantic:** E2⇢E13, E9→E13 | — | Embeddings / related notes / ContextBuilder v2 | Gated on spike (#319); degrades to link-only if it slips | +| **Rename:** E1→E7→E18 | — | Identity change | 🔴 Highest single-epic risk; ships alone | + +**Read this as:** the product's _value_ (value loop) is only 5 deep and mostly TS app work. +The _operational cost_ (backend chain) is 6 deep and the part most worth trimming for a +2-person team. + +--- + +## 3. Spike gates (decide before building) + +| Spike | Gates | Pass bar | If it fails | +| ---------------------------------- | ---------------- | ----------------------------------------------------------- | --------------------------------------------------------------- | +| #318 graph WebGL | E11 | 5k nodes / 20k edges ≥ 30 fps in Electron | E11 ships with degradation/ego-graph only, or library swap | +| #319 sqlite-vec | E13 | semantic beats FTS5 on a golden-query eval over the real DB | E13 ships link-only; ContextBuilder v2 works without embeddings | +| #320 Tauri | (stack decision) | paper PoC only — door stays closed regardless | Record decision (#321), kill the option | +| #380 Connect w/ Claude (Agent SDK) | E12 | official-path auth ergonomics acceptable | E12 falls back to demoted API-key path | + +**Run E2's spikes in Q1, in parallel with E1.** Their verdicts shape two whole Q3 epics, so +late spikes = late re-planning. + +--- + +## 4. Suggested re-sequencing + +1. **Pull frontmatter schema (#363) into late Q1.** E9 is the keystone for all of Q3 but + sits in Q2. The _schema_ (not the full link index) is small and pure-core; landing it + early de-risks E10/E11/E13 and lets the MCP tool contract stabilize sooner. Fold in D1 + (the `type` enum) while doing it. +2. **Gate E14 by trigger, not by calendar** (reconciliation D3). Keep `billing-webhooks`; + defer `ai-gateway` (desktop BYO doesn't use it) and `plugin-registry` (only when E16 is next). +3. **Protect the rename week (E7).** Its body already says "nothing else ships in its week" — + hold the line; the upgrade-path e2e (#354) is the only gate. +4. **Treat E17 (QA) as continuous, not Q4.** Its own body says charter items burn down + ~3/week across all quarters. Q4 is just where completion is _tracked_, not where QA starts. + +--- + +## 5. Parallelization for 2 people + +The `wf:` labels are already a clean work-breakdown into 9 tracks. Mapped to two owners +(adjust to reality — Alicio is referenced as the graph owner in E11): + +| Owner | Primary tracks | Rationale | +| ------------------------- | --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| **Founder** | `wf:stabilization`, `wf:knowledge-layer`, `wf:mcp-agents`, `wf:backend-services`, `wf:rename-brand` | The keystones + risky/identity work + backend; the spine of the value loop | +| **Collaborator (Alicio)** | `wf:graph`, `wf:palette-ux`, plus `wf:plugin-platform` extractions | Graph is explicitly theirs; palette + satellite extractions are isolatable, plugin-API-mediated units | +| **Shared / async** | `wf:qa-infra` | Continuous; both contribute, neither owns full-time | + +**Parallelism rules that fall out of the DAG:** + +- E2 spikes ∥ E1 (independent — start day 1). +- Once E3's RFC (#322) lands, E4 satellites ∥ E5 backend contracts (different tracks, no shared files). +- E9 (founder) ∥ E6 extraction (collaborator) in Q2 — but **E7 rename is a stop-the-world week** for both. +- In Q3, the value loop (E9→E10→E12, founder) runs ∥ graph (E11, collaborator) ∥ semantic + (E13, gated). E14 backend is the deferrable one. + +**Capacity reality check:** ~30 / 28 / 24 / 16 child tasks per quarter ≈ 2–2.5 tasks/week +sustained, with several large items (table WYSIWYG #342, WebGL graph #375, 5-Worker split). +This is aggressive but the sequencing is sound. The deferrals in §4.2 are the main lever if +velocity lags — cut backend surface before cutting the value loop. + +--- + +## 6. What "done" looks like per quarter + +| Q | Demonstrable outcome | +| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Q1 | Green e2e gate, signed v0.15.4 shipped, plugin-API RFC + npm publish, spike verdicts recorded | +| Q2 | App is "dripnex" (rename done, zero data loss), built-ins are real satellites, frontmatter+backlinks live | +| Q3 | **The pivot is real:** Claude Code/Desktop writes validated notes via MCP, graph renders, palette + Connect-with-Claude work, semantic search beats FTS | +| Q4 | Marketplace browses real registry, QA is a credible gate, dripnex.app launched around the MCP write-loop demo | diff --git a/docs/dripnex-scope-reconciliation.md b/docs/dripnex-scope-reconciliation.md new file mode 100644 index 00000000..84332566 --- /dev/null +++ b/docs/dripnex-scope-reconciliation.md @@ -0,0 +1,137 @@ +# Dripnex — Scope Reconciliation (Vision ↔ Issues ↔ Code) + +> **Status:** Living decision doc. Last reconciled against the open GitHub issue tree +> (E1–E18, 116 issues) and the working code at `v0.15.2`. +> +> **Source-of-truth hierarchy:** running code > GitHub issues (the real roadmap) > +> this doc > the "Agentic Software Memory System" vision doc > `plan.md` / `docs/ROADMAP.md` +> (both **stale, do not trust**). + +## Why this doc exists + +Four documents describe Dripnex and they disagree: + +| Doc | What it is | Trust | +| ------------------------------------------------- | ----------------------------------------------- | --------------------------------- | +| **GitHub issues E1–E18** | The real, sequenced, dependency-aware roadmap | ✅ Source of truth | +| **Vision doc** ("Agentic Software Memory System") | North-star narrative / concept | ⚠️ Concept only, not a stack spec | +| `plan.md` (root) | v1.0 "markdown-first, AI deferred" architecture | ❌ Stale — superseded by pivot | +| `docs/ROADMAP.md` | v0.5.0 "close the Inkdrop gap" plan | ❌ Stale — pre-pivot | + +This doc resolves the divergences so nobody (human or agent) builds against the wrong map. + +--- + +## 1. Divergence table + +Legend: **DECIDED** = issues already chose, build accordingly · **ALIGNED** = vision and +issues agree · **OPEN** = needs founder ratification (see §2). + +| # | Topic | Vision doc says | Issues + code reality | Status | +| --- | ----------------- | --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | +| 1 | Desktop runtime | **Tauri v2** | Electron, locked. E2 (#320) is a _half-day paper spike_ only — "Decision already locked: Electron base, no rewrite" | **DECIDED → Electron** | +| 2 | Core engine | **Rust** "the heart of the product" | TypeScript: `packages/core` + `ai-core` `ContextBuilder` + `storage` link index. No Rust anywhere (no `Cargo.toml`) | **DECIDED → TS** | +| 3 | MCP server | **Rust MCP server** | TypeScript `@dripnex/mcp-server`, extracted as public satellite (E4 #333) | **DECIDED → TS** | +| 4 | Primary unit | **Artifact-first** (not notes) | Notes + **typed frontmatter** carry the artifact logic (E9 #363). "Artifact" = note with a `type` + structured frontmatter | **OPEN** (see D1) | +| 5 | Artifact taxonomy | Plan / Decision / RFC / Investigation / Migration / Audit / Incident / Task | E9 frontmatter v1 ships generic `project / status / relations / dates / custom`; **no `type` enum yet** | **OPEN — gap** (see D1) | +| 6 | Semantic storage | SQLite + FTS5 + sqlite-vec | Exactly that — E13 (#384) embeddings table, gated on E2 spike (#319) | **ALIGNED** | +| 7 | Graph | Relations in SQLite, **no Neo4j** | E9 link index (#364) + E11 WebGL graph (#375). Same call | **ALIGNED** | +| 8 | Sync | **CRDTs (Yjs / Automerge)** | Existing AES-256-GCM E2E sync; E8 `sync-service` is **push/pull/conflict**, not CRDT | **OPEN** (see D2) | +| 9 | Model-agnostic | Claude / GPT / Gemini / Ollama / vLLM | `ai-core` has anthropic / openai / ollama via `fetch`; E14 `ai-gateway` adds quotas/keys. Gemini + vLLM not built | **ALIGNED** (subset shipped) | +| 10 | Local-first | Default, offline | Same — offline-first is a non-negotiable | **ALIGNED** | +| 11 | Backend | (silent) | E5/E8/E14: decompose Hono+Turso monolith into **5 Cloudflare Workers** | **OPEN — capacity** (see D3) | +| 12 | "Context Engine" | Rust, builds optimal agent context | `ContextBuilder v2` (E13 #387) — TS, graph traversal + semantic fill under token budget | **ALIGNED** (TS, not Rust) | + +**Bottom line:** the vision doc's _stack_ (Rust + Tauri + Rust-MCP) was rejected. Its +_ideas_ (artifact-centric memory, local-first, model-agnostic, context-before-generation, +SQLite graph, semantic retrieval) are all alive and map cleanly onto the TS/Electron +implementation. Treat the vision doc as **product narrative**, never as a build spec. + +--- + +## 2. Open decisions (need a founder call) + +### D1 — Encode the artifact-type taxonomy in frontmatter v1? **(recommend: YES, lightweight)** + +The vision's whole thesis is "artifact-first." Today E9 (#363) ships a generic frontmatter +schema with no `type`. If artifact-first is real, add a `type` enum now — it's cheap at +schema-design time and expensive to retrofit once notes exist in the wild. + +Recommendation: add an **open** `type` field to frontmatter v1: + +```yaml +--- +type: decision # plan | decision | rfc | investigation | migration | audit | incident | task | note +status: accepted +project: dripnex-core +relations: + supersedes: [adr-001] +--- +``` + +Keep it open-vocabulary (string, with a known set) so the MCP tools (E10) and ContextBuilder +(E13) can filter by artifact type without a migration later. This makes #363 the single +schema doc that becomes the MCP tool contract in Q3 — exactly as E9's body promises. + +### D2 — CRDT sync (Yjs/Automerge) vs keep push/pull? **(recommend: DEFER CRDT)** + +CRDTs only pay off for real-time multi-writer collaboration. Dripnex is single-user +local-first today, and `plan.md`'s own non-goals listed collaboration as "Never (v1)." +The shipped AES E2E push/pull/conflict model (E8 #359) is sufficient and far simpler. + +Recommendation: **defer CRDT** to a future "teams" epic; do not let the vision doc pull +Yjs/Automerge into the Q2 sync-service split. Revisit only if real-time collab becomes a +funded goal. + +### D3 — Full 5-Worker split vs slim monolith? **(recommend: PHASE IT — auth+sync only until traction)** + +This is the single largest scope/capacity risk. E5+E8+E14 decompose the backend into +auth / sync / ai-gateway / plugin-registry / billing-webhooks. The issues themselves admit +"contract-versioning overhead on a 2-person team." Five independently-deployed Workers, +five staging envs, five smoke suites, shared `@dripnex/contracts` versioning — that is a lot +of operational surface for two people pre-revenue. + +Recommendation: + +- **Do** E8 (auth + sync extraction) — gives a collaborator one ownable unit and isolates the + two cleanest boundaries. +- **Gate** E14 (ai-gateway / plugin-registry / billing split) on actual need: + - `plugin-registry` only when the marketplace (E16) is genuinely next. + - `billing-webhooks` isolation is cheap and worth it (smallest blast radius for Stripe). + - `ai-gateway` only matters for a **web/team** future — the desktop BYO-key path explicitly + does _not_ route through it (per E14's own body). So it can wait. +- Keep a **slim monolith** for everything not yet extracted; let traffic evidence decide the + monolith-retirement audit (#390), not architectural purity. + +### D4 — Tauri: close the door? **(recommend: keep #320 as a paper spike, then formally kill it)** + +E2 already treats Tauri as a non-starter ("Electron base, no rewrite"). Run #320 as the +half-day paper PoC, write the decision record (#321), and **close the option explicitly** so +it stops resurfacing. The vision doc should be updated to say "Electron" or annotated as +superseded. + +--- + +## 3. Concept → implementation map (for the vision doc readers) + +| Vision concept | Where it actually lives | +| --------------- | ---------------------------------------------------------------------------- | +| Artifact | Note + typed frontmatter (`packages/core`, E9) | +| Core Engine | `packages/core` + `ai-core/ContextBuilder` + `storage` link/embeddings index | +| Context Engine | `ContextBuilder v2` (#387) — graph traversal + semantic fill, token-budgeted | +| MCP Layer | `@dripnex/mcp-server` (TS), E10 write/query/graph tools | +| Embedding Layer | sqlite-vec + incremental indexing (E13 #384), offline-first source per spike | +| Graph Layer | `links` table in SQLite (E9 #364) + `@dripnex/plugin-graph` WebGL (E11) | +| Sync Layer | `sync-service` Worker push/pull/conflict (E8) — **not** CRDT (see D2) | +| Model-agnostic | `ai-core` providers + `ai-gateway` (E14) | + +--- + +## 4. Action items out of this reconciliation + +- [ ] Ratify D1–D4 (founder). +- [ ] If D1 = yes: amend #363 to include the `type` enum before building the schema. +- [ ] Annotate the vision doc header: "north-star concept; stack is Electron/TS, see scope-reconciliation." +- [ ] Mark `plan.md` and `docs/ROADMAP.md` as superseded (move to `docs/archived/`). +- [ ] If D3 = phase: re-label E14 children — keep `billing-webhooks`, gate `ai-gateway` and + `plugin-registry` behind explicit triggers. diff --git a/docs/releases/v0.18.0.md b/docs/releases/v0.18.0.md new file mode 100644 index 00000000..3f0d0286 --- /dev/null +++ b/docs/releases/v0.18.0.md @@ -0,0 +1,17 @@ +--- +version: 0.18.0 +date: 2026-08-24 +title: Harbor Dusk +status: draft +--- + +A coastal evening palette for long writing sessions. Harbor Dusk joins +the official themes already in the desktop — same Settings → Themes +list, same token contract as Parchment, Wave, Night, and Fog. No new +fonts, no plugin pack. + +## Themes + +- **Harbor Dusk.** Official dark palette. Deep slate water, mist text, + muted teal on chrome, amber lanterns on editor links. Evening sibling + to Fog (coastal gray morning). Pick it on Settings → Themes. diff --git a/docs/themes/LOG.md b/docs/themes/LOG.md index fc9eb8ba..923def31 100644 --- a/docs/themes/LOG.md +++ b/docs/themes/LOG.md @@ -8,3 +8,4 @@ Format: `YYYY-MM-DD | id | name | topic | colorScheme` 2026-08-22 | dripnex-matcha | Matcha | green-tea paper, calm reading | light 2026-08-22 | dripnex-phosphor | Phosphor | amber CRT / terminal glow | dark 2026-08-22 | dripnex-fog | Fog | coastal gray morning, muted blue accent | light +2026-08-24 | dripnex-harbor-dusk | Harbor Dusk | coastal evening, mist text, muted teal, amber lanterns | dark diff --git a/packages/api/tests/runMigrations.test.ts b/packages/api/tests/runMigrations.test.ts index 2fb7efa4..45c167a2 100644 --- a/packages/api/tests/runMigrations.test.ts +++ b/packages/api/tests/runMigrations.test.ts @@ -30,36 +30,50 @@ describe('applyMigrations', () => { } }); - it('applies the catalog to an empty database, then no-ops', async () => { - const path = `/tmp/dripnex-migrate-${randomUUID()}.db`; - paths.push(path); - const client = createClient({ url: `file:${path}` }); + // These two drive a real libsql file database through the entire migration + // catalog — the second one twice. That is legitimately slower than vitest's + // 5s default (3.0s and 7.4s on CI), so they carry their own budget rather + // than the suite carrying a global bump that would mask slow unit tests. + const MIGRATION_TIMEOUT_MS = 30_000; - const first = await applyMigrations(client); - expect(first.skipped).toEqual([]); - expect(first.applied.length + first.recordedExisting.length).toBe(MIGRATIONS.length); + it( + 'applies the catalog to an empty database, then no-ops', + async () => { + const path = `/tmp/dripnex-migrate-${randomUUID()}.db`; + paths.push(path); + const client = createClient({ url: `file:${path}` }); - const tables = await client.execute( - "SELECT name FROM sqlite_master WHERE type='table' AND name='plugin_versions'" - ); - expect(tables.rows).toHaveLength(1); + const first = await applyMigrations(client); + expect(first.skipped).toEqual([]); + expect(first.applied.length + first.recordedExisting.length).toBe(MIGRATIONS.length); - const second = await applyMigrations(client); - expect(second.applied).toEqual([]); - expect(second.skipped).toEqual(MIGRATIONS.map(m => m.id)); - }); + const tables = await client.execute( + "SELECT name FROM sqlite_master WHERE type='table' AND name='plugin_versions'" + ); + expect(tables.rows).toHaveLength(1); - it('records migrations that already exist in the schema', async () => { - const path = `/tmp/dripnex-migrate-${randomUUID()}.db`; - paths.push(path); - const client = createClient({ url: `file:${path}` }); - await client.execute( - 'CREATE TABLE users (id text PRIMARY KEY, email text, created_at text, updated_at text)' - ); + const second = await applyMigrations(client); + expect(second.applied).toEqual([]); + expect(second.skipped).toEqual(MIGRATIONS.map(m => m.id)); + }, + MIGRATION_TIMEOUT_MS + ); - const report = await applyMigrations(client); - expect(report.recordedExisting).toContain('0000_chubby_zzzax'); - const second = await applyMigrations(client); - expect(second.skipped).toContain('0000_chubby_zzzax'); - }); + it( + 'records migrations that already exist in the schema', + async () => { + const path = `/tmp/dripnex-migrate-${randomUUID()}.db`; + paths.push(path); + const client = createClient({ url: `file:${path}` }); + await client.execute( + 'CREATE TABLE users (id text PRIMARY KEY, email text, created_at text, updated_at text)' + ); + + const report = await applyMigrations(client); + expect(report.recordedExisting).toContain('0000_chubby_zzzax'); + const second = await applyMigrations(client); + expect(second.skipped).toContain('0000_chubby_zzzax'); + }, + MIGRATION_TIMEOUT_MS + ); });