Skip to content

Speed up corpus home document index by trimming GraphQL over-fetch#1647

Open
JSv4 wants to merge 3 commits into
mainfrom
claude/optimize-corpus-loading-Z84HA
Open

Speed up corpus home document index by trimming GraphQL over-fetch#1647
JSv4 wants to merge 3 commits into
mainfrom
claude/optimize-corpus-loading-Z84HA

Conversation

@JSv4
Copy link
Copy Markdown
Collaborator

@JSv4 JSv4 commented May 14, 2026

The "Loading document structure..." spinner on Corpus Home was driven by
two GraphQL queries that requested far more data than the TOC renders.

  1. GET_DOCUMENT_RELATIONSHIPS (cap: 500 rows) selected full source/target
    document objects, corpus, creator, annotation label color/icon, data,
    timestamps, and myPermissions on every row. DocumentTableOfContents
    actually consumes only relationshipType, annotationLabel.text, and the
    two document IDs -- the rest was discarded. The per-relationship icon
    fields still triggered build_absolute_uri() calls in
    create_file_resolver, and "parent" filtering happened entirely on the
    client.

  2. GET_CORPUS_DOCUMENTS_FOR_TOC selected icon (unused -- renderNode
    derives the on-screen icon from fileType via getFileIcon) and
    creator (also unused), adding a file-URL resolver call per document
    plus an extra join.

Fixes:

  • Add annotation_label_text iexact filter to DocumentRelationshipFilter
    so the parent-label filter runs server-side alongside the existing
    relationship_type filter.
  • Introduce a new ultra-lean GET_CORPUS_DOCUMENT_TOC_EDGES query that
    returns only { id, sourceDocument.id, targetDocument.id } and lives
    alongside (does not replace) GET_DOCUMENT_RELATIONSHIPS -- the latter
    is still consumed by CorpusDocumentRelationships and
    DocumentRelationshipModal with its full payload.
  • Slim GET_CORPUS_DOCUMENTS_FOR_TOC to { id, title, description, slug,
    fileType }; RunCorpusActionModal (the only other consumer) only reads
    id and title.
  • Rewire DocumentTableOfContents to consume the new edges query, drop
    redundant document-metadata overwrites from relationship rows, and
    remove now-dead icon plumbing.

The "Loading document structure..." spinner on Corpus Home was driven by
two GraphQL queries that requested far more data than the TOC renders.

1) GET_DOCUMENT_RELATIONSHIPS (cap: 500 rows) selected full source/target
   document objects, corpus, creator, annotation label color/icon, data,
   timestamps, and myPermissions on every row. DocumentTableOfContents
   actually consumes only relationshipType, annotationLabel.text, and the
   two document IDs -- the rest was discarded. The per-relationship icon
   fields still triggered build_absolute_uri() calls in
   create_file_resolver, and "parent" filtering happened entirely on the
   client.

2) GET_CORPUS_DOCUMENTS_FOR_TOC selected icon (unused -- renderNode
   derives the on-screen icon from fileType via getFileIcon) and
   creator (also unused), adding a file-URL resolver call per document
   plus an extra join.

Fixes:
- Add annotation_label_text iexact filter to DocumentRelationshipFilter
  so the parent-label filter runs server-side alongside the existing
  relationship_type filter.
- Introduce a new ultra-lean GET_CORPUS_DOCUMENT_TOC_EDGES query that
  returns only { id, sourceDocument.id, targetDocument.id } and lives
  alongside (does not replace) GET_DOCUMENT_RELATIONSHIPS -- the latter
  is still consumed by CorpusDocumentRelationships and
  DocumentRelationshipModal with its full payload.
- Slim GET_CORPUS_DOCUMENTS_FOR_TOC to { id, title, description, slug,
  fileType }; RunCorpusActionModal (the only other consumer) only reads
  id and title.
- Rewire DocumentTableOfContents to consume the new edges query, drop
  redundant document-metadata overwrites from relationship rows, and
  remove now-dead icon plumbing.
@claude
Copy link
Copy Markdown

claude Bot commented May 14, 2026

test comment

@claude
Copy link
Copy Markdown

claude Bot commented May 14, 2026

Code Review — PR 1647: Speed up corpus home document index

@claude
Copy link
Copy Markdown

claude Bot commented May 14, 2026

PLACEHOLDER_REVIEW_2

@claude
Copy link
Copy Markdown

claude Bot commented May 14, 2026

Part 1 of review - Testing single line with special chars: backtick test code and hash test

@claude
Copy link
Copy Markdown

claude Bot commented May 14, 2026

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

OpenContracts is an MIT-licensed enterprise document analytics platform for PDFs and text-based formats. It features a Django/GraphQL backend with PostgreSQL + pgvector, a React/TypeScript frontend with Jotai state management, and pluggable document processing pipelines powered by machine learning models.

Baseline Commit Rules

  1. Always ensure all affected (or new) tests pass - backend tests suite should only be run in its entirety for good reason as it takes 30+ minutes.
  2. Always make sure typescript compiles and pre-commits pass before committing new code.
  3. Never credit Claude or Claude Code in commit messages, PR messages, comments, or any other artifacts. This includes Co-Authored-By lines, "Generated by Claude", etc.

Essential Commands

Backend (Django)

# Run backend tests (sequential, use --keepdb to speed up subsequent runs)
docker compose -f test.yml run django python manage.py test --keepdb

# Run backend tests in PARALLEL (recommended - ~4x faster)
# Uses pytest-xdist with 4 workers, --dist loadscope keeps class tests together
docker compose -f test.yml run django pytest -n 4 --dist loadscope

# Run parallel tests with auto-detected worker count (uses all CPU cores)
docker compose -f test.yml run django pytest -n auto --dist loadscope

# Run parallel tests with fresh databases (first run or after schema changes)
docker compose -f test.yml run django pytest -n 4 --dist loadscope --create-db

# Run specific test file
docker compose -f test.yml run django python manage.py test opencontractserver.tests.test_notifications --keepdb

# Run specific test file in parallel
docker compose -f test.yml run django pytest opencontractserver/tests/test_notifications.py -n 4 --dist loadscope

# Run specific test class/method
docker compose -f test.yml run django python manage.py test opencontractserver.tests.test_notifications.TestNotificationModel.test_create_notification --keepdb

# Apply database migrations
docker compose -f local.yml run django python manage.py migrate

# Create new migration
docker compose -f local.yml run django python manage.py makemigrations

# Django shell
docker compose -f local.yml run django python manage.py shell

# Code quality (runs automatically via pre-commit hooks)
pre-commit run --all-files

Frontend (React/TypeScript)

cd frontend

# Start development server (proxies to Django on :8000)
yarn start

# Run unit tests (Vitest) - watches by default
yarn test:unit

# Run component tests (Playwright) - CRITICAL: Use --reporter=list to prevent hanging
yarn test:ct --reporter=list

# Run component tests with grep filter
yarn test:ct --reporter=list -g "test name pattern"

# Run E2E tests
yarn test:e2e

# Coverage reports (unit tests via Vitest, component tests via Playwright + Istanbul)
yarn test:coverage:unit
yarn test:coverage:ct

# Linting and formatting
yarn lint
yarn fix-styles

# Build for production
yarn build

# Preview production build locally
yarn serve

Production Deployment

# CRITICAL: Always run migrations FIRST in production
docker compose -f production.yml --profile migrate up migrate

# Then start main services
docker compose -f production.yml up

High-Level Architecture

Backend Architecture

Stack: Django 4.x + GraphQL (Graphene) + PostgreSQL + pgvector + Celery

Key Patterns:

  1. GraphQL Schema Organization:

    • config/graphql/graphene_types.py - All GraphQL type definitions
    • config/graphql/queries.py - Query resolvers
    • config/graphql/*_mutations.py - Mutation files (organized by feature)
    • config/graphql/schema.py - Schema composition
  2. Permission System (CRITICAL - see docs/permissioning/consolidated_permissioning_guide.md):

    • Annotations & Relationships: NO individual permissions - inherited from document + corpus
    • Documents & Corpuses: Direct object-level permissions via django-guardian
    • Analyses & Extracts: Hybrid model (own permissions + corpus permissions + document filtering)
    • Formula: Effective Permission = MIN(document_permission, corpus_permission)
    • Structural items are ALWAYS read-only except for superusers
    • Use Model.objects.visible_to_user(user) pattern (NOT resolve_oc_model_queryset - DEPRECATED)
  3. AnnotatePermissionsForReadMixin:

    • Most GraphQL types inherit this mixin (see config/graphql/permissioning/permission_annotator/mixins.py)
    • Adds my_permissions, is_published, object_shared_with fields
    • Requires model to have guardian permission tables ({model}userobjectpermission_set)
    • Notifications use simple ownership model and DON'T use this mixin
  4. Django Signal Handlers:

    • Automatic notification creation on model changes (see opencontractserver/notifications/signals.py)
    • Must be imported in app's apps.py ready() method
    • Use _skip_signals attribute to prevent duplicate notifications in tests
  5. Pluggable Parser Pipeline:

    • Base classes in opencontractserver/pipeline/base/
    • Parsers, embedders, thumbnailers auto-discovered and registered
    • Multiple backends: Docling (ML-based), LlamaParse, Text
    • All convert to unified PAWLs format for frontend
  6. Agent Tool Architecture (see docs/architecture/llms/README.md):

    • CoreTool (framework-agnostic) → PydanticAIToolWrapper (pydantic-ai specific)
    • All production tools MUST be async (a-prefixed in core_tools.py). The wrapper supports sync functions for lightweight helpers/tests but does NOT wrap them in a thread pool — sync ORM calls will raise SynchronousOnlyOperation.
    • Tool fault tolerance (issue Agent Tools Not Fault Tolerant #820): operational exceptions are caught and returned as error strings to the LLM; security exceptions (PermissionError, ToolConfirmationRequired) propagate.
    • Pre-execution checks run on every call (not cached): permission validation, resource ID validation, approval gates.

Frontend Architecture

Stack: React 18 + TypeScript + Apollo Client + Jotai (atoms) + PDF.js + Vite

Key Patterns:

  1. State Management - Jotai Atoms:

    • Global state via atoms in frontend/src/atoms/ (NOT Redux/Context)
    • Key atoms: selectedFolderIdAtom, folderCorpusIdAtom, selectedMessageIdAtom
    • Derived atoms automatically update when dependencies change
    • Apollo reactive vars in frontend/src/graphql/cache.ts for UI state
    • AuthGate pattern ensures auth completes before rendering
  2. Central Routing System (see docs/frontend/routing_system.md):

    • Single source of truth: frontend/src/routing/CentralRouteManager.tsx
    • URL paths → Entity resolution via GraphQL slug queries
    • URL params ↔ Reactive vars (bidirectional sync)
    • Components consume state via reactive vars, never touch URLs directly
    • Deep linking and canonical redirects handled automatically
  3. PDF Annotation System (see .cursor/rules/pdf-viewer-and-annotator-architecture.mdc):

    • Virtualized rendering: Only visible pages (+overscan) rendered for performance
    • Binary search to find visible page range (O(log n))
    • Height caching per zoom level
    • Two-phase scroll-to-annotation system
    • Dual-layer architecture: Document layer (annotations) + Knowledge layer (summaries)
  4. Unified Filtering Architecture:

    • useVisibleAnnotations hook provides single-source-of-truth filtering for both annotations and relationship-connected annotations
    • Reads from Jotai atoms via useAnnotationDisplay, useAnnotationControls, and useAnnotationSelection (from UISettingsAtom)
    • Ensures consistency across all components
    • Forced visibility for selected items and their relationship connections
  5. Component Testing (see .cursor/rules/test-document-knowledge-base.mdc):

    • ALWAYS mount components through test wrappers (e.g., DocumentKnowledgeBaseTestWrapper)
    • Wrapper provides: MockedProvider + InMemoryCache + Jotai Provider + asset mocking
    • Use --reporter=list flag to prevent hanging
    • Increase timeouts (20s+) for PDF rendering in Chromium
    • GraphQL mocks must match variables EXACTLY (null vs undefined matters)
    • Mock same query multiple times for refetches
    • Use page.mouse for PDF canvas interactions (NOT locator.dragTo)
    • Add settle time after drag operations (500ms UI, 1000ms Apollo cache)
  6. Development Server Configuration:

    • Vite dev server on :3000 proxies to Django on :8000
    • WebSocket proxy for /wsws://localhost:8000
    • GraphQL proxy for /graphqlhttp://localhost:8000
    • REST API proxy for /apihttp://localhost:8000
    • Auth0 optional via REACT_APP_USE_AUTH0 environment variable

Data Flow Architecture

Document Processing:

  1. Upload → Parser Selection (Docling/LlamaParse/Text)
  2. Parser generates PAWLs JSON (tokens with bounding boxes)
  3. Text layer extracted from PAWLs
  4. Annotations created for structure (headers, sections, etc.)
  5. Relationships detected between elements
  6. Vector embeddings generated for search

GraphQL Permission Flow:

  1. Query resolver filters objects with .visible_to_user(user)
  2. GraphQL types resolve my_permissions via AnnotatePermissionsForReadMixin
  3. Frontend uses permissions to enable/disable UI features
  4. Mutations check permissions and return consistent errors to prevent IDOR

Critical Security Patterns

  1. IDOR Prevention:

    • Query by both ID AND user-owned field: Model.objects.get(pk=pk, recipient=user)
    • Return same error message whether object doesn't exist or belongs to another user
    • Prevents enumeration via timing or different error messages
  2. Permission Checks:

    • NEVER trust frontend - always check server-side
    • Use visible_to_user() manager method for querysets
    • Check user_has_permission_for_obj() for individual objects (in opencontractserver.utils.permissioning)
  3. XSS Prevention:

    • User-generated content in JSON fields must be escaped on frontend
    • GraphQL's GenericScalar handles JSON serialization safely
    • Document this requirement in resolver comments

Critical Concepts

  1. No dead code - when deprecating or replacing code, always try to fully replace older code and, once it's no longer in use, delete it and related texts.
  2. DRY - please always architect code for maximal dryness and always see if you can consolidate related code and remove duplicative code.
  3. Single Responsibility Principle - Generally, ensure that each module / script has a single purpose or related purpose.
  4. No magic numbers - we have constants files in opencontractserver/constants/ (backend) and frontend/src/assets/configurations/constants.ts (frontend). Use them for any hardcoded values.
  5. Don't touch old tests without permission - if pre-existing tests fail after changes, try to identify why and present user with root cause analysis. If the test logic is correct but expectations need updating due to intentional behavior changes, document the change clearly.
  6. Utility functions belong in utility files:
    • Before writing new utilities: Check existing utility files first (frontend/src/utils/, opencontractserver/utils/)
    • When reviewing code: If you find utility functions defined inline in components/views, check if they already exist in utility files. If not, consider whether they should be extracted there for reuse.
    • Frontend utilities: frontend/src/utils/formatters.ts (formatting), frontend/src/utils/files.ts (file operations), etc.
    • Backend utilities: opencontractserver/utils/ contains permissioning, PDF processing, and other shared utilities

Testing Patterns

Manual Test Scripts

Location: docs/test_scripts/

When performing manual testing (e.g., testing migrations, verifying database state, testing API endpoints interactively), always document the test steps in a markdown file under docs/test_scripts/. These scripts will later be used to build automated integration tests.

Format:

# Test: [Brief description]

## Purpose
What this test verifies.

## Prerequisites
- Required state (e.g., "migration at 0058")
- Required data (e.g., "at least one document exists")

## Steps
1. Step one with exact command
   ```bash
   docker compose -f local.yml run --rm django python manage.py shell -c "..."
  1. Step two...

Expected Results

  • What should happen after each step
  • Success criteria

Cleanup

Commands to restore original state if needed.


**When to document**:
- Migration testing (rollback, create test data, migrate forward)
- Database constraint validation
- Race condition verification
- Any manual verification requested during code review

### Backend Tests

**Location**: `opencontractserver/tests/`

**Parallel Testing** (pytest-xdist):
- Run with `-n 4` (or `-n auto`) for parallel execution across workers
- Use `--dist loadscope` to keep tests from the same class on the same worker (respects `setUpClass`)
- Each worker gets its own database (test_db_gw0, test_db_gw1, etc.)
- Use `@pytest.mark.serial` to mark tests that cannot run in parallel
- First run or after DB schema changes: add `--create-db` flag

**Patterns**:
- Use `TransactionTestCase` for tests with signals/asynchronous behavior
- Use `TestCase` for faster tests without transaction isolation
- Clear auto-created notifications when testing moderation: `Notification.objects.filter(recipient=user).delete()`
- Use `_skip_signals` attribute on instances to prevent signal handlers during fixtures

### Frontend Component Tests

**Location**: `frontend/tests/`

**Critical Requirements**:
- Mount through test wrappers that provide all required context
- GraphQL mocks must match query variables exactly
- Include mocks for empty-string variants (unexpected boot calls)
- Wait for visible evidence, not just network-idle
- Use `page.mouse` for PDF canvas interactions (NOT `locator.dragTo`)
- Add settle time after drag operations (500ms UI, 1000ms Apollo cache)

**Test Wrapper Pattern**:
```typescript
// ALWAYS use wrappers, never mount components directly
const component = await mount(
  <DocumentKnowledgeBaseTestWrapper corpusId="corpus-1" documentId="doc-1">
    <DocumentKnowledgeBase />
  </DocumentKnowledgeBaseTestWrapper>
);

Automated Documentation Screenshots

Location: docs/assets/images/screenshots/auto/ (output) | frontend/tests/utils/docScreenshot.ts (utility)

Screenshots for documentation are captured during Playwright component tests and pushed back to a PR branch by the screenshots.yml workflow. The workflow is manual / on-demand only (workflow_dispatch) — it does not auto-trigger on PR push or merge. Run it from the Actions tab or via gh workflow run "Update Documentation Screenshots" -f pr_number=<N> when you want to refresh the screenshots on a PR.

How it works:

  1. Import docScreenshot from ./utils/docScreenshot in any .ct.tsx test file
  2. Call await docScreenshot(page, "area--component--state") after the component reaches the desired visual state
  3. Reference the image in markdown: ![Alt text](../assets/images/screenshots/auto/area--component--state.png)
  4. Trigger the screenshot workflow on demand for the PR; it captures screenshots and commits any changes back to the PR branch

Naming convention (-- separates segments, - within words):

Segment Purpose Examples
area Feature area landing, badges, corpus, versioning, annotations
component Specific view or component hero-section, celebration-modal, list-view
state Visual state captured anonymous, with-data, empty, auto-award

At least 2 segments required, 3 recommended. All lowercase alphanumeric with single hyphens.

Example:

import { docScreenshot } from "./utils/docScreenshot";

// After the component renders and assertions pass:
await docScreenshot(page, "badges--celebration-modal--auto-award");

Rules:

  • Place docScreenshot() calls AFTER assertions that confirm the desired visual state
  • The filename IS the contract between tests and docs — keep names stable
  • Never manually edit files in docs/assets/images/screenshots/auto/ — they are overwritten by CI
  • Manually curated screenshots stay in docs/assets/images/screenshots/ (parent directory)

Release Screenshots (Point-in-Time)

For release notes, use releaseScreenshot to capture screenshots that are locked in amber — they show the UI at a specific release and never change.

Location: docs/assets/images/screenshots/releases/{version}/ (output)

import { releaseScreenshot } from "./utils/docScreenshot";

await releaseScreenshot(page, "v3.0.0.b3", "landing-page", { fullPage: true });

Key differences from docScreenshot:

  • Output: docs/assets/images/screenshots/releases/{version}/{name}.png
  • Write-once: If the file already exists, the function is a no-op (won't overwrite)
  • CI never touches the releases/ directory
  • Name is a simple kebab-case string (no -- segment convention)
  • Version must match v{major}.{minor}.{patch} format (with optional suffix)

When to use which:

  • docScreenshot → README, quickstart, guides (always fresh)
  • releaseScreenshot → Release notes (frozen at release time)

Authenticated Playwright Testing (Live Frontend Debugging)

When you need to interact with the running frontend as an authenticated user (e.g., debugging why a query returns empty results), use Django admin session cookies to authenticate GraphQL requests.

Architecture context: The frontend uses Auth0 for authentication, but the Django backend also accepts session cookie auth. Apollo Client sends GraphQL requests directly to http://localhost:8000/graphql/ (cross-origin from the Vite dev server at localhost:5173). Since fetch defaults to credentials: 'same-origin', browser cookies aren't sent cross-origin. The workaround is to inject the session cookie into request headers via Playwright's route interception.

Step 1: Set a password for the superuser (one-time setup):

docker compose -f local.yml exec django python manage.py shell -c "
from django.contrib.auth import get_user_model
User = get_user_model()
u = User.objects.filter(is_superuser=True).first()
u.set_password('testpass123')
u.save()
print(f'Password set for {u.username}')
"

Step 2: Playwright script pattern:

const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch({ headless: true });
  const context = await browser.newContext();
  const page = await context.newPage();

  // Collect console messages for debugging
  const consoleMsgs = [];
  page.on('console', msg => consoleMsgs.push('[' + msg.type() + '] ' + msg.text()));

  // 1. Login to Django admin to get session cookie
  await page.goto('http://localhost:8000/admin/login/');
  await page.fill('#id_username', '<superuser-username>');
  await page.fill('#id_password', 'testpass123');
  await page.click('input[type=submit]');
  await page.waitForTimeout(2000);

  // 2. Extract the session cookie
  const cookies = await context.cookies();
  const sessionCookie = cookies.find(c => c.name === 'sessionid');

  // 3. Intercept GraphQL requests to inject the session cookie
  //    (needed because Apollo sends cross-origin requests to :8000)
  await page.route('**/graphql/**', async (route) => {
    const headers = {
      ...route.request().headers(),
      'Cookie': 'sessionid=' + sessionCookie.value,
    };
    await route.continue({ headers });
  });

  // 4. Navigate to the frontend page under test
  await page.goto('http://localhost:5173/extracts');
  await page.waitForTimeout(5000);

  // 5. Inspect results
  const bodyText = await page.textContent('body');
  console.log(bodyText);

  await browser.close();
})();

Run from the frontend directory (where playwright is a dependency):

cd frontend && node /path/to/script.js

Key details:

  • The Django admin login sets a sessionid cookie for localhost
  • page.route('**/graphql/**') intercepts Apollo's requests to localhost:8000/graphql/ and injects the cookie header
  • The AuthGate will still show anonymous state (no Auth0 session), but GraphQL queries execute as the authenticated Django user
  • This is useful for verifying backend query results, permission filtering, and data flow through the real frontend
  • For verifying just the GraphQL response without the full frontend, curl with the session cookie also works:
    curl -s -X POST http://localhost:8000/graphql/ \
      -H "Content-Type: application/json" \
      -H "Cookie: sessionid=<session-key>" \
      -d '{"query":"query { extracts { edges { node { id name } } } }"}' | python3 -m json.tool

Alternative — create a session programmatically (no admin login needed):

docker compose -f local.yml exec django python manage.py shell -c "
from django.contrib.sessions.backends.db import SessionStore
from django.contrib.auth import get_user_model
User = get_user_model()
user = User.objects.filter(is_superuser=True).first()
session = SessionStore()
session['_auth_user_id'] = str(user.pk)
session['_auth_user_backend'] = 'django.contrib.auth.backends.ModelBackend'
session['_auth_user_hash'] = user.get_session_auth_hash()
session.save()
print(f'Session key: {session.session_key}')
"

Then use the printed session key directly in curl or Playwright route interception.

Documentation Locations

  • Permissioning: docs/permissioning/consolidated_permissioning_guide.md
  • Frontend Routing: docs/frontend/routing_system.md
  • Frontend Auth Flow: docs/frontend/auth_flow.md
  • PDF Data Layer: docs/architecture/PDF-data-layer.md
  • PAWLs Format (v1/v2): docs/architecture/pawls-format.md
  • Parser Pipeline: docs/pipelines/pipeline_overview.md
  • LLM Framework: docs/architecture/llms/README.md
  • Collaboration System: docs/commenting_system/README.md
  • Auth Pattern (detailed): frontend/src/docs/AUTHENTICATION_PATTERN.md
  • Documentation Screenshots: docs/development/screenshots.md
  • Query Permission Patterns: docs/architecture/query_permission_patterns.md
  • OS-Legal-Style Migration Guide: docs/frontend/os-legal-style-migration-guide.md

Branch Strategy

This project follows trunk-based development:

  • Work directly on main branch or use short-lived feature branches
  • Feature branches: feature/description-issue-number
  • Merge feature branches quickly (within a day or two)
  • Commit message format: Descriptive with issue references (e.g., "Closes Epic: Notification System #562")

Changelog Maintenance

IMPORTANT: Always update CHANGELOG.md when making significant changes to the codebase.

The changelog follows Keep a Changelog format:

## [Unreleased] - YYYY-MM-DD

### Added
- New features

### Fixed
- Bug fixes with file locations and line numbers

### Changed
- Changes to existing functionality

### Technical Details
- Implementation specifics, architectural notes

When to update:

  • New features or models added
  • Production code bugs fixed (document file location, line numbers, and impact)
  • Breaking changes to APIs or data models
  • Test suite fixes that reveal production issues
  • Database migrations
  • Architecture changes

What to include:

  • File paths and line numbers for code changes
  • Clear description of the issue and fix
  • Impact on system behavior
  • Migration notes if applicable

Pre-commit Hooks

Automatically run on commit:

  • black (Python formatting)
  • isort (import sorting)
  • flake8 (linting)
  • prettier (frontend formatting)
  • pyupgrade (Python syntax modernization)

Run manually: pre-commit run --all-files

Common Pitfalls

  1. Frontend tests hanging: Always use --reporter=list flag
  2. Permission N+1 queries: Use .visible_to_user() NOT individual permission checks. For Conversation list queries in GraphQL resolvers, use ConversationQueryOptimizer from opencontractserver.conversations.query_optimizer - it provides request-level caching to avoid repeated corpus/document visibility subqueries
  3. Missing GraphQL mocks: Check variables match exactly (null vs undefined matters), add duplicates for refetches
  4. Notification duplication in tests: Moderation methods auto-create ModerationAction records
  5. Structural annotation editing: Always read-only except for superusers
  6. Missing signal imports: Import signal handlers in apps.py ready() method
  7. PDF rendering slow in tests: Increase timeouts to 20s+ for Chromium
  8. Cache serialization crashes: Keep InMemoryCache definition inside wrapper, not test file
  9. Backend Tests Waiting > 10 seconds on Postgres to be Ready: Docker network issue. Fix with: docker compose -f test.yml down && docker kill $(docker ps -q) && docker compose -f test.yml down
  10. Empty lists on direct navigation: AuthGate pattern solves this (don't check auth status, it's always ready)
  11. URL desynchronization: Use CentralRouteManager, don't bypass routing system
  12. Jotai state not updating: Ensure atoms are properly imported and used with useAtom hook
  13. Writing sync agent tools: All agent tools must be async. The PydanticAIToolWrapper accepts sync functions but calls them directly (no thread pool) — sync Django ORM calls will raise SynchronousOnlyOperation. Use the a-prefixed async versions in core_tools.py.
  14. PydanticAI system_prompt silently dropped: When creating PydanticAIAgent, use instructions= NOT system_prompt=. The system_prompt parameter is only included when message_history is None, but OpenContracts' chat() persists a HUMAN message before calling pydantic-ai's run(), so history is always non-empty. This causes the system prompt to be silently dropped. See docs/architecture/llms/README.md for full details.
  15. Apollo cache keyArgs must use field argument names, not variable names: In cache.ts, relayStylePagination(["corpus", "name_Contains"]) uses the GraphQL field argument names (e.g., extracts(corpus: $id, name_Contains: $text)), NOT the variable names ($id, $text). Mismatched keyArgs silently fail to isolate cache entries, causing queries with different filters to share stale cached results.
  16. Playwright CT split-import rule: In frontend/tests/*.ct.tsx, keep JSX-component imports in their own import { Wrapper } from "./Wrapper" statement, separate from any helper/constant imports from the same file. Playwright CT's babel transform only rewrites component references into importRefs when every specifier in the statement is a JSX component; mixing a component with helpers leaves the component unrewritten and mount() throws. Do not let auto-import organisers merge them.
  17. Corrupted Docker iptables chains (RARE): If you see Chain 'DOCKER-ISOLATION-STAGE-2' does not exist errors, Docker's iptables chains have been corrupted during docker cycling. Run this nuclear fix:
    sudo systemctl stop docker && sudo systemctl stop docker.socket && sudo ip link delete docker0 2>/dev/null || true && sudo iptables -t nat -F && sudo iptables -t nat -X && sudo iptables -t filter -F && sudo iptables -t filter -X 2>/dev/null || true && sudo iptables -t mangle -F && sudo iptables -t mangle -X && sudo iptables -t filter -N INPUT 2>/dev/null || true && sudo iptables -t filter -N FORWARD 2>/dev/null || true && sudo iptables -t filter -N OUTPUT 2>/dev/null || true && sudo iptables -P INPUT ACCEPT && sudo iptables -P FORWARD ACCEPT && sudo iptables -P OUTPUT ACCEPT && sudo systemctl start docker
    This completely resets Docker's networking and iptables state. Docker will recreate all required chains on startup.

@claude
Copy link
Copy Markdown

claude Bot commented May 14, 2026

line1\nline2\nline3

@claude
Copy link
Copy Markdown

claude Bot commented May 14, 2026

Code Review - PR 1647: Speed up corpus home document index

test content

@claude
Copy link
Copy Markdown

claude Bot commented May 14, 2026

Code Review — PR #1647: Speed up corpus home document index

Overview

This is a well-targeted performance fix. The root cause analysis (over-fetched GraphQL fields + client-side filtering of what should be a server-side predicate) is correct, and the solution is proportionate: a new ultra-lean TOC-specific query, a new backend filter, and clean removal of dead field plumbing. No existing consumers of GET_DOCUMENT_RELATIONSHIPS or GET_CORPUS_DOCUMENTS_FOR_TOC are broken.


What works well

  • Server-side filtering is the right call. Moving the relationshipType === "RELATIONSHIP" && annotationLabel.text.toLowerCase() === "parent" predicate to the backend eliminates the full N-row fetch and makes the result cacheable at the right granularity.
  • Non-destructive query addition. Introducing GET_CORPUS_DOCUMENT_TOC_EDGES alongside (not in place of) GET_DOCUMENT_RELATIONSHIPS correctly preserves the rich payload for CorpusDocumentRelationships and DocumentRelationshipModal.
  • Null guards on the new edge loop (edge.node?.sourceDocument?.id) are cleaner than the old code, which assumed non-null.
  • iexact lookup on annotation_label_text is the right choice — label text matching should not be case-sensitive.
  • icon removal is saferenderNode derives the on-screen icon from fileType via getFileIcon, confirmed by reading the component.
  • creator removal from GET_CORPUS_DOCUMENTS_FOR_TOC is safe — verified that RunCorpusActionModal only accesses node.id and node.title.

Issues

1. Missing constants for hardcoded strings (CLAUDE.md — no magic numbers)

DocumentTableOfContents.tsx hardcodes two domain strings directly in the query variables:

relationshipType: "RELATIONSHIP",
annotationLabelText: "parent",

Per project convention these belong in frontend/src/assets/configurations/constants.ts alongside DOCUMENT_RELATIONSHIP_TOC_LIMIT. The backend already has RELATIONSHIP_TYPE_CHOICES; the frontend should mirror it with a named export.

2. No new backend test for the annotation_label_text filter

Neither test_document_relationships.py nor test_query_optimizer_methods.py exercises the new filter field. Without a test, a future refactor of DocumentRelationshipFilter could silently break server-side TOC filtering and the component would revert to displaying a flat, unstructured list with no error.

A minimal addition — create two relationships with different labels, query with annotationLabelText: "parent", and assert only the matching row is returned — would close this gap.

3. totalCount semantics changed on the lean query

isLimitExceeded (line 518) compares relationshipTotalCount > DOCUMENT_RELATIONSHIP_TOC_LIMIT. Previously this was the count of all relationships in the corpus; now it is the count of parent-labeled RELATIONSHIP rows only. A corpus with 600 total relationships but only 50 parent ones will no longer trigger the truncation warning. This is arguably more accurate for the TOC, but it is a behaviour change worth documenting with a short inline comment so a future reader does not mistake the narrower count for a bug.

4. Nullable document refs on edges whose DB FKs are non-null

CorpusDocumentTocEdge types both sourceDocument and targetDocument as | null. The null guard in the component (if (!sourceId || !targetId) return;) handles it safely, but a brief comment — "GraphQL marks these nullable even though the DB FK is non-nullable" — would stop a future reviewer from removing the guard as dead code.


Minor observations

  • The CHANGELOG entry is accurate and detailed but runs to a full paragraph. Consider trimming to 2–3 sentences and linking to the PR for the full write-up.
  • Including totalCount and pageInfo.hasNextPage in the lean query is correct and not over-fetch; they feed the existing isLimitExceeded check.
  • The camelCase to snake_case conversion from annotationLabelText to annotation_label_text is handled automatically by graphene-django via DjangoFilterConnectionField + DocumentRelationshipFilter.

Summary

The core change is correct and the performance win is real. Two things worth addressing before merging:

  1. Extract "RELATIONSHIP" and "parent" to named constants in constants.ts.
  2. Add a backend test asserting that annotationLabelText: "parent" filters the queryset, so server-side filtering cannot silently regress.

The totalCount semantic note and the null-guard comment are nice-to-haves, not blockers.

@codecov
Copy link
Copy Markdown

codecov Bot commented May 14, 2026

Codecov Report

❌ Patch coverage is 87.50000% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...rc/components/corpuses/DocumentTableOfContents.tsx 80.00% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

JSv4 added 2 commits May 13, 2026 20:50
- Hoist hard-coded "RELATIONSHIP" / "parent" query-variable strings into
  DOCUMENT_RELATIONSHIP_TYPE_RELATIONSHIP and DOCUMENT_RELATIONSHIP_LABEL_PARENT
  in frontend/src/assets/configurations/constants.ts and consume them from
  DocumentTableOfContents.tsx + the CT wrapper (no magic strings rule).
- Add inline comment on relationshipTotalCount explaining that the lean TOC
  edges query counts only parent-labeled RELATIONSHIP rows server-side; the
  truncation threshold is narrower than the legacy fetch-all behaviour.
- Add interface comment on CorpusDocumentTocEdge.{source,target}Document
  documenting why the GraphQL schema marks them nullable when the DB FKs are
  non-null (consumers still must null-guard).
- Pin the new DocumentRelationshipFilter.annotation_label_text iexact filter
  with test_document_relationships_annotation_label_text_filter — creates
  three relationships (2 from setUp + 1 parent-labeled), queries with mixed-
  case "PARENT" and asserts exactly one edge is returned. Guards against a
  future refactor silently dropping the filter and reverting the TOC to a
  full fetch.
- Rewire the CT wrapper to the new GET_CORPUS_DOCUMENT_TOC_EDGES query shape:
  id + source/target IDs only, with relationshipType / annotationLabelText
  variables matching what the component sends (Apollo MockedProvider matches
  variables by deep-equal).
- Slim the test mocks' document edges to {id,title,description,slug,fileType}
  (icon / creator dropped server-side) and tighten the cache keyArgs for
  documentRelationships to include the new filter args.
- Bump strict-mode-violating getByText("Parent Document") calls in the CT
  tests to exact match — the mocked description "A parent document for
  testing hierarchy" also contains the substring.
@claude
Copy link
Copy Markdown

claude Bot commented May 14, 2026

Code Review – PR #1647: Speed up corpus home document index by trimming GraphQL over-fetch

Overview

This PR targets a real, measurable performance regression: the "Loading document structure…" wait on Corpus Home was caused by two over-fetched GraphQL queries. The fix is correct in principle — push filters server-side, slim the query shapes, and eliminate wasted resolver calls. The implementation is clean and well-commented. One cache isolation bug must be fixed before merging.


🔴 Critical: Production Apollo cache not updated

File: frontend/src/graphql/cache.ts:309

// Current (unchanged by this PR):
documentRelationships: relayStylePagination(["corpusId", "documentId"]),

The new GET_CORPUS_DOCUMENT_TOC_EDGES query passes relationshipType and annotationLabelText as field arguments. Because those two arguments are not in the production keyArgs list, Apollo treats TOC edge results and the full GET_DOCUMENT_RELATIONSHIPS results as the same cache entry whenever they share the same corpusId.

Concrete failure scenario:

  1. User opens Corpus Home → TOC fires GET_CORPUS_DOCUMENT_TOC_EDGES with relationshipType="RELATIONSHIP" and annotationLabelText="parent" → Apollo stores 50 ID-only edges under key documentRelationships({"corpusId":"X"}).
  2. User opens CorpusDocumentRelationships or DocumentRelationshipModalGET_DOCUMENT_RELATIONSHIPS fires for the same corpus → Apollo writes 600 full-payload edges to the same key, or reads 50 stale ID-only edges and renders wrong data.

The test wrapper correctly adds these to its local InMemoryCache ("relationshipType", "annotationLabelText"), but the production cache (cache.ts) was not updated, so the tests pass while the production bug remains.

Fix:

// frontend/src/graphql/cache.ts
documentRelationships: relayStylePagination([
  "corpusId",
  "documentId",
  "relationshipType",
  "annotationLabelText",
]),

This is exactly the pattern called out in CLAUDE.md Common Pitfall #15 ("Apollo cache keyArgs must use field argument names, not variable names").


🟡 Minor: Backend test covers label filter in isolation, not the combined filter the TOC actually sends

test_document_relationships_annotation_label_text_filter verifies annotationLabelText alone works with iexact. The production TOC query sends both relationshipType and annotationLabelText simultaneously. A second assertion (or a second test method) that combines both variables would pin the exact server-side behavior and guard against a future refactor accidentally wiring annotationLabelText to a different field than annotation_label__text.

This is low risk given the filter implementation is simple, but worth adding for completeness.


✅ What's done well

  • Server-side filter: Adding annotation_label_text iexact to DocumentRelationshipFilter is the right place for this logic. Client-side filtering of a 500-row result set is a recurring pattern across the codebase; eliminating it here is a good model for future cleanup.
  • Additive query design: GET_CORPUS_DOCUMENT_TOC_EDGES is introduced alongside (not replacing) GET_DOCUMENT_RELATIONSHIPS. The two callers that need the full payload (CorpusDocumentRelationships, DocumentRelationshipModal) are correctly left on the rich query.
  • Constants over magic strings: DOCUMENT_RELATIONSHIP_TYPE_RELATIONSHIP and DOCUMENT_RELATIONSHIP_LABEL_PARENT are properly placed in constants.ts and imported wherever used — consistent with the no-magic-numbers rule in CLAUDE.md.
  • TypeScript nullability comment: The explanation on CorpusDocumentTocEdge.sourceDocument/targetDocument (non-null at DB level but nullable in GraphQL schema) is exactly the kind of non-obvious WHY comment worth keeping.
  • Test mock cleanup: Removing hasPreviousPage, startCursor, endCursor from pageInfo in mocks that only need hasNextPage reduces noise without breaking anything.
  • { exact: true } fix: The Playwright assertion hardening is correct and prevents the substring collision with the description text.
  • CHANGELOG: Well-documented entry with file paths, root cause, and measurable impact.

Summary

One change required before this is production-safe:

// frontend/src/graphql/cache.ts ~line 309
documentRelationships: relayStylePagination([
  "corpusId",
  "documentId",
  "relationshipType",   // ← add
  "annotationLabelText", // ← add
]),

Without it, the new lean TOC query and the existing full query will silently share cache entries for the same corpus, leading to either stale ID-only data in relationship modals or oversized payloads polluting the TOC tree.

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