Skip to content

Release v0.2.0 — Collaborative Ontology Editor#1

Merged
JohnRDOrazio merged 110 commits intomainfrom
release/v0.2.0
Mar 9, 2026
Merged

Release v0.2.0 — Collaborative Ontology Editor#1
JohnRDOrazio merged 110 commits intomainfrom
release/v0.2.0

Conversation

@damienriehl
Copy link
Contributor

@damienriehl damienriehl commented Mar 4, 2026

OntoKit — Comprehensive Change Summary

Scope: All changes across ontokit-web (20 commits, 104 files, +17,590/−924 lines) and ontokit-api (5 commits, 31 files, +3,070/−86 lines) since the feature branches diverged from origin/main.


Overview

This PR transforms OntoKit from a basic ontology viewer into a full-featured collaborative ontology editor. The changes span both the frontend (Next.js) and backend (FastAPI) and can be grouped into four themes:

Editing experience — Users can now create, edit, and reparent ontology entities through structured forms rather than raw Turtle source. A WebProtege-style auto-save system prevents data loss: edits are drafted locally on blur and committed to git on navigation. Classes open read-only by default to prevent accidental changes, with an explicit "Edit Item" button and an optional continuous-editing toggle. Drag-and-drop reparenting supports OWL multi-parent semantics with cycle detection and undo.

Collaboration — A new "suggester" role allows non-editors to propose changes. Suggesters edit on dedicated branches; their work is auto-saved via sendBeacon and can be submitted as pull requests for editor review. Editors can approve, reject, or request changes through a built-in review page, with notifications keeping everyone informed. New users now default to the suggester role to ensure all initial contributions go through review.

Visualization & navigation — An interactive graph view (React Flow + ELK layout) renders entity relationships with expandable nodes and edge-type styling. Full ancestry tracing ensures every node connects back to root instead of floating. Entity types (classes, individuals, properties) are color-coded with letter badges and a collapsible legend. A shared FOLIO-style tree renderer supports keyboard navigation, filtered search, and multi-level expand/collapse across Classes, Properties, and Individuals. Language tags display as flag emojis for a cleaner, more visual presentation.

Quality & accessibility — Both repos gained comprehensive test suites (111 tests total). The frontend added CI/CD, ESLint strict rules, Zod environment validation, error boundaries, and API retry logic. The backend gained lifespan handlers, JWT role extraction, full-text search, rate limiting, security headers, and custom exception types. Accessibility improvements include skip links, ARIA live regions, screen reader announcements for drag-and-drop, keyboard shortcuts, and reduced-motion/high-contrast media queries.


Table of Contents

  1. Infrastructure & Code Quality
  2. Editor Mode System & Theming
  3. Entity Operations & Context Menus
  4. Form-Based Editing
  5. Auto-Save System
  6. Read-Only Default & Continuous Editing
  7. Shared Tree Renderer & Entity Panels
  8. Suggestion Workflow
  9. Drag-and-Drop Class Reparenting
  10. Graph Visualization
  11. Keyboard Shortcuts
  12. Accessibility
  13. UI Polish & Bug Fixes
  14. Backend — API Infrastructure
  15. Backend — Suggestion Sessions
  16. Backend — Bug Fixes & Role Changes

1. Infrastructure & Code Quality

Commit: a3c8d33 (web)
Commit: 1cab6c0 (api — infrastructure portion)

Frontend (ontokit-web)

  • CI/CD pipeline — GitHub Actions workflow (.github/workflows/ci.yml): lint → type-check → test → build, runs on push to main and PRs, Node.js 22, cached node_modules
  • ESLint flat config — Migrated to eslint.config.mjs with @typescript-eslint/no-explicit-any: "warn" and stricter rules
  • Environment validationlib/env.ts uses Zod to validate required env vars at build time with helpful error messages; separates server-side and client-side vars
  • Test suite — Vitest configured (vitest.config.ts) with happy-dom, path aliases, coverage thresholds; 26 tests covering:
    • Utility functions (getLocalName, getNamespace, getPreferredLabel, formatDate, debounce, generateId)
    • API client (api.get, api.post, api.put, api.delete, ApiError, query params, empty responses)
    • Component rendering (button, project-card, BranchSelector)
  • Error boundarycomponents/error-boundary.tsx: React error boundary wrapping the app with a friendly retry UI
  • API retry logiclib/api/client.ts now has 30s request timeout, retry on 5xx errors (max 2 retries, exponential backoff)
  • i18n locale detectionAccept-Language header detection, cookie-based locale persistence

Backend (ontokit-api)

  • 85 passing tests across unit and integration suites:
    • test_auth.py — Token validation, role extraction, permission checker
    • test_config.py — Settings loading, property methods
    • test_search.py — Search service methods
    • test_linter.py — Individual lint rules
    • test_ontology_service.py — RDF graph operations, format conversion
    • test_projects_routes.py — Project CRUD endpoints
    • test_project_workflow.py — End-to-end: create project → add ontology → create branch → commit → create PR → merge
  • Test fixturestests/conftest.py: async test client, mock DB/Redis/MinIO, authenticated user fixture, sample data

2. Editor Mode System & Theming

Commit: 03e2575

Adds a Standard/Developer mode toggle and full theme support, decomposing the monolithic editor page into mode-specific layouts.

  • Zustand store (lib/stores/editorModeStore.ts) — persists editorMode ("standard" | "developer") and theme ("light" | "dark" | "system") to localStorage
  • Theme syncuseThemeSync() hook keeps DOM class attribute in sync; inline <script> in <head> prevents flash of unstyled content
  • Dark mode — Tailwind class-based (darkMode: "class"), all components styled with dark: variants
  • ThemeToggle — lives in the global Header component, available on all pages
  • ModeSwitcher — lives in the editor header, toggles between Standard and Developer views
  • Page decomposition:
    • Orchestrator (app/projects/[id]/editor/page.tsx, ~850 lines) — owns project loading, auth, branch management, source loading, IRI indexing, entity CRUD, commit flow
    • Developer layout (components/editor/developer/DeveloperEditorLayout.tsx) — tree/source view switching, search, three-panel layout
    • Standard layout (components/editor/standard/StandardEditorLayout.tsx) — tree + detail panel with inline editing
  • Settings page (app/settings/page.tsx) — Editor Preferences section for mode and theme

3. Entity Operations & Context Menus

Commits: 4147e40, e722205

IRI Generation & Entity Creation

  • lib/ontology/iriGenerator.ts — generates valid IRIs from labels using ontology namespace, handles collisions
  • AddEntityDialog — modal for creating Classes, Properties, Individuals with parent selection and IRI preview
  • Optimistic tree updates — new nodes appear instantly before server confirmation

Context Menus & Toast System

  • Right-click context menus on tree nodes via Radix UI (components/ui/context-menu.tsx)
    • Add Subclass (Plus icon) — gated on canEdit
    • Copy IRI (Copy icon) — always shown
    • View in Source (Code icon) — developer mode only
    • Delete (Trash2 icon) — gated on canEdit, styled destructive
  • Toast notification systemcomponents/ui/toast-container.tsx rendering floating notifications (bottom-right, auto-dismiss), wired into app/providers.tsx
  • Delete class — confirmation dialog → optimistic removal from tree → API call → rollback on error
  • Copy IRI — one-click copy from tree context menu or detail panel header

4. Form-Based Editing

Commit: 108d93b

Replaces raw Turtle source editing with structured, form-based class editing. All saves route through Turtle text manipulation since the backend class endpoint only supports GET.

  • ClassDetailPanel (components/editor/ClassDetailPanel.tsx) — structured editing for:
    • Labels (multilingual, with language tags)
    • Comments/definitions
    • Annotations (44 known annotation properties from lib/ontology/annotationProperties.ts)
    • Relationships (rdfs:seeAlso, rdfs:isDefinedBy, and other IRI-valued properties)
    • Parent classes (rdfs:subClassOf)
  • InlineAnnotationAdder — persistent row with property dropdown + value input (replaces popup picker); ghost rows with descriptive placeholders ("Add another Definition — or translation.")
  • RelationshipSection — dedicated section for IRI-valued properties with entity search
  • Turtle source savelib/ontology/turtleClassUpdater.ts finds the class block in Turtle text, regenerates it from form data, replaces in source, then saves via PUT /source
    • findBlock carefully checks continuation lines (;/,) to avoid matching object references instead of subject definitions
  • ClassUpdatePayload — preserves deprecated, equivalent_iris, disjoint_iris during form edits
  • ResizablePanelDivider — draggable tree/detail panel divider in both layouts
  • Entity search combobox — async label resolution for IRI-valued fields

5. Auto-Save System

Commit: 9925e96

WebProtege-style auto-save with a two-tier draft/commit model.

  • Draft store (lib/stores/draftStore.ts) — Zustand + localStorage persist (ontokit-drafts), keyed by "projectId:branch:classIri"
  • useAutoSave hook (lib/hooks/useAutoSave.ts):
    • Tier 1 (blur) — field blur writes to draft store instantly; validates labels before saving
    • Tier 2 (navigate) — navigating away from a class builds a ClassUpdatePayload, calls the source save endpoint, clears the draft
    • discardDraft() — clears draft from store, resets status
    • onError callback — enables toast notification on failed commits
  • AutoSaveStatusBar (components/editor/AutoSaveStatusBar.tsx) — states: idle (hidden), draft (amber dot), saving (spinner), saved (green check), error (red + retry button); has role="status" and aria-live
  • Tree badgesClassTree accepts draftIris?: Set<string>, renders amber dot indicator on nodes with unsaved changes

6. Read-Only Default & Continuous Editing

Commit: a3bf985

  • Read-only by default — classes open in read-only view even when the user has edit rights; prevents accidental edits while browsing
  • "Edit Item" button — explicit entry into edit mode
  • "Cancel" button — discards draft, reverts to server state, returns to read-only; sets cancelledIriRef to prevent auto-re-entry
  • Continuous editing toggle (ContinuousEditingToggle) — persisted boolean in editorModeStore
    • When ON: classes auto-enter edit mode when data loads
    • Cancel overrides: prevents re-entry for that specific classIri
    • Available in editor header and settings page
  • Navigate-away behavior — auto-commits current edits, new class opens read-only (or auto-editable if continuous editing is ON)

7. Shared Tree Renderer & Entity Panels

Commits: 4554022, eb36c2a, 1a2a5d3, 8b4e347

FOLIO-Style Shared Tree Renderer

  • EntityTree / EntityTreeNode (components/editor/shared/) — shared tree component used by Classes, Properties, and Individuals
  • Keyboard navigation — arrow keys for up/down/expand/collapse, Enter/Space to select
  • Filtered search — type-ahead search with ancestor-path context preservation
  • Expand/collapse controls — toolbar buttons for all/none/selected
  • Tree node alignment — leaf dots share chevron column width for consistent indentation; stable selected-node border

Entity Tabs & Detail Panels

  • Unified entity tabs — Classes, Properties, Individuals tabs in both Standard and Developer layouts; removed redundant header
  • PropertyDetailPanel — form-based editing for OWL properties (domain, range, characteristics)
  • IndividualDetailPanel — form-based editing for OWL individuals (types, property assertions)
  • Aligned column layout — consistent read-only section formatting across all entity types

8. Suggestion Workflow

Commits: 9e9ee50 (web), 2cd425c (api)

A complete suggester workflow allowing non-editors to propose changes that go through review.

Role System

  • "suggester" role — new role between editor and viewer
    • canEdit: owner | admin | editor | superadmin
    • canSuggest: canEdit || suggester
    • isSuggestionMode: suggester && !canEdit → routes saves to suggestion API
  • Default role for new users — changed from editor/viewer to suggester

Frontend (ontokit-web)

  • API client (lib/api/suggestions.ts) — session CRUD, save, submit, discard, beacon flush, and review methods (listPending, approve, reject, requestChanges, resubmit)
  • useSuggestionSession hook — manages full session lifecycle:
    • Create → save → submit → discard
    • Resume changes-requested sessions (resumeSession)
    • Resubmit with incremented revision
    • isResumed flag for UI differentiation
  • useSuggestionBeacon hookvisibilitychange + beforeunload flush via navigator.sendBeacon
  • useAutoSave extensionsaveMode: "commit" | "suggest" + onSuggestSave callback
  • Notification system:
    • lib/api/notifications.ts — unified notification API (list, markAsRead, markAllAsRead)
    • useNotifications hook — 30s polling + event-driven refetch
    • NotificationBell (components/layout/notification-bell.tsx) — flat list with type-based icons, unread dots, mark-all-as-read
    • Types: suggestion_submitted/approved/rejected/changes_requested/auto_submitted, join_request, pr_opened/merged/review
  • My Suggestions page (app/projects/[id]/suggestions/page.tsx) — lists user's suggestions with status, feedback display, resume button, revision badge, GitHub PR link
  • Review page (app/projects/[id]/suggestions/review/page.tsx) — editors/admins review pending suggestions:
    • Summary and files tabs
    • Inline diff view
    • Approve / reject / request-changes action bar
    • RejectSuggestionDialog, RequestChangesDialog
  • Editor integration:
    • "Review Suggestions" button in editor header with pending count badge
    • ?resumeSession={sid}&branch={branch} query params for auto-resume
    • Resubmit vs Submit button based on isResumed

Backend (ontokit-api)

  • Database model (ontokit/models/suggestion_session.py) — SuggestionSession with statuses: active, submitted, auto-submitted, discarded, merged, rejected, changes-requested
  • Service layer (ontokit/services/suggestion_service.py, 537 lines) — create session → save commits to dedicated branch → submit as PR → discard with branch cleanup
  • 6 API endpoints — create, save, submit, list, discard, beacon
  • Beacon token auth (ontokit/core/beacon_token.py) — HMAC-signed JWT for sendBeacon authentication (no session cookie needed)
  • Background worker (ontokit/worker.py) — auto-submit stale sessions (>30min inactive) via cron job every 10 minutes
  • Alembic migrationsuggestion_sessions table with foreign keys to projects and pull_requests
  • Pydantic schemas (ontokit/schemas/suggestion.py) — request/response models for all endpoints

9. Drag-and-Drop Class Reparenting

Commit: 3494b1c

  • Library: @dnd-kit/core v6 + @dnd-kit/utilities
  • useTreeDragDrop hook (lib/hooks/useTreeDragDrop.ts) — manages drag state, cycle detection, Alt-key tracking, auto-expand timer, undo state
  • MOVE mode (default) — removes old parent, adds new parent
  • ADD mode (Alt/Option held) — keeps old parents, adds new parent (OWL multi-parent support)
  • Cycle detection — client-side tree walk for instant feedback; API ancestor check as fallback for unexpanded subtrees
  • Auto-expand — 800ms hover on collapsed nodes during drag triggers expansion
  • Root drop zone — thin dashed bar at top of tree; dropping removes all rdfs:subClassOf
  • Undo — 5-second toast with Undo button; snapshots oldParentIris for rollback
  • Multi-parent confirmation — dialog when moving a class that has multiple parents
  • Edge cases handled: self-drop rejection, drop-on-descendant prevention, drop-on-current-parent no-op, drag disabled during search, drag disabled on currently-editing node, suggestion mode routing
  • DraggableTreeWrapper (components/editor/shared/DraggableTreeWrapper.tsx) — wraps tree in DndContext with pointer + keyboard sensors
  • Screen reader supportonAnnounce callback for drag events via useAnnounce()

10. Graph Visualization

Commits: c24260a, 4ff1731, 20f58e7, 7318bcd, 71e1b06, d217a00

Interactive entity relationship graph using React Flow and ELK layout.

  • Dependencies: @xyflow/react v12 + elkjs (lazy-loaded via next/dynamic)
  • Graph data builder (lib/graph/buildGraphData.ts) — builds graph from Map<string, OWLClassDetail>, handles subClassOf / equivalentClass / disjointWith / seeAlso edges, caps children at 20/node, tracks visited set for cycle prevention
  • ELK layout (lib/graph/elkLayout.ts) — layered algorithm, TB/LR direction toggle, 40px node spacing, 80px layer spacing
  • useGraphData hook (lib/hooks/useGraphData.ts) — fetches focus node + depth-2 neighbors, expandNode() for on-demand expansion, resetGraph(), caps at 100 resolved nodes
  • Full ancestry tracing — after depth-2 fetch, calls /ontology/tree/{iri}/ancestors for each displayed class to discover the full path to root; adds missing ancestor nodes + subClassOf edges so no node appears floating
  • owl:Thing filteringowl:Thing is excluded from the graph (no node, no edges); classes whose only parent is Thing are classified as root nodes
  • Entity type color coding — non-class entities discovered via search API are color-coded:
    • Individuals: pink border/bg with "I" letter badge (matches app's owl-individual convention)
    • Properties: blue border/bg with "P" letter badge (matches app's owl-property convention)
  • Root node styling — branch root nodes (direct children of owl:Thing) use bold amber/gold border + background to signal hierarchy anchors
  • Custom nodes (components/graph/OntologyNode.tsx) — 7 node type styles (focus, class, root, individual, property, external, unexplored) with dark mode support and type letter badges
  • Custom edges (components/graph/OntologyEdge.tsx) — 4 edge type styles (subClassOf, equivalentClass, disjointWith, seeAlso) with hover labels
  • Collapsible legend (GraphLegend in OntologyGraph.tsx) — positioned bottom-right, shows all node type swatches (with badges) and edge type line samples; uses both color and shape/text for accessibility
  • OntologyGraph (components/graph/OntologyGraph.tsx) — ReactFlow canvas + MiniMap + Controls + layout direction toolbar + legend
  • Integration:
    • Developer layout: "Graph" tab alongside Tree/Source
    • Standard layout: "Graph" button in ClassDetailPanel header row
  • Session expiry guard — detects RefreshAccessTokenError during graph data fetches

11. Keyboard Shortcuts

Commit: c24260a

  • useKeyboardShortcuts hook (lib/hooks/useKeyboardShortcuts.ts) — single keydown listener with Mac Cmd/Ctrl compatibility
  • Shortcuts:
    • Ctrl+S / Cmd+S — flush current draft to git
    • Ctrl+N / Cmd+N — open Add Entity dialog
    • ? — open keyboard shortcuts help dialog
    • Escape — close current overlay/dialog
  • Input suppression — shortcuts disabled inside Monaco editor, text inputs, textareas, dialogs
  • KeyboardShortcutDialog (components/editor/KeyboardShortcutDialog.tsx) — groups shortcuts by category with <kbd> badges
  • UI integration — keyboard icon button in editor header; Ctrl+K hint displayed near search icon in toolbar
  • HelpersformatShortcut() and isMac() exported for consistent display

12. Accessibility

Commit: c24260a

  • Skip links — "Skip to main content" link in app/layout.tsx with .skip-link CSS class
  • Screen reader announcer (components/ui/ScreenReaderAnnouncer.tsx) — useAnnounce() hook with polite + assertive ARIA live regions; mounted in app/providers.tsx
  • ARIA live regionsAutoSaveStatusBar has role="status" + aria-live on all status variants
  • Drag-and-drop announceuseTreeDragDrop has onAnnounce callback; both layouts pass announce from useAnnounce()
  • Tree accessibilityaria-activedescendant on tree container, unique id on each tree item
  • Icon buttons — all toolbar icon buttons use aria-label instead of title
  • Form inputsAnnotationRow and InlineAnnotationAdder inputs have aria-label
  • Reduced motion@media (prefers-reduced-motion: reduce) disables all animations
  • High contrast@media (prefers-contrast: more) increases focus ring width

13. UI Polish & Bug Fixes

Commits: 1e38288, 15d9cfa, 20f58e7, bcbfaaf, 4ff1731, 9a379b5

  • Language flags — replaced language code text badges (e.g., "en", "la") with country flag emojis for a cleaner, more visual presentation
  • Flag alignment fix — invisible placeholder divs ensure consistent row height when a language flag is unavailable
  • Graph button positioning — moved "Graph" button into ClassDetailPanel header row to fix overlap with the "Edit" button
  • Session expiry guard — detects RefreshAccessTokenError and redirects to sign-in instead of showing cryptic errors; added to graph data fetches
  • seeAlso edges in graph — included rdfs:seeAlso relationships in graph visualization
  • Multi-level expand/collapse — split-button controls: click expands/collapses one level; dropdown arrow offers "Expand All" / "Collapse All"
  • Default suggester role — new users default to "suggester" instead of "editor", ensuring all contributions go through the review workflow initially

14. Backend — API Infrastructure

Commit: 1cab6c0

Comprehensive backend improvements covering missing features, code quality, and test coverage.

Lifespan Handlers (ontokit/main.py)

  • Async database engine initialization + graceful shutdown
  • Redis connection pool startup + cleanup
  • MinIO bucket initialization
  • Structured startup/shutdown logging

Authentication & Authorization (ontokit/core/auth.py)

  • JWT role extraction from Zitadel urn:zitadel:iam:org:project:roles claim
  • JWKS cache with 1-hour TTL (was previously unbounded)
  • Roles field added to TokenPayload and CurrentUser

Search Service (ontokit/services/search.py)

  • Full-text search via PostgreSQL tsvector/tsquery
  • SPARQL query execution via RDFLib (SELECT/ASK/CONSTRUCT)
  • Pagination support
  • Wildcard support (* query returns all entities of requested type)

Rate Limiting

  • slowapi dependency with per-IP limits (100 req/min default)
  • Rate limit headers in responses

Middleware (ontokit/core/middleware.py)

  • Request ID middleware (UUID per request in X-Request-ID header)
  • Access logging (method, path, status code, duration)
  • Security headers: X-Content-Type-Options, X-Frame-Options, X-XSS-Protection, Strict-Transport-Security, Content-Security-Policy

Code Quality

  • Custom exception types (ontokit/core/exceptions.py) — NotFoundError, ValidationError, ConflictError, ForbiddenError with global handlers
  • IRI format validation (Pydantic validators)
  • String length limits on text fields

15. Backend — Suggestion Sessions

Commit: 2cd425c

Full backend implementation of the suggestion workflow (see Section 8 for the frontend counterpart).

  • Database modelSuggestionSession table with session lifecycle tracking
  • Service layer (537 lines) — create session → save commits to dedicated branch → submit as PR → discard with branch cleanup
  • 6 API endpoints — create, save, submit, list, discard, beacon
  • Beacon token auth — HMAC-signed tokens for navigator.sendBeacon (tab close/hide)
  • Auto-submit worker — cron job catches abandoned sessions (>30min) and auto-creates PRs
  • Alembic migrationsuggestion_sessions table with proper FKs and constraints

16. Backend — Bug Fixes & Role Changes

Commits: 3fe55df, eb8e306

  • HttpUrl crash fix — Pydantic v2 HttpUrl is not a plain string; wrapped cls.iri.lower() in str() for class tree sort keys in both get_root_classes and get_class_children
  • Default suggester roleMemberBase schema default changed from "viewer" to "suggester"; join request approval assigns "suggester" instead of "editor"

Stats Summary

Metric ontokit-web ontokit-api Total
Commits 20 5 25
Files changed 104 31 135
Insertions +17,590 +3,070 +20,660
Deletions −924 −86 −1,010
Tests added 26 85 111

New Dependencies (ontokit-web)

  • @dnd-kit/core, @dnd-kit/utilities — drag-and-drop
  • @xyflow/react, elkjs — graph visualization
  • vitest, @vitejs/plugin-react, happy-dom — testing
  • zod — environment validation

Key Architectural Decisions

  1. Turtle source save — All entity edits go through Turtle text manipulation + PUT /source because the backend class endpoint only supports GET. This enables form-based editing without backend schema changes.
  2. Two-tier auto-save — Blur saves to localStorage (instant, survives refresh); navigate-away commits to git (batched, no data loss). Modeled after WebProtege.
  3. Suggester role — Non-destructive contribution path: suggesters propose changes on dedicated branches that go through editor review before merging.
  4. Shared tree rendererEntityTree/EntityTreeNode components reused across Classes, Properties, and Individuals tabs, reducing duplication and ensuring consistent behavior.
  5. Graph lazy loadingelkjs and @xyflow/react loaded via next/dynamic to avoid bundling heavy visualization libraries for users who don't need them.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Major editor expansion: Standard & Developer modes, tree/source/graph views, structured detail panels, suggestion workflow UI, Project Analytics, notifications, toasts, embeddings-powered semantic search, upstream sync, autosave/drafts, keyboard shortcuts, theme & continuous-edit toggles.
  • Bug Fixes & Improvements

    • Improved auth retry/refresh flow, accessibility anchors and screen-reader announcements, drag‑and‑drop and UX polish, new "suggester" role.
  • CI & Config

    • Added CI workflow and runtime env validation.
  • Tests

    • Added extensive unit and integration tests (utils, graph, env, API, hooks).

damienriehl and others added 30 commits February 22, 2026 12:42
…ts), error boundary, API retry logic, and i18n locale detection

- Fix auth.ts type safety: replace `as any` with `as User`
- Add GitHub Actions CI: lint, type-check, test, build on Node.js 22
- Add ESLint flat config with no-explicit-any warn and no-unused-vars error
- Add Zod-based server/client environment variable validation
- Configure Vitest with jsdom, React plugin, and path aliases
- Add 26 passing tests for utility functions and API client
- Add React ErrorBoundary component with retry support
- Add 30s request timeout and 5xx retry with exponential backoff to API client
- Implement Accept-Language header detection and cookie-based locale persistence

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds WebProtege-style entity creation via source injection: generates
valid Turtle snippets (all 5 OWL entity types) and appends them to the
editor buffer. The tree optimistically shows new classes immediately,
with the detail panel gracefully falling back to tree-node data for
unsaved entities. Source and tree views stay in sync via getValue() ref
and view-switch syncing.

New files:
- lib/ontology/iriGeneration.ts (base62 UUID, pattern detection, IRI gen)
- lib/ontology/turtleSnippetGenerator.ts (Turtle output for all types)
- components/editor/AddEntityDialog.tsx (label, type, IRI fields)
- 41 new tests across 2 test files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… and page decomposition

Phase 1 of Standard Mode implementation:
- Create Zustand editorModeStore with mode (standard/developer) and theme (light/dark/system) preferences persisted to localStorage
- Switch Tailwind dark mode from media queries to class-based selectors
- Add inline theme script in layout.tsx to prevent flash of wrong theme
- Add ModeSwitcher component (segmented control in editor header)
- Add ThemeToggle component (light/dark/system in global header)
- Decompose 1127-line editor page into orchestrator + DeveloperEditorLayout + StandardEditorLayout
- Add Editor Preferences section to Settings page

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Right-click context menu on tree nodes with Add Subclass, Copy IRI,
View in Source (developer mode), and Delete class with confirmation.
Toast notification system wired into providers. Copy IRI button in
detail panel header. Optimistic tree removal on delete. Fix canEdit
to default authenticated users without explicit role to edit access.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ons, and relationships (Phases 3 + 3.5)

Implements structured editing of OWL classes through the detail panel, routing saves through
Turtle source manipulation since the backend class endpoint only supports GET. Adds annotation
editing with inline property adder, relationship section for IRI-valued properties (rdfs:seeAlso),
entity search combobox, resizable panel divider, entity tabs, and async label resolution for
relationship targets.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fields are now always editable when canEdit — no Edit/Save/Cancel buttons.
Blur saves instantly to a Zustand draft store (localStorage-persisted),
and navigating to a different class flushes the draft as a single git
commit. Tree nodes show amber dot badges for classes with uncommitted
drafts, and a status bar shows draft/saving/saved/error state.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… and label resolution fixes

- Default read-only detail panel with explicit Edit Item / Cancel buttons
- Continuous editing toggle (persisted in Zustand store) auto-enters edit mode across class navigation
- Cancel overrides continuous editing for the current class via cancelledIriRef
- discardDraft() and onError callback added to useAutoSave hook
- Tree preserves expansion state after class updates (updateNodeLabel instead of loadRootClasses)
- Fix layout shift: always-rendered + button in tree rows reserves space
- Resolve rdfs:label for individuals/properties via searchEntities fallback
- Patch edit-mode relationship labels when async resolution completes
- Continuous editing preference added to Settings page

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…nt header

- Add Classes/Properties/Individuals tab bar to Developer layout tree panel
- Remove duplicative text header below tab bar in Standard layout
- Extract EntityPlaceholderDetail into shared component used by both layouts
- Both views now have identical entity tab structure with compact toolbar

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…xpand/collapse

Refactor tree UI across all entity tabs (Classes, Properties, Individuals) to use
a shared EntityTree/EntityTreeNode renderer with FOLIO-inspired patterns: leaf dots
instead of C/P/I badges, root emphasis, double-click expand, keyboard navigation
(arrow keys + Enter/Space), and filtered tree search with ancestor-path context.

Extract reusable EntityTreeToolbar with expand-all/collapse-all buttons and
useTreeSearch hook to eliminate duplicated search state across both layouts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… border

- Place leaf dot inside same 16px container as chevron so labels align at every depth
- Add permanent transparent left border on root items to prevent 2px shift on selection

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… aligned column layout

Implements full detail view and edit capability for Properties and Individuals,
parsing metadata from Turtle source. Includes shared Turtle utilities, block parser,
entity detail extractors, updaters, generic auto-save hook, IRI label resolution,
and property assertion sections. Read-only multi-row sections (Annotations, Object/Data
Properties) use flat row layout with field name badges aligned in the section heading column.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…l panels

Add langToFlag() utility and shared LanguageFlag component that maps ISO language
tags to flag emojis (with grey badge fallback for unmappable codes). Applied across
all view-mode and edit-mode locations in Class, Property, and Individual panels.
Primary Label omits flags in view mode since English is the assumed default.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Implements Phases A+B (suggestion submission) and Phase D (review & admin):
- Suggester role with session-based editing, auto-save to suggestion branch, and PR submission
- Review page for editors/admins with diff viewing, approve/reject/request-changes actions
- Feedback loop: suggesters see reviewer feedback and can resume editing changed-requested sessions
- Unified notification system with polling hook, type-based icons, and mark-as-read
- Notification bell rewritten to use useNotifications hook with flat notification list

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Drag a class node onto another class to reparent it, with optimistic
UI updates, undo toast, auto-expand on hover, root drop zone, Alt-key
ADD mode (multi-parent), and cycle detection. Uses @dnd-kit/core with
a pointer-tracking overlay for reliable cursor alignment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 6A: Entity graph visualization using React Flow + ELK.js with
custom nodes/edges, lazy-loaded via next/dynamic. Supports TB/LR
layout, node expansion, and dark mode.

Phase 6B: Global keyboard shortcuts (Ctrl+S save, Ctrl+N new entity,
Ctrl+K search, ? help dialog) with Mac/Windows support and
input/Monaco/dialog suppression.

Phase 6C: Accessibility improvements including skip links, ARIA live
regions via ScreenReaderAnnouncer, aria-labels on icon buttons and
form inputs, aria-activedescendant on tree, reduced motion and high
contrast media queries.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
SessionGuard: detect RefreshAccessTokenError globally and force
re-authentication when the Zitadel refresh token is dead, preventing
stale-token API failures across all pages.

Graph: include seeAlso/isDefinedBy IRI targets when expanding
neighbor nodes so relationship edges render in the visualization.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…h Edit button

Added headerActions prop to ClassDetailPanel and moved the Graph
button from an absolute-positioned overlay into the header flex row
so it sits side-by-side with the Edit/Cancel button.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
LanguageFlag now always renders a fixed h-5 w-5 container regardless
of whether a flag emoji can be resolved, keeping the flag column
width consistent across all annotation rows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace single expand-all/collapse-all icon buttons with split-button
groups offering incremental control: expand one level (default) with
expand-all as secondary, and collapse all (default) with collapse one
level as secondary. Includes disabled states, loading spinner, and a
dismissible tip for first-time users.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
New users joining via join request approval, manual member addition,
or no-role fallback now default to suggester instead of editor.
Existing editors keep their roles.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Release v0.2.0 — collaborative ontology editing, suggestion workflow,
graph visualization, keyboard shortcuts, accessibility, and 111 tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Graph nodes showed opaque IRI fragments when resolveLabel() couldn't find
a label. This adds three fallback tiers: tree label hints from in-memory
class tree nodes, a label resolution pass for depth-3+ relationship
targets, and search API lookups by local name for non-class entities
(individuals/properties) that fail getClassDetail.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Trace every displayed class node's ancestry back to root via the ancestors
API so nodes no longer appear floating. Color-code individuals (pink) and
properties (blue) with letter badges, and add a collapsible legend showing
all node and edge types. Filter out owl:Thing from the graph since it adds
no informational value.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Nodes whose only parent is owl:Thing are now classified as root nodes
so they receive the amber visual treatment instead of plain class styling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pass tree-derived label hints through layout components to
IndividualDetailPanel and PropertyDetailPanel, so useIriLabels
can resolve class names without failing API calls.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When the auth provider (Zitadel) is restarting, the error page now
shows an amber spinner with a 10s countdown and retries up to 6 times
instead of dead-ending with a static "Configuration Error" message.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… indicator)

Enables projects to track external GitHub repos for upstream changes.
Includes config CRUD, job polling, notification types, and sync status
indicator in the editor toolbar.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…emantic search

Includes cross-references, similar concepts, entity history, delete impact
analysis, smart parent suggestions, semantic search toggle, health check
consistency/duplicates tabs, project analytics page, and atomization plan.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
JohnRDOrazio and others added 3 commits March 9, 2026 03:35
In suggestion mode, property and individual updates were bypassing the
suggestion branch and committing directly. Add handleSuggestPropertyUpdate
and handleSuggestIndividualUpdate handlers that save via
suggestionSession.saveToSession, matching the class update pattern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add explicit assertion that the focus→parent subClassOf edge is
preserved even when child-cap filtering prunes other siblings.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add test verifying that a node attached as a child in one path is not
duplicated as a root when another path has empty ancestors (failed
ancestor fetch).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@JohnRDOrazio
Copy link
Member

Additional Fixes (Round 2)

Addressed CodeRabbit re-review findings, CodeQL security alert, and CI type-check failure with 16 individual commits:

CI / Security

  1. 8a0c72c — Fix CodeQL alert: use startsWith for URL namespace checks instead of includes to prevent substring bypass (CWE-20)
  2. fdf1328 — Fix CI failure: resolve TS2322 in auth session callback by using Object.assign to preserve AdapterUser fields

CodeRabbit — Critical / Major

  1. 3fc2384 — Use ApiError status codes instead of parsing err.message strings in suggestions review page
  2. 51aff6c — Exit loading state when user is unauthenticated (prevents perpetual spinner)
  3. 8b31c5f — Guard diff loading against stale responses when switching sessions/tabs
  4. 98d14c7 — Surface errors from approve/reject/request-changes action handlers
  5. cfb46ef — Persist edited relationship targets from draft instead of reading stale detail values
  6. db23c12 — Allow adding labels in individual editor with trailing empty row
  7. 1936fcf — Show read-only relationships from detail data, not empty edit state
  8. 197ec48 — Ignore stale autocomplete responses in IriList search
  9. 4c56aae — Ignore stale search responses in both RelationshipSection dropdowns
  10. 8bd05d5 — Derive tree roots from parentage instead of path position (prevents duplicate roots on failed ancestor fetch)
  11. 38f0e4d — Invalidate sourceContent after class deletion to prevent stale edits
  12. 2f2598e — Route property/individual edits through suggestion session in suggestion mode

Tests

  1. ef39e7a — Assert focus node's parent edge survives child-cap filtering
  2. 38bf1e0 — Cover root deduplication when ancestor fetch fails

Verification: 90 tests pass, 0 lint errors, 0 type-check errors.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (2)
components/editor/IndividualDetailPanel.tsx (2)

268-272: ⚠️ Potential issue | 🟠 Major

Keep the trailing empty label row on restore and edit.

The label-row invariant is still broken here: restored drafts set d.labels verbatim, and updateLabel / removeLabel stop reapplying ensureTrailingEmpty. Once the last blank row is filled, or a draft is restored without one, there is no path to add another label.

💡 Suggested fix
-      setEditLabels(d.labels);
+      setEditLabels(ensureTrailingEmpty(d.labels.map((l) => ({ ...l }))));

   const updateLabel = useCallback((index: number, field: "value" | "lang", val: string) => {
-    setEditLabels((prev) => prev.map((l, i) => (i === index ? { ...l, [field]: val } : l)));
+    setEditLabels((prev) =>
+      ensureTrailingEmpty(prev.map((l, i) => (i === index ? { ...l, [field]: val } : l)))
+    );
   }, []);
   const removeLabel = useCallback((index: number) => {
-    setEditLabels((prev) => prev.filter((_, i) => i !== index));
+    setEditLabels((prev) => ensureTrailingEmpty(prev.filter((_, i) => i !== index)));
     requestAnimationFrame(() => triggerSave());
   }, [triggerSave]);

Also applies to: 292-297

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/editor/IndividualDetailPanel.tsx` around lines 268 - 272, Restored
drafts currently assign d.labels verbatim to state, and label mutators
(updateLabel/removeLabel) stop reapplying ensureTrailingEmpty, which breaks the
invariant of keeping a trailing empty label row; update the restore path (where
restoredDraft is cast to IndividualDraftEntry and setEditLabels is called) to
pass labels through ensureTrailingEmpty (e.g.,
setEditLabels(ensureTrailingEmpty(d.labels))) and likewise modify the label
mutation functions (updateLabel and removeLabel) to always call
ensureTrailingEmpty before setting state so the trailing empty label is
preserved after edits and restores.

173-196: ⚠️ Potential issue | 🟠 Major

Don't serialize seeAlso / isDefinedBy twice.

relationshipAnnotations still includes every relationship group, and the same two groups are also sent via seeAlsoIris and isDefinedByIris. That leaves the updater with two sources of truth for the same triples and can duplicate them on save.

💡 Suggested fix
+      const IS_DEFINED_BY_IRI = "http://www.w3.org/2000/01/rdf-schema#isDefinedBy";
+
       const relationshipAnnotations: AnnotationUpdate[] = draft.relationships
-        .filter((g) => g.targets.length > 0)
+        .filter(
+          (g) =>
+            g.targets.length > 0 &&
+            g.property_iri !== SEE_ALSO_IRI &&
+            g.property_iri !== IS_DEFINED_BY_IRI
+        )
         .map((g) => ({
           property_iri: g.property_iri,
           values: g.targets.map((t) => ({ value: t.iri, lang: "" })),
         }));
@@
         seeAlsoIris: draft.relationships
           .filter((g) => g.property_iri === SEE_ALSO_IRI)
           .flatMap((g) => g.targets.map((t) => t.iri)),
         isDefinedByIris: draft.relationships
-          .filter((g) => g.property_iri === "http://www.w3.org/2000/01/rdf-schema#isDefinedBy")
+          .filter((g) => g.property_iri === IS_DEFINED_BY_IRI)
           .flatMap((g) => g.targets.map((t) => t.iri)),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/editor/IndividualDetailPanel.tsx` around lines 173 - 196,
relationshipAnnotations is built from all draft.relationships and ends up
duplicating seeAlso and isDefinedBy because those same groups are also passed
via seeAlsoIris and isDefinedByIris; modify the relationshipAnnotations creation
in IndividualDetailPanel (the relationshipAnnotations variable) to filter out
groups whose property_iri equals SEE_ALSO_IRI or
"http://www.w3.org/2000/01/rdf-schema#isDefinedBy" before mapping, so only
non-seeAlso/non-isDefinedBy relationships are serialized into annotations while
seeAlsoIris and isDefinedByIris remain the sole sources for those predicates.
🧹 Nitpick comments (6)
components/editor/standard/RelationshipSection.tsx (5)

429-442: Add accessible label to the entity search input.

Same accessibility concern as the property picker — this input needs an accessible name for screen readers.

♿ Proposed accessibility fix
       <input
         type="text"
         value={query}
         onChange={(e) => setQuery(e.target.value)}
         onFocus={() => setIsFocused(true)}
         onKeyDown={(e) => {
           if (e.key === "Escape") {
             setIsFocused(false);
             (e.target as HTMLInputElement).blur();
           }
         }}
         placeholder="Search entities to add..."
+        aria-label="Search entities to add"
         className="flex-1 rounded-md border border-dashed border-slate-300 bg-white px-2.5 py-1.5 text-sm placeholder:text-slate-400 focus:border-primary-500 focus:border-solid focus:outline-none focus:ring-1 focus:ring-primary-500 dark:border-slate-600 dark:bg-slate-700 dark:text-white dark:placeholder:text-slate-500"
       />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/editor/standard/RelationshipSection.tsx` around lines 429 - 442,
The entity search input in RelationshipSection lacks an accessible name; add one
by providing an aria-label or linking a visible/visually-hidden <label> to the
input (use the input's id) so screen readers announce it. Locate the input
element using the RelationshipSection component and the variables/handlers
query, setQuery and setIsFocused, then add a descriptive aria-label (e.g.,
"Search entities to add") or an associated label element and ensure focus/escape
behavior remains unchanged.

257-263: Improve dropdown button accessibility.

The property picker button lacks ARIA attributes to communicate dropdown state to assistive technologies.

♿ Proposed accessibility fix
       <button
         onClick={() => setIsOpen(!isOpen)}
+        aria-haspopup="listbox"
+        aria-expanded={isOpen}
         className="flex w-full items-center gap-1 rounded-md border border-slate-300 bg-white px-2 py-1 text-left text-xs font-medium text-slate-700 hover:border-slate-400 dark:border-slate-600 dark:bg-slate-700 dark:text-slate-300 dark:hover:border-slate-500"
       >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/editor/standard/RelationshipSection.tsx` around lines 257 - 263,
The dropdown toggle button lacks accessibility attributes and should announce
its role and state: update the button (the element using onClick={() =>
setIsOpen(!isOpen)} and rendering currentLabel and <ChevronDown />) to include
type="button", aria-haspopup="listbox" (or "popup" if the panel is a menu),
aria-expanded={isOpen}, and aria-controls pointing to the id of the dropdown
panel; ensure the dropdown panel element has a matching id and appropriate role
(e.g., role="listbox" or role="menu") so assistive tech can associate the button
with the controlled popup.

240-241: Consider logging search errors for observability.

The empty catch blocks silently swallow API errors. While the UI gracefully falls back to empty results, logging these errors would help with debugging in production.

🔍 Example improvement
-      } catch {
-        if (!cancelled) setApiResults([]);
+      } catch (error) {
+        console.error("Property search failed:", error);
+        if (!cancelled) setApiResults([]);

Also applies to: 396-397

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/editor/standard/RelationshipSection.tsx` around lines 240 - 241,
The catch blocks currently swallow errors; modify both catch clauses (the one
surrounding setApiResults([]) at the try/catch near setApiResults and the
similar block around lines 396-397) to accept an error parameter (e.g., catch
(err)) and log that error for observability before falling back to
setApiResults([]) — use the app's logger if available or console.error with a
short contextual message mentioning RelationshipSection and the operation, and
preserve the cancelled check logic.

269-279: Add accessible label to the search input.

The search input lacks an accessible name. Screen readers won't convey its purpose to users.

♿ Proposed accessibility fix
             <input
               ref={inputRef}
               type="text"
               value={query}
               onChange={(e) => setQuery(e.target.value)}
               onKeyDown={(e) => {
                 if (e.key === "Escape") setIsOpen(false);
               }}
               placeholder="Search properties..."
+              aria-label="Search properties"
               className="flex-1 bg-transparent text-xs text-slate-900 placeholder:text-slate-400 focus:outline-none dark:text-white dark:placeholder:text-slate-500"
             />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/editor/standard/RelationshipSection.tsx` around lines 269 - 279,
The search input in RelationshipSection.tsx (the input element using ref
inputRef, value={query}, onChange={setQuery}) has no accessible name; add an
accessible label by either giving the input an id and associating a <label
htmlFor="..."> (visually hidden if needed) or adding an
aria-label/aria-labelledby attribute such as aria-label="Search properties" so
screen readers can announce its purpose; ensure you update the input element
where setQuery and setIsOpen are used to include the chosen accessible label.

434-439: Consider adding keyboard navigation for dropdown results.

Both dropdowns support Escape to close but lack arrow key navigation (Up/Down) and Enter to select. This would improve keyboard accessibility for users who can't use a mouse.

Also applies to: 274-276

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/editor/standard/RelationshipSection.tsx` around lines 434 - 439,
The onKeyDown handler currently only handles Escape (setting setIsFocused(false)
and blurring the input) and needs to support ArrowUp/ArrowDown and Enter to
enable keyboard navigation: add a highlightedIndex state (e.g.,
highlightedIndex, setHighlightedIndex) used by the dropdown render to highlight
items, update the two onKeyDown handlers (the one shown and the similar handler
around lines 274-276) to intercept "ArrowDown"/"ArrowUp" to increment/decrement
highlightedIndex (with bounds or wrap-around) and call e.preventDefault(), and
handle "Enter" to select the item at highlightedIndex (invoke the existing
selection callback used by click handlers) then close the dropdown
(setIsFocused(false)) and blur the input; ensure the highlightedIndex resets
when the dropdown opens or the results list changes.
components/editor/standard/PropertyTree.tsx (1)

59-84: Property type classification relies on unreliable IRI pattern heuristics instead of API-provided RDF types.

The code infers property types (Object, Data, Annotation) from IRI substrings (e.g., iri.includes("objectproperty")). However, EntitySearchResult from the API provides only a generic entity_type: "property" field—no subtype information exists. This means properties with standard IRIs like http://purl.org/dc/... will incorrectly default to "Object Properties" regardless of their actual RDF type (lines 80-82).

The codebase defines proper OWL property types elsewhere (see turtleSnippetGenerator.ts). The backend API should be enhanced to return the actual RDF property type in the search response, or this limitation should be clearly documented for users expecting correct categorization.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/editor/standard/PropertyTree.tsx` around lines 59 - 84, Update the
classification in PropertyTree.tsx to stop relying solely on IRI substring
heuristics and instead use the API-provided RDF property type (add/expect a
field on EntitySearchResult such as rdfType, rdf_type or propertyType). In the
loop over response.results, switch on prop.rdfType values (e.g.,
"owl:ObjectProperty", "owl:DatatypeProperty", "owl:AnnotationProperty") to push
into objectProps, dataProps, or annotationProps; if that field is missing, fall
back to the existing IRI heuristics but emit a console.warn or comment
documenting the limitation and referencing EntitySearchResult so callers/backend
can be updated to return the RDF type. Ensure variable names objectProps,
dataProps, annotationProps and the response.results loop are the only changed
locations.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@__tests__/lib/graph/buildGraphData.test.ts`:
- Around line 232-242: The test builds a Map called resolved with the focusIri
entry inserted before childDetails which makes the pruning assertion pass due to
Map insertion order; change the construction so the focus entry is added after
the spread of childDetails (or otherwise randomize/order differently) so that
resolved = new Map([...childDetails, [focusIri, makeClassDetail({...})]])
ensures the cap-pruning test for subClassOf edges verifies that the focus edge
is preserved intentionally; update references to resolved, focusIri,
childDetails, and makeClassDetail accordingly.

In `@app/projects/`[id]/editor/page.tsx:
- Around line 879-887: When switching branches in handleBranchChange, also
discard the active suggestion session to avoid carrying stale session state;
call suggestionSession.discardSession() (or the appropriate discard method on
the suggestionSession object) before clearing source state and updating router,
and reset any session-related state like changesCount and entitiesModified (or
their setters) and beacon/context references so the new branch starts with a
fresh session context; update handleBranchChange to invoke the discard call and
session-state resets alongside setActiveBranch, setSourceContent,
setSourceError, setSourceIriIndex, and preloadStartedRef.current assignment.

In `@components/editor/IndividualDetailPanel.tsx`:
- Around line 240-248: The current useEffect in IndividualDetailPanel.tsx calls
flushToGit after individualIri has already changed, causing flushToGit (from
useEntityAutoSave.ts) to key the draft by the new IRI; fix by invoking the flush
for the previous entity before the prop switch: either move the flushToGit()
call into the cleanup function of that effect so it runs while
prevIriRef.current still refers to the old individual, or update flushToGit to
accept an explicit IRI/snapshot and call flushToGit(prevIriRef.current) before
updating prevIriRef/current state (ensure you still set
editInitializedRef.current = false, setIsEditing(false), and
cancelledIriRef.current = null after flushing).

---

Duplicate comments:
In `@components/editor/IndividualDetailPanel.tsx`:
- Around line 268-272: Restored drafts currently assign d.labels verbatim to
state, and label mutators (updateLabel/removeLabel) stop reapplying
ensureTrailingEmpty, which breaks the invariant of keeping a trailing empty
label row; update the restore path (where restoredDraft is cast to
IndividualDraftEntry and setEditLabels is called) to pass labels through
ensureTrailingEmpty (e.g., setEditLabels(ensureTrailingEmpty(d.labels))) and
likewise modify the label mutation functions (updateLabel and removeLabel) to
always call ensureTrailingEmpty before setting state so the trailing empty label
is preserved after edits and restores.
- Around line 173-196: relationshipAnnotations is built from all
draft.relationships and ends up duplicating seeAlso and isDefinedBy because
those same groups are also passed via seeAlsoIris and isDefinedByIris; modify
the relationshipAnnotations creation in IndividualDetailPanel (the
relationshipAnnotations variable) to filter out groups whose property_iri equals
SEE_ALSO_IRI or "http://www.w3.org/2000/01/rdf-schema#isDefinedBy" before
mapping, so only non-seeAlso/non-isDefinedBy relationships are serialized into
annotations while seeAlsoIris and isDefinedByIris remain the sole sources for
those predicates.

---

Nitpick comments:
In `@components/editor/standard/PropertyTree.tsx`:
- Around line 59-84: Update the classification in PropertyTree.tsx to stop
relying solely on IRI substring heuristics and instead use the API-provided RDF
property type (add/expect a field on EntitySearchResult such as rdfType,
rdf_type or propertyType). In the loop over response.results, switch on
prop.rdfType values (e.g., "owl:ObjectProperty", "owl:DatatypeProperty",
"owl:AnnotationProperty") to push into objectProps, dataProps, or
annotationProps; if that field is missing, fall back to the existing IRI
heuristics but emit a console.warn or comment documenting the limitation and
referencing EntitySearchResult so callers/backend can be updated to return the
RDF type. Ensure variable names objectProps, dataProps, annotationProps and the
response.results loop are the only changed locations.

In `@components/editor/standard/RelationshipSection.tsx`:
- Around line 429-442: The entity search input in RelationshipSection lacks an
accessible name; add one by providing an aria-label or linking a
visible/visually-hidden <label> to the input (use the input's id) so screen
readers announce it. Locate the input element using the RelationshipSection
component and the variables/handlers query, setQuery and setIsFocused, then add
a descriptive aria-label (e.g., "Search entities to add") or an associated label
element and ensure focus/escape behavior remains unchanged.
- Around line 257-263: The dropdown toggle button lacks accessibility attributes
and should announce its role and state: update the button (the element using
onClick={() => setIsOpen(!isOpen)} and rendering currentLabel and <ChevronDown
/>) to include type="button", aria-haspopup="listbox" (or "popup" if the panel
is a menu), aria-expanded={isOpen}, and aria-controls pointing to the id of the
dropdown panel; ensure the dropdown panel element has a matching id and
appropriate role (e.g., role="listbox" or role="menu") so assistive tech can
associate the button with the controlled popup.
- Around line 240-241: The catch blocks currently swallow errors; modify both
catch clauses (the one surrounding setApiResults([]) at the try/catch near
setApiResults and the similar block around lines 396-397) to accept an error
parameter (e.g., catch (err)) and log that error for observability before
falling back to setApiResults([]) — use the app's logger if available or
console.error with a short contextual message mentioning RelationshipSection and
the operation, and preserve the cancelled check logic.
- Around line 269-279: The search input in RelationshipSection.tsx (the input
element using ref inputRef, value={query}, onChange={setQuery}) has no
accessible name; add an accessible label by either giving the input an id and
associating a <label htmlFor="..."> (visually hidden if needed) or adding an
aria-label/aria-labelledby attribute such as aria-label="Search properties" so
screen readers can announce its purpose; ensure you update the input element
where setQuery and setIsOpen are used to include the chosen accessible label.
- Around line 434-439: The onKeyDown handler currently only handles Escape
(setting setIsFocused(false) and blurring the input) and needs to support
ArrowUp/ArrowDown and Enter to enable keyboard navigation: add a
highlightedIndex state (e.g., highlightedIndex, setHighlightedIndex) used by the
dropdown render to highlight items, update the two onKeyDown handlers (the one
shown and the similar handler around lines 274-276) to intercept
"ArrowDown"/"ArrowUp" to increment/decrement highlightedIndex (with bounds or
wrap-around) and call e.preventDefault(), and handle "Enter" to select the item
at highlightedIndex (invoke the existing selection callback used by click
handlers) then close the dropdown (setIsFocused(false)) and blur the input;
ensure the highlightedIndex resets when the dropdown opens or the results list
changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 16040f06-e982-413e-83cf-1ad1933cfc81

📥 Commits

Reviewing files that changed from the base of the PR and between 1d212c5 and 38bf1e0.

📒 Files selected for processing (9)
  • __tests__/lib/graph/buildGraphData.test.ts
  • __tests__/lib/hooks/useFilteredTree.test.ts
  • app/projects/[id]/editor/page.tsx
  • app/projects/[id]/suggestions/review/page.tsx
  • auth.ts
  • components/editor/IndividualDetailPanel.tsx
  • components/editor/standard/PropertyTree.tsx
  • components/editor/standard/RelationshipSection.tsx
  • lib/hooks/useFilteredTree.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/projects/[id]/suggestions/review/page.tsx
  • tests/lib/hooks/useFilteredTree.test.ts
  • auth.ts

JohnRDOrazio and others added 3 commits March 9, 2026 03:55
The child-cap filter dropped the focus node's subClassOf edge when it
was processed after 20 siblings. Always keep edges involving the focus
node so it's never pruned from the graph. Updated the test to insert
the focus node after siblings, verifying the fix is order-independent.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
handleBranchChange now calls suggestionSession.discardSession() so
that stale session state (sessionId, changesCount, entitiesModified)
doesn't persist across branch switches.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move flushToGit into a cleanup function so it runs with the previous
entity's closure before React switches to the new individualIri.
This prevents the old entity's draft from being keyed by the new
IRI and ensures the previous entity's edits are committed correctly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@JohnRDOrazio
Copy link
Member

Additional Fixes (Round 3)

Addressed 3 CodeRabbit re-review findings with individual commits:

Bug Fixes

  1. 7d0a8dd — Fix: preserve focus node's edges during child-cap filtering. The graph's child-cap filter dropped the focus node's subClassOf edge when it was processed after 20 siblings. Now the focus node's edges are always kept. Also fixed the test to insert the focus node after siblings, proving the fix is order-independent.
  2. 85b95ed — Fix: discard suggestion session when switching branches. handleBranchChange now calls suggestionSession.discardSession() so stale session state doesn't persist across branch switches.
  3. 1ccf301 — Fix: flush previous entity before individual IRI switch. Moved flushToGit into a cleanup function so it runs with the previous entity's closure, preventing the old entity's draft from being keyed by the new IRI.

Verification: 90 tests pass, 0 lint errors, 0 type-check errors.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
components/editor/IndividualDetailPanel.tsx (1)

297-303: ⚠️ Potential issue | 🟡 Minor

Label editor doesn't maintain trailing empty row during edits.

updateLabel doesn't call ensureTrailingEmpty, unlike updateComment and updateDefinition. Once the user types in the last label row, no new empty row is added, preventing additional labels from being created.

🐛 Proposed fix
 const updateLabel = useCallback((index: number, field: "value" | "lang", val: string) => {
-  setEditLabels((prev) => prev.map((l, i) => (i === index ? { ...l, [field]: val } : l)));
+  setEditLabels((prev) => ensureTrailingEmpty(prev.map((l, i) => (i === index ? { ...l, [field]: val } : l))));
 }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/editor/IndividualDetailPanel.tsx` around lines 297 - 303,
updateLabel currently updates setEditLabels without ensuring a trailing empty
row, so typing into the last label won't create a new empty row; mirror the
pattern used by updateComment/updateDefinition by invoking ensureTrailingEmpty
when labels change—either call ensureTrailingEmpty inside the setEditLabels
updater (after computing the new labels) or call it in a requestAnimationFrame
callback just like removeLabel does; reference updateLabel, ensureTrailingEmpty,
setEditLabels, and triggerSave when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@components/editor/IndividualDetailPanel.tsx`:
- Around line 297-303: updateLabel currently updates setEditLabels without
ensuring a trailing empty row, so typing into the last label won't create a new
empty row; mirror the pattern used by updateComment/updateDefinition by invoking
ensureTrailingEmpty when labels change—either call ensureTrailingEmpty inside
the setEditLabels updater (after computing the new labels) or call it in a
requestAnimationFrame callback just like removeLabel does; reference
updateLabel, ensureTrailingEmpty, setEditLabels, and triggerSave when making the
change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3b4dd340-dec7-4423-8fba-89c693ddc06d

📥 Commits

Reviewing files that changed from the base of the PR and between 38bf1e0 and 1ccf301.

📒 Files selected for processing (4)
  • __tests__/lib/graph/buildGraphData.test.ts
  • app/projects/[id]/editor/page.tsx
  • components/editor/IndividualDetailPanel.tsx
  • lib/graph/buildGraphData.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • lib/graph/buildGraphData.ts
  • tests/lib/graph/buildGraphData.test.ts

Wrap updateLabel's setEditLabels with ensureTrailingEmpty so typing
into the last label row creates a new empty row, matching the
pattern used by updateComment and updateDefinition.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@JohnRDOrazio
Copy link
Member

Additional Fix (Round 4)

  1. 3c228fe — Fix: ensure trailing empty row when editing labels. Wrapped updateLabel's setEditLabels with ensureTrailingEmpty so typing into the last label row creates a new empty row, matching the pattern used by updateComment and updateDefinition.

Verification: 90 tests pass, 0 lint errors, 0 type-check errors.

JohnRDOrazio and others added 13 commits March 9, 2026 05:26
The startup script had a non-standard default of 53000 while all
other configuration (Zitadel redirect URIs, AUTH_URL, docs) assumed
port 3000.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add comprehensive v0.2.0 changelog entry with new features,
improvements, accessibility, testing, and bug fix categories.
Fix v0.1.0 date from 2025-06-15 to 2026-02-18 (verified via git tag).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add suggestion workflow, auto-save, editor modes, graph visualization,
keyboard shortcuts, and suggester role to the docs page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Scalar uses body.dark-mode/light-mode classes internally and its
forceDarkModeState prop is not reactive after mount. Use a
MutationObserver to mirror the html.dark class onto body classes,
and hide Scalar's own toggle to avoid conflicts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use index-suffixed keys to handle OWL multiple inheritance where the
same IRI can appear under different parent nodes in the tree.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds missing @testing-library/dom and related transitive dependencies
that were causing npm ci to fail in CI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Poll the embedding status endpoint every 2 seconds while a generation
job is in progress, so the entities-embedded count and progress bar
update live. Also resumes polling on page load if a job is already running.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When stdin is not a terminal (piped, redirected, or run from a script),
automatically enable force mode to avoid infinite interactive prompts
on port conflicts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…onfig

Pre-populates repo owner, name, branch, and file path from active GitHub
integration when no upstream sync config exists. Also updates the hook to
handle 200/null responses instead of catching 404 errors.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add github_hook_id to GitHubIntegration type and setupWebhook API method
- Add webhook frequency option to upstream sync types
- Rearrange GitHub integration info boxes in project settings
- Enhance WebhookConfigPanel with auto-setup status and fallback to manual
- Add upstream sync info box when webhooks are enabled
- Add admin:repo_hook scope guidance in user settings page

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When webhooks are enabled and upstream sync is webhook-driven, repo
fields are read-only with dimmed styling and an explanatory note is
shown in both configured and editing views.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@JohnRDOrazio
Copy link
Member

@damienriehl I added CodeRabbit which I have been using on a few other projects, it does a wonderful job of reviewing and nit-picking. I then feed CodeRabbit's review issues back into Claude Code. I find that it is best to address each issue with a commit but specifically instruct Claude not to push until all CodeRabbit issues have been addressed, because CodeRabbit triggers a new review every time you push but it also implements rate-limiting, so with many pushes it'll just quit. So best to make all the commits first, then push them all at once when they're ready. I either copy the "Prompt for AI Agents" section for each issue, one at a time; or, at the risk of perhaps consuming a few more tokens but speeding up the process, I tell Claude to directly read all the issues raised by CodeRabbit's review on the PR (I have the GitHub CLI installed, which Claude uses to read the PR), but again instructing specifically to address each issue with one commit per fix but not push until all have been addressed.

So now, linting and tests are passing!

@JohnRDOrazio
Copy link
Member

I added a webhooks UI to the GitHub integration, which should be a valid alternative to "Upstream Source Tracking". The idea is that as soon as webhooks are enabled and installed, "Upstream Source Tracking" will be readonly; if webhooks are not enabled, "Upstream Source Tracking" can be set but will default to the same GitHub repo when GitHub integration is active.

@JohnRDOrazio JohnRDOrazio merged commit cc1c707 into main Mar 9, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants