diff --git a/HACKATHON_TEAM_PLAN.md b/HACKATHON_TEAM_PLAN.md new file mode 100644 index 0000000..cd85c17 --- /dev/null +++ b/HACKATHON_TEAM_PLAN.md @@ -0,0 +1,227 @@ +# ProjectOS — 16-Person Hackathon Team Plan + +> Companion to `architecture/section_01...18`. This document does not change the architecture — it maps your actual 16-person roster onto it so parallel work doesn't collide, and replaces Section 16's small-team sequential phasing with a parallel-squad model. + +--- + +## 0. Named Roster + +| Name | Role | +|---|---| +| Karan Bosamiya | Sr Software Engineer — Python | +| Ishit Desai | Software Engineer — Python | +| Meet Shah | Software Engineer — Python | +| Parth Shah | Software Engineer — Python | +| Prit Chavda | Software Engineer — Machine Learning | +| Khushal Batham | Sr Software Engineer — React | +| Bhargav Valani | Software Engineer — React | +| Shubham Nayak | QA Engineer | +| Tarun Nagpal | Trainee — AI/ML | +| Aman Rajput | Trainee — Data Engineering | +| Chaitanya Vekaria | Trainee — DevOps | +| Arjun Mulagapaka | Trainee — Python | +| Devansh Jagatiya | Trainee — Python | +| Parth Harpal | Trainee — Python | +| Smit Thakkar | Trainee — Python | +| Chandresh Thakkar | Trainee — QA | + +--- + +## 9. Phased Module Plan — Named Assignments + +This is the concrete, name-level version of §2–§5: six named modules, three phases, everyone's responsibility spelled out so nothing falls through at integration time. Read each module's row across all three phases to see one person's/pair's full arc through the hackathon. + +### The Six Modules + +| Module | Maps to Squad | Owns | +|---|---|---| +| **M1 — Core Engine** (AI Service + RAG + Auth) | A | §4, §7, §8, §13 | +| **M2 — Knowledge Hub** (ingestion, chunking, connectors) | B | §6 | +| **M3 — Planning Engine** (requirements → stories → sprints) | C | §8.6.1–8.6.3, §9.4–9.5 | +| **M4 — Meetings & Risk Intelligence** | D | §8.6.4–8.6.5, §9.6–9.7 | +| **M5 — Frontend Experience** | E | §12.3, all UI | +| **M6 — DevOps & Quality** | F | §10, §14, §17 | + +--- + +### Phase 1 — Foundation & Interface Freeze (Hours 0–3) + +Goal: every module has a working skeleton and a frozen contract to build against. Nobody starts real feature work until M1's stub interfaces are published — this is the one moment the whole team is synchronized. + +- **M1 — Core Engine**: scaffold `app/core/*`, DB connection, GitHub/Microsoft OAuth + JWT skeleton, **publish stub `AIService.call_claude()` and `rag_service.retrieve()` signatures by hour 1.5** *(Karan Bosamiya — Sr SE Python, lead; Prit Chavda — SE Machine Learning; Tarun Nagpal — Trainee AI/ML)* +- **M2 — Knowledge Hub**: scaffold `SourceConnector` protocol, stub `GitHubConnector`, read M1's stub contracts *(Ishit Desai — SE Python, lead; Aman Rajput — Trainee Data Engineering; Arjun Mulagapaka — Trainee Python)* +- **M3 — Planning Engine**: scaffold Pydantic schemas for work packages/stories/sprints, requirements upload endpoint shell *(Meet Shah — SE Python, lead; Devansh Jagatiya — Trainee Python; Parth Harpal — Trainee Python)* +- **M4 — Meetings & Risk**: scaffold meeting/risk schemas, transcript submission endpoint shell *(Parth Shah — SE Python, lead; Smit Thakkar — Trainee Python)* +- **M5 — Frontend Experience**: app shell, routing, login screen, shared component library kickoff (data table, form panel, status badge) *(Khushal Batham — Sr SE React, lead; Bhargav Valani — SE React)* +- **M6 — DevOps & Quality**: `docker-compose.yml`, CI skeleton (lint + test on PR), `.env.example`, draft test plan against §9's endpoint tables *(Chaitanya Vekaria — Trainee DevOps; Shubham Nayak — QA Engineer, lead; Chandresh Thakkar — Trainee QA)* + +**Phase 1 exit checkpoint**: a user can log in, M1's stub `AIService`/`rag_service` are merged to `main`, and every other module compiles against them. + +--- + +### Phase 2 — Core Feature Build (Hours 3–14) + +Goal: each module builds its real feature set in parallel against M1's contracts, swapping stubs for real implementations without breaking anyone downstream. + +- **M1 — Core Engine**: swap in real Claude calls (retry/backoff, §8.5), real ChromaDB retrieval, prompt caching; build `chat_service.py` backend (SSE + citation resolution) *(Karan Bosamiya — Sr SE Python; Prit Chavda — SE Machine Learning; Tarun Nagpal — Trainee AI/ML)* +- **M2 — Knowledge Hub**: markdown/YAML/ticket chunkers, then tree-sitter code chunker, initial full-index pipeline, `POST /knowledge/repositories` *(Ishit Desai — SE Python; Aman Rajput — Trainee Data Engineering; Arjun Mulagapaka — Trainee Python)* +- **M3 — Planning Engine**: work-package extraction + dedup, story generation + INVEST critique, greedy sprint bin-packing + dependency inference *(Meet Shah — SE Python; Devansh Jagatiya — Trainee Python owns requirements extraction; Parth Harpal — Trainee Python owns story CRUD/sprint endpoints)* +- **M4 — Meetings & Risk**: meeting summarization (text path first, audio via faster-whisper if time allows), non-LLM risk checks (overloaded sprints, slipping action items) + LLM risk digest, Slack alert *(Parth Shah — SE Python; Smit Thakkar — Trainee Python owns meeting endpoint + action-item CRUD)* +- **M5 — Frontend Experience**: project/dashboard screens, requirements/stories UI, sprint Kanban (`@dnd-kit`), chat UI with SSE streaming — in that priority order per §6's frontend risk note *(Khushal Batham — Sr SE React; Bhargav Valani — SE React)* +- **M6 — DevOps & Quality**: integration tests landing as each module's endpoints ship, deploy to demo host, nginx SSE config, manual scenario testing begins on completed features *(Chaitanya Vekaria — Trainee DevOps; Shubham Nayak — QA Engineer; Chandresh Thakkar — Trainee QA)* + +**Phase 2 exit checkpoint**: each module's demo checkpoint from §16.3–16.5 passes in isolation — story generation works, a connected repo answers chat questions with citations, a meeting produces a linked risk register entry. + +--- + +### Phase 3 — Integration, Polish & Demo Prep (Hours 14–24) + +Goal: modules stop working in isolation and start working *together* — this is where "everyone gets work" turns into "the system actually holds together end to end." + +- **M1 — Core Engine**: architecture diagram generation (stretch, only if ahead), then float to unblock whichever module is stuck *(Karan Bosamiya; Prit Chavda; Tarun Nagpal)* +- **M2 — Knowledge Hub**: index health endpoint, incremental webhook re-index (stretch), pair with M5 on chat citation rendering end to end *(Ishit Desai; Aman Rajput; Arjun Mulagapaka)* +- **M3 — Planning Engine**: story export to GitHub Issues (stretch), pair with M5 to finish Kanban drag-and-drop wiring *(Meet Shah; Devansh Jagatiya; Parth Harpal)* +- **M4 — Meetings & Risk**: meeting-to-story linking (rapidfuzz), `story_events` review flags, pair with M3 since linking writes to the same `stories` table *(Parth Shah; Smit Thakkar)* +- **M5 — Frontend Experience**: risk register UI, knowledge hub management UI (only if the golden path is already solid — see §6), final polish on golden-path screens *(Khushal Batham; Bhargav Valani)* +- **M6 — DevOps & Quality**: end-to-end smoke tests across all six modules together, freeze the deploy, drive **two full demo dress rehearsals**, final regression pass *(Chaitanya Vekaria; Shubham Nayak — leads rehearsal checklist; Chandresh Thakkar)* + +**Phase 3 exit checkpoint**: the full golden-path demo runs start to finish without a live engineer intervening — upload requirements → generate stories → plan sprint → connect a repo → ask a cited onboarding question → submit a meeting → see a risk register entry and a Slack alert. + +--- + +### Cross-Module Pairing at Integration Points (Why Nobody Works in a Silo) + +- **M1 ↔ M2/M3/M4**: every module's Phase 2 work depends on M1's real implementation landing on schedule — Karan's squad is the one that must not slip, since a delay there delays all three. +- **M2 ↔ M5**: chat citations only render correctly if M2's `knowledge_chunks` metadata and M1's `chat_service.py` citation format agree — Ishit and Khushal's squads should sync directly at the Phase 2→3 boundary, not just via the general standup. +- **M3 ↔ M4**: `story_events` (meeting-driven story flags, FR-5.5) is written by M4 but displayed against M3's story records — Parth Shah and Meet Shah should agree on the `story_events` schema by end of Phase 1, not discover the mismatch in Phase 3. +- **M5 ↔ everyone**: every backend module ships its API before the corresponding frontend screen can be finished — Khushal/Bhargav should get read access to each module's Pydantic response schemas (§4.8) as soon as they're drafted in Phase 1, not wait for the endpoint to be fully working. + +## 1. Your Roster + +| # | Role | Level | +|---|---|---| +| 1 | Software Engineer — Python | Sr | +| 2 | Software Engineer — Python | Mid | +| 3 | Software Engineer — Python | Mid | +| 4 | Software Engineer — Python | Mid | +| 5 | Software Engineer — Machine Learning | Mid | +| 6 | Software Engineer — React | Sr | +| 7 | Software Engineer — React | Mid | +| 8 | QA Engineer | Mid | +| 9 | Trainee — AI/ML | Jr | +| 10 | Trainee — Data Engineering | Jr | +| 11 | Trainee — DevOps | Jr | +| 12 | Trainee — Python | Jr | +| 13 | Trainee — Python | Jr | +| 14 | Trainee — Python | Jr | +| 15 | Trainee — Python | Jr | +| 16 | Trainee — QA | Jr | + +**Skill distribution that shapes this plan**: 4 experienced Python engineers (1 Sr + 3 Mid) is enough to lead one backend module each. 1 ML engineer is a strong, deliberate fit for the AI/RAG core rather than a generic backend slot. Frontend has exactly 2 people (1 Sr, 1 Mid) for the entire UI surface — this is the tightest resource in the roster and is called out explicitly in §6. 6 trainees (4 Python, 1 AI/ML, 1 Data Engineering) need pairing with a senior/mid on their squad, not independent ownership of a module. + +--- + +## 2. Squad Structure (16 people, 6 squads) + +Squad boundaries follow the folder/service ownership lines in §12.2 and §4.4.3, and every squad with trainees has at least one experienced engineer to pair with — a squad is never all-trainee. + +| Squad | Members | Owns (folder paths) | Sections | +|---|---|---|---| +| **A — Platform & AI Core** | Sr SE Python (lead), SE Machine Learning, Trainee AI/ML | `app/core/*`, `ai_service.py`, `rag_service.py`, `ai/prompts.py`, `chat_service.py`, `arch_diagram_service.py`, auth, early migrations | §4, §5, §7, §8, §13 | +| **B — Knowledge Hub** | SE Python #1 (lead), Trainee Data Engineering, Trainee Python #1 | `services/knowledge_hub/*`, `api/v1/knowledge.py`, `api/v1/webhooks.py` | §6 | +| **C — Planning Engine** | SE Python #2 (lead), Trainee Python #2, Trainee Python #3 | `requirements_service.py`, `story_service.py`, `sprint_service.py`, `api/v1/{requirements,stories,sprints}.py` | §8.6.1–8.6.3, §9.4–9.5 | +| **D — Meetings & Risk** | SE Python #3 (lead), Trainee Python #4 | `meeting_service.py`, `risk_service.py`, `whisper_service.py`, `api/v1/{meetings,risks}.py` | §8.6.4–8.6.5, §9.6–9.7 | +| **E — Frontend** | Sr SE React (lead), SE React | `frontend/*` — entire UI surface | §12.3, all FR UI requirements | +| **F — DevOps/QA** | Trainee DevOps, QA Engineer (lead), Trainee QA | `infra/*`, `.github/workflows/*`, `tests/integration/*`, deploy pipeline, demo environment | §10, §14, §17 | + +### Why the ML engineer goes to Squad A, not a generic backend squad + +Embedding model selection (Voyage vs. OpenAI, §11.2), retrieval quality (re-ranking heuristics, chunk-type filtering, §7.4), and prompt template design for structured output (§8.3) are the parts of this system where ML judgment matters more than general backend skill. Pairing the ML engineer with the Sr Python engineer on `rag_service.py`/`ai_service.py`, and pairing the AI/ML trainee under the ML engineer, concentrates the one ML-specific skill set exactly where the architecture needs it most — and gives the trainee real mentorship instead of being dropped into a generic Python squad where nobody can review their embedding/prompt-quality decisions. + +### Why Data Engineering trainee goes to Knowledge Hub + +The ingestion pipeline (§6.3) — connectors pulling from GitHub/Azure DevOps, chunking, batch embedding, incremental re-indexing — is structurally an ETL pipeline. This is the one trainee-role-to-module match in the roster that's almost exact; use it. + +--- + +## 3. The Interface-Freeze Strategy (Load-Bearing Idea) + +The biggest risk is Squads B, C, D all needing `AIService.call_claude()` and `rag_service.retrieve()` before Squad A has a real implementation. Squad A's Sr Python engineer publishes both function signatures plus Pydantic schemas in the **first 90 minutes**, backed by a stub: + +```python +# Hour 0–1.5 deliverable from Squad A — merged to main immediately +class AIService: + async def call_claude(self, project_id, prompt_template, variables, model="sonnet", + response_schema=None, stream=False, cache_key=None): + # STUB: returns canned responses matching response_schema + ... + +async def retrieve(project_id, query, collection_name, chunk_types=None, + repository_id=None, top_k=10) -> list[RetrievedChunk]: + # STUB: returns fixed fake RetrievedChunk objects + ... +``` + +- Squads B/C/D build their entire feature against the stub using real Pydantic schemas (§4.8, §5), so code compiles and tests pass against realistic shapes from hour 2 onward. +- The ML engineer and Sr Python engineer swap in the real implementation (real Claude calls, real Chroma retrieval, retry/caching per §8.4–§8.5) over the following hours **without changing the signature** — downstream squads' code doesn't change when the swap lands. +- **Non-negotiable rule**: nobody outside Squad A edits `ai_service.py` or `rag_service.py`. Requests for new capability go to Squad A as a ticket, not a PR. + +This matters even more with a trainee-heavy roster than it would with 16 senior engineers — trainees in Squads B/C/D need a stable, documented contract to build against; they're not the ones who should be debugging a half-finished `rag_service.py` while also learning the codebase. + +--- + +## 4. Mentorship Pairing (Explicit, Not Assumed) + +Every squad with a trainee pairs them with a named senior/mid for code review and unblocking — call this out at the very first standup so it's not left implicit: + +| Trainee | Paired with | What they own under supervision | +|---|---|---| +| Trainee AI/ML | SE Machine Learning | Prompt template drafts (§8.3), embedding call wiring, citation-resolution logic (§7.6) | +| Trainee Data Engineering | SE Python #1 (Squad B lead) | Connector implementation (`GitHubConnector`, §6.2), incremental re-index logic (§6.5) | +| Trainee Python #1 | SE Python #1 (Squad B lead) | Chunkers (§6.4) — start with markdown/YAML chunkers, simpler than tree-sitter code chunking | +| Trainee Python #2, #3 | SE Python #2 (Squad C lead) | Requirements extraction endpoint, story CRUD endpoints — split by sub-feature, not by pairing both on everything | +| Trainee Python #4 | SE Python #3 (Squad D lead) | Meeting submission endpoint, action-item CRUD | +| Trainee DevOps | QA Engineer + whoever is free in Squad A after hour 3 | `docker-compose.yml`, CI pipeline steps (§14.6) — devops trainee should NOT be solely responsible for the demo environment going live; treat as paired work | +| Trainee QA | QA Engineer (Squad F lead) | Manual test pass execution against each squad's endpoints as they land, following the integration test scenarios in §9's error tables | + +**Every PR from a trainee is reviewed by their pair before merge** — this is a review-cadence rule, not a suggestion, since a trainee-authored PR merged straight to `main` in a 6-squad hackathon is exactly how a silent `project_id` filter omission (the #1 risk called out in §13.5) slips through. + +--- + +## 5. Parallel Timeline (24–36 hour hackathon) + +| Hours | A (Platform/AI) | B (Knowledge Hub) | C (Planning) | D (Meetings/Risk) | E (Frontend) | F (DevOps/QA) | +|---|---|---|---|---|---|---| +| 0–1.5 | Repo scaffold, DB connection, auth skeleton, **publish `AIService`/`rag_service` stubs** | Read interfaces, scaffold `SourceConnector` | Read interfaces, scaffold schemas | Read interfaces, scaffold schemas | Frontend shell, routing, auth screens | CI skeleton, Docker images, `.env.example` | +| 1.5–3 | OAuth + JWT (§13.2), migrations 0001/0002/0010 (§17); ML engineer starts embedding integration | Trainee Data Eng + SE: `GitHubConnector`, file tree walk | Trainee pair: requirements upload + extraction against stub | Trainee: meeting transcript submission against stub | Project CRUD screens, dashboard shell | CI running tests on every PR | +| 3–8 | Swap in real `call_claude`; ML engineer + trainee AI/ML build real `retrieve()` against live Chroma | Chunkers (markdown/YAML first, then code); initial index pipeline | Story generation + INVEST critique end to end | Meeting summarization (text path) end to end | Chat UI + SSE consumption (against Squad A's now-real chat backend) | Integration tests: auth + project CRUD; nginx SSE config draft (§14.4) | +| 8–14 | Chat backend (`chat_service.py`) + citation resolution; support other squads | Chat retrieval smoke test with Squad E; tree-sitter code chunking | Sprint bin-packing + dependency inference + Kanban API | Risk digest (LLM + non-LLM checks), Slack alert | Kanban drag-and-drop (with Squad C); citation rendering | Deploy to demo host; QA begins scenario testing on landed features | +| 14–20 | Architecture diagram service (`arch_diagram_service.py`) — stretch, only if ahead of schedule | Index health endpoint; incremental webhook re-index if time allows | Story export (stretch) | Meeting-to-story linking (rapidfuzz), story_events | **Focus period — see §6 frontend risk**: prioritize dashboard, stories, kanban, chat over risk register/knowledge hub UI if behind | End-to-end smoke tests across all modules; trainee QA executes manual scenarios | +| 20–24+ | Bugfix rotation — floats to whichever squad is blocked | Bugfix rotation | Bugfix rotation | Bugfix rotation | Final UI polish on the golden-path screens only | Freeze deploy; QA Engineer runs final regression pass | + +--- + +## 6. Explicit Risk: Frontend Has Only 2 People + +Two React engineers (1 Sr, 1 Mid) against seven UI surfaces (dashboard, requirements/stories, sprint Kanban, meetings, risk register, knowledge hub, chat with SSE+Mermaid) is the tightest ratio in this roster — tighter than any backend squad. Mitigations: + +1. **Build one shared component library first** (data table, form panel, status badge, loading/streaming indicator) in the first few hours — every subsequent page assembles from these instead of each screen being bespoke. +2. **Prioritize by demo path, not by module parity.** The golden-path demo (§16.3–16.5's checkpoints) needs: dashboard → requirements/stories → sprint Kanban → chat with citations. Risk register and knowledge hub management screens can be simpler (a table, no fancy interaction) if time runs short — say this explicitly at the hour-8 standup, don't let it be a surprise at hour 20. +3. **Do not pull a Python trainee into React work as a rescue plan** — context-switching a Python trainee into an unfamiliar frontend stack under time pressure produces worse output than 2 focused React engineers cutting scope deliberately. If truly desperate late in the timeline, the QA engineer or DevOps trainee doing static HTML/CSS for a low-interactivity screen (e.g., a read-only risk table) is a safer fallback than pulling a backend trainee. + +--- + +## 7. Coordination Mechanics + +- **Standups**: 15 min, every 3–4 hours. One rep per squad: done / blocked / needs-from-another-squad. +- **`#blockers` channel**, separate from each squad's working channel. +- **Branching**: `main` + one branch per squad, PRs merge after CI passes (§14.6). Rebase off `main` at each standup. +- **Schema/migration ownership**: Squad A owns `app/models/` and the Alembic sequence (§17.2) — other squads request changes, they don't write their own migrations, avoiding numbering collisions across 6 branches. +- **Merge order**: matches FK dependency order (§5.5/§17.2) — A's auth/DB tables first, then C's traceability chain, then B's knowledge hub tables, then D's meetings/risk tables. +- **Trainee PR review gate** (§4): a trainee's PR needs their named pair's approval before merge — make this a branch protection rule in the repo, not a verbal agreement, given the pace of a hackathon. + +--- + +## 8. Demo Rehearsal (Final 2–3 Hours) + +Run the full dress rehearsal at least twice. QA Engineer drives the checklist; one person per squad is on standby to fix their module live if something breaks during rehearsal. Assign a single demo narrator (could be the Sr Python or Sr React engineer, or a non-coding team lead if one exists) walking the golden path end to end: upload requirements → generate stories → plan sprint → connect a repo → ask the onboarding assistant a cited question → submit a meeting → run risk assessment → see a Slack alert. This is the real test of whether the interface-freeze strategy in §3 held up under integration — if it didn't, this rehearsal is where you find out with time left to fix it, not during the actual demo. diff --git a/architecture/section_06_knowledge_hub_architecture.md b/architecture/section_06_knowledge_hub_architecture.md new file mode 100644 index 0000000..80c87ff --- /dev/null +++ b/architecture/section_06_knowledge_hub_architecture.md @@ -0,0 +1,221 @@ +# Section 6: Knowledge Hub Architecture + +> **ProjectOS** — AI-powered project management and knowledge platform +> This section details the continuous ingestion pipeline that turns a connected repository, its tickets, its CI config, and project meetings into the queryable brain behind the Onboarding Assistant (FR-7.1–FR-7.9, FR-5.7). + +--- + +## 6.1 Scope and Boundary + +The Knowledge Hub is the **write path** into `proj_{project_id}_knowledge` (the ChromaDB collection dedicated to code/docs/tickets/CI/meetings, per ADR-2 in §3.6). It owns: + +- Repository connection and webhook registration (FR-7.1, FR-7.4). +- Initial full index and incremental re-index (FR-7.2, FR-7.4). +- Multi-source ingestion: code, docs, issues/PRs, CI/CD definitions, Azure DevOps work items (FR-7.5–FR-7.7). +- Chunk metadata persistence in Postgres `knowledge_chunks` (§5.3.6) and vector persistence in ChromaDB. +- Index health reporting (FR-7.9). + +It does **not** own retrieval or answer generation — that's the RAG pipeline and Onboarding Assistant, covered in Section 7 and Section 8. The Knowledge Hub's only contract with those layers is: "every chunk it writes is embedded, tagged with accurate metadata, and filterable by `project_id` and `chunk_type`." + +--- + +## 6.2 Source Connectors + +| Source | Connector | Auth | Trigger | FR | +|---|---|---|---|---| +| GitHub repo files | `PyGithub` (`Repository.get_contents`, `get_git_tree`) | GitHub App installation token or PAT (encrypted, `integrations.encrypted_credentials`) | Initial connect + push webhook | FR-7.1, FR-7.2 | +| GitHub Issues & PRs | `PyGithub` (`get_issues`, `get_pulls`, comment threads) | Same as above | Initial connect + `issues`/`pull_request` webhook | FR-7.5 | +| CI/CD definitions | `PyGithub` file read on `.github/workflows/*.yml` | Same as above | Same as code files (push webhook covers workflow file changes) | FR-7.6 | +| Azure DevOps repos & work items | `azure-devops` Python SDK (`GitClient`, `WorkItemTrackingClient`) | Azure DevOps PAT (encrypted) | Initial connect + Azure DevOps service hook | FR-7.7 | +| Meeting summaries | Internal — `MeetingService` calls `IngestionService.ingest_meeting()` directly after summarization completes | N/A (internal call, not external API) | On meeting summarization completion | FR-5.7 | + +Each connector implements a common interface so the ingestion pipeline doesn't special-case providers past the fetch step: + +```python +# app/services/knowledge_hub/connectors/base.py +class SourceConnector(Protocol): + async def list_files(self, repo: Repository) -> list[FileRef]: ... + async def fetch_content(self, repo: Repository, file_ref: FileRef) -> bytes: ... + async def list_changed_files(self, repo: Repository, before_sha: str, after_sha: str) -> list[FileRef]: ... +``` + +`GitHubConnector` and `AzureDevOpsConnector` both implement this; `list_changed_files` is what makes FR-7.4's incremental re-index possible without a full tree walk on every push. + +--- + +## 6.3 Ingestion Pipeline Flow + +```mermaid +flowchart TD + A[Trigger] -->|initial connect| B[Full Repo Walk] + A -->|GitHub push webhook| C[Diff Changed Files\nlist_changed_files before_sha→after_sha] + A -->|issues/pull_request webhook| D[Fetch Issue/PR Body + Comments] + A -->|Azure DevOps service hook| E[Fetch Work Item / Repo Diff] + A -->|meeting summarized| F[MeetingService.ingest_meeting] + + B --> G{File Type Router} + C --> G + D --> H[Ticket Chunker\nsingle chunk: title + body + comments] + E --> H + F --> I[Meeting Chunker\nfixed-size, 500-token windows\nwith 50-token overlap] + + G -->|*.py .js .ts .go .java etc| J[tree-sitter Chunker\nfunction/class boundaries] + G -->|*.md README wiki| K[Markdown Chunker\nsplit by heading, max 800 tokens] + G -->|.github/workflows/*.yml\nazure-pipelines.yml| L[YAML Chunker\nwhole-file if under 2000 tokens,\notherwise split by top-level job/stage key] + G -->|binary / >5MB / .lock .min.js| M[Skip — record in\ningestion_jobs.error_log as 'skipped_unsupported'] + + J --> N[Chunk Batch\ngroups of 20] + K --> N + L --> N + H --> N + I --> N + + N --> O[Voyage AI Embed\nvoyage-code-2 for code chunks,\ntext-embedding-3-small for doc/ticket/meeting chunks] + O --> P{Embed Success?} + P -->|yes| Q[Upsert to ChromaDB\ncollection proj_id_knowledge] + P -->|no, retryable| R[Exponential Backoff Retry\n5s → 300s max, 5 attempts] + R --> O + P -->|no, exhausted retries| S[Record in ingestion_jobs.error_log\nstatus=failed, reason=embedding_error] + + Q --> T[Write knowledge_chunks row\nchroma_id, file_path, commit_sha, metadata] + T --> U[Update ingestion_jobs\nfiles_processed++, last_indexed_at=now] + S --> U +``` + +**Why the router branches by file type, not by source**: a `.md` file is chunked the same way whether it came from a GitHub repo or an Azure DevOps repo — the chunker cares about *content shape*, not *provider*. This keeps `IngestionService` from growing a combinatorial number of code paths as more providers get added. + +--- + +## 6.4 Chunking Strategy + +### 6.4.1 Code — tree-sitter (FR-7.2) + +Naive line-based chunking (e.g., every 50 lines) routinely splits a function in half, which destroys retrieval quality — the embedding for "half a function" doesn't match a query about what that function does. `tree-sitter` (via `tree-sitter-languages`) parses each file into a syntax tree and chunks at **function and class boundaries**: + +```python +# app/services/knowledge_hub/chunkers/code_chunker.py +def chunk_code_file(content: str, language: str) -> list[Chunk]: + parser = get_parser(language) # tree-sitter-languages registry + tree = parser.parse(content.encode()) + chunks = [] + for node in walk_top_level_definitions(tree.root_node): # function_definition, class_definition + if node.type in ("function_definition", "class_definition", "method_definition"): + chunks.append(Chunk( + text=content[node.start_byte:node.end_byte], + start_line=node.start_point[0] + 1, + end_line=node.end_point[0] + 1, + chunk_type="code", + )) + if not chunks: # file has no top-level functions/classes (e.g. a constants file) + chunks.append(Chunk(text=content, start_line=1, end_line=content.count("\n") + 1, chunk_type="code")) + return chunks +``` + +A function/class exceeding **1500 tokens** is split further at statement-block boundaries (still tree-sitter-aware, never mid-line) to stay within the embedding model's effective context window. + +### 6.4.2 Docs — heading-based markdown split (FR-7.2) + +Markdown is split at heading boundaries (`##`, `###`), capped at 800 tokens per chunk, using `mistune`'s AST to find heading nodes rather than regex — front-matter and code blocks inside docs must not be split mid-block. + +### 6.4.3 CI/CD YAML (FR-7.6) + +Most workflow files are small enough to embed whole (better retrieval context than fragments — "what does the deploy job do" needs the full job definition). Files over 2000 tokens split by top-level YAML key (`jobs.`, `stages.`). + +### 6.4.4 Tickets (FR-7.5) + +One chunk per issue/PR: title + body + all comments concatenated, capped at 1500 tokens (oldest comments truncated first if over cap — the issue title and body are never truncated, since that's the highest-signal part). + +### 6.4.5 Meetings (FR-5.7) + +Fixed-size 500-token windows with 50-token overlap over the structured summary text (decisions + action items + open questions concatenated), not the raw transcript — the raw transcript is noisy and rarely what an onboarding query needs; the *summary* is what should be retrievable ("what did we decide about the reporting widget"). + +--- + +## 6.5 Incremental Indexing (FR-7.4) + +On a `push` webhook: + +1. `list_changed_files(before_sha, after_sha)` — a single Git diff API call, not a tree walk. Returns added/modified/deleted file paths only. +2. For **deleted** files: `DELETE FROM knowledge_chunks WHERE repository_id = ? AND file_path = ?`, then `chroma_collection.delete(ids=[...chroma_ids])` — stale chunks for a removed file are never left retrievable. +3. For **added/modified** files: delete any existing chunk rows for that `file_path` (a file may have had 3 chunks before and now has 5 after a refactor — no attempt to diff at the chunk level, the whole file's chunk set is replaced), then re-chunk, re-embed, re-insert. +4. `repositories.last_indexed_at` and the triggering `ingestion_jobs` row are updated only after all changed files in the push are processed — a webhook that touches 40 files is one `ingestion_jobs` row, not 40, so FR-7.9's health endpoint reports coherent per-push status. + +This is why `knowledge_chunks` (§5.3.6) has the index `(project_id, repository_id, file_path)` — it's the exact filter used in step 3's delete-then-reinsert, on every incremental update. + +--- + +## 6.6 Vector Store Namespacing (FR-7.8) + +One ChromaDB collection per project, `proj_{project_id}_knowledge`, distinct from `proj_{project_id}_planning` (ADR-2). Within a project's knowledge collection, chunks from multiple connected repositories coexist, disambiguated by `metadata.repository_id` — a project with two repos does **not** get two collections, since cross-repo retrieval ("does the frontend or the backend own auth?") is a legitimate onboarding query that a per-repo collection split would make impossible without a fan-out query. + +Chunk metadata written to Chroma (mirrors the Postgres `knowledge_chunks` row so retrieval never needs a Postgres join on the hot path): + +```python +chroma_collection.upsert( + ids=[chunk.chroma_id], + embeddings=[vector], + documents=[chunk.text], + metadatas=[{ + "project_id": str(project_id), + "repository_id": str(repository_id), + "chunk_type": "code", + "file_path": "app/services/auth_service.py", + "language": "python", + "start_line": 42, + "end_line": 78, + "commit_sha": "a1b2c3d", + "last_updated": "2026-07-01T12:00:00Z", + }], +) +``` + +--- + +## 6.7 Failure Handling & Retry (NFR-AVAIL-4) + +| Failure | Handling | +|---|---| +| GitHub/Azure API rate-limited (`403`/`429`) | Respect `Retry-After` header; if absent, exponential backoff starting at 5 s, capped at 300 s, 5 attempts, then mark file `skipped_rate_limited` in `ingestion_jobs.error_log` | +| File fetch 404 (deleted between webhook and fetch) | Treated as a delete — chunk rows removed, not retried | +| tree-sitter parse error (malformed syntax, unsupported language) | Fall back to whole-file chunking (§6.4.1's no-definitions branch); logged as `parse_fallback`, not a failure | +| Voyage AI embedding API error | Exponential backoff (same schedule as above); after exhaustion, `EmbeddingError` is raised, caught at the service boundary, recorded per-file in `error_log`, and the rest of the batch continues — one bad file never blocks the other 19 in its batch (§4.7.5's `asyncio.gather` batches) | +| ChromaDB unreachable | `IngestionJob.status = 'failed'`; `NFR-AVAIL-3`'s degradation applies — reads against the knowledge collection return `503`, ingestion retries the whole job on next webhook or manual re-index trigger | +| Process restart mid-job | `ingestion_jobs.status = 'running'` rows found stuck on startup are requeued by the same recovery routine as §4.7.3 (ADR-6), scoped additionally to `trigger != 'initial'` re-running only the outstanding file diff, not the whole repo | + +--- + +## 6.8 Index Health Endpoint (FR-7.9) + +``` +GET /api/v1/projects/{project_id}/knowledge/health +``` + +```json +{ + "repositories": [ + { + "repository_id": "uuid", + "repo_url": "https://github.com/acme/backend", + "index_status": "indexed", + "last_indexed_at": "2026-07-02T09:14:00Z", + "chunks_by_type": { "code": 4213, "doc": 89, "ticket": 512, "ci": 6 }, + "failed_files": [ + { "file_path": "vendor/legacy.min.js", "reason": "skipped_unsupported" } + ] + } + ], + "meetings_indexed": 34 +} +``` + +Computed as one `GROUP BY repository_id, chunk_type` aggregation over `knowledge_chunks` joined with the latest `ingestion_jobs.error_log` per repository — no ChromaDB round-trip needed, since chunk counts and error state live entirely in Postgres metadata (§5.1, principle 4). + +--- + +## 6.9 Key Decisions + +**ADR-K1: Chunk by content shape, not by source provider.** A single router (§6.3) dispatches on file extension/MIME, so GitHub and Azure DevOps content reuse the same four chunkers. Adding a third provider (e.g., GitLab) means writing a `SourceConnector`, not a new chunking path. + +**ADR-K2: Delete-then-reinsert per file on incremental update, not chunk-level diffing.** Diffing chunk-by-chunk within a modified file would require stable chunk identity across refactors (a renamed function shifts every chunk's line range) — not worth the complexity at ProjectOS's scale (NFR-SCALE-1: 500 files/min initial throughput target, 60 s incremental SLA already met by whole-file replacement). + +**ADR-K3: Meetings are ingested as their structured summary, not raw transcript.** The Knowledge Hub retrieves what's useful to a query ("what did we decide"), not everything that was said. Raw transcripts stay in `meetings.raw_transcript` (Postgres) for audit but are never embedded. diff --git a/architecture/section_07_rag_architecture.md b/architecture/section_07_rag_architecture.md new file mode 100644 index 0000000..dd7e743 --- /dev/null +++ b/architecture/section_07_rag_architecture.md @@ -0,0 +1,147 @@ +# Section 7: RAG Architecture + +> **ProjectOS** — AI-powered project management and knowledge platform +> This section details the shared retrieval-augmented generation pipeline — the single `rag_service.py` that both the Onboarding Assistant (FR-8.1–FR-8.9) and the Project Management Engine's context-hungry features (Risk Assessment FR-6.1, Sprint dependency inference FR-4.3) call through, per ADR-2 (§3.6). + +--- + +## 7.1 Why One RAG Service, Not Two + +Every AI-generation feature in ProjectOS needs the same three steps in the same order: **retrieve relevant context → assemble it into a prompt → generate a grounded, cited answer**. The only things that differ between "answer a developer's onboarding question" and "assess project risk" are: which collection to search, what the query text is, and what shape the output should take. Building this twice — once per product half — is exactly the mistake called out in §0 of the requirements doc ("don't let two sub-teams build two separate retrieval stacks"). `rag_service.py` takes a `collection_name` and a `retrieval_config`, and every caller is a thin wrapper around it. + +```python +# app/services/rag_service.py +async def retrieve( + project_id: UUID, + query: str, + collection_name: Literal["knowledge", "planning"], + chunk_types: list[str] | None = None, + repository_id: UUID | None = None, + top_k: int = 10, +) -> list[RetrievedChunk]: ... + +async def generate_with_citations( + query: str, + chunks: list[RetrievedChunk], + prompt_template: str, + model: Literal["sonnet", "haiku"], + stream: bool = False, + conversation_history: list[ChatTurn] | None = None, +) -> AsyncGenerator[str, None] | GeneratedAnswer: ... +``` + +--- + +## 7.2 RAG Pipeline Flow + +```mermaid +flowchart TD + A[Caller: ChatService / RiskService / SprintService] --> B[Build Query Text] + B --> C[Embed Query\nVoyage AI — same model as ingestion] + C --> D[ChromaDB Similarity Search\ncollection=proj_id_knowledge or proj_id_planning\nfilter: project_id + chunk_type + repository_id] + D --> E{Results >= 1?} + E -->|no| F[Raise KnowledgeHubEmpty\nor InsufficientProjectData] + E -->|yes| G[Re-rank\ncosine score + recency boost\nfor chunk_type=code/doc] + G --> H[Deduplicate\ncollapse chunks from same file\nwithin 20 lines of each other] + H --> I[Token-Budget Trim\nkeep top-k until context budget hit\ndefault top_k=10, ~4k tokens] + I --> J[Context Assembly\nchunks + conversation_history\n+ project metadata header] + J --> K{Prompt Cache Hit?\nsystem prompt + static context} + K -->|yes| L[Anthropic Prompt Cache\ncache_control: ephemeral on system block] + K -->|no| M[Full Prompt Sent] + L --> N[Claude API Call\nSonnet: chat/risk narrative\nHaiku: dependency inference/classification] + M --> N + N --> O{stream=true?} + O -->|yes| P[SSE token-by-token\nto ChatService] + O -->|no| Q[Structured JSON\nparsed + validated against Pydantic schema] + P --> R[Attach Citations\nchunk.file_path + line_range + repo_name\nper claim, resolved via knowledge_chunks] + Q --> R + R --> S[Return to Caller] +``` + +--- + +## 7.3 Retrieval Configuration by Consumer + +| Consumer | Collection | `chunk_types` filter | `top_k` | Query text source | FR | +|---|---|---|---|---|---| +| Onboarding chat (`ChatService.stream_answer`) | `knowledge` | none (all types) unless user narrows via `repository_scope` | 10 | User's literal question, expanded with last 2 turns of history for pronoun resolution ("what about **it**") | FR-8.1–FR-8.2 | +| Risk assessment digest (`RiskService.assess`) | `planning` | `["requirement","story","meeting"]` | 15 (wider — risk needs breadth, not a single best-match answer) | Synthesized digest query per risk category (e.g., "ambiguous or contradictory requirement language") rather than a single free-text question | FR-6.1 | +| Sprint dependency inference (`SprintService.plan`) | N/A — no retrieval | N/A | N/A | Story text is passed directly to Claude Haiku; no vector search needed since the input set (this sprint's candidate stories) is already fully known and small enough to pass whole | FR-4.3 | +| Architecture diagram Q&A follow-up | `knowledge`, `chunk_types=["code"]` | 20 | Higher `top_k` since import-graph questions need broader file coverage than a single-function question | FR-8.5 | + +Dependency inference is listed to make an explicit point: **not every AI feature in ProjectOS is RAG.** When the full relevant input already fits in context (a sprint's candidate story set, typically under 50 stories), retrieval adds latency and a failure mode (`KnowledgeHubEmpty`) with no benefit. `rag_service.py` is used only where the relevant context is larger than what can be passed directly — that's the actual definition of "needs retrieval" in this system, not "involves an LLM call." + +--- + +## 7.4 Re-ranking and Deduplication (§7.2 steps G–H) + +Raw HNSW cosine similarity from ChromaDB is a good first pass but has two known failure modes for ProjectOS's data: + +1. **Multiple chunks from the same file cluttering top-k.** A large file chunked into 8 functions can return 4 of its own chunks in the top 10 if the query is broadly about that file's domain, crowding out other relevant files. Chunks within 20 lines of an already-selected chunk from the same `file_path` are collapsed into one (keeping the highest-scoring one) before the token-budget trim. +2. **Recency matters for code, not for docs.** A `code` chunk with a newer `commit_sha` timestamp gets a small score boost (+0.02 per week of recency, capped at +0.1) — stale code that's been superseded by a refactor shouldn't outrank current code purely on semantic similarity. `doc` and `ticket` chunks get no recency boost — an old design doc can still be the correct answer to "why was this built this way." + +No cross-encoder re-ranking model is used — at ProjectOS's target scale (NFR-SCALE-3: up to 1M chunks/project), an extra re-ranking API call would add latency without a demonstrated quality problem the cheaper heuristics above don't already solve. This is a deliberate scope cut, not an oversight — documented as a future enhancement in Section 15. + +--- + +## 7.5 Context Assembly + +The assembled prompt has three parts, in this fixed order to maximize Anthropic prompt-cache hits (NFR-SCALE-4): + +``` +[SYSTEM — cache_control: ephemeral] + - Role instructions (e.g. "You are ProjectOS's onboarding assistant...") + - Citation format instructions (mandatory: every claim needs a [file:line] tag) + - Output schema (for structured callers like RiskService) + +[STATIC CONTEXT — cache_control: ephemeral when caller is a repeated digest, e.g. RiskService] + - Project metadata (name, team size, connected repos) + - Retrieved chunks, each tagged: `[chunk_id | file_path:start_line-end_line | repo_name]` + +[DYNAMIC — never cached] + - Conversation history (chat only, last 10 turns per FR-8.6) + - The current user query / digest instruction +``` + +The system block and static context block are identical across consecutive risk-assessment runs on the same project within a short window, and across chat turns in the same session (the retrieved-chunk set often only shifts slightly turn to turn) — placing them before the dynamic block is what makes Anthropic's prompt caching actually fire, rather than caching a block that changes every call. + +--- + +## 7.6 Citation Enforcement (FR-8.3, hallucination guard from §1.5) + +Citations are not a post-hoc regex extraction from free text — they're enforced structurally: + +- **Structured callers** (Risk Assessment, Story Generation via requirement citations): the Pydantic output schema requires a `source_signal` / `source_paragraph` field per item (§5.3.2, §5.3.5). Claude's structured JSON mode makes omitting this field a schema validation failure, which `AIService` catches and retries once with an explicit reminder before surfacing `AIServiceError`. +- **Chat** (FR-8.3): the system prompt instructs Claude to tag every factual claim with `[chunk_id]` inline. After generation, `ChatService` parses these tags, resolves each `chunk_id` against the `retrieved_chunk_ids` passed into that turn (never against arbitrary IDs — a hallucinated `chunk_id` that wasn't actually retrieved is dropped, not trusted), and looks up `file_path`/`start_line`/`end_line`/`repo_name` from `knowledge_chunks` for the final `citations[]` array returned in the SSE `done` event (§4.7.4). +- If Claude's answer contains **zero** citation tags for a query that had non-empty retrieval results, `ChatService` appends a soft warning to the response ("This answer may not be fully grounded in the indexed codebase — consider rephrasing") rather than silently returning an uncited answer — this is the practical enforcement of the "no generation with no retrieval backing" constraint from §1.5. + +--- + +## 7.7 Conversation History and Follow-ups (FR-8.6) + +The last 10 `chat_messages` rows for a `session_id` (indexed per §5.4) are fetched and included in two ways: + +1. **Query expansion**: the last 2 turns are concatenated with the current query before embedding, so a follow-up like "what about the refresh token flow?" retrieves against "how does authentication work in this repo? what about the refresh token flow?" rather than the ambiguous fragment alone. +2. **Prompt context**: all 10 turns (query + answer pairs, not full chunk text) are included in the dynamic block so Claude's generated answer stays coherent with what it already told the user in this session. + +--- + +## 7.8 Failure Modes + +| Condition | Exception | HTTP | Recovery | +|---|---|---|---| +| Collection has zero chunks for this project (no repo connected yet) | `KnowledgeHubEmpty` | 422 `knowledge_hub_empty` | UI prompts "Index a repository first" (§4.6) | +| Retrieval succeeds but risk digest has insufficient signal (e.g., brand-new project with no meetings, no stories) | `InsufficientProjectData` | 422 `insufficient_project_data` with `missing: [...]` | UI shows which inputs are missing (e.g., "add at least one story or meeting to run risk assessment") | +| ChromaDB unreachable | Caught in `rag_service.retrieve`, re-raised as `EmbeddingError`-adjacent `503` per NFR-AVAIL-3 | 503 `knowledge_hub_unavailable` | Chat/risk features degrade; project CRUD unaffected | +| Voyage AI embedding call fails on the query itself (not ingestion) | `EmbeddingError` | 503 `embedding_service_unavailable` | Retried once with backoff (same policy as §6.7) before surfacing | +| Claude API fails after retrieval succeeded (context assembled, generation call fails) | `AIServiceError(retryable=True)` | 503 `ai_unavailable` | Retrieved chunks are not wasted — `AIService` retries the generation call (not re-retrieval) up to the retry policy in Section 8 | + +--- + +## 7.9 Key Decisions + +**ADR-R1: Retrieval is opt-in per feature, not a default wrapper on every AI call.** Only features whose relevant context exceeds what fits directly in a prompt use `rag_service.retrieve()`. This avoids `KnowledgeHubEmpty` becoming a spurious failure mode for features like dependency inference that never needed the vector store in the first place. + +**ADR-R2: No cross-encoder re-ranker at MVP.** Heuristic re-ranking (file-proximity dedup + recency boost for code) is cheap, explainable, and sufficient at target scale. Revisit if retrieval-quality feedback (FR-8.8 thumbs-down rate) shows a pattern a re-ranker would fix — tracked in Section 15. + +**ADR-R3: Citations are resolved against the actual retrieved-chunk set, never freeform.** A citation tag Claude emits that doesn't match a chunk actually passed into that generation call is dropped rather than trusted, closing the loophole where a model could "cite" a plausible-looking but non-existent source. diff --git a/architecture/section_08_ai_services.md b/architecture/section_08_ai_services.md new file mode 100644 index 0000000..6cc06ec --- /dev/null +++ b/architecture/section_08_ai_services.md @@ -0,0 +1,311 @@ + +# Section 8: AI Services + +> **ProjectOS** — AI-powered project management and knowledge platform +> This section details `AIService` — the single chokepoint every Claude API call passes through — plus one sequence diagram per distinct AI workflow in the system. + +--- + +## 8.1 AIService Responsibilities + +Every service in §4.4 (`RequirementsService`, `StoryService`, `SprintService`, `MeetingService`, `RiskService`, `IngestionService`, `ChatService`) calls Claude only through `AIService` — never `anthropic.Anthropic()` directly. This is what makes NFR-OBS-2 (per-call cost logging), NFR-SEC-6 (no prompt logging in prod), and NFR-SCALE-4 (prompt caching) enforceable in one place instead of seven. + +```python +# app/services/ai_service.py +class AIService: + async def call_claude( + self, + project_id: UUID, + prompt_template: PromptTemplate, + variables: dict, + model: Literal["sonnet", "haiku"] = "sonnet", + response_schema: type[BaseModel] | None = None, + stream: bool = False, + cache_key: str | None = None, # Redis response cache (§8.4.2), distinct from Anthropic prompt caching (§8.4.1) + ) -> dict | AsyncGenerator[str, None]: + async with self._get_semaphore(project_id): # §4.7.2 — max 10 in-flight per project + if cache_key and (cached := await self._check_redis_cache(cache_key)): + return cached + response = await self._call_with_retry(prompt_template, variables, model, response_schema, stream) + await self._log_usage(project_id, prompt_template, model, response) # NFR-OBS-2 + if cache_key: + await self._store_redis_cache(cache_key, response, ttl_seconds=604800) # NFR-DATA-3, 7-day TTL + return response +``` + +--- + +## 8.2 Model Selection Matrix + +| Model | Used for | Why | FR | +|---|---|---|---| +| Claude Sonnet | Work package extraction, story generation, sprint sanity pass, meeting summarization, RAG chat answers | Requires nuanced reasoning over ambiguous natural language and long-form generation quality that shows in the demo | FR-2.2, FR-3.1, FR-4.5, FR-5.3, FR-8.3 | +| Claude Haiku | INVEST critique, dependency inference, risk classification/scoring | High call frequency, lower per-call reasoning depth needed, cost matters more than marginal quality (NFR-SCALE-4, §1.1 of the requirements doc: "great place to use a cheaper/faster model since it runs frequently") | FR-3.3, FR-4.3, FR-6.1 | + +The model is selected by the **caller**, not inferred by `AIService` — a service passing `model="sonnet"` for a Haiku-appropriate task is a code review finding, not a runtime decision `AIService` should silently override. + +--- + +## 8.3 Prompt Template Registry (NFR-MAINT-5) + +All prompts are versioned constants in `app/ai/prompts.py`, never inline f-strings in service code: + +```python +# app/ai/prompts.py +WORK_PACKAGE_EXTRACTION_PROMPT_V1 = PromptTemplate( + system="You are extracting discrete work packages from a requirements document...", + response_schema=WorkPackageExtractionResult, +) +STORY_GENERATION_PROMPT_V2 = PromptTemplate(...) +INVEST_CRITIQUE_PROMPT_V1 = PromptTemplate(...) +DEPENDENCY_INFERENCE_PROMPT_V1 = PromptTemplate(...) +SPRINT_SANITY_PASS_PROMPT_V1 = PromptTemplate(...) +MEETING_SUMMARY_PROMPT_V3 = PromptTemplate(...) +RISK_DIGEST_PROMPT_V2 = PromptTemplate(...) +RAG_CHAT_PROMPT_V1 = PromptTemplate(...) +``` + +Bumping `V2 → V3` adds a new constant; the old one stays until the new version is validated in production (a prompt regression is diagnosable by rolling `MEETING_SUMMARY_PROMPT_V3` back to `V2` in one line, not by reconstructing a lost string from git history). `AIService._log_usage` records `prompt_version` per call (NFR-OBS-2), so a quality regression after a prompt bump is visible in the usage logs before it's visible in user complaints. + +--- + +## 8.4 Caching + +### 8.4.1 Anthropic Prompt Caching (NFR-SCALE-4) + +The system-prompt block and the static-context block (§7.5) carry `cache_control: {"type": "ephemeral"}`. This matters most for: +- **Risk assessment** (FR-6.1): the digest system prompt and category-instruction text are identical across every project's risk run — cached across all projects, not just repeat runs on one project. +- **Chat** (FR-8.3): within a session, consecutive turns often retrieve a largely overlapping chunk set — the static context block cache-hits turn to turn. + +### 8.4.2 Redis Response Cache (§1.5 idempotency note, distinct concern) + +A hash of `(prompt_version, variables)` → cached full response, TTL 7 days (NFR-DATA-3). This is **not** prompt caching — it's whole-response memoization for the case where the exact same input is re-submitted (e.g., a user re-uploads the identical requirements doc during dev/demo iteration, per the gaps doc's cost-control note). Only applied to non-idempotency-sensitive calls: work package extraction and INVEST critique use it; story generation, sprint planning, risk assessment, and chat do **not**, since those are explicitly meant to produce a new versioned snapshot on every run (§1.5) — caching them would silently defeat the versioning guarantee. + +--- + +## 8.5 Retry & Backoff Policy + +| Failure | Retry? | Policy | +|---|---|---| +| Claude API `529` (overloaded) / `500`/`502`/`503` | Yes | Exponential backoff: 2s, 4s, 8s — 3 attempts, then `AIServiceError(retryable=True)` | +| Claude API `429` (rate limited) | Yes | Honor `retry-after` header if present, else same backoff as above | +| Claude API `400` (invalid request — e.g. malformed schema) | No | `AIServiceError(retryable=False)` — a schema bug won't fix itself on retry | +| Structured output fails Pydantic validation | Yes, once | Retry with an appended system reminder ("your last response didn't match the required schema — return valid JSON matching..."), then surface `AIServiceError` if the retry also fails | +| Network timeout (httpx, 30s connect / 120s read per NFR-SEC-7) | Yes | Same exponential backoff as 5xx | + +--- + +## 8.6 Sequence Diagrams by Workflow + +### 8.6.1 Work Package Extraction (FR-2.2–FR-2.5) + +```mermaid +sequenceDiagram + participant FE as React Frontend + participant API as POST /projects/{id}/requirements + participant ReqSvc as Requirements Service + participant AISvc as AI Service + participant Claude as Claude Sonnet + participant PG as PostgreSQL + + FE->>API: multipart upload (PDF/DOCX/MD) + API->>ReqSvc: extract_work_packages(project_id, file_bytes, mime_type) + ReqSvc->>ReqSvc: parse to raw_text (pdfplumber / python-docx / passthrough) + ReqSvc->>PG: INSERT requirements_documents (status=processing) + ReqSvc->>AISvc: call_claude(WORK_PACKAGE_EXTRACTION_PROMPT_V1, raw_text, cache_key=hash(raw_text)) + AISvc->>AISvc: check Redis cache — miss + AISvc->>Claude: POST /messages [Sonnet, structured JSON: WorkPackageExtractionResult] + Claude-->>AISvc: { work_packages: [{title, description, priority, source_paragraph}] } + AISvc->>AISvc: cache response in Redis (7d TTL) + AISvc-->>ReqSvc: work_packages[] + ReqSvc->>AISvc: embed(each work_package.description) for dedup check + AISvc-->>ReqSvc: vectors[] + ReqSvc->>ReqSvc: cosine similarity vs existing accepted work packages\nflag duplicate_of_id where similarity > 0.92 (FR-2.5) + ReqSvc->>PG: INSERT work_packages (status=pending, duplicate flags set) + ReqSvc->>PG: UPDATE requirements_documents SET status=extracted + ReqSvc-->>API: WorkPackageExtractionResult + API-->>FE: HTTP 201 { work_packages: [...] } +``` + +### 8.6.2 Story Generation + INVEST Critique (FR-3.1–FR-3.4) + +```mermaid +sequenceDiagram + participant API as POST /stories/generate + participant StorySvc as Story Service + participant AISvc as AI Service + participant Sonnet as Claude Sonnet + participant Haiku as Claude Haiku + participant PG as PostgreSQL + + API->>StorySvc: generate(project_id, work_package_ids) + StorySvc->>PG: SELECT work_packages WHERE id IN (...) AND status='accepted' + PG-->>StorySvc: work packages + StorySvc->>AISvc: call_claude(STORY_GENERATION_PROMPT_V2, model=sonnet) + AISvc->>Sonnet: structured JSON: {stories: [{title, description, acceptance_criteria(Gherkin), story_points}]} + Sonnet-->>AISvc: draft stories + AISvc-->>StorySvc: stories[] + StorySvc->>AISvc: call_claude(INVEST_CRITIQUE_PROMPT_V1, model=haiku, variables=stories) + AISvc->>Haiku: structured JSON: {critiques: [{story_id, violations[I,N,V,E,S,T], suggested_fix}]} + Haiku-->>AISvc: critiques + AISvc-->>StorySvc: critiques[] + StorySvc->>StorySvc: merge critiques into story drafts\n(violations surfaced in UI, not auto-applied — FR-3.5 keeps human in the loop) + StorySvc->>PG: INSERT stories (status=draft, generation_version=N, source_work_package_id) + StorySvc->>PG: INSERT invest_critiques + StorySvc-->>API: StoryGenerationResult{stories, invest_critiques} +``` + +### 8.6.3 Sprint Planning: Dependency Inference + Sanity Pass (FR-4.2–FR-4.5) + +```mermaid +sequenceDiagram + participant API as POST /sprints/plan + participant SprintSvc as Sprint Service + participant AISvc as AI Service + participant Haiku as Claude Haiku + participant Sonnet as Claude Sonnet + participant PG as PostgreSQL + + API->>SprintSvc: plan(project_id, config) + SprintSvc->>PG: SELECT stories WHERE project_id=... AND status='draft' + PG-->>SprintSvc: story pool + alt story pool empty + SprintSvc-->>API: raise NoDraftStories + end + SprintSvc->>AISvc: call_claude(DEPENDENCY_INFERENCE_PROMPT_V1, model=haiku, variables=story_pool) + AISvc->>Haiku: structured JSON: {dependencies: [{story_id, blocked_by: [story_id, ...]}]} + Haiku-->>AISvc: dependency graph + AISvc-->>SprintSvc: dependency graph + SprintSvc->>SprintSvc: greedy bin-pack into sprints (plain Python)\nrespecting team_velocity capacity + SprintSvc->>SprintSvc: reorder to satisfy dependency graph\n(topological pass; unresolvable cycle → flag, don't crash) + SprintSvc->>AISvc: call_claude(SPRINT_SANITY_PASS_PROMPT_V1, model=sonnet, variables=draft_plan) + AISvc->>Sonnet: structured JSON: {overcommitment_warnings, cycle_flags, rebalancing_suggestions} + Sonnet-->>AISvc: sanity pass result + AISvc-->>SprintSvc: sanity pass result + SprintSvc->>PG: INSERT sprint_plans (version=N+1, sanity_pass_warnings) + SprintSvc->>PG: INSERT sprints, sprint_assignments + SprintSvc-->>API: SprintPlan{sprints, warnings} +``` + +### 8.6.4 Meeting Summarization — Audio Path (FR-5.1–FR-5.6) + +```mermaid +sequenceDiagram + participant FE as React Frontend + participant API as POST /meetings + participant MeetingSvc as Meeting Service + participant Whisper as faster-whisper (in-process) + participant AISvc as AI Service + participant Sonnet as Claude Sonnet + participant PG as PostgreSQL + participant FileStore as Local File Store + + FE->>API: multipart audio upload (.mp3/.wav/.m4a) + API->>MeetingSvc: summarize(project_id, audio_bytes=..., transcript_text=None) + MeetingSvc->>FileStore: write temp audio file + MeetingSvc->>PG: INSERT meetings (status=processing) + MeetingSvc->>Whisper: transcribe(audio_path) [lazy-loaded model, ADR-5] + Whisper-->>MeetingSvc: raw_transcript text + MeetingSvc->>FileStore: delete audio file (NFR-DATA-2, within 10 min) + MeetingSvc->>PG: UPDATE meetings SET raw_transcript=... + MeetingSvc->>AISvc: call_claude(MEETING_SUMMARY_PROMPT_V3, model=sonnet, variables=raw_transcript) + AISvc->>Sonnet: structured JSON: {decisions[], action_items[{text,owner,due_date}], risks[], open_questions[]} + Sonnet-->>AISvc: structured summary + AISvc-->>MeetingSvc: summary + MeetingSvc->>PG: SELECT stories WHERE project_id=... (title index for fuzzy match) + MeetingSvc->>MeetingSvc: rapidfuzz match decisions/action_items against story titles\n(threshold 80%, FR-5.4) + MeetingSvc->>PG: INSERT meeting_decisions, action_items (linked_story_id where matched) + MeetingSvc->>PG: INSERT story_events (needs_review=true) for each linked match + MeetingSvc->>PG: UPDATE meetings SET status=summarized + MeetingSvc->>MeetingSvc: schedule_ingest_to_knowledge_hub(meeting_id) [FR-5.7, via IngestionService, async] + MeetingSvc-->>API: MeetingSummary + API-->>FE: HTTP 201 { summary, linked_stories, unassigned_action_items_warning } +``` + +### 8.6.5 Risk Assessment Digest (FR-6.1–FR-6.8) + +```mermaid +sequenceDiagram + participant API as POST /risks/assess + participant RiskSvc as Risk Service + participant PG as PostgreSQL + participant AISvc as AI Service + participant Haiku as Claude Haiku + participant Slack as Slack Webhook + + API->>RiskSvc: assess(project_id) + RiskSvc->>PG: SELECT requirements_documents, stories, sprint_plans (latest), meetings LIMIT 5 + PG-->>RiskSvc: raw rows + alt insufficient data (no stories AND no meetings) + RiskSvc-->>API: raise InsufficientProjectData(missing=[...]) + end + RiskSvc->>RiskSvc: non-LLM check: sprints.is_overloaded (>110% velocity, FR-6.4) + RiskSvc->>RiskSvc: non-LLM check: action_items.slip_count >= 2 across meetings (FR-6.5) + RiskSvc->>RiskSvc: summarize raw rows into a digest (not the full firehose — cost control per §0) + RiskSvc->>AISvc: call_claude(RISK_DIGEST_PROMPT_V2, model=haiku, variables=digest) + AISvc->>Haiku: structured JSON: {risks: [{category, description, likelihood, impact, mitigation_suggestion, source_signal}]} + Haiku-->>AISvc: LLM-derived risks + AISvc-->>RiskSvc: LLM-derived risks + RiskSvc->>RiskSvc: merge LLM risks + non-LLM auto-flagged risks\ncompute severity = likelihood * impact + RiskSvc->>PG: INSERT risk_register_snapshots (version=N+1) + RiskSvc->>PG: INSERT risks (all entries, this snapshot) + alt any risk.severity >= 15 + RiskSvc->>Slack: POST webhook {new high-severity risk} (FR-6.7) + end + RiskSvc-->>API: RiskRegisterSnapshot +``` + +### 8.6.6 Architecture Diagram Generation (FR-8.5) + +```mermaid +sequenceDiagram + participant FE as React Frontend (chat) + participant API as POST /chat/stream {intent: diagram} + participant ArchDiagSvc as Arch Diagram Service + participant GHApi as GitHub API + participant AST as Python ast / tree-sitter + participant AISvc as AI Service + participant Sonnet as Claude Sonnet + + FE->>API: "show me the architecture of this repo" + API->>ArchDiagSvc: generate_diagram(project_id, repository_id) + ArchDiagSvc->>GHApi: fetch all source files (cached from last index if commit_sha unchanged) + ArchDiagSvc->>AST: parse imports per file (ast for Python, tree-sitter import queries for polyglot) + AST-->>ArchDiagSvc: import edges [{from_module, to_module}] + ArchDiagSvc->>ArchDiagSvc: build dependency graph, collapse to module-level nodes\n(function-level would be unreadable in a diagram) + ArchDiagSvc->>AISvc: call_claude(ARCH_DIAGRAM_DESCRIBE_PROMPT_V1, model=sonnet, variables=graph_edges) + AISvc->>Sonnet: "describe these module relationships and label the diagram clearly" + Sonnet-->>AISvc: mermaid graph syntax + narrative description + AISvc-->>ArchDiagSvc: mermaid diagram text + ArchDiagSvc-->>API: { diagram_type: "mermaid", diagram_source: "graph TD...", narrative } + API-->>FE: SSE stream: narrative tokens + final mermaid block\n(rendered client-side by the chat UI's mermaid renderer) +``` + +**Note**: the diagram is built **from a real parsed import graph, not from Claude's guess at the architecture** — Claude's only job here is turning verified graph edges into a readable Mermaid layout and a plain-language description, per the explicit requirement in the source doc: "generated from actually parsing the code," not vibes (§9 of the requirements doc). + +### 8.6.7 Onboarding Chat + +Already fully diagrammed in §3.4.3 (request flow) and §7.2 (retrieval pipeline) — not repeated here. The only AI-Service-specific addition: chat is the **one workflow that always sets `stream=True`** on `call_claude`, since FR-8.4 requires token-by-token SSE delivery; every other workflow in this section calls with `stream=False` and structured output, since they're one-shot generation flows the UI is fine waiting on (NFR-UX-1's `job_id`/SSE distinction applies only to the audio-upload path of meeting summarization in practice, given its 90s latency budget). + +--- + +## 8.7 Cost & Token Logging (NFR-OBS-2) + +Every `AIService.call_claude` invocation, regardless of caller, emits: + +```json +{ + "event": "llm_call", + "project_id": "uuid", + "feature": "story_generation", + "model": "claude-sonnet-4-6", + "prompt_version": "STORY_GENERATION_PROMPT_V2", + "input_tokens": 2140, + "output_tokens": 890, + "cached_tokens": 1600, + "latency_ms": 4210, + "retry_count": 0 +} +``` + +`feature` is set by the caller at the call site (not inferred from the prompt template name), giving a clean `projectos_tokens_used_total{feature, model, type}` metric (NFR-OBS-4) that answers "what's actually costing us money" per module, not just per model. diff --git a/architecture/section_09_api_design.md b/architecture/section_09_api_design.md new file mode 100644 index 0000000..9f7e6b8 --- /dev/null +++ b/architecture/section_09_api_design.md @@ -0,0 +1,154 @@ +# Section 9: API Design + +> **ProjectOS** — AI-powered project management and knowledge platform +> This section is the complete REST contract: every endpoint, method, request/response shape, and status code, grouped by the routers defined in §4.2. + +--- + +## 9.1 Conventions + +- **Versioning**: path-based, `/api/v1/*` (§4.2). No header versioning — simpler to test with curl/Postman during a hackathon, and avoids the "which header did the client forget" class of bugs. +- **Pagination**: cursor-based on all list endpoints returning more than a bounded small set (`GET /stories`, `GET /meetings`, `GET /chat/{session_id}/messages`), via `?cursor=&limit=`. Response envelope: `{ "items": [...], "next_cursor": "..." | null }`. Offset pagination is avoided because `risk_register_snapshots` and `sprint_plans` are append-only (§5.1) — cursor-on-`created_at` stays stable even as new versions are inserted mid-scroll. +- **Filtering**: query params map directly to indexed columns (§5.4) — e.g. `GET /stories?status=draft` uses the `(project_id, status)` index directly, never a full scan filtered in application code. +- **Response envelope for single resources**: the resource itself, no wrapper (`{"id": ..., "title": ...}`, not `{"data": {...}}`) — one less nesting level for the frontend to unwrap on every call. +- **Error envelope** (fixed shape everywhere, per §4.6): `{ "error": "", ...context_fields }`. Every error code in this section is drawn from the exact table in §4.6 — no endpoint invents a new ad-hoc error shape. +- **Idempotency**: `POST` endpoints that *generate* an artifact (stories, sprint plan, risk register) never overwrite — each call creates a new version (§1.5, §5.1). This is reflected in their response: they return the new version's data plus a `version` field, not a 200-with-no-change. +- **Auth on every route**: all routes below require `Authorization: Bearer ` except `POST /auth/github/callback`, `POST /auth/microsoft/callback`, `POST /auth/refresh`, `GET /health`, and `POST /webhooks/*` (which authenticate via HMAC signature, not JWT — NFR-SEC-1). + +--- + +## 9.2 Auth Router — `/api/v1/auth` + +| Method | Path | Body / Params | Success | Errors | +|---|---|---|---|---| +| `GET` | `/auth/github/login` | — | `302` redirect to GitHub OAuth consent | — | +| `GET` | `/auth/github/callback` | `?code=...&state=...` | `200 { access_token, refresh_token, user }` | `401 invalid_oauth_state` | +| `GET` | `/auth/microsoft/login` | — | `302` redirect to Microsoft OAuth consent | — | +| `GET` | `/auth/microsoft/callback` | `?code=...&state=...` | `200 { access_token, refresh_token, user }` | `401 invalid_oauth_state` | +| `POST` | `/auth/refresh` | `{ refresh_token }` | `200 { access_token, refresh_token }` (rotated, NFR-SEC-2) | `401 token_expired`, `401 token_reused` | +| `POST` | `/auth/logout` | — | `204` (revokes current refresh token) | `401 invalid_token` | +| `GET` | `/auth/me` | — | `200 UserOut` | `401 invalid_token` | + +`token_reused` (§4.6) fires when a refresh token that was already rotated is presented again — treated as a possible token-theft signal, not a benign retry; the entire refresh-token family for that user is revoked, forcing re-login (detailed in Section 13). + +--- + +## 9.3 Projects Router — `/api/v1/projects` + +| Method | Path | Body / Params | Success | Errors | +|---|---|---|---|---| +| `POST` | `/projects` | `{ name, description, team_size, default_sprint_length_days }` | `201 ProjectOut` | `422` validation | +| `GET` | `/projects` | `?cursor&limit` (projects the user is a member of) | `200 { items: ProjectOut[], next_cursor }` | — | +| `GET` | `/projects/{project_id}` | — | `200 ProjectOut` | `403 not_project_member`, `404 project_not_found` | +| `PATCH` | `/projects/{project_id}` | partial `{ name?, description?, team_size?, default_sprint_length_days? }` | `200 ProjectOut` | `403`, `404`, `422` | +| `DELETE` | `/projects/{project_id}` | — | `204` (cascades per NFR-DATA-4, drops Chroma collections, revokes webhooks) | `403 not_project_member` (only `project_owner` may delete — checked in service layer) | +| `GET` | `/projects/{project_id}/dashboard` | — | `200 DashboardOut` — current sprint status, open risk count, recent meeting summaries, index health (FR-1.5) | `403`, `404` | +| `POST` | `/projects/{project_id}/members` | `{ email_or_github_handle, role }` | `201 ProjectMemberOut` | `403 not_project_member` (only owner), `422 invalid_role` | +| `DELETE` | `/projects/{project_id}/members/{user_id}` | — | `204` | `403`, `404 member_not_found` | +| `POST` | `/projects/{project_id}/integrations` | `{ provider, credential, config }` | `201 IntegrationOut` (credential immediately encrypted, never echoed back) | `403`, `422 unsupported_provider` | + +--- + +## 9.4 Requirements & Stories Router — `/api/v1/projects/{project_id}/...` + +| Method | Path | Body / Params | Success | Errors | FR | +|---|---|---|---|---|---| +| `POST` | `/requirements` | multipart file OR `{ raw_text }` | `201 { requirements_document_id, status: "processing" }` | `415 unsupported_file_type` | FR-2.1 | +| `GET` | `/requirements/{doc_id}/work-packages` | — | `200 { items: WorkPackageOut[] }` | `404` | FR-2.2 | +| `PATCH` | `/work-packages/{wp_id}` | `{ status?, title?, description?, priority? }` | `200 WorkPackageOut` (FR-2.4 accept/reject/edit) | `404 work_package_not_found`, `422` | +| `POST` | `/stories/generate` | `{ work_package_ids: UUID[] }` | `201 StoryGenerationResult{stories, invest_critiques}` | `404 work_package_not_found`, `503 ai_unavailable` | FR-3.1 | +| `GET` | `/stories` | `?status&work_package_id&cursor&limit` | `200 { items: StoryOut[], next_cursor }` | — | — | +| `GET` | `/stories/{story_id}` | — | `200 StoryOut` | `404 story_not_found` | — | +| `PATCH` | `/stories/{story_id}` | `{ title?, description?, acceptance_criteria?, story_points?, status? }` | `200 StoryOut` (FR-3.5 manual edit) | `404`, `422` | +| `POST` | `/stories/{story_id}/regenerate` | — | `201 StoryOut` (new `generation_version`, old marked `superseded`) | `404`, `503 ai_unavailable` | FR-3.5 | +| `DELETE` | `/stories/{story_id}` | — | `204` | `404` | FR-3.5 | +| `POST` | `/stories/{story_id}/export` | `{ provider: "github_issues"\|"azure_devops" }` | `201 ExternalTicketLinkOut` | `404`, `422 integration_not_configured`, `502 external_api_error` | FR-3.6 | + +--- + +## 9.5 Sprints Router — `/api/v1/projects/{project_id}/sprints` + +| Method | Path | Body / Params | Success | Errors | FR | +|---|---|---|---|---|---| +| `POST` | `/sprints/plan` | `SprintPlanRequest{ sprint_length_days, team_velocity, num_sprints }` | `201 SprintPlan{sprints, warnings, version}` | `422 no_draft_stories`, `503 ai_unavailable` | FR-4.1–FR-4.5 | +| `GET` | `/sprints/plans` | `?cursor&limit` | `200 { items: SprintPlanOut[], next_cursor }` (versions, newest first) | — | — | +| `GET` | `/sprints/plans/{plan_id}` | — | `200 SprintPlanOut` (full Kanban data) | `404` | FR-4.6 | +| `PATCH` | `/sprints/{sprint_id}/assignments/{story_id}` | `{ target_sprint_id, position }` | `200 SprintAssignmentOut` (manual drag-drop, `manually_moved=true`) | `404`, `422 capacity_exceeded_confirm_override` | FR-4.7 | +| `POST` | `/sprints/plans/{plan_id}/finalize` | — | `200 SprintPlanOut{status: "finalized"}` | `404`, `422 plan_already_finalized` | — | +| `POST` | `/sprints/plans/{plan_id}/sync` | `{ provider: "azure_devops_iterations"\|"github_projects" }` | `202 { sync_job_id }` | `404`, `422 integration_not_configured` | FR-4.8 | + +--- + +## 9.6 Meetings Router — `/api/v1/projects/{project_id}/meetings` + +| Method | Path | Body / Params | Success | Errors | FR | +|---|---|---|---|---|---| +| `POST` | `/meetings` | multipart audio OR `{ transcript_text, meeting_type }` | `201 { meeting_id, status: "processing" }` immediately; summary arrives via polling or SSE progress (NFR-UX-1) | `422 transcription_failed`, `415 unsupported_file_type` | FR-5.1–FR-5.3 | +| `GET` | `/meetings/{meeting_id}` | — | `200 MeetingOut{summary, decisions, action_items, risks, open_questions}` | `404` | FR-5.3 | +| `GET` | `/meetings` | `?meeting_type&cursor&limit` | `200 { items: MeetingOut[], next_cursor }` | — | — | +| `PATCH` | `/meetings/{meeting_id}/action-items/{item_id}` | `{ owner?, due_date?, resolved_at? }` | `200 ActionItemOut` (FR-5.6 manual owner assignment) | `404`, `422` | +| `GET` | `/meetings/{meeting_id}/story-events` | — | `200 { items: StoryEventOut[] }` (FR-5.5 flagged linkages needing review) | `404` | + +--- + +## 9.7 Risks Router — `/api/v1/projects/{project_id}/risks` + +| Method | Path | Body / Params | Success | Errors | FR | +|---|---|---|---|---|---| +| `POST` | `/risks/assess` | — | `201 RiskRegisterSnapshot{risks[], version}` | `422 insufficient_project_data`, `503 ai_unavailable` | FR-6.1–FR-6.2 | +| `GET` | `/risks/snapshots` | `?cursor&limit` | `200 { items: RiskRegisterSnapshotOut[], next_cursor }` | — | FR-6.8 | +| `GET` | `/risks/snapshots/{snapshot_id}` | — | `200 RiskRegisterSnapshotOut` (full register, sorted severity desc per §5.4) | `404` | — | +| `PATCH` | `/risks/{risk_id}/disposition` | `{ disposition: "accepted"\|"mitigated"\|"dismissed", comment }` | `200 RiskEntry` | `404`, `422 invalid_disposition` | FR-6.6 | + +--- + +## 9.8 Knowledge Hub Router — `/api/v1/projects/{project_id}/knowledge` + +| Method | Path | Body / Params | Success | Errors | FR | +|---|---|---|---|---|---| +| `POST` | `/knowledge/repositories` | `{ provider, repo_url, personal_access_token }` | `201 { repository_id, index_status: "pending" }` — triggers initial full index as a background job (§8, §6.5) | `422 repo_access_denied`, `422 unsupported_provider` | FR-7.1, FR-7.2 | +| `GET` | `/knowledge/repositories` | — | `200 { items: RepositoryOut[] }` | — | — | +| `DELETE` | `/knowledge/repositories/{repository_id}` | — | `204` (drops chunk rows + Chroma vectors for this repo, revokes webhook) | `404` | — | +| `POST` | `/knowledge/repositories/{repository_id}/reindex` | — | `202 { ingestion_job_id }` (manual full re-index, `trigger="manual_reindex"`) | `404`, `409 index_already_running` | — | +| `GET` | `/knowledge/health` | — | `200` — the payload from §6.8 | `404` | FR-7.9 | +| `GET` | `/knowledge/ingestion-jobs/{job_id}` | — | `200 IngestionJobOut{status, files_processed, files_failed, error_log}` | `404` | FR-7.9 | + +--- + +## 9.9 Chat Router — `/api/v1/projects/{project_id}/chat` + +| Method | Path | Body / Params | Success | Errors | FR | +|---|---|---|---|---|---| +| `POST` | `/chat/sessions` | `{ repository_scope? }` | `201 { session_id }` | `404` (bad `repository_scope`) | FR-8.6 | +| `POST` | `/chat/stream` | `{ session_id, query }` | `200 text/event-stream` — SSE tokens then `{done: true, citations}` (§4.7.4, §7.6) | `422 knowledge_hub_empty`, `503 ai_unavailable` | FR-8.1–FR-8.4 | +| `GET` | `/chat/sessions/{session_id}/messages` | `?cursor&limit` | `200 { items: ChatMessageOut[], next_cursor }` | `404` | FR-8.7 | +| `POST` | `/chat/messages/{message_id}/feedback` | `{ feedback: "thumbs_up"\|"thumbs_down" }` | `200 ChatMessageOut` | `404`, `422 invalid_feedback_value` | FR-8.8 | +| `POST` | `/chat/diagram` | `{ session_id, repository_id }` | `200 text/event-stream` — narrative + final `mermaid` block (§8.6.6) | `404`, `422 repository_not_indexed` | FR-8.5 | +| `GET` | `/dashboard/documentation-gaps` | `?limit` | `200 { items: [{query_cluster, count, sample_queries[]}] }` (FR-8.9, computed on read per §5.3.7) | — | FR-8.9 | + +--- + +## 9.10 Webhooks Router — `/api/v1/webhooks` + +| Method | Path | Auth | Success | Notes | +|---|---|---|---|---| +| `POST` | `/webhooks/github` | HMAC-SHA256 signature (`X-Hub-Signature-256`) | `202 Accepted` (always — fast ack, real work is a `BackgroundTask`, §4.7.3) | Handles `push`, `issues`, `pull_request` event types; deduped via `X-GitHub-Delivery` in Redis | +| `POST` | `/webhooks/azure-devops` | Shared-secret query param validated server-side | `202 Accepted` | Handles code push + work item update service hooks | +| `POST` | `/webhooks/slack/interactive` | Slack signing secret | `200` | Reserved for future risk-disposition-from-Slack interactivity (not MVP — see Section 15) | + +A malformed or unverifiable signature on any webhook returns `400 { "error": "invalid_signature" }` and is never queued for processing — an unauthenticated caller cannot trigger a repo re-index or spoof a push event. + +--- + +## 9.11 Health & Admin Router — `/api/v1/` + +| Method | Path | Success | Notes | +|---|---|---|---| +| `GET` | `/health` | `200 { status, db, vector_store, ai_api }` (NFR-MAINT-3 — always 200, never 500 for degraded) | Load balancer target | +| `GET` | `/metrics` | `200 text/plain` (Prometheus exposition format, NFR-OBS-4) | Internal network only — not exposed publicly (enforced at the reverse-proxy layer, §14) | + +--- + +## 9.12 OpenAPI Generation + +FastAPI's automatic OpenAPI schema (`/docs`, `/openapi.json`, per §4.1) is the single source of truth for the contract in this section — every table above corresponds 1:1 to a router function with typed Pydantic request/response models (§4.8), so this document and the generated schema cannot silently drift: a change to a `StoryOut` field shows up in both simultaneously. `redoc_url=None` (§4.1) because Swagger UI's "Try it out" is more useful than ReDoc during active API development. diff --git a/architecture/section_10_background_workers.md b/architecture/section_10_background_workers.md new file mode 100644 index 0000000..753b877 --- /dev/null +++ b/architecture/section_10_background_workers.md @@ -0,0 +1,115 @@ +# Section 10: Background Workers + +> **ProjectOS** — AI-powered project management and knowledge platform +> This section catalogs every asynchronous job in the system, how it's scheduled, how it recovers from failure, and the explicit migration path from FastAPI `BackgroundTasks` to Celery when load demands it (ADR-6, §3.6). + +--- + +## 10.1 Why `BackgroundTasks` + `asyncio`, Not Celery, at MVP + +Per §0 of the requirements doc and ADR-6: "full Celery is probably overkill unless ingestion is slow." ProjectOS's background work at hackathon/MVP scale is bounded by a single process's `asyncio` event loop plus FastAPI's `BackgroundTasks`: + +- No separate worker process to deploy, monitor, or keep in sync with the API's dependency versions. +- No message broker (Redis-as-queue or RabbitMQ) to operate — Redis is reserved for rate limiting and caching only (ADR-6). +- Job durability is achieved a different way: every background job's state is persisted in PostgreSQL **before** the async task starts (`ingestion_jobs`, and the equivalent pattern for other job types below), so a process restart mid-job is recoverable by querying for `status IN ('pending','running')` rows on startup — not by relying on a broker's message durability. + +**The trade-off is explicit** (NFR-SCALE-5, ADR-6): this doesn't survive multi-instance horizontal scaling without care — two API replicas each running their own `BackgroundTasks` need a lock so the same job isn't picked up twice on restart-recovery. §10.6 covers this. + +--- + +## 10.2 Job Catalog + +| Job | Trigger | Concurrency control | Persisted state table | FR/NFR | +|---|---|---|---|---| +| Initial repository index | `POST /knowledge/repositories` | `INGESTION_WORKER_CONCURRENCY` (default 5) via semaphore | `ingestion_jobs` | FR-7.2 | +| Incremental webhook re-index | GitHub/Azure DevOps push webhook | Same semaphore pool as initial index | `ingestion_jobs` | FR-7.4 | +| Issue/PR ingestion | `issues`/`pull_request` webhook | Same pool | `ingestion_jobs` | FR-7.5 | +| Meeting → Knowledge Hub ingestion | Meeting summarization completes | Same pool | `ingestion_jobs` (repository_id NULL, meeting_id set) | FR-5.7 | +| Audio file cleanup | Immediately after successful transcription | N/A — single delete, not pooled | none (fire-and-forget, logged) | NFR-DATA-2 | +| Redis LLM cache purge | Nightly cron (`asyncio` scheduled task, 02:00 UTC) | N/A | Redis TTL already expires entries; this job clears any orphaned keys missing TTL | NFR-DATA-3 | +| Soft-delete sweep | Nightly cron, 03:00 UTC | N/A | `projects.deleted_at` / cascades | NFR-DATA-1 | +| Hard-delete sweep | Nightly cron, 03:30 UTC | N/A | Permanently removes rows soft-deleted > 30 days | NFR-DATA-1 | +| Stuck-job recovery | App startup (`lifespan`, §4.1) | N/A — runs once before accepting traffic | `ingestion_jobs` where `status IN ('pending','running')` | ADR-6 | +| High-severity risk Slack alert | Synchronous, inline in `RiskService.assess` (not backgrounded — needs to complete before the request returns per FR-6.7's "newly created" semantics) | N/A | none | FR-6.7 | +| External ticket sync (GitHub Issues / Azure DevOps push) | `POST /stories/{id}/export`, `POST /sprints/plans/{id}/sync` | Per-project semaphore shared with `AIService` calls is not reused here — a separate small pool (concurrency 3) since these are HTTP calls to third-party APIs with their own rate limits | `external_ticket_links` | FR-3.6, FR-4.8 | + +--- + +## 10.3 Ingestion Job Lifecycle + +```mermaid +stateDiagram-v2 + [*] --> pending: INSERT ingestion_jobs (webhook ack returns 202 immediately) + pending --> running: BackgroundTask picked up by worker + running --> completed: all files processed (per-file failures logged, don't block job completion) + running --> failed: unrecoverable error (Chroma unreachable, repo access revoked mid-job) + pending --> running: recovered on startup if process restarted while pending + running --> running: recovered on startup — re-diffs remaining files if process restarted while running (idempotent re-run, not resume-from-checkpoint) + completed --> [*] + failed --> pending: manual re-trigger via POST /knowledge/repositories/{id}/reindex +``` + +A `running` job recovered after a restart does **not** attempt to resume from a per-file checkpoint — it re-runs `list_changed_files` (or the full walk for `trigger=initial`) and relies on delete-then-reinsert per file (§6.5) being naturally idempotent. This is simpler and safer than tracking a resumable cursor, at the cost of some duplicate embedding work on the rare restart-mid-job case — an acceptable trade given how infrequently it happens. + +--- + +## 10.4 Scheduled (Cron-style) Jobs + +FastAPI has no built-in scheduler; ProjectOS runs scheduled jobs via `apscheduler`'s `AsyncIOScheduler`, started in the same `lifespan` context as the rest of startup (§4.1): + +```python +# app/core/scheduler.py +from apscheduler.schedulers.asyncio import AsyncIOScheduler + +scheduler = AsyncIOScheduler() + +def register_jobs(): + scheduler.add_job(purge_expired_cache_keys, "cron", hour=2, minute=0) + scheduler.add_job(soft_delete_sweep, "cron", hour=3, minute=0) + scheduler.add_job(hard_delete_sweep, "cron", hour=3, minute=30) +``` + +| Job | Schedule | What it does | +|---|---|---| +| `purge_expired_cache_keys` | Daily 02:00 UTC | Scans for Redis keys under the `llm_cache:*` prefix with no TTL set (defensive — normally TTL is set at write time) and deletes them; also emits a metric of cache hit rate over the prior 24h for cost-tracking visibility | +| `soft_delete_sweep` | Daily 03:00 UTC | Finds projects with `last_activity_at` older than 24 months (NFR-DATA-1), sets `deleted_at = now()` — does not touch ChromaDB or remove rows yet | +| `hard_delete_sweep` | Daily 03:30 UTC | Finds projects with `deleted_at` older than 30 days, executes the full delete cascade from §9.3's `DELETE /projects/{id}` path (reusing the same service function, not a separate code path) — including dropping the ChromaDB collection and revoking webhooks | + +Running these as in-process `apscheduler` jobs (not a separate cron container) keeps the same "single deployable unit" property as ADR-1 — at the cost that if two API replicas are ever run side by side without the leader-election guard in §10.6, the sweep would run twice redundantly (harmless here, since both sweeps are idempotent `WHERE` filters, but still called out honestly). + +--- + +## 10.5 Retry & Failure Handling + +Reuses the exponential backoff policy from §6.7 and §8.5 uniformly — there is exactly one backoff implementation in the codebase (`app/core/retry.py`), used by ingestion file processing, AI service calls, and external ticket sync alike: + +```python +# app/core/retry.py +async def with_backoff(fn, max_attempts=5, base_delay=5, max_delay=300): + for attempt in range(max_attempts): + try: + return await fn() + except RetryableError: + if attempt == max_attempts - 1: + raise + delay = min(base_delay * (2 ** attempt), max_delay) + await asyncio.sleep(delay) +``` + +Permanently failed items (ingestion file, external ticket sync) are recorded with their reason rather than silently dropped — `ingestion_jobs.error_log` (FR-7.9) and `external_ticket_links` simply never gets a row (the export endpoint returns `502 external_api_error` synchronously to the caller instead, since ticket export is a user-initiated action expecting an immediate result, not a background fire-and-forget). + +--- + +## 10.6 Path to Celery (Documented, Not Built) + +NFR-SCALE-1 requires the API tier to be stateless and horizontally scalable. `BackgroundTasks` living in the same process as the API that received the request works cleanly with **one** replica. The moment a second API replica is added behind a load balancer (NFR-SCALE-2), two problems appear that Celery solves and `BackgroundTasks` does not: + +1. **Startup recovery races**: both replicas' `lifespan` recovery routines (§10.3) would query the same `ingestion_jobs WHERE status='pending'` rows and could double-process a job. +2. **Load imbalance**: a webhook landing on replica A schedules its `BackgroundTask` on replica A specifically — if replica A is already saturated with ingestion work, replica B sits idle even though it could take the job. + +**Migration path** (not built at MVP, but the seam is deliberate): `IngestionService.process_webhook_job` and the AI-service-calling functions are already written as plain `async def` functions taking a job ID and doing their own DB fetch of state — this is exactly the shape a Celery task needs. Migrating means: +- Swap `background_tasks.add_task(fn, job_id)` for `celery_app.send_task("ingest_job", args=[job_id])`. +- Add `SELECT ... FOR UPDATE SKIP LOCKED` (or a Celery-native single-delivery guarantee) to the recovery routine to fix the double-processing race. +- No change to service-layer logic, schema, or job-state tables — the job catalog in §10.2 and the state machine in §10.3 are the actual contract; `BackgroundTasks` vs. Celery is just which mechanism drains the queue. + +This is called out explicitly so a reviewer doesn't mistake "no Celery" for "no plan for scale" — it's a scoped, reversible decision (ADR-6), not an oversight. diff --git a/architecture/section_11_technology_stack.md b/architecture/section_11_technology_stack.md new file mode 100644 index 0000000..5834352 --- /dev/null +++ b/architecture/section_11_technology_stack.md @@ -0,0 +1,104 @@ +# Section 11: Technology Stack + +> **ProjectOS** — AI-powered project management and knowledge platform +> Every library and service choice made in Sections 3–10, consolidated with version pins and the rejected alternative for each. + +--- + +## 11.1 Backend Core + +| Component | Choice | Version | Why | Rejected alternative | +|---|---|---|---|---| +| Web framework | FastAPI | `>=0.115` | Native async, automatic OpenAPI (§9.12), first-class SSE streaming support for chat (FR-8.4) and dependency injection that matches the layered service architecture in §4.3–§4.4 | Flask — no native async, would need `gevent`/threading hacks for concurrent Claude calls (§4.7.2's semaphore pattern needs real async) | +| ASGI server | Uvicorn (with `uvloop`) | `>=0.30` | Standard FastAPI pairing; `uvloop` gives a meaningful throughput bump for the concurrent-LLM-call workload (NFR-THROUGHPUT) | Hypercorn — no material advantage for ProjectOS's HTTP/1.1 + SSE needs, adds an unfamiliar deployment surface | +| Language | Python | 3.12 | Team's existing skillset (per the requirements doc's framing — "a Python team"); `tree-sitter-languages` and `faster-whisper` are Python-native | N/A — Python was set by the target team | +| ORM | SQLAlchemy (async) | `>=2.0` | Async session support (`asyncpg` driver) required for §4.7.1's no-blocking-calls rule; 2.0's typed `select()` API pairs cleanly with Pydantic schemas | Raw `asyncpg` queries — would mean hand-writing every query in §5, losing Alembic's autogenerate | +| DB driver | `asyncpg` | `>=0.29` | Fastest async Postgres driver available for SQLAlchemy 2.0 | `psycopg3` async mode — comparable, but `asyncpg` has more production mileage with SQLAlchemy async | +| Migrations | Alembic | `>=1.13` | NFR-MAINT-2 mandate; autogenerate from SQLAlchemy models keeps the 18-table schema (§5) from drifting between code and DB | Manual SQL migration files — loses autogenerate diffing, higher risk of the exact schema drift NFR-MAINT-2 exists to prevent | +| Config | `pydantic-settings` | `>=2.4` | NFR-MAINT-4 — typed env var loading with startup-time validation (§4.10) | `python-decouple` / raw `os.environ` — no type coercion or startup-fail-fast behavior | +| Validation / schemas | Pydantic | `v2` | Required by FastAPI; v2's Rust core gives real throughput benefit on the JSON-heavy structured-output paths (§4.8, §8) | N/A — pinned by FastAPI's own dependency | +| HTTP client | `httpx` (async) | `>=0.27` | NFR-SEC-7 mandates HTTPS-only with explicit timeouts; async client needed for concurrent GitHub/Azure DevOps/Claude calls | `aiohttp` — comparable, but `httpx`'s sync/async API symmetry simplifies testing with `TestClient` (NFR-MAINT-1) | +| Scheduled jobs | `apscheduler` (`AsyncIOScheduler`) | `>=3.10` | In-process cron for the nightly sweeps (§10.4) without adding a separate cron container — matches ADR-1's single-deployable-unit philosophy | System crontab calling a script — would run outside the app's lifespan, duplicating config/DB connection setup | + +--- + +## 11.2 AI & ML + +| Component | Choice | Version | Why | Rejected alternative | +|---|---|---|---|---| +| LLM (reasoning/generation) | Claude Sonnet | `claude-sonnet-4-6` (§4.10 config) | Story generation, meeting summarization, RAG chat, sprint sanity pass — tasks needing nuanced long-form generation quality (§8.2) | GPT-4-class models — ProjectOS standardizes on one LLM vendor to keep the prompt-caching (NFR-SCALE-4) and cost-logging (NFR-OBS-2) pipeline uniform | +| LLM (cheap/fast classification) | Claude Haiku | `claude-haiku-4-5-20251001` | INVEST critique, dependency inference, risk classification — high-frequency, lower-reasoning-depth calls (§8.2) | Running everything on Sonnet — would inflate cost roughly proportional to the fraction of calls that are classification-shaped, without a demonstrated quality need | +| LLM SDK | `anthropic` (official Python SDK) | `>=0.34` | Native support for prompt caching (`cache_control`), streaming, and structured tool-use output that `AIService` (§8.1) wraps | Raw `httpx` calls to the Messages API — reinvents retry/streaming handling the SDK already provides | +| Embeddings (code) | Voyage AI `voyage-code-2` | API-based | Purpose-built for code retrieval quality — directly named in the requirements doc as the code-specific choice (§0) | OpenAI `text-embedding-3-small` for code — noticeably weaker on code-semantic retrieval per the requirements doc's own guidance | +| Embeddings (text/docs/tickets/meetings) | OpenAI `text-embedding-3-small` | API-based | Cheap, fast, adequate for natural-language chunk types where code-specific embedding strength isn't needed | Voyage for everything — one extra vendor dependency for no retrieval-quality gain on prose content | +| Vector store | ChromaDB (embedded mode) | `>=0.5` | Zero-ops, in-process, matches ADR-4 (§3.6) — collection-per-project namespacing (§5.1, §6.6) built in | Qdrant — production-grade but requires a separate service to operate; ChromaDB's `HttpClient` swap is the documented upgrade path (ADR-4) if scale demands it | +| Audio transcription | `faster-whisper` | `>=1.0` | Runs fully in-process, no external API cost or network dependency, works offline for a live demo (ADR-5) | OpenAI Whisper API — adds per-call cost and a network dependency for a feature (audio meeting upload) that benefits most from working without connectivity during a demo | +| Code parsing | `tree-sitter` + `tree-sitter-languages` | latest | Syntax-aware chunking at function/class boundaries (§6.4.1) — directly named in the requirements doc as dramatically improving code retrieval quality over naive line-splitting | Naive line-based or regex chunking — the exact anti-pattern the requirements doc calls out | +| Static import analysis | Python `ast` (stdlib) + `tree-sitter` import queries | stdlib / latest | Architecture diagram generation (FR-8.5) needs real parsed import edges, not LLM-guessed relationships (§8.6.6) | LLM-inferred architecture from prose description — explicitly rejected in the requirements doc's gaps section (§9): "generated from actually parsing the code... not generated from vibes" | +| Fuzzy string matching | `rapidfuzz` | `>=3.9` | Meeting-to-story linking (FR-5.4) and slip detection (FR-6.5) at 80% similarity threshold — `rapidfuzz` is a faster, actively maintained drop-in for the older `fuzzywuzzy` | `fuzzywuzzy` — slower (pure Python vs. `rapidfuzz`'s C++ core) and effectively unmaintained | +| Diagram rendering | Mermaid (client-rendered) | — | Chat UI already needs a markdown/mermaid renderer for FR-8.5's diagram output; keeping diagram generation as text (mermaid syntax) instead of a rendered image means it's editable and diffable | `graphviz` server-side rendering — an extra binary dependency and a static image instead of an interactive diagram in the chat UI | + +--- + +## 11.3 Data Stores + +| Component | Choice | Version | Why | Rejected alternative | +|---|---|---|---|---| +| Relational database | PostgreSQL | 16 | JSONB support (used throughout §5 for `acceptance_criteria`, `citations`, `error_log`), mature async driver support, generated columns (`risks.severity`) | MySQL — weaker JSONB/generated-column ergonomics for the schema's needs | +| Cache / rate limiter | Redis | 7.x | Sliding-window rate limiting (NFR-SEC-9), LLM response cache (§8.4.2), webhook dedup (§4.7.3) — three narrowly-scoped uses per ADR-6, deliberately not a message broker | Memcached — no native sorted-set/TTL primitives needed for sliding-window rate limiting | +| Vector store | ChromaDB | `>=0.5` (see §11.2) | — | — | +| Object/file storage | Local filesystem (`/uploads`, `/data/chromadb`) | — | Adequate at hackathon/single-instance scale (NFR-DATA-2's audio files are deleted within 10 minutes anyway); documented upgrade path to S3-compatible storage in Section 15 | S3 from day one — adds IAM/bucket-policy setup with no MVP benefit given the short-lived nature of uploaded files | + +--- + +## 11.4 Frontend + +| Component | Choice | Version | Why | Rejected alternative | +|---|---|---|---|---| +| Framework | React | 18 | Team familiarity assumption from the requirements doc (§0); SSE consumption (`EventSource`/`fetch` streaming) for chat (FR-8.4) and Kanban drag-and-drop (FR-4.7) are both well-supported ecosystems | Vue/Svelte — no strong reason to deviate from the requirements doc's suggested default | +| Build tool | Vite | `>=5` | Fast dev server iteration during active feature-building; named explicitly in the requirements doc's suggested stack (§0) | Create React App — deprecated, slower HMR | +| Styling | Tailwind CSS | `>=3.4` | Rapid UI iteration without a separate design-system build step — appropriate for the delivery timeline | CSS Modules / styled-components — more setup overhead for comparable visual output at this stage | +| Drag-and-drop (Kanban) | `@dnd-kit/core` | latest | Sprint board manual reordering (FR-4.7) needs accessible, well-maintained DnD primitives | `react-beautiful-dnd` — unmaintained since 2022 | +| Diagram rendering | `mermaid` (JS) | `>=10` | Renders both the architecture diagrams (FR-8.5) and any embedded ER/flow diagrams surfaced in-app from the same syntax used throughout this architecture doc | Custom SVG rendering — no reason to hand-roll what Mermaid already solves | + +--- + +## 11.5 Security & Auth + +| Component | Choice | Why | +|---|---|---| +| JWT handling | `python-jose` (or `PyJWT`) | Access/refresh token issuance and validation (§4.3, NFR-SEC-2) | +| Password/token hashing | `bcrypt` (via `passlib`) | Refresh tokens stored hashed, never plaintext (NFR-SEC-2) | +| Field encryption | `cryptography` (Fernet/AES-256-GCM) | `integrations.encrypted_credentials` (NFR-SEC-5) | +| OAuth | `authlib` | GitHub/Microsoft OAuth2 flows (FR-1.1) — handles the full authorization-code exchange without hand-rolling redirect/state validation | +| Malware scanning | ClamAV (`pyclamd` client) | NFR-SEC-3, uploaded requirements docs and audio files — flagged P2/post-hackathon in the NFR priority matrix (§2.9), included here for completeness of the target stack | +| SQL injection prevention | SQLAlchemy parameterized queries + `bandit` pre-commit hook (`-t B608`) | NFR-SEC-8 | + +--- + +## 11.6 External Integrations + +| Integration | Library | Purpose | +|---|---|---| +| GitHub | `PyGithub` | Repo file/tree access, Issues/PRs, webhook payload parsing (§6.2) | +| Azure DevOps | `azure-devops` (official Python SDK) | Work items, repos, pipelines (§6.2) | +| Slack | Incoming Webhooks (plain `httpx` POST, no SDK needed) | High-severity risk alerts (FR-6.7) | +| Document parsing | `pdfplumber` (PDF), `python-docx` (DOCX) | Requirements document text extraction (FR-2.1) | + +--- + +## 11.7 Observability & Quality + +| Component | Choice | Why | +|---|---|---| +| Structured logging | `structlog` | JSON log lines matching NFR-OBS-1/2/3's exact field contracts | +| Metrics | `prometheus-client` | NFR-OBS-4's named metrics, exposed at `/metrics` (§9.11) | +| Testing | `pytest` + `pytest-asyncio` + `httpx.AsyncClient` (as `TestClient`) | NFR-MAINT-1's integration test coverage across auth/membership/validation/503 scenarios | +| Static analysis | `bandit`, `ruff` | Pre-commit security (NFR-SEC-8) and lint/format enforcement | +| Type checking | `mypy` (or `pyright`) | Catches service-layer contract violations (§4.4.2's typed method signatures) before runtime | + +--- + +## 11.8 Version Pinning Policy + +All dependencies are pinned via `pyproject.toml` + a locked `uv.lock` (or `poetry.lock`) — exact versions in CI and production, with `>=` ranges shown above representing the minimum tested version at architecture-design time, not a floor to build against loosely. Claude model identifiers (`CLAUDE_SONNET_MODEL`, `CLAUDE_HAIKU_MODEL`, §4.10) are environment-configurable specifically so a model version bump is a config change, not a code deploy — consistent with NFR-MAINT-4's "no hardcoded config" rule extending to model selection. diff --git a/architecture/section_12_folder_structure.md b/architecture/section_12_folder_structure.md new file mode 100644 index 0000000..36a9586 --- /dev/null +++ b/architecture/section_12_folder_structure.md @@ -0,0 +1,219 @@ +# Section 12: Folder Structure + +> **ProjectOS** — AI-powered project management and knowledge platform +> The concrete repository layout implementing every module boundary described in Sections 4–11 — every file referenced by path elsewhere in this document appears here. + +--- + +## 12.1 Repository Root + +``` +projectos/ +├── backend/ # FastAPI application (this doc's primary subject) +├── frontend/ # React + Vite + Tailwind app +├── infra/ # Docker Compose, Kubernetes manifests, nginx config (Section 14) +├── docs/ +│ └── architecture/ # this document, section by section +├── .github/ +│ └── workflows/ +│ ├── ci.yml # lint, type-check, test on PR +│ └── deploy.yml # build + push image on merge to main +├── .env.example +└── README.md +``` + +--- + +## 12.2 Backend Structure + +``` +backend/ +├── app/ +│ ├── main.py # FastAPI app factory, lifespan (§4.1) +│ │ +│ ├── api/ +│ │ └── v1/ +│ │ ├── auth.py # /auth/* (§9.2) +│ │ ├── projects.py # /projects/* (§9.3) +│ │ ├── requirements.py # /projects/{id}/requirements/* (§9.4) +│ │ ├── stories.py # /projects/{id}/stories/* (§9.4) +│ │ ├── sprints.py # /projects/{id}/sprints/* (§9.5) +│ │ ├── meetings.py # /projects/{id}/meetings/* (§9.6) +│ │ ├── risks.py # /projects/{id}/risks/* (§9.7) +│ │ ├── knowledge.py # /projects/{id}/knowledge/* (§9.8) +│ │ ├── chat.py # /projects/{id}/chat/* (§9.9) +│ │ ├── webhooks.py # /webhooks/* (§9.10) +│ │ └── health.py # /health, /metrics (§9.11) +│ │ +│ ├── core/ +│ │ ├── config.py # Settings (pydantic-settings) (§4.10) +│ │ ├── database.py # SQLAlchemy async engine, session factory +│ │ ├── vector_store.py # ChromaDB client + collection helpers (proj_{id}_knowledge / _planning) +│ │ ├── redis_client.py # Redis connection pool +│ │ ├── deps.py # get_db, get_current_user, assert_project_member (§4.3) +│ │ ├── exceptions.py # Domain exception hierarchy (§4.6) +│ │ ├── middleware.py # RequestID, RequestLogging, RateLimit (§4.5) +│ │ ├── retry.py # Shared exponential backoff (§10.5) +│ │ ├── scheduler.py # apscheduler cron job registration (§10.4) +│ │ └── security.py # JWT encode/decode, token encryption (§13) +│ │ +│ ├── models/ # SQLAlchemy ORM models — one file per table group (§5) +│ │ ├── user.py # User, ProjectMember +│ │ ├── project.py # Project, Integration +│ │ ├── requirements.py # RequirementsDocument, WorkPackage +│ │ ├── story.py # Story, INVESTCritique, ExternalTicketLink +│ │ ├── sprint.py # SprintPlan, Sprint, SprintAssignment +│ │ ├── meeting.py # Meeting, MeetingDecision, ActionItem, MeetingRiskSignal, StoryEvent +│ │ ├── risk.py # RiskRegisterSnapshot, Risk +│ │ ├── knowledge.py # Repository, IngestionJob, KnowledgeChunk +│ │ └── chat.py # ChatSession, ChatMessage +│ │ +│ ├── schemas/ # Pydantic request/response models (§4.8) +│ │ ├── auth.py +│ │ ├── projects.py +│ │ ├── requirements.py +│ │ ├── stories.py +│ │ ├── sprints.py +│ │ ├── meetings.py +│ │ ├── risks.py +│ │ ├── knowledge.py +│ │ └── chat.py +│ │ +│ ├── services/ # Business logic — routers call these, never each other (§4.4) +│ │ ├── auth_service.py +│ │ ├── requirements_service.py +│ │ ├── story_service.py +│ │ ├── sprint_service.py +│ │ ├── meeting_service.py +│ │ ├── risk_service.py +│ │ ├── chat_service.py +│ │ ├── arch_diagram_service.py # Static analysis + mermaid generation (§8.6.6) +│ │ ├── ai_service.py # Claude call chokepoint (§8.1) +│ │ ├── rag_service.py # Shared retrieve() + generate_with_citations() (§7.1) +│ │ ├── whisper_service.py # faster-whisper wrapper, lazy model load (ADR-5) +│ │ └── knowledge_hub/ +│ │ ├── ingestion_service.py # Orchestrates connectors → chunkers → embed → upsert (§6.3) +│ │ ├── connectors/ +│ │ │ ├── base.py # SourceConnector protocol (§6.2) +│ │ │ ├── github_connector.py +│ │ │ └── azure_devops_connector.py +│ │ └── chunkers/ +│ │ ├── code_chunker.py # tree-sitter (§6.4.1) +│ │ ├── markdown_chunker.py # heading-based split (§6.4.2) +│ │ ├── yaml_chunker.py # CI/CD definitions (§6.4.3) +│ │ ├── ticket_chunker.py # Issues/PRs (§6.4.4) +│ │ └── meeting_chunker.py # Fixed-window summary chunks (§6.4.5) +│ │ +│ ├── ai/ +│ │ ├── prompts.py # Versioned prompt template constants (§8.3) +│ │ └── prompt_templates.py # PromptTemplate dataclass, cache_control block builder (§7.5) +│ │ +│ ├── workers/ +│ │ ├── ingestion_jobs.py # BackgroundTask entry points (§10.2) +│ │ ├── sweeps.py # Nightly cache purge, soft/hard delete (§10.4) +│ │ └── recovery.py # Startup stuck-job requeue (§10.3, ADR-6) +│ │ +│ └── integrations/ # Thin wrappers around third-party SDKs +│ ├── github_client.py # PyGithub wrapper (OAuth, Issues, webhooks) +│ ├── azure_devops_client.py # azure-devops SDK wrapper +│ └── slack_client.py # Incoming webhook POST (§11.6) +│ +├── alembic/ +│ ├── versions/ # One migration per table group, dependency-ordered (§5.5, §17) +│ └── env.py +│ +├── tests/ +│ ├── conftest.py # pytest fixtures — test DB, mocked Claude/Chroma +│ ├── integration/ +│ │ ├── test_auth.py +│ │ ├── test_projects.py +│ │ ├── test_stories.py +│ │ ├── test_sprints.py +│ │ ├── test_meetings.py +│ │ ├── test_risks.py +│ │ ├── test_knowledge.py +│ │ ├── test_chat.py +│ │ └── test_webhooks.py +│ └── unit/ +│ ├── test_chunkers.py +│ ├── test_rag_service.py +│ ├── test_ai_service_retry.py +│ └── test_bin_packing.py # Sprint greedy allocation algorithm (FR-4.2) +│ +├── pyproject.toml +├── uv.lock # or poetry.lock — exact-pinned per §11.8 +├── alembic.ini +└── Dockerfile +``` + +--- + +## 12.3 Frontend Structure + +``` +frontend/ +├── src/ +│ ├── main.tsx +│ ├── App.tsx +│ ├── api/ # Typed API client, one module per router (mirrors §9) +│ │ ├── auth.ts +│ │ ├── projects.ts +│ │ ├── stories.ts +│ │ ├── sprints.ts +│ │ ├── meetings.ts +│ │ ├── risks.ts +│ │ ├── knowledge.ts +│ │ └── chat.ts +│ ├── pages/ +│ │ ├── Dashboard.tsx # FR-1.5 +│ │ ├── Requirements.tsx # FR-2.1–FR-2.4 +│ │ ├── Stories.tsx # FR-3.1–FR-3.7 +│ │ ├── SprintBoard.tsx # FR-4.6–FR-4.7 (Kanban, @dnd-kit) +│ │ ├── Meetings.tsx # FR-5.1–FR-5.6 +│ │ ├── RiskRegister.tsx # FR-6.1–FR-6.8 +│ │ ├── KnowledgeHub.tsx # FR-7.1, FR-7.9 (repo connection, index health) +│ │ └── Chat.tsx # FR-8.1–FR-8.9 (SSE streaming, mermaid rendering) +│ ├── components/ +│ │ ├── kanban/ +│ │ ├── chat/ +│ │ │ ├── MessageStream.tsx # SSE consumer +│ │ │ └── MermaidRenderer.tsx +│ │ └── shared/ +│ ├── hooks/ +│ │ ├── useSSE.ts +│ │ └── useProjectMember.ts +│ └── lib/ +│ └── mermaid.ts +├── index.html +├── vite.config.ts +├── tailwind.config.js +└── package.json +``` + +--- + +## 12.4 Infra Structure + +``` +infra/ +├── docker-compose.yml # Local dev: postgres, redis, backend, frontend (Section 14) +├── docker-compose.prod.yml +├── nginx/ +│ └── nginx.conf # X-Accel-Buffering: no for SSE (§4.7.4), reverse proxy to FastAPI +└── k8s/ # Optional — post-hackathon scaling path (Section 14, 15) + ├── backend-deployment.yaml + ├── backend-service.yaml + └── ingress.yaml +``` + +--- + +## 12.5 Structural Invariants (Enforced, Not Just Convention) + +These mirror the dependency-graph rules from §4.11 and are checked by `ruff`'s import-linting rules in CI, not just documented: + +- `app/services/` never imports from `app/api/`. +- `app/api/` never imports from another `app/api/*` module directly — cross-router needs go through a shared service. +- `app/core/` never imports from `app/services/` or `app/api/` — it's the dependency floor everything else builds on. +- `app/services/knowledge_hub/connectors/*` never imports from `app/services/knowledge_hub/chunkers/*` and vice versa — connectors fetch bytes, chunkers process bytes; `ingestion_service.py` is the only module that imports both, keeping the router pattern in §6.3 a single well-defined seam. +- Every file in `app/models/` has a corresponding file in `app/schemas/` (ORM row vs. API-facing shape are never the same class) and a corresponding Alembic migration under `alembic/versions/` (§5.5). diff --git a/architecture/section_13_security_architecture.md b/architecture/section_13_security_architecture.md new file mode 100644 index 0000000..cdc6cf6 --- /dev/null +++ b/architecture/section_13_security_architecture.md @@ -0,0 +1,179 @@ +# Section 13: Security Architecture + +> **ProjectOS** — AI-powered project management and knowledge platform +> This section consolidates every security control referenced across Sections 2, 4, 5, and 9 into one place, plus the OAuth/JWT flow diagram. + +--- + +## 13.1 Threat Model Summary + +ProjectOS handles: OAuth-authenticated user sessions, customer source code (via connected repositories), third-party API credentials (GitHub/Azure DevOps PATs), and meeting content that may include sensitive project discussion. The controls below map to these four asset classes plus the multi-tenancy boundary that must hold across all of them (FR-1.4). + +| Asset | Primary threat | Primary control | +|---|---|---| +| User session | Token theft / replay | Short-lived access tokens, rotated single-use refresh tokens (§13.3) | +| Connected repo source code | Cross-project data leakage | `project_id` filtering at every layer, Chroma collection namespacing (§13.5) | +| GitHub/Azure DevOps PATs | Credential exposure at rest | AES-256 field encryption (§13.6) | +| Meeting/chat content | Sensitive content in logs | Prompt/response logging disabled by default in prod (§13.7) | +| Webhook endpoints | Spoofed ingestion trigger | HMAC signature verification (§13.8) | + +--- + +## 13.2 Authentication & Authorization Flow + +```mermaid +sequenceDiagram + participant User + participant FE as React Frontend + participant API as FastAPI /auth + participant GH as GitHub OAuth + participant PG as PostgreSQL + + User->>FE: Click "Sign in with GitHub" + FE->>API: GET /auth/github/login + API->>API: generate state token, store in short-lived Redis key + API-->>FE: 302 redirect to GitHub consent screen (with state) + FE->>GH: User authorizes + GH-->>API: GET /auth/github/callback?code=...&state=... + API->>API: verify state matches Redis-stored value (CSRF protection) + API->>GH: exchange code for GitHub access token + GH-->>API: GitHub access token + user profile + API->>PG: SELECT users WHERE oauth_provider='github' AND oauth_subject=? + alt user exists + PG-->>API: existing user row + else new user + API->>PG: INSERT users (oauth_provider, oauth_subject, email, display_name) + end + API->>API: issue JWT access_token (15 min TTL, NFR-SEC-2) + API->>API: generate refresh_token, bcrypt-hash it, store in PostgreSQL refresh_tokens + API-->>FE: { access_token, refresh_token, user } + FE->>FE: store access_token in memory, refresh_token in httpOnly cookie + + Note over FE,API: 15 minutes later — access token expired + FE->>API: POST /auth/refresh { refresh_token } + API->>PG: SELECT refresh_tokens WHERE token_hash=bcrypt(refresh_token) + alt token valid and unused + API->>PG: UPDATE refresh_tokens SET used=true (rotation) + API->>PG: INSERT new refresh_tokens row + API-->>FE: { access_token (new), refresh_token (new) } + else token already marked used (reuse detected) + API->>PG: revoke ALL refresh_tokens for this user (token theft response) + API-->>FE: 401 token_reused — force full re-login + end +``` + +**Why rotation + reuse detection, not just a long-lived refresh token**: a stolen refresh token used by an attacker after the legitimate client has already rotated it is the classic signal of theft. Revoking the entire token family on reuse (rather than just rejecting the one request) is what makes stealing a refresh token a dead end rather than a standing backdoor — this is the concrete mechanism behind the `token_reused` error code introduced in §4.6 and §9.2. + +--- + +## 13.3 Token Lifecycle Detail (NFR-SEC-2) + +| Token | TTL | Storage | Rotation | +|---|---|---|---| +| Access token (JWT) | 15 minutes | Client memory only — never persisted to `localStorage` (XSS mitigation: a compromised page can't read it after the tab closes) | Not rotated — simply expires; client calls `/auth/refresh` | +| Refresh token | 7 days | `httpOnly`, `Secure`, `SameSite=Strict` cookie on the client; `bcrypt`-hashed in PostgreSQL `refresh_tokens` server-side | Single-use — every `/auth/refresh` call issues a new one and marks the old one `used=true` | + +JWT claims: `{ sub: user_id, iat, exp, jti }`. `jti` (JWT ID) is logged with every request (tying into `request_id` in NFR-OBS-1) so a specific token's usage pattern is traceable without decoding the token itself from logs. + +--- + +## 13.4 Authorization — Project Membership Enforcement (NFR-SEC-4) + +Enforced at **two layers**, not one, per NFR-SEC-4's explicit requirement that membership checks live in the service layer, not just routing: + +1. **Route dependency**: `Depends(assert_project_member)` (§4.3) runs before any handler body executes, rejecting non-members with `403 not_project_member` before a single query touches project data. +2. **Service-layer defense in depth**: every service method that accepts a `project_id` re-derives its queries with an explicit `WHERE project_id = ?` clause — even though the route dependency already checked membership, a service method is never trusted to be called *only* from a route (background jobs and the recovery routine in §10.3 call services directly, bypassing the route dependency chain entirely, so the filter has to be structural in the query itself, not just in a decorator). + +Role enforcement (`project_owner` / `project_member` / `viewer`, §1.3) is checked inline in the specific service methods that need it (delete project, invite members, connect integrations) rather than as a generic decorator — RBAC is intentionally shallow at MVP (§1.3's explicit note), so a generic role-decorator system would be premature infrastructure for three flat roles. + +--- + +## 13.5 Multi-Tenancy Isolation (FR-1.4, NFR-SCALE-3) + +| Layer | Isolation mechanism | +|---|---| +| PostgreSQL | Every tenant-scoped table has non-nullable `project_id` FK (§5.1); no query omits it | +| ChromaDB | Collection-per-project (`proj_{project_id}_knowledge`, `proj_{project_id}_planning`) — not a shared collection with a metadata filter. A bug that forgets a metadata filter on a shared collection leaks data; a bug that queries the wrong collection name gets zero results, which is a visibly broken feature, not a silent leak. Namespacing at the collection boundary converts a security bug class into a functional bug class. | +| Redis | Cache keys and rate-limit counters are prefixed with `project_id` or `user_id` where relevant (e.g., `llm_cache:{project_id}:{prompt_hash}`) so no cross-project cache poisoning is possible even if a hash collision occurred | +| File storage | Uploaded files are written to `/uploads/{project_id}/{uuid}` — never a flat shared directory | + +This is the concrete implementation of the requirements doc's explicit warning (§0, §9): "every table and every vector-store query needs a `project_id` filter from day one... retrofitting this later is painful." + +--- + +## 13.6 Secrets & Credential Management + +| Secret | At rest | In transit | Rotation | +|---|---|---|---| +| GitHub/Azure DevOps PATs | AES-256-GCM encrypted in `integrations.encrypted_credentials` (NFR-SEC-5), key from `TOKEN_ENCRYPTION_KEY` env var | HTTPS only to GitHub/Azure DevOps APIs (NFR-SEC-7) | User-initiated re-connect via `POST /projects/{id}/integrations`; no silent auto-rotation at MVP | +| `JWT_SECRET_KEY`, `TOKEN_ENCRYPTION_KEY`, `ANTHROPIC_API_KEY`, `VOYAGE_API_KEY` | Injected via environment variables (§4.10), never committed, never logged | N/A (server-side only) | Manual rotation via deployment config update (Section 14's secrets manager) | +| Webhook secrets (`repositories.webhook_secret`) | Plaintext column — these are HMAC secrets used to *verify* inbound signatures, not credentials that grant access outward, so encryption-at-rest is lower priority than for PATs, but the column is still excluded from any API response schema | Sent to GitHub/Azure DevOps once at webhook registration time over HTTPS | Regenerated on `POST /knowledge/repositories/{id}/reindex` if a webhook secret compromise is suspected (manual trigger, not automatic) | + +--- + +## 13.7 Data Handling & Logging (NFR-SEC-6, NFR-DATA-1–4) + +- **No prompt/response logging by default**: `LOG_PROMPTS=false` in production (§4.10) — meeting transcripts, requirements documents, and chat queries frequently contain project-sensitive content that shouldn't sit in log aggregation with a long retention window. +- **Audio deleted within 10 minutes of transcription** (NFR-DATA-2) — the raw voice recording is the highest-sensitivity artifact in the system (potentially containing off-the-cuff remarks never meant to be permanently stored) and is treated as ephemeral by design, not just by cleanup-job diligence. +- **Structured logs never include full request/response bodies** — only IDs, status codes, and durations (NFR-OBS-1). A developer debugging a production issue works from `request_id` correlation, not from replaying logged payloads. + +--- + +## 13.8 Webhook Signature Verification (§9.10) + +```python +# app/api/v1/webhooks.py +def verify_github_signature(request: Request, raw_body: bytes, secret: str): + expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest() + received = request.headers.get("X-Hub-Signature-256", "") + if not hmac.compare_digest(expected, received): # constant-time comparison — timing attack resistant + raise HTTPException(status_code=400, detail="invalid_signature") +``` + +Using `hmac.compare_digest` instead of `==` is deliberate — a naive string comparison short-circuits on the first mismatched byte, which is a measurable timing side-channel an attacker could use to guess the correct signature byte-by-byte. This is a small enough detail to skip by accident and exactly the kind of thing that's cheap to get right up front. + +--- + +## 13.9 Rate Limiting (NFR-SEC-9) + +Redis sliding-window counter, keyed `(user_id, endpoint_class)`: + +| Tier | Limit | Endpoints | +|---|---|---| +| Standard | 100 req/min | All CRUD endpoints | +| LLM-heavy | 5 req/min | `/stories/generate`, `/chat/stream`, `/risks/assess`, `/meetings/summarize` (§4.5.3) | + +This is a **security** control as much as a cost control: uncapped access to LLM-heavy endpoints is a vector for both cost-exhaustion attacks and for a compromised account to exfiltrate the entire knowledge base via rapid-fire chat queries — the same 5 req/min ceiling that protects the Claude API budget also bounds how fast a bad actor with valid credentials can scrape retrieval results. + +--- + +## 13.10 Input Validation & Injection Prevention (NFR-SEC-8) + +- **SQL**: SQLAlchemy ORM / parameterized `text()` exclusively; `bandit -t B608` blocks string-interpolated SQL at pre-commit (§11.7). +- **File upload validation**: MIME type checked against an allowlist (`application/pdf`, `.docx`'s MIME type, `text/markdown`, `.mp3`/`.wav`/`.m4a` audio types) before processing — rejected with `415 unsupported_file_type` (§4.6) rather than attempting to parse an unexpected format. +- **Malware scanning**: ClamAV scan on all uploads before they're written to disk in a readable state (NFR-SEC-3) — deferred to post-hackathon per the NFR priority matrix (§2.9), but the upload handler's interface already takes a scan step as a no-op hook so wiring in ClamAV later doesn't require restructuring the upload path. +- **LLM prompt injection**: retrieved chunks (from potentially untrusted repo content — a malicious commit could contain text designed to manipulate the chat assistant) are wrapped in clearly delimited context blocks in the prompt template (§7.5) with explicit system-prompt instructions that content inside `[chunk: ...]` tags is *reference material*, not instructions to follow — this doesn't eliminate prompt injection risk entirely (no current technique does), but it's the standard mitigation and is called out here so it isn't silently absent from the design. + +--- + +## 13.11 Transport Security (NFR-SEC-7) + +- All external calls (Claude, GitHub, Azure DevOps, Voyage AI, Slack) use `httpx` with `verify=True` (default certificate validation), 30s connect / 120s read timeouts. +- CORS is allowlist-based (`CORS_ORIGINS` env var, §4.10) — never `allow_origins=["*"]` in production, since the API issues bearer tokens that a wildcard CORS policy combined with credentialed requests would expose to any origin. +- TLS termination happens at the reverse proxy (nginx, §14) — the FastAPI app itself never terminates TLS directly. + +--- + +## 13.12 Security Control Summary Table + +| NFR | Control | Section | +|---|---|---| +| NFR-SEC-1 | JWT required on all routes except public auth/health/webhook endpoints | §13.2, §9.1 | +| NFR-SEC-2 | 15 min access / 7 day rotated refresh tokens | §13.3 | +| NFR-SEC-3 | ClamAV scan on uploads (P2, post-hackathon) | §13.10 | +| NFR-SEC-4 | Service-layer + route-layer membership checks | §13.4 | +| NFR-SEC-5 | AES-256 encrypted integration credentials | §13.6 | +| NFR-SEC-6 | No prompt/response logging in prod | §13.7 | +| NFR-SEC-7 | HTTPS-only external calls, explicit timeouts | §13.11 | +| NFR-SEC-8 | Parameterized SQL, `bandit` pre-commit gate | §13.10 | +| NFR-SEC-9 | Tiered Redis rate limiting | §13.9 | diff --git a/architecture/section_14_deployment_architecture.md b/architecture/section_14_deployment_architecture.md new file mode 100644 index 0000000..4bab6f1 --- /dev/null +++ b/architecture/section_14_deployment_architecture.md @@ -0,0 +1,231 @@ +# Section 14: Deployment Architecture + +> **ProjectOS** — AI-powered project management and knowledge platform +> Two deployment targets are described: the **hackathon/demo topology** (single-host Docker Compose, ships in hours) and the **post-hackathon topology** (containers behind a load balancer, managed data stores) — with an explicit, low-friction path from one to the other. + +--- + +## 14.1 Deployment Diagram — Hackathon / Demo Topology + +```mermaid +graph TB + subgraph "Single Host (VM or laptop)" + Nginx["nginx\nTLS termination\nSSE-aware reverse proxy"] + + subgraph "Docker Compose" + API1["FastAPI container\n(Uvicorn + uvloop)\nport 8000"] + FE["React static build\nserved by nginx directly"] + PG[("PostgreSQL 16\ncontainer, volume-mounted")] + Redis[("Redis 7\ncontainer")] + ChromaVol[("ChromaDB\nembedded in API container\n/data/chromadb volume")] + end + end + + Internet["Internet\n(demo judges, team)"] -->|HTTPS 443| Nginx + Nginx -->|/api/*| API1 + Nginx -->|/| FE + API1 --> PG + API1 --> Redis + API1 --> ChromaVol + + API1 -.->|HTTPS| ClaudeAPI["Claude API\n(Anthropic)"] + API1 -.->|HTTPS| VoyageAPI["Voyage AI / OpenAI\nEmbeddings"] + API1 -.->|HTTPS| GHAPI["GitHub API"] + API1 -.->|HTTPS| ADOAPI["Azure DevOps API"] + API1 -.->|HTTPS| SlackAPI["Slack Webhook"] + GHAPI -.->|webhook POST| Nginx +``` + +**Single-host rationale**: this matches ADR-1 (monolith) and ADR-4 (ChromaDB embedded) exactly — one process holds the API, the vector store, and the background task loop. Deployable via `docker-compose up -d` in minutes, which matters for a hackathon build/demo cycle where redeploy iteration speed is a real constraint. + +--- + +## 14.2 Deployment Diagram — Post-Hackathon / Scaled Topology + +```mermaid +graph TB + Internet["Internet"] -->|HTTPS 443| LB["Load Balancer\n(managed, e.g. ALB/Cloud LB)"] + LB --> Nginx1["nginx / ingress\nreplica 1"] + LB --> Nginx2["nginx / ingress\nreplica 2"] + + Nginx1 --> API1["FastAPI container\nreplica 1 — stateless (NFR-SCALE-1)"] + Nginx2 --> API2["FastAPI container\nreplica 2"] + + API1 --> PGPrimary[("PostgreSQL primary\nmanaged / RDS Multi-AZ")] + API2 --> PGPrimary + PGPrimary -.->|streaming replication\nNFR-AVAIL-5| PGReplica[("PostgreSQL\nhot standby")] + + API1 --> RedisCluster[("Redis\nmanaged, single primary\n(rate limit + cache only)")] + API2 --> RedisCluster + + API1 --> ChromaService["ChromaDB\nHttpClient mode\n(separate service, ADR-4 swap)"] + API2 --> ChromaService + + subgraph "Object Storage" + S3[("S3-compatible bucket\nrequirements docs, temp audio")] + end + API1 --> S3 + API2 --> S3 + + API1 -.->|HTTPS| ClaudeAPI["Claude API"] + API2 -.->|HTTPS| ClaudeAPI + + GHWebhook["GitHub Webhooks"] -->|HTTPS| LB +``` + +**What actually changes between the two topologies** (this is the important part — it's a config/infra change, not a rewrite): + +| Component | Hackathon | Scaled | Code change required? | +|---|---|---|---| +| API replicas | 1 | N (behind LB) | None — `NFR-SCALE-1`'s stateless design (§2.3) is already the baseline | +| ChromaDB | Embedded (`chromadb.Client()`) | `chromadb.HttpClient(host=...)` | One-line config swap (ADR-4, §3.6) | +| PostgreSQL | Single container | Managed, Multi-AZ, hot standby | Connection string change only | +| File storage | Local `/uploads` volume | S3-compatible bucket | `app/core/file_storage.py` abstraction swap (interface unchanged — see §12.2's structure) | +| Ingestion jobs | `BackgroundTasks` | Celery (if load demands, §10.6) | Documented seam, not built at MVP | +| Static frontend | Served by the same nginx as the API proxy | CDN-fronted (CloudFront/Cloudflare) | Build output relocation only | + +--- + +## 14.3 Container Definitions + +```dockerfile +# backend/Dockerfile +FROM python:3.12-slim +WORKDIR /app +COPY pyproject.toml uv.lock ./ +RUN pip install uv && uv sync --frozen --no-dev +COPY app/ ./app/ +COPY alembic/ ./alembic/ +COPY alembic.ini ./ +EXPOSE 8000 +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--loop", "uvloop"] +``` + +`docker-compose.yml` (hackathon topology, §14.1): + +```yaml +services: + backend: + build: ./backend + env_file: .env + volumes: + - chroma_data:/data/chromadb + - upload_data:/app/uploads + depends_on: + postgres: { condition: service_healthy } + redis: { condition: service_healthy } + ports: ["8000:8000"] + + postgres: + image: postgres:16 + environment: + POSTGRES_DB: projectos + volumes: ["pg_data:/var/lib/postgresql/data"] + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + + redis: + image: redis:7 + healthcheck: + test: ["CMD", "redis-cli", "ping"] + + nginx: + image: nginx:alpine + volumes: ["./infra/nginx/nginx.conf:/etc/nginx/nginx.conf:ro"] + ports: ["443:443", "80:80"] + depends_on: [backend] + +volumes: + pg_data: + chroma_data: + upload_data: +``` + +--- + +## 14.4 nginx Configuration — SSE-Aware Reverse Proxy + +The single most important nginx detail in this system is disabling response buffering for the chat streaming endpoints (§4.7.4) — buffered SSE defeats the entire point of token-by-token delivery (FR-8.4, NFR-UX-4's "typing indicator within 300ms"): + +```nginx +# infra/nginx/nginx.conf +location /api/v1/projects/ { + proxy_pass http://backend:8000; + proxy_http_version 1.1; + proxy_set_header Connection ""; + + # SSE-critical settings — chat/stream and chat/diagram endpoints + proxy_buffering off; + proxy_cache off; + proxy_set_header X-Accel-Buffering no; + proxy_read_timeout 120s; # covers the 45s story-gen / 90s audio-meeting worst case (§2.1) +} + +location /webhooks/ { + proxy_pass http://backend:8000; + client_max_body_size 5M; # GitHub webhook payloads are small; large payloads are rejected, not buffered +} + +location /metrics { + deny all; # NFR-OBS-4 — internal-only, never internet-facing +} +``` + +--- + +## 14.5 Environment & Secrets + +All runtime config flows through `pydantic-settings` (§4.10) sourced from environment variables. Deployment-specific secret injection: + +| Environment | Secret source | +|---|---| +| Local dev | `.env` file (git-ignored, `.env.example` committed as a template) | +| Hackathon demo | `.env` file on the demo host, or Docker Compose `env_file` | +| Post-hackathon | Managed secrets manager (AWS Secrets Manager / GCP Secret Manager / Kubernetes Secrets) injected as environment variables at container start — never baked into the image | + +No secret ever appears in `Dockerfile`, `docker-compose.yml`, or version control — this is the same rule as NFR-MAINT-4 extended to the deployment layer. + +--- + +## 14.6 CI/CD Pipeline + +``` +.github/workflows/ci.yml — on every PR: + 1. ruff check (lint) + 2. mypy (type check) + 3. bandit -t B608 (SQL injection gate, NFR-SEC-8) + 4. pytest (integration + unit, NFR-MAINT-1's 80% coverage gate) + 5. docker build (verify image builds cleanly) + +.github/workflows/deploy.yml — on merge to main: + 1. docker build + tag with commit SHA + 2. push to container registry + 3. run `alembic upgrade head` against target DB (pre-deploy migration step — never auto-run inside app startup, per NFR-MAINT-2's explicit up/down migration discipline) + 4. rolling restart of API containers (single-host: `docker-compose up -d --no-deps backend`; scaled: rolling update via orchestrator) + 5. post-deploy: hit /health, roll back the image tag if status != "ok" within 60s +``` + +Migrations run as a **separate pipeline step before** the new container starts, not inside `lifespan` — this avoids the failure mode where two API replicas both try to run `alembic upgrade head` concurrently during a rolling deploy. + +--- + +## 14.7 Scaling Path (NFR-SCALE-1, NFR-SCALE-2) + +Horizontal scaling of the API tier requires zero code changes because of the statelessness guarantee already built into §4 (no in-process session state; all state in Postgres/Redis/Chroma). Adding a second replica is purely an infra change: + +1. Point ChromaDB at `HttpClient` mode (ADR-4) — a shared, network-accessible vector store instead of one embedded per process. +2. Point PostgreSQL connections at the managed instance (already the case if using RDS/Cloud SQL). +3. Add the replica behind the load balancer. +4. Apply the leader-election guard from §10.6 to the `apscheduler` cron jobs and the startup recovery routine — this is the **one** piece of actual code change needed when going from 1 to N replicas (a `SELECT ... FOR UPDATE SKIP LOCKED` guard or a dedicated single "scheduler" replica), called out explicitly rather than glossed over. + +--- + +## 14.8 Backup & Disaster Recovery + +| Data | Backup strategy | RPO/RTO | +|---|---|---| +| PostgreSQL | Automated daily snapshots (managed service) + streaming replication to hot standby (NFR-AVAIL-5) | RPO ~5 min (replication lag), RTO < 30s (automatic failover) | +| ChromaDB | Rebuildable from PostgreSQL `knowledge_chunks` metadata + source repos (§5.1 principle 4) — not separately backed up at MVP; a full re-index is the recovery path | RTO = time to re-run initial index (§2.1: 500 files/min) | +| Uploaded files | Ephemeral by design (audio deleted in 10 min, requirements docs retained only as long as `requirements_documents.raw_text` in Postgres, which *is* backed up) | N/A — the durable copy is the extracted text in Postgres | + +The deliberate choice **not** to back up ChromaDB separately is consistent with §5.1's design principle: Postgres is the source of truth for provenance, Chroma is a derived index. Losing Chroma is a (bounded, measurable) re-indexing cost, not a data-loss event. diff --git a/architecture/section_15_future_scalability.md b/architecture/section_15_future_scalability.md new file mode 100644 index 0000000..ebb6752 --- /dev/null +++ b/architecture/section_15_future_scalability.md @@ -0,0 +1,96 @@ +# Section 15: Future Scalability + +> **ProjectOS** — AI-powered project management and knowledge platform +> Every deliberate scope cut made in Sections 1–14 is listed here with its trigger condition and upgrade path — nothing in this document was "forgotten," it was deferred with a stated reason. + +--- + +## 15.1 Philosophy + +Throughout this architecture, features and infrastructure were scoped to the hackathon/MVP target (NFR priority matrix, §2.9) with an explicit note of what a real production evolution looks like. This section is the consolidated version of those notes — a punch list for "what does ProjectOS look like at 10x, 100x scale, or with a real ops team behind it," so a reviewer can distinguish "not built because it's out of scope" from "not built because it was overlooked." + +--- + +## 15.2 Retrieval Quality + +**Cross-encoder re-ranking** (ADR-R2, §7.9): heuristic re-ranking (file-proximity dedup + recency boost) is sufficient at MVP scale. **Trigger to revisit**: FR-8.8 thumbs-down feedback rate on chat answers exceeding a set threshold (e.g., 15% sustained over a rolling week), or FR-8.9's documentation-gap signal showing repeated re-asking of the same question in different phrasings (a symptom of retrieval missing the right chunk on the first phrasing). **Upgrade path**: add a re-ranking pass (e.g., Voyage's rerank endpoint or a Cohere/cross-encoder call) between §7.2's steps G and I — it's an additional pipeline stage, not a redesign, since `rag_service.retrieve()` already isolates re-ranking as its own step. + +**Query expansion beyond 2-turn history** (§7.7): follow-up resolution currently uses the last 2 turns. **Trigger**: multi-turn technical conversations (e.g., a developer debugging across 5+ turns) showing degraded retrieval relevance deeper into a session. **Upgrade path**: replace the fixed 2-turn window with an LLM-generated standalone-query rewrite step (a small Haiku call that condenses the full session into one self-contained query) before embedding. + +--- + +## 15.3 Vector Store & Knowledge Hub Scale + +**ChromaDB embedded → HttpClient** (ADR-4, §3.6, §14.2): already the documented one-line swap for horizontal scaling. **Trigger**: second API replica needed (NFR-SCALE-2), or a single project's chunk count approaching the practical ceiling of embedded-mode performance. + +**Collection sharding beyond 500 projects × 1M chunks** (NFR-SCALE-3): the current namespacing (`proj_{id}_knowledge`) scales linearly with project count but each collection is still a single logical unit. **Trigger**: an individual enterprise customer with a monorepo exceeding ~1M chunks (large polyglot codebases, e.g. a 10-year-old enterprise monolith). **Upgrade path**: partition by `repository_id` sub-collections within a project once the requirements doc's original "cross-repo query" concern (§6.6) is re-balanced against single-repo scale — likely a hybrid: keep single-repo queries fast via per-repo sub-collections, fan out only when the user's query is genuinely cross-repo. + +**Additional source connectors** (§6.2, ADR-K1): GitLab, Bitbucket, Confluence, Notion. **Upgrade path**: implement `SourceConnector` (§6.2's Protocol interface) — the chunking pipeline (§6.3–§6.4) already dispatches on content shape, not provider, so a new connector is additive, not a refactor. + +**Streaming/parallel initial index for very large repos**: current initial index target is 500 files/min via `asyncio.gather` batches of 20 (§4.7.5). **Trigger**: a connected monorepo with 100k+ files where a full initial index takes over an hour. **Upgrade path**: increase batch parallelism via `INGESTION_WORKER_CONCURRENCY` (already an env var, §11.1) and/or move ingestion to dedicated Celery workers (§15.4) that can scale independently of the API tier's replica count. + +--- + +## 15.4 Background Processing at Scale + +**Celery migration** (§10.6, ADR-6): the single most explicitly-planned deferred piece in this document. **Trigger**: second API replica in production (the startup-recovery race and load-imbalance problems in §10.6 become real, not theoretical, at that point). **Upgrade path**: fully documented in §10.6 — service functions are already Celery-task-shaped (`async def` taking a job ID, doing its own state fetch), so the migration is a dispatch-mechanism swap plus a `SELECT ... FOR UPDATE SKIP LOCKED` guard on the recovery routine, not a service-layer rewrite. + +**Dead-letter handling for permanently failed ingestion files**: currently, failed files after 5 retries are recorded in `error_log` and simply skipped (§6.7, §10.5). **Trigger**: a customer with a repo containing many files hitting `embedding_error` repeatedly (e.g., an embedding provider outage during their initial index) needing a bulk retry mechanism rather than a full re-index. **Upgrade path**: a `POST /knowledge/repositories/{id}/retry-failed` endpoint that re-queues only the `error_log` entries, reusing the same delete-then-reinsert logic scoped to just those file paths. + +--- + +## 15.5 Data Layer + +**Read replicas for PostgreSQL beyond the hot standby** (NFR-AVAIL-5): the current standby is for failover, not read scaling. **Trigger**: dashboard/read-heavy endpoints (project dashboard, risk register history, chat message history) showing primary DB CPU pressure from concurrent users beyond the 50-user MVP target (§2.1). **Upgrade path**: route read-only queries (list endpoints in §9) through a read-replica connection pool; write paths stay on the primary — a config-layer change given SQLAlchemy's async engine already abstracts the connection. + +**Object storage migration** (§14.2 table): local filesystem → S3-compatible bucket. **Trigger**: any deployment beyond a single host (files on local disk don't survive a container being rescheduled to a different node). **Upgrade path**: already isolated behind a `file_storage` abstraction in the folder structure (§12.2) — swap the implementation, not the call sites. + +**Chat/meeting data retention beyond 24 months** (NFR-DATA-1): currently soft-delete then hard-delete on a fixed timeline. **Trigger**: enterprise customers with compliance requirements needing configurable per-project retention windows (e.g., legal hold). **Upgrade path**: add a `retention_override_days` column to `projects`, checked by the nightly sweep (§10.4) before applying the default. + +--- + +## 15.6 Security & Access Control + +**Fine-grained RBAC** (§1.3, §13.4): three flat roles (`project_owner`, `project_member`, `viewer`) is an explicit MVP scope cut, not an oversight — the note in §1.3 says so directly. **Trigger**: a customer needing per-module permissions (e.g., "can view risk register but not edit sprint plans") or a role beyond project scope (org-level admin across multiple projects). **Upgrade path**: introduce a `permissions` JSONB column on `project_members` or a separate `role_permissions` table, checked in the same service-layer location that currently does simple role string comparisons (§13.4) — the enforcement *pattern* (service-layer check, not just route-layer) doesn't change, only the granularity of what's checked. + +**Enterprise SSO/SAML**: current auth is GitHub/Microsoft OAuth only (FR-1.1). **Trigger**: an enterprise customer whose IT policy mandates SAML/Okta/Azure AD SSO rather than direct GitHub/Microsoft OAuth. **Upgrade path**: `authlib` (§11.5) supports SAML providers; the `users.oauth_provider` check (`CHECK` constraint, §5.3.1) widens to include the new provider value, and a new callback route is added alongside the existing two — the `users`/`project_members` schema doesn't change shape. + +**ClamAV malware scanning** (NFR-SEC-3, §13.10): deferred to post-hackathon per the NFR priority matrix (§2.9). **Trigger**: any production deployment handling untrusted user uploads at scale — this is a should-have-before-general-availability item, not a distant future one. **Upgrade path**: the upload handler already has a scan-step hook reserved (§13.10) — wiring in `pyclamd` is additive. + +--- + +## 15.7 Cost & Model Strategy + +**Multi-LLM fallback**: ProjectOS currently standardizes on Claude exclusively (§11.2) for pipeline uniformity. **Trigger**: sustained Claude API degradation beyond what retry/backoff (§8.5) can absorb, or a cost-optimization push once usage patterns are well understood via NFR-OBS-2/4's per-feature cost logging. **Upgrade path**: `AIService.call_claude` (§8.1) is the single chokepoint — a fallback model parameter and provider-agnostic wrapper could be added there without touching any of the seven calling services, since none of them talk to Claude directly. + +**Cross-encoder / rerank-as-a-service cost**: any re-ranking upgrade (§15.2) adds a per-query cost; this should be evaluated against the thumbs-down-rate trigger, not added speculatively. + +**Fine-tuned or distilled models for high-frequency classification tasks** (INVEST critique, risk classification, dependency inference — currently Haiku): once call volume for these specific tasks is large enough to justify the training/eval investment, a smaller fine-tuned classifier could replace Haiku for the highest-frequency, most-templated of these three. **Trigger**: token cost attribution (NFR-OBS-4's `projectos_tokens_used_total{feature}`) showing one of these three features as a disproportionate share of spend at high user volume. + +--- + +## 15.8 Product Surface Area + +**Evaluation/feedback loop beyond thumbs up/down** (FR-8.8, and the "gaps" note in the requirements doc §9): currently a simple binary signal on chat answers. **Trigger**: enough usage volume that a lightweight signal stops being diagnostic enough to drive prompt/retrieval improvements. **Upgrade path**: extend `chat_messages.feedback` to a richer taxonomy (`incorrect`, `incomplete`, `not_grounded`, `unhelpful_tone`) and apply the same pattern to story generation and risk assessment outputs (currently un-instrumented for feedback) — the schema change is additive (new enum values, or a new `feedback_reason` column), not a redesign. + +**Slack/Teams interactivity beyond one-way alerts** (§9.10's reserved `/webhooks/slack/interactive` route, FR-6.7's current scope of alert-only): a user dispositioning a risk directly from a Slack message, or approving a sprint plan from a Teams card. **Trigger**: explicit customer request — this is a nice-to-have named in the requirements doc's gaps section (§9: "notifications... cheap, high demo impact") that was deliberately scoped down to outbound-only for MVP. **Upgrade path**: the route is already reserved and unused; wiring it up means validating Slack's interactive-payload signature (same HMAC pattern as §13.8) and mapping button actions to the existing `PATCH /risks/{id}/disposition` service call. + +**Multi-region deployment**: not addressed at all in Section 14 — single-region by design at this scale. **Trigger**: latency-sensitive customers in multiple geographies, or data-residency compliance requirements. **Upgrade path**: this is the largest lift on this list — it touches the vector store (per-region collections or replication), the primary database (multi-region Postgres is a significant operational step up from the hot-standby pattern in §14.8), and would likely need is own dedicated architecture pass rather than a quick patch to this document. + +--- + +## 15.9 Summary Table — Trigger Conditions at a Glance + +| Deferred item | Trigger to build | Effort to add | +|---|---|---| +| Cross-encoder re-ranking | Sustained chat thumbs-down rate | Small — one pipeline stage | +| ChromaDB HttpClient mode | 2nd API replica | Trivial — config swap | +| Celery migration | 2nd API replica | Medium — dispatch swap + locking guard | +| S3 object storage | Any multi-host deployment | Small — abstraction already isolated | +| Fine-grained RBAC | Customer needs per-module permissions | Medium — new permissions table | +| SSO/SAML | Enterprise IT policy requirement | Small — `authlib` provider addition | +| ClamAV scanning | Any GA deployment | Small — hook already reserved | +| Multi-LLM fallback | Sustained Claude outage / cost push | Medium — one chokepoint to modify | +| Richer feedback taxonomy | Feedback volume high enough to act on | Small — schema field addition | +| Slack interactivity | Explicit customer request | Small — route already reserved | +| Multi-region | Geo-latency / data residency need | Large — dedicated design effort | diff --git a/architecture/section_16_development_roadmap.md b/architecture/section_16_development_roadmap.md new file mode 100644 index 0000000..e3cc7ae --- /dev/null +++ b/architecture/section_16_development_roadmap.md @@ -0,0 +1,113 @@ +# Section 16: Development Roadmap + +> **ProjectOS** — AI-powered project management and knowledge platform +> A phased build order that matches the requirements doc's own prioritization (§10: "Building all 8 modules well in a hackathon timeframe is unlikely") — the shared RAG pipeline first, then the highest-priority module pair, with everything else sequenced by dependency and demo impact. + +--- + +## 16.1 Guiding Principle + +The requirements doc is explicit: *"Build the shared RAG service first... that's the piece worth getting right before splitting up into parallel workstreams."* This roadmap treats §7 (RAG Architecture) and §6 (Knowledge Hub) as Phase 0 — nothing else in the system should be built in parallel with the retrieval pipeline, because Risk Assessment (§8.6.5), Onboarding Chat (§7.2), and even the meeting-to-knowledge-hub ingestion (§6.4.5) all depend on `rag_service.py` existing and being correct. + +--- + +## 16.2 Phase 0 — Foundations (Day 1, Hours 0–8) + +**Goal**: a working skeleton every other module builds on top of. + +| Task | Owner track | Section reference | +|---|---|---| +| Repo scaffold per §12.2's folder structure | Backend lead | §12 | +| `docker-compose.yml` — Postgres, Redis, FastAPI skeleton | DevOps/backend | §14.1, §14.3 | +| `core/config.py`, `core/database.py`, `core/deps.py` | Backend | §4.10, §4.3 | +| Initial Alembic migration: `users`, `projects`, `project_members` | Backend | §5.3.1, §17 | +| GitHub OAuth login flow end-to-end | Backend | §13.2, FR-1.1 | +| `AIService.call_claude` skeleton (no caching/retry yet — just a working Claude call) | Backend | §8.1 | +| `rag_service.py` skeleton: `retrieve()` against a single test ChromaDB collection | Backend | §7.1 | + +**Exit criteria**: a user can log in via GitHub, create a project, and the backend can make one successful Claude API call and one successful ChromaDB query. Nothing product-facing works yet — this phase is infrastructure only. + +--- + +## 16.3 Phase 1 — Knowledge Hub + Onboarding Q&A (Day 1 Hours 8–20 → Day 2 Hours 0–8) + +Matches the requirements doc's #1 priority (§10). This is the largest single phase because it's the foundation for the demo's strongest "wow" moment (RAG chat with citations) and exercises the full shared pipeline end to end. + +| Task | Section reference | +|---|---| +| `GitHubConnector` — file tree walk, content fetch | §6.2 | +| `tree-sitter` code chunker (start with Python + JS support only — add languages incrementally) | §6.4.1 | +| Markdown chunker | §6.4.2 | +| Embedding calls (Voyage AI for code, OpenAI for docs) wired into `AIService` | §8.1, §11.2 | +| `knowledge_chunks` table + Chroma upsert | §5.3.6, §6.6 | +| `POST /knowledge/repositories` — triggers initial index as a background task | §9.8, §10.2 | +| `GET /knowledge/health` | §6.8, §9.8 | +| `ChatService.stream_answer` — retrieval + SSE generation + citation resolution | §7.6, §8.6.7 | +| Chat UI with SSE consumption and citation rendering | §12.3 | +| **Demo checkpoint**: connect a real repo, ask "how does X work," get a cited, streamed answer | FR-8.1–FR-8.4 | + +**Deliberately deferred within this phase**: GitHub webhook incremental re-indexing (FR-7.4), Azure DevOps connector, CI/CD YAML ingestion, architecture diagram generation (FR-8.5). These are valuable but not required for the core "ask a question, get a cited answer" demo loop — added in Phase 4 if time remains. + +--- + +## 16.4 Phase 2 — User Stories + Sprint Planning (Day 2, Hours 8–16) + +Matches priority #2 (§10). Can start as soon as Phase 0's `AIService` exists — does not depend on Phase 1's RAG pipeline being complete, since dependency inference (FR-4.3) and story generation don't retrieve from the knowledge hub (§7.3's table explicitly notes this). + +| Task | Section reference | +|---|---| +| Requirements upload + `pdfplumber`/`python-docx` text extraction | FR-2.1, §11.6 | +| `work_packages` extraction via Claude Sonnet, structured JSON | §8.6.1 | +| Duplicate detection (cosine similarity > 0.92) | FR-2.5 | +| Story generation (Sonnet) + INVEST critique (Haiku) two-pass | §8.6.2 | +| Greedy bin-packing sprint allocation (plain Python) | FR-4.2 | +| Dependency inference (Haiku) + sanity pass (Sonnet) | §8.6.3 | +| Kanban board UI with `@dnd-kit` drag-and-drop | FR-4.6, FR-4.7, §11.4 | +| **Demo checkpoint**: upload a requirements doc → generated stories with acceptance criteria → sprint plan on a Kanban board | FR-2.2–FR-4.7 | + +--- + +## 16.5 Phase 3 — Meeting Summarization + Risk Assessment (Day 2, Hours 16–22) + +Matches priorities #3 and #4 (§10), grouped together because Risk Assessment's "last 5 meetings" input (FR-6.1) needs at least one working meeting summary to be meaningfully demoable — building them back-to-back lets the same test data serve both. + +| Task | Section reference | +|---|---| +| Text transcript submission + Claude summarization (decisions/action items/risks/open questions) | §8.6.4 | +| `faster-whisper` audio path (lazy-loaded model) — **stretch goal if time allows**, text-only is an acceptable fallback | ADR-5, FR-5.2 | +| `rapidfuzz` story linking (80% threshold) | FR-5.4 | +| Non-LLM risk checks: overloaded sprints, slipping action items | FR-6.4, FR-6.5 | +| LLM risk digest (Haiku) merged with non-LLM checks | §8.6.5 | +| Slack webhook alert on `severity >= 15` | FR-6.7 | +| Risk register UI, sorted by severity | §5.4, FR-6.2 | +| **Demo checkpoint**: submit a meeting → summary with linked stories → run risk assessment → see a populated, severity-sorted risk register, with a live Slack alert firing for a high-severity risk | FR-5.1–FR-6.8 | + +--- + +## 16.6 Phase 4 — Polish & Stretch Goals (Day 2, Hours 22–24+) + +Only attempted after Phases 1–3's demo checkpoints all pass. Prioritized by demo impact per hour of effort: + +1. **Architecture diagram generation** (FR-8.5) — high demo impact ("ask for a diagram, get a real one built from parsed imports"), moderate effort given `ast`/tree-sitter parsing is already built for chunking. +2. **GitHub webhook incremental re-indexing** (FR-7.4) — proves the "continuously builds a knowledge hub" claim is real, not one-shot; moderate effort. +3. **External ticket export** (FR-3.6, FR-4.8) — good demo moment ("click button, stories appear in your backlog") but depends on having a real GitHub/Azure DevOps target project set up for the demo. +4. **Documentation gap dashboard signal** (FR-8.9) — nice cross-module callback, low effort once chat history exists. +5. **Prompt caching** (§8.4.1) — invisible to the demo audience but worth wiring up if cost/rate-limit issues appear during dev iteration. + +--- + +## 16.7 Post-Hackathon Roadmap (If Continued) + +| Milestone | Scope | Reference | +|---|---|---| +| M1 — Hardening | Full integration test suite to NFR-MAINT-1's 80% coverage target; ClamAV upload scanning; Alembic migration cleanup | §2.5, §13.10, §17 | +| M2 — Multi-instance readiness | ChromaDB → HttpClient mode; Celery migration; leader-election guard on scheduled jobs | §10.6, §14.7, §15.3–15.4 | +| M3 — Enterprise readiness | Fine-grained RBAC; SSO/SAML; S3 object storage; configurable data retention | §15.6, §15.5 | +| M4 — Quality feedback loop | Richer feedback taxonomy across all AI-generated artifacts (not just chat); cross-encoder re-ranking if warranted by feedback data | §15.8, §15.2 | +| M5 — Additional integrations | GitLab/Bitbucket connectors; Slack interactive risk disposition; multi-LLM fallback | §15.3, §15.7, §15.8 | + +--- + +## 16.8 Sequencing Rationale — Why Not Build in Parallel From Hour 0 + +A four-person team could be tempted to split into "planning engine" and "knowledge hub" sub-teams immediately. The requirements doc explicitly warns against this (§0: "don't let two sub-teams build two separate retrieval stacks"), and this roadmap encodes that warning structurally: Phase 0 is a hard gate before any module work starts, and Phase 1 (Knowledge Hub) is sequenced *before* Phase 2 (Planning Engine) specifically so that by the time sprint planning's dependency inference or risk assessment's digest logic is being written, `rag_service.py` and `AIService` already exist as stable, tested contracts — not something two teams are simultaneously trying to design differently. diff --git a/architecture/section_17_database_migration_plan.md b/architecture/section_17_database_migration_plan.md new file mode 100644 index 0000000..3598a6b --- /dev/null +++ b/architecture/section_17_database_migration_plan.md @@ -0,0 +1,139 @@ +# Section 17: Database Migration Plan + +> **ProjectOS** — AI-powered project management and knowledge platform +> The concrete Alembic migration sequence implementing the schema from Section 5, ordered to match the build phases in Section 16, plus rollback and zero-downtime discipline. + +--- + +## 17.1 Migration Principles (NFR-MAINT-2) + +- Every migration has both `upgrade()` and `downgrade()` — no one-way migrations. A migration without a working `downgrade()` is a rejected PR, full stop. +- Migrations are ordered so that **a table's migration never precedes a migration creating a table it foreign-keys into** (§5.5) — enforced naturally by Alembic's revision chain, but the ordering below is deliberately chosen to match §16's build phases so the schema grows in lockstep with feature work, not ahead of or behind it. +- No `ALTER TABLE` runs outside Alembic — direct schema changes in application code are prohibited (NFR-MAINT-2). +- Migrations run as a **separate CI/CD pipeline step before** the new container starts (§14.6) — never inside FastAPI's `lifespan` — to avoid two replicas racing to apply the same migration during a rolling deploy. + +--- + +## 17.2 Migration Sequence + +| # | Revision | Tables created | Depends on | Roadmap phase | +|---|---|---|---|---| +| 0001 | `initial_identity` | `users`, `projects`, `project_members` | — | Phase 0 (§16.2) | +| 0002 | `integrations` | `integrations` | 0001 (`projects`) | Phase 0 | +| 0003 | `requirements_traceability` | `requirements_documents`, `work_packages` | 0001 | Phase 2 (§16.4) | +| 0004 | `stories` | `stories`, `invest_critiques`, `external_ticket_links` | 0003 (`work_packages`) | Phase 2 | +| 0005 | `sprints` | `sprint_plans`, `sprints`, `sprint_assignments` | 0004 (`stories`) | Phase 2 | +| 0006 | `meetings` | `meetings`, `meeting_decisions`, `action_items`, `meeting_risk_signals`, `story_events` | 0001, 0004 (`story_events.story_id` → `stories`) | Phase 3 (§16.5) | +| 0007 | `risk_register` | `risk_register_snapshots`, `risks` | 0005 (`risks.related_sprint_id`), 0006 (`risks.related_action_item_id`) | Phase 3 | +| 0008 | `knowledge_hub` | `repositories`, `ingestion_jobs`, `knowledge_chunks` | 0001, 0006 (`knowledge_chunks.meeting_id`) | Phase 1 (§16.3) | +| 0009 | `chat` | `chat_sessions`, `chat_messages` | 0001, 0008 (`chat_sessions.repository_scope`) | Phase 1 | +| 0010 | `refresh_tokens` | `refresh_tokens` (auth support table, §13.3 — not in the ERD in §5.2 since it's an auth/security concern, not a domain entity) | 0001 | Phase 0 | + +**Note on ordering vs. build order**: Phase 1 (Knowledge Hub) is built *first* per the roadmap (§16.3), but its migrations (0008–0009) are numbered *after* the traceability-chain migrations (0003–0007) in this table because `knowledge_chunks.meeting_id` foreign-keys into `meetings` (0006). In practice, during Phase 1 development, `knowledge_chunks.meeting_id` and `repository_id` are the only columns exercised (repos/tickets/CI, no meeting ingestion yet since Phase 3 hasn't happened) — the migration can be written with the nullable FK in place from the start (§5.3.6 already specifies `meeting_id` as nullable) without requiring `meetings` to exist functionally, only for the migration file to run without a dangling FK reference. **Practical resolution**: migration 0006 (`meetings`) is written and applied during Phase 0 as an empty-but-structurally-complete table, even though the meeting summarization feature itself isn't built until Phase 3 — this is a one-time reordering exception, called out explicitly rather than silently deviating from the "table before its dependents" rule. + +--- + +## 17.3 Sample Migration — `0004_stories` + +```python +# alembic/versions/0004_stories.py +def upgrade(): + op.create_table( + "stories", + sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")), + sa.Column("project_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("projects.id"), nullable=False), + sa.Column("work_package_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("work_packages.id"), nullable=False), + sa.Column("title", sa.String(300), nullable=False), + sa.Column("description", sa.Text, nullable=False), + sa.Column("acceptance_criteria", postgresql.JSONB, nullable=False), + sa.Column("story_points", sa.SmallInteger, sa.CheckConstraint("story_points IN (1,2,3,5,8,13)"), nullable=False), + sa.Column("status", sa.String(20), nullable=False, server_default="draft"), + sa.Column("generation_model", sa.String(50), nullable=False), + sa.Column("generation_version", sa.Integer, nullable=False, server_default="1"), + sa.Column("created_at", sa.TIMESTAMP(timezone=True), nullable=False, server_default=sa.func.now()), + ) + op.create_index("ix_stories_project_status", "stories", ["project_id", "status"]) + op.create_index("ix_stories_work_package", "stories", ["work_package_id"]) + + op.create_table( + "invest_critiques", + sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")), + sa.Column("story_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("stories.id"), nullable=False), + sa.Column("violations", postgresql.JSONB, nullable=False), + sa.Column("suggested_fix", sa.Text, nullable=True), + sa.Column("created_at", sa.TIMESTAMP(timezone=True), nullable=False, server_default=sa.func.now()), + ) + + op.create_table( + "external_ticket_links", + sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")), + sa.Column("story_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("stories.id"), nullable=False), + sa.Column("provider", sa.String(20), nullable=False), + sa.Column("external_id", sa.String(100), nullable=False), + sa.Column("external_url", sa.Text, nullable=False), + sa.Column("synced_at", sa.TIMESTAMP(timezone=True), nullable=False, server_default=sa.func.now()), + sa.UniqueConstraint("story_id", "provider"), + ) + +def downgrade(): + op.drop_table("external_ticket_links") + op.drop_table("invest_critiques") + op.drop_index("ix_stories_work_package", table_name="stories") + op.drop_index("ix_stories_project_status", table_name="stories") + op.drop_table("stories") +``` + +`downgrade()` drops in strict reverse-dependency order (child tables before the parent `stories` table) — Alembic won't catch a wrong drop order at author time, only at `downgrade` execution time, so this is a manual-discipline point called out for reviewers. + +--- + +## 17.4 Seed Data Migration + +One additional migration, `0011_seed_dev_data`, is **environment-gated** — it only runs when `ENVIRONMENT=development` (checked inside the migration via `context.get_x_argument()`, not applied blindly in every environment): + +```python +def upgrade(): + if context.get_x_argument(as_dictionary=True).get("env") != "development": + return + # Seed a demo user, demo project, and one sample connected repository + # so a fresh dev environment has something to look at immediately +``` + +This is run via `alembic upgrade head -x env=development` in local dev setup scripts, never in the production deploy pipeline (§14.6's `alembic upgrade head` step passes no `-x env=` flag in production, so this migration becomes a no-op there). + +--- + +## 17.5 Rollback Strategy + +| Scenario | Action | +|---|---| +| Migration fails mid-apply (e.g., a `CHECK` constraint violation on existing data) | Alembic's transactional DDL (Postgres supports transactional schema changes) means a failed migration leaves the schema at its pre-migration state — no partial application to clean up | +| Migration applies successfully but the accompanying code deploy fails health check (§14.6) | The deploy pipeline rolls back the **container image** to the previous tag; the schema is **not** automatically rolled back — new columns/tables from the applied migration are additive and inert until the new code is deployed. This is why every migration in this plan is additive-only (new tables/columns), never a destructive rename-in-place, at least for the initial 10 migrations — a column rename or type change would need the expand/contract pattern in §17.6 to be rollback-safe | +| A migration needs to be reverted after being live for a while (e.g., a schema decision proves wrong) | `alembic downgrade -1` run manually, with the corresponding code revert deployed first (never downgrade a table a currently-running code version still queries) | + +--- + +## 17.6 Zero-Downtime Pattern for Future Schema Changes (Expand/Contract) + +Sections 15 and 16 already name concrete future schema changes: `permissions` JSONB on `project_members` (§15.6's RBAC upgrade), `retention_override_days` on `projects` (§15.5), a `feedback_reason` column on `chat_messages` (§15.8). Each of these is additive (new nullable column with a sensible default) and safe to apply without a maintenance window — old code ignores the new column, new code populates it. + +For any future change that **isn't** purely additive (e.g., eventually splitting `stories.acceptance_criteria` JSONB into a normalized `acceptance_criteria` table if query patterns demand it), the expand/contract pattern applies: + +1. **Expand**: add the new column/table alongside the old one; deploy code that writes to both. +2. **Backfill**: a one-off script (not an Alembic migration — a data migration, kept separate per NFR-MAINT-2's spirit of schema migrations being structural, not bulk data operations) populates the new structure from the old. +3. **Migrate reads**: deploy code that reads from the new structure only, still writing to both for one more deploy cycle as a safety net. +4. **Contract**: a final migration drops the old column/table once the new path has been live and verified. + +No such multi-step migration is needed for the initial schema (§17.2's 10 migrations are all additive-only, first-time table creation) — this section exists so the *pattern* is documented before it's needed, matching the same "state the upgrade path before you need it" discipline used throughout Section 15. + +--- + +## 17.7 Migration-to-Roadmap Cross-Reference + +This is the same information as §17.2's table, restated as a checklist against the phases in §16 so a reviewer can verify schema and feature work are never out of sync: + +- [ ] Phase 0 done ⇒ migrations 0001, 0002, 0010 applied +- [ ] Phase 1 (Knowledge Hub) done ⇒ migrations 0006 (structural only), 0008, 0009 applied +- [ ] Phase 2 (Stories/Sprints) done ⇒ migrations 0003, 0004, 0005 applied +- [ ] Phase 3 (Meetings/Risk) done ⇒ migration 0006 (functionally exercised), 0007 applied +- [ ] Any Section 15 upgrade item started ⇒ a new numbered migration (0012+) is added to this table before the corresponding code lands, not after diff --git a/architecture/section_18_risks_and_tradeoffs.md b/architecture/section_18_risks_and_tradeoffs.md new file mode 100644 index 0000000..e3ede04 --- /dev/null +++ b/architecture/section_18_risks_and_tradeoffs.md @@ -0,0 +1,127 @@ +# Section 18: Risks and Trade-offs + +> **ProjectOS** — AI-powered project management and knowledge platform +> The closing section: every architectural decision recorded as an ADR across this document, consolidated with its trade-off, plus the risks that sit above any single component — delivery risk, cost risk, and quality risk for a system this AI-dependent. + +--- + +## 18.1 Consolidated ADR Trade-off Table + +| ADR | Decision | Section | Trade-off accepted | +|---|---|---|---| +| ADR-1 | Monolith over microservices | §3.6 | Can't scale individual services independently; mitigated by stateless horizontal scaling of the whole API tier | +| ADR-2 | One RAG pipeline, two Chroma collections | §3.6 | Two collections to maintain per project — trivial operationally, but a real second thing that can drift out of sync if `rag_service.py`'s collection-selection logic has a bug | +| ADR-3 | Synchronous LLM calls for planning ops | §3.6 | Long-running requests hold a connection (up to 90s for audio meetings); mitigated by nginx timeout config (§14.4) and SSE for chat specifically | +| ADR-4 | ChromaDB embedded, not Qdrant | §3.6 | Not horizontally scalable in embedded mode; documented one-line swap to `HttpClient` mode when a second replica is needed (§15.3) | +| ADR-5 | `faster-whisper` in-process | §3.6 | ~150MB model loaded into API process memory; mitigated by lazy loading on first audio upload | +| ADR-6 | Redis for rate-limit/cache only, not a queue | §3.6 | Background tasks aren't durable across a process restart on their own; mitigated by persisting job state in Postgres (`ingestion_jobs`) and a startup recovery routine (§10.3) | +| ADR-K1 | Chunk by content shape, not source provider | §6.9 | None significant — this is a pure simplification with no real downside identified | +| ADR-K2 | Delete-then-reinsert per file on incremental index | §6.9 | Slightly more re-embedding work than true chunk-level diffing would require; accepted because chunk-level diffing needs stable chunk identity across refactors, which is a harder problem than the cost it would save | +| ADR-K3 | Meetings ingested as structured summary, not raw transcript | §6.9 | Raw transcript nuance is not searchable via the Knowledge Hub (only via direct meeting-record lookup); accepted because the summary is what "what did we decide" queries actually need | +| ADR-R1 | Retrieval opt-in per feature, not default | §7.9 | Requires each new AI feature's author to consciously decide whether it needs RAG; a future feature could wrongly skip retrieval when it shouldn't — mitigated by this being a design-review checklist item, not just convention | +| ADR-R2 | No cross-encoder re-ranker at MVP | §7.9 | Retrieval ranking quality is capped by heuristics (file-proximity dedup, recency boost) rather than a learned re-ranker; explicit trigger to revisit defined in §15.2 | +| ADR-R3 | Citations resolved only against actually-retrieved chunks | §7.9 | A small amount of extra bookkeeping per chat turn to track and validate `chunk_id` references; accepted because the alternative (trusting freeform citations) directly undermines the hallucination-guard requirement (§1.5) | + +--- + +## 18.2 Technical Risks + +### R-T1: tree-sitter parsing failures on unusual or legacy code + +**Risk**: a connected repository with heavily macro-generated code, unusual language dialects, or syntactically invalid-but-compiling code (rare but real in legacy codebases) causes `tree-sitter` to fail to produce meaningful function/class boundaries. + +**Impact**: falls back to whole-file chunking (§6.4.1's no-definitions branch) — retrieval quality for that specific file degrades to naive-chunking level, but the system doesn't crash or silently drop the file. + +**Mitigation**: already designed as a graceful degradation (`parse_fallback` logged, not a hard failure, §6.7) rather than a hard dependency — the risk is bounded to "one file has worse retrieval," not "ingestion halts." + +### R-T2: LLM structured-output schema drift + +**Risk**: Claude occasionally returns JSON that's syntactically valid but doesn't match the expected Pydantic schema shape (missing a required field, wrong enum value) — more likely under prompt changes or model version upgrades. + +**Impact**: without the retry-with-reminder pattern (§8.5), this would surface as a confusing 500 error to the end user with no clear cause. + +**Mitigation**: already designed — one retry with an explicit schema-reminder system message (§8.5), then a clean `AIServiceError` surfaced as `503 ai_unavailable` rather than a stack trace. **Residual risk**: a *systematic* schema mismatch (e.g., after a Claude model version bump changes default JSON formatting behavior) would fail the same way on every retry — mitigated by NFR-OBS-2's per-call logging making this pattern visible quickly (a spike in retry_count > 0 across all calls using one prompt version is a clear signal), not eliminated outright. + +### R-T3: Vector store / relational store drift + +**Risk**: a `knowledge_chunks` Postgres row exists without a corresponding Chroma vector (or vice versa) — e.g., an ingestion job crashes between the Postgres insert and the Chroma upsert. + +**Impact**: a citation could resolve to a `knowledge_chunks` row whose vector no longer contributes to retrieval (silent, low-severity — the chunk just stops being retrievable) or, worse, a Chroma vector with no metadata backing it (a citation that can't resolve `file_path`/`line_range`). + +**Mitigation**: §6.3's pipeline order writes to Chroma *before* the Postgres row (`Q → T` in the ingestion flowchart) — a crash between those two steps leaves an orphaned vector with no chunk row, which is the safer failure mode (the citation-resolution path in §7.6 simply won't find a match for a `chroma_id` with no Postgres row, and the answer proceeds without that citation, rather than resolving to wrong metadata). **Not fully solved**: a periodic reconciliation job (query Chroma for a project's `chroma_id`s, diff against `knowledge_chunks`, delete orphans) is not built at MVP — flagged here as a known gap rather than silently omitted. + +### R-T4: Single-process ChromaDB as a scaling ceiling + +Already covered in depth in §15.3 — restated here because it's a real risk, not just a future-nicety: embedded ChromaDB caps the system at one API replica until the `HttpClient` swap is made. If growth happens faster than expected, this is the first wall the architecture hits. + +--- + +## 18.3 Product & Quality Risks + +### R-P1: Hallucination despite citation enforcement + +**Risk**: the citation-enforcement mechanism (§7.6, ADR-R3) guarantees a cited claim traces back to a chunk that was actually retrieved — it does **not** guarantee the claim is an accurate summary of that chunk's content. Claude could cite a real file/line and still mischaracterize what the code does. + +**Impact**: a developer trusts a subtly wrong answer because it "looks" grounded (has a citation). + +**Mitigation**: partial, by design — citations let a user *verify* the claim in one click (jump to file:line), which is the actual mitigation the requirements doc calls for (§0: "don't let it just narrate confidently with no pointer back to ground truth"). This shifts trust-verification cost to the user rather than eliminating the underlying hallucination risk, which no current RAG technique fully solves. Thumbs-down feedback (FR-8.8) is the longer-term signal for detecting a pattern of this happening. + +### R-P2: Duplicate/contradictory requirement detection threshold tuning + +**Risk**: the fixed 0.92 cosine similarity threshold (FR-2.5) for flagging duplicate work packages is a single number chosen without production tuning data — it could be too strict (misses genuine near-duplicates phrased very differently) or too loose (flags legitimately distinct requirements that happen to share vocabulary). + +**Impact**: either noisy false-positive duplicate flags (annoying, erodes trust in the flag) or missed duplicates (the problem FR-2.5 exists to solve, silently not solved). + +**Mitigation**: the threshold is a config value, not hardcoded in logic (should be sourced from `Settings`, matching NFR-MAINT-4's philosophy) — tunable without a code change once real usage data shows which direction it's miscalibrated. + +### R-P3: Sprint bin-packing greedy algorithm producing suboptimal plans + +**Risk**: greedy bin-packing (FR-4.2) is a deliberate simplicity choice over ILP (§0: "greedy is enough for a demo and far less debugging risk") — it can produce a materially worse allocation than an optimal solver would, especially with many interdependent stories. + +**Impact**: sprint plans that look obviously sub-optimal to a project manager, undermining confidence in the tool. + +**Mitigation**: the Claude sanity pass (FR-4.5) exists specifically to catch and flag the cases where greedy allocation produces something nonsensical (overcommitment, unresolved dependency ordering) — it's a check on the algorithm's output, not a replacement for a better algorithm, which is an accepted trade-off for delivery speed. + +--- + +## 18.4 Cost & Operational Risks + +### R-C1: LLM cost runaway during development iteration + +**Risk**: repeated manual testing of story generation, risk assessment, or requirements extraction during active development racks up real Claude API cost, especially before caching (§8.4) is wired in. + +**Mitigation**: the Redis response cache (§8.4.2) is specifically designed for this — re-running the same requirements doc during dev iteration hits cache, not the API, for the idempotency-safe operations. **Residual risk**: story generation, sprint planning, and risk assessment are deliberately *not* cached (§8.4.2 — caching them would defeat the versioning guarantee), so repeated dev testing of those specific flows has no cost mitigation beyond developer discipline. This is a conscious trade-off between correctness (§1.5's idempotency-as-versioning rule) and dev-time cost — correctness wins. + +### R-C2: Rate-limit cascades under concurrent load + +**Risk**: multiple users or background ingestion jobs hitting the Claude API simultaneously could trigger provider-side rate limiting, degrading the whole platform's AI-dependent features at once. + +**Mitigation**: already designed — the per-project semaphore (§4.7.2, max 10 in-flight per project) and the retry/backoff policy honoring `retry-after` headers (§8.5) are the direct countermeasures. This caps blast radius to "this project's AI features are briefly delayed," not "the whole platform's Claude integration is rate-limited." + +### R-C3: Single-vendor LLM dependency + +**Risk**: exclusive reliance on Anthropic's Claude API (§11.2) means any sustained Anthropic-side outage degrades every AI-dependent feature simultaneously, with no fallback provider. + +**Mitigation**: NFR-AVAIL-2's graceful degradation (non-AI endpoints stay up, AI endpoints return `503 ai_unavailable`) bounds the *severity* of an outage — the platform doesn't go fully down — but does not solve the *availability* of the AI features themselves during that window. Multi-LLM fallback is a named future item (§15.7), not built at MVP; accepted because building and maintaining prompt-compatibility across two LLM vendors is a real ongoing cost that isn't justified until Claude-specific outage frequency actually warrants it. + +--- + +## 18.5 Delivery Risks (Hackathon-Specific) + +### R-D1: Scope exceeds hackathon timeline despite phased roadmap + +**Risk**: even with the phased roadmap (§16) sequencing work by priority, a four-module system (Knowledge Hub, Stories/Sprints, Meetings, Risk) plus the shared RAG foundation is a lot to build correctly in 24–48 hours. + +**Mitigation**: the roadmap's phase structure (§16.2–16.6) is explicitly ordered so that cutting scope late means dropping Phase 4 stretch goals or trimming Phase 3, never abandoning the Phase 0/1 foundation the demo's strongest moment depends on. The requirements doc's own priority order (§10) is followed exactly for this reason. + +### R-D2: Team unfamiliarity with tree-sitter / RAG plumbing under time pressure + +**Risk**: syntax-aware chunking and a full RAG pipeline are non-trivial to get right on the first attempt, and debugging retrieval quality issues under time pressure is easy to get stuck on. + +**Mitigation**: §0 of the requirements doc explicitly flags this same risk for heavier frameworks ("LlamaIndex/LangChain abstractions can cost you more time than they save") and this architecture follows that guidance by hand-rolling the RAG pipeline (§7.1) rather than adopting a framework whose abstractions the team would need to learn *and* debug simultaneously. + +--- + +## 18.6 Closing Note + +This architecture is built for a specific, stated constraint: a Python team building ProjectOS in a hackathon timeframe, with an explicit and credible path to production hardening afterward. Every deferred item in Section 15, every ADR trade-off in §18.1, and every risk in this section is deferred or accepted *with a named trigger condition* — the goal throughout this document has been that nothing here is deferred by accident. A reviewer picking up this system post-hackathon should be able to read Sections 15 and 18 alone and know exactly what to harden first, why it wasn't done initially, and what signal indicates it's time.