Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions .github/workflows/security-scan.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

93 changes: 0 additions & 93 deletions .github/workflows/version-propagation.yml

This file was deleted.

5 changes: 2 additions & 3 deletions plans/GOAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ Build a local-first, structured knowledge engine that empowers users to capture,
8. **Data Integrity** — Zod validation at all boundaries (Plan 072) ✓
9. **Honest Product Surface** — Accurate labels, no false-success controls (Plan 072) ✓

## Remaining Work (Plan 073)
## Remaining Work (Plan 074)
- Bidirectional Yjs/Zustand sync bridge
- UI/UX hardening (44px targets, ARIA tree, Playwright device coverage)
- Full accessibility audit
- Full accessibility audit (axe scanning, screen reader verification, zoom/reflow)
24 changes: 16 additions & 8 deletions plans/INDEX.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,22 @@

## Current Status

Plan 072 remediates findings from Plan 071 (codebase gap audit). The following
changes have been implemented:
Plan 073 closes out remaining gaps from Plan 072. All quality gates pass.

### Plan 073 — Closeout of Plan 072 Remaining Gaps (2026-07-24)

| Wave | Goal | Status | Changes |
|------|------|--------|---------|
| W0 | Baseline & Verification | Done | 10 gaps verified from source |
| W1 | Import Safety (P0) | Done | Import preview step, `importWithRollback` with atomic replacement and undo |
| W2 | Product Honesty + CI (P1) | Done | Chat label fixed, mobile BM25 wired, claim CRUD (edit/delete), version-propagation removed, security scanners fail-closed |
| W3 | UI/UX Polish (P2) | Done | Mind map ARIA tree + roving tabindex, Playwright mobile/tablet projects, coverage thresholds raised (30/30/25/20), 44px touch targets |
| W4 | Documentation (P2) | Done | This update |

### Deferred to Plan 074

- G2: Bidirectional Yjs/Zustand sync bridge (requires G1 validation boundary)
- Full accessibility audit (axe scanning, screen reader verification, 200% zoom)

### Plan 072 — GOAP Remediation (2026-07-24)

Expand All @@ -18,12 +32,6 @@ changes have been implemented:
| W3 | Product Honesty (P1) | Done | Semantic→Ranked labels, Re-sync removed, coming soon tooltips fixed |
| W5 | Documentation (P2) | Done | This update |

### Deferred to Plan 073

- G2: Bidirectional Yjs/Zustand sync bridge (requires G1 validation boundary)
- G5: UI/UX hardening (44px targets, ARIA tree, Playwright devices)
- Full accessibility audit (axe, screen reader, zoom/reflow)

## Historical Completed Work

## Completed - 2026-07-18 Session (Phase 10: Docs & Code Hygiene)
Expand Down
10 changes: 9 additions & 1 deletion playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
timeout: 30000,
retries: 0,
retries: 1,
reporter: 'list',
use: {
baseURL: 'http://localhost:3000',
Expand All @@ -14,6 +14,14 @@ export default defineConfig({
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'mobile',
use: { ...devices['iPhone 13'] },
},
{
name: 'tablet',
use: { ...devices['iPad Pro 11'] },
},
],
webServer: {
command: 'pnpm run dev',
Expand Down
14 changes: 12 additions & 2 deletions src/components/studio/mobile-drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { ENTITY_TYPE_META } from '@/lib/studio/types'
import { NAV_GROUPS } from './sidebar'
import { cn } from '@/lib/utils'
import { useReducedMotion } from '@/lib/studio/use-reduced-motion'
import { search } from '@/lib/search/retrieval'

/**
* MobileDrawer — slide-in drawer from the left, visible only below `lg`
Expand Down Expand Up @@ -305,10 +306,19 @@ function SearchTab({ onSelect }: { onSelect: () => void }) {
const searchQuery = useStudioStore((s) => s.searchQuery)
const setSearchQuery = useStudioStore((s) => s.setSearchQuery)
const entities = useStudioStore((s) => s.entities)
const claims = useStudioStore((s) => s.claims)
const startEdit = useStudioStore((s) => s.startEdit)
const filtered = useFilteredEntities()
const [mode, setMode] = useState<'keyword' | 'ranked'>('keyword')

const rankedResults = mode === 'ranked' && searchQuery.trim()
? search(entities, claims, searchQuery, 20)
: []

const displayEntities = mode === 'ranked' && searchQuery.trim()
? rankedResults.map((r) => entities.find((e) => e.id === (r.entityId ?? r.id))).filter(Boolean).slice(0, 20) as typeof entities
: filtered

// Empty-state copy follows the desktop SearchPanel exactly
const emptyCopy = searchQuery ? 'No matches found.' : 'Your library is empty.'

Expand Down Expand Up @@ -354,14 +364,14 @@ function SearchTab({ onSelect }: { onSelect: () => void }) {
</div>

<div className="flex-1 overflow-y-auto p-3">
{filtered.length === 0 ? (
{displayEntities.length === 0 ? (
<div className="flex flex-col items-center justify-center gap-2 py-12 text-center">
<FileText className="h-8 w-8 text-ink-faint/50" />
<p className="text-[12px] text-ink-mute">{emptyCopy}</p>
</div>
) : (
<ul className="space-y-1.5">
{filtered.slice(0, 20).map((e) => {
{displayEntities.map((e) => {
const meta = ENTITY_TYPE_META[e.type]
return (
<li key={e.id}>
Expand Down
2 changes: 1 addition & 1 deletion src/components/studio/ui/shared-primitives.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const BUTTON_VARIANTS: Record<ButtonVariant, string> = {
'danger-solid':
'bg-red-600 px-4 py-1.5 text-[12px] text-white shadow-sm hover:bg-red-700',
'icon-primary':
'h-9 w-9 rounded-md bg-primary text-primary-foreground shadow-sm hover:opacity-90 press-scale',
'min-h-[44px] min-w-[44px] rounded-md bg-primary text-primary-foreground shadow-sm hover:opacity-90 press-scale',
}

const BUTTON_SIZES: Record<ButtonSize, string> = {
Expand Down
4 changes: 2 additions & 2 deletions src/components/studio/views/chat-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ export function ChatView() {
Ask your library
</h2>
<p className="mb-6 max-w-sm text-[14px] text-ink-mute">
Chat with your knowledge base. Ask questions, request summaries, or explore connections between entities.
Ask questions about your library. Answers are based on local search — no data leaves your device.
</p>
<div className="flex flex-wrap justify-center gap-2">
{SUGGESTIONS.map((s) => (
Expand Down Expand Up @@ -255,7 +255,7 @@ export function ChatView() {
<button
onClick={() => { handleSend() }}
disabled={!input.trim() || chatLoading}
className="flex h-9 w-9 items-center justify-center rounded-lg bg-primary text-primary-foreground shadow-sm transition-all hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-40 press-scale focus-ring"
className="flex min-h-[44px] min-w-[44px] items-center justify-center rounded-lg bg-primary text-primary-foreground shadow-sm transition-all hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-40 press-scale focus-ring"
aria-label="Send message"
>
<Send className="h-4 w-4" />
Expand Down
71 changes: 61 additions & 10 deletions src/components/studio/views/editor-claims-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState } from 'react'
import { Plus, ExternalLink, ShieldCheck, CheckCircle2, AlertTriangle, Circle, Save } from 'lucide-react'
import { Plus, ExternalLink, ShieldCheck, CheckCircle2, AlertTriangle, Circle, Save, Pencil, Trash2 } from 'lucide-react'
import { toast } from 'sonner'
import type { Claim, VerificationStatus } from '@/lib/studio/types'

Expand Down Expand Up @@ -30,12 +30,17 @@ export function ClaimsPanel({
claims,
editingEntityId,
addClaim,
updateClaim,
deleteClaim,
}: {
claims: Claim[]
editingEntityId: string
addClaim: (claim: Omit<Claim, 'id'>) => void
updateClaim: (id: string, updates: Partial<Omit<Claim, 'id' | 'entityId'>>) => void
deleteClaim: (id: string) => void
}) {
const [showForm, setShowForm] = useState(false)
const [editingId, setEditingId] = useState<string | null>(null)
const [statement, setStatement] = useState('')
const [verification, setVerification] = useState<VerificationStatus>('unverified')
const [confidence, setConfidence] = useState(50)
Expand All @@ -47,6 +52,16 @@ export function ClaimsPanel({
setConfidence(50)
setSource('')
setShowForm(false)
setEditingId(null)
}

const startEdit = (claim: Claim) => {
setEditingId(claim.id)
setStatement(claim.statement)
setVerification(claim.verification)
setConfidence(Math.round(claim.confidence * 100))
setSource(claim.source ?? '')
setShowForm(true)
}

const handleSave = () => {
Expand All @@ -55,16 +70,31 @@ export function ClaimsPanel({
toast.error('Claim statement is required')
return
}
addClaim({
entityId: editingEntityId,
statement: trimmed,
verification,
confidence: confidence / 100,
source: source.trim() || undefined,
})
if (editingId) {
updateClaim(editingId, {
statement: trimmed,
verification,
confidence: confidence / 100,
source: source.trim() || undefined,
})
toast.success('Claim updated')
} else {
addClaim({
entityId: editingEntityId,
statement: trimmed,
verification,
confidence: confidence / 100,
source: source.trim() || undefined,
})
}
resetForm()
}

const handleDelete = (id: string) => {
deleteClaim(id)
toast.success('Claim deleted')
}

return (
<section
aria-label="Claims"
Expand Down Expand Up @@ -136,14 +166,35 @@ export function ClaimsPanel({
{c.evidence}
</p>
)}

<div className="mt-2 flex items-center gap-1.5">
<button
type="button"
onClick={() => { startEdit(c) }}
className="flex items-center gap-1 rounded-md px-2 py-1 text-caption font-medium text-ink-faint transition-colors hover:bg-muted hover:text-ink focus-ring"
aria-label="Edit claim"
>
<Pencil className="h-3 w-3" />
Edit
</button>
<button
type="button"
onClick={() => { handleDelete(c.id) }}
className="flex items-center gap-1 rounded-md px-2 py-1 text-caption font-medium text-ink-faint transition-colors hover:bg-red-50 hover:text-red-600 dark:hover:bg-red-950/40 focus-ring"
aria-label="Delete claim"
>
<Trash2 className="h-3 w-3" />
Delete
</button>
</div>
</li>
))}
</ul>

{showForm && (
<div className="mt-3 rounded-md border border-saffron/40 bg-background p-3">
<label htmlFor="claim-statement" className="mb-1 block text-label font-semibold uppercase tracking-wide text-ink-faint">
Statement
{editingId ? 'Edit statement' : 'Statement'}
</label>
<textarea
id="claim-statement"
Expand Down Expand Up @@ -216,7 +267,7 @@ export function ClaimsPanel({
className="flex items-center gap-1.5 rounded-md bg-primary px-4 py-1.5 text-[12px] font-semibold text-primary-foreground shadow-sm transition-all hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-40 press-scale focus-ring"
>
<Save className="h-3.5 w-3.5" />
Save claim
{editingId ? 'Update claim' : 'Save claim'}
</button>
</div>
</div>
Expand Down
Loading
Loading