From 3953dcf3648225056baba9347b7c436edbb51933 Mon Sep 17 00:00:00 2001 From: xcaliber Date: Mon, 2 Mar 2026 22:16:08 +0000 Subject: [PATCH] Changeset 0.22.4 (#146) --- CHANGELOG.md | 34 + VERSION | 2 +- docs/ARCHITECTURE.md | 5 +- docs/ROADMAP.md | 1290 +++++++++++++++-- server/config/config.go | 7 + .../migrations/015_v0224_health_enhance.sql | 33 + .../sqlite/014_v0224_health_enhance.sql | 22 + server/handlers/attachments.go | 115 ++ server/handlers/completion.go | 5 + server/handlers/export.go | 110 ++ server/handlers/health_admin.go | 3 + server/handlers/stream_loop.go | 27 +- server/health/accumulator.go | 220 ++- server/health/accumulator_test.go | 8 + server/main.go | 36 +- server/models/models.go | 28 + server/store/interfaces.go | 1 + server/store/postgres/attachment.go | 31 +- server/store/postgres/health.go | 94 +- server/store/sqlite/attachment.go | 31 +- server/store/sqlite/health.go | 94 +- src/js/api.js | 29 + src/js/editor-mode.js | 33 + src/js/projects-ui.js | 137 ++ src/js/ui-admin.js | 4 + 25 files changed, 2254 insertions(+), 145 deletions(-) create mode 100644 server/database/migrations/015_v0224_health_enhance.sql create mode 100644 server/database/migrations/sqlite/014_v0224_health_enhance.sql create mode 100644 server/handlers/export.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 446789e..df3a87a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,40 @@ All notable changes to Chat Switchboard. +## [0.22.4] — 2026-03-02 + +### Added +- **Rate limit tracking.** Provider health windows now track HTTP 429 / rate limit events separately from general errors. New `rate_limit_count` column on `provider_health` table. `RecordRateLimit()` method on health accumulator. Completion handler and stream loop classify `HTTP 429` / `rate limit` / `Too Many Requests` errors automatically. +- **Auto-disable policy.** Providers that report "down" status for N consecutive hourly windows are automatically deactivated. Configurable via `PROVIDER_AUTO_DISABLE_THRESHOLD` env var (default: 3, set to 0 to disable). `AutoDisabler` interface on health store, `checkAutoDisable()` runs on each flush cycle. +- **Tool health tracking.** Built-in tools (web_search, url_fetch, etc.) now record success/error/latency to `tool_health` table via the health accumulator. `RecordToolSuccess()` / `RecordToolError()` methods. Both tool execution sites in stream loop instrumented. `ToolHealthWindow` and `ToolHealthSummary` models. +- **Search provider health tracking.** web_search and url_fetch tool calls are now included in the tool health pipeline with per-invocation latency and error recording. +- **Project-specific file uploads.** `POST /api/v1/projects/:id/files` (multipart upload) and `GET /api/v1/projects/:id/files` (list). `project_id` column on `attachments` table. Files are stored under `projects/{id}/` prefix. Project access verified via ownership or team membership. Files queued for text extraction if enabled. +- **Workspace settings UI.** Git configuration section in project settings panel: remote URL, branch, credential selector. Appears when a workspace is bound. Saves via `PATCH /api/v1/workspaces/:id`. `updateWorkspace()` and `listGitCredentials()` API client methods. +- **PDF/DOCX export via pandoc.** New `POST /api/v1/export` endpoint converts markdown content to PDF or DOCX using pandoc. Editor export dropdown now includes "Export as PDF" and "Export as DOCX" options. Graceful error handling when pandoc is unavailable. +- **Project files UI.** File list and upload button in project settings panel. Shows file names and sizes. Multi-file upload support. +- **API client methods.** `projectUploadFile`, `projectListFiles`, `exportDocument`, `updateWorkspace`, `listGitCredentials`. + +### Changed +- `health/accumulator.go`: Added `rateLimitCount` to bucket, `RecordRateLimit()`, `RecordToolSuccess()`, `RecordToolError()`, `flushTools()`, `checkAutoDisable()`, `SetAutoDisable()`, `AutoDisabler` interface, `toolBucket` struct. `Store` interface extended with `UpsertToolWindow()`, `ListAllToolCurrent()`. +- `handlers/completion.go`: `HealthRecorder` interface extended with `RecordRateLimit()`, `RecordToolSuccess()`, `RecordToolError()`. `recordHealth()` classifies HTTP 429 errors. +- `handlers/stream_loop.go`: Both tool execution sites now record tool health. `recordHealthFn()` classifies rate limit errors. +- `models/models.go`: `RateLimitCount` field on `ProviderHealthWindow`. `ToolHealthWindow`, `ToolHealthSummary` types. `ErrorCount`, `RateLimitCount`, `TimeoutCount` fields on `ProviderHealthSummary`. `ProjectID` field on `Attachment`. +- `store/postgres/health.go`: All queries updated for `rate_limit_count`. `UpsertToolWindow()`, `ListAllToolCurrent()`, `DeactivateProvider()` methods. +- `store/sqlite/health.go`: Full rewrite with rate_limit_count, tool health, and auto-disable support. +- `store/postgres/attachment.go`: `project_id` in cols, scan, create, and new `GetByProject()`. +- `store/sqlite/attachment.go`: Same project_id additions. +- `store/interfaces.go`: `GetByProject()` on `AttachmentStore`. +- `config/config.go`: `ProviderAutoDisableThreshold` field + env var loading. +- `main.go`: SQLite health store branching, auto-disable wiring, export handler route, project file routes. +- `handlers/health_admin.go`: Health summary includes error_count, rate_limit_count, timeout_count. +- `src/js/ui-admin.js`: Health dashboard cards show "Rate Limits" count with warning color. +- `src/js/projects-ui.js`: Git settings section, project files section, file upload handler. +- `src/js/editor-mode.js`: PDF and DOCX export menu items and handler. +- `src/js/api.js`: 5 new API client methods. + +### Database +- Migration 015 (Postgres) / 014 (SQLite): `rate_limit_count` column on `provider_health`, `tool_health` table, `project_id` column on `attachments`. + ## [0.22.3] — 2026-03-02 ### Added diff --git a/VERSION b/VERSION index fabc491..4240544 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.22.3 \ No newline at end of file +0.22.4 diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index c30e553..2871313 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -151,14 +151,14 @@ server/ ├── crypto/ # AES-256-GCM API key encryption ├── events/ # EventBus + WebSocket hub ├── extraction/ # Document text extraction pipeline -├── health/ # Provider health tracking (v0.22.0) +├── health/ # Provider health tracking (v0.22.0), tool health + auto-disable (v0.22.4) ├── routing/ # Policy-based request routing (v0.22.2) │ ├── types.go # Policy, Candidate, Context, Decision types │ ├── evaluator.go # Policy evaluation engine (4 policy types) │ ├── evaluator_test.go # 12 tests │ ├── fallback.go # RunWithFallback candidate dispatcher │ └── convert.go # DB model → routing type conversion -│ ├── accumulator.go # In-memory counters, 60s flush to DB, status derivation +│ ├── accumulator.go # In-memory counters, 60s flush, rate limits, tool health, auto-disable │ └── accumulator_test.go # Concurrent recording, flush isolation, threshold tests ├── memory/ # Long-term memory extraction + scanning │ ├── extractor.go # Conversation analysis → fact extraction @@ -178,6 +178,7 @@ server/ │ ├── stream_loop.go # Shared streaming + tool execution loop │ ├── capabilities.go # ModelHandler: model list + health enrichment + ResolveModelCaps (v0.22.3) │ ├── health_admin.go # Provider health + capability override admin endpoints +│ ├── export.go # Markdown → PDF/DOCX conversion via pandoc (v0.22.4) │ ├── routing_admin.go # Routing policy CRUD + dry-run test (v0.22.2) │ ├── presets.go # Persona CRUD (all scopes) │ ├── apiconfigs.go # User provider config CRUD (BYOK) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 3d5b602..8416fd6 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -17,129 +17,1220 @@ Features have real dependencies. This ordering respects them. ``` v0.9.x Stability + Quick UX Wins ✅ - │ -v0.10.x Providers + Models + Roles ✅ - │ -v0.11.x Tool Framework + Extension Base ✅ - │ -v0.12.x File Storage + Vision ✅ - │ -v0.13.x Web Tools + Toggle ✅ - │ -v0.14.x Knowledge Bases (RAG) ✅ - │ -v0.15.x Auto-Compaction ✅ - │ -v0.16.x Groups + Resource Grants ✅ - │ -v0.17.x Personas + Notes/CM6 + SQLite ✅ - │ -v0.18.x Memory System ✅ - │ -v0.19.x Projects/Workspaces ✅ - │ -v0.20.x Surface System ✅ - │ -v0.21.x Admin UX + Routing ✅ - │ -v0.22.x Routing Policies + Provider Chains ✅ - │ -v1.0 Stability + Multi-Tenant ⏳ + │ +v0.9.3 Content Visibility & Block Controls ✅ + │ +v0.9.4 API Key Encryption + Vault ✅ + │ +v0.10.0 Model Roles (utility + embedding) ✅ + + Usage Tracking + │ +v0.10.2 Summarize & Continue ✅ + │ +v0.10.3 Frontend Refactor ✅ + │ +v0.10.4 Model Type Pipeline + Role Fixes ✅ + │ +v0.10.5 UI Primitives + Extension Surfaces ✅ + │ +v0.11.0 Extension Foundation (browser tier) ✅ + │ + ┌───────┴──────────────┐ + │ │ +v0.12.0 File Handling v0.13.0 Admin Panel + + Vision ✅ Refactor (fullscreen) ✅ + │ │ + │ v0.13.1 Web Search + │ + url_fetch ✅ + │ │ + └───────┬──────────────┘ + │ +v0.14.0 Knowledge Bases ✅ (embedding role + file storage + pgvector) + │ +v0.15.0 Compaction ✅ (utility role + background job) + │ +v0.15.1 Context Recall Tools ✅ (attachment_recall, conversation_search) + │ + ┌───────┴──────────────┐ + │ │ +v0.16.0 User Groups v0.17.0 Persona-KB Binding + + Resource Grants ✅ + Enterprise KB Mode + │ + QOL / UX Wins + └───────┬──────────────┘ + │ + ┌───────┴──────────────┐ + │ │ +v0.17.1 SQLite Backend ✅ v0.17.2 CodeMirror 6 ✅ +v0.17.3 Notes Graph + Wikilinks ✅ + (dual DB) (editor bundle, chat + │ input, ext editor) + └───────┬──────────────┘ + │ +v0.18.0 Memory ✅ (user + persona scopes, review pipeline) + │ +v0.18.1 Side Panel Architecture ✅ (single-slot, ctx.ui primitives, mermaid refactor) + │ +v0.19.0 Projects / Workspaces ✅ (project groups, KB resolution, drag-drop sidebar) + │ +v0.19.1 Active Project + System Prompt + Detail Panel ✅ + │ +v0.19.2 Project Persona Default + Archive + Reorder ✅ + │ +v0.20.0 Notifications + @mention Routing + Multi-model ✅ + │ +v0.21.0 Workspace Storage Primitive ✅ + │ + ┌───────┴──────────────┐ + │ │ +v0.21.1 Workspace ✅ v0.21.3 Surface Infra ✅ + Tools + Bindings + REPL (parallel) + │ │ +v0.21.2 Workspace ✅ v0.21.5 Editor Surface ✅ + Indexing + Search (Development Mode) + │ │ +v0.21.4 Git ✅ v0.21.6 Editor Surface ✅ + Integration + Document Output + └───────┬──────────────┘ + │ +v0.21.7 Bugfix: scanJSON driver buffer aliasing ✅ + │ +v0.22.0 Provider Health + Capability Overrides ✅ + + Workspace Pane Refactor (FE) + │ +v0.22.1 Provider Extensions (declarative config) ✅ + │ +v0.22.2 Routing Policies + Fallback Chains ✅ + │ +v0.22.3 Provider Admin UI + Deferred Polish ✅ + │ +v0.22.4 Provider Health UX + Project Files + Export ✅ + │ +v0.23.0 Multi-Participant Channels + Presence + │ +v0.24.0 Auth Strategy (mTLS/OIDC) + Full RBAC + Group permissions + │ + ┌───────┴──────────────┐ + │ │ +v0.25.0 Workflow v0.26.0 Tasks / + Engine Autonomous Agents + (team-owned, (service channels, + staged processes, scheduler, unattended + human + AI collab) execution) ``` +--- + ## Completed Releases -### ✅ v0.22.3 — Provider Admin UI + Deferred Polish +> Full details for all completed versions are in [CHANGELOG.md](../CHANGELOG.md). + +### ✅ v0.9.0 — Schema Consolidation + BYOK +21 migrations → single schema, store layer abstraction, persona-as-trust-boundary, +BYOK with auto-fetch, composite model IDs, journey integration tests. + +### ✅ v0.9.1 — Capability Architecture + Docs +Removed static known model table. Resolution chain: catalog → heuristic only. +Frontend relies solely on backend for capabilities. Docs rewrite. + +### ✅ v0.9.2 — Quick UX Wins + Hardening +Collapsible code blocks, HTML preview, token counter, context warning, proxy +interception detection, environment injection, team admin audit scoping. + +### ✅ v0.9.3 — Content Visibility & Block Controls +Button-driven code collapse, always-rendered thinking blocks, tool call +persistence in history, Notes panel → unified side panel, streaming refactor +(`streamWithToolLoop`), regenerate fix. + +### ✅ v0.9.4 — API Key Encryption + Vault +Two-tier AES-256-GCM: env-var key for global/team, per-user UEK (Argon2id) +for personal BYOK. `server/crypto/` package. Migration 003. Startup backfill. +DOMPurify strict allowlist (XSS fix). + +### ✅ v0.10.0 — Model Roles + Usage Tracking + Vault Debt +Named role slots (utility, embedding) with fallback. `Provider.Embed()` interface. +`usage_log` + `model_pricing` tables. Streaming token capture. Vault debt: +UEK re-wrap on password change, destruction on admin reset. + +### ✅ v0.10.1 — Polish + UX +Admin system prompt injection, Personas tab, Team Management modal extraction, +preview pane enhancements, code download, personal usage scoped to BYOK. + +### ✅ v0.10.2 — Summarize & Continue + Role Cleanup +User-triggered conversation compaction via utility role. Summary as tree node +with boundary metadata. BYOK role overrides (personal → team → global). +Utility rate limiting. Generation role removed. + +### ✅ v0.10.3 — Frontend Refactor +2 monolith files → 13 domain-scoped files (~544 lines avg). Zero features, +no function renames. Vanilla JS, no build step. +See [REFACTOR-0.10.3.md](REFACTOR-0.10.3.md). + +### ✅ v0.10.4 — Model Type Pipeline + Role Fixes +`model_type` (chat/embedding/image) captured end-to-end from provider API +through catalog to frontend. Role dropdowns filter by type. + +### ✅ v0.10.5 — UI Primitives + Extension Surfaces +Shared `Providers`/`Roles` registries, 6 consolidated render primitives, +`showConfirm()` replacing native dialogs, `.popup-menu` CSS primitive. +Removed ~400 lines of duplication. + +### ✅ v0.11.0 — Extension Foundation (Browser Tier) +Full extension lifecycle: manifest, loader, scoped `ctx.*` API, browser tool +bridge via WebSocket. Custom renderer pipeline. 2 server tools (calculator, +datetime), 6 built-in browser extensions (Mermaid, KaTeX, CSV, Diff, JS +Sandbox, Regex). See [EXTENSIONS.md](EXTENSIONS.md). + +### ✅ v0.12.0 — File Handling + Vision +S3/PVC storage backend, image/file upload (📎, drag-drop, paste), multimodal +message assembly, text extraction pipeline (PDF/DOCX/XLSX/PPTX/ODT/RTF), +vault CLI (`rekey`/`status`), per-chat model persistence. + +### ✅ v0.13.0 — Admin Panel Refactor +12-tab modal → fullscreen admin panel. 4 categories (People, AI, System, +Monitoring) × section sidebar. URL-based routing, responsive, banner-aware. +CSS design token cleanup. + +### ✅ v0.13.1 — Web Search + URL Fetch +`web_search` + `url_fetch` tools. Search provider abstraction (DuckDuckGo, +SearXNG). Tool categories with per-tool toggle UI in chat bar. + +### ✅ v0.14.0 — Knowledge Bases +RAG: upload → chunk → embed (pgvector) → `kb_search` tool. Channel KB toggle. +Team/personal KB scopes. Notes semantic search. Admin panel KB management. +See [DESIGN-0.14.0.md](DESIGN-0.14.0.md). + +### ✅ v0.15.0 — Compaction +Background scanner with configurable thresholds. Automatic summarization via +utility role. Per-channel opt-in/out. Context budget guard rail (80% ceiling). + +### ✅ v0.15.1 — Context Recall Tools +`attachment_recall` (list + read, channel-scoped), `conversation_search` +(full-text via `plainto_tsquery`). Token estimator attachment awareness. + +### ✅ v0.16.0 — User Groups + Resource Grants +Groups (global/team-scoped ACLs) decoupled from team membership. Three-way +resource grants (team_only/global/groups) for Personas and KBs. Grant picker +UI. Schema consolidation: 9 migrations → single `001_v016_schema.sql`. + +--- + +## v0.17.0 — Persona-KB Binding + Enterprise KB Mode + QOL ✅ + +Personas become **gateways** to knowledge. In enterprise deployments, +users don't interact with KBs directly — they talk to Personas that +have KBs attached. Plus quality-of-life improvements: chat rename, +token display, state persistence, utility model auto-naming. + +Depends on: knowledge bases (v0.14.0), user groups (v0.16.0). + +**Persona-KB Binding** +- [x] `persona_knowledge_bases` table: `persona_id`, `kb_id`, `auto_search` (bool — always search vs tool-only) +- [x] When a channel uses a Persona with bound KBs, `kb_search` is auto-scoped to those KBs +- [x] No user toggle needed — Persona carries its own KB context +- [x] Channel KB toggle becomes "additional KBs" on top of Persona-provided ones +- [x] Persona system prompt auto-includes KB listing hint (reuses `BuildKBHint` pattern) +- [x] Admin/team admin UI: KB picker on Persona create/edit form + +**Enterprise KB Mode** +- [x] `discoverable` flag on knowledge_bases: `true` (default — appears in user's KB popup) or `false` (hidden, only accessible through Persona binding) +- [x] Hidden KBs don't appear in `GET /api/v1/knowledge-bases-discoverable` for non-admins +- [x] Hidden KBs still searchable by `kb_search` tool when accessed through a Persona +- [x] Admin panel: toggle discoverability per KB +- [x] Platform policy: `kb_direct_access` — when `false`, disables channel KB popup entirely (strict enterprise mode) + +**Access Control Integration** +- [x] Persona grants (v0.16.0 groups) control who can *use* a Persona +- [x] KB grants control who can *manage* a KB (upload/delete docs) +- [x] Persona-KB binding grants implicit *search* access through the Persona +- [x] Users who can use a Persona can search its KBs, even if they can't see those KBs directly + +**Team Admin Workflow** +- [x] Team admin creates KB → uploads documents → marks non-discoverable +- [x] Team admin creates Persona → binds KBs → sets access to Team Only or specific Groups +- [x] Team members see the Persona in their preset list, use it, get KB-powered answers +- [x] Team members never see or manage the underlying KBs + +**Technical Debt (carried forward)** +- [x] **Security**: KB create handler authorization — team/global scope verified +- [x] **Test hygiene**: stripped DIAG diagnostics from `TestGroupBasedPersonaAccess` +- [x] **Architecture**: `KnowledgeBaseStore.UpdateDocumentStorageKey()` — moved to store layer +- [x] **UX**: Embedding dropdown fix — tolerant type filter, manual model ID fallback, auto-switch on empty _(already implemented in `ui-primitives.js`)_ +- [x] **Minor**: Paste-to-file character threshold — synced from backend `PublicSettings` (`storage.paste_to_file_chars`), admin-configurable, default 2000 + +**Role Fallback Alerts** ([#69](https://git.gobha.me/xcaliber/chat-switchboard/issues/69)) +- [x] Audit log entry on fallback fire: `action = "role.fallback"` with primary/fallback model IDs and error string +- [x] `role.fallback` WebSocket event via EventBus (`PublishAsync`, zero latency impact on completion path) +- [x] In-memory cooldown on `Resolver` (per-role, 5-minute TTL) to deduplicate repeated fallback alerts +- [x] Admin UI: persistent alert banner on `role.fallback` event (persists until dismissed) +- [x] Wire `*events.Bus` into `roles.NewResolver` via `.WithBus()` builder + +**QOL / UX Wins** +- [x] **Chat rename**: inline edit on sidebar item — dblclick title → input → blur/Enter saves via `API.updateChannel(id, { title })` +- [x] **Chat token count**: display `Tokens.estimateConversation()` in model bar, update on `selectChat` and after each message +- [x] **State restore on refresh**: persist `App.currentChatId` to `sessionStorage` on select, restore on load +- [x] **Utility model auto-naming**: after first assistant response, background utility role request → `POST /channels/:id/generate-title` → re-render sidebar. Fallback to truncation when no utility model configured. Debounced via `_autoNamePending` set +- [x] **SW extension cache fix**: exclude `/extensions/` from service worker fetch handler +- [x] **Mermaid renderer**: promoted enhanced version (v2.0 — pan/zoom, SVG/PNG export, source copy) to `extensions/builtin/mermaid-renderer/` + +--- + +## v0.17.1 — SQLite Backend ✅ + +Dual-database support. Every subsequent schema change is developed +against both Postgres and SQLite from day one — no retrofit. SQLite +enables single-binary deployment for dev, demo, edge, and single-user. + +Depends on: v0.17.0 DB debt cleanup (store layer is sole DB interface). + +**Store Layer** +- [x] 19 SQLite store implementations covering all domain interfaces +- [x] `DB_DRIVER` env var: `postgres` (default) | `sqlite` +- [x] Separate migration files per driver (`migrations/sqlite/`) +- [x] WAL mode + single-writer connection pooling for SQLite concurrency +- [x] CI matrix: full handler integration tests against both Postgres and SQLite + +**Vector Search** +- [x] App-level cosine similarity in Go (pure Go, no CGO/sqlite-vec required) +- [x] Embeddings stored as JSON text in `kb_chunks.embedding` column +- [x] Full feature parity with pgvector: `SimilaritySearch`, `InsertChunks` +- [x] Graceful: adequate performance for SQLite-scale deployments (single-user, not millions of chunks) + +**Schema Compatibility** +- [x] UUID generation: application-side `store.NewID()` (Go `uuid.New()`) +- [x] JSONB → JSON text columns with `ToJSON()`/`ScanJSON()` helpers +- [x] `UUID[]` arrays → JSON arrays with `ToJSONArray()`/`ScanJSONArray()` +- [x] `TIMESTAMPTZ` → SQLite `TEXT` with `datetime('now')` +- [x] `ON CONFLICT excluded.*` pattern (works in both dialects) +- [x] `INSERT RETURNING id` → separate query pattern for SQLite + +**Test Infrastructure** +- [x] `dialectSQL()` converts `$N` → `?` and strips `::jsonb` at runtime +- [x] `database.PH(n)` returns dialect-appropriate placeholder +- [x] `database.TruncateAll()` dialect-aware (DELETE + PRAGMA vs TRUNCATE CASCADE) +- [x] Generic provider config: `PROVIDER`/`PROVIDER_KEY`/`PROVIDER_URL` (replaces `VENICE_API_KEY`) +- [x] Model selection prefers non-reasoning models in live tests + +**What SQLite mode skips** (feature-gated, not broken): +- [x] `LISTEN/NOTIFY` — WebSocket event fan-out uses in-process EventBus (already works) +- [x] Full-text search `tsvector` — conversation_search uses LIKE fallback + +--- + +## v0.17.2 — CodeMirror 6 Integration ✅ + +Rich editor infrastructure for the frontend. CM6 is ESM-native; the build +step runs in Docker (esbuild → single IIFE bundle). No change to dev +experience for non-editor code — the rest of `src/js/` stays vanilla. + +Depends on: nothing (frontend-only). Prerequisite for: extension surfaces +(v0.21.5 editor surface). See [DESIGN-CM6.md](DESIGN-CM6.md) for full spec. + +**Build Pipeline** +- [x] `src/editor/` directory: `package.json`, `build.mjs`, `index.mjs`, language/theme modules +- [x] `scripts/build-editor.sh`: shared build script for both Dockerfiles and local dev +- [x] New Docker stage (`cm6-build`): `npm ci` + `node build.mjs` → `vendor/codemirror/` +- [x] Both `Dockerfile.frontend` and `Dockerfile` (unified) use the shared build script +- [x] Bundle output: `codemirror.bundle.js` (~295KB min, ~90KB gzip) +- [x] Graceful degradation: all integration points check `window.CM` — falls back to `