merge: feature/extension-cli-parity-gap-analysis (v0.8.1) — extension<->CLI parity gap analysis (forward plan referenced by STATUS/ROADMAP)

This commit is contained in:
adlee-was-taken
2026-06-20 22:21:05 -04:00

View File

@@ -0,0 +1,169 @@
# Extension ↔ CLI Parity Gap Analysis
- **Date:** 2026-06-20
- **Author:** Dev-D, reconciled against an independent PM parity sweep
- **Status:** Draft for review — **forward-planning**, NOT v0.8.1 scope
- **Anchor commit:** `origin/main` `b09e0ce` (v0.8.0 org vault + v0.8.1 Dev-A foundation merged; Dev-B/C/D in flight)
- **Scope note:** This plans a *future* milestone. Extension org **writes** remain explicitly out of scope for v0.8.1 per `docs/superpowers/specs/2026-06-20-relicario-v0.8.1-parity.md` (Plan B-2).
## Purpose
Survey the gap between the Relicario **CLI** (`relicario`) and the **browser extension**, classify every gap as a *real parity gap*, an *intended CLI-only* capability, or *already-planned-in-a-spec*, and produce a prioritized work list (with rough sizing) to bring the extension up to CLI parity. The driver is the project's **CLI/extension parity philosophy**: features should not ship "CLI-first, extension-later" without an explicit, recorded decision — this doc is that record for the current backlog.
## Method
Two **independent** surveys were run and then reconciled:
- **PM sweep** — 3 inventory agents + synthesis.
- **Dev-D sweep** — 4 parallel readers (CLI / extension-UI / extension-SW / specs+roadmap) → synthesis → an adversarial completeness/accuracy critic, all reading from a worktree pinned at `b09e0ce`.
The two sweeps were deliberately blind to each other. All load-bearing claims in this document were **hand-verified against source** (greps + line reads); where the two sweeps disagree, the disagreement is flagged explicitly in §Reconciliation. Line citations are point-in-time against `b09e0ce` and may drift.
## Executive summary
Core item-CRUD parity is **excellent**. All 7 item types (Login, Secure Note, Identity, Card, Key, TOTP, Document) and the add / edit / view / list / trash / restore lifecycle are at full parity, and in several places the **extension is the richer surface** (live TOTP codes, custom fields/sections, TOTP-from-QR, password coloring, session auto-lock, autofill/capture). Where a *per-type* gap exists it is most often on the **CLI** side, not the extension's.
The genuine **extension-side** gaps cluster into three buckets:
1. **Metadata-management gaps (the headline finding):** editing **groups**, **tags**, and **filtering** is wired into only specific forms/surfaces in the extension, while the CLI offers them uniformly across all types; **favorites** has *zero* extension UI (strictly worse than the CLI). These are real, currently-shipping parity gaps on the *personal* vault.
2. **Backend-exists-but-no-wire/UI:** attachment **removal** (`removeAttachmentsFromItem` helper exists, no `remove_attachment` router message), **per-item purge** (`purge_item` handler exists, only a bulk "empty trash" UI), and the `isInTab()` popup-mode gate that hides login/secure-note attachment editing in the popup window.
3. **The org (enterprise) vault** — the single largest gap. The entire org feature (shipped CLI-only in v0.8.0) has **no extension presence** (no org routes, no org context). This is fully specced and explicitly deferred (Dev-D org-read / Plan B-2 org-write).
Plus one quality gap on the personal side surfaced by the PM sweep: **autofill hostname matching** is a naive exact-equality match.
Intentionally **CLI-only by design** (not gaps): real `git pull`/`push` and `.git`-history backup bundling (the extension writes straight to the host Contents API and keeps no local repo), the `imgsecret embed` recovery subcommand, recovery-QR's deliberate no-file-write contract, org **admin** (members/collections/grants/rotate/audit), and shell completions.
## Reconciliation with the PM sweep
The PM sweep concluded: extension at *near-full parity* on the personal surface, ahead in places, with the **org vault as the one material gap** and **autofill hostname matching as the only personal-side quality gap**.
**Agreements (both sweeps, independently):**
- Org vault is the largest gap; it is fully specced and deferred (Dev-D read / Plan B-2 write).
- The extension leads on live TOTP, custom fields/sections, password coloring.
- The intended-CLI-only set: git sync/push, `.git` backup bundling, device-key deploy-key plumbing, org admin, shell completions.
**Dev-D refines / partially refutes the PM:** the personal surface is **not** "near-full parity with autofill as the only gap." There is a real cluster of **personal-side extension gaps** the PM sweep understated:
- **Favorites — none in the extension** (`favorite` only round-trips through save fns; no toggle, no star in lists, no filter). The CLI is itself only add-only, so the extension is *strictly worse*. The PM hypothesis did not list this.
- **Group editing — Login-form only** (`f-group` + `wireGroupAutocomplete` live in `login.ts` only; card/key/identity/totp/document forms pass `group` through without an input).
- **Tag editing — Document-form only** (`f-tags` in `document.ts` only; other forms preserve-but-don't-edit).
- **Filter — popup has no type filter** (vault-tab only) and **no tag filter** anywhere.
- **Per-item purge** and **attachment add/remove** have working backends but no popup-reachable UI / no router wire.
**PM caught, Dev-D's taxonomy missed:** **autofill hostname matching.** `service-worker/vault.ts` (`findByHostname`, equality at `:344`) matches credentials by exact `icon_hint` equality (`(e.icon_hint ?? '').toLowerCase() === hostname`) — no `www.` strip, no registrable-domain (eTLD+1) match, so `www.example.com` will not match an item stored as `example.com`. Confirmed; folded in as a real LOW-MED personal-side gap. (Dev-D's capability taxonomy centered on item-CRUD/features and under-weighted the content-script autofill path — the PM sweep is the reason it appears here.)
**Methodology correction (a Dev-D self-sweep error, struck here):** the Dev-D extension-SW inventory referred to a `messages.ts` "that does not exist at that path." **That is false** — the file exists at `extension/src/shared/messages.ts` (227 lines): it holds the `PopupMessage` union (with `delete_item // soft-delete` at line 23), `POPUP_ONLY_TYPES` (line 168), and `CONTENT_CALLABLE_TYPES` (line 224). The inventory had merely dropped the `shared/` directory prefix. The substantive findings it supported (the unwired `searchItems`/`removeAttachmentsFromItem` helpers) are independently verified correct; the "file doesn't exist" caveat is removed from this document.
## Parity matrix
Support: **full** / **partial** / **none** / **n/a**. `gap_class`: **at-parity** · **real-gap** (extension work) · **real-gap (CLI-side)** (extension already ahead; CLI backlog) · **cli-only-by-design** · **already-planned**.
### Item types
| Capability | CLI | Ext | gap_class | Notes (evidence @ `b09e0ce`) |
|---|---|---|---|---|
| Login: create/view/edit | full | full | at-parity | CLI `add/get/edit login`; ext form + `add_item`/`update_item`/`get_item`. |
| Secure Note: create/view/edit | full | full | at-parity | Both complete. |
| Identity: create | full | full | at-parity | Both; ext also exposes `address`. |
| Identity: view | full | full | at-parity | Both. |
| Identity: edit | partial | full | real-gap (CLI-side) | CLI `edit_identity` omits `date_of_birth` + records no history; ext edits all. CLI backlog. |
| Card: create/view | full | full | at-parity | Both. |
| Card: edit | partial | full | real-gap (CLI-side) | CLI `edit_card` = holder+number only (no CVV/PIN/expiry/kind); ext edits all. CLI backlog. |
| Key: create/view | full | full | at-parity | Both; ext takes `public_key` interactively. |
| Key: edit | partial | full | real-gap (CLI-side) | CLI `edit_key` = key-material only (no label/algorithm/public_key); ext edits all. CLI backlog. |
| TOTP: create | full | full | at-parity | Both; ext adds Steam Guard kind. |
| TOTP: view | partial | full | real-gap (CLI-side) | CLI shows metadata only; ext shows live rotating code. See "TOTP live code". |
| TOTP: edit | full | full | at-parity | Both. |
| Document: create | full | full | at-parity | CLI encrypts file as attachment; ext `upload_attachment`. |
| Document: view | partial | full | real-gap (CLI-side) | CLI metadata + `extract`; ext inline image preview. CLI backlog. |
| Document: edit | none | full | real-gap (CLI-side) | CLI `edit` on Document is a no-op redirect to attach/extract; ext changes primary/supplementary files. CLI backlog. |
### Operations
| Capability | CLI | Ext | gap_class | Notes |
|---|---|---|---|---|
| add / edit / get / list | full | full | at-parity | All 7 types both surfaces. |
| rm / soft-delete | full | full | at-parity | CLI `rm`; ext `delete_item` (`messages.ts:23`, handler `popup-only.ts`). |
| trash (list) | full | full | at-parity | CLI `trash`; ext trash view. |
| restore from trash | full | full | at-parity | CLI `restore`; ext `restore_item`. |
| purge (permanent) | full | partial | **real-gap** | Ext UI only bulk "empty trash" (`purge_all_trash`, `popup-only.ts:420`); **no per-item purge UI**, though `purge_item` handler exists (`popup-only.ts:409`). CLI has single + bulk. |
### Features
| Capability | CLI | Ext | gap_class | Notes |
|---|---|---|---|---|
| Attachments: add | full | partial | **real-gap** | Login/secure-note attachment editing gated behind `isInTab()` (`login.ts:370,388`; `secure-note.ts:123,140`) — unavailable in popup window; document renders unconditionally (`document.ts`). SW `upload_attachment` is full. |
| Attachments: view/download | full | full | at-parity | CLI `extract`; ext download + `download_attachment`. |
| Attachments: remove | full | partial | **real-gap** | SW helper `removeAttachmentsFromItem` (`vault.ts:492`) has **no router wire** (`remove_attachment` absent — confirmed). UI removes refs at form-save only, with the same `isInTab()` caveat. CLI `detach` is full. |
| TOTP: live code | none | full | real-gap (CLI-side) | CLI reveals raw base32 only; ext computes live codes. Extension leads. No spec mandates CLI OTP. |
| Generator: password / passphrase | full | full | at-parity | CLI `generate`; ext generator-panel + `generate_password`. |
| Settings: view / edit | full | full | at-parity | CLI `settings`; ext `get/set_vault_settings`. |
| Search | partial | partial | at-parity | CLI: title-substring. Ext: client-side over title/group/tags/icon_hint; SW `searchItems` (`vault.ts:316`) exists but **unwired** (no `search_items` message). Neither does field-value full-text. |
| Filter | full | partial | **real-gap** | CLI `list` filters type/group/tag/trashed. Ext: type filter is **vault-tab-only**; popup has none; **no tag filter anywhere**. SW `list_items` filters by `group` only. |
| Favorites | partial | **none** | **real-gap** | CLI add-only (`--favorite` on Login add, `*` in list; no toggle/filter). Ext: **zero UI**`favorite` only round-trips. Ext strictly worse; needs a paired CLI+ext design. |
| Tags | full | partial | **real-gap** | CLI full create+filter all types. Ext: only Document form edits tags (`f-tags` in `document.ts`); no tag chips in lists; no tag filter. SW round-trips tags. |
| Groups/folders | full | partial | **real-gap** | CLI all types `--group`, `list --group`. Ext: only Login form has `f-group`+autocomplete; other forms set no group; vault-tab "group" filter is actually a type filter. SW `list_groups`/group-filter full. |
| Field history (view) | full | full | at-parity | CLI `history`; ext `get_field_history`. |
| Custom fields / sections | none | full | real-gap (CLI-side) | CLI has no custom-field/section commands (core supports them); ext `renderSectionsEditor` covers all 7 types. CLI backlog. |
| Autofill hostname matching | n/a | partial | **real-gap** | Ext-only feature; matcher (`vault.ts` `findByHostname`, `:344`) is exact `icon_hint` equality — no `www.` strip / eTLD+1. `www.x.com``x.com`. (PM-surfaced.) |
### Org (enterprise) vault
| Capability | CLI | Ext | gap_class | Notes |
|---|---|---|---|---|
| Org: read items | full | none | already-planned | CLI `org get/list` grant-filtered, all 7 types. Ext has zero org code. Planned Dev-D. Spec: `2026-06-06-relicario-enterprise-org-vault-design.md` § Extension — Org Context; ROADMAP "Extension org parity — read". |
| Org: write (add/edit/rm) | partial | none | already-planned | CLI write = Login/SecureNote/Identity only (`OrgAddKind`, `main.rs:560`; Card/Key/Document/Totp absent — v0.8.1 lift in flight). Ext none. Planned Plan B-2. Spec: `2026-06-20-relicario-v0.8.1-parity.md` § Out of scope. |
| Org: member/collection mgmt | full | none | cli-only-by-design | CLI full lifecycle (~19 subcommands). Ext none — org **admin** is intended CLI-only (high-trust, low-frequency). |
### Vault lifecycle / infra
| Capability | CLI | Ext | gap_class | Notes |
|---|---|---|---|---|
| Vault init / setup | full | full | at-parity | CLI `init`; ext setup wizard + `create_vault`/`attach_vault`. |
| Git sync (pull/push) | full | partial | cli-only-by-design | CLI real `git pull --rebase`/`push`. Ext writes straight to host Contents API; no local graph (`ahead`/`behind` always 0). Functionally syncs; architecturally different by design (`extension/ARCHITECTURE.md`). |
| Device management | full | full | at-parity | CLI `device`; ext `renderDevices` + SW device CRUD. (GitHub/GitLab deploy-key API is the deferred edge.) |
| Backup / restore | full | full | at-parity | CLI `.relbak` + git-history bundling; ext `export/restore_backup`. `.git` bundling sub-aspect is cli-only-by-design (ext has no local repo). |
| Import (LastPass) | partial | partial | at-parity | Both LastPass-CSV only; other importers deferred both surfaces by policy. |
| Recovery QR | full | full | at-parity | CLI generate/unwrap; ext `generate/unwrap_recovery_qr`. Webcam scan deferred both. |
| Standalone generate (no vault) | full | none | cli-only-by-design (low-confidence) | CLI `generate`/`rate` work outside a vault; ext generator is embedded in login form + settings (needs unlocked vault). A browser extension lacks the "no vault" generator use-case a shell has. No spec; flag if user demand appears. |
### Intended CLI-only (no taxonomy row; recorded so they are not re-litigated as gaps)
| Capability | gap_class | Notes / spec |
|---|---|---|
| Recovery-QR file-write (`--out`) | cli-only-by-design | Negative API contract — no surface writes the payload to disk; absence *is* the security property. `2026-05-01-recovery-qr-design.md`. |
| Org delete-org push to remote | cli-only-by-design | Phase-1 delete-org is a local tombstone; pre-receive hook rejects protected-file deletion. Pushable delete-org is phase-2. `2026-06-06-...-design.md`. |
| `imgsecret embed` subcommand | cli-only-by-design | CLI disaster-recovery tool; the extension setup wizard's image flow covers the equivalent. |
| Password coloring (CLI TTY) | cli-only-by-design (inverted) | Ext shipped it (v0.5.1); CLI TTY parity deferred until demand. `2026-05-01-password-coloring-design.md` § Out of scope. |
| Shell completions | cli-only-by-design | No extension analogue. |
## Gap classification summary
- **Real extension gaps (extension work closes them):** per-item purge UI; attachment add/remove UI + `remove_attachment` wire + `isInTab()` gate; popup type filter + tag filter; tag editing on all forms; group editing on all forms; favorites UI; autofill registrable-domain matching; **org read** (specced); **org write** (specced, behind CLI type parity).
- **CLI-side gaps (extension already ahead — separate CLI backlog, NOT extension work):** Identity/Card/Key edit field coverage; Document view/edit; live TOTP code; custom fields/sections commands.
- **Intended CLI-only (not gaps):** git pull/push, `.git` backup bundling, org admin, `imgsecret embed`, recovery-QR file-write, shell completions, standalone generate.
- **Already planned / deferred:** org read (Dev-D), org write + org item-type breadth (Plan B-2), org attachments/multi-vault (behind org).
## Prioritized forward work (extension)
Only items where **extension work** closes the gap. CLI-side gaps and intended-CLI-only items are excluded.
| Pri | Item | Size | Why | Depends on |
|---|---|---|---|---|
| **P0** | **Attachment remove + un-gate popup:** wire a `remove_attachment` router message to `removeAttachmentsFromItem` (`vault.ts:492`); drop the `isInTab()` gate so login/secure-note attachment **add & remove** work in the popup. Closes *both* the add half (row "Attachments: add") and remove half. | M | Backend exists and is unreachable — highest value-per-effort. The popup-mode gate is a UX cliff (can't manage login attachments without popping out). | — |
| **P0** | **Per-item purge UI:** surface the existing `purge_item` handler (`popup-only.ts:409`) as a per-row permanent-delete in the trash view (today only bulk `purge_all_trash`). | S | Pure UI wiring over an existing handler; CLI has single-item purge. | — |
| **P1** | **Group editing on all type forms:** add `f-group` + `wireGroupAutocomplete` to card/key/identity/totp/document (Login-only today). | M | SW (`list_groups`, group filter) already full; replicate one existing form pattern across 5 forms. | — |
| **P1** | **Tag editing on all type forms + tag chips in lists:** promote Document's `f-tags` to a shared affordance on all 7 forms. | M | SW round-trips tags fully; only Document edits them today. | — |
| **P1** | **Filter parity:** add a type filter to the popup (vault-tab has it) and a tag filter to popup + vault tab; optionally push type/tag params into `list_items`. | M | CLI filters type/group/tag; ext type filter is fullscreen-only, tag filter absent. | Tag editing (so tags exist to filter on) |
| **P2** | **Favorites (paired CLI + ext):** favorite toggle in detail/edit, favorites filter, star in list rows — and extend the CLI beyond add-only, to reach *true* parity per the parity philosophy. | M | Ext strictly worse than CLI (none vs partial); both surfaces weak. Write a short spec first. | — (spec TBD) |
| **P2** | **Autofill registrable-domain matching:** replace exact `icon_hint` equality (`vault.ts` `findByHostname:344`) with `www.`-strip + eTLD+1 matching. | SM | `www.x.com``x.com` today; the one personal-side quality gap. | — |
| **P2** | **Search wire-up (hardening):** expose `searchItems` (`vault.ts:316`) via a `search_items` message, or formally adopt client-side filtering and remove the dangling helper. | S | Functionally at-parity, but an unwired helper is dead-code drift. | — |
| **P3** | **Org read in extension (Dev-D):** org context switcher + SW org handlers (unwrap org master key into a `Zeroizing` session handle) + grant-filtered manifest browse/read in popup + vault tab. | XL | Largest single gap; entire org feature is CLI-only in the extension. Specced, deferred. | — |
| **P3** | **Org offline read-only indicator:** "org offline — writes disabled" banner when the git remote is unreachable in org context. | S | Spec-mandated UX. | Org read |
| **P3** | **Org SW acceptance tests:** org context replaces personal manifest cleanly; org master key never in localStorage/IndexedDB; offline mode triggers on network error. | M | Spec-mandated coverage following the feature. | Org read |
| **P3** | **Org write in extension (Plan B-2):** org add/edit/rm including Card/Key/Document/Totp. | XL | Closes org write + item breadth. Deferred past v0.8.1. | Org read **and** CLI reaching Card/Key/Document/Totp org-write parity (v0.8.1) |
## Caveats
- Line citations are point-in-time against `b09e0ce` and drift with edits.
- This is a planning artifact, not a commitment; sizes are rough (S ≈ hours, M ≈ a day, L ≈ days, XL ≈ a multi-stream lift).
- Two analytical errors caught during cross-check and corrected here: (1) the struck `messages.ts`-doesn't-exist claim (file exists at `extension/src/shared/messages.ts`); (2) a few inventory line numbers were off by single digits and have been replaced with hand-verified ones.