Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 35 additions & 4 deletions .s2s/BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

---

Expand Down
Loading