docs: rebuild documentation from scratch with clean IA
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6c31b331f7
commit
0c5b199b76
30 changed files with 1647 additions and 649 deletions
|
|
@ -1,12 +0,0 @@
|
|||
# API Contracts
|
||||
|
||||
`Scholarr` is designed with strictly typed Pydantic V2 models, serialized across the network through standard OpenAPI `v3` specs via FastAPI.
|
||||
|
||||
## DTO Models Structure
|
||||
FastAPI routes dynamically ingest request/response Data Transfer Objects.
|
||||
- **Example**: `PublicationListItem` no longer contains a hard-coded `.doi`. It contains a `.display_identifier` property resolving the highest confidence identifier regardless of backend origin.
|
||||
- **Testing**: Run `./scripts/check_frontend_api_contract.py` or equivalent integration steps mapped in CI to ensure that backend python routes strictly map to the TypeScript types compiled by the Frontend.
|
||||
|
||||
## Frontend Contract Requirements
|
||||
The UI exclusively calls routes exposed by FastAPI's `APIRouter`.
|
||||
All frontend integration tests enforce that Vue 3 Components do not assume hard properties directly outside the bounded TS schemas. This avoids type mismatch runtime explosions if the database is scaled horizontally.
|
||||
|
|
@ -1,58 +1,173 @@
|
|||
# Domain Boundaries
|
||||
---
|
||||
title: Architecture
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# Architecture
|
||||
|
||||
## Data Model Rules
|
||||
|
||||
- Scholar tracking is user-scoped.
|
||||
- Publications are global/deduplicated records.
|
||||
- Read/favorite/visibility state stays on scholar-publication link rows.
|
||||
- **Scholar tracking is user-scoped.** Each user manages their own list of tracked scholars. Validate mapping/join tables; never assume global links between users and Scholar IDs.
|
||||
- **Publications are global, deduplicated records.** Deduplicate via Scholar cluster ID and normalized fingerprinting prior to database insertion.
|
||||
- **State lives on the link.** Read/unread, favorites, and visibility state exist exclusively on the scholar-publication link table, not the global publication table.
|
||||
|
||||
## Service Boundaries
|
||||
## Domain Service Boundaries
|
||||
|
||||
Canonical business logic belongs in `app/services/domains/*`.
|
||||
All business logic resides in `app/services/<domain>/`. Flat files in the `app/services/` root are strictly prohibited.
|
||||
|
||||
- `app/services/domains/ingestion/*`: run orchestration, continuation queue, scheduler, and scrape safety.
|
||||
- `app/services/domains/scholar/*`: fail-fast scholar parsing and source fetch adapters.
|
||||
- `app/services/domains/scholars/*`: scholar CRUD, profile image, and name-search controls.
|
||||
- `app/services/domains/publications/*`: listing/read-state, favorite toggles, enrichment scheduling, and retry paths.
|
||||
- `app/services/domains/arxiv/*`: typed API client, global DB-backed throttle, query cache, and in-flight request coalescing.
|
||||
- `app/services/domains/crossref/*` + `app/services/domains/unpaywall/*`: DOI/OA enrichment with bounded pacing.
|
||||
- `app/services/domains/runs/*`: run history and continuation queue operations.
|
||||
- `app/services/domains/portability/*`: import/export workflows.
|
||||
### Ingestion (`app/services/ingestion/`)
|
||||
|
||||
## Frontend Behavior Notes
|
||||
Run orchestration, continuation queue, scheduler integration, and scrape safety policy enforcement. The `ScholarIngestionService` drives the primary data acquisition loop.
|
||||
|
||||
- Navigation: Mobile primary nav is in a left drawer and closes on route change or logout.
|
||||
- Lists: Long list views use internal scroll containers to prevent viewport overflow.
|
||||
- Rate Limiting: Name search remains intentionally constrained due to upstream anti-bot behavior; production onboarding should prefer scholar ID/profile URLs directly if possible.
|
||||
Key modules:
|
||||
- `application.py` - Main ingestion orchestrator
|
||||
- `scheduler.py` - Background tick loop, queue batch processing
|
||||
- `constants.py` - Safety policy constants and floor values
|
||||
- `fingerprints.py` - Publication fingerprinting for deduplication
|
||||
- `types.py` - Ingestion result types and state enums
|
||||
|
||||
## Data Integration & Acquisition Flow
|
||||
### Scholar Parsing (`app/services/scholar/`)
|
||||
|
||||
Fail-fast Google Scholar HTML parsing and source fetch adapters. Handles paginated HTML feeds, extracts publication blocks via regex and DOM invariants (e.g., `gsc_vcd_cib`).
|
||||
|
||||
Key modules:
|
||||
- `parser.py` - HTML parser for publication extraction
|
||||
- `parser_utils.py` - Parsing helpers and DOM selectors
|
||||
- `source.py` - HTTP fetch adapters with browser headers
|
||||
- `profile_rows.py` - Profile metadata extraction
|
||||
- `author_rows.py` - Author citation row parsing
|
||||
- `state_detection.py` - Blocked/CAPTCHA/rate-limit detection
|
||||
|
||||
### Scholar Management (`app/services/scholars/`)
|
||||
|
||||
Scholar CRUD, profile image management, and name-search controls with rate limiting.
|
||||
|
||||
Key modules:
|
||||
- `application.py` - Scholar lifecycle operations
|
||||
- `uploads.py` - Image upload handling
|
||||
- `search_hints.py` - Name search with caching and cooldowns
|
||||
- `validators.py` - Input validation for scholar creation
|
||||
|
||||
### Publications (`app/services/publications/`)
|
||||
|
||||
Listing, read-state management, favorite toggles, enrichment scheduling, deduplication, and PDF queue management.
|
||||
|
||||
Key modules:
|
||||
- `application.py` - Publication service facade
|
||||
- `listing.py` - Filtered listing with pagination (modes: all/unread/latest)
|
||||
- `queries.py` - Database query builders
|
||||
- `counts.py` - Aggregation counts for dashboard
|
||||
- `dedup.py` - Duplicate detection and merging
|
||||
- `enrichment.py` - Identifier and metadata enrichment orchestration
|
||||
- `pdf_queue.py` - PDF resolution queue policy
|
||||
- `pdf_resolution_pipeline.py` - Multi-source PDF resolution (Unpaywall, arXiv)
|
||||
- `types.py` - Publication DTOs and response types
|
||||
|
||||
### Publication Identifiers (`app/services/publication_identifiers/`)
|
||||
|
||||
Multi-identifier resolution engine. A single publication can have multiple identifiers (DOI, arXiv ID, PMID, PMCID). This decoupled approach replaced the earlier hardcoded DOI-only model.
|
||||
|
||||
Key modules:
|
||||
- `application.py` - Identifier gathering orchestration
|
||||
- `normalize.py` - Identifier normalization and validation
|
||||
|
||||
### arXiv (`app/services/arxiv/`)
|
||||
|
||||
Typed API client with global DB-backed throttle, query cache, and in-flight request coalescing.
|
||||
|
||||
Key modules:
|
||||
- `client.py` - HTTP client for arXiv export API
|
||||
- `gateway.py` - Gateway with advisory lock, caching, and coalescing
|
||||
- `cache.py` - Query cache with TTL and max-entry pruning
|
||||
- `rate_limit.py` - Global rate limiter via `arxiv_runtime_state` table
|
||||
- `guards.py` - Load-shedding guards (skip when DOI/arXiv evidence exists)
|
||||
- `parser.py` - Atom XML response parser
|
||||
|
||||
Safety features:
|
||||
- Requests are globally serialized via a PostgreSQL advisory lock and shared runtime row (`arxiv_runtime_state`)
|
||||
- Identical request payloads are fingerprinted and cached in `arxiv_query_cache_entries` with TTL + max-entry pruning
|
||||
- Concurrent identical misses are coalesced in-process (one outbound call serves all waiters)
|
||||
|
||||
### Crossref (`app/services/crossref/`)
|
||||
|
||||
DOI lookup via Crossref REST API with bounded pacing and configurable batch limits.
|
||||
|
||||
### Unpaywall (`app/services/unpaywall/`)
|
||||
|
||||
Open-access PDF resolution via Unpaywall API, with HTML-based PDF link discovery as a fallback.
|
||||
|
||||
Key modules:
|
||||
- `application.py` - Unpaywall service facade
|
||||
- `pdf_discovery.py` - HTML page scraping for PDF link candidates
|
||||
|
||||
### OpenAlex (`app/services/openalex/`)
|
||||
|
||||
Metadata matching via OpenAlex API for supplementary identifier resolution.
|
||||
|
||||
Key modules:
|
||||
- `client.py` - OpenAlex API client
|
||||
- `matching.py` - Fuzzy title/author matching
|
||||
|
||||
### Runs (`app/services/runs/`)
|
||||
|
||||
Run history tracking and continuation queue operations.
|
||||
|
||||
Key modules:
|
||||
- `application.py` - Run lifecycle management
|
||||
- `queue_service.py` - Continuation queue operations (retry, drop, clear)
|
||||
- `queue_queries.py` - Queue item queries
|
||||
|
||||
### Portability (`app/services/portability/`)
|
||||
|
||||
Import/export workflows for scholar data with full publication state preservation.
|
||||
|
||||
Key modules:
|
||||
- `application.py` - Import/export orchestration
|
||||
- `exporting.py` - Scholar export serialization
|
||||
- `publication_import.py` - Publication import with deduplication
|
||||
- `scholar_import.py` - Scholar import with link reconstruction
|
||||
- `normalize.py` - Payload normalization and validation
|
||||
|
||||
### Database Operations (`app/services/dbops/`)
|
||||
|
||||
Integrity checking, link repair, and near-duplicate repair operations exposed via admin API and CLI scripts.
|
||||
|
||||
Key modules:
|
||||
- `__init__.py` - `collect_integrity_report`, `run_publication_link_repair`
|
||||
- `near_duplicate_repair.py` - Near-duplicate publication detection and merging
|
||||
|
||||
## Data Integration Flow
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
UI[Frontend Vue App] --> API[FastAPI Backend]
|
||||
API --> Scheduler[Background Celery/Async Scheduler]
|
||||
|
||||
API --> Scheduler[Background Async Scheduler]
|
||||
|
||||
Scheduler -->|1. Fetch HTML| Scholar[Google Scholar HTML Parser]
|
||||
Scholar -->|2. Extract Metadata| IdentifierModule[Identifier Gathering]
|
||||
|
||||
|
||||
IdentifierModule -->|Search arXiv API| API1(arXiv)
|
||||
IdentifierModule -->|Search Crossref API| API2(Crossref)
|
||||
|
||||
|
||||
IdentifierModule -->|3. Save Identifiers| DB[(PostgreSQL)]
|
||||
|
||||
|
||||
Scheduler -->|4. Resolve PDF| PDFPipeline[PDF Resolution Pipeline]
|
||||
DB --> |Identified DOIs| PDFPipeline
|
||||
PDFPipeline -->|Search Open Access APIs| Unpaywall(Unpaywall API)
|
||||
|
||||
|
||||
Unpaywall --> |Acquire PDF URL| PDFWorker[PDF Download Worker]
|
||||
PDFWorker --> |Store PDF Metadata| DB
|
||||
PDFWorker --> |Store PDF Metadata| DB
|
||||
```
|
||||
|
||||
### Identifier Engine Philosophy
|
||||
The platform previously treated the `DOI` as a hardcoded 1:1 property of a publication. It now utilizes a decoupled *Identifier Gathering* module (`PublicationIdentifier` table). A single publication can have multiple identifiers (ex. `doi`, `arxiv`, `pmid`, `pmcid`). This creates high resilience when integrating with external APIs, allowing systems like Unpaywall to be fed explicitly with high-confidence DOIs, rather than relying on unstructured search heuristics.
|
||||
## API Layer
|
||||
|
||||
### arXiv Safety and Efficiency
|
||||
- arXiv requests are globally serialized via a PostgreSQL advisory lock and shared runtime row (`arxiv_runtime_state`).
|
||||
- identical request payloads are fingerprinted and cached in `arxiv_query_cache_entries` with TTL + optional max-entry pruning.
|
||||
- concurrent identical misses are coalesced in-process, so one outbound call serves all waiters.
|
||||
- structured logs provide auditability for scheduling/completion/cooldown/cache behavior.
|
||||
Routes live in `app/api/routers/`. All responses under `/api/v1` use a strict envelope format. See [API Reference](../reference/api.md) for the full contract.
|
||||
|
||||
## Middleware Stack
|
||||
|
||||
Applied in `app/main.py`:
|
||||
|
||||
1. **CSRF Protection** - Token-based CSRF validation
|
||||
2. **Session Middleware** - Cookie-based session management (itsdangerous signing)
|
||||
3. **Request Logging** - Structured request/response logging with configurable skip paths
|
||||
4. **Security Headers** - Configurable HTTP security headers and CSP
|
||||
|
|
|
|||
|
|
@ -1,24 +1,78 @@
|
|||
---
|
||||
title: Contributing
|
||||
sidebar_position: 4
|
||||
---
|
||||
|
||||
# Contributing
|
||||
|
||||
## Scope
|
||||
This project favors small, reviewable pull requests that keep runtime behavior clear and operationally safe.
|
||||
## PR Process
|
||||
|
||||
## Essential-File Policy
|
||||
Commit only source-of-truth files required to build, run, test, or document the app.
|
||||
1. Create a feature branch from `main`.
|
||||
2. Make your changes following the code standards below.
|
||||
3. Run tests inside the container (see [Testing](testing.md)).
|
||||
4. Run `ruff check .` and `mypy app/` to catch lint and type errors.
|
||||
5. Open a pull request with a clear title and description.
|
||||
|
||||
Do not commit generated or local-only artifacts, including:
|
||||
- `__pycache__/`, `*.pyc`, `.pytest_cache/`, `.mypy_cache/`, `.ruff_cache/`
|
||||
- frontend build/install outputs like `frontend/dist/`, `frontend/node_modules/`, `frontend/.vite/`
|
||||
- coverage outputs (`.coverage`, `htmlcov/`)
|
||||
- packaging/build leftovers (`*.egg-info/`, `build/`, `dist/`)
|
||||
- local probe/scratch material (`planning/`)
|
||||
## Commit Conventions
|
||||
|
||||
CI enforces this with `scripts/check_no_generated_artifacts.sh`.
|
||||
This project uses [Conventional Commits](https://www.conventionalcommits.org/) with [python-semantic-release](https://python-semantic-release.readthedocs.io/) for automated versioning.
|
||||
|
||||
## Merge Checklist
|
||||
- [ ] Changes are minimal, purposeful, and remove obsolete/dead code in touched areas.
|
||||
- [ ] Backend tests pass (`uv run pytest tests/unit` and integration scope as needed).
|
||||
- [ ] Frontend checks pass (`npm run typecheck`, `npm run test:run`, `npm run build`).
|
||||
- [ ] API/behavior docs are updated when env vars, endpoints, or payloads change.
|
||||
- [ ] `README.md`, `.env.example`, and deployment notes stay aligned.
|
||||
- [ ] `scripts/check_no_generated_artifacts.sh` passes locally.
|
||||
Commit message format:
|
||||
|
||||
```
|
||||
<type>(<scope>): <description>
|
||||
|
||||
[optional body]
|
||||
```
|
||||
|
||||
Types: `feat`, `fix`, `docs`, `ci`, `refactor`, `test`, `chore`, `perf`.
|
||||
|
||||
Examples:
|
||||
- `feat(scholars): add bulk import from CSV`
|
||||
- `fix(ingestion): handle empty citation blocks`
|
||||
- `docs: update configuration reference`
|
||||
|
||||
## Code Standards
|
||||
|
||||
### Function Length
|
||||
|
||||
Maximum 50 lines per function. Break complex logic into small, testable, single-responsibility functions.
|
||||
|
||||
### DRY
|
||||
|
||||
Abstract repetitive logic immediately. No duplicate boilerplate for database queries, API responses, or error handling.
|
||||
|
||||
### Negative Space Programming
|
||||
|
||||
Use explicit assertions and constraints to define invalid states. Fail fast and early. Do not allow silent failures or cascading malformed data, especially in DOM parsing.
|
||||
|
||||
### Cyclomatic Complexity
|
||||
|
||||
Flatten logic. Use early returns and guard clauses instead of deep nesting. No magic numbers.
|
||||
|
||||
### Domain Service Boundaries
|
||||
|
||||
All business logic resides in `app/services/<domain>/`. Flat files in the `app/services/` root are strictly prohibited. Each domain owns its own application service, types, and helpers.
|
||||
|
||||
### API Envelope
|
||||
|
||||
All `/api/v1` responses use the strict envelope format:
|
||||
|
||||
- Success: `{"data": ..., "meta": {"request_id": "..."}}`
|
||||
- Error: `{"error": {"code": "...", "message": "...", "details": ...}, "meta": {"request_id": "..."}}`
|
||||
|
||||
### Data Isolation
|
||||
|
||||
- Scholar tracking is user-scoped.
|
||||
- Publications are global, deduplicated records.
|
||||
- Read/favorite/visibility state lives on scholar-publication link rows.
|
||||
|
||||
### Scrape Safety
|
||||
|
||||
Rate limits and cooldowns are immutable constraints. They prevent IP bans and must not be optimized away or set to zero.
|
||||
|
||||
## UI Standards
|
||||
|
||||
- Integrate Tailwind with the preset theming system (`frontend/src/theme/presets/`).
|
||||
- Every UI element must have a clear purpose.
|
||||
- Clarity through both styling and language.
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
# Documentation Standards
|
||||
|
||||
This project keeps docs organized by audience and document type.
|
||||
|
||||
## Audience Split
|
||||
|
||||
- `user/`: onboarding and usage tasks for self-hosted users.
|
||||
- `operations/`: runbooks and checklists for production operation.
|
||||
- `developer/`: architecture, contribution workflow, and implementation guides.
|
||||
- `reference/`: stable contracts (API, env variables, configuration behavior).
|
||||
|
||||
## Document Quality Rules
|
||||
|
||||
- One primary audience per page.
|
||||
- Task pages must include prerequisites, exact commands, and verification steps.
|
||||
- Reference pages should be contract-first and avoid procedural noise.
|
||||
- Runbooks should include recovery steps and rollback/safety notes.
|
||||
- Prefer concise pages with strong cross-links over long mixed-purpose pages.
|
||||
|
||||
## Required Top-Level Entrypoints
|
||||
|
||||
- `index.md`: user/developer/operator navigation hub.
|
||||
- `user/getting-started.md`: first-run path.
|
||||
- `developer/local-development.md`: contributor setup and validation path.
|
||||
- `operations/overview.md`: operational playbook index.
|
||||
- `reference/overview.md`: contract index.
|
||||
|
|
@ -1,89 +1,105 @@
|
|||
# Theme Inventory (Phase 0)
|
||||
---
|
||||
title: Frontend Theme Inventory
|
||||
sidebar_position: 6
|
||||
---
|
||||
|
||||
This file captures the semantic color system baseline for the theme refactor.
|
||||
# Frontend Theme Inventory
|
||||
|
||||
## Token Domains
|
||||
|
||||
- `scale`:
|
||||
- `brand` (`50..950`)
|
||||
- `info` (`50..950`)
|
||||
- `success` (`50..950`)
|
||||
- `warning` (`50..950`)
|
||||
- `danger` (`50..950`)
|
||||
- `surface`:
|
||||
- `app`
|
||||
- `nav`
|
||||
- `nav_active`
|
||||
- `card`
|
||||
- `card_muted`
|
||||
- `table`
|
||||
- `table_header`
|
||||
- `input`
|
||||
- `overlay`
|
||||
- `text`:
|
||||
- `primary`
|
||||
- `secondary`
|
||||
- `muted`
|
||||
- `inverse`
|
||||
- `link`
|
||||
- `border`:
|
||||
- `default`
|
||||
- `strong`
|
||||
- `subtle`
|
||||
- `interactive`
|
||||
- `focus`:
|
||||
- `ring`
|
||||
- `ring_offset`
|
||||
- `action` variants (`primary`, `secondary`, `ghost`, `danger`):
|
||||
- `bg`
|
||||
- `border`
|
||||
- `text`
|
||||
- `hover_bg`
|
||||
- `hover_border`
|
||||
- `hover_text`
|
||||
- `state` variants (`info`, `success`, `warning`, `danger`):
|
||||
- `bg`
|
||||
- `border`
|
||||
- `text`
|
||||
The theme system uses semantic tokens organized into domains:
|
||||
|
||||
## Theme Sources
|
||||
### Scale Colors
|
||||
|
||||
Theme presets are dynamically loaded from `frontend/src/theme/presets/*.{json,js}`.
|
||||
Color ramps from `50` to `950` for each intent:
|
||||
|
||||
Current preset files:
|
||||
- `brand` - Primary brand color
|
||||
- `info` - Informational elements
|
||||
- `success` - Success states
|
||||
- `warning` - Warning states
|
||||
- `danger` - Error/destructive states
|
||||
|
||||
- `frontend/src/theme/presets/parchment.js`
|
||||
- `frontend/src/theme/presets/lilac.js`
|
||||
- `frontend/src/theme/presets/dune.js`
|
||||
- `frontend/src/theme/presets/oatmeal.js`
|
||||
- `frontend/src/theme/presets/scholarly.json`
|
||||
- `frontend/src/theme/presets/graphite.json`
|
||||
- `frontend/src/theme/presets/tide.json`
|
||||
### Surface
|
||||
|
||||
Each preset contains both `light` and `dark` mode token definitions.
|
||||
Background colors for major UI areas:
|
||||
|
||||
## Adoption Status (Phase 0-3 complete baseline)
|
||||
| Token | Usage |
|
||||
|-------|-------|
|
||||
| `app` | Application background |
|
||||
| `nav` | Navigation bar |
|
||||
| `nav_active` | Active navigation item |
|
||||
| `card` | Card backgrounds |
|
||||
| `card_muted` | Muted card variant |
|
||||
| `table` | Table body |
|
||||
| `table_header` | Table header |
|
||||
| `input` | Form inputs |
|
||||
| `overlay` | Modal/dialog overlays |
|
||||
|
||||
Tokenized foundation components:
|
||||
### Text
|
||||
|
||||
- `AppButton`
|
||||
- `AppCard`
|
||||
- `AppCheckbox`
|
||||
- `AppEmptyState`
|
||||
- `AppHelpHint`
|
||||
- `AppInput`
|
||||
- `AppSelect`
|
||||
- `AppTable`
|
||||
- `AppModal`
|
||||
- `AppHeader`
|
||||
- `AppNav`
|
||||
- `AppAlert`
|
||||
- `AppBadge`
|
||||
- `RunStatusBadge`
|
||||
- `QueueHealthBadge`
|
||||
| Token | Usage |
|
||||
|-------|-------|
|
||||
| `primary` | Default text |
|
||||
| `secondary` | Supporting text |
|
||||
| `muted` | Disabled/placeholder text |
|
||||
| `inverse` | Text on dark backgrounds |
|
||||
| `link` | Hyperlinks |
|
||||
|
||||
Hardening in place:
|
||||
### Border
|
||||
|
||||
- Frontend token policy check script: `frontend/scripts/check_theme_tokens.mjs`
|
||||
- CI enforcement step in `frontend-quality` workflow
|
||||
- Theme preset integrity tests in `frontend/src/theme/presets.test.ts`
|
||||
| Token | Usage |
|
||||
|-------|-------|
|
||||
| `default` | Standard borders |
|
||||
| `strong` | Emphasized borders |
|
||||
| `subtle` | Light separators |
|
||||
| `interactive` | Hover/focus borders |
|
||||
|
||||
### Focus
|
||||
|
||||
| Token | Usage |
|
||||
|-------|-------|
|
||||
| `ring` | Focus ring color |
|
||||
| `ring_offset` | Focus ring offset color |
|
||||
|
||||
### Action Variants
|
||||
|
||||
Each action type (`primary`, `secondary`, `ghost`, `danger`) provides:
|
||||
|
||||
`bg`, `border`, `text`, `hover_bg`, `hover_border`, `hover_text`
|
||||
|
||||
### State Variants
|
||||
|
||||
Each state (`info`, `success`, `warning`, `danger`) provides:
|
||||
|
||||
`bg`, `border`, `text`
|
||||
|
||||
## Theme Presets
|
||||
|
||||
Presets are loaded from `frontend/src/theme/presets/*.{json,js}`. Each contains both `light` and `dark` mode token definitions.
|
||||
|
||||
Current presets:
|
||||
|
||||
| Preset | Format |
|
||||
|--------|--------|
|
||||
| `parchment` | JS |
|
||||
| `lilac` | JS |
|
||||
| `dune` | JS |
|
||||
| `oatmeal` | JS |
|
||||
| `scholarly` | JSON |
|
||||
| `graphite` | JSON |
|
||||
| `tide` | JSON |
|
||||
|
||||
## Tokenized Components
|
||||
|
||||
Foundation components using the token system:
|
||||
|
||||
- `AppButton`, `AppCard`, `AppCheckbox`, `AppEmptyState`
|
||||
- `AppHelpHint`, `AppInput`, `AppSelect`, `AppTable`
|
||||
- `AppModal`, `AppHeader`, `AppNav`, `AppAlert`
|
||||
- `AppBadge`, `RunStatusBadge`, `QueueHealthBadge`
|
||||
|
||||
## Enforcement
|
||||
|
||||
- Token policy check: `frontend/scripts/check_theme_tokens.mjs`
|
||||
- CI step in `frontend-quality` workflow
|
||||
- Preset integrity tests: `frontend/src/theme/presets.test.ts`
|
||||
|
|
|
|||
|
|
@ -1,38 +1,89 @@
|
|||
# Ingestion System & Backoff Strategies
|
||||
---
|
||||
title: Ingestion Pipeline
|
||||
sidebar_position: 5
|
||||
---
|
||||
|
||||
The `ScholarIngestionService` drives the primary data acquisition loop. Since Google Scholar utilizes heavy bot protection and rate limits, this package contains nuanced backoffs to protect user networks from automated IP bans.
|
||||
# Ingestion Pipeline
|
||||
|
||||
## Ingestion Overview
|
||||
The underlying ingestion process:
|
||||
1. Receives an explicit request or background cron trigger to resolve a `scholar_profile_id`.
|
||||
2. Connects asynchronously using configured HTTPX adapters with strict browser headers.
|
||||
3. Downloads the paginated HTML feed for a user across multiple page iterations.
|
||||
4. Uses `regex` and DOM-invariants (e.g. `gsc_vcd_cib`) to pull individual publication blocks.
|
||||
The `ScholarIngestionService` drives the primary data acquisition loop. Google Scholar uses heavy bot protection, so the pipeline includes nuanced backoff strategies to protect user networks from IP bans.
|
||||
|
||||
## Handling 429 Too Many Requests
|
||||
Google Scholar aggressively throws HTTP 429 responses if multiple concurrent tabs or rapidly sequential commands query the same IP address for specific API endpoints (like `citations?view_op=view_citation...`).
|
||||
## Pipeline Overview
|
||||
|
||||
Scholarr treats these distinct from random network timeouts.
|
||||
- **Network Error Retries**: Handled via `ingestion_network_error_retries` with a base backoff of `ingestion_retry_backoff_seconds` (Default 1.0s).
|
||||
- **Rate Limit 429 Retries**: When `ParseState.BLOCKED_OR_CAPTCHA` captures `blocked_http_429_rate_limited`, the system applies a dedicated cooldown. It respects `ingestion_rate_limit_retries` multiplied by `ingestion_rate_limit_backoff_seconds` (Default 30.0s). This prevents the pipeline from fatalizing a user's job completely, pausing operations seamlessly instead.
|
||||
1. The scheduler (or a manual trigger) starts a **run** for one or more scholars.
|
||||
2. The service connects via HTTPX with strict browser headers.
|
||||
3. Paginated HTML feeds are downloaded for each scholar profile.
|
||||
4. A regex + DOM-invariant parser (`gsc_vcd_cib` selectors) extracts publication blocks.
|
||||
5. Publications are fingerprinted and deduplicated against the global store.
|
||||
6. External APIs resolve additional identifiers.
|
||||
7. The PDF resolution pipeline runs asynchronously for publications with known DOIs.
|
||||
|
||||
## Publication Identifiers Loop
|
||||
Once a publication is built, the `gather_identifiers_for_publication` module isolates keys explicitly.
|
||||
- **Local Parsing**: Searches for direct identifiers within the HTML parameters (DOI patterns, arXiv regexes).
|
||||
- **API Fetching**: Queries secondary bibliographic platforms sequentially:
|
||||
- `export.arxiv.org/api/query` (Queries by Title and Author strings).
|
||||
- `crossref.restful` APIs (Queries by Title and Author strings).
|
||||
## Rate Limiting & Backoff
|
||||
|
||||
These identifiers are accumulated in `publication_identifiers` instead of being bound as hard-coded properties, maximizing matching resilience in the automated Unpaywall PDF acquisition stage.
|
||||
### Network Errors
|
||||
|
||||
Handled via `INGESTION_NETWORK_ERROR_RETRIES` (default: 1) with base backoff of `INGESTION_RETRY_BACKOFF_SECONDS` (default: 1.0s).
|
||||
|
||||
### HTTP 429 (Rate Limited)
|
||||
|
||||
When the parser detects `BLOCKED_OR_CAPTCHA` with `blocked_http_429_rate_limited`, a dedicated cooldown applies:
|
||||
|
||||
- Retries: `INGESTION_RATE_LIMIT_RETRIES` (default: 3)
|
||||
- Backoff per retry: `INGESTION_RATE_LIMIT_BACKOFF_SECONDS` (default: 30s)
|
||||
|
||||
This pauses the pipeline gracefully instead of failing the entire run.
|
||||
|
||||
### Safety Cooldowns
|
||||
|
||||
Threshold-based cooldowns halt all ingestion after repeated failures:
|
||||
|
||||
| Threshold | Variable | Default | Cooldown |
|
||||
|-----------|----------|---------|----------|
|
||||
| Blocked failures | `INGESTION_ALERT_BLOCKED_FAILURE_THRESHOLD` | 1 | 1800s (30 min) |
|
||||
| Network failures | `INGESTION_ALERT_NETWORK_FAILURE_THRESHOLD` | 2 | 900s (15 min) |
|
||||
|
||||
## Continuation Queue
|
||||
|
||||
Multi-page ingestion uses a continuation queue to spread load over time:
|
||||
|
||||
- `INGESTION_CONTINUATION_QUEUE_ENABLED` (default: `1`)
|
||||
- Base delay: `INGESTION_CONTINUATION_BASE_DELAY_SECONDS` (default: 120s)
|
||||
- Max delay: `INGESTION_CONTINUATION_MAX_DELAY_SECONDS` (default: 3600s)
|
||||
- Max attempts: `INGESTION_CONTINUATION_MAX_ATTEMPTS` (default: 6)
|
||||
|
||||
Each continuation item is re-enqueued with exponential backoff.
|
||||
|
||||
## Identifier Resolution
|
||||
|
||||
After publication extraction, the `gather_identifiers_for_publication` module resolves identifiers:
|
||||
|
||||
1. **Local parsing** - Searches HTML parameters for DOI patterns and arXiv regex matches.
|
||||
2. **arXiv API** - Queries `export.arxiv.org/api/query` by title and author strings.
|
||||
3. **Crossref API** - Queries Crossref REST API by title and author strings.
|
||||
|
||||
Identifiers are stored in the `publication_identifiers` table rather than as hardcoded properties, maximizing matching resilience for the PDF resolution stage.
|
||||
|
||||
## PDF Resolution
|
||||
|
||||
Publications with resolved DOIs enter the PDF resolution pipeline:
|
||||
|
||||
1. **Unpaywall** - Queries the Unpaywall API for open-access PDF URLs.
|
||||
2. **PDF Discovery** - If Unpaywall returns an OA page URL without a direct PDF link, the service fetches the HTML and searches for PDF link candidates.
|
||||
3. **arXiv Direct** - If an arXiv ID is known, the PDF URL is derived directly.
|
||||
|
||||
Auto-retry is configured via `PDF_AUTO_RETRY_*` variables.
|
||||
|
||||
## arXiv Request Controls
|
||||
- **Global throttle state**: arXiv calls share `arxiv_runtime_state` so all workers respect one cooldown/interval clock.
|
||||
- **Query cache**: identical request parameters map to a stable fingerprint and are stored in `arxiv_query_cache_entries`.
|
||||
- **In-flight coalescing**: duplicate concurrent misses join one outbound request instead of fan-out.
|
||||
- **Caller load-shedding**: arXiv lookups are skipped when high-confidence DOI/arXiv evidence already exists, or when title quality is below threshold.
|
||||
|
||||
## arXiv Observability Events
|
||||
- `arxiv.request_scheduled`: emitted before a gated request; includes `wait_seconds`, `cooldown_remaining_seconds`, `source_path`.
|
||||
- `arxiv.request_completed`: emitted after response; includes `status_code`, `wait_seconds`, `cooldown_remaining_seconds`, `source_path`.
|
||||
- `arxiv.cooldown_activated`: emitted when status `429` triggers cooldown.
|
||||
- `arxiv.cache_hit` / `arxiv.cache_miss`: emitted on query cache lookup with `source_path`.
|
||||
- **Global throttle**: arXiv calls share `arxiv_runtime_state` so all workers respect one cooldown/interval clock.
|
||||
- **Query cache**: Identical request parameters are fingerprinted and cached in `arxiv_query_cache_entries`.
|
||||
- **In-flight coalescing**: Duplicate concurrent misses join one outbound request.
|
||||
- **Load shedding**: arXiv lookups are skipped when high-confidence DOI/arXiv evidence already exists, or when title quality is below threshold.
|
||||
|
||||
### Observability Events
|
||||
|
||||
| Event | Description |
|
||||
|-------|-------------|
|
||||
| `arxiv.request_scheduled` | Emitted before a gated request. Includes `wait_seconds`, `cooldown_remaining_seconds`, `source_path`. |
|
||||
| `arxiv.request_completed` | Emitted after response. Includes `status_code`, `wait_seconds`, `source_path`. |
|
||||
| `arxiv.cooldown_activated` | Emitted when status `429` triggers cooldown. |
|
||||
| `arxiv.cache_hit` / `arxiv.cache_miss` | Emitted on query cache lookup with `source_path`. |
|
||||
|
|
|
|||
|
|
@ -1,52 +1,100 @@
|
|||
# Developer Local Development
|
||||
---
|
||||
title: Local Development
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
## Start the Dev Stack
|
||||
# Local Development
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker and Docker Compose v2+
|
||||
- Python 3.12+ (for IDE support and local linting)
|
||||
- Node.js 20+ (for frontend development)
|
||||
|
||||
## Starting the Dev Stack
|
||||
|
||||
The development compose file overlays the production config with hot-reload and a separate Vite dev server:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build
|
||||
```
|
||||
|
||||
Open:
|
||||
- API: `http://localhost:8000`
|
||||
- Frontend dev server: `http://localhost:5173`
|
||||
This starts three services:
|
||||
|
||||
Stop:
|
||||
| Service | Port | Description |
|
||||
|---------|------|-------------|
|
||||
| `db` | 5432 (internal) | PostgreSQL 15 |
|
||||
| `app` | 8000 | FastAPI backend with `APP_RELOAD=1` |
|
||||
| `frontend` | 5173 | Vite dev server proxying API calls to `app:8000` |
|
||||
|
||||
### Dev-Specific Overrides
|
||||
|
||||
The `docker-compose.dev.yml` file applies these changes:
|
||||
|
||||
- **`app`**: Uses `scholarr-dev:local` image built from the `dev` stage. Mounts the project root as `/app` for hot reload. Disables `SESSION_COOKIE_SECURE` and the built frontend.
|
||||
- **`frontend`**: Node 20 container running `npm install && npm run dev`. Mounts `./frontend` with a named volume for `node_modules`. Uses polling for file watching (`CHOKIDAR_USEPOLLING=1`).
|
||||
|
||||
## Environment Setup
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml down
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
## Backend Validation
|
||||
Set at minimum:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml run --rm app uv run pytest tests/unit
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml run --rm app uv run pytest -m integration
|
||||
POSTGRES_PASSWORD=localdev
|
||||
SESSION_SECRET_KEY=local-dev-secret-at-least-32-characters
|
||||
SESSION_COOKIE_SECURE=0
|
||||
```
|
||||
|
||||
## Frontend Validation
|
||||
## Running Tests
|
||||
|
||||
All tests run inside containers:
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
npm run typecheck
|
||||
npm run test:run
|
||||
npm run build
|
||||
# Run unit tests (default: excludes integration markers)
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml run --rm app \
|
||||
python -m pytest
|
||||
|
||||
# Run integration tests
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml run --rm app \
|
||||
python -m pytest -m integration
|
||||
|
||||
# Run a specific test file
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml run --rm app \
|
||||
python -m pytest tests/unit/test_fingerprints.py -v
|
||||
```
|
||||
|
||||
## Repository Gates
|
||||
See [Testing](testing.md) for markers, fixtures, and conventions.
|
||||
|
||||
## Linting and Type Checking
|
||||
|
||||
```bash
|
||||
python3 scripts/check_frontend_api_contract.py
|
||||
python3 scripts/check_env_contract.py
|
||||
./scripts/check_no_generated_artifacts.sh
|
||||
# Ruff linting
|
||||
ruff check .
|
||||
|
||||
# Ruff formatting check
|
||||
ruff format --check .
|
||||
|
||||
# Mypy type checking
|
||||
mypy app/
|
||||
```
|
||||
|
||||
## Docs Site (Contributor Workflow)
|
||||
Ruff config is in `pyproject.toml`: target Python 3.12, line length 120, rules `E F W I UP B SIM RUF`.
|
||||
|
||||
Docs tooling is colocated in `docs/website/`:
|
||||
## Database Migrations
|
||||
|
||||
Alembic migrations run automatically on startup when `MIGRATE_ON_START=1`. To run manually:
|
||||
|
||||
```bash
|
||||
cd docs/website
|
||||
npm install
|
||||
npm run build
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml run --rm app \
|
||||
alembic upgrade head
|
||||
```
|
||||
|
||||
To create a new migration:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml run --rm app \
|
||||
alembic revision --autogenerate -m "description of change"
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1,11 +1,65 @@
|
|||
# Developer Documentation
|
||||
---
|
||||
title: Developer Overview
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
Use this section if you are contributing code or running full quality gates.
|
||||
# Developer Overview
|
||||
|
||||
- [Documentation Standards](./documentation-standards.md)
|
||||
- [Local Development](./local-development.md)
|
||||
- [Architecture Boundaries](./architecture.md)
|
||||
- [Ingestion API & Backoff Strategies](./ingestion.md)
|
||||
- [API Contracts](./api-contract.md)
|
||||
- [Contributing](./contributing.md)
|
||||
- [Frontend Theme Inventory](./frontend-theme-inventory.md)
|
||||
Scholarr is a Python 3.12+ FastAPI backend with an async SQLAlchemy ORM layer, PostgreSQL database, and a Vue 3 + TypeScript + Vite frontend.
|
||||
|
||||
## Stack
|
||||
|
||||
| Layer | Technology |
|
||||
|-------|------------|
|
||||
| Backend | Python 3.12+, FastAPI, SQLAlchemy 2.0 (async/asyncpg), Alembic |
|
||||
| Frontend | TypeScript, Vue 3, Vite, Tailwind CSS |
|
||||
| Database | PostgreSQL 15 |
|
||||
| Infrastructure | Multi-stage Docker, Docker Compose |
|
||||
| Linting | ruff (E, F, W, I, UP, B, SIM, RUF), mypy |
|
||||
| Testing | pytest, pytest-asyncio |
|
||||
| Versioning | python-semantic-release, conventional commits |
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Build the dev image and start services
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build
|
||||
|
||||
# Backend: http://localhost:8000 (hot reload enabled)
|
||||
# Frontend: http://localhost:5173 (Vite dev server with API proxy)
|
||||
```
|
||||
|
||||
See [Local Development](local-development.md) for the full setup guide.
|
||||
|
||||
## Project Layout
|
||||
|
||||
```
|
||||
app/
|
||||
├── api/routers/ # FastAPI route handlers
|
||||
├── auth/ # Session + CSRF middleware
|
||||
├── db/ # SQLAlchemy models, session factory, migrations
|
||||
├── services/ # Domain service modules (see Architecture)
|
||||
│ ├── arxiv/ # arXiv API client, cache, rate limiting
|
||||
│ ├── crossref/ # Crossref DOI lookups
|
||||
│ ├── dbops/ # Database integrity + repair operations
|
||||
│ ├── ingestion/ # Run orchestration, scheduler, safety gates
|
||||
│ ├── openalex/ # OpenAlex metadata matching
|
||||
│ ├── portability/ # Import/export workflows
|
||||
│ ├── publication_identifiers/ # Multi-identifier resolution
|
||||
│ ├── publications/ # Listing, enrichment, dedup, PDF queue
|
||||
│ ├── runs/ # Run history, continuation queue
|
||||
│ ├── scholar/ # HTML parser, source fetch adapters
|
||||
│ ├── scholars/ # Scholar CRUD, image upload, name search
|
||||
│ └── unpaywall/ # Unpaywall PDF discovery
|
||||
├── main.py # App factory, lifespan, middleware stack
|
||||
frontend/
|
||||
├── src/
|
||||
│ ├── components/ # Reusable Vue components
|
||||
│ ├── theme/presets/ # Color theme presets (light + dark)
|
||||
│ └── ...
|
||||
scripts/db/ # Operational database scripts
|
||||
tests/
|
||||
├── unit/ # Fast, no-database tests
|
||||
├── integration/ # Tests requiring database + services
|
||||
└── fixtures/ # Shared test fixtures
|
||||
```
|
||||
|
|
|
|||
102
docs/developer/testing.md
Normal file
102
docs/developer/testing.md
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
---
|
||||
title: Testing
|
||||
sidebar_position: 7
|
||||
---
|
||||
|
||||
# Testing
|
||||
|
||||
## Running Tests
|
||||
|
||||
All tests must run inside containers:
|
||||
|
||||
```bash
|
||||
# Unit tests (default: excludes integration markers)
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml run --rm app \
|
||||
python -m pytest
|
||||
|
||||
# Integration tests
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml run --rm app \
|
||||
python -m pytest -m integration
|
||||
|
||||
# Specific marker
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml run --rm app \
|
||||
python -m pytest -m db
|
||||
|
||||
# Verbose output for a specific file
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml run --rm app \
|
||||
python -m pytest tests/unit/test_fingerprints.py -v
|
||||
```
|
||||
|
||||
## Test Configuration
|
||||
|
||||
From `pyproject.toml`:
|
||||
|
||||
```toml
|
||||
[tool.pytest.ini_options]
|
||||
addopts = "-q -m \"not integration\" --import-mode=importlib"
|
||||
asyncio_mode = "auto"
|
||||
testpaths = ["tests"]
|
||||
```
|
||||
|
||||
- Default run excludes `integration` marked tests
|
||||
- Uses `importlib` import mode to resolve module name collisions
|
||||
- Async tests run automatically (no `@pytest.mark.asyncio` needed)
|
||||
|
||||
## Markers
|
||||
|
||||
| Marker | Description |
|
||||
|--------|-------------|
|
||||
| `integration` | Tests requiring external services (database, network) |
|
||||
| `db` | Tests that validate database behavior and constraints |
|
||||
| `migrations` | Tests focused on Alembic schema migration correctness |
|
||||
| `schema` | Tests focused on multi-tenant schema invariants |
|
||||
| `smoke` | Smoke tests for containerized runtime |
|
||||
|
||||
## Test Tiers
|
||||
|
||||
### Unit Tests (`tests/unit/`)
|
||||
|
||||
Fast, no-database tests. Mock external dependencies. These run by default.
|
||||
|
||||
Examples:
|
||||
- `test_fingerprints.py` - Publication fingerprinting logic
|
||||
- `test_scholar_parser.py` - HTML parsing without network calls
|
||||
- `test_doi_normalize.py` - DOI normalization rules
|
||||
- `test_ingestion_arxiv_rate_limit.py` - Rate limiter behavior
|
||||
- `test_publication_pdf_resolution_pipeline.py` - PDF pipeline logic
|
||||
|
||||
Domain-specific unit tests are organized under `tests/unit/services/domains/`:
|
||||
- `arxiv/` - Cache, client, gateway, guards, parser, rate limit tests
|
||||
- `openalex/` - Client and matching tests
|
||||
- `publications/` - Dedup tests
|
||||
|
||||
### Integration Tests (`tests/integration/`)
|
||||
|
||||
Require a running database. Test full request/response flows and data consistency.
|
||||
|
||||
Examples:
|
||||
- `test_api_v1.py` - API endpoint integration tests
|
||||
- `test_db_integrity.py` - Database integrity checks
|
||||
- `test_run_lifecycle_consistency.py` - Run state machine transitions
|
||||
- `test_deferred_enrichment.py` - Enrichment pipeline with real data
|
||||
- `test_fixture_probe_runs.py` - Fixture-based run probes
|
||||
|
||||
### Smoke Tests
|
||||
|
||||
Marked with `@pytest.mark.smoke`. Validate the containerized runtime starts and serves basic requests.
|
||||
|
||||
## Fixtures
|
||||
|
||||
Test fixtures live in `tests/fixtures/`:
|
||||
|
||||
```
|
||||
tests/fixtures/
|
||||
└── scholar/
|
||||
├── profile_ok_amIMrIEAAAAJ.html # Successful profile HTML
|
||||
└── regression/
|
||||
├── profile_P1RwlvoAAAAJ.html # Regression case
|
||||
├── profile_LZ5D_p4AAAAJ.html # Regression case
|
||||
└── profile_AAAAAAAAAAAA.html # Regression case
|
||||
```
|
||||
|
||||
Scholar HTML fixtures are real Google Scholar profile pages used to test parser robustness against DOM structure changes.
|
||||
Loading…
Add table
Add a link
Reference in a new issue