diff --git a/CLAUDE.md b/CLAUDE.md index 1b14cd2..2439b87 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,9 +37,9 @@ driving `tests/e2e/test_compose_smoke.py` against a real uvicorn process, which skips itself unless `SMOKE_BASE_URL` is set. That maps to plan 001 Phases 1-6 done. -Plan 002 (`/v1/points` CRUD and keyword search) is **Phase 1 done**. Landed: the -`PointRepository` port (`src/application/ports/point_repository.py`) with its -`Point` read model (`src/application/points/point.py`), the Qdrant adapter +Plan 002 (`/v1/points` CRUD and keyword search) is **Phases 1-2 done**. Phase 1 +landed the `PointRepository` port (`src/application/ports/point_repository.py`) +with its `Point` read model (`src/application/points/point.py`), the Qdrant adapter (`src/infrastructure/qdrant/point_repository.py`), request/response schemas (`src/api/schemas/points.py`), and lifespan wiring. This port is **separate from `PointStorage`**, which stays exactly the two bulk operations ingestion @@ -59,9 +59,31 @@ and ordered listing paginates by `order_id` value rather than offset (Qdrant returns no page offset under `order_by`, and an offset cursor skips or repeats rows under a concurrent insert). -Not built yet: plan 002 Phases 2-6 — the `/v1/points` routes themselves, soft -delete with neighbour relinking, create/replace/patch, reorder and batch, and -the `api_request_logs`/`point_audit_events` tables — and `src/agent/`. +Phase 2 added the **read routes**: `GET /v1/points/{point_id}`, +`GET /v1/points?file_id=...`, `GET /v1/points/count`, `GET /v1/points/search`, +and `GET /v1/files/{file_id}/points`, over `src/application/points/queries.py` +(`src/api/routers/points.py`). All are gated on `points:read`, which — with +`points:write` — is now in `DEFAULT_SCOPES`; `GET /v1/files/{file_id}/points` +uses `points:read` rather than `files:write`, so the scope follows the data +rather than the URL prefix. `PointNotFoundError` maps to `404` in +`src/api/errors.py`, never `403`. Three route-level rules are load-bearing: +`/count` and `/search` are declared **before** `/{point_id}` (FastAPI matches in +declaration order, so reordering them makes `/v1/points/count` a `422`), +`file_id` is **required** on the listing (the cursor is an `order_id` value and +`order_id` is unique only within one file), and `search_points` folds the query +with `normalize_persian_text` before matching, because ingestion letter-folds +content and an unfolded Arabic-keyboard query would return an empty result set +silently rather than erroring (ADR-0002). + +Also worth knowing before touching the points tests: `tests/support/point_contract.py` +holds **one** scenario suite run against both `FakePointRepository` (unit) and +`QdrantPointRepository` (integration), so new repository behaviour belongs there +rather than in one of the two runners — that is what keeps the fake from drifting +more permissive than the real store. + +Not built yet: plan 002 Phases 3-6 — soft delete with neighbour relinking, +create/replace/patch, reorder and batch, and the +`api_request_logs`/`point_audit_events` tables — and `src/agent/`. Architecture decisions live in `docs/adr/` (18 ADRs plus the 0000 template; 0001–0004 are `Accepted` — 0004 amended by 0018; 0014 is `Superseded by 0017`;