Repository navigation
feat: #96 Confluence + Jira RAG knowledge platform (onboarding + knowledge chat) - #104
Yogesh-Kisslay wants to merge 36 commits into
Conversation
- 7-day onboarding plan generator (role-specific, grounded in company docs) - Per-day content generation with block renderer (diagrams, code, tables) - Final quiz with attempt persistence (onboarding_quiz_attempts table) - Share toggle + public read-only view - Delete plans from list page - Separate LLM pipeline (StructuredOnboardingDayContent, prompts/onboarding.py) - Inline link rendering in BlockRenderer ([label](url) → <a> tags) - 3 Alembic migrations: onboarding_plans, onboarding_days, quiz_attempts - Full test coverage for all new onboarding modules (979 tests, 99.14% coverage)
- Rewrote OnboardingQuizPanel with view state machine (loading/not_generated/generating/ready/taking/submitted) — fixes Try Again resetting to empty state and Generate Quiz showing on return after navigation - Split quiz endpoint into GET (fetch cached, 404 if not generated) + POST /generate — enables auto-fetch on mount - Fixed Gemini plan/quiz generation by adding explicit OUTPUT FORMAT to system prompts - Added role-based doc filtering so only relevant docs are passed per role - Added force-regenerate query param to day content endpoint - Added LlmBar to onboarding index page
…→ fresh)
Build onboarding on top of a company's own Confluence, replacing the
filesystem prototype docs with per-company ingested docs.
Backend:
- Confluence OAuth 2.0 (3LO): company-level encrypted tokens, auto-refresh,
connect/callback/status endpoints
- Ingestion funnel: list spaces (personal spaces filtered) → cheap keyword
filter → LLM relevance classification → confirm (add-only). Async via
BackgroundTasks with a progress endpoint
- Uses the Confluence search API (content/search with expand) since the v1
/space and /content/{id} endpoints are retired and v2 needs granular scopes
- Generation reads approved+active onboarding_docs by company + role,
replacing filesystem _load_docs
- Freshness: webhook receiver (created/updated/removed) with shared-secret auth
- Correctness: reconcile_company + gap detector
- New tables: companies, confluence_connections, onboarding_docs, ingestion_jobs
- Removed dead legacy get_final_quiz
Frontend:
- ConfluenceSetup: connect, space picker (smart defaults), live ingest
progress, candidate review/confirm, docs-ready count, gap nudge, reconcile
Docs: ONBOARDING_DESIGN.md. Full suite: 1135 tests, 99% coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pivot toward a Confluence-wide RAG knowledge base (ingest everything, retrieve per query). This lands the foundation; the ingest pipeline rewrite follows. - Data layer: document_pages + document_chunks (pgvector vector(768) + HNSW); ingestion_jobs gains total_chunks / embedded_chunks - Chunker: chunk_text with overlap - Embeddings: Gemini text-embedding-004 service, batched with backoff (system key: GEMINI_EMBEDDING_API_KEY) - Confluence client: read spaces + page bodies via the search API; filter personal (~) spaces - Design doc rewritten as a forward RAG plan Full suite: 1148 tests, 99% coverage.
Replace the relevance-funnel with a whole-Confluence RAG pipeline. - Ingest: begin_ingest fetches every page from every space → upsert document_pages → chunk → embed → document_chunks. Resumable background job; page + chunk progress - Retrieval: services/retrieval.py — pgvector cosine nearest-k - Generation: onboarding _load_docs retrieves top-k chunks by role/topic - Webhooks + reconcile reshaped onto document_pages/document_chunks - Removed the old funnel (picker, keyword filter, LLM relevance classify, candidates/confirm, gaps, role→tag mapping) - Dropped onboarding_docs + OnboardingDoc (migration e2f3a4b5c6d7) Full suite: 1108 tests, 99% coverage.
- /knowledge/query: retrieve top-k chunks → LLM answer grounded in the docs, with deduped citations (prompts/knowledge, llm.answer_from_context, services/knowledge, route + schema) - UI: rewrote ConfluenceSetup for the ingest-all flow — connect, one-click "Ingest all docs", live page + chunk progress, indexed counts, reconcile; removed the space picker / candidates / gaps - UI: KnowledgeAsk box (ask-anything over ingested docs, shows sources) Full suite: 1119 tests, 99% coverage.
… key) Ingestion and query embeddings use the connecting user's saved Gemini key from the DB (resolve_embedding_key), falling back to an optional server-side GEMINI_EMBEDDING_API_KEY. No separate key configuration needed. - llm.get_user_gemini_key: fetch the user's Gemini key regardless of active provider - confluence.resolve_embedding_key: connecting user's key → system fallback - thread the key through ingest, webhook re-embed, and retrieval - begin_ingest 503s asking for a Gemini key only when none is available Full suite: 1124 tests, 99% coverage.
Segregate the onboarding/RAG feature into its own `onboarding/` package and harden the ingest pipeline end to end. Embeddings (local, no API key): - Replace Gemini embeddings with fastembed BAAI/bge-small-en-v1.5 (384-dim), running locally on CPU with no key and no quota. Migration shrinks the document_chunks vector column 768 -> 384 and rebuilds the HNSW index. - Remove the Gemini embedding-key plumbing (resolve_embedding_key, the begin_ingest 503, get_user_gemini_key) since ingestion needs no key. - scripts/warm_embeddings.py pre-downloads the model (wired into setup.sh) so the first ingest doesn't wait; requirements + .coveragerc updated. Ingest pipeline (staged, batched, resilient): - Three visible phases: reading spaces -> scanning pages -> embedding chunks (ingestion_jobs gains phase + space counters). - Embed chunks in cross-page batches (~256) instead of one call per page. - Fault isolation: a failing space / page / embed-batch is skipped and retried next run rather than aborting the whole job; only whole-run problems fail it. Read calls retry transient network errors / 5xx with backoff. - get_status reports the running job so a page refresh resumes the live view. UI: - Staged progress bars with per-phase status and a live elapsed timer that survives refresh; a distinct "connection expired -> Reconnect" state; ingest 409 re-fetches status. Also drop the unused confluence_connections.space_keys column. Full suite green at 99% coverage; UI typecheck clean.
…G polish
Turn the "ask about your docs" box into a full ChatGPT-style Knowledge section
and sharpen the onboarding RAG.
Knowledge chat:
- Dedicated /knowledge page with its own sidebar entry; collapses the app chrome
into a two-pane chat (history + new chat on the left, thread + composer on the
right), Openship brand, back-to-Onboarding button, and the provider switcher.
- Persistent chats: knowledge_chats + knowledge_messages tables; per-user,
company-scoped; multi-turn (recent history fed back to the model).
- Structured answers: replies are typed content blocks (headings, code, tables,
mermaid diagrams) rendered by the same BlockRenderer as chapters/day content.
- Smart + conversational: greets naturally, answers from the docs with inline
links when covered, and otherwise says it's not in the docs and answers from
general knowledge - never fabricating company facts. A used_docs flag gates the
source chips so greetings/general answers don't show irrelevant sources.
Onboarding RAG:
- Tune retrieval: focused per-day queries (role + topic + task, no filler), broad
plan/quiz sweep, quiz grounded in the actual day topics; tuned k.
- Reconcile now shows a clear "Syncing..." status panel instead of a silent spinner.
Also: fix mermaid rendering for {} inside [] node labels (shared block-renderer,
so onboarding + courses both benefit).
Full suite green at 99% coverage; UI typecheck clean.
Reconcile + progress: - Reconcile is now a tracked background job (ingestion_jobs gains a `kind` column: ingest | reconcile) with staged progress, reusing the ingest UI. - get_status surfaces last_result so a job that finished while away announces its outcome once; no-op syncs say "Already up to date". Review hardening (adversarial pass over onboarding + knowledge): - FIX token-expiry timezone bug: token_expires_at was stored aware-UTC into a naive column and shifted by the DB session tz, so expiry math thought a dead token was valid for hours -> 401 surfaced as "Failed to list spaces". Store naive UTC so it round-trips regardless of session tz. - Auth failures fail loudly: 401/403 -> "session expired; reconnect", mark the connection error (Reconnect UI), and a mid-run 401 fails the job instead of silently skipping every space (which for reconcile would deactivate the KB). - Reap orphaned 'running' jobs on startup so a mid-run restart can't wedge it. - Block personal/free email domains from connecting (they'd pool unrelated users into one shared company and leak docs). - Webhook handler never raises so a failure can't 500-storm Atlassian retries. - FIX critical crash: BookOpen unimported in the Knowledge not-ready gate (root tsconfig is references-only so tsc --noEmit never checked it; use tsc -b). - Knowledge chat: guard cross-chat send/switch races, clear composer on switch, stop poisoning history with a "(no answer)" sentinel. Full suite green: 1176 passed, 99% coverage.
Extend the Confluence RAG platform into a full Confluence+Jira knowledge base and harden multi-tenancy. Onboarding stays Confluence-only; chat spans both. Jira + ingestion: - Atlassian OAuth grants Confluence + Jira in one app; Jira enhanced JQL search (/rest/api/3/search/jql, nextPageToken paging) - structured assignee/reporter/status columns + JSONB meta on document_pages; Confluence author/last_editor in meta and embedded into chunk text - source column throughout; reuse of the pages/chunks tables Retrieval + people analytics: - hybrid retrieval (semantic pgvector + word-boundary lexical), full-name phrase matching, adaptive semantic ratio, lowercase-name detection - deterministic count / list / leaderboard answers from the DB (exact, not top-k), planner LLM to classify person questions - strict link scrubbing so the model can't emit hallucinated URLs Freshness: - Confluence + Jira webhooks, shared-secret auth, BackgroundTasks instant ack; reconcile as durability backstop Multi-tenancy + connector identity: - services/company.py: company keyed by email domain (corporate) or full email (generic/personal solo org); users.company_id FK set at signup, shown read-only in Settings -> Account - connect requires the Atlassian account email == Openship login email (read:me scope), fail-closed UI: Connections page (moved out of Onboarding), citation chips with source badges + safe links, company field in account settings. Docs: KNOWLEDGE_PLATFORM_DESIGN.md (renamed). Full suite: 1321 passed, 99%.
…fix + docs Ingestion (onboarding/services/confluence.py + tests): - honor Atlassian Retry-After on 429/503 (up to 10 attempts, 5-min cap) so throttling no longer drops whole projects - refresh the OAuth token before each project (5-min headroom) so a multi-hour run never dies mid-read on token expiry - stream one project/space at a time (bounded memory) + chunk inside the embed loop; pre-count chunks for an accurate progress bar; lift the per-project pagination cap. Enables a full first-time ingest of a 133k-issue tenant. Courses UI build fix: - add missing hljs import in syllabi/detail.tsx (fixes a runtime error) - remove unused vars/imports in sampleApp + syllabi so the tsc build passes Deploy + docs: - add Dockerfile + .dockerignore (bakes the local embedding model into the image) - rewrite KNOWLEDGE_PLATFORM_DESIGN.md, add LARGE_INGESTION_CHALLENGE.md, update README
- ContentBlock: coerce out-of-vocabulary block types from the LLM (e.g. Gemini's list_item) to a valid type instead of failing the whole structured answer; valid types untouched. Fixes a real crash in knowledge/onboarding/course output. - generation: truncated response -> specific 422 "try again"; transient error -> one-shot retry. Brings knowledge + onboarding to parity with course generation. - tests: block-type coercion + retry/truncation (unit + end-to-end).
…oarding branch Brings the adaptive-learning system (BKT + forgetting curve + bandit + weekly-quiz remediation) from main into feature/issue-96-onboarding. This branch had forked before PR #101 merged, so it lacked the adaptive weekly-quiz composition (current-week + weak-from-previous + forgotten topics) and the week_remediation_topics table — which is why the weekly quiz only produced the current week's questions. Conflict resolved: both branches independently used Alembic revision id "y3z4a5b6c7d8". The adaptive migration (schema-independent of onboarding) was re-chained to a unique id "adaptive_topic_canon" onto the onboarding head, so the migration history is a single linear chain. Full suite: 1379 passed, 99%. Single alembic head (onb_user_company -> adaptive_topic_canon).
Heuristic check 3 (topic keywords present) stripped ALL punctuation from
keyword tokens, so "Express.js" became "expressjs" and never matched the
chapter prose "express.js" — failing generation for such chapters.
Keep internal dots when extracting keywords (still strip surrounding
punctuation and leading/trailing dots), and match the dot optionally so the
keyword also matches an un-dotted spelling ("ExpressJS"). Adds tests.
Onboarding/knowledge day + plan generation ground only in Confluence, but the pgvector HNSW index returns the GLOBAL nearest chunks and applies the source filter AFTER — so a Confluence-sparse topic on a Jira-heavy base (e.g. RBAC / audit) could be filtered down to ZERO chunks even though relevant docs exist, surfacing "No onboarding documents". When a `sources` filter is set, disable the index (SET LOCAL enable_indexscan/bitmapscan = OFF) so Postgres ranks by distance within the filtered rows and always returns that source's true nearest; the filtered subset is small so the exact scan is fast. Unfiltered chat keeps the index. Also add INFO logging across retrieval, onboarding, and knowledge (retrieved counts, grounding size, generation steps) so these flows are observable in the server terminal like courses already are.
Onboarding + knowledge logs never reached the terminal while course logs did. Root cause: run_startup_migrations() runs Alembic in-process, and alembic/env.py called fileConfig(config_file) which defaults to disable_existing_loggers=True. That disabled every logger already created at import time — including the onboarding.* loggers (their modules are imported at startup when routes register). Course loggers survived only because those modules are imported lazily on first generation, after migrations ran. Pass disable_existing_loggers=False so startup migrations leave app loggers enabled, and wire "onboarding" into main.py's logger loop so it gets an INFO handler exactly like services/controllers.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis PR adds a company-scoped Atlassian knowledge platform with Confluence/Jira ingestion, local embeddings and retrieval, onboarding plan/day/quiz generation, persisted knowledge chat, and supporting migrations, scripts, docs, and UI surfaces. It also includes a separate syllabi/sample-app UI cleanup. ChangesKnowledge Platform rollout
Sequence Diagram(s)sequenceDiagram
participant UI
participant OnboardingRoute
participant OnboardingService
participant Retrieval
participant LLM
UI->>OnboardingRoute: POST /onboarding/generate
OnboardingRoute->>OnboardingService: generate_plan(role, company)
OnboardingService->>Retrieval: retrieve_context(company_id, query)
OnboardingService->>LLM: generate_onboarding_plan(docs_text)
LLM-->>OnboardingService: 7-day plan
OnboardingService-->>UI: plan + days
sequenceDiagram
participant WebhookRoute
participant ConfluenceService
participant DocumentPage
participant DocumentChunk
WebhookRoute->>ConfluenceService: verify secret + handle_webhook
ConfluenceService->>DocumentPage: fetch/upsert page
ConfluenceService->>DocumentChunk: re-chunk and embed content
ChangesSyllabi and sample app UI cleanup
Estimated code review effort: 5 (Critical) | ~150 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 14
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🟡 Minor comments (8)
ui/src/app/plugins/syllabi/block-renderer.tsx-27-27 (1)
27-27: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winMarkdown link parsing truncates URLs containing
).The URL segment
https?:\/\/[^)]+stops at the first closing parenthesis, so links whose URL contains a literal)(common in Confluence/Jira/Wiki page URLs) are cut short — thehrefbecomes a broken URL and the trailing)...leaks into the text. This path renders Atlassian-sourced knowledge/onboarding content, so such URLs are realistic. Consider balancing a single level of parens or falling back to raw text when the closing)doesn't terminate the token.Also applies to: 39-41
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@ui/src/app/plugins/syllabi/block-renderer.tsx` at line 27, The Markdown link matcher in block-renderer.tsx is too narrow because the pattern used by the rendering logic stops URLs at the first closing parenthesis, which breaks links containing literal parentheses. Update the link parsing in the block-rendering flow (the regex assigned to pattern and any related handling around the markdown tokenization) so it can safely accept balanced parentheses or otherwise avoid splitting valid Atlassian URLs, and fall back to treating the token as plain text when the closing parenthesis is not the actual end of the link.alembic/versions/e2f3a4b5c6d7_drop_onboarding_docs.py-20-22 (1)
20-22: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winAvoid dropping
onboarding_docswithout a data migration
alembic/versions/e2f3a4b5c6d7_drop_onboarding_docs.py:20-22drops the legacy table outright, anddowngrade()only recreates an empty schema. If any deployed database still hasonboarding_docsrows, that data is lost on upgrade; add a backfill/export step or confirm the table is empty before removing it.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@alembic/versions/e2f3a4b5c6d7_drop_onboarding_docs.py` around lines 20 - 22, The upgrade in drop_onboarding_docs() is deleting onboarding_docs outright without preserving existing rows, so update this migration to include a data migration or safety check before calling op.drop_table; use the existing upgrade() and downgrade() flow to backfill/export legacy data or verify the table is empty, and keep the table removal only after the data path is handled.scripts/test_webhook.sh-40-46 (1)
40-46: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick winUse
mktempinstead of a PID-based temp file name.
/tmp/_wh_body.$$is predictable; a local attacker could pre-create/symlink it (CWE-377). Trivial to harden even for a dev-only script.🔒 Proposed fix
post() { # post <label> <url> <json> ; prints the HTTP status local label="$1" url="$2" body="$3" code - code="$(curl -s -o /tmp/_wh_body.$$ -w '%{http_code}' -X POST "$url" \ + local tmpfile + tmpfile="$(mktemp)" + code="$(curl -s -o "$tmpfile" -w '%{http_code}' -X POST "$url" \ -H 'Content-Type: application/json' -d "$body" || echo "000")" - printf ' %-42s -> %s %s\n' "$label" "$code" "$(cat /tmp/_wh_body.$$ 2>/dev/null)" - rm -f "/tmp/_wh_body.$$" + printf ' %-42s -> %s %s\n' "$label" "$code" "$(cat "$tmpfile" 2>/dev/null)" + rm -f "$tmpfile" }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/test_webhook.sh` around lines 40 - 46, In the `post` helper in `test_webhook.sh`, replace the predictable `/tmp/_wh_body.$$` temp file with a `mktemp`-created path and use that variable consistently for both the `curl` output and the `cat`/cleanup steps. Keep the helper’s behavior the same, but ensure the temporary body file is created securely and removed afterward.Source: Linters/SAST tools
scripts/test_webhook.sh-50-71 (1)
50-71: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick winSend webhook secrets in a header instead of the URL. The routes already accept
X-Webhook-Secret, so these test calls can avoid leaking secrets through logs, history, and proxies.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/test_webhook.sh` around lines 50 - 71, The webhook test calls are passing secrets in the query string instead of the supported X-Webhook-Secret header, which can leak them through logs and history. Update the post invocations in test_webhook.sh for the Jira and Confluence cases (and the wrong-secret auth check) to send the secret via the header expected by the webhook routes, keeping the request bodies and endpoints the same.ui/src/app/plugins/onboarding/detail.tsx-332-337 (1)
332-337: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winQuiz attempt persistence failure is silent.
If
postRequestthrows orsuccessis false, the catch does nothing — the user still sees a "submitted"/pass-fail screen (already computed locally) even though the attempt was never recorded server-side.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@ui/src/app/plugins/onboarding/detail.tsx` around lines 332 - 337, The quiz attempt save in the onboarding detail flow is failing silently because the `postRequest` result is only handled on success and the `catch` block ignores errors. Update the attempt submission logic in `detail.tsx` around the quiz submit handler so failures from `postRequest` or a false `success` are surfaced to the user and the UI does not imply the attempt was persisted. Use the `setAttempts` and `setSubmitting` flow to keep the local state consistent, and ensure the submit path in the quiz attempt handler clearly distinguishes between local pass/fail computation and server persistence.ui/src/app/plugins/onboarding/index.tsx-154-165 (1)
154-165: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winSilent failure on plan delete.
Same pattern as in
knowledge/index.tsx'sdeleteChat— a failed delete gives no user feedback; the dialog just closes as if it succeeded.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@ui/src/app/plugins/onboarding/index.tsx` around lines 154 - 165, The delete flow in handleDeleteConfirm is swallowing errors, so a failed plan deletion closes the dialog without any user feedback. Update the catch path in handleDeleteConfirm (and any related delete state handling in ui/src/app/plugins/onboarding/index.tsx) to surface a visible error message or notification instead of leaving the UI unchanged. Follow the same pattern used by knowledge/index.tsx’s deleteChat so users can tell the delete request failed.ui/src/app/plugins/knowledge/index.tsx-95-99 (1)
95-99: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winSilent failure on chat delete.
If
api.deletethrows, the function justreturns with no user feedback — the chat stays in the list but the user gets no indication the delete failed.🔔 Suggested fix
- try { await api.delete(`/py/knowledge/chats/${id}`) } catch { return } + try { await api.delete(`/py/knowledge/chats/${id}`) } catch { + toast({ variant: "destructive", title: "Error", description: "Failed to delete chat." }) + return + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@ui/src/app/plugins/knowledge/index.tsx` around lines 95 - 99, The deleteChat flow currently swallows api.delete failures and exits silently, leaving the UI unchanged with no feedback. Update deleteChat to handle the catch path explicitly in the knowledge plugin, using the existing api.delete call and surrounding state updates to surface a user-visible error or retry prompt instead of immediately returning. Keep the successful path unchanged so chats are only removed from setChats and activeId/newChat are updated after a confirmed delete.ui/src/app/plugins/onboarding/detail.tsx-854-863 (1)
854-863: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winSilent failure on share toggle.
catch { /* leave unchanged */ }gives no feedback if enabling/disabling the public link fails; the button just reverts state with no explanation.🔔 Suggested fix
- try { - await api.patch(`/py/onboarding/${plan.id}/share?enable=${next}`) - setShareEnabled(next) - } catch { /* leave unchanged */ } - finally { setTogglingShare(false) } + try { + await api.patch(`/py/onboarding/${plan.id}/share?enable=${next}`) + setShareEnabled(next) + } catch { + toast({ variant: "destructive", title: "Error", description: "Failed to update sharing." }) + } + finally { setTogglingShare(false) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@ui/src/app/plugins/onboarding/detail.tsx` around lines 854 - 863, The share toggle in handleToggleShare is silently swallowing API failures, so users get no feedback when /py/onboarding/${plan.id}/share fails. Update the catch path in handleToggleShare to surface the error through the existing UI pattern used in this component (for example, a toast or inline error state) and keep the shareEnabled value unchanged unless the request succeeds; use the same plan, setTogglingShare, and setShareEnabled flow to locate the fix.
🧹 Nitpick comments (23)
ui/src/app/plugins/syllabi/detail.tsx (1)
2-2: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winUse
highlight.js/lib/commonorhighlight.js/lib/corehere
import hljs from "highlight.js"pulls in all supported languages and bloats the client bundle. If you only need the usual common set, switch tohighlight.js/lib/common; if you only highlight a few known languages, usehighlight.js/lib/coreand register just those grammars.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@ui/src/app/plugins/syllabi/detail.tsx` at line 2, The import in the syllabi detail component is pulling in the full highlight.js package and unnecessarily increasing the client bundle. Update the import in detail.tsx from the top-level highlight.js entry to either highlight.js/lib/common for the standard language set or highlight.js/lib/core if only a few grammars are needed, and register only the languages used by the highlighting logic in the relevant component/module.KNOWLEDGE_PLATFORM_DESIGN.md (1)
27-33: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAdd a language hint to the fenced diagram block.
Static analysis flags this as an unlabeled fenced code block (MD040).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@KNOWLEDGE_PLATFORM_DESIGN.md` around lines 27 - 33, The fenced diagram block is unlabeled and triggers MD040; update the markdown in the knowledge design document so the diagram fence includes an appropriate language hint, using the diagram block itself as the reference point rather than changing its content.Source: Linters/SAST tools
services/company.py (2)
57-72: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winReturn type can theoretically be
Nonedespite the-> Companyannotation.If the IntegrityError branch's re-query (Line 70) doesn't find a row (e.g., another session's transaction rolled back after the constraint conflict, or an isolation-level edge case),
get_or_create_companyreturnsNonewhile typed asCompany, and callers (e.g.,services/user.py'screate_user, which dereferencescompany.id) would hit anAttributeErrorrather than a clear failure.Consider raising an explicit error if the refetch still comes back empty, to fail loudly instead of propagating
Nonedownstream.♻️ Suggested fix
except IntegrityError: # concurrent create — fetch the winner session.rollback() - return session.exec(select(Company).where(Company.domain == key)).first() + winner = session.exec(select(Company).where(Company.domain == key)).first() + if winner is None: + raise RuntimeError(f"Company creation race for key={key!r} did not resolve.") + return winner🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@services/company.py` around lines 57 - 72, The get_or_create_company flow can still return None from the IntegrityError retry path even though it is annotated to return Company, which can break callers like create_user when they access company.id. Update get_or_create_company to use the refetch result after session.rollback() and, if no Company is found, raise an explicit error instead of returning None; keep the change localized around the IntegrityError branch and the final return in get_or_create_company.
20-42: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueGeneric-email allowlist is a maintenance surface.
_GENERIC_EMAIL_DOMAINSis a hardcoded set; providers not listed (e.g., regional webmail likeqq.com,163.com,fastmail.com) would be incorrectly treated as corporate domains, pooling unrelated users into one company. Not a bug introduced by this PR, but worth tracking as the list will need ongoing curation.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@services/company.py` around lines 20 - 42, The hardcoded _GENERIC_EMAIL_DOMAINS allowlist in services.company.Company is a maintenance risk because missing providers will be misclassified as corporate domains. Update the domain handling in Company to use a centralized, easily extensible source for generic mail providers, and add the missing common webmail domains (for example regional and provider-hosted mail services) so the list stays accurate over time.README.md (1)
224-229: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winDocument
EMBEDDING_DIMENSIONSalongsideEMBEDDING_MODEL.
config.pyrequiresEMBEDDING_DIMENSIONSto stay in sync withEMBEDDING_MODELand thedocument_chunkspgvector column width, but this table doesn't mention the variable at all. Add a row so operators overridingEMBEDDING_MODELknow they may also need to setEMBEDDING_DIMENSIONS(and re-run the relevant migration).📝 Proposed addition
| `EMBEDDING_MODEL` | No | Local embedding model (default: `BAAI/bge-small-en-v1.5`, 384-dim) | +| `EMBEDDING_DIMENSIONS` | No | Must match the embedding model's output width and the `document_chunks` pgvector column (default: `384`) |🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@README.md` around lines 224 - 229, Update the configuration table in README to include EMBEDDING_DIMENSIONS alongside EMBEDDING_MODEL, since config.py depends on it matching the chosen model and the document_chunks pgvector width. Add a clear row near EMBEDDING_MODEL in the docs so operators know that overriding EMBEDDING_MODEL may also require setting EMBEDDING_DIMENSIONS and re-running the related migration. Reference the existing EMBEDDING_MODEL entry and the surrounding environment-variable table to place the new documentation correctly.config.py (2)
111-124: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winOverriding
ATLASSIAN_OAUTH_SCOPEScan silently break refresh.If an operator sets
ATLASSIAN_OAUTH_SCOPESwithout includingoffline_access, no refresh token will be issued, and_refresh_access_token(onboarding/services/confluence.py) will fail once the initial access token expires (~1 hour). Consider validating that overridden scopes still containoffline_access, or warn/log at startup if it's missing.💡 Proposed guard
ATLASSIAN_OAUTH_SCOPES = _strip_opt("ATLASSIAN_OAUTH_SCOPES") or _DEFAULT_ATLASSIAN_SCOPES +if "offline_access" not in ATLASSIAN_OAUTH_SCOPES.split(): + raise ValueError( + "ATLASSIAN_OAUTH_SCOPES must include 'offline_access' or refresh tokens won't be issued." + )🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@config.py` around lines 111 - 124, The ATLASSIAN_OAUTH_SCOPES override in config.py can remove offline_access and silently break token refresh later in _refresh_access_token. Add startup validation where ATLASSIAN_OAUTH_SCOPES is derived from _DEFAULT_ATLASSIAN_SCOPES to ensure any overridden value still includes offline_access, and if it does not, either reject it with a clear error or emit a prominent warning/log before the connector starts.
136-141: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winAdd a startup guard for embedding width drift
EMBEDDING_MODELandEMBEDDING_DIMENSIONSare only documented as needing to match thedocument_chunkscolumn; no runtime check ties the model’s output width to the configured dimension. A mismatch will fail later during inserts or similarity search, so the embedding path should assert the vector length up front.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@config.py` around lines 136 - 141, Add a runtime startup guard for embedding width drift by checking that the configured embedding model’s output size matches EMBEDDING_DIMENSIONS before any inserts or search use it. Use the existing EMBEDDING_MODEL and EMBEDDING_DIMENSIONS settings in config.py and place the assertion where the embedding path is initialized so the app fails fast with a clear error if the vector length does not match the document_chunks schema.alembic/versions/a5b6c7d8e9f0_add_onboarding_quiz_attempt.py (1)
24-35: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winConsider indexing
plan_id(and possiblyuser_id).
plan_idis a FK withondelete="CASCADE"but Postgres doesn't auto-index FK columns. Without an index, cascading deletes fromonboarding_plansand lookups (e.g. attempt history) will scan sequentially as the table grows.♻️ Proposed fix
op.create_table( "onboarding_quiz_attempts", sa.Column("id", sa.Integer(), nullable=False), sa.Column("plan_id", sa.Integer(), nullable=False), sa.Column("user_id", sa.String(), nullable=False), sa.Column("score", sa.Integer(), nullable=False), sa.Column("correct", sa.Integer(), nullable=False), sa.Column("total", sa.Integer(), nullable=False), sa.Column("answers", sa.Text(), nullable=True), sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=True), sa.ForeignKeyConstraint(["plan_id"], ["onboarding_plans.id"], ondelete="CASCADE"), sa.PrimaryKeyConstraint("id"), ) + op.create_index("ix_onboarding_quiz_attempts_plan_id", "onboarding_quiz_attempts", ["plan_id"]) + op.create_index("ix_onboarding_quiz_attempts_user_id", "onboarding_quiz_attempts", ["user_id"])Remember to drop these indexes in
downgrade()beforedrop_table.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@alembic/versions/a5b6c7d8e9f0_add_onboarding_quiz_attempt.py` around lines 24 - 35, Add indexes for the onboarding_quiz_attempts foreign key columns, especially plan_id and optionally user_id, in the migration that defines onboarding_quiz_attempts. Update the upgrade logic in a5b6c7d8e9f0_add_onboarding_quiz_attempt.py to create these indexes alongside the table, and make sure downgrade drops the indexes before drop_table. Use the onboarding_quiz_attempts table definition and the new index declarations as the main place to apply the fix.alembic/versions/d1e2f3a4b5c6_add_document_pages_and_chunks.py (1)
32-32: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
confluence_page_idnaming will become source-agnostic.Given
DocumentChunk.sourcelater distinguishesconfluence/jira(peronboarding/models/document_chunk.py), this column name will be misleading once Jira issues are also stored indocument_pages. Not a functional bug now, but worth a rename (or comment) in the source-generalization migration if not already addressed there.Also applies to: 49-51
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@alembic/versions/d1e2f3a4b5c6_add_document_pages_and_chunks.py` at line 32, The column name in the document page migration is source-specific and will be misleading once Jira pages are stored alongside Confluence pages. Update the migration around the document page table definition to use a source-agnostic name or add an explicit comment/documentation aligned with the later generalization in DocumentChunk.source and the document_pages schema.alembic/versions/onb_user_company.py (2)
57-81: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoffRow-by-row backfill loop may be slow/lock-heavy on large
userstables.Each user triggers up to 3 round-trips (lookup, optional insert, update) inside a single migration transaction. For tenants with large user tables this could hold locks for a long time during deploy. Given the PR's stated focus on scaling for "large tenants," consider batching (e.g., bulk
INSERT ... ON CONFLICTkeyed by domain, then a single bulkUPDATE ... FROM) if user counts are expected to be large.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@alembic/versions/onb_user_company.py` around lines 57 - 81, The backfill loop in the migration is doing per-user lookup/insert/update work, which can be slow and lock-heavy for large `users` tables. Refactor the `op.get_bind()` backfill logic to batch the work in `onb_user_company.py` by first collecting distinct email domains, upserting companies in bulk (or with conflict handling), and then applying a set-based `UPDATE ... FROM` to link `users.company_id` in one pass. Keep `_key_and_name` for domain/name derivation, but avoid the current row-by-row `users` iteration and repeated `bind.execute` round-trips.
45-50: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low valueCorporate company
namedefaults to the raw domain string.For corporate emails,
nameis set equal todomain(e.g."acme.com") rather than a human-friendly name. Acceptable for a backfill placeholder if it's editable later, but worth confirming the UI/company settings allow renaming, since this becomes the company's display name.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@alembic/versions/onb_user_company.py` around lines 45 - 50, The corporate-email branch in _key_and_name currently sets the company display name to the raw domain, so update the backfill/default behavior to ensure that value is either a friendlier placeholder or is explicitly editable in company settings after creation. Verify the company creation and rename flow around _key_and_name so the stored name does not become a permanent unreadable display name for corporate accounts.alembic/versions/onb_job_kind.py (1)
19-23: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winConsider a CHECK constraint for
kind.The comment states
kindis constrained to"ingest" | "reconcile", but the column accepts any string. Adding aCHECK (kind IN ('ingest','reconcile'))constraint would enforce this at the DB layer, consistent with how similar enum-like columns should be defended in depth (schema, service, and DB constraint).♻️ Optional CHECK constraint
def upgrade(): op.add_column( "ingestion_jobs", sa.Column("kind", sa.String(length=16), nullable=False, server_default="ingest"), ) + op.create_check_constraint( + "ck_ingestion_jobs_kind", "ingestion_jobs", "kind IN ('ingest', 'reconcile')" + )Based on learnings, enum-like string columns (like
skills.quiz_difficulty) should be validated consistently at the API/schema, service, and DB constraint layers with a matching CHECK constraint.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@alembic/versions/onb_job_kind.py` around lines 19 - 23, The ingestion_jobs.kind column is currently just a free-form string, so update the migration in onb_job_kind.py to add a DB-level CHECK constraint that limits values to the two supported kinds. Use the existing upgrade() migration that adds kind, and add a matching named constraint so the schema enforces the same enum-like values described by the service/API. If there is a downgrade path, remove the constraint there as well, keeping the migration consistent with the new kind column.Source: Learnings
alembic/versions/onb_knowledge_chats.py (1)
32-46: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win
rolecolumn also lacks a CHECK constraint.Per the ORM docstring,
roleis'user' | 'assistant'only, but the column accepts any string up to 16 chars. Consider adding a CHECK constraint for the same reason askind/sourcein the sibling migrations.♻️ Optional CHECK constraint
op.create_index("ix_knowledge_messages_chat_id", "knowledge_messages", ["chat_id"]) + op.create_check_constraint( + "ck_knowledge_messages_role", "knowledge_messages", "role IN ('user', 'assistant')" + )Based on learnings, enum-like string columns should be enforced with a matching DB CHECK constraint in addition to the application-level contract.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@alembic/versions/onb_knowledge_chats.py` around lines 32 - 46, The knowledge_messages table creation in the onb_knowledge_chats migration leaves role as a free-form string, which should be constrained like the sibling enum-like columns. Update the op.create_table block for knowledge_messages to add a database CHECK constraint on role in the same style used for kind/source elsewhere, limiting values to the ORM-supported user and assistant roles. Use the create_table definition and the role column as the anchors when applying the constraint.Source: Learnings
alembic/versions/onb_job_source.py (1)
19-23: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winSame enum-hardening gap as
kind: consider a CHECK constraint forsource.♻️ Optional CHECK constraint
def upgrade(): op.add_column( "ingestion_jobs", sa.Column("source", sa.String(length=32), nullable=False, server_default="confluence"), ) + op.create_check_constraint( + "ck_ingestion_jobs_source", "ingestion_jobs", "source IN ('confluence', 'jira')" + )Based on learnings, enum-like string columns should be enforced with a matching DB CHECK constraint alongside the application-level default set.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@alembic/versions/onb_job_source.py` around lines 19 - 23, The new `source` column in `upgrade()` is being added as an enum-like string without database enforcement, so mirror the hardening used for `kind` by adding a matching CHECK constraint alongside the `op.add_column` call. Update the `ingestion_jobs` migration to constrain `source` to the allowed value set (starting with the default `confluence`) so the schema enforces valid values even if the application default is bypassed.Source: Learnings
onboarding/services/retrieval.py (2)
345-360: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDocstring contradicts the actual implementation of "involved".
count_involvement's docstring states involved counts include "any of those, or named in the text", but_involved_clause(used for theinvolvedcount) explicitly does not match free-text mentions — only role columns (assignee/reporter/author/editor). This mismatch could mislead anyone relying on the docstring to interpret people-analytics results (or an LLM prompt referencing this behavior).📝 Suggested docstring fix
def count_involvement(company_id: int, name: str) -> dict: """Exact counts for a person by role across the whole knowledge base: assigned + - reported (Jira), authored (Confluence docs written/edited), and involved (any of - those, or named in the text). Uses word-boundary matching to avoid over-counting.""" + reported (Jira), authored (Confluence docs written/edited), and involved (any of + those role-holdings — NOT mere text mentions). Uses word-boundary matching to + avoid over-counting."""🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@onboarding/services/retrieval.py` around lines 345 - 360, The docstring for count_involvement is inconsistent with the actual involved logic implemented in _involved_clause. Update the count_involvement docstring to describe involved as only role-based matches (assignee, reporter, author, last editor) and remove any mention of free-text name mentions. Keep the wording aligned with _involved_clause and the involved count calculation so the documented behavior matches the code.
413-418: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winInvalid
metricsilently falls back instead of erroring.
leaderboard()coerces any unrecognizedmetricvalue to"involved"without signaling the caller. If this is reachable from an API endpoint with a user-suppliedmetric, a typo silently returns different data instead of a 400, which is easy to miss during integration/testing.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@onboarding/services/retrieval.py` around lines 413 - 418, The leaderboard() function currently hides invalid user input by silently replacing any unsupported metric with "involved". Update leaderboard() to validate the metric argument against _LEADERBOARD_METRICS and raise an explicit error instead of coercing it, so callers using a user-supplied metric can detect bad values and return a proper failure response. Keep the change localized to leaderboard() and any nearby API-facing handling that consumes its metric parameter.onboarding/models/document_chunk.py (1)
17-24: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winConsider explicit
ondeleteoncompany_id/page_idforeign keys.Unlike
OnboardingQuizAttempt.plan_id(which setsondelete="CASCADE"), these FKs have no cascade behavior. Deleting aDocumentPage/Companywithout first removing dependent chunks will hit a DB constraint violation instead of cascading cleanly. Ingestion already deletes chunks explicitly before re-embedding, but any other page/company deletion path is unprotected.♻️ Suggested consistency fix
- company_id: int = Field(foreign_key="companies.id", index=True) - page_id: int = Field(foreign_key="document_pages.id", index=True) + company_id: int = Field(foreign_key="companies.id", index=True, ondelete="CASCADE") + page_id: int = Field(foreign_key="document_pages.id", index=True, ondelete="CASCADE")Would need a matching Alembic migration for the FK constraint.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@onboarding/models/document_chunk.py` around lines 17 - 24, Add explicit cascade behavior to the DocumentChunk foreign keys so dependent chunks are removed automatically when a Company or DocumentPage is deleted. Update the DocumentChunk model’s company_id and page_id relationships to use ondelete="CASCADE" in a way consistent with OnboardingQuizAttempt.plan_id, and make sure the ORM/schema reflects this change. Also add the matching Alembic migration to drop and recreate the FK constraints with cascade semantics so deletion paths remain consistent across the database.onboarding/services/embeddings.py (1)
19-27: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winLazy singleton model load isn't thread-safe.
_get_model()'s check-then-set on the global_modelhas a race: concurrent first calls (e.g., parallel ingestion workers) can each see_model is Noneand construct/download the model redundantly. Community fastembed usage patterns guard this init with a lock.🔒 Suggested fix
+import threading + _model: Optional[TextEmbedding] = None +_model_lock = threading.Lock() def _get_model() -> TextEmbedding: """Lazily load and cache the embedding model (first call downloads it).""" global _model if _model is None: - logger.info("Loading embedding model %s", config.EMBEDDING_MODEL) - _model = TextEmbedding(model_name=config.EMBEDDING_MODEL) + with _model_lock: + if _model is None: + logger.info("Loading embedding model %s", config.EMBEDDING_MODEL) + _model = TextEmbedding(model_name=config.EMBEDDING_MODEL) return _model🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@onboarding/services/embeddings.py` around lines 19 - 27, _get_model currently uses a racy check-then-set on the global _model, so concurrent first callers can initialize TextEmbedding multiple times. Update onboarding/services/embeddings.py to guard the lazy singleton initialization with a thread lock around the _model is None check and assignment, keeping the existing _get_model and _model symbols as the entry point and cached instance.tests/test_retrieval.py (1)
465-476: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMocked-session test doesn't verify the
sourcesfilter is actually applied to the query.
test_sources_filter_appliedmockssession.execto return fixed rows regardless of what_base_selectbuilds, so it can't catch a regression where thesources.where(...)clause (or theSET LOCAL enable_indexscan/bitmapscancalls in theif sources:branch) is dropped or broken. Based on learnings, mocked-session tests can't exercise DB-side WHERE-clause filtering — this needs either an integration test against a real DB (SQLite/Postgres) or an assertion on the generated statement/conn.exec_driver_sqlcalls to actually cover this path.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/test_retrieval.py` around lines 465 - 476, The current `test_sources_filter_applied` only stubs `session.exec` with fixed rows, so it does not prove that `retrieve()` and its `_base_select` query actually apply the `sources` filter or the `SET LOCAL enable_indexscan/bitmapscan` calls in the `if sources:` branch. Update this test to either assert against the generated statement/SQL execution path (for example by inspecting the statement passed through `retrieve()`/`session.exec`) or replace it with an integration-style test against a real database so the `sources` `.where(...)` clause is genuinely exercised.Source: Learnings
onboarding/services/confluence.py (1)
841-861: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winAdd
strict=Truetozip(chunks, vectors)to fail loudly on a length mismatch.If
embedding_service.embed_textsever returns fewer/more vectors than input chunks (partial failure, upstream bug),zip()silently truncates and pairs the wrong vector with the wrong chunk content — corrupting embeddings without any error._flush_embed_batchalready relies onvectors[offset:offset+n]slicing lining up exactly withchunks;strict=Trueturns a silent corruption into a loud, debuggable failure.🛡️ Proposed fix
with Session(engine) as session: - for i, (content, vector) in enumerate(zip(chunks, vectors)): + for i, (content, vector) in enumerate(zip(chunks, vectors, strict=True)): session.add(Also applies to: 983-996
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@onboarding/services/confluence.py` around lines 841 - 861, The _store_chunks helper silently truncates when pairing chunks with vectors, which can corrupt embeddings if their lengths differ. Update the zip(chunks, vectors) iteration in _store_chunks to use strict mode so a mismatch fails immediately, and ensure the same alignment assumption still holds for the _flush_embed_batch flow that passes sliced chunk/vector batches into _store_chunks.Source: Linters/SAST tools
tests/test_knowledge.py (1)
797-807: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winMocked session doesn't verify tenant/user WHERE-clause scoping.
list_chatsfilters bycompany_idANDuser_idat the SQL layer, but this test fully mockssession.exec(...).all(), so the WHERE predicates never execute — a regression that dropped or swapped thecompany_id/user_idfilter would still pass this test. Given this scopes cross-tenant data access, consider an integration test against a real/SQLite-backed session (or assert on the compiled statement's filter clauses) to actually exercise the isolation logic.Based on learnings, for query functions where filtering happens purely in the SQL WHERE clause, mocked-session tests can't simulate database-side filtering — recommend an integration-style test with a real DB/SQLite or inspecting the generated statement's predicates.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/test_knowledge.py` around lines 797 - 807, The test for list_chats is only mocking session.exec().all(), so it never verifies the company_id/user_id SQL filtering and can miss cross-tenant scoping regressions. Update test_list_chats to use a real SQLite-backed session or assert the compiled statement built by onboarding.services.knowledge.list_chats includes the correct WHERE predicates for both company_id and user_id, so the isolation logic is actually exercised.Source: Learnings
onboarding/services/knowledge.py (1)
354-420: 🧹 Nitpick | 🔵 TrivialConsider access controls for people-analytics.
The leaderboard/count/list "people" answers surface individual work-item counts and rankings to any authenticated company user. If some roles shouldn't see comparative performance data across teammates, consider gating this path (e.g., manager-only) rather than exposing it to all knowledge-chat users.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@onboarding/services/knowledge.py` around lines 354 - 420, The people-analytics path in _maybe_person_answer currently returns leaderboard, count, and list results to any authenticated user, so add an access-control check before calling _leaderboard_answer, _count_answer, or _list_answer. Use the existing user/role context available to this codepath to gate comparative teammate performance data (for example, manager-only) and fall through to normal RAG when the caller is not authorized.services/content_validator.py (1)
202-214: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winExtract duplicated keyword-extraction loop into a helper.
The extraction logic (strip/normalize/filter stopwords) is copy-pasted for
keyword_sourceand thetask_descriptionfallback. Consolidating avoids future divergence between the two blocks.♻️ Proposed refactor
+def _extract_keywords(source: str) -> set: + keywords: set = set() + for raw_w in source.split(): + w = re.sub(r"[^\w.-]", "", raw_w).strip(".-").lower() + if len(w) > 3 and w not in _STOPWORDS: + keywords.add(w) + return keywords + keyword_source = topic if topic else task_description - task_keywords: set = set() - for raw_w in keyword_source.split(): - w = re.sub(r"[^\w.-]", "", raw_w).strip(".-").lower() - if len(w) > 3 and w not in _STOPWORDS: - task_keywords.add(w) + task_keywords: set = _extract_keywords(keyword_source) # If topic yielded no usable keywords, fall back to task_description so # check 3 is never silently disabled. if not task_keywords and topic: - for raw_w in task_description.split(): - w = re.sub(r"[^\w.-]", "", raw_w).strip(".-").lower() - if len(w) > 3 and w not in _STOPWORDS: - task_keywords.add(w) + task_keywords = _extract_keywords(task_description)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@services/content_validator.py` around lines 202 - 214, The keyword extraction logic is duplicated between the main `keyword_source` loop and the `task_description` fallback in `content_validator.py`. Extract the normalization/filtering steps into a small helper used by `task_keywords` population so both paths share the same behavior. Keep the fallback in the existing `check 3` flow, but have it call the helper instead of repeating the `re.sub`, strip, lowercase, length, and stopword checks.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: c098b4a3-30a5-467c-83c2-7c13c3219259
⛔ Files ignored due to path filters (1)
.DS_Storeis excluded by!**/.DS_Store
📒 Files selected for processing (102)
.coveragerc.dockerignore.env.example.gitignoreDockerfileKNOWLEDGE_PLATFORM_DESIGN.mdLARGE_INGESTION_CHALLENGE.mdREADME.mdalembic/env.pyalembic/versions/a5b6c7d8e9f0_add_onboarding_quiz_attempt.pyalembic/versions/adaptive_topic_canon_fix_topic_canonicalization.pyalembic/versions/b6c7d8e9f0a1_add_company_and_confluence_connection.pyalembic/versions/c7d8e9f0a1b2_add_onboarding_docs_and_ingestion_jobs.pyalembic/versions/d1e2f3a4b5c6_add_document_pages_and_chunks.pyalembic/versions/e2f3a4b5c6d7_drop_onboarding_docs.pyalembic/versions/f3a4b5c6d7e8_embedding_dim_768_to_384.pyalembic/versions/onb_doc_source.pyalembic/versions/onb_drop_space_keys.pyalembic/versions/onb_ingest_phase_spaces.pyalembic/versions/onb_jira_fields.pyalembic/versions/onb_job_kind.pyalembic/versions/onb_job_source.pyalembic/versions/onb_knowledge_chats.pyalembic/versions/onb_knowledge_msg_blocks.pyalembic/versions/onb_page_meta.pyalembic/versions/onb_user_company.pyalembic/versions/y3z4a5b6c7d8_add_onboarding_tables.pyalembic/versions/z4a5b6c7d8e9_add_share_enabled_to_onboarding_plans.pyconfig.pycontrollers/auth.pymain.pymiddleware/auth.pymodels/__init__.pymodels/user.pyonboarding/__init__.pyonboarding/controllers/__init__.pyonboarding/controllers/confluence.pyonboarding/controllers/knowledge.pyonboarding/controllers/onboarding.pyonboarding/models/__init__.pyonboarding/models/company.pyonboarding/models/confluence_connection.pyonboarding/models/document_chunk.pyonboarding/models/document_page.pyonboarding/models/ingestion_job.pyonboarding/models/knowledge_chat.pyonboarding/models/knowledge_message.pyonboarding/models/onboarding_day.pyonboarding/models/onboarding_plan.pyonboarding/models/onboarding_quiz_attempt.pyonboarding/prompts/__init__.pyonboarding/prompts/knowledge.pyonboarding/prompts/onboarding.pyonboarding/routes/__init__.pyonboarding/routes/confluence.pyonboarding/routes/knowledge.pyonboarding/routes/onboarding.pyonboarding/schemas/__init__.pyonboarding/schemas/knowledge.pyonboarding/schemas/onboarding.pyonboarding/services/__init__.pyonboarding/services/chunking.pyonboarding/services/confluence.pyonboarding/services/embeddings.pyonboarding/services/generation.pyonboarding/services/knowledge.pyonboarding/services/onboarding.pyonboarding/services/retrieval.pyrequirements.txtroutes/__init__.pyscripts/setup.shscripts/test_webhook.shscripts/warm_embeddings.pyservices/company.pyservices/content_validator.pyservices/encryption.pyservices/llm.pyservices/user.pytests/test_chunking_embeddings.pytests/test_company.pytests/test_confluence.pytests/test_content_blocks.pytests/test_content_validator.pytests/test_generation_retry.pytests/test_knowledge.pytests/test_onboarding.pytests/test_retrieval.pytests/test_user_service.pyui/src/app/plugins/connections/banner.tsxui/src/app/plugins/connections/index.tsxui/src/app/plugins/knowledge/index.tsxui/src/app/plugins/onboarding/detail.tsxui/src/app/plugins/onboarding/index.tsxui/src/app/plugins/onboarding/public.tsxui/src/app/plugins/sampleApp/index.tsxui/src/app/plugins/syllabi/block-renderer.tsxui/src/app/plugins/syllabi/detail.tsxui/src/app/plugins/syllabi/quiz.tsxui/src/components/app-sidebar.tsxui/src/components/nav-user.tsxui/src/routes/index.tsxui/src/store/authStore.ts
💤 Files with no reviewable changes (1)
- ui/src/app/plugins/syllabi/quiz.tsx
…enancy Correctness & robustness: - Quiz answer key: normalize_correct_answer extracts the intended a/b/c/d letter (word-boundary) instead of the first char, so "The correct answer is B" scores. - Quiz length: reject a response whose question count != requested (10). - Quiz integrity: get_quiz no longer sends correct_answer to the client; save_quiz_attempt grades server-side and returns per-question results; the UI quiz panel grades from that response. - Retry parity: extract_people_query / answer_from_context route through _generate_with_retry (retry + truncation mapping). - get_day_content: don't hold a DB session across the LLM call. - Retrieval: source-filtered exact-KNN only disables the ANN index scan (keeps bitmapscan so the (company_id, source) filter index is still used). - Block types: normalize valid mixed-case/whitespace-padded types instead of failing. - Ingest single-flight: partial unique index (one running job per company) + begin_ingest/begin_reconcile return the winning job on IntegrityError. Multi-tenancy (no hardcoded "Locus"): - plan_system_prompt is parameterized by company; prompt examples genericized. - company is resolved server-side from the tenant, not the client payload; UI drops the hardcoded company and the "Locus docs" copy. - Generic-email companies use the full email as their display name (was local part); backfill migration updates existing rows. Full suite: 1386 passing, 99% coverage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Prepend the user's own company (resolved from their tenant, labeled "COMPANY:") to the chat context, and tell the model it is authoritative — so "what's my company?" is answered directly, combined with any company details found in the docs.
- Dockerfile: run as a non-root user (appuser) and bump the base image to Python 3.12-slim; keep the embedding-model bake cached and readable by that user. - .dockerignore: exclude .cache/ and logs/ so a host copy can't clobber the baked-in model cache. - Knowledge page Back button navigates to the previous page instead of a hardcoded /onboarding.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
alembic/versions/ingest_single_flight_one_running_job.py (1)
33-39: 🚀 Performance & Scalability | 🔵 TrivialConsider
CREATE INDEX CONCURRENTLYfor largeingestion_jobstables.
op.create_indexhere takes a full table lock while building. Ifingestion_jobsgrows large in multi-tenant deployments, this could briefly block reads/writes during deploy. Alembic runs migrations in a transaction by default, soCONCURRENTLYwould require wrapping this inop.get_context().autocommit_block().Given the row count is likely low (one row per ingestion run), this is a low-priority note rather than a blocker.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@alembic/versions/ingest_single_flight_one_running_job.py` around lines 33 - 39, The `op.create_index` call in the migration that creates the `ingestion_jobs` partial unique index can take a blocking table lock during deployment. Update the migration around `op.create_index` to use a concurrent index build for large deployments by wrapping it in `op.get_context().autocommit_block()` and performing the index creation there, keeping the existing index name, columns, and `postgresql_where` condition intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@onboarding/services/onboarding.py`:
- Around line 277-282: The quiz payload in get_quiz is still exposing the answer
rationale by keeping explanation in each question object. Update the question
sanitization in get_quiz so it removes explanation along with correct_answer
before returning questions, and mirror the same filtering in save_quiz_attempt
or any shared quiz serialization path used for the same payload. Use the
existing get_quiz and save_quiz_attempt logic as the place to apply the
exclusion, and only include explanations in a post-submission response path if
needed.
- Around line 340-343: The quiz scoring logic in
onboarding/services/onboarding.py should fail closed when a question is missing
correct_answer instead of defaulting to "a". Update the question evaluation in
the quiz loop (the block using enumerate(questions), correct_key, selected, and
is_correct) to treat a missing or empty correct_answer as invalid and mark that
question incorrect rather than comparing against a fallback. Use the existing
quiz-processing code path to locate the fix and preserve current behavior only
for questions with an explicit stored answer key.
---
Nitpick comments:
In `@alembic/versions/ingest_single_flight_one_running_job.py`:
- Around line 33-39: The `op.create_index` call in the migration that creates
the `ingestion_jobs` partial unique index can take a blocking table lock during
deployment. Update the migration around `op.create_index` to use a concurrent
index build for large deployments by wrapping it in
`op.get_context().autocommit_block()` and performing the index creation there,
keeping the existing index name, columns, and `postgresql_where` condition
intact.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 06ff8190-5075-4072-b825-f7b64632ccfb
📒 Files selected for processing (23)
.dockerignoreDockerfilealembic/versions/company_name_full_email_generic.pyalembic/versions/ingest_single_flight_one_running_job.pyonboarding/controllers/onboarding.pyonboarding/models/onboarding_plan.pyonboarding/prompts/knowledge.pyonboarding/prompts/onboarding.pyonboarding/schemas/onboarding.pyonboarding/services/confluence.pyonboarding/services/generation.pyonboarding/services/knowledge.pyonboarding/services/onboarding.pyonboarding/services/retrieval.pyservices/company.pyservices/llm.pytests/test_company.pytests/test_confluence.pytests/test_knowledge.pytests/test_onboarding.pyui/src/app/plugins/knowledge/index.tsxui/src/app/plugins/onboarding/detail.tsxui/src/app/plugins/onboarding/index.tsx
🚧 Files skipped from review as they are similar to previous changes (16)
- .dockerignore
- onboarding/models/onboarding_plan.py
- tests/test_company.py
- services/company.py
- services/llm.py
- onboarding/prompts/knowledge.py
- onboarding/controllers/onboarding.py
- tests/test_onboarding.py
- onboarding/services/knowledge.py
- ui/src/app/plugins/onboarding/index.tsx
- ui/src/app/plugins/knowledge/index.tsx
- ui/src/app/plugins/onboarding/detail.tsx
- tests/test_knowledge.py
- onboarding/services/generation.py
- onboarding/services/retrieval.py
- onboarding/services/confluence.py
…coring - get_quiz strips explanation as well as correct_answer (the rationale reveals the answer); explanation is returned in the post-submission results instead, and the UI shows it from there. - save_quiz_attempt fails closed: a question with no stored answer key is marked incorrect rather than defaulting to "a". Full suite: 1387 passing.
Make a fresh clone run without manual database/pgvector fiddling. - Migration d1e2f3a4b5c6 self-enables pgvector (CREATE EXTENSION IF NOT EXISTS vector) so a brand-new database migrates cleanly, no manual step. - docker-compose.yml: Postgres 16 + pgvector (version-matched image) as the local DB. - setup.sh: Docker-first — requires Docker (does NOT auto-install it), starts the DB container, waits for readiness, writes .env, installs deps (auto-installs Node), runs migrations. Local Postgres removed in favor of the reliable Docker path. - Windows: scripts/setup.ps1 + scripts/dev.ps1 (Docker-based), with setup.cmd / dev.cmd wrappers so users run a short command or double-click. - Makefile: make db-up / make db-down; make dev starts the DB first. - docs/LOCAL_SETUP.md: rewritten — Mac + Windows, automatic and manual paths. Verified: fresh pgvector container -> app boots -> migrations run on startup -> serves 200; PowerShell scripts parse clean and their portable logic checks out. Full suite: 1387 passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
docker-compose.yml (1)
18-19: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winBind the published port to localhost.
"5432:5432"publishes on0.0.0.0, exposing a DB with trivialopenship/openshipcredentials to the whole local network. For a local-dev DB, bind to loopback. This also makes the collision with a host-local Postgres explicit rather than silently competing for the interface.🔒 Proposed change
ports: - - "5432:5432" + - "127.0.0.1:5432:5432"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docker-compose.yml` around lines 18 - 19, The postgres port mapping is currently published on all interfaces, which exposes the local-dev database beyond loopback and can silently compete with a host Postgres. Update the ports entry in docker-compose.yml to bind the existing 5432 mapping to localhost only, keeping the same service port while using a loopback host binding so it is explicit and not reachable from the wider network.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@scripts/dev.cmd`:
- Around line 1-3: The Windows launcher wrapper in the dev.cmd script needs to
be saved with CRLF line endings instead of LF-only. Update the script file
encoding/line endings so cmd.exe can parse the wrapper reliably before handing
off to dev.ps1, and verify the wrapper still invokes the PowerShell entrypoint
correctly.
In `@scripts/setup.ps1`:
- Around line 29-89: The step labels in setup.ps1 are inconsistent with the
mirrored setup.sh flow and the total count, since the script starts at “Step
0/6” and skips “Step 2”. Update the Info calls in the setup sequence (the
Python, Database, and subsequent secret/.env sections) so the numbering matches
setup.sh’s 1/6 through 6/6 progression and uses a consistent label order
throughout.
- Around line 44-52: The Python version check in setup.ps1 is happening
immediately after winget installs Python, but the current PowerShell session may
not have the updated PATH yet. Update the flow around the winget install/Ok path
so the script either reloads PATH or tells the user to reopen the shell before
running the final `py` version check, matching the existing Node setup guidance.
Keep the fix localized to the `winget` branch and the `Ok ("Python: " + (&
$PyExe `@PyArgs` --version))` call site.
---
Nitpick comments:
In `@docker-compose.yml`:
- Around line 18-19: The postgres port mapping is currently published on all
interfaces, which exposes the local-dev database beyond loopback and can
silently compete with a host Postgres. Update the ports entry in
docker-compose.yml to bind the existing 5432 mapping to localhost only, keeping
the same service port while using a loopback host binding so it is explicit and
not reachable from the wider network.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8ef223ed-9080-495b-9be4-2b9ed85de146
📒 Files selected for processing (9)
Makefilealembic/versions/d1e2f3a4b5c6_add_document_pages_and_chunks.pydocker-compose.ymldocs/LOCAL_SETUP.mdscripts/dev.cmdscripts/dev.ps1scripts/setup.cmdscripts/setup.ps1scripts/setup.sh
✅ Files skipped from review due to trivial changes (1)
- docs/LOCAL_SETUP.md
🚧 Files skipped from review as they are similar to previous changes (1)
- alembic/versions/d1e2f3a4b5c6_add_document_pages_and_chunks.py
| @echo off | ||
| REM Windows convenience wrapper — double-click, or run: scripts\dev.cmd | ||
| powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0dev.ps1" %* |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Save this wrapper with CRLF line endings.
LF-only .cmd files can misparse under cmd.exe and break the launcher before PowerShell starts.
🧰 Tools
🪛 Blinter (1.0.113)
[error] 1-1: Unix line endings detected. Explanation: Batch file uses Unix line endings (LF-only) which can cause GOTO/CALL label parsing failures and script malfunction due to Windows batch parser 512-byte boundary bugs. Recommendation: Convert file to Windows line endings (CRLF). Use tools like dos2unix, notepad++, or configure git with 'git config core.autocrlf true'. Context: File uses Unix line endings (LF-only) - 3 LF sequences found
(E018)
[error] 3-3: PowerShell execution policy bypass. Explanation: Bypassing PowerShell execution policy can allow malicious scripts to run. Recommendation: Avoid using -ExecutionPolicy Bypass unless absolutely necessary. Context: PowerShell execution policy bypass detected
(SEC009)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/dev.cmd` around lines 1 - 3, The Windows launcher wrapper in the
dev.cmd script needs to be saved with CRLF line endings instead of LF-only.
Update the script file encoding/line endings so cmd.exe can parse the wrapper
reliably before handing off to dev.ps1, and verify the wrapper still invokes the
PowerShell entrypoint correctly.
Source: Linters/SAST tools
- scripts/*.cmd: CRLF line endings + .gitattributes (*.cmd text eol=crlf) so cmd.exe parses the wrappers reliably on Windows. - setup.ps1: renumber steps to a clean 1/6..6/6 (matches setup.sh). - setup.ps1: after winget installs Python/Node, refresh PATH from the registry and re-detect (or tell the user to reopen PowerShell) before using them. - docs/LOCAL_SETUP.md: Windows prerequisites shown as a table (Git, Docker Desktop; Python/Node via winget), matching the Mac section.
Windows PowerShell 5.1 reads scripts as ANSI (not UTF-8) when there is no BOM, so the box-drawing separators and em dashes in comments were garbled and broke parsing on real Windows. Replace all non-ASCII characters with ASCII in the .ps1 / .cmd files. (PowerShell 7 on macOS defaults to UTF-8, which is why this slipped through earlier.)
$ErrorActionPreference="Stop" made PowerShell treat any native-tool stderr as a terminating error, so a harmless `docker info` WARNING aborted setup at Step 2. Switch setup.ps1/dev.ps1 to "Continue" and add explicit $LASTEXITCODE checks (venv, pip, npm, alembic) so real failures still stop with a clear message. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tings; drop SMTP from generated .env - Theme (light/dark) toggle added to the course view, onboarding plan, and the knowledge section, reusing the existing ThemeToggle component. - Knowledge: added a Settings button (chat header + "not ready" empty state) and a Back button on the "not ready" empty state (navigate(-1), matching the chat view). - setup.sh / setup.ps1: stop writing the unused SMTP block into the generated .env (email isn't used yet; add it back manually if/when needed).
- Inline `code` chips had a hardcoded light background with no dark variant, so they rendered as harsh white chips in dark mode. Add dark:bg-zinc-800 / dark:text-zinc-200. - Body text (paragraphs, lists, table cells, blockquotes) used text-zinc-600 with no dark variant and looked too dim; add dark:text-zinc-300 for readability. Light mode is unchanged; headings/code blocks untouched. Shared renderer, so this also improves onboarding day content and knowledge answers in dark mode.
…rrupted
If the server's background week-generation is interrupted (lost LLM connection /
restart), the week stayed unlocked-but-empty with no way to recover. Add:
- POST /quiz/{skill_id}/week/{week}/regenerate -> controllers.quiz.regenerate_week,
which re-runs the SAME _generate_next_week with identical args. Guarded: only an
unlocked (week <= generated_weeks), week >= 2, currently-EMPTY week; 409 if it
already has content, 400 if not unlocked / no LLM key.
- UI: the course sidebar detects an unlocked-but-empty week and shows a
"Generate Week N" retry button that calls the endpoint and polls until it appears.
Tests: 7 new controller tests (controllers/quiz.py at 100%). Full suite 1394 passing.
Rewrite Getting Started to match the Docker-based local setup: per-OS prerequisites, automatic (make setup / scripts\setup.cmd) and manual paths for both macOS and Windows, updated command table (db-up/db-down, Windows .cmd), and fixed the Environment Variables intro (setup generates .env, not from .env.example).
- Ingest now runs a guarded deletion sweep (mark-and-sweep) in the same
read: adds, updates, and removes upstream-deleted pages/issues in one pass.
Guards: skip sweep on cancel/partial read; protect containers that failed
to read; require >=1 successful container.
- Cancel: POST /confluence/ingest/{job_id}/cancel; worker stops cleanly
between containers/embed-batches (no half-written page, no sweep on cancel).
- UI: single "Sync" button + Cancel (Connections page).
- Cross-platform git hooks: .husky/pre-commit finds venv on Windows; setup.ps1
wires husky via root npm install.
Full suite: 1407 tests, 99% coverage.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The commit hooks were Unix-only and silently skipped (or errored) on Windows. Make them resolve identically on both: - .husky/pre-commit: detect the venv dir (.venv/bin on Unix, .venv/Scripts on Windows) and put it on PATH, then run pre-commit from there. - .pre-commit-config.yaml: use bare `python` / `pytest` (resolved via PATH) instead of `python3` and `.venv/bin/pytest`. - scripts/black_then_collapse.py: resolve black at .venv/Scripts/black.exe on Windows, .venv/bin/black on Unix. Full suite: 1407 tests, 99% coverage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The -v/verbose stream printed ~1400 lines per commit, which was slow and triggered console flush errors on Windows git-bash. -q keeps it compact; pre-commit still surfaces full output when a test fails.
pre-commit streaming the full pytest + coverage output through git-bash deadlocks the console on Windows. The hook now exits early on Windows (MINGW/MSYS/CYGWIN) and runs the full Black + test suite on macOS/Linux (and CI) as before. Baked into the tracked hook so it survives future npm install / setup runs.
…dashboard - README + KNOWLEDGE_PLATFORM_DESIGN: describe the merged Sync (one read = add + update + remove upstream-deleted) and the Cancel control; drop the separate "reconcile job" framing; note the cancelled job status. - Dashboard empty state (analytics): replace the three learning-only cards with four covering Adaptive AI Courses, Company Knowledge Chat (RAG), Guided Onboarding, and Progress & Streaks; broaden the hero copy beyond courses.
…Connections - content_validator: split topic keywords on separators like "/" so a topic such as "Basic Input/Output" is checked against "input" + "output" instead of the unmatchable glued "inputoutput" (kept internal dots so "Express.js" survives). Fixes the 503 when generating content for slashed topics. - syllabi weekly quiz: the "not generated" screen now says one question per topic (was a hardcoded "5 questions"), matching num_questions = len(topics). - config: post-connect redirect now defaults to /connections (was /onboarding), so reconnecting Atlassian keeps you on the Connections page.
Closes #96.
Ingests a company's entire Confluence + Jira into a pgvector knowledge base and
serves it via RAG:
in the company's own docs
people-analytics (who did what)
Highlights
Tests: 1382 passing, 99% coverage.
Summary by CodeRabbit