# Decisions — LexAI

> Lightweight ADRs. One entry per material decision: what, why, what was rejected, when. Newest at top. Supersede rather than rewrite.

## Log

### D-2026-03-06-01 — BYO-LLM-key, no backend

- **Status:** accepted
- **Context:** Hosted writing assistants cost a subscription and route user text through a third party. Target users already hold LLM API keys.
- **Decision:** No LexAI server. The background service worker calls the user's chosen provider (OpenAI/Anthropic/Groq/OpenRouter) directly with the user's key.
- **Alternatives considered:** A thin proxy backend (rejected: adds cost, privacy surface, and an account system); on-device browser AI only (rejected: too limited across providers — noted as a Proofly reference pattern).
- **Consequences:** Zero server cost and strong privacy story; shifts key handling and CORS entirely into the extension; no server-side rate limiting or abuse controls.
- **Verification:** Manual — confirm no network calls leave the extension except to the selected provider endpoint.
- **Owner / date:** Phase 1, 2026-03-06

### D-2026-03-06-02 — Inline styles, no Tailwind

- **Status:** accepted
- **Context:** Content-script UI must not be broken by host-page CSS; WXT PostCSS/Tailwind integration was not wired.
- **Decision:** Style all UI with inline style objects (`Object.assign(el.style, …)` / `style={{…}}`), using a dark Catppuccin-ish palette.
- **Alternatives considered:** Tailwind (left in devDependencies but inactive); shadow DOM + stylesheet (deferred — see D-...-03).
- **Consequences:** Reliable rendering on any host page; palette/button styles get duplicated across content/Options/Popup (refactor tracked in TASKS #5).
- **Verification:** Visual check on multiple sites.
- **Owner / date:** Phase 1, 2026-03-06

### D-2026-03-06-03 — No shadow DOM; `data-lexai` guard instead

- **Status:** accepted
- **Context:** Injected toolbar/modal could collide with host-page styles or re-trigger LexAI's own handlers.
- **Decision:** Inject directly into `document.body`; mark every LexAI node `data-lexai="true"` and skip events whose target is `closest('[data-lexai="true"]')`; use max `z-index` (2147483647).
- **Alternatives considered:** Shadow DOM (rejected for now: added complexity; revisit if style isolation issues appear).
- **Consequences:** Simple and working; weaker isolation than shadow DOM; highly customized editors (e.g. Google Docs) may not accept programmatic replace.
- **Verification:** Manual across textarea/input/contenteditable sites.
- **Owner / date:** Phase 1, 2026-03-06

### D-2026-03-06-04 — Eager selection snapshot before async

- **Status:** accepted
- **Context:** Focus shifts to the toolbar and the live selection is gone by the time an async provider response returns.
- **Decision:** Capture the active element + selection offsets eagerly (on `mouseup` and on button `mousedown`) and snapshot before any `await`; Replace uses the snapshot. Handle both textarea/input (`selectionStart/End`) and contenteditable/DOM (`Range` API).
- **Alternatives considered:** Re-reading selection after the response (rejected: selection no longer exists).
- **Consequences:** Replace works reliably; the pattern is fragile — editing content.ts must preserve snapshot-before-await. Zero automated coverage today (TASKS #10).
- **Owner / date:** Phase 1, 2026-03-06

### D-2026-03-06-05 — Dual `ANALYZE_TEXT` message shapes; `return true` listener

- **Status:** accepted
- **Context:** Content script/popup send `{ payload: {...} }`; other call sites send flat `{ text, action, style }`. Async responses need the message channel held open.
- **Decision:** The handler accepts both shapes; the `onMessage` listener returns `true`. `fix` normalizes to `grammar`.
- **Consequences:** Flexible but must be preserved in both forms; removing `return true` silently breaks all responses.
- **Owner / date:** Phase 1, 2026-03-06

### D-2026-xx-xx-06 — tweetnacl secretbox for the API key (obfuscation, not protection)

- **Status:** accepted — flagged for revisit
- **Context:** Storing the raw key in `chrome.storage.local` looked bad; added tweetnacl `secretbox` encryption (`apiKeyEnc` + `encKey`).
- **Decision:** Prefer the encrypted path; keep plaintext `apiKey` as back-compat until a migration exists.
- **Known weakness:** `encKey` is stored next to `apiKeyEnc`, so anyone who can read storage can decrypt. This is obfuscation, not protection (RECOMMENDATIONS #2).
- **Alternatives to consider:** derive the key from `chrome.storage.session` / WebCrypto / a user passphrase; and be honest in the UI ("stored locally, obscured"). Tracked in TASKS #2.
- **Owner / date:** post-Phase 1

### D-2026-03-06-07 — Gitea CI + Chrome Web Store deploy

- **Status:** accepted
- **Context:** Project hosts CI on Gitea, not GitHub Actions.
- **Decision:** `.gitea/workflows/ci.yml` (typecheck→test→build→zip to registry) and `deploy-chrome.yml` (on `v*.*.*` tag → CWS). Telegram notifications. Version must match in `package.json` and `wxt.config.ts`.
- **Known weakness:** workflows `git clone` into `/tmp` and set `http.sslVerify false` (RECOMMENDATIONS #17). Revisit for speed/security.
- **Owner / date:** Phase 1, 2026-03-06

### D-2026-08-12-08 — CHANGELOG.md drives Gitea release notes

- **Status:** accepted
- **Context:** Releases previously shipped a hardcoded release body (`## LexAI ${VERSION}` + generic install steps) that never said what actually changed in that version.
- **Decision:** Release notes live in `CHANGELOG.md` (Keep a Changelog format, Semantic Versioning). `.gitea/workflows/release.yml` extracts the section matching the pushed tag's version and uses it as the Gitea release body, with a generic fallback if no matching section exists. A version bump is not considered done until `CHANGELOG.md` has that version's section.
- **Alternatives considered:** Auto-generating notes from commit messages (rejected: commit history is not curated for user-facing wording); keeping the hardcoded body (rejected: uninformative to installers).
- **Consequences:** Every version bump now requires a `CHANGELOG.md` entry alongside the `package.json` bump; the release workflow degrades gracefully (generic body + logged warning) if that entry is missed rather than failing the release.
- **Verification:** `release.yml` reviewed by an independent critic; P2 findings fixed. Confirmed locally that the section-extraction logic matches `## [1.1.0]` and stops at the next `## [` heading.
- **Owner / date:** 2026-08-12

## Open / proposed

### D-PROPOSED — Narrow host permissions from `<all_urls>`

- **Status:** proposed (decide before serious Web Store push)
- **Context:** Content script injects into every frame of every site, including banking/email/internal apps; also the #1 CWS review slowdown (RECOMMENDATIONS #1).
- **Options:** `activeTab` + on-demand injection, or a user-configurable allowlist.
- **Verification:** confirm actions still work after narrowing; measure review outcome.
