diff --git a/.agents/skills/memory-context/memory-context b/.agents/skills/memory-context/memory-context
deleted file mode 120000
index 71eebe2a..00000000
--- a/.agents/skills/memory-context/memory-context
+++ /dev/null
@@ -1 +0,0 @@
-../../.agents/skills/memory-context
\ No newline at end of file
diff --git a/.agents/skills/test-runner/test-runner b/.agents/skills/test-runner/test-runner
deleted file mode 120000
index e120457f..00000000
--- a/.agents/skills/test-runner/test-runner
+++ /dev/null
@@ -1 +0,0 @@
-../../.agents/skills/test-runner
\ No newline at end of file
diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml
index bc406cc8..3184ef74 100644
--- a/.github/workflows/security-scan.yml
+++ b/.github/workflows/security-scan.yml
@@ -37,7 +37,6 @@ jobs:
- name: Run ShellCheck with Security Focus
uses: ludeeus/action-shellcheck@00cae500b08a931fb5698e11e79bfbd38e612a38 # 2.0.0
- continue-on-error: true
with:
severity: warning
format: tty
diff --git a/.github/workflows/version-propagation.yml b/.github/workflows/version-propagation.yml
index 63b6bc91..2225768e 100644
--- a/.github/workflows/version-propagation.yml
+++ b/.github/workflows/version-propagation.yml
@@ -1,13 +1,11 @@
---
name: Version Propagation
+# DISABLED: This workflow depends on VERSION, CHANGELOG.md, and
+# scripts/propagate-version.sh which are not present in the repository.
+# Re-enable when those artifacts are restored.
"on":
- push:
- paths:
- - VERSION
- branches:
- - main
- - 'feat/**'
+ workflow_dispatch: {}
permissions:
contents: write
diff --git a/plans/071-goap-codebase-gap-audit-2026-07-19.md b/plans/071-goap-codebase-gap-audit-2026-07-19.md
new file mode 100644
index 00000000..519f46c9
--- /dev/null
+++ b/plans/071-goap-codebase-gap-audit-2026-07-19.md
@@ -0,0 +1,505 @@
+# Plan 071 — Codebase Gap Audit and Remediation GOAP (2026-07-19)
+
+**Date**: 2026-07-19
+**Status**: OPEN
+**Method**: GOAP (Goal-Oriented Action Planning)
+**Scope**: implementation completeness, product opportunities, agent harness,
+skills, instructions, coding workflow, tests, and UI/UX
+**Constraint**: preserve the local-first Next.js baseline from ADR 018
+
+## Outcome
+
+Restore a trustworthy relationship between the product, its documentation, and
+its quality gates. The current application has a strong local-first foundation,
+but the audit found two data-integrity risks, an incomplete synchronization
+boundary, several misleading product claims, gaps in agent-harness enforcement,
+weak coverage thresholds, and responsive/accessibility work that is not fully
+verified.
+
+This plan replaces the statement in `plans/INDEX.md` that no open P0-P3 work
+remains. Current source is authoritative; checked boxes in older Vite/SQLite
+plans are historical evidence, not proof of behavior in the current Next.js app.
+
+## Evidence Baseline
+
+### Current architecture
+
+- Next.js 16 / React 19 single-shell application.
+- Zustand plus `localStorage` is canonical persistence.
+- Markdown is an interchange format, not canonical storage.
+- Yjs, IndexedDB, and WebRTC are optional collaboration infrastructure.
+- Search is lexical BM25, despite some UI and documentation calling it
+ semantic search.
+- OpenRouter and Ollama are available in AI Harness; the main Chat view uses a
+ local deterministic retrieval response.
+
+### Audit limitations
+
+- Source, tests, plans, scripts, workflow files, and existing generated reports
+ were inspected.
+- The prior UI audit in `plans/ui-ux-audit-2026-07-11.md` was source-only and is
+ partially stale: keyboard semantics, dialog semantics, graph export/history,
+ and editor focus treatment have since improved.
+- The retry audit ran headless Chromium at 390×844, 768×1024, 1024×768, and
+ 1440×900. No document-level horizontal overflow was observed at those widths.
+- The retry audit ran fresh lint, typecheck, coverage, and Playwright commands.
+ Existing generated reports outside those runs remain untrusted.
+- No screen-reader, automated axe, 200% text zoom, or 400% reflow pass was
+ completed. Those remain G5 verification requirements.
+
+### Fresh retry baseline
+
+| Check | Result | Interpretation |
+|---|---|---|
+| `pnpm run lint` | Passed | No lint output warnings |
+| `pnpm run typecheck` | Passed | No type errors |
+| `pnpm run test:coverage` | 254/254 passed | 26.44% lines, 16.66% branches, 19.14% functions, 25.86% statements |
+| `pnpm run test:e2e` | 57/58 passed | One strict-locator failure in `search-and-filter.spec.ts` |
+| Browser viewport checks | 4 viewports inspected | No page overflow; target-size and compact-text issues reproduced |
+
+The coverage run emitted React, Yjs, and Vitest warnings while still passing.
+This directly confirms that the documented warnings-as-errors policy is not
+implemented by the current gates.
+
+## Goal Graph
+
+```text
+G1 Data integrity ───────┬──> G3 Honest product surface ──> G5 UI/UX verification
+ │
+G2 Sync correctness ────┘
+
+G4 Harness integrity ───────> G6 Reliable quality gates ──> G7 Documentation truth
+```
+
+G1 and G2 are the release blockers. G4 and G6 can run in parallel with product
+work. G3 must precede final UI copy and documentation updates so the interface
+does not continue to promise behavior that is absent.
+
+## Prioritized Findings
+
+### P0 — Data integrity and false-success behavior
+
+#### F1. Collaboration is not connected to canonical state
+
+`src/components/studio/views/sync-view.tsx` writes a Zustand snapshot into Yjs
+when joining, but no production lifecycle subscribes Yjs changes back into the
+store. Store CRUD does not propagate ongoing updates or deletions to Yjs.
+Conflict resolution accepts a resolution map but clears conflicts without
+applying the selected values. Existing bridge tests cover isolated map helpers,
+not two-way canonical-state synchronization.
+
+**Risk**: peers can report successful synchronization while remote edits are not
+visible or durable in the canonical library.
+
+**Decision**: ADR 027.
+
+#### F2. Import can replace valid data with a partially accepted payload
+
+`src/components/studio/views/export-helpers.ts` uses shallow record guards and
+silently filters invalid records even though complete Zod schemas exist in
+`src/lib/studio/schema.ts`. `export-view.tsx` then replaces the canonical
+library without a preview, backup, or rollback path.
+
+**Risk**: malformed, future-version, or partially valid archives can cause
+silent record loss followed by destructive replacement.
+
+**Decision**: ADR 028.
+
+### P1 — Durability, workflow, and product-trust gaps
+
+#### F3. Persisted hydration trusts unknown runtime data
+
+The Zustand persist migration in `src/lib/studio/store.ts` is a no-op type cast.
+Corrupt or old `localStorage` content is not validated at the boundary.
+
+**Remediation**: implement ADR 028 for both import and hydration.
+
+#### F4. Entity deletion leaves incoming links behind
+
+`deleteEntity` removes the entity and claims but does not remove links in other
+entities whose `targetId` references the deleted entity.
+
+**Remediation**: make deletion one atomic history transaction and test incoming
+and outgoing relationships.
+
+#### F5. API-key ciphertext outlives its session encryption key
+
+`src/lib/studio/ai-settings.ts` persists encrypted settings while keeping the
+encryption key in `sessionStorage`. After the session, a newly generated key
+cannot decrypt the saved ciphertext.
+
+**Remediation**: choose and document one honest retention model: session-only
+credential storage, or passphrase-derived persistent encryption. Do not imply
+persistence that cannot survive restart.
+
+#### F6. Search and Chat labels overstate implementation
+
+The retrieval engine is BM25, not embedding/vector semantic search. Desktop
+labels it “Semantic”; mobile stores the selected mode but always renders the
+filtered entity list. Chat presents a fixed local retrieval response while the
+copy invites AI synthesis. The actual provider-backed path is AI Harness.
+
+**Remediation**: first rename modes to Keyword and Ranked/Relevance, wire the
+same ranking on mobile, and label Chat as local library Q&A. Treat local
+embeddings and provider-backed synthesis as separate opt-in features.
+
+#### F7. Agent-surface validation can pass broken skill artifacts
+
+Two tracked skill symlinks are broken:
+
+- `.agents/skills/memory-context/memory-context`
+- `.agents/skills/test-runner/test-runner`
+
+`scripts/agent-surface.py` only checks minimal `SKILL.md` content. It does not
+validate symlinks, frontmatter, directory/name agreement, eval schemas, or the
+declared surfaces in `.agents/manifest.json`. `validate-skills.sh` therefore
+returns success for an invalid surface.
+
+**Decision**: ADR 029.
+
+#### F8. The documented release workflow references absent artifacts
+
+`AGENTS.md` and `.github/workflows/version-propagation.yml` describe a VERSION-
+driven release flow, but `VERSION`, `CHANGELOG.md`, and
+`scripts/propagate-version.sh` are absent.
+
+**Remediation**: either restore and test the complete automated mechanism or
+retire the obsolete claim and workflow. Do not add a manual release path.
+
+#### F9. Security and warning policies are not enforced as documented
+
+The security workflow marks key scanners non-blocking or uses a zero exit code,
+then summarizes job success rather than finding counts. Quality scripts inspect
+command exit status but do not enforce the root rule that warnings are errors;
+`MAX_ALLOWED_WARNINGS` is unused.
+
+**Remediation**: preserve artifact upload under `if: always()`, but allow the
+designated scanners to fail. Capture quality-command output and enforce a small,
+explicit warning policy.
+
+### P2 — Tests, accessibility, responsive behavior, and completeness
+
+#### F10. Coverage thresholds are too low to protect critical workflows
+
+`vitest.config.ts` requires only 15% lines/statements/functions and 10% branches.
+There are focused tests for store, export helpers, retrieval, provider adapters,
+encryption, drafts, and isolated sync helpers, but no end-to-end integration
+proof for canonical Yjs synchronization, conflict application, deletion
+tombstones, safe import rollback, or malformed persisted-state recovery.
+
+Fresh coverage was 26.44% lines, 16.66% branches, 19.14% functions, and 25.86%
+statements. Store and export helpers have useful focused coverage, but studio UI
+is 6.71% by lines and important views remain at 0% unit coverage.
+
+The Playwright suite contains 58 tests across eight spec files but configures
+only one Desktop Chrome project, uses no retries, and does not run in the
+inspected CI workflow. Fresh execution produced 57 passes and one failure; with
+`trace: 'on-first-retry'` and retries disabled, the failure produced no useful
+trace. Nine conditional branches can skip intended assertions, and several
+responsive, keyboard, validation, and search tests assert only the page title or
+another unrelated element. The accessibility suite checks selected roles and
+labels but does not run an automated WCAG engine or verify screen-reader
+behavior.
+
+#### F11. Touch targets and compact functional text remain inconsistent
+
+Current source still contains multiple `h-8`, `h-9`, `h-10`, 9-11px functional
+labels, and custom controls below the repository’s 44px interactive-target rule.
+Examples occur in topbar, mobile drawer, voice controls, chat, AI Harness,
+presence, conflict resolution, and shared primitives.
+
+Browser measurement reproduced the problem. At 390px, all 17 measured Graph
+controls and all 17 measured Mind Map controls were below 44px on at least one
+axis; Chat was 24/24. At 1440px, Graph and Mind Map were each 23/23. The mobile
+menu/search controls measured 36×36px. Shared primitives currently encode this
+constraint with `h-9 w-9` defaults.
+
+#### F12. Tablet and mobile behavior is weakly specified and weakly tested
+
+The shell, drawer, and responsive breakpoints are improved, but tests do not
+prove overflow, content priority, target size, focus order, 200% text zoom, or
+orientation changes. Playwright has only a Desktop Chrome project; viewport
+tests are manual resize scenarios rather than device projects.
+
+Fresh browser checks found no document-level overflow at 390, 768, 1024, or
+1440px. The earlier three-pane-at-1024px finding is stale: the right panel now
+waits for the 1100px `wide` breakpoint, leaving a measured 776px main area at
+1024px. Remaining concerns are dense topbar controls, aggressive mobile text
+truncation, coarse-pointer targets, and the absence of device, touch,
+orientation, zoom, and reflow assertions.
+
+#### F13. Product completeness claims remain stale
+
+Examples:
+
+- The offline indicator promises later synchronization, but no durable queue is
+ implemented.
+- AI Harness “Re-sync” emits success although prompt context is already rebuilt
+ directly from current arrays.
+- Claims can be created but do not have a complete edit/delete/provenance flow.
+- “Graph snapshot” stores one view-state bookmark, not data revisions or diffing.
+- README uses obsolete package-manager/provider/architecture information.
+- `plans/GOAL.md` and `plans/PHASES.md` retain retired SQLite, OPFS, Orama,
+ Tiptap, CLI, and embeddings claims as current completion.
+
+#### F14. Overlay and composite-widget accessibility is incomplete
+
+The mobile drawer, command palette, shortcuts dialog, editor fields, library
+rows, and graph nodes have materially improved semantics since the prior audit.
+Two current gaps remain:
+
+- the encrypted export overlay lacks complete dialog semantics, initial focus,
+ Escape handling, focus containment/restoration, and explicit label/input
+ relationships;
+- the mind map gives every node `tabIndex={0}` without a containing ARIA tree,
+ roving tabindex, or complete Up/Down/Home/End tree navigation.
+
+**Remediation**: move export overlays to one tested dialog primitive and
+implement the standard ARIA tree keyboard model for the mind map.
+
+### P3 — Maintainability and polish
+
+- Graph and mind-map disabled history buttons say “coming soon” even when the
+ actual condition is only “nothing to undo/redo.”
+- Agent documentation references scripts and commands that do not exist.
+- Skill setup supports fewer client surfaces than the manifest and docs claim.
+- Project-specific skills conflict with current architecture: for example,
+ local-chat guidance requires SQLite although the canonical store is Zustand.
+- Skill families overlap significantly, increasing routing ambiguity.
+- The root instruction file is substantially longer than its own progressive-
+ disclosure guidance.
+
+## Coverage Map
+
+| Subsystem | Current evidence | Important missing proof |
+|---|---|---|
+| Store CRUD/history | Strong unit coverage | dangling-link cleanup; invalid hydration |
+| Persistence | Basic localStorage integration | migrations, corruption recovery, rollback |
+| Export | Strong helper tests | strict import preview/replace/rollback E2E |
+| Search | BM25 unit tests | desktop/mobile parity; large-library budgets |
+| AI providers | adapter/context/research tests; providers only 10% lines | credential restart semantics; UI failure paths |
+| Chat | store timer/response tests | honest-mode UI and optional provider integration |
+| Graph/mind map | graph index and basic E2E | large graph, keyboard model, snapshot semantics |
+| Sync | isolated bridge/merge/type tests; 38.5% lines | two-peer lifecycle, deletes, conflict choices |
+| PWA/offline | indicator unit test | navigation fallback and reconnect behavior |
+| Accessibility | selected semantic E2E tests | axe, screen reader, zoom/reflow, target sizes |
+| Responsive UI | five scenarios among 58 Desktop Chrome tests | non-conditional assertions and device coverage |
+| Agent harness | shell validators | manifest conformance and executable evals |
+| CI/security | unit/build/coverage jobs; no E2E job | fail-closed findings, E2E, warnings-as-errors |
+
+## Verified Improvements Since the Prior UI Audit
+
+These improvements should be preserved and must not be reopened as current
+defects without new evidence:
+
+- Library list rows expose keyboard-operable link semantics and names.
+- Graph nodes expose role, name, focus, and Enter/Space handling.
+- Editor name/description/content fields have persistent labels and visible
+ focus treatment; editor modes expose radio state.
+- Graph positions are deterministic, edge lookup is indexed, and PNG export plus
+ store-backed undo/redo are implemented.
+- The right panel waits for the 1100px breakpoint instead of crowding 1024px.
+- Light-theme faint text contrast was improved from the value recorded in the
+ 2026-07-11 audit.
+- Home now puts recent work before compact statistics rather than leading with
+ four oversized metric cards.
+
+Remaining UI work is therefore concentrated in touch sizing, functional text
+size, honest product language, encrypted-export dialogs, mind-map tree behavior,
+view-specific loading feedback, and fresh WCAG verification.
+
+## GOAP Actions
+
+### G1 — Protect canonical local data
+
+**Priority**: P0
+**Preconditions**: ADR 028 accepted
+
+| ID | Action | Verification |
+|---|---|---|
+| G1.1 | Validate persisted state with a versioned Zod schema | malformed and old-version hydration tests |
+| G1.2 | Replace shallow import guards with strict payload parsing | invalid field, reference, and version tests |
+| G1.3 | Add import preview, explicit replace confirmation, and rollback snapshot | export/import/rollback E2E |
+| G1.4 | Remove dangling links during entity deletion | atomic history and persistence tests |
+| G1.5 | Decide and implement credential retention semantics | reload and new-session tests |
+
+**Exit criteria**:
+
+- [ ] Invalid imports cannot modify canonical state.
+- [ ] A user can recover the exact pre-import state after replacement failure.
+- [ ] Invalid persisted data is rejected or recovered without a crash.
+- [ ] Entity deletion leaves no dangling claims or links.
+
+### G2 — Complete synchronization semantics
+
+**Priority**: P0
+**Preconditions**: ADR 027 accepted; G1 validation boundary available
+
+| ID | Action | Verification |
+|---|---|---|
+| G2.1 | Add one lifecycle-owned bidirectional Zustand/Yjs bridge | two independent store/doc integration test |
+| G2.2 | Apply manual conflict selections before dismissal | conflict field-value assertions |
+| G2.3 | Add deletion tombstones or equivalent versioned delete semantics | edit-versus-delete convergence tests |
+| G2.4 | Validate all Yjs data before canonical import | malformed peer update test |
+| G2.5 | Correct offline and sync status copy | UI assertions for connected/offline/retry states |
+
+**Exit criteria**:
+
+- [ ] Create, update, and delete converge in both directions.
+- [ ] Reload preserves the converged state.
+- [ ] Manual resolution changes the chosen canonical fields.
+- [ ] No sync UI reports success before canonical commit succeeds.
+
+### G3 — Make the product surface honest
+
+**Priority**: P1
+**Preconditions**: G1/G2 behavior contracts known
+
+| ID | Action | Verification |
+|---|---|---|
+| G3.1 | Rename lexical search and implement desktop/mobile parity | shared result-order tests |
+| G3.2 | Relabel Chat as local Q&A or add explicit provider opt-in | mode-specific UI/integration tests |
+| G3.3 | Remove no-op “Re-sync” and false-success controls | no inert primary controls in source/browser audit |
+| G3.4 | Rename graph view bookmark or implement true revisions/diff | semantics-specific tests |
+| G3.5 | Complete claim update/delete/provenance workflow | CRUD persistence tests |
+
+### G4 — Make the agent harness executable
+
+**Priority**: P1
+**Preconditions**: ADR 029 accepted
+
+| ID | Action | Verification |
+|---|---|---|
+| G4.1 | Repair broken tracked skill symlinks | `find .agents -xtype l` returns none |
+| G4.2 | Make validation manifest-driven | fixture tests for every invalid condition |
+| G4.3 | Validate frontmatter, eval JSON, links, and target surfaces | validation fails on seeded defects |
+| G4.4 | Align setup behavior with declared clients | clean checkout setup/validate test |
+| G4.5 | Remove or repair dead commands in agent docs | repository-relative command-link check |
+| G4.6 | Align project-specific skills with Zustand/localStorage | skill evals reject SQLite/backend guidance |
+
+### G5 — Verify and harden UI/UX
+
+**Priority**: P2
+**Preconditions**: G3 labels and controls settled
+
+| ID | Action | Verification |
+|---|---|---|
+| G5.1 | Enforce 44px coarse-pointer targets in shared controls and outliers | browser target-size audit |
+| G5.2 | Establish readable minimum type sizes and contrast-safe text tokens | both-theme contrast report |
+| G5.3 | Move encrypted export/reset/delete overlays to one complete dialog contract | keyboard and focus-restoration E2E |
+| G5.4 | Validate mobile/tablet/desktop information priority and overflow | 320, 390, 768, 1024, 1280, 1440px evidence |
+| G5.5 | Validate keyboard, screen reader, 200% text zoom, and 400% reflow | WCAG 2.2 AA audit |
+| G5.6 | Profile editor typing and 1,000-node graph interaction | recorded performance budgets |
+| G5.7 | Implement ARIA tree semantics and roving focus for mind map | complete tree keyboard-model E2E |
+| G5.8 | Add accessible graph list and larger node hit regions | mobile keyboard/touch E2E |
+
+### G6 — Turn quality policy into enforceable gates
+
+**Priority**: P1
+**Preconditions**: G2/G4 tests define critical paths
+
+| ID | Action | Verification |
+|---|---|---|
+| G6.1 | Add Playwright to CI and retain traces on failure | required CI job |
+| G6.2 | Fix the current 57/58 Playwright baseline and replace conditional/no-op assertions | mutation or deliberate-break proof |
+| G6.3 | Add axe scanning plus manual-audit checklist | accessibility artifact |
+| G6.4 | Raise coverage incrementally around critical modules | per-module targets before global increase |
+| G6.5 | Fail on React/Yjs/tool warnings and make security policy fail closed | seeded warning/finding workflow test |
+| G6.6 | Validate staged/PR diffs explicitly | staged, untracked, base/head fixture tests |
+
+Suggested coverage progression after critical tests land:
+
+1. Sync, import, store, persistence, and export modules: 80% lines/branches.
+2. AI/search and core UI state modules: 70% lines/60% branches.
+3. Global floor: raise from 15/10 to 50% lines/functions/statements and 40%
+ branches, then ratchet without lowering.
+
+### G7 — Reconcile instructions, plans, and product docs
+
+**Priority**: P2
+**Preconditions**: G1-G6 behavior and gates complete
+
+| ID | Action | Verification |
+|---|---|---|
+| G7.1 | Replace historical “all complete” claims with a current feature matrix | matrix links to source/tests |
+| G7.2 | Mark retired Vite/SQLite plans as historical | no current architecture ambiguity |
+| G7.3 | Repair pnpm, provider, view-count, persistence, and release docs | documentation command check |
+| G7.4 | Reduce root instructions via progressive disclosure | canonical rules remain discoverable |
+| G7.5 | Re-run code, harness, test, and browser audits | closeout report with fresh evidence |
+
+## New Feature Opportunities
+
+These are not defect remediation and should start only after G1-G3:
+
+1. Safe import merge-by-ID with duplicate/conflict preview.
+2. Local hybrid BM25/vector retrieval with an optional downloadable model.
+3. Explicit provider-backed Chat mode with local retrieval citations.
+4. Named graph-data revisions with restore and diff.
+5. Mind-map reparenting, ordering, drag/drop, and cycle prevention.
+6. Markdown and encrypted-archive restore paths.
+7. Durable offline operation queue after sync semantics are complete.
+
+## Execution Waves
+
+### Wave 0 — Decisions and truth (parallel)
+
+- Accept or revise ADRs 027-029.
+- Correct only the highest-risk false-success copy while fixes are in flight.
+- Establish fresh test, coverage, browser, and harness baselines.
+
+### Wave 1 — Data safety and synchronization
+
+- Execute G1 and G2 sequentially at the canonical-state boundary.
+- Do not add sync queueing or new collaboration UX before convergence tests pass.
+
+### Wave 2 — Harness and CI integrity (parallel with Wave 1)
+
+- Execute G4, then G6.
+- A green validator must mean the declared artifact is actually usable.
+
+### Wave 3 — Product and UI trust
+
+- Execute G3, then G5.
+- Browser-check every changed workflow at mobile, tablet, and desktop sizes.
+
+### Wave 4 — Documentation closeout
+
+- Execute G7 only from verified current behavior.
+- Update `plans/INDEX.md`, `plans/PHASES.md`, README, and root instructions without
+ copying unchecked historical claims.
+
+## Global Quality Gates
+
+For each implementation wave:
+
+```bash
+pnpm run lint
+pnpm run typecheck
+pnpm run test
+pnpm run test:coverage
+pnpm run build
+pnpm run test:e2e
+./scripts/quality_gate.sh
+```
+
+Additionally:
+
+- Run agent-surface validation with negative fixtures after G4.
+- Run a structured code review after CI passes and before merge.
+- Treat warnings and skipped/conditional critical assertions as failures.
+- Record any unavoidable blocker as a new scoped plan; do not mark this plan
+ complete based only on source implementation.
+
+## Completion Criteria
+
+- [ ] All P0 findings are resolved with regression tests.
+- [ ] All P1 findings are resolved or superseded by an accepted ADR.
+- [ ] Critical local data paths reject invalid external state.
+- [ ] Sync converges for create, update, delete, reload, and manual conflicts.
+- [ ] Skill/harness validation fails on broken symlinks and invalid manifests.
+- [ ] Security and warning gates cannot summarize known findings as success.
+- [ ] Playwright runs in CI with meaningful mobile/tablet/desktop assertions.
+- [ ] Fresh accessibility evidence covers keyboard, semantics, contrast, zoom,
+ reflow, and target size.
+- [ ] Product copy and plans describe only behavior verified in current source.
+- [ ] `plans/INDEX.md` is updated with measured, fresh closeout metrics.
diff --git a/plans/072-goap-plan071-remediation.md b/plans/072-goap-plan071-remediation.md
new file mode 100644
index 00000000..8e6fa1e2
--- /dev/null
+++ b/plans/072-goap-plan071-remediation.md
@@ -0,0 +1,529 @@
+# Plan 072 — GOAP Remediation of Plan 071 Findings
+
+**Date**: 2026-07-24
+**Status**: OPEN
+**Method**: GOAP with swarm parallel execution
+**Source**: Plan 071 (071-goap-codebase-gap-audit-2026-07-19.md)
+**Constraint**: preserve the local-first Next.js baseline from ADR 018
+
+## 1. Objective
+
+Remediate all 14 findings from plan 071 across P0–P3, accepting three proposed
+ADRs (027, 028, 029) and restoring a trustworthy relationship between product
+behavior, documentation, and quality gates.
+
+## 2. Goal Graph
+
+```text
+G1 Data integrity (P0) ──┬──> G3 Honest product surface (P1) ──> G5 UI/UX verification (P2)
+ │
+G2 Sync correctness (P0) ┘
+
+G4 Harness integrity (P1) ──> G6 Reliable quality gates (P1) ──> G7 Documentation truth (P2)
+```
+
+## 3. Wave Structure
+
+### Wave 0 — Decisions and Baseline (parallel, no dependencies)
+
+**Goal**: Accept ADRs, fix broken symlinks, establish fresh baselines.
+
+| ID | Action | Finding | Agent | Files |
+|----|--------|---------|-------|-------|
+| W0.1 | Accept ADR 027 | F1 | plan-agent | `plans/ADRs/027-*.md` (status → Accepted) |
+| W0.2 | Accept ADR 028 | F2, F3 | plan-agent | `plans/ADRs/028-*.md` (status → Accepted) |
+| W0.3 | Accept ADR 029 | F7 | plan-agent | `plans/ADRs/029-*.md` (status → Accepted) |
+| W0.4 | Fix broken skill symlinks | F7 | fix-agent | `.agents/skills/memory-context/memory-context`, `.agents/skills/test-runner/test-runner` |
+| W0.5 | Baseline: run full quality suite | — | baseline-agent | Generate `/tmp/baseline-072.txt` |
+| W0.6 | Baseline: coverage snapshot | F10 | baseline-agent | Record current thresholds from `vitest.config.ts` |
+
+**Parallelization**: All W0 tasks are independent. Spawn 3 agents:
+- Agent A: W0.1 + W0.2 + W0.3 (ADR acceptance, sequential file edits)
+- Agent B: W0.4 (symlink repair)
+- Agent C: W0.5 + W0.6 (baseline capture)
+
+**Quality Gate 0**:
+- `find .agents -xtype l` returns empty
+- ADR status fields read "Accepted"
+- Baseline file exists with lint/typecheck/test/build/e2e results
+
+---
+
+### Wave 1 — Data Integrity (P0, sequential within goal, parallel across goals)
+
+**Preconditions**: ADR 028 accepted (W0.2)
+
+#### G1 — Protect Canonical Local Data
+
+| ID | Action | Finding | Agent | Files |
+|----|--------|---------|-------|-------|
+| G1.1 | Define persisted-state envelope schema with version field | F3 | schema-agent | `src/lib/studio/schema.ts` |
+| G1.2 | Replace no-op `migrate` with versioned Zod validation in store hydration | F3 | store-agent | `src/lib/studio/store.ts` (lines 286–290) |
+| G1.3 | Replace shallow `isEntity`/`isClaim` guards with Zod parsing in import | F2 | import-agent | `src/components/studio/views/export-helpers.ts` (lines 434–471) |
+| G1.4 | Add import preview step: show entity/claim counts, version, conflicts | F2 | import-agent | `src/components/studio/views/export-view.tsx` |
+| G1.5 | Add pre-import snapshot + atomic replacement + rollback action | F2 | import-agent | `src/lib/studio/store.ts` (new `importWithRollback` action) |
+| G1.6 | Fix `deleteEntity` to remove incoming links from other entities | F4 | store-agent | `src/lib/studio/store.ts` (lines 189–197) |
+| G1.7 | Implement credential retention: session-only storage, honest UI labeling | F5 | ai-agent | `src/lib/studio/ai-settings.ts` |
+
+**Dependency chain**: G1.1 → G1.2 → G1.3 → G1.4 → G1.5 (sequential, schema must exist before consumers)
+**Parallel**: G1.6 and G1.7 are independent of G1.1–G1.5
+
+**Agent assignments**:
+- **schema-agent** (general): G1.1 — extend `src/lib/studio/schema.ts` with `PersistedEnvelopeSchema`
+- **store-agent** (general): G1.2, G1.6 — modify `src/lib/studio/store.ts` hydration and deletion
+- **import-agent** (general): G1.3, G1.4, G1.5 — rewrite import path in export-helpers + export-view
+- **ai-agent** (general): G1.7 — fix credential lifecycle in ai-settings.ts
+
+**Spawn plan**:
+```
+Phase 1a (parallel):
+ - schema-agent → G1.1
+ - store-agent → G1.6 (independent)
+ - ai-agent → G1.7 (independent)
+
+Phase 1b (after G1.1 completes):
+ - store-agent → G1.2 (needs schema)
+ - import-agent → G1.3 → G1.4 → G1.5 (sequential chain)
+```
+
+**Test requirements**:
+- `src/lib/studio/schema.test.ts`: envelope validation, version rejection, migration chain
+- `src/lib/studio/store.test.ts`: dangling-link cleanup, invalid hydration rejection, rollback
+- `src/components/studio/views/export-helpers.test.ts`: Zod-based import validation, invalid field/referential integrity rejection
+- `src/components/studio/views/export-view.test.tsx`: preview counts, rollback after failure
+
+**Quality Gate 1**:
+```bash
+pnpm run lint
+pnpm run typecheck
+pnpm run test
+pnpm run build
+```
+- Invalid imports leave canonical state unchanged (test proof)
+- Entity deletion leaves no dangling links (test proof)
+- Invalid localStorage hydration rejects or recovers (test proof)
+
+---
+
+### Wave 2 — Harness and CI Integrity (parallel with Wave 1)
+
+**Preconditions**: ADR 029 accepted (W0.3)
+
+| ID | Action | Finding | Agent | Files |
+|----|--------|---------|-------|-------|
+| G4.1 | Make validation manifest-driven: read `.agents/manifest.json` | F7 | harness-agent | `scripts/agent-surface.py` |
+| G4.2 | Add symlink, frontmatter, name/directory, eval JSON validation | F7 | harness-agent | `scripts/agent-surface.py`, `scripts/validate-skills.sh` |
+| G4.3 | Add negative fixture tests for each failure condition | F7 | harness-test-agent | `tests/agent-surface.test.ts` or `scripts/test-agent-surface.sh` |
+| G4.4 | Remove or repair dead commands in agent docs | F7 | docs-agent | `agents-docs/` files |
+| G4.5 | Retire or restore version-propagation workflow | F8 | ci-agent | `.github/workflows/version-propagation.yml`, `VERSION`, `CHANGELOG.md`, `scripts/propagate-version.sh` |
+| G4.6 | Make security scanners fail-closed | F9 | ci-agent | `.github/workflows/security.yml` |
+| G4.7 | Enforce warnings-as-errors in quality scripts | F9 | ci-agent | `scripts/quality_gate.sh` |
+
+**Dependency chain**: G4.1 → G4.2 → G4.3 (sequential)
+**Parallel**: G4.4, G4.5, G4.6, G4.7 are independent
+
+**Agent assignments**:
+- **harness-agent** (general): G4.1, G4.2 — rewrite validation in agent-surface.py
+- **harness-test-agent** (general): G4.3 — create negative fixtures
+- **docs-agent** (general): G4.4 — audit and repair agent-docs
+- **ci-agent** (general): G4.5, G4.6, G4.7 — fix workflows and quality scripts
+
+**Spawn plan**:
+```
+Phase 2a (parallel):
+ - harness-agent → G4.1 → G4.2 (sequential)
+ - docs-agent → G4.4
+ - ci-agent → G4.5 + G4.6 + G4.7 (sequential within)
+
+Phase 2b (after G4.2 completes):
+ - harness-test-agent → G4.3
+```
+
+**Test requirements**:
+- Negative fixtures: broken symlink, missing frontmatter, name mismatch, malformed eval, missing target surface
+- `validate-skills.sh` returns non-zero on seeded defects
+- Quality gate script captures output and enforces warning count
+- Security workflow fails when scanners find issues
+
+**Quality Gate 2**:
+```bash
+./scripts/validate-skills.sh # must pass with clean surface
+pnpm run lint
+pnpm run typecheck
+pnpm run test
+pnpm run build
+```
+- `find .agents -xtype l` returns empty
+- Agent-surface validation fails on introduced broken symlink (negative test)
+
+---
+
+### Wave 3 — Honest Product Surface (P1, after Wave 1)
+
+**Preconditions**: G1 validation boundary available (Wave 1 complete)
+
+| ID | Action | Finding | Agent | Files |
+|----|--------|---------|-------|-------|
+| G3.1 | Rename "Semantic" search to "Keyword" / "Ranked" | F6 | label-agent | `src/components/studio/views/search-view.tsx`, related UI |
+| G3.2 | Wire mobile search to use same BM25 ranking as desktop | F6 | search-agent | `src/components/studio/views/search-view.tsx` |
+| G3.3 | Relabel Chat as "Local Library Q&A" | F6 | label-agent | `src/components/studio/views/chat-view.tsx` |
+| G3.4 | Remove no-op "Re-sync" control from AI Harness | F13 | cleanup-agent | `src/components/studio/views/ai-harness-view.tsx` |
+| G3.5 | Rename "Graph Snapshot" to "View Bookmark" or implement revisions | F13 | label-agent | graph-related views |
+| G3.6 | Complete claim edit/delete/provenance workflow | F13 | claim-agent | `src/lib/studio/store.ts`, editor views |
+| G3.7 | Fix offline indicator: remove sync promise or implement queue | F13 | cleanup-agent | PWA/offline components |
+
+**Parallelization**: G3.1, G3.2, G3.3 are independent label/search fixes. G3.4, G3.5, G3.7 are independent cleanup. G3.6 is the largest task.
+
+**Agent assignments**:
+- **label-agent** (general): G3.1, G3.3, G3.5 — rename labels across views
+- **search-agent** (general): G3.2 — wire mobile BM25 ranking
+- **cleanup-agent** (general): G3.4, G3.7 — remove false-success controls
+- **claim-agent** (general): G3.6 — implement claim CRUD
+
+**Spawn plan**:
+```
+Phase 3a (parallel):
+ - label-agent → G3.1 + G3.3 + G3.5
+ - search-agent → G3.2
+ - cleanup-agent → G3.4 + G3.7
+ - claim-agent → G3.6
+```
+
+**Test requirements**:
+- Search mode labels match implementation in both desktop and mobile
+- Chat view shows "Local Library Q&A" or similar honest label
+- No inert "Re-sync" button renders
+- Claim CRUD: create, update, delete persist correctly
+
+**Quality Gate 3**:
+```bash
+pnpm run lint
+pnpm run typecheck
+pnpm run test
+pnpm run build
+pnpm run test:e2e
+```
+
+---
+
+### Wave 4 — UI/UX Hardening (P2, after Wave 3)
+
+**Preconditions**: G3 labels and controls settled
+
+| ID | Action | Finding | Agent | Files |
+|----|--------|---------|-------|-------|
+| G5.1 | Enforce 44px minimum interactive targets | F11 | touch-agent | Shared primitives, topbar, drawer, chat, graph, mindmap controls |
+| G5.2 | Establish readable minimum type sizes (≥12px functional text) | F11 | type-agent | CSS tokens, component overrides |
+| G5.3 | Move encrypted export overlay to complete dialog primitive | F14 | dialog-agent | `src/components/studio/views/export-view.tsx`, `src/lib/export/encrypt.ts` |
+| G5.4 | Implement ARIA tree + roving tabindex for mind map | F14 | a11y-agent | Mind map component |
+| G5.5 | Add Playwright device projects (mobile, tablet) | F12 | test-agent | `playwright.config.ts` |
+| G5.6 | Fix conditional/no-op assertions in E2E tests | F10 | test-agent | `e2e/*.spec.ts` |
+| G5.7 | Raise coverage thresholds incrementally | F10 | test-agent | `vitest.config.ts` |
+
+**Parallelization**: G5.1–G5.4 are independent UI tasks. G5.5–G5.7 are test infrastructure.
+
+**Agent assignments**:
+- **touch-agent** (general): G5.1 — audit and fix all controls below 44px
+- **type-agent** (general): G5.2 — fix functional text below 12px
+- **dialog-agent** (general): G5.3 — build accessible dialog for export overlays
+- **a11y-agent** (general): G5.4 — implement ARIA tree model for mind map
+- **test-agent** (general): G5.5, G5.6, G5.7 — fix E2E infrastructure and coverage
+
+**Spawn plan**:
+```
+Phase 4a (parallel):
+ - touch-agent → G5.1
+ - type-agent → G5.2
+ - dialog-agent → G5.3
+ - a11y-agent → G5.4
+ - test-agent → G5.5 + G5.6 + G5.7
+```
+
+**Test requirements**:
+- Browser audit: no interactive control below 44px at 390px, 768px, 1440px
+- No functional text below 12px in either theme
+- Encrypted export dialog: focus trap, Escape, label/input relationships
+- Mind map: Up/Down/Home/End keyboard navigation within ARIA tree
+- Playwright: mobile (390×844) and tablet (768×1024) projects exist and pass
+- Coverage thresholds: lines ≥ 30%, statements ≥ 30%, functions ≥ 25%, branches ≥ 20%
+
+**Quality Gate 4**:
+```bash
+pnpm run lint
+pnpm run typecheck
+pnpm run test
+pnpm run test:coverage
+pnpm run build
+pnpm run test:e2e
+```
+
+---
+
+### Wave 5 — Documentation Truth and Closeout (P2, after Wave 4)
+
+**Preconditions**: G1–G6 behavior and gates complete
+
+| ID | Action | Finding | Agent | Files |
+|----|--------|---------|-------|-------|
+| G7.1 | Replace "all complete" claims in INDEX.md with current feature matrix | F13 | docs-agent | `plans/INDEX.md` |
+| G7.2 | Mark retired Vite/SQLite plans as historical | F13 | docs-agent | `plans/PHASES.md`, `plans/GOAL.md` |
+| G7.3 | Repair README: pnpm, provider, persistence, architecture docs | F13 | docs-agent | `README.md` |
+| G7.4 | Update agent docs: remove references to nonexistent scripts | F7 | docs-agent | `agents-docs/` |
+| G7.5 | Run closeout audit: fresh evidence for all verification criteria | — | audit-agent | `plans/072-closeout-report.md` |
+
+**Agent assignments**:
+- **docs-agent** (general): G7.1–G7.4 — documentation reconciliation
+- **audit-agent** (general): G7.5 — final verification
+
+**Quality Gate 5 (Final)**:
+```bash
+pnpm run lint
+pnpm run typecheck
+pnpm run test
+pnpm run test:coverage
+pnpm run build
+pnpm run test:e2e
+./scripts/quality_gate.sh
+./scripts/validate-skills.sh
+```
+
+---
+
+## 4. Wave Dependency Graph
+
+```text
+Wave 0 (parallel) ──> Wave 1 (P0 data) ──> Wave 3 (P1 labels) ──> Wave 4 (P2 UI) ──> Wave 5 (docs)
+ │ │
+ └──────────────────> Wave 2 (P1 harness) ──────────────────────────────────────┘
+```
+
+- Wave 0: All tasks parallel
+- Wave 1 and Wave 2: Parallel with each other (no cross-dependency)
+- Wave 3: Depends on Wave 1 (needs validation boundary)
+- Wave 4: Depends on Wave 3 (needs settled labels)
+- Wave 5: Depends on Wave 4 (needs all behavior finalized)
+- Wave 2 feeds into Wave 5 (harness must be complete before closeout)
+
+## 5. Agent Swarm Summary
+
+| Wave | Agents | Max Parallel | Estimated Effort |
+|------|--------|-------------|------------------|
+| W0 | 3 | 3 | 30min |
+| W1 | 4 | 3 (phase 1a), 2 (phase 1b) | 4–6h |
+| W2 | 4 | 3 (phase 2a), 1 (phase 2b) | 3–4h |
+| W3 | 4 | 4 | 2–3h |
+| W4 | 5 | 5 | 4–6h |
+| W5 | 2 | 2 | 1–2h |
+| **Total** | — | — | **15–22h** |
+
+## 6. PR Strategy
+
+**Single PR** is preferred because:
+- Findings are interconnected (e.g., import validation feeds sync, labels depend on search behavior)
+- ADRs must land together with their implementations
+- Quality gates are cumulative
+- Review is easier with one coherent changeset
+
+**Branch**: `feat/072-plan071-remediation`
+
+**PR title**: `feat: remediate plan 071 audit findings — data integrity, sync, harness, UI/UX`
+
+**PR body structure**:
+```markdown
+## Summary
+Remediates all 14 findings from plan 071 (P0–P3) across data integrity,
+synchronization, agent harness, product honesty, and UI/UX.
+
+## ADRs Accepted
+- ADR 027: Canonical State and P2P Sync Bridge
+- ADR 028: Validated and Recoverable Local Data Boundaries
+- ADR 029: Manifest-Driven Agent Harness
+
+## Changes by Goal
+### G1 — Data Integrity (P0)
+- Validated Zod hydration with versioned envelope
+- Strict Zod import validation replacing shallow guards
+- Import preview + atomic replacement + rollback
+- Entity deletion removes incoming links
+- Session-only credential storage
+
+### G2 — Sync Correctness (P0)
+- [Deferred to follow-up: bidirectional bridge requires G1 schema first]
+- Note: G2 (bidirectional sync, tombstones, conflict application) is the
+ largest single task and should be a dedicated follow-up PR after G1 lands.
+ This PR establishes the validation boundary that G2 depends on.
+
+### G3 — Product Honesty (P1)
+- Search renamed from "Semantic" to "Keyword"
+- Chat relabeled as "Local Library Q&A"
+- False-success controls removed
+
+### G4 — Harness Integrity (P1)
+- Manifest-driven validation
+- Broken symlinks fixed and fail validation
+- Dead agent doc commands removed
+
+### G5 — UI/UX (P2)
+- 44px minimum interactive targets
+- Complete dialog for encrypted export
+- ARIA tree for mind map
+- Playwright device projects
+
+### G6 — Quality Gates (P1)
+- Coverage thresholds raised
+- Warnings-as-errors enforced
+- Security scanners fail-closed
+
+### G7 — Documentation (P2)
+- Feature matrix replaces stale claims
+- Retired plans marked historical
+```
+
+## 7. Detailed File Changes
+
+### `src/lib/studio/schema.ts` (G1.1)
+- Add `PersistedEnvelopeSchema` with version, entities, claims, metadata
+- Add `CURRENT_SCHEMA_VERSION` constant
+- Add migration registry type
+- Export `validatePersistedState()`, `validateImportPayload()`, `migrateToCurrent()`
+
+### `src/lib/studio/store.ts` (G1.2, G1.5, G1.6)
+- Replace `migrate: (persistedState: unknown) => persistedState as unknown` with versioned Zod validation
+- Add `importWithRollback(entities, claims)` action: snapshot → validate → replace → persist → expose undo
+- Fix `deleteEntity`: remove links in other entities where `targetId === deletedId`
+- Add `deleteClaim(id)` action for G3.6
+
+### `src/components/studio/views/export-helpers.ts` (G1.3)
+- Replace `isEntity()` and `isClaim()` shallow guards with Zod-based `parseAndValidateImport()`
+- Return structured result: `{ success: true, data } | { success: false, errors: ValidationError[] }`
+- Remove silent filtering of invalid records
+
+### `src/components/studio/views/export-view.tsx` (G1.4, G1.5, G5.3)
+- Add import preview step: show counts, version, detected conflicts before replacement
+- Use `importWithRollback` from store instead of raw `importData`
+- Move encrypted export/reset/delete overlays to a single `
- ) : mode === 'semantic' ? (
-
- {semanticResults.map((r: SearchResult) => {
+ ) : mode === 'ranked' ? (
+
+ {rankedResults.map((r: SearchResult) => {
const targetId = r.type === 'entity' ? r.id : r.entityId
const resolvedEntity = targetId ? entities.find((e) => e.id === targetId) : undefined
const meta = resolvedEntity ? ENTITY_TYPE_META[resolvedEntity.type] : undefined
diff --git a/src/components/studio/views/ai-harness-view.tsx b/src/components/studio/views/ai-harness-view.tsx
index 12038695..479a3d42 100644
--- a/src/components/studio/views/ai-harness-view.tsx
+++ b/src/components/studio/views/ai-harness-view.tsx
@@ -234,16 +234,14 @@ export function AIHarnessView() {
-
diff --git a/src/components/studio/views/export-helpers.test.ts b/src/components/studio/views/export-helpers.test.ts
index a27e74a8..f85d2732 100644
--- a/src/components/studio/views/export-helpers.test.ts
+++ b/src/components/studio/views/export-helpers.test.ts
@@ -2,8 +2,6 @@ import { describe, it, expect } from 'vitest'
import {
escapeHtml,
parseImportFile,
- isEntity,
- isClaim,
buildJsonExport,
buildMarkdownExport,
buildHtmlExport,
@@ -71,42 +69,6 @@ describe('escapeHtml', () => {
})
})
-describe('isEntity', () => {
- it('returns true for valid entity', () => {
- expect(isEntity(SAMPLE_ENTITIES[0])).toBe(true)
- })
-
- it('returns false for null', () => {
- expect(isEntity(null)).toBe(false)
- })
-
- it('returns false for non-object', () => {
- expect(isEntity('string')).toBe(false)
- })
-
- it('returns false for missing required fields', () => {
- expect(isEntity({ id: '1', name: 'test' })).toBe(false)
- })
-
- it('returns false for wrong field types', () => {
- expect(isEntity({ id: 1, name: 'test', type: 'note', content: 'x' })).toBe(false)
- })
-})
-
-describe('isClaim', () => {
- it('returns true for valid claim', () => {
- expect(isClaim(SAMPLE_CLAIMS[0])).toBe(true)
- })
-
- it('returns false for null', () => {
- expect(isClaim(null)).toBe(false)
- })
-
- it('returns false for missing required fields', () => {
- expect(isClaim({ id: '1', entityId: 'e1' })).toBe(false)
- })
-})
-
describe('buildJsonExport', () => {
it('produces valid JSON with version and metadata', () => {
const json = buildJsonExport(SAMPLE_ENTITIES, SAMPLE_CLAIMS)
@@ -172,33 +134,44 @@ describe('parseImportFile', () => {
it('parses valid JSON export', () => {
const json = buildJsonExport(SAMPLE_ENTITIES, SAMPLE_CLAIMS)
const result = parseImportFile(json)
- expect(result.entities).toHaveLength(1)
- expect(result.claims).toHaveLength(1)
- })
-
- it('throws on invalid JSON', () => {
- expect(() => parseImportFile('not json')).toThrow('File is not valid JSON')
+ expect(result.success).toBe(true)
+ if (result.success) {
+ expect(result.entities).toHaveLength(1)
+ expect(result.claims).toHaveLength(1)
+ }
})
- it('throws on missing entities array', () => {
- expect(() => parseImportFile('{"claims":[]}')).toThrow('entities')
+ it('returns error on invalid JSON', () => {
+ const result = parseImportFile('not json')
+ expect(result.success).toBe(false)
+ if (!result.success) {
+ expect(result.errors[0].message).toContain('not valid JSON')
+ }
})
- it('throws on missing claims array', () => {
- expect(() => parseImportFile('{"entities":[]}')).toThrow('claims')
+ it('returns error on missing entities array', () => {
+ const result = parseImportFile('{"claims":[]}')
+ expect(result.success).toBe(false)
+ if (!result.success) {
+ expect(result.errors.some((e) => e.message.includes('entities') || e.path.includes('entities'))).toBe(true)
+ }
})
- it('throws on empty entities', () => {
- expect(() => parseImportFile('{"entities":[],"claims":[]}')).toThrow('No valid entities')
+ it('returns error on empty entities', () => {
+ const result = parseImportFile('{"entities":[],"claims":[]}')
+ expect(result.success).toBe(false)
+ if (!result.success) {
+ expect(result.errors[0].message).toContain('No valid entities')
+ }
})
- it('filters out invalid entities', () => {
+ it('rejects invalid entities via Zod validation', () => {
const data = {
entities: [SAMPLE_ENTITIES[0], { invalid: true }],
claims: [],
}
const result = parseImportFile(JSON.stringify(data))
- expect(result.entities).toHaveLength(1)
+ expect(result.success).toBe(false)
})
})
diff --git a/src/components/studio/views/export-helpers.ts b/src/components/studio/views/export-helpers.ts
index 64b5ced5..97379223 100644
--- a/src/components/studio/views/export-helpers.ts
+++ b/src/components/studio/views/export-helpers.ts
@@ -16,6 +16,11 @@ import {
AlignmentType,
ExternalHyperlink,
} from 'docx'
+import { validateImportPayload, type ValidationError } from '@/lib/studio/schema'
+
+export type ImportResult =
+ | { success: true; entities: Entity[]; claims: Claim[] }
+ | { success: false; errors: ValidationError[] }
export interface ExportFormat {
id: string
@@ -431,41 +436,34 @@ export async function buildDocxExport(entities: Entity[], claims: Claim[]): Prom
return Packer.toBlob(doc)
}
-export function isEntity(x: unknown): x is Entity {
- if (!x || typeof x !== 'object') return false
- const e = x as Record
- return (
- typeof e.id === 'string' &&
- typeof e.name === 'string' &&
- typeof e.type === 'string' &&
- typeof e.content === 'string'
- )
-}
-
-export function isClaim(x: unknown): x is Claim {
- if (!x || typeof x !== 'object') return false
- const c = x as Record
- return (
- typeof c.id === 'string' &&
- typeof c.entityId === 'string' &&
- typeof c.statement === 'string' &&
- typeof c.verification === 'string'
- )
-}
-
-export function parseImportFile(text: string): { entities: Entity[]; claims: Claim[] } {
+export function parseImportFile(text: string): ImportResult {
let data: unknown
try {
data = JSON.parse(text)
} catch {
- throw new Error('File is not valid JSON.')
+ return { success: false, errors: [{ path: 'root', message: 'File is not valid JSON.' }] }
+ }
+ if (!data || typeof data !== 'object') {
+ return { success: false, errors: [{ path: 'root', message: 'JSON root must be an object.' }] }
}
- if (!data || typeof data !== 'object') throw new Error('JSON root must be an object.')
+
const root = data as Record
- if (!Array.isArray(root.entities)) throw new Error('JSON must contain an "entities" array.')
- if (!Array.isArray(root.claims)) throw new Error('JSON must contain a "claims" array.')
- const entities = root.entities.filter(isEntity) as Entity[]
- const claims = root.claims.filter(isClaim) as Claim[]
- if (entities.length === 0) throw new Error('No valid entities found in file.')
- return { entities, claims }
+ const payload = {
+ version: typeof root.version === 'number' ? root.version : 1,
+ exportedAt: typeof root.exportedAt === 'string' ? root.exportedAt : new Date().toISOString(),
+ entities: root.entities ?? [],
+ claims: root.claims ?? [],
+ }
+
+ const result = validateImportPayload(payload)
+ if (!result.success) {
+ return { success: false, errors: result.errors }
+ }
+
+ const { entities, claims } = result.data
+ if (entities.length === 0) {
+ return { success: false, errors: [{ path: 'entities', message: 'No valid entities found in file.' }] }
+ }
+
+ return { success: true, entities, claims }
}
diff --git a/src/components/studio/views/export-view.tsx b/src/components/studio/views/export-view.tsx
index c883bc96..9e837a03 100644
--- a/src/components/studio/views/export-view.tsx
+++ b/src/components/studio/views/export-view.tsx
@@ -149,17 +149,19 @@ export function ExportView() {
const reader = new FileReader()
reader.onload = () => {
const text = String(reader.result || '')
- try {
- const { entities: ents, claims: cls } = parseImportFile(text)
- importData(ents, cls)
- toast.success('Import complete', {
- description: `${ents.length} entities · ${cls.length} claims replaced the current library.`,
- })
- } catch (err) {
+ const result = parseImportFile(text)
+ if (!result.success) {
+ const errorMessages = result.errors.map((e) => `${e.path}: ${e.message}`).join('; ')
toast.error('Import failed', {
- description: err instanceof Error ? err.message : 'Unknown error',
+ description: errorMessages,
})
+ return
}
+ const { entities: ents, claims: cls } = result
+ importData(ents, cls)
+ toast.success('Import complete', {
+ description: `${ents.length} entities · ${cls.length} claims replaced the current library.`,
+ })
}
reader.onerror = () => {
toast.error('Import failed', { description: 'Could not read the file.' })
diff --git a/src/components/studio/views/graph-view.tsx b/src/components/studio/views/graph-view.tsx
index 5b6fae92..8c20b8b1 100644
--- a/src/components/studio/views/graph-view.tsx
+++ b/src/components/studio/views/graph-view.tsx
@@ -377,7 +377,7 @@ function ToolbarBtn({