diff --git a/.s2s/BACKLOG.md b/.s2s/BACKLOG.md index fbf8fc3..85413f0 100644 --- a/.s2s/BACKLOG.md +++ b/.s2s/BACKLOG.md @@ -71,18 +71,49 @@ The RAG pipeline and `/api/v1/search` are unaffected: they resolve the provider ### DEBT-028: chunk quota counts a table the active provider may not write -**Status**: planned | **Priority**: low | **Created**: 2026-07-13 +**Status**: resolved (2026-07-15) | **Priority**: low | **Created**: 2026-07-13 | **PR**: #114 **Origin**: BUG-023 (2026-07-13). **Context**: `check_namespace_quota` (`vektra-admin/quotas.py:91`) counts `document_chunks` with raw SQL ("to avoid cross-module imports"). Under ADR-0026 that table is private to the pgvector provider, so in Qdrant mode the count is always 0 and the chunk quota would never be enforced. It is currently **dead code**: `check_namespace_quota` has no callers. The defect is therefore latent, not live, which is the only reason it is not filed as a bug. +**Resolution**: routed, not removed. `check_namespace_quota` now counts chunks through `VectorStoreProvider.count_chunks()` (the provider injected as the `vektra_shared` Protocol, so the import-linter boundary holds) instead of the pgvector-private `document_chunks`. Kept rather than deleted because the quota is a live requirement (REQ-048, must) — the function is parked scaffolding of the admin-enforcement plan, with no production callers yet. + +**Acceptance criteria**: +- [x] The chunk count goes through `VectorStoreProvider.count_chunks()` +- [x] Kept (not removed): the quota backs a live requirement, so the function is corrected rather than deleted + +**Scope note**: this de-trapped the *counting* only. Nothing in the ingest path calls `check_namespace_quota`, so the quota is still enforced nowhere — see FEAT-025. + +**Traceability**: ADR-0026, BUG-023, REQ-048 (spawned FEAT-025) + +--- + +### FEAT-025: wire namespace quota enforcement into the ingest path + +**Status**: planned | **Priority**: medium | **Created**: 2026-07-15 +**Origin**: DEBT-028 (2026-07-15), which fixed the quota *count* and surfaced that nothing enforces it. + +**Context**: REQ-048 (priority **must**) names quota enforcement as part of its Phase 2 scope — "Phase 2: multi-namespace with per-API-key namespace restrictions, **quota enforcement**, and per-namespace configuration overrides". The pieces exist: the `namespaces` table carries `quota_documents` / `quota_chunks`, and `check_namespace_quota` (`vektra-admin/quotas.py`) now counts correctly through the vector store (DEBT-028). But **no path calls it**: an operator can set `quota_chunks=1000` on a namespace and ingest will blow straight past it. The control is configurable and unenforced. + +This is the same family as DEBT-032 (REQ-064's "cleanup afterwards" clause): a clause of a **must** requirement, named in the requirement, never built — not debt, an unbuilt obligation. It is filed as a FEAT because it is a capability to build, not a regression to fix. + +**Note on priority**: spec-priority and operational urgency diverge here, and honestly. REQ-048 is *must*, but quota enforcement matters for **multi-tenant** deployments; the current single-tenant e-learning deployment does not need it today. Tagged medium, not high, for that reason — raise it when multi-tenant / resource-capping actually lands on the roadmap. It should not sit at *low*, because a configurable-but-ignored control is a trap (an operator who sets a quota reasonably expects it to hold). + +**Proposed approach**: the check must run where the chunk count is known — after chunking, before store — but **`vektra_ingest` cannot import `vektra_admin`**, where `check_namespace_quota` lives (import-linter contract "vektra_ingest must not import from other vektra components"). So do **not** call it directly from the pipeline. Two boundary-respecting options, to be decided as part of the design: +1. **Move the quota logic into `vektra_shared`** (it is a cross-cutting concern like the other things there) and have both `vektra_admin` and the ingest path call it — `vektra_ingest` *is* allowed to import `vektra_shared`. +2. **Inject an enforcement hook through the registry.** `run_ingest` already receives `registry` and pulls its providers from it; a quota-enforcement callable registered by the app layer (which can import both components) would let the pipeline enforce without importing `vektra_admin`. This matches the existing provider-injection pattern. +On exceed, fail the ingest with the REQ-010 envelope (`ERR-QUOTA-001` already exists) and a clear remediation, and store nothing (decide what happens to a document already partway through extraction). + **Acceptance criteria**: -- [ ] The chunk count goes through `VectorStoreProvider.count_chunks()` before the quota is wired up to anything -- [ ] Or, if the quota is not going to be used, the function is removed rather than left as a trap +- [ ] An ingest that would exceed `quota_chunks` (or `quota_documents`) is rejected with the `ERR-QUOTA-001` envelope, and stores nothing +- [ ] An ingest within quota is unaffected; a namespace with null quotas is unaffected (Phase 1 behaviour preserved) +- [ ] Verified against `VEKTRA_VECTOR_STORE_PROVIDER=qdrant`, since the count now goes through the provider (this is the path DEBT-028 fixed) — a pgvector-only test would not exercise it +- [ ] The cross-namespace / active-version subtleties of `count_chunks` are respected (a reindex mid-flight must not let a namespace slip its quota) +- [ ] The import-linter contracts stay green: `vektra_ingest` does not import `vektra_admin` (the enforcement is wired through `vektra_shared` or the registry, per the approach above) -**Traceability**: ADR-0026, BUG-023 +**Traceability**: REQ-048 (must, Phase 2 clause), DEBT-028 (the count it builds on), ADR-0026 (the Protocol it counts through), ERR-QUOTA-001 ---