{"id":49824137,"url":"https://github.com/lopadova/AskMyDocs","last_synced_at":"2026-05-30T05:00:24.724Z","repository":{"id":351856088,"uuid":"1212731323","full_name":"lopadova/AskMyDocs","owner":"lopadova","description":"AI Hub \u0026 Intelligent Agentic Platform for the Enterprise - self-hostable AI hub for enterprise knowledge.","archived":false,"fork":false,"pushed_at":"2026-05-26T11:39:53.000Z","size":13272,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-26T12:36:50.549Z","etag":null,"topics":["ai","ai-act","ai-agent","ai-assistant","aihub","chatbot","embeddings","intelligent-agent","kb","laravel","postgresql","rag","rag-chatbot","vector-database"],"latest_commit_sha":null,"homepage":"","language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lopadova.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-16T17:13:58.000Z","updated_at":"2026-05-26T10:23:03.000Z","dependencies_parsed_at":null,"dependency_job_id":"1322523a-a56b-4d2e-b9c6-b7b824ab8795","html_url":"https://github.com/lopadova/AskMyDocs","commit_stats":null,"previous_names":["lopadova/askmydocs"],"tags_count":54,"template":false,"template_full_name":null,"purl":"pkg:github/lopadova/AskMyDocs","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lopadova%2FAskMyDocs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lopadova%2FAskMyDocs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lopadova%2FAskMyDocs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lopadova%2FAskMyDocs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lopadova","download_url":"https://codeload.github.com/lopadova/AskMyDocs/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lopadova%2FAskMyDocs/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33680527,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-30T02:00:06.278Z","response_time":92,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ai","ai-act","ai-agent","ai-assistant","aihub","chatbot","embeddings","intelligent-agent","kb","laravel","postgresql","rag","rag-chatbot","vector-database"],"created_at":"2026-05-13T14:07:54.554Z","updated_at":"2026-05-30T05:00:24.713Z","avatar_url":"https://github.com/lopadova.png","language":"PHP","funding_links":[],"categories":["Built with Regolo"],"sub_categories":[],"readme":"# AskMyDocs — AI Hub \u0026 Intelligent Agentic Platform for the Enterprise\n\n\u003e **Enterprise RAG + Knowledge Graph + Agentic Tool Use, self-hostable, MIT licensed.**\n\nAskMyDocs is a self-hostable AI hub for enterprise knowledge. It fuses\nhybrid retrieval-augmented generation (pgvector + FTS + reranker), a\ntyped canonical knowledge graph with human-gated promotion, a streaming\nchat surface on the Vercel AI SDK, and a full admin operations cockpit\ninto a single Laravel platform. It is the open-source, on-prem alternative\nto Glean / Notion AI / ChatGPT Enterprise — without the per-seat lock-in.\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"resources/cover-AskMyDocs.png\" alt=\"AskMyDocs\" width=\"100%\" /\u003e\n\u003c/p\u003e\n\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"#quick-start-5-minutes\"\u003e\u003cimg src=\"https://img.shields.io/badge/Laravel-13+-FF2D20?style=flat-square\u0026logo=laravel\u0026logoColor=white\" alt=\"Laravel\"\u003e\u003c/a\u003e\n  \u003ca href=\"#features-by-area\"\u003e\u003cimg src=\"https://img.shields.io/badge/Claude-Compatible-cc785c?style=flat-square\u0026logo=anthropic\u0026logoColor=white\" alt=\"Claude\"\u003e\u003c/a\u003e\n  \u003ca href=\"#features-by-area\"\u003e\u003cimg src=\"https://img.shields.io/badge/OpenAI-Compatible-412991?style=flat-square\u0026logo=openai\u0026logoColor=white\" alt=\"OpenAI\"\u003e\u003c/a\u003e\n  \u003ca href=\"#features-by-area\"\u003e\u003cimg src=\"https://img.shields.io/badge/Gemini-Compatible-4285F4?style=flat-square\u0026logo=google\u0026logoColor=white\" alt=\"Gemini\"\u003e\u003c/a\u003e\n  \u003ca href=\"#features-by-area\"\u003e\u003cimg src=\"https://img.shields.io/badge/OpenRouter-Multi--Model-6366f1?style=flat-square\" alt=\"OpenRouter\"\u003e\u003c/a\u003e\n  \u003ca href=\"#features-by-area\"\u003e\u003cimg src=\"https://img.shields.io/badge/Regolo.ai-EU-10b981?style=flat-square\" alt=\"Regolo.ai\"\u003e\u003c/a\u003e\n  \u003ca href=\"#features-by-area\"\u003e\u003cimg src=\"https://img.shields.io/badge/MCP-10%20tools-0ea5e9?style=flat-square\" alt=\"MCP Server\"\u003e\u003c/a\u003e\n  \u003ca href=\"#features-by-area\"\u003e\u003cimg src=\"https://img.shields.io/badge/Canonical--KB-9%20types-ff7a00?style=flat-square\" alt=\"Canonical KB\"\u003e\u003c/a\u003e\n  \u003ca href=\"#features-by-area\"\u003e\u003cimg src=\"https://img.shields.io/badge/Knowledge%20Graph-10%20relations-7c3aed?style=flat-square\" alt=\"Knowledge Graph\"\u003e\u003c/a\u003e\n  \u003ca href=\"#features-by-area\"\u003e\u003cimg src=\"https://img.shields.io/badge/Anti--Repetition-%E2%9A%A0%EF%B8%8F%20built--in-dc2626?style=flat-square\" alt=\"Anti-Repetition Memory\"\u003e\u003c/a\u003e\n  \u003ca href=\"#prerequisites\"\u003e\u003cimg src=\"https://img.shields.io/badge/PostgreSQL-pgvector-336791?style=flat-square\u0026logo=postgresql\u0026logoColor=white\" alt=\"PostgreSQL + pgvector\"\u003e\u003c/a\u003e\n  \u003ca href=\"#license\"\u003e\u003cimg src=\"https://img.shields.io/badge/License-MIT-green?style=flat-square\" alt=\"MIT License\"\u003e\u003c/a\u003e\n  \u003ca href=\"#prerequisites\"\u003e\u003cimg src=\"https://img.shields.io/badge/PHP-8.3+-777BB4?style=flat-square\u0026logo=php\u0026logoColor=white\" alt=\"PHP 8.3+\"\u003e\u003c/a\u003e\n  \u003ca href=\"CHANGELOG.md\"\u003e\u003cimg src=\"https://img.shields.io/badge/release-v8.0.3-blueviolet?style=flat-square\" alt=\"Release v8.0.3\"\u003e\u003c/a\u003e\n  \u003ca href=\"#universal-connectors\"\u003e\u003cimg src=\"https://img.shields.io/badge/connectors-7%20native-0ea5e9?style=flat-square\" alt=\"7 Native Connectors\"\u003e\u003c/a\u003e\n  \u003ca href=\"#quality--observability\"\u003e\u003cimg src=\"https://img.shields.io/badge/tests-1750%20PHPUnit%20%2B%20436%20Vitest-brightgreen?style=flat-square\" alt=\"1750 PHPUnit + 436 Vitest\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cstrong\u003eAsk your docs. Get grounded answers. See the sources. Run agentic tools.\u003c/strong\u003e\n\u003c/p\u003e\n\n\n# CHATBOT UI/UX\n![AskMyDoc - ChatBot.png](resources/screenshots/AskMyDoc%20-%20ChatBot.png)\n\n# DASHBOARD UI/UX\n![AskMyDoc - Dashboard.png](resources/screenshots/AskMyDoc%20-%20Dashboard.png)\n\n---\n\n## Table of Contents\n\n- [What it is](#what-it-is)\n- [Why AskMyDocs — the 5 moats](#why-askmydocs--the-5-moats)\n- [✨ Universal Connectors](#universal-connectors)\n- [✨ Modern Chat Surface (Vercel AI SDK UI)](#modern-chat-surface-vercel-ai-sdk-ui)\n- [Features by area](#features-by-area)\n  - [Retrieval \u0026 Knowledge](#retrieval--knowledge)\n  - [Chat \u0026 Conversation](#chat--conversation)\n  - [Security \u0026 Compliance](#security--compliance)\n  - [Admin \u0026 Operations](#admin--operations)\n  - [Integrations \u0026 Extensibility](#integrations--extensibility)\n  - [Quality \u0026 Observability](#quality--observability)\n- [Quick start (5 minutes)](#quick-start-5-minutes)\n- [Architecture](#architecture)\n- [Roadmap](#roadmap)\n- [Documentation](#documentation)\n- [Screenshots gallery](#screenshots-gallery)\n- [Sister packages](#sister-packages)\n- [Contributing](#contributing)\n- [License](#license)\n- [Changelog](#changelog)\n\n---\n\n## What it is\n\n**What.** AskMyDocs is an **AI hub for enterprise knowledge** built on\nLaravel 13 + PostgreSQL + pgvector. It ingests markdown, text, PDF and\nDOCX documents into a typed canonical knowledge graph, answers\nquestions over them with streaming RAG, exposes the same knowledge as\nMCP tools for any agentic client (Claude Desktop, Claude Code,\nCursor, custom agents), and ships a full React admin SPA — KPI\ndashboard, canonical KB explorer with inline editor and graph viewer,\nlog viewer (five tabs), whitelisted Artisan maintenance runner, daily\nAI-insights panel — all behind Spatie role-based access control with\naudit trails on every destructive mutation.\n\n**Why.** Most \"RAG over docs\" tools treat your KB as a pile of\ninterchangeable chunks. They re-discover the answer from zero on every\nquery, never persist what your team has *already decided*, and\nre-propose options that were explicitly dismissed three quarters ago.\nSaaS competitors (Glean, ChatGPT Enterprise, Notion AI) either lock\nyou into per-seat contracts and proprietary data residency, or charge\n~$500K/year for the on-prem option. AskMyDocs is MIT-licensed,\nself-hostable, EU-sovereign-feasible, and ships a typed canonical layer\nwith human-gated promotion that no public competitor offers.\n\n**For whom.** Enterprise teams ingesting their architectural decisions\n/ runbooks / standards / incidents / domain concepts into a *navigable*\nKB; operators of regulated-industry RAG (GDPR, AI Act) needing\nfield-level PII redaction at every persistence boundary; engineering\norgs that want LLMs to stop re-proposing rejected approaches; Italian\nsoftware companies filing under `documentazione_idonea` Patent Box;\nand anyone allergic to vendor lock-in.\n\n---\n\n## Why AskMyDocs — the 5 moats\n\nThese five differentiators come from the public competitor audit at\n[`docs/v4-platform/AUDIT-2026-05-11-competitor-comparison.md`](docs/v4-platform/AUDIT-2026-05-11-competitor-comparison.md)\n(Section 3, \"Where AskMyDocs is genuinely AHEAD\"). They are the moats\nno other public RAG platform — open-source or SaaS — currently ships.\n\n| ★ | Moat | One-line |\n|:---:|---|---|\n| ★ | **Human-gated canonical promotion pipeline** (ADR 0003) | Three-stage API (`/suggest` → `/candidates` → `/promote`) holds the LLM at \"draft\"; only humans (git push → GH Action) and operators (`kb:promote` CLI) commit canonical storage. Immutable `kb_canonical_audit` trail. No public competitor splits \"AI proposes\" from \"human writes\" this way. |\n| ★ | **Retrieval-time knowledge graph + rejected-approach injection** | `GraphExpander` walks `kb_edges` 1-hop at every query and folds neighbours into the `SearchResult`. `RejectedApproachInjector` vector-correlates the query against `rejected-approach` canonical docs and surfaces them under a ⚠ marker so the LLM stops re-proposing dismissed options. ChatGPT Enterprise / Glean / Vectara do not do this. |\n| ★ | **PII redaction at 11 persistence boundaries** (default-OFF, granular per touch-point) | `padosoft/laravel-pii-redactor` v1.2 wired at 11 touch-points across observers, middleware, Monolog processor, failed-job listener, Flow payload redactor, insights inspector. EU-GDPR-grade *field-level* redaction inside the app boundary — not just data-residency. Every knob default-OFF so v3 / v4.0 hosts see byte-identical behaviour until they opt in. |\n| ★ | **MIT-licensed, self-hostable, on-prem feasible** (no $500K/yr vendor contract) | Vectara is the only competitor that ships on-prem ($500K/yr public list). Glean / Notion AI / ChatGPT Enterprise / M365 Copilot are SaaS-only. AskMyDocs runs on any Laravel + PostgreSQL + pgvector host with zero vendor lock-in; the entire sister-package stack is MIT and independently reusable. |\n| ★ | **Eval-harness CI gate + nightly LLM-as-judge + adversarial cohorts** | `padosoft/eval-harness` v1.2 RAG regression gate on every PR (4 datasets / 1 baseline + 3 adversarial / 7 metrics including custom `CitationGroundednessMetric` + `CosineGroundednessMetric`); `eval:nightly` Artisan cron at 05:30 UTC with three-fence cost guard, regression detection vs prior baseline, `Log::alert` + sidecar on regression; adversarial-lane nightly opt-in shipped in v4.4. Out-of-the-box eval surface nobody else publicly ships. |\n\n---\n\n## ✨ Universal Connectors\n\n**Plug AskMyDocs into Google Drive, Notion, OneDrive, Evernote, Fabric, Confluence, and Jira with OAuth in one click — every document chunked and cited correctly per source.**\n\nMost \"RAG over docs\" tools either expect a pile of pre-flattened\nmarkdown or ship a single brittle \"Google Drive sync\" feature. AskMyDocs\nv4.5 ships a real **connector framework** + **seven native connectors**\n+ **per-source chunkers** so every external knowledge corpus lands in\nthe canonical KB with its provenance, native IDs, ACL hints, and\nstatus preserved — and gets chunked the way that source actually wants\nto be chunked.\n\n- **7 native connectors live in v4.5** — `google-drive` (OAuth2 + delta-query), `notion` (OAuth2 + block paginator), `evernote` (OAuth + `.enex` bulk import), `fabric` (API-key, OAuth pending upstream), `onedrive` (Microsoft Graph delta-query — supports `text/markdown` / `text/plain` / `application/pdf`; Office formats `.docx` / `.xlsx` / `.pptx` ingestion deferred), `confluence` (Atlassian OAuth 2.0 3LO; `cloud_id` persisted in tenant-scoped `connector_credentials.extra_json.cloud_id`, optionally reused by a Jira install in the same tenant/workspace), `jira` (Atlassian OAuth 2.0 3LO + ADF-to-markdown + injection-safe JQL builder).\n- **Per-source chunkers** — `NotionBlockChunker` / `ConfluencePageChunker` / `OfficeDocChunker` / `AtomicNoteChunker` / `JiraIssueChunker` / `PdfPageChunker` dispatched via `PipelineRegistry::resolveChunker()` (R23 FQCN-validated + `supports()` mutex-checked at boot).\n- **Rich frontmatter capture** — every connector populates document-level metadata (`connector`, `external_id`, `external_url`, native timestamps) plus chunk-level metadata (`source_type`, `search_tags` (top-level in chunk metadata), `recency_bucket`, ACL hint, status, preamble-path). Drives `KbSearchService` facets + `Reranker` Layer-4 signals (tag overlap + recency + status-active + preamble-match).\n- **Admin OAuth flow at `/app/admin/connectors`** — React SPA + Spatie super-admin gate + signed OAuth callback + per-installation `connector_installations` + `connector_credentials` rows + scheduler-driven incremental sync via `App\\Jobs\\ConnectorSyncJob`.\n- **Opt-in live-test recording infrastructure** — `tests/Live/Connectors/` skeleton + per-provider env-var guard + `docs/v4-platform/RUNBOOK-live-fixture-recording.md` junior-proof setup guide. CI runs only `Unit` + `Feature`; operators refresh fixtures explicitly when provider APIs drift.\n\n### How it compares\n\n| Capability | AskMyDocs v4.5 | Glean | Notion AI | ChatGPT Enterprise | M365 Copilot | Mendable | Vectara |\n|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|\n| Self-hostable + connector framework | ✅ MIT | ❌ SaaS | ❌ SaaS | ❌ SaaS | ❌ SaaS | ❌ SaaS | ❌ $500K/yr |\n| Native Google Drive | ✅ | ✅ | ❌ | ✅ | ❌ | partial | ❌ |\n| Native Notion | ✅ | ✅ | ✅ | ✅ | ❌ | partial | ❌ |\n| Native OneDrive | ✅ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ |\n| Native Evernote | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |\n| Native Confluence | ✅ | ✅ | ❌ | ❌ | partial | partial | ❌ |\n| Native Jira | ✅ | ✅ | ❌ | ❌ | ❌ | partial | ❌ |\n| Source-aware chunking framework | ✅ | private | ❌ | ❌ | ❌ | partial | partial |\n| Plugin/package extensibility | ✅ (v4.6 packages) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |\n\n**Try it.** Read [`docs/connectors/README.md`](docs/connectors/README.md)\nfor the developer guide (10-method `ConnectorInterface` contract +\nauto-discovery + framework reuse pattern), then log in as a\nsuper-admin and navigate to `/app/admin/connectors` to install the first\nconnector.\n\n---\n\n## ✨ Modern Chat Surface (Vercel AI SDK UI)\n\n**Stop / regenerate / branch / inline-edit / token-cost meter — the chat surface every modern AI app should have, with full streaming citations and suggested follow-ups.**\n\nThe chat UX gap against Claude Desktop / ChatGPT Plus / Vercel\nreference apps is what 90% of first-time users notice and 0% of\nself-hostable RAG OSS ships. v4.5 closes that gap on all seven Tier 1\naffordances plus the first Tier 2 win (suggested follow-ups), built on\ntop of the v4.0 Vercel AI SDK v6 `UIMessageChunk` streaming foundation.\n\n- **7 Tier 1 features** — stop-streaming button (`AbortController`-backed), regenerate-last-assistant, branch-from-message endpoint (forks the conversation tree), inline-edit user message, token+cost meter (BE `config('ai.cost_rates')`), enhanced per-message provider+model+timestamp badge, copy-code-block.\n- **Suggested follow-up pills** — `SuggestedFollowupGenerator` derives three follow-up prompts from the assistant's last reply; renders as clickable pill chips under the message; submits via the streaming endpoint when clicked.\n- **Full Vercel AI SDK v6 message-parts integration** — `MessageStreamController` emits canonical `start` / `text-start` / `text-delta` / `text-end` / `source-url` / `data` / `finish` frames over SSE; `useChatStream()` exposes `data-state=\"idle|loading|ready|empty|error\"` for deterministic Playwright waits (SDK `submitted` and `streaming` statuses both map to `loading` via `mapStatusToDataState()` — see `frontend/src/features/chat/map-status-to-data-state.test.ts`).\n- **Canvas-ready architecture (artifact panel deferred to v5.x)** — Tier 2 stretch (tool-result rendering, streaming source-document parts, conversation export, image attachments, artifact panel) is deliberately deferred to a v5.x milestone so it can be designed alongside the MCP **client** tool-result surface and share one storage contract. See ADR 0008 D4.\n- **Zero-config for OpenAI / Anthropic / Gemini / OpenRouter / Regolo** — OpenAI, Anthropic, Gemini, and OpenRouter are called via raw `Http::` (no SDK); Regolo is wired through the `padosoft/laravel-ai-regolo` SDK adapter on `laravel/ai`. `AiManager::chatStream()` synthesises a single-chunk SSE for providers without native streaming via the `FallbackStreaming` trait.\n\n**Try it.** Open `/app/chat` in the React SPA. Start a long answer\nand hit Stop; click Regenerate; hover the assistant message and pick\nBranch (a new conversation forks from that point); pick a follow-up\npill chip to chain into the next prompt; hover any code block for the\nCopy button.\n\n### Database\n```env\nDB_CONNECTION=pgsql\nDB_HOST=127.0.0.1\nDB_PORT=5432\nDB_DATABASE=askmydocs\nDB_USERNAME=postgres\nDB_PASSWORD=secret\n```\n\n### AI Provider\n\nThe system supports **five providers**. OpenAI, Anthropic, Gemini, and OpenRouter are called via raw `Http::`, which keeps auth, retries, timeouts, and response parsing under our control. Regolo is the exception: it is wired through the `padosoft/laravel-ai-regolo` SDK adapter (built on `Laravel\\Ai`), so chat + embeddings reuse its OpenAI-compatible client.\n\nConfig file: `config/ai.php`\n\n#### Defaults\n\n```env\n# Chat provider. Supported: openai, anthropic, gemini, openrouter, regolo\nAI_PROVIDER=openrouter\n\n# Embeddings provider. Must support embeddings (openai, gemini, regolo, openrouter).\n# Anthropic does NOT offer embeddings. OpenRouter exposes OpenAI-compatible\n# /v1/embeddings (since Oct 2025) routing openai/text-embedding-3-small (default)\n# and qwen/qwen3-embedding-4b. Leave empty to let AiManager reuse AI_PROVIDER\n# when the default chat provider supports embeddings; otherwise it falls back\n# to the first embeddings-capable provider with a configured API key in this order:\n# openai → openrouter → regolo → gemini. The 1536-dim defaults (openai +\n# openrouter) come first so the stock KB_EMBEDDINGS_DIMENSIONS=1536 pgvector\n# schema stays consistent under auto-selection — regolo (4096) and gemini\n# (768) require a pgvector resize in lock-step, set AI_EMBEDDINGS_PROVIDER\n# explicitly to opt in.\nAI_EMBEDDINGS_PROVIDER=openai\n```\n\n#### OpenAI\n\n```env\nAI_PROVIDER=openai\nOPENAI_API_KEY=sk-...\nOPENAI_CHAT_MODEL=gpt-4o\nOPENAI_EMBEDDINGS_MODEL=text-embedding-3-small\nOPENAI_TEMPERATURE=0.2\nOPENAI_MAX_TOKENS=4096\nOPENAI_TIMEOUT=120\n```\n\n#### Anthropic (Claude)\n\nAnthropic has no embeddings endpoint, so pair it with any embeddings-capable provider — OpenAI, OpenRouter, Regolo, or Gemini. If `AI_EMBEDDINGS_PROVIDER` is left empty, `AiManager` auto-selects the first one with a configured API key in this order: openai → openrouter → regolo → gemini. The 1536-dim defaults (OpenAI's `text-embedding-3-small` + OpenRouter routing the same model) come first so a deployment with the stock `KB_EMBEDDINGS_DIMENSIONS=1536` pgvector schema stays consistent under auto-selection; Regolo (4096) and Gemini (768) require a `vector(N)` resize and a matching `KB_EMBEDDINGS_DIMENSIONS` change before use, so set `AI_EMBEDDINGS_PROVIDER=regolo|gemini` explicitly when you've migrated.\n\n```env\nAI_PROVIDER=anthropic\nAI_EMBEDDINGS_PROVIDER=openai\n\nANTHROPIC_API_KEY=sk-ant-...\nANTHROPIC_CHAT_MODEL=claude-sonnet-4-20250514\nOPENAI_API_KEY=sk-...\n```\n\n#### Google Gemini\n\nGemini supports both chat and embeddings. `text-embedding-004` is **768-dim**, so switching embedding providers requires updating `KB_EMBEDDINGS_DIMENSIONS` **and** re-indexing.\n\n```env\nAI_PROVIDER=gemini\nGEMINI_API_KEY=AIza...\nGEMINI_CHAT_MODEL=gemini-2.0-flash\nGEMINI_EMBEDDINGS_MODEL=text-embedding-004\n```\n\n#### OpenRouter (multi-model gateway) — default\n\nOpenRouter proxies hundreds of models. Since Oct 2025 it also exposes an\nOpenAI-compatible `/v1/embeddings` endpoint, so it can serve both chat\nand embeddings from the same gateway. Default embedding model is\n`openai/text-embedding-3-small` (1536 dims — matches the default\n`KB_EMBEDDINGS_DIMENSIONS`, no re-index needed). Alternative\n`qwen/qwen3-embedding-4b` (2560 dims) requires resizing the pgvector\ncolumn on `knowledge_chunks.embedding` + `embedding_cache.embedding`\nand re-indexing. Pair with a separate provider if you prefer.\n\n```env\nAI_PROVIDER=openrouter\nAI_EMBEDDINGS_PROVIDER=openrouter\n\nOPENROUTER_API_KEY=sk-or-...\nOPENROUTER_CHAT_MODEL=openai/gpt-4o-mini\nOPENROUTER_EMBEDDINGS_MODEL=openai/text-embedding-3-small\nOPENROUTER_APP_NAME=\"AskMyDocs\"\nOPENROUTER_SITE_URL=https://kb.example.com\n```\n\n#### Regolo.ai (by Seeweb)\n\nEU-based, GDPR-compliant, **OpenAI-compatible** REST API. Supports chat, streaming, embeddings, and reranking via the [`padosoft/laravel-ai-regolo`](https://github.com/padosoft/laravel-ai-regolo) extension on top of the official `laravel/ai` SDK. Get keys at [dashboard.regolo.ai](https://dashboard.regolo.ai) and see [docs.regolo.ai](https://docs.regolo.ai) for the full model catalogue.\n\n```env\nAI_PROVIDER=regolo\nAI_EMBEDDINGS_PROVIDER=regolo\n\nREGOLO_API_KEY=...\nREGOLO_BASE_URL=https://api.regolo.ai/v1\n\n# Chat models — `cheapest` / `smartest` aliases pick the right model for\n# cost-vs-quality shortcuts (see `Lab::Cheapest` / `Lab::Smartest` in laravel/ai).\nREGOLO_CHAT_MODEL=Llama-3.3-70B-Instruct\nREGOLO_CHAT_MODEL_CHEAPEST=Llama-3.1-8B-Instruct\nREGOLO_CHAT_MODEL_SMARTEST=Llama-3.3-70B-Instruct\n\n# Embeddings — set KB_EMBEDDINGS_DIMENSIONS to the same value below.\nREGOLO_EMBEDDINGS_MODEL=Qwen3-Embedding-8B\nREGOLO_EMBEDDINGS_DIMENSIONS=4096\n\n# Reranker — used when KB_RERANKING_ENABLED=true.\nREGOLO_RERANKING_MODEL=jina-reranker-v2\n\n# Transport + per-call defaults. `REGOLO_MAX_TOKENS` / `REGOLO_TEMPERATURE`\n# are the provider-level fallbacks; per-call `$options['max_tokens']` /\n# `$options['temperature']` (e.g. `ConversationController::generateTitle`\n# capping titles at 60 tokens) take precedence.\nREGOLO_TIMEOUT=120\nREGOLO_MAX_TOKENS=4096\nREGOLO_TEMPERATURE=0.2\n```\n\n#### Embedding dimension gotcha\n\nIf you change the embeddings provider/model (e.g. from OpenAI 1536-dim to Gemini 768-dim):\n\n1. Update `KB_EMBEDDINGS_DIMENSIONS` in `.env`\n2. Create a new migration that resizes the `embedding` `vector(N)` column on `knowledge_chunks` and `embedding_cache`\n3. Flush the cache so stale-dimension vectors don't pollute retrieval — call `app(\\App\\Services\\Kb\\EmbeddingCacheService::class)-\u003eflush()` (or scope by retired provider with `-\u003eflush('openai')`) from a tinker session. `kb:prune-embedding-cache --days=N` only evicts rows older than N days and returns early when `N \u003c= 0`, so it is **not** a full-flush substitute.\n4. Re-index all documents\n\n### Storage (Laravel disks)\n\nKB markdown files are read through a Laravel filesystem disk, so the ingestion pipeline is **storage-agnostic**: local for dev, S3 for production, MinIO for on-prem — no code change needed.\n\nConfig file: `config/filesystems.php`. The dedicated `kb` disk defaults to `storage/app/kb`:\n\n```env\n# Disk used by kb:ingest and DocumentIngestor (see config/filesystems.php)\nKB_FILESYSTEM_DISK=kb\nKB_DISK_DRIVER=local\n# KB_DISK_ROOT=/absolute/path/to/markdown/root\n\n# Optional path prefix prepended to every ingested path\nKB_PATH_PREFIX=\n```\n\n#### Switching to S3\n\nInstall the Flysystem S3 adapter once:\n\n```bash\ncomposer require league/flysystem-aws-s3-v3 \"^3.0\"\n```\n\nThen switch the disk driver and fill the AWS credentials:\n\n```env\nKB_FILESYSTEM_DISK=s3\n\nAWS_ACCESS_KEY_ID=...\nAWS_SECRET_ACCESS_KEY=...\nAWS_DEFAULT_REGION=eu-west-1\nAWS_BUCKET=askmydocs-kb\nAWS_URL=\nAWS_ENDPOINT=              # set for MinIO / R2 / Wasabi\nAWS_USE_PATH_STYLE_ENDPOINT=false\n```\n\n#### Ingesting a document\n\n```bash\n# Reads storage/app/kb/docs/setup.md (local disk)\nphp artisan kb:ingest docs/setup.md --project=erp-core --title=\"Installation Guide\"\n\n# Override the disk ad-hoc\nphp artisan kb:ingest docs/setup.md --project=erp-core --disk=s3\n```\n\n### Chat Logging\n\nChat logging is **off by default**. Enable it to get structured analytics about every Q\u0026A turn.\n\nConfig file: `config/chat-log.php`\n\n```env\nCHAT_LOG_ENABLED=true\nCHAT_LOG_DRIVER=database\nCHAT_LOG_DB_CONNECTION=      # optional: dedicated DB connection\nCHAT_LOG_RETENTION_DAYS=90   # scheduler rotates rows older than N days\n```\n\n#### Fields persisted per interaction\n\n| Field | Description |\n|---|---|\n| `session_id` | Session UUID (from `X-Session-Id` header or auto-generated) |\n| `user_id` | Authenticated user id (nullable) |\n| `question` | User question |\n| `answer` | Assistant response |\n| `project_key` | Project key used as RAG filter |\n| `ai_provider` | openai / anthropic / gemini / openrouter / regolo |\n| `ai_model` | Specific model used |\n| `chunks_count` | Number of retrieved context chunks |\n| `sources` | Source document paths that contributed context |\n| `prompt_tokens` / `completion_tokens` / `total_tokens` | Token usage |\n| `latency_ms` | End-to-end latency |\n| `client_ip` / `user_agent` | Client metadata |\n| `extra` | JSON for custom fields (e.g. `few_shot_count`) |\n\nLogging is wrapped in try/catch — a driver failure never breaks the user response.\n\n### Knowledge Base\n\nConfig file: `config/kb.php`\n\n```env\nKB_EMBEDDINGS_DIMENSIONS=1536\nKB_MIN_SIMILARITY=0.30\nKB_DEFAULT_LIMIT=8\n\n# Chunking\nKB_CHUNK_TARGET_TOKENS=512\nKB_CHUNK_HARD_CAP_TOKENS=1024\nKB_CHUNK_OVERLAP_TOKENS=64\n\n# Embedding cache\nKB_EMBEDDING_CACHE_ENABLED=true\nKB_EMBEDDING_CACHE_RETENTION_DAYS=30\n```\n\n### `GET /api/kb/documents/search` (v3.0+)\n\nDocument title/path autocomplete used by the chat composer's `@mention` popover (T2.7/T2.8). Sanctum-protected.\n\n**Query params:**\n\n- `q` — search string (2-120 chars, escaped for `LIKE` wildcards via `\\` + `ESCAPE '\\\\'` clause per R19; literal `_` and `%` in the query do NOT act as wildcards)\n- `project_keys[]` — optional tenant scope (zero or more)\n\n**Response:** `{ \"data\": [{ \"id\", \"project_key\", \"title\", \"source_path\", \"source_type\", \"canonical_type\" }] }`\n\nUp to 20 results per request. Archived documents are excluded.\n\n### Saved filter presets (v3.0+)\n\nAuthenticated users can save / load / delete personal filter combinations via `RESTful /api/chat-filter-presets` (consumed by the FE FilterBar dropdown — UI work in a follow-up FE PR).\n\n- `GET    /api/chat-filter-presets` — list the user's presets (alphabetical by name).\n- `POST   /api/chat-filter-presets` — create. Required body: `{ \"name\": \"…\", \"filters\": { … } }`. Per-user uniqueness enforced on `name` (422 on duplicate within the same account). Different users may pick the same display name independently.\n- `GET    /api/chat-filter-presets/{id}` — show one. Returns `404` for IDs owned by a different user (deliberate — the API does not leak the existence of other users' presets).\n- `PUT    /api/chat-filter-presets/{id}` — update name + filters; same `404` semantics for non-owned rows.\n- `DELETE /api/chat-filter-presets/{id}` — delete; `204` on success, `404` for non-owned rows.\n\nThe `filters` JSON column carries a serialised RetrievalFilters payload — the same shape the chat controller's `KbChatRequest::toFilters()` consumes. Round-trip is lossless: load preset → POST to `/api/kb/chat` produces identical retrieval scope as if the user had re-selected every filter manually.\n\n### Chat filters (v3.0+)\n\n`POST /api/kb/chat` accepts an optional `filters` object that narrows the retrieval scope BEFORE reranking + graph expansion + rejected-approach injection — filters change the candidate population, not the post-hoc ranking. Every dimension is optional.\n\n```json\n{\n  \"question\": \"What is our cache invalidation policy?\",\n  \"filters\": {\n    \"project_keys\": [\"hr-portal\", \"engineering\"],\n    \"tag_slugs\": [\"policy\", \"security\"],\n    \"source_types\": [\"markdown\", \"pdf\"],\n    \"canonical_types\": [\"decision\", \"runbook\"],\n    \"connector_types\": [\"local\", \"google-drive\"],\n    \"doc_ids\": [42, 99],\n    \"folder_globs\": [\"hr/policies/**\"],\n    \"date_from\": \"2026-01-01\",\n    \"date_to\": \"2026-12-31\",\n    \"languages\": [\"it\", \"en\"]\n  }\n}\n```\n\nField semantics:\n\n- `project_keys` — multi-tenant scope; takes precedence over the legacy `project_key` field when both are sent.\n- `tag_slugs` — match documents tagged with ANY listed slug (T2.3 join, ships in a follow-up).\n- `source_types` — one of `markdown`, `text`, `pdf`, `docx` (validated against `App\\Support\\Kb\\SourceType` so adding a new type extends the validator automatically).\n- `canonical_types` — one of the `App\\Support\\Canonical\\CanonicalType` enum values currently stored on `knowledge_documents.canonical_type`: `decision`, `module-kb`, `runbook`, `standard`, `incident`, `integration`, `domain-concept`, `rejected-approach`, `project-index`. The validator is built from `CanonicalType::cases()` so adding a new case auto-extends the accepted set.\n- `connector_types` — connector identifier strings (for example `local`, `google-drive`, `onedrive`, `notion`, `asana`, `imap`). Accepted in v3.0 but currently a no-op in retrieval until the `connector_type` column is added in v3.1.\n- `doc_ids` — explicit document-id allowlist (used by the `@mention` UI in the chat composer, T2.7).\n- `folder_globs` — path globs against `source_path`. `*` matches a single segment (does NOT cross `/`), `**` matches across segments (e.g. `hr/policies/**` matches `hr/policies/leave.md` AND `hr/policies/inner/leave.md`), `?` matches a single char (not `/`). Applied PHP-side after the SQL pre-filter via `App\\Support\\KbPath::matchesAnyGlob` (PostgreSQL has no native fnmatch and `**` doesn't translate to LIKE cleanly).\n- `date_from` / `date_to` — ISO 8601 date range against `indexed_at`. `date_to` must be after-or-equal to `date_from`.\n- `languages` — ISO 639-1 codes (normalized to lowercase during DTO construction; the validator enforces `size:2`).\n\nPre-T2.2 callers using the legacy `{question, project_key}` payload keep working unchanged — internally `project_key` is wrapped into `filters.project_keys = [project_key]`. The response `meta.filters_selected` echoes the count of user-selected filter dimensions for the FE composer to render \"5 filters selected\".\n\n### Multi-format ingest (v3.0+)\n\n`kb:ingest-folder` now picks up `.md`, `.markdown`, `.txt`, `.pdf`, and `.docx` files automatically (default `--pattern` is the union of every supported extension). Operators who want pre-T1.8 markdown-only behavior pass `--pattern=md,markdown` explicitly.\n\nThe `POST /api/kb/ingest` endpoint accepts an optional `mime_type` field per document (defaults to `text/markdown` for back-compat). Binary formats (`application/pdf`, `application/vnd.openxmlformats-officedocument.wordprocessingml.document`) require `documents.*.content` to be **base64-encoded**; the controller decodes-or-422 before writing to disk. Text MIMEs (`text/markdown`, `text/x-markdown`, `text/plain`) keep accepting raw content. Unsupported MIME types return 422 with an actionable error naming the supported set.\n\nThe `App\\Support\\Kb\\SourceType` enum is a typed helper for the markdown/text/pdf/docx domain — `SourceType::fromMime()` and `SourceType::fromExtension()` are the canonical conversions used by the API controller and the folder walker. The actual ingest routing is config-driven via `config/kb-pipeline.php` (`converters` / `chunkers` / `mime_to_source_type`); adding a new format requires updating BOTH `config/kb-pipeline.php` AND `SourceType::fromMime()` / `fromExtension()` / `toMime()` / `supportedMimes()` so the API/CLI surfaces stay consistent with what the registry resolves.\n\n### Extending the Ingestion Pipeline\n\nAskMyDocs v3.0 introduces a pluggable ingestion pipeline driven by `config/kb-pipeline.php`. To add support for a new file format:\n\n1. **Implement** `App\\Services\\Kb\\Contracts\\ConverterInterface` — convert raw bytes to a `ConvertedDocument` (markdown + extraction metadata). Every converter MUST populate `extractionMeta['filename'] = basename($doc-\u003esourcePath)` so the chunker can attribute chunks back to their source file.\n2. **Implement** `App\\Services\\Kb\\Contracts\\ChunkerInterface` — or reuse `MarkdownChunker` if your converter outputs markdown (the default for prose formats).\n3. **Register** in `config/kb-pipeline.php` under `converters` and `chunkers`.\n4. **Map** the MIME type in `mime_to_source_type` so the pipeline can route to the right chunker.\n\nBuilt-in converters (v3.0):\n\n- `MarkdownPassthroughConverter` — `text/markdown`, `text/x-markdown`\n- `TextPassthroughConverter` — `text/plain` (wraps prose in a `# {basename}` header so MarkdownChunker can section it)\n- `PdfConverter` — `application/pdf` (smalot/pdfparser primary; falls back to `pdftotext` from Poppler when smalot rejects the file)\n- `DocxConverter` — `application/vnd.openxmlformats-officedocument.wordprocessingml.document` (parses the `.docx` package via `phpoffice/phpword`; maps `Heading{N}` paragraph styles to `#{×N+1}` markdown headings nested under the basename H1; tables become markdown pipe-tables. Embedded images are NOT extracted in v3.0 — planned for v3.1 with the vision-LLM pipeline.)\n\n**PDF support:** `smalot/pdfparser` is a hard `require` (pure PHP, no system deps). For more robust extraction on complex PDFs (multi-column layouts, certain XFA forms, mixed encodings), install `poppler-utils` on the host (`apt install poppler-utils` on Debian/Ubuntu, `brew install poppler` on macOS) — the `PdfConverter` automatically falls back to the `pdftotext` binary when smalot raises an exception. `extractionMeta.extraction_strategy` records which strategy was used per document so you can audit the rate of fallbacks in production.\n\nBuilt-in chunkers (v3.0):\n\n- `PdfPageChunker` — handles `pdf` source-type. Slices on the `## Page N` heading boundaries emitted by `PdfConverter`; emits one chunk per non-empty page with `heading_path = \"Page N\"` so citations like \"see page N of foo.pdf\" map 1:1 to a single chunk row. Pages exceeding `KB_CHUNK_HARD_CAP_TOKENS` are split intra-page on `\\n\\n` paragraph boundaries; all pieces of the same page share the same `heading_path` so page-level citations still resolve cleanly.\n- `MarkdownChunker` — handles `markdown`, `md`, `text`, `docx` source types (any source whose converter outputs markdown). Uses `section_aware` mode: emits one chunk per ATX heading section with `heading_path` as a `\u003e`-joined breadcrumb of H1-H3 ancestors. Falls back to `paragraph_split` (one chunk per blank-line-separated block) for documents without headings.\n\nThe chunker registry is order-significant — `PdfPageChunker` is listed FIRST in `config/kb-pipeline.php`'s `chunkers` so the first-match-wins resolution prefers it for `pdf` over the markdown fallback.\n\nThe polymorphic entry point is `DocumentIngestor::ingest(string $projectKey, SourceDocument $source, string $title, array $extraMetadata = [])`. The pre-v3 `ingestMarkdown(...)` is now a thin facade that synthesises a `text/markdown` `SourceDocument` and delegates to `ingest()` — IngestDocumentJob and the GitHub Action keep working unchanged.\n\n### Multi-tenant deployment (v4.0)\n\nThe v4.0 cycle adds a **per-request tenant context** that scopes every Eloquent query against tenant-aware tables (R30/R31). Existing v3.x deployments are backward-compatible — every row gets `tenant_id = 'default'` and the resolver returns `'default'` unless explicitly configured otherwise.\n\n**The plumbing**\n\n| Piece | Path | Responsibility |\n|---|---|---|\n| `TenantContext` | `app/Support/TenantContext.php` | Request-scoped singleton; holds the active `tenant_id` for the duration of one HTTP request or one CLI command |\n| `ResolveTenant` middleware | `app/Http/Middleware/ResolveTenant.php` | Reads the tenant from the configured resolver and sets `TenantContext`; runs at the top of the global middleware stack so every controller / job dispatched from the request inherits the context |\n| `BelongsToTenant` trait | `app/Models/Concerns/BelongsToTenant.php` | Auto-fills `tenant_id` on `creating` events from `TenantContext::current()`; provides `forTenant($id)` query scope |\n| `--tenant=X` CLI option | every domain Artisan command | CLI commands (`kb:ingest-folder`, `kb:rebuild-graph`, `kb:promote`, `insights:compute`) accept the option and set the context before running |\n\n**Configuration (`.env`)**\n\n```bash\n# Single-tenant deployment (v3.x backward compatible — DEFAULT)\nTENANT_DEFAULT=default\nTENANT_RESOLVER=default          # always returns 'default'\n\n# Multi-tenant by HTTP header (suitable for B2B SaaS with API gateway routing)\nTENANT_RESOLVER=header\nTENANT_HEADER_NAME=X-Tenant-ID\n\n# Multi-tenant by domain (suitable for subdomain-per-customer deployments)\nTENANT_RESOLVER=domain\nTENANT_DOMAIN_PATTERN='([^.]+)\\\\.example\\\\.com'   # captures tenant slug\n\n# Multi-tenant by authenticated user (suitable for shared-host SaaS)\nTENANT_RESOLVER=auth\nTENANT_USER_COLUMN=tenant_id     # column on the User model that holds the tenant\n```\n\n**What's tenant-scoped (and what isn't)**\n\nThe 20 tenant-aware models (enumerated in `tests/Architecture/TenantIdMandatoryTest::TENANT_AWARE_MODELS` — `KnowledgeDocument`, `KnowledgeChunk`, `ChatLog`, `Conversation`, `Message`, `KbNode`, `KbEdge`, `KbCanonicalAudit`, `ProjectMembership`, `KbTag`, `KnowledgeDocumentAcl`, `AdminCommandAudit`, `AdminCommandNonce`, `AdminInsightsSnapshot`, `ChatFilterPreset`, `ChatLogProvenance`, `TabularReview`, `TabularCell`, `Workflow`, `HiddenWorkflow`) all carry `tenant_id` and use the `BelongsToTenant` trait. The architecture test gates new models on every CI run so this list stays in lock-step with the migrations. Composite tenant-scoped FKs on `kb_edges` make cross-tenant edges **structurally impossible** at the database level.\n\n`embedding_cache` is **intentionally NOT tenant-scoped** — the cache is a cross-tenant reuse layer keyed on `text_hash` UNIQUE alone (provider + model are retrieval-time filters). Sharing embeddings across tenants is a deliberate cost optimisation; eviction goes through `EmbeddingCacheService::flush($provider)` whenever the embedding model changes.\n\n**The 6 v4 cycle rules guard the boundary**\n\n| Rule | What it enforces |\n|---|---|\n| **R30** | Every Eloquent query against a tenant-aware table MUST be scoped to the active tenant via `forTenant()` or explicit `where('tenant_id', $ctx-\u003ecurrent())` — cross-tenant leak is a GDPR catastrophe |\n| **R31** | Every tenant-aware model MUST `use BelongsToTenant;` and list `'tenant_id'` in `$fillable`; `tests/Architecture/TenantIdMandatoryTest.php` enumerates the model list and gates new entries on every CI run |\n| **R36** | Mandatory Copilot review + CI green loop on every PR — caught the v4 PR #98 regression where `embedding_cache` was wrongly tagged tenant-scoped |\n| **R37** | `feature/vX.Y` integration branch + once-per-major merge to main — preserves stable consumers from in-flight major work |\n| **R38** | Heavy work (`migrate:fresh`, big seeders) belongs in CLI workflow steps, not behind `php artisan serve` — keeps E2E reliable |\n| **R39** | Tag `vX.Y.0-rcN` at every Wn weekly milestone closure pinned to the exact closure SHA — gives auditors and downstream consumers serialised milestone visibility |\n---\n\n## Features by area\n\nSix grouped feature tables. Every entry is verifiable against the\ncodebase (see [`CLAUDE.md`](CLAUDE.md) section 3 for the component map,\nthe per-cycle STATUS docs under [`docs/v4-platform/`](docs/v4-platform/),\nand the ADR set under [`docs/adr/`](docs/adr/)).\n\n### Retrieval \u0026 Knowledge\n\n| Feature | Description | Since |\n|---|---|---|\n| Hybrid retrieval (pgvector + FTS + reranker) | Vector top-K (pgvector cosine) fused with full-text top-K (PostgreSQL `to_tsvector` GIN index) via Reciprocal Rank Fusion; `Reranker` runs `0.55·vec + 0.25·kw + 0.05·heading` on top of 3× over-retrieval | v1.0 |\n| Multi-format ingestion pipeline | `markdown`, `text`, **PDF** (`smalot/pdfparser` + Poppler fallback), **DOCX** (`phpoffice/phpword`) — all converge on `DocumentIngestor::ingest(SourceDocument)` via the `PipelineRegistry` (R23: FQCN validated at boot, `supports()` mutex-checked) | v3.0 |\n| Canonical knowledge graph (9 node types / 10 edge types) | `kb_nodes` + `kb_edges` with `decision` / `runbook` / `standard` / `incident` / `integration` / `domain-concept` / `module-kb` / `rejected-approach` / `project-index` nodes and `depends_on` / `uses` / `implements` / `related_to` / `supersedes` / `invalidated_by` / `decision_for` / `documented_by` / `affects` / `owned_by` edges. Tenant-scoped composite FKs make cross-tenant edges structurally impossible | v3.0 |\n| `CanonicalParser` (9 canonical types / 6 statuses) | YAML frontmatter parser via `symfony/yaml`; validates `type`, `status`, `slug`, `retrieval_priority` in `[0, 100]`. Invalid frontmatter degrades gracefully to non-canonical (R4) | v3.0 |\n| `GraphExpander` 1-hop graph expansion at retrieval | Walks `kb_edges` from canonical seed docs at retrieval time, returns best chunk per neighbour; config-gated via `KB_GRAPH_EXPANSION_ENABLED=true` (default); degrades to no-op when no canonical docs exist | v3.0 |\n| `RejectedApproachInjector` anti-repetition memory | Vector-correlates the query against `rejected-approach` canonical docs above `KB_REJECTED_MIN_SIMILARITY` (default 0.45); top-N (default 3) injected into the prompt under `⚠ REJECTED APPROACHES`; the LLM sees dismissed options *before* answering | v3.0 |\n| Promotion pipeline (`suggest` / `candidates` / `promote`) | Three-stage human-gated API (ADR 0003): `/suggest` extracts candidates via LLM (writes nothing), `/candidates` validates a draft (writes nothing), `/promote` writes markdown + dispatches `IngestDocumentJob` (HTTP 202). Only humans and `kb:promote` CLI commit canonical storage | v3.0 |\n| Idempotent SHA-256 ingestion | Composite UNIQUE on `(project_key, source_path, version_hash)`; re-pushing identical bytes is a no-op; a new version archives the prior; `$tries=3` with backoff `[10, 30, 60]` on `IngestDocumentJob` | v1.0 |\n| `MarkdownChunker` section-aware fence-safe FSM | Custom line-based fence-aware state machine: emits one chunk per ATX heading section with `heading_path` breadcrumb (H1\u003eH2\u003eH3); fences (` ``` `, `~~~`) suppress heading detection inside code blocks; falls back to `paragraph_split` on docs without headings | v3.0 |\n| `PdfPageChunker` page-aware PDF chunking | Slices on the `## Page N` boundaries emitted by `PdfConverter`; emits one chunk per non-empty page with `heading_path = \"Page N\"` for page-precise citations; intra-page split on `\\n\\n` when over `KB_CHUNK_HARD_CAP_TOKENS` | v3.0 |\n| Embedding cache (cross-tenant by design) | DB-backed LRU cache keyed on SHA-256(`text`) UNIQUE; eliminates redundant API calls on re-ingestion and repeated queries; `EmbeddingCacheService::flush($provider)` on provider/model change. Conditional approval gate via `KB_EMBEDDING_CACHE_APPROVAL_THRESHOLD` (default 5000) on v4.2+ | v1.0 |\n| Soft delete + retention sweep | `SoftDeletes` on `KnowledgeDocument`; hidden from every read path by default; `kb:prune-deleted` (03:30 daily) hard-deletes after `KB_SOFT_DELETE_RETENTION_DAYS` (default 30); cascades `kb_nodes` + `kb_edges` on final hard delete; immutable `kb_canonical_audit` row survives | v3.0 |\n| MCP server `enterprise-kb` (10 tools) | 5 retrieval tools (`kb.search` / `kb.read_document` / `kb.read_chunk` / `kb.recent_changes` / `kb.search_by_project`) + 5 canonical/promotion tools (`kb.graph.neighbours` / `kb.graph.subgraph` / `kb.documents.by_slug` / `kb.documents.by_type` / `kb.promotion.suggest`) exposed at `/mcp/kb` for Claude Desktop / Claude Code / any MCP-compatible agent | v3.0 |\n| Enterprise chat filters (10 dimensions) | `RetrievalFilters` DTO with `project_keys` / `tag_slugs` / `source_types` / `canonical_types` / `connector_types` / `doc_ids` / `folder_globs` / `date_from` / `date_to` / `languages`. Per-user saved presets with 404-not-403 cross-user isolation; `@mention` doc pinning via cursor-context detection | v3.0 |\n| Reranker canonical boost + status penalty | Reranker applies `priority × 0.003` canonical boost and `superseded −0.4` / `deprecated −0.4` / `archived −0.6` status penalties on top of the vector/keyword/heading fusion; non-canonical chunks get zero adjustment (legacy behaviour preserved) | v3.0 |\n| Source-aware chunkers + rich frontmatter capture | `PipelineRegistry::resolveChunker($sourceType)` dispatches per source (R23 FQCN-validated + `supports()` mutex-checked at boot) to: `NotionBlockChunker` / `ConfluencePageChunker` / `OfficeDocChunker` / `AtomicNoteChunker` / `JiraIssueChunker` / `PdfPageChunker` / `MarkdownChunker`. Document-level metadata carries `connector` + `external_id` + `external_url` + native timestamps; chunk-level metadata carries `source_type` + `search_tags` (top-level) + `recency_bucket` + ACL hint + status + preamble-path | v4.5 |\n| `Reranker` Layer-4 signals (tag overlap, recency, status-active, preamble) | Four additive Layer-4 deltas: `tag_overlap_weight=0.05` + `preamble_match_weight=0.05` + `recency_weight=0.02` + `status_active_weight=0.02`, on top of the base `0.55·vec + 0.25·kw + 0.05·heading`. Max score ~1.44 (documented in code); base 4 signals still sum to 1.0 | v4.5 |\n| `KbSearchService` facets (`source` + `tag`) | `searchWithContext()` accepts optional `facets` param; emits `facets[source]` + `facets[tag]` counts; backed by 2 new GIN-on-`jsonb` indexes (`source_type` + `search_tags`) plus 1 B-tree expression index on `metadata-\u003e\u003e'recency_bucket'` on `knowledge_chunks`, all PostgreSQL-only (SQLite is a no-op) | v4.5 |\n| **Tabular Review** (spreadsheet-style document extraction) | `tabular_reviews` + `tabular_cells` tables; `TabularReviewExtractor` runs ONE multi-column LLM call per document (cost `O(documents)` not `O(documents × columns)`); 17 format types (Mike's 9 + 8 AskMyDocs-new including the LLM-free `json_path` shortcut leveraging v4.5/W5.5 source-aware metadata); R14 loud refusal with red flag + reasoning on no-evidence / LLM error / JSON parse failure; DB-level upsert keyed on the composite UNIQUE `(tenant_id, review_id, document_id, column_index)` prevents duplicate rows under concurrent generate/regenerate. Admin SPA at `/app/admin/tabular-reviews` (list / show / create + grid view with flag-tinted cells + a per-cell flag glyph and inline reasoning text, plus an `aria-label` combining summary + flag + reasoning so AT users get the same context as sighted users — R15); SSE streaming variant `POST /api/admin/tabular-reviews/{id}/generate-stream` is wired end-to-end on the BE and emits per-cell `event: cell` frames, but the v4.7 GA SPA still calls the synchronous `/generate` endpoint — the progressive-paint FE consumer ships in v4.7.x alongside the Glide Data Grid migration (ADR 0010 D1) | v4.7 GA |\n| **Workflows** (reusable prompt templates + AI-suggested catalogue) | `workflows` + `workflow_shares` + `hidden_workflows` tables; `WorkflowService` enforces ownership / share / hide semantics with per-user scope; `WorkflowSuggester` analyzes the tenant's KB (`MetadataPatternAnalyzer` detects recurring practices / projects / column patterns) and proposes up to 5 assistant + tabular workflow drafts via the LLM. 15 system-shipped templates (legal review / GDPR DPIA / DPA review / commercial agreement triage / privacy policy audit / vendor due diligence / employment policy review / regulatory mapping / risk register / litigation timeline / NDA review / IP-licensing review / consent record audit / processor-list extraction / contract-clause comparison). Admin SPA at `/app/admin/workflows` with Mine / Shared / System scope tabs + AI-suggest gallery + create dialog (**assistant type only in GA**; tabular create UI deferred to v4.7.x — tabular workflows ARE accepted by the JSON API and via the AI-suggest gallery's save-this path); email-based share model scales to invitees not yet on the platform | v4.7 GA |\n\n### Chat \u0026 Conversation\n\n| Feature | Description | Since |\n|---|---|---|\n| Vercel AI SDK v6 streaming | `MessageStreamController` emits SDK v6 `UIMessageChunk` frames (`start` / `text-start` / `text-delta` / `text-end` / `source-url` / `data` / `finish`) over SSE; first-token latency dropped from ~2.8 s synchronous to ~400 ms streaming on the Lighthouse baseline | v4.0 |\n| `useChatStream()` React hook | `mapStatusToDataState()` adapter exposes `data-state=\"idle\\|loading\\|ready\\|empty\\|error\"` for deterministic Playwright waits (SDK `submitted` and `streaming` statuses both collapse to `loading` per the R11 comment in `MessageThread.tsx`); unit-tested in `frontend/src/features/chat/map-status-to-data-state.test.ts` | v4.0 |\n| Citations panel | Every assistant reply ships the source documents (`document_id`, `title`, `source_path`, `headings`, `chunks_used`); persisted on `messages.metadata.citations`; survives conversation reload | v1.0 |\n| Conversation history | `conversations` + `messages` tables (user-scoped); inline rename, delete with confirmation, AI-generated title after first turn, full multi-turn history sent to provider on every request | v1.0 |\n| Composite confidence score (0–100) | `ConfidenceCalculator`: `0.40·mean_top_k_sim + 0.20·threshold_margin + 0.20·chunk_diversity + 0.20·citation_density`; renders as `high / moderate / low / refused` tier in the `ConfidenceBadge` | v3.0 |\n| Refusal handling | Two refusal paths: deterministic `no_relevant_context` short-circuit (Mockery `shouldNotReceive('chat')` per R26 proves no LLM call) and `llm_self_refusal` via exact-match-after-trim `__NO_GROUNDED_ANSWER__` sentinel. `RefusalNotice` uses `role=\"status\"` not `alert` (R24) | v3.0 |\n| `@mention` doc pinning | Type `@docname` in the composer → `/api/kb/documents/search` autocomplete → `MentionPopover` with cursor-context detection → pinned `doc_id` forces inclusion in retrieval even when scored below the similarity floor | v3.0 |\n| Filter chips + saved presets | Persistent `FilterBar` with per-dimension removable `FilterChip`s; tabbed `FilterPickerPopover` (Project / Type / Tag / Folder / Date / Language); per-user saved presets at `RESTful /api/chat-filter-presets` (lossless round-trip) | v3.0 |\n| Speech-to-text (Web Speech API) | Browser-native mic input via `webkitSpeechRecognition`; zero external service, zero cost; defaults to `it-IT` (configurable). Chrome / Edge / Safari supported | v1.0 |\n| Few-shot learning loop | Thumbs up/down rating on every assistant message; `FewShotService` retrieves last 3 positively-rated Q\u0026As per user/project and injects as \"Examples of Well-Rated Answers\" in the system prompt | v1.0 |\n| Smart visual artifacts | `~~~chart` JSON blocks render as Chart.js bar/line/pie/doughnut; `~~~actions` JSON renders as copy/download buttons; every code block ships a \"Copy\" button | v1.0 |\n| Multi-provider AI federation | OpenAI / Anthropic / Gemini / OpenRouter via raw `Http::` calls (no SDK); Regolo via the `padosoft/laravel-ai-regolo` SDK adapter on `laravel/ai`; `AiManager::chat()` + `chatStream()` + `embeddings()`; per-provider streaming where supported (all 5 native or via `FallbackStreaming` trait); chat and embeddings providers configured separately | v1.0 |\n| Stateless JSON chat API | `POST /api/kb/chat` synchronous endpoint kept as backward-compat fallback alongside the v4 SSE streaming path; same hybrid retrieval pipeline + refusal short-circuit + confidence score serve both | v1.0 |\n| Stop / regenerate / branch / inline-edit affordances | Vercel AI SDK UI Tier 1 closure: stop-streaming via `AbortController`; regenerate-last-assistant; branch-from-message endpoint (forks the conversation tree); inline-edit user message; copy-code-block. All wired on `MessageStreamController` + the `useChatStream()` hook | v4.5 |\n| Per-message provider/model/cost metadata | Enhanced badge below every assistant message shows `provider`, `model`, `started_at`, prompt + completion tokens, and derived USD cost when `config('ai.cost_rates')` is populated (keyed by `provider → model → {input, output}`); cost is omitted (not zero) when rates are missing. Public lookup at `GET /api/chat/cost-rates` with 1-hour CDN cache | v4.5 |\n| Suggested follow-up pills | `SuggestedFollowupGenerator` derives three follow-up prompts from the assistant's last reply via `AiManager::chat()`; renders as clickable pill chips above the composer; clicking submits via the streaming endpoint. Best-effort — provider error / parse failure / empty response returns `[]` and the row is not rendered. Triggered once on `onFinish` per assistant turn at `POST /conversations/{id}/suggested-followups` | v4.5 |\n\n### Security \u0026 Compliance\n\n| Feature | Description | Since |\n|---|---|---|\n| PII redaction at 11 persistence boundaries | `padosoft/laravel-pii-redactor` v1.2 wired at: (1) chat-message middleware, (2) embedding-cache pre-redact, (3) AI-insights snippet sanitiser, (4) operator detokenize endpoint, (5) Monolog log channel processor, (6) failed-jobs sanitiser via `JobFailed` listener with deterministic UUID match, (7) `Conversation`+`Message` `saving` observers, (8) `ChatLog::creating` observer, (9) `AdminCommandAudit::creating` observer, (10) `AdminInsightsSnapshot::creating` observer (6 JSON columns), (11) Flow `CurrentPayloadRedactorProvider` contract binding (covers run input + step results + audit + webhook outbox + approvals in one wire). All 5 v4.3 env knobs default OFF | v4.3 |\n| Multi-tenant isolation (R30 + R31) | 20 tenant-aware models carry `tenant_id` (enumerated in `tests/Architecture/TenantIdMandatoryTest::TENANT_AWARE_MODELS`); `BelongsToTenant` trait auto-fills from `TenantContext` on `creating`; composite tenant-scoped FK on `kb_edges` makes cross-tenant edges structurally impossible; architecture test `TenantIdMandatoryTest` gates new models | v4.0 |\n| `ResolveTenant` middleware + 4 resolvers | Header (`X-Tenant-ID`), domain regex, authenticated user column, or `'default'` (v3 backward compat); per-request singleton; queue workers re-bind tenant via try/finally restore | v4.0 |\n| Spatie RBAC (5 roles) | `super-admin` / `admin` / `editor` / `viewer` / `dpo` (DPO added in v4.2 for PII admin); permission matrix grouped by dotted-prefix domain; gates wired at controller + route + middleware layer | v3.0 |\n| Sanctum stateful SPA + Bearer tokens | Two transports feed the same guard: cookie-based SPA (`/sanctum/csrf-cookie` + `X-XSRF-TOKEN`) and personal access tokens for API clients / MCP / GitHub Action; `AuthenticateForSse` middleware emits JSON 401 (not HTML redirect) on streaming endpoints | v3.0 |\n| Immutable audit trail | `kb_canonical_audit` records every promote/update/deprecate/hard-delete (no `updated_at`, no FK to docs — survives hard deletes for forensic access); `admin_command_audit` stamps every destructive maintenance run with started/completed/failed timestamps + output/error capture | v3.0 |\n| DB-backed single-use confirm tokens for destructive commands | `AdminCommandNonce` table; signed `confirm_token` issued at preview, consumed inside `DB::transaction` with `lockForUpdate()` + `update()` in the same closure (R21 atomic invariant); composite UNIQUE on `(token_hash, consumed_at)` | v3.0 |\n| 6-gate Artisan whitelist runner | `CommandRunnerService` enforces: (1) whitelist lookup in `config('admin.allowed_commands')`, (2) args_schema validation, (3) confirm_token + single-use nonce, (4) Spatie permission gate (`commands.run` admin / `commands.destructive` super-admin), (5) audit-before-execute, (6) per-user `throttle:10,1` rate limit | v3.0 |\n| 2FA stub | `TwoFactorController` skeleton behind `AUTH_2FA_ENABLED=false` for future TOTP rollout | v3.0 |\n| Operator detokenize endpoint | `POST /api/admin/logs/chat/{id}/detokenize` round-trips a tokenised chat-log row back to original PII text; 422 when strategy ≠ `tokenise`; 403 when caller lacks `kb.pii_redactor.detokenize_permission` (default `pii.detokenize`); every 200/403 writes `admin_command_audit` row | v4.1 |\n| GDPR-aware soft delete + retention | `KB_SOFT_DELETE_ENABLED=true` (default); `KB_SOFT_DELETE_RETENTION_DAYS` (default 30); `kb:prune-deleted` (03:30 daily) hard-deletes file on disk + chunks + audit-trails the deprecation | v3.0 |\n| CSRF + CORS hardening | `SANCTUM_STATEFUL_DOMAINS` + `CORS_ALLOWED_ORIGINS`; wildcard `*` forbidden because `supports_credentials=true`; whitelist-driven origin parsing with whitespace-safe CSV (R19) | v3.0 |\n\n### Admin \u0026 Operations\n\n| Feature | Description | Since |\n|---|---|---|\n| Admin SPA shell (`/app/admin/*`) | React 18+ (React 19 since v4.3) + TypeScript + Vite + TanStack Router/Query + shadcn/ui; dark-first glassmorphism; code-split routes (~400 KB initial gzipped); RBAC-gated via Spatie; sidebar visibility enforced server-side | v3.0 |\n| Dashboard KPIs + health | 6 KPIs (docs / chunks / chats / p95 latency / cache hit rate / canonical coverage) + 6 health probes (db / pgvector / queue / kb-disk / embeddings / chat) + 3 code-split recharts cards (chat volume area, token burn stacked, rating donut) + top projects + activity feed; 30s `Cache::remember` layer keyed by kind+project+days | v3.0 |\n| Users + Roles + Memberships | Filterable users table with soft-delete + restore; 3-tab edit drawer (Details / Roles / Memberships with `scope_allowlist` JSON editor); Spatie-backed role CRUD with grouped permission matrix; `project_memberships` rows scope canonical visibility per project | v3.0 |\n| KB Explorer (tree + 5 right-panel tabs) | Memory-safe `chunkById(100)` tree walker with canonical-aware modes (`canonical \\| raw \\| all`, `with_trashed=0\\|1`); right-panel tabs Preview (remark-rendered + frontmatter pills) / Meta (canonical grid + AI tags) / **Source** (CodeMirror 6 editor with PATCH `/raw` → validate → write → audit → re-ingest) / **Graph** (1-hop tenant-scoped subgraph, SVG radial, ≤ 50 nodes) / **History** (paginated `kb_canonical_audit`) | v3.0 |\n| PDF export (Browsershot + Dompdf fallback) | `PdfRenderer` interface with `BrowsershotPdfRenderer` primary (full CSS / fonts / charts) and `DompdfPdfRenderer` fallback (no headless Chromium dependency); A4 print-optimised; renderer chosen at controller level (R23 registry mutex) | v3.0 |\n| Log viewer (5 tabs) | Five deep-linkable tabs (`?tab=chat\\|audit\\|app\\|activity\\|failed`): chat logs with model/project/rating filters; canonical audit trail with event-type/actor filters; reverse-seek `SplFileObject`-powered application log tailer (whitelist regex, 2000-line cap, optional live polling via `?live=1`); Spatie activity log; failed-jobs read-only table | v3.0 |\n| Maintenance command runner | Three-step React wizard (Preview → Confirm with type-in for destructive → Run → Result); whitelist + args_schema + confirm_token + Spatie gates + audit + throttle (see Security row); scheduler widget reports next run of every queued command | v3.0 |\n| AI insights panel | Daily `insights:compute` (05:00 UTC) writes one row to `admin_insights_snapshots`; six widget cards (Promotion Suggestions / Orphan Docs / Suggested Tags / Coverage Gaps / Stale Docs / Quality Report) read from JSON columns; O(1) DB read, zero LLM calls per page load | v3.0 |\n| Per-user notification feed (bell + panel + API) | Top-bar `\u003cNotificationBell /\u003e` polls `/api/notifications/unread-count` every 30s (R11 `data-state` + `aria-busy`); `/app/admin/notifications` full panel with `unread\\|read\\|dismissed\\|all` tabs, BE-derived event-type filter (R18 — `GET /api/notifications/event-types`), pagination, per-row mark-read/dismiss, bulk mark-all-read scoped to the active filter; HMAC-signed one-click email unsubscribe; channels (`in_app`, `email`) ship as part of v8.0/W1.3, joined by **W2.1** external channels `discord` + `slack` + `teams` + generic `webhook` (all default-OFF — opt in by setting the corresponding `NOTIFICATIONS_DISCORD_URL` / `NOTIFICATIONS_SLACK_URL` / `NOTIFICATIONS_TEAMS_URL` / `NOTIFICATIONS_WEBHOOK_URL` env var; the generic webhook channel additionally signs every request with `X-AskMyDocs-Signature: sha256=\u003chmac\u003e` when `NOTIFICATIONS_WEBHOOK_SECRET` is set). External-channel sends route through the queueable `SendExternalNotificationJob` with `[5, 30, 120]s` backoff (R14 — terminal failure recorded on the row's `channel_dispatch_log`); 4xx responses (except 429) are surfaced as `failed` immediately without retry. Per-user `notification_preferences` matrix wired in v8.0/W2; daily `notifications:prune` 04:10 retains rows for `NOTIFICATIONS_RETENTION_DAYS` (default 90, set 0 to disable) — see env block below. R21 atomic mark-read + dismiss (`whereNull('read_at')-\u003eupdate(...)` + COALESCE); R30 cross-tenant isolation enforced on every endpoint including mutations; presenter strips forensic `channel_dispatch_log` + `tenant_id` + `user_id` from the FE feed. | v8.0 |\n| Cross-mounted admin SPAs (3 packages) | `padosoft/laravel-pii-redactor-admin` v1.0.2 at `/admin/pii-redactor` (cross-mount since v4.4/W2) + `padosoft/laravel-flow-admin` v1.0.0 at `/admin/flows` (iframe — Blade+Alpine, will remain iframed per ADR 0005) + `padosoft/eval-harness-ui` v1.0.0 at `/admin/eval-harness` non-prod-only (cross-mount since v4.4/W3, 3 fail-closed fences preserved) | v4.2 |\n| Laravel scheduler (13+ entries) | `kb:prune-embedding-cache` 03:10 / `chat-log:prune` 03:20 / `kb:prune-deleted` 03:30 / `kb:rebuild-graph` 03:40 / `queue:prune-failed` 04:00 / **`notifications:prune` 04:10 (v8.0/W1.5, default 90d retention via `NOTIFICATIONS_RETENTION_DAYS`; set 0 to disable)** / `admin-audit:prune` 04:30 / `kb:prune-orphan-files` 04:40 / `admin-nonces:prune` 04:50 / `insights:compute` 05:00 / `eval:nightly` 05:30 (v4.3+, default OFF); all `onOneServer()-\u003ewithoutOverlapping()`. **v8.0/W2.4 — every slot's cron + enabled flag is now env-tunable** via the 24 `SCHEDULE_*_CRON` / `SCHEDULE_*_ENABLED` knobs (see `.env.example` Tier-1 scheduler section); defaults preserve the overnight rotation above byte-for-byte. The `GET /api/admin/commands/scheduler-status` widget surfaces the effective cron times after env overrides. | v3.0 |\n| Sidebar gating + R29 testid hierarchy | Sidebar entries always rendered, visibility enforced server-side via per-route fences (RequireRole + middleware `can:` + env `abort(404)`); every actionable element uses `feature-resource-{id}-{action[-substep]}` testid convention for Playwright stability | v3.0 |\n| Connector admin SPA (`/app/admin/connectors`) | React DataTable with per-connector install/uninstall flow; OAuth callback handler at `/app/admin/connectors/$key/callback`; per-installation `connector_installations` + `connector_credentials` rows (encrypted via `OAuthCredentialVault`); scheduler-driven `ConnectorSyncJob`; Spatie `manageConnectors` super-admin gate at controller + route layer | v4.5 |\n\n### Integrations \u0026 Extensibility\n\n| Feature | Description | Since |\n|---|---|---|\n| MCP server (inward, 10 tools) | `enterprise-kb` server at `/mcp/kb` exposes the KB to Claude Desktop / Claude Code / any MCP-compatible agent (5 retrieval + 5 canonical/promote tools); `auth:sanctum` + `throttle:api` | v3.0 |\n| GitHub composite action `ingest-to-askmydocs` (v2) | Reusable action with diff-mode (every push: `git diff --diff-filter=AMR` ingest + `D`+`R` delete batches via `DELETE /api/kb/documents`) and full-sync mode; canonical-folder aware; max 100 docs / batch; `--rawfile` for ARG_MAX safety (R5) | v3.0 |\n| 9 registered Flow definitions (saga / compensation) | `kb.ingest` (5-step) / `kb.canonical-index` (3-step) / `kb.promote` (4-step approval-gated, first use of `approval-gate` primitive) / `kb.delete` (4-step) / `kb.prune-deleted` / `kb.prune-embedding-cache` (conditional approval gate) / `kb.prune-chat-logs` / `kb.rebuild-graph` / `kb.ingest-folder` (3-step fan-out). Reverse-order compensation chains; persisted to `flow_runs` + `flow_steps` + `flow_audit` + `flow_approvals` + `flow_webhook_outbox` | v4.2 |\n| Multi-AI-provider abstraction | OpenAI / Anthropic / Gemini / OpenRouter via raw `Http::` (no SDK); Regolo via the `padosoft/laravel-ai-regolo` SDK adapter on `laravel/ai`; `FallbackStreaming` trait synthesises single-chunk SSE for providers without native streaming | v1.0 |\n| Pluggable ingestion pipeline | 3 contracts (`ConverterInterface` / `ChunkerInterface` / `EnricherInterface`); `PipelineRegistry` with FQCN-validated-at-boot + `supports()` mutex (R23); add a new format = implement 3 interfaces + register in `config/kb-pipeline.php` | v3.0 |\n| Pluggable chat-log driver | `ChatLogDriverInterface`; `database` driver shipped; BigQuery / CloudWatch are extension points via `ChatLogManager::resolveDriver()` | v1.0 |\n| Sister `padosoft/*` package stack | `laravel-ai-regolo` v1.0 (Regolo provider for `laravel/ai`) + `laravel-pii-redactor` v1.2 (PII detection with EU country packs: Italy + Germany + Spain) + `laravel-pii-redactor-admin` v1.0.2 + `laravel-flow` v1.0 (saga engine + approval gates + webhook outbox + replay) + `laravel-flow-admin` v1.0.0 + `eval-harness` v1.2 (golden datasets + 7 metrics + cohorts + adversarial + LLM-as-judge) + `eval-harness-ui` v1.0.0 — every package MIT, every architecture test enforces standalone-agnostic invariants (zero refs to `KnowledgeDocument` / `kb_*` tables / `lopadova/askmydocs` in `src/`) | v4.2 |\n| External Patent Box dossier tool | `padosoft/laravel-patent-box-tracker` v0.1 generates audit-grade Italian Patent Box dossiers; **deliberately NOT in AskMyDocs `composer.json`** — operators install it in a separate Laravel project (R37 standalone-agnostic) and consume `tools/patent-box/2026.yml` from this repo. Commercialista-validated 2026-05-02 | v4.0 |\n| Connector framework + 7 native connectors | Plugin/package architecture (`ConnectorInterface` 10-method contract + `BaseConnector` + `OAuthCredentialVault` + `ConnectorRegistry` with R23 FQCN-validated discovery via `config/connectors.php::built_in` OR `composer.json::extra.askmydocs.connectors`). 7 native connectors: `google-drive` + `notion` + `evernote` + `fabric` + `onedrive` + `confluence` + `jira` (all inline for v4.5; extracted to `padosoft/askmydocs-connector-*` packages in v4.6 per ADR 0008 D1) | v4.5 |\n| **MCP client framework** | AskMyDocs as MCP **CLIENT** (outward direction) — tenant-scoped `McpServerRegistry` + `McpToolCallingService` orchestrates multi-turn tool-calling loops (max 3 iterations, configurable); `McpToolAuthorizer` gates per-user/per-server/per-tool access; v7.0/W6.3.B retired the v5.0 Node sidecar and now drives JSON-RPC directly over native HTTP / SSE / stdio transports via `padosoft/askmydocs-mcp-pack`; `McpHandshakeService` persists initialize+tools/list under `mcp_servers.handshake_response_json`; immutable audit trail in `mcp_tool_call_audit` (with `transport_error` status when the upstream connection is unreachable but not timing out); admin API for server CRUD + handshake + tool-list management; `AI_AGENTIC_ENABLED` master switch; OpenAI + OpenRouter providers wire tool schemas automatically | v5.0 |\n| **MCP admin web panel** (optional companion) | Standalone Laravel package `padosoft/askmydocs-mcp-pack-admin` ships a React SPA that cross-mounts under `/admin/mcp-pack` and surfaces every MCP-side capability above through 12 routes (Dashboard, Servers list + new-server wizard, per-server detail with 7 tabs, Tools matrix + try-it, Resources tree, Prompts playground, Audit log + drilldown, Circuit breakers, OpenAPI explorer, Settings, Help). **v1.1.0** (shipped 2026-05-18) drives the full live `padosoft/askmydocs-mcp-pack` v1.5+ REST surface end-to-end — 22 typed endpoints, 23 TanStack Query hooks across read+write paths, R21 two-call confirm-token protocol on tool invoke / audit replay / breaker reset, SSE live-feed consumer, 154 Vitest specs covering every binding. Composer-discoverable, RBAC-gated, dark+light themed — see [Optional: mount the MCP admin web panel](#optional-mount-the-mcp-admin-web-panel) | v7.0 |\n\n### Quality \u0026 Observability\n\n| Feature | Description | Since |\n|---|---|---|\n| RAG regression CI gate | `.github/workflows/rag-regression.yml` triggers on every PR touching `app/Services/Kb/**` / `app/Ai/**` / `app/Eval/**` / `tests/Eval/golden/**` / `composer.lock`. Drives the golden Q\u0026A set through the LIVE `KbSearchService` + `GraphExpander` + `RejectedApproachInjector` + `AiManager::chat()` against the seeded `DemoSeeder` corpus; fails the build on regression; 14-day artifact retention | v4.2 |\n| 4 eval datasets × per-lane metric stacks | 1 baseline (42 samples, 4 metrics: `contains` + `cosine-embedding` + custom `CosineGroundednessMetric` + custom `CitationGroundednessMetric`) + 3 adversarial cohorts (12 samples each: out-of-corpus refusal / contradicting claims / rejected-approach trigger — 3 metrics: `contains` + `refusal-quality` + `CitationGroundednessMetric`). Cohorts: `source_type × canonical_type × language × query_complexity` | v4.2 |\n| Custom RAG-specific metrics | `CosineGroundednessMetric` (cosine of answer-vs-cited-chunk-text — catches \"fluent answer that doesn't track its own citations\") and `CitationGroundednessMetric` (every expected `source_path` must appear; phantom citations cap score at 0.5; refusal-with-citations drops to 0) | v4.2 |\n| `eval:nightly` cron with LLM-as-judge | Default-OFF via `EVAL_NIGHTLY_ENABLED`; three-fence cost guard (enable flag + `EVAL_NIGHTLY_LIVE` provider-key check + key presence check inside the command); R26 defense-in-depth test pre-seeds both flags + asserts `Http::assertNothingSent()`; persisted `\u003cdate\u003e.json` + `\u003cdate\u003e.md` artefacts; regression detection vs prior baseline; `Log::alert()` + `\u003cdate\u003e.alert.json` sidecar on regression \u003e `EVAL_NIGHTLY_REGRESSION_THRESHOLD` (default 0.05); auto-prunes beyond `EVAL_NIGHTLY_RETENTION_DAYS` (default 90); 3 ops flags (`--dry-run` / `--status` / `--prune-only`). ADR 0006 | v4.3 |\n| Adversarial nightly opt-in | 2 env knobs (`EVAL_NIGHTLY_ADVERSARIAL` / `EVAL_NIGHTLY_ADVERSARIAL_DATASETS`) default OFF; runs the 3 adversarial datasets after baseline SUCCESS using the `nightly` batch profile; advisory-only summary sidecar; baseline-gates-adversarial alerting policy. ADR 0007 | v4.4 |\n| Regression-detection self-test | `RegressionDetectionTest` proves the gate ACTUALLY catches regressions: runs the metric stack against a canonical SUT (asserts green report) then against a hallucinating SUT (asserts `citation-groundedness-strict` mean AND macro_f1 drop, strict `\u003e` comparison per R16) | v4.2 |\n| Playwright E2E suite | Real Postgres + pgvector in CI; deterministic via `data-state` + `data-testid` contract (R11); happy-path + failure-injection per feature (R12); real data only — `page.route()` reserved for external boundaries (R13) gated by `scripts/verify-e2e-real-data.sh` | v3.0 |\n| Test inventory | **~1695 PHPUnit tests** across PHP 8.3 / 8.4 / 8.5 + **408 Vitest react scenarios** + **18 Vitest legacy** + 39 Playwright spec files + RAG regression workflow — all green as of v5.0.0 GA | v5.0 |\n| Opt-in live-test recording infrastructure | `tests/Live/Connectors/` skeleton + `LiveConnectorTestCase` per-provider env-var guard: each test gates on `CONNECTOR_\u003cPROVIDER\u003e_LIVE=1` (e.g. `CONNECTOR_NOTION_LIVE=1`) and needs the provider credential vars (e.g. `CONNECTOR_NOTION_TOKEN`, `CONNECTOR_CONFLUENCE_TOKEN`+`CONNECTOR_CONFLUENCE_CLOUD_ID`); fixture recording is enabled via `CONNECTOR_RECORD_FIXTURES=1`. Default CI runs `Unit` + `Feature` only (zero provider cost). Manual workflow `.github/workflows/live-recording-nightly.yml` available via `workflow_dispatch`. Junior-proof per-provider setup in [`docs/v4-platform/RUNBOOK-live-fixture-recording.md`](docs/v4-platform/RUNBOOK-live-fixture-recording.md) | v4.5 |\n| Structured chat logging | DB driver (extensible to BigQuery / CloudWatch); `session_id` / `user_id` / `question` / `answer` / `project_key` / `ai_provider` / `ai_model` / `chunks_count` / `sources` / `prompt_tokens` / `completion_tokens` / `total_tokens` / `latency_ms` / `client_ip` / `user_agent` / `extra` columns; try/catch — never propagates failures | v1.0 |\n| 39 codified review rules (R1–R39) | Distilled from ~110+ live Copilot findings across PR #4–#142; mirrored in `CLAUDE.md` + `.github/copilot-instructions.md` + per-rule `.claude/skills/\u003crule\u003e/`; auto-loaded by Claude Code when trigger conditions match; pre-push agent at `.claude/agents/copilot-review-anticipator.md` | v3.0 |\n| ADR set (ADR 0001 → 0010) | Architectural decisions records: 0001 ingestion path, 0002 storage agnostic, 0003 human-gated promotion, 0004 v4.2 sister-package integration, 0005 React 19 host bump + iframe→cross-mount deferral, 0006 nightly eval cron, 0007 adversarial nightly opt-in, 0008 v4.5 universal connectors + source-aware ingestion + modern chat surface, 0009 v4.6 connector package extraction, 0010 v4.7 tabular review + workflows architecture | v3.0 |\n\n---\n\n## Canonical Knowledge Compilation\n\nAskMyDocs's signature differentiator: every retrieved chunk passes through a\n**typed canonical knowledge graph** with **human-gated promotion**. The LLM\nproposes; only humans (or operators via `kb:promote`) commit canonical storage.\n\nThe three-stage promotion API is the architectural boundary between \"AI\ndrafting\" and \"knowledge canon\":\n\n| Stage | Route | Effect |\n|---|---|---|\n| **Suggest** | `POST /api/kb/promotion/suggest` | LLM extracts candidate artefacts from a transcript via `PromotionSuggestService`. **Writes nothing.** |\n| **Validate** | `POST /api/kb/promotion/candidates` | Validates a markdown draft against `CanonicalParser` (9 canonical types / 6 statuses / YAML frontmatter). Returns `{valid, errors}`. **Writes nothing.** |\n| **Promote** | `POST /api/kb/promotion/promote` | `CanonicalWriter` writes markdown to KB disk + dispatches `IngestDocumentJob`. HTTP 202. **Only this stage commits canonical storage.** |\n\nClaude skills + the `suggest` / `candidates` endpoints stop at the validation\nboundary. Only humans (via git push → GitHub Action → ingest) and operators\n(via `kb:promote` CLI) commit canonical storage. Every promotion writes an\nimmutable `kb_canonical_audit` row — promotion is forever traceable.\n\nSee **ADR 0003** for the architectural decision rationale + the\n**Retrieval \u0026 Knowledge** features table above for the surrounding canonical\ninfrastructure (typed parser, knowledge graph, rejected-approach injection).\n\n---\n\n## Quick start (5 minutes)\n\n### Prerequisites\n\n- **PHP** `\u003e= 8.3`\n- **Composer** `2.x`\n- **PostgreSQL** `\u003e= 15` with the **pgvector** extension\n- **Node.js** `\u003e= 20` (Vite SPA build)\n- **npm** (bundled with Node)\n\nFastest PostgreSQL + pgvector setup:\n\n```bash\ndocker run -d --name askmydocs-pg \\\n    -e POSTGRES_USER=askmydocs \\\n    -e POSTGRES_PASSWORD=askmydocs \\\n    -e POSTGRES_DB=askmydocs \\\n    -p 5432:5432 \\\n    pgvector/pgvector:pg16\n```\n\n### Clone → working SPA\n\n```bash\n# 1. Clone + install\ngit clone https://github.com/lopadova/AskMyDocs.git\ncd AskMyDocs\ncomposer install\nnpm ci \u0026\u0026 npm run build\n\n# 2. Configure\ncp .env.example .env\nphp artisan key:generate\n# Edit .env: DB_*, AI_PROVIDER, OPENROUTER_API_KEY (or OPENAI_API_KEY)\n\n# 3. Migrate + seed\nphp artisan migrate\nphp artisan db:seed --class=RbacSeeder      # 5 roles + permission matrix\nphp artisan db:seed --class=DemoSeeder      # 3 demo accounts + canonical KB\n\n# 4. Run\nphp artisan serve\n```\n\nOpen `http://localhost:8000` and log in as `super@demo.local` /\n`password` (DemoSeeder creates the account with the `super-admin` role).\nThe SPA redirects to `/app/chat`; click **Dashboard** in the sidebar\nto land on `/app/admin`.\n\n### Full configuration reference\n\nEvery environment variable is documented inline in\n[`.env.example`](.env.example). Sister-package configs live in\n[`config/ai.php`](config/ai.php), [`config/kb.php`](config/kb.php),\n[`config/kb-pipeline.php`](config/kb-pipeline.php),\n[`config/chat-log.php`](config/chat-log.php),\n[`config/admin.php`](config/admin.php),\n[`config/laravel-flow.php`](config/laravel-flow.php),\n[`config/eval-harness.php`](config/eval-harness.php).\n\n### Optional: mount the MCP admin web panel\n\nThe MCP client framework (v5.0+) is exposed through the parent host\nadmin under `/app/admin/mcp-tools` with a server-list page and chat-time\ntool-call UI. For a richer single-pane-of-glass view dedicated to the\nMCP fleet (12 routes — fleet table, three-step new-server wizard,\nseven-tab per-server detail, tools matrix + try-it, three-pane resource\ntree, prompt playground, audit drilldown, circuit-breaker grid,\nOpenAPI explorer, settings + tour), install the standalone companion\npackage:\n\n```bash\ncomposer require padosoft/askmydocs-mcp-pack-admin:^1.1\n# Service provider auto-discovers via composer.json::extra.laravel.providers\n# Pre-built SPA bundle ships inside vendor/padosoft/askmydocs-mcp-pack-admin/public/vendor/mcp-pack-admin/\n# Publish the assets so Laravel can serve them at /vendor/mcp-pack-admin/:\nphp artisan vendor:publish --tag=mcp-pack-admin-assets --force\n# Optionally override the mount prefix (default: /admin/mcp-pack)\nphp artisan vendor:publish --tag=mcp-pack-admin-config\n```\n\nThen sign in as a `super-admin` and open\n`http://localhost:8000/admin/mcp-pack`.\n\n\u003e **Status note (v1.1.0 GA — shipped 2026-05-18):** the panel drives\n\u003e the full live `padosoft/askmydocs-mcp-pack` v1.5+ REST surface\n\u003e end-to-end: 22 typed endpoints, 23 TanStack Query hooks across\n\u003e read+write paths, R21 two-call confirm-token protocol on tool\n\u003e invoke / audit replay / breaker reset, SSE live-feed consumer\n\u003e replacing the prototype simulator. 154 Vitest specs cover every\n\u003e endpoint binding with loading / error / empty / ready states +\n\u003e R21 happy + failure paths + ValidationError surfacing + SSE\n\u003e consumer behaviour via MSW handlers shaped to the real wire\n\u003e schema. AskMyDocs v7.1+ requires `padosoft/askmydocs-mcp-pack:^1.5`\n\u003e (auto-resolved by composer) so all 22 admin endpoints answer the\n\u003e SPA out of the box.\n\n### Enabling AI Act compliance features (junior-proof)\n\nThe v6.0 GA wires AskMyDocs as **the first Laravel platform AI-Act-ready\nout of the box** — the 9 baseline compliance modules (Disclosure, DSAR,\nRisk Register, Bias Monitoring, Human Review Tracker, Incident, Consent,\nCybersecurity, Attestation) ship configured and active. The v6.1 catch-up\nadds four additional capabilities that **default OFF** so existing\ninstalls see no behavioural change. Turn them on in this order — each\nsection is independently optional.\n\n#### 1. Pluggable bias-metric registry (v1.2 — already active)\n\nThree reference metrics ship and auto-register:\n\n```env\nAI_ACT_BIAS_DEFAULT_METRIC=demographic_parity   # alternatives: equalized_odds | calibration\nAI_ACT_BIAS_DISPARITY_THRESHOLD=0.05            # drift alert threshold (0..1)\n```\n\nSwitch the active metric on the chat path:\n\n```php\napp(\\Padosoft\\AiActCompliance\\BiasMonitoring\\Services\\BiasMonitorService::class)\n    -\u003ecapture([\n        'metric_name' =\u003e 'equalized_odds',\n        'cohort_dimension' =\u003e 'language',\n        // ... domain payload\n    ]);\n```\n\nVerification one-liner:\n\n```bash\nphp artisan tinker --execute='dump(app(Padosoft\\AiActCompliance\\BiasMonitoring\\Services\\MetricRegistry::class)-\u003ehas(\"equalized_odds\"))'\n# expect: true\n```\n\n#### 2. Cohort-drift real-time alerting cascade (v1.3 — opt-in)\n\n```env\nAI_ACT_ALERTING_ENABLED=true\nAI_ACT_ALERT_THROTTLE_MINUTES=60\nAI_ACT_ALERT_CB_FAILURES=5\nAI_ACT_ALERT_CB_COOLDOWN=30\n# Optional click-through link for the DPO email body:\nAI_ACT_ALERT_EVIDENCE_URL_TEMPLATE=\"${APP_URL}/admin/ai-act-compliance/bias?tenant={tenant_id}\u0026metric={metric_name}\"\n```\n\nSeed at least one channel route (Slack / Discord / email):\n\n```php\n\\Padosoft\\AiActCompliance\\Alerting\\Models\\AlertRoute::query()-\u003ecreate([\n    'tenant_id' =\u003e null,                                                  // null = platform-global\n    'channel' =\u003e 'slack',\n    'webhook_url' =\u003e 'https://hooks.slack.com/services/T0/B0/xyz',        // auto-encrypted at rest\n    'enabled' =\u003e true,\n]);\n\\Padosoft\\AiActCompliance\\Alerting\\Models\\AlertRoute::query()-\u003ecreate([\n    'tenant_id' =\u003e null,\n    'channel' =\u003e 'email',\n    'email' =\u003e 'dpo@yourcompany.example',\n    'enabled' =\u003e true,\n]);\n```\n\nMake sure `queue.default` is NOT `sync` for production deployments\n(otherwise alerts fire on the request thread). `database` is fine for\nsmall installs; `redis` for prod-grade.\n\n#### 3. EU AI Act regulatory-feed auto-flagger (v1.4 — opt-in)\n\n```env\nAI_ACT_REGULATORY_FEED_ENABLED=true\nAI_ACT_REGULATORY_FEED_URL=https://eur-lex.europa.eu/EN/legal-content/summaries/AI-act.xml\nAI_ACT_REGULATORY_FEED_MAX_ENTRIES=50\nAI_ACT_REGULATORY_FEED_TIMEOUT=15\n```\n\n`bootstrap/app.php` schedules `ai-act:regulatory-poll` daily at 04:10\nonce the env flag is on. Trigger it on demand:\n\n```bash\nphp artisan ai-act:regulatory-poll\n# expect output: \"Regulatory poll complete: ingested=N skipped=M failures=0\"\n```\n\nDPO operators triage the resulting rows on\n`/admin/ai-act-compliance/regulatory` (companion admin SPA cross-mount).\n\n#### 4. DPO multi-org tenant management (v1.5 — opt-in)\n\nNo env vars — driven entirely via the `tenants` table. Create a tenant:\n\n```bash\nphp artisan tinker --execute='\n\\Padosoft\\AiActCompliance\\MultiTenancy\\Models\\Tenant::query()-\u003ecreate([\n    \"slug\" =\u003e \"acme\",\n    \"name\" =\u003e \"Acme Inc.\",\n    \"subscription_tier\" =\u003e \"enterprise\",\n    \"dpo_email\" =\u003e \"dpo@acme.example\",\n    \"config_overrides_json\" =\u003e [\"bias.disparity_threshold\" =\u003e 0.02],\n]);\n'\n```\n\nSend a request with the tenant header:\n\n```bash\ncurl -H \"X-Tenant-Id: acme\" http://localhost:8000/api/admin/ai-act-compliance/tenants/acme\n# expect: {\"data\":{\"tenant\":{\"slug\":\"acme\",...},\"kpis\":{...}}}\n```\n\nAskMyDocs's `ResolveTenant` middleware propagates the host tenant id\ninto the sister-package context via `App\\Compliance\\TenantContextBridge`\nautomatically — every call to `TenantConfigResolver::resolve()` returns\nthe per-tenant override when it exists, the host config otherwise.\n\nVerification: an unknown slug returns 404, suspended → 423 Locked,\narchived → 410 Gone (per the package's `ai-act.tenant-context`\nmiddleware).\n\n#### Reference\n\n- Full `.env.example` section: search for `# AI Act compliance v1.2 → v1.5`.\n- Backend package: \u003chttps://github.com/padosoft/laravel-ai-act-compliance\u003e (READMEs §4-§6 \"killer modules\")\n- Admin SPA package: \u003chttps://github.com/padosoft/laravel-ai-act-compliance-admin\u003e (11 screens)\n- Host-side end-to-end tests live in [`tests/Feature/AiAct/`](tests/Feature/AiAct/) — open them for working code samples of every flow.\n\n---\n\n## Architecture\n\nThe v5.x platform routes every request through `ResolveTenant`\nmiddleware that populates the `TenantContext` singleton, so every\nEloquent query that follows is tenant-scoped (R30 / R31). The chat\nsurface ships **two interchangeable transports** — the v3 synchronous\nJSON path on `KbChatController` (backward-compat fallback) and the v4\nSSE streaming path on `MessageStreamController` (default for the React\nSPA, emits SDK v6 `UIMessageChunk` frames). Both converge on the same\nhybrid retrieval pipeline (vector + FTS + reranker + canonical graph\nexpansion + rejected-approach injection). When `AI_AGENTIC_ENABLED=true`,\n`McpToolCallingService` intercepts after the first provider response and\nruns a multi-turn tool-calling loop (max `AI_MCP_TOOL_CALL_MAX_ITERATIONS`\niterations) — invoking registered MCP servers via native JSON-RPC\ntransports (HTTP / SSE / stdio) provided by `padosoft/askmydocs-mcp-pack`\nand accumulating results before returning the final answer. (v5.0\nshipped this via a separate Node sidecar process; v7.0/W6.3.B retired\nthe sidecar and moved every call onto the host PHP process.)\n\n```\n┌─────────────────────────────────────────────────────────────────────────────┐\n│                Client (React SPA / API / MCP / GitHub Action)               │\n│                                                                             │\n│   v4 streaming           v3 JSON              ingest                        │\n│   POST /conversations/   POST /api/kb/chat    POST /api/kb/ingest           │\n│        {id}/messages/    (legacy fallback)    (Sanctum, batch ≤ 100)        │\n│        stream (SSE)                                                         │\n└──────────────────────────────┬──────────────────────────────────────────────┘\n                               ▼\n┌──────────────────────────────────────────────────────────────────────────────┐\n│  ResolveTenant middleware → TenantContext singleton (R30/R31)                │\n│  AuthenticateForSse middleware (JSON 401 on streaming endpoints)             │\n│  RedactChatPii middleware (v4.1 W4.1 — default-OFF, narrow scope)            │\n└──────────────────────────────┬──────────────────────────────────────────────┘\n                               ▼\n┌──────────────────────────────────────────────────────────────────────────────┐\n│  Chat orchestrators                                                          │\n│  ┌──────────────────────────────┐   ┌──────────────────────────────────┐     │\n│  │ KbChatController (v3 sync)   │   │ MessageStreamController (v4 SSE) │     │\n│  │ • Refusal short-circuit R26  │   │ • Refusal short-circuit R26      │     │\n│  │ • KbSearchService            │   │ • KbSearchService                │     │\n│  │ • AiManager::chat()          │   │ • AiManager::chatStream()        │     │\n│  │ • McpToolCallingService       │   │ • McpToolCallingService (v5)     │     │\n│  │   (v5, if AI_AGENTIC_ENABLED) │   │   multi-turn tool loop →        │     │\n│  │ → { answer, citations,       │   │   native JSON-RPC transport      │     │\n│  │     refusal_reason,          │   │   (v7 — HTTP / SSE / stdio,      │     │\n│  │                              │   │    no Node sidecar)              │     │\n│  │                              │   │ → UIMessageChunk frames          │     │\n│  │     confidence, meta,        │   │   (start/text-delta/source-url/  │     │\n│  │     tool_calls }             │   │    data-confidence/data-refusal/ │     │\n│  │                              │   │    finish)                       │     │\n│  └──────────────────────────────┘   └──────────────────────────────────┘     │\n│           │                                   │                              │\n│           └─────────────────┬─────────────────┘                              │\n│                             ▼                                                │\n│  ChatLogManager::log() — try/catch; never propagates failures                │\n└──────────────────────────────┬──────────────────────────────────────────────┘\n        ┌──────────────────────┼──────────────────────────┐\n        ▼                      ▼                          ▼\n┌───────────────────┐  ┌───────────────────┐  ┌──────────────────────────────┐\n│ KbSearchService   │  │ AI Providers      │  │ Persistence (Postgres)       │\n│ • Embed query     │  │ via raw Http::    │  │                              │\n│ • pgvector top-K  │  │                   │  │ • knowledge_documents +      │\n│ • FTS GIN top-K   │  │ • OpenAI          │  │   knowledge_chunks (FK       │\n│ • RRF + reranker  │  │ • Anthropic       │  │   CASCADE)                   │\n│   0.6v + 0.3k +   │  │ • Gemini          │  │ • embedding_cache (cross-    │\n│   0.1h            │  │ • OpenRouter      │  │   tenant on text_hash)       │\n│ • Canonical boost │  │ • Regolo (via     │  │ • kb_nodes / kb_edges /      │\n│ • Status penalty  │  │   laravel-ai-     │  │   kb_canonical_audit         │\n│ • GraphExpander   │  │   regolo)         │  │ • chat_logs / conversations  │\n│   1-hop kb_edges  │  │                   │  │   / messages                 │\n│ • RejectedApproach│  │                   │  │ • admin_command_audit /      │\n│   Injector        │  │                   │  │   admin_command_nonces /     │\n│ → SearchResult    │  │                   │  │   admin_insights_snapshots   │\n│   { primary,      │  │                   │  │ • flow_runs / flow_steps /   │\n│     expanded,     │  │                   │  │   flow_audit / approvals /   │\n│     rejected,     │  │                   │  │   webhook_outbox             │\n│     meta }        │  │                   │  │ • pii_token_maps (v4.1)      │\n└───────────────────┘  └───────────────────┘  │ • mcp_servers /              │\n                                               │   mcp_tool_call_audit (v5.0) │\n                                               └──────────────────────────────┘\n```\n\n**Ingestion** has two entrypoints (CLI `kb:ingest-folder` + HTTP\n`POST /api/kb/ingest`) that converge on a single execution path:\n`IngestDocumentJob` → `DocumentIngestor::ingest(SourceDocument)` →\n`PipelineRegistry`-resolved `Converter`+`Chunker`+`Enricher` chain →\nidempotent SHA-256 upsert on `(project_key, source_path,\nversion_hash)`. When canonical YAML frontmatter is detected,\n`CanonicalParser` validates it and `CanonicalIndexerJob` populates\n`kb_nodes` + `kb_edges` after commit.\n\n**Promotion** (ADR 0003) is human-gated: `/suggest` extracts\ncandidates from a transcript, `/candidates` validates a draft,\n`/promote` writes the markdown and dispatches ingestion. Only humans\n(git push → GH Action) and operators (`kb:promote` CLI) commit\ncanonical storage.\n\nFor the full component map see [`CLAUDE.md`](CLAUDE.md) section 3.\n\n---\n\n## Roadmap\n\n| Major | Status | Theme |\n|---|:---:|---|\n| **v4.0** | ✅ shipped 2026-05-02 | Enterprise platform foundation — multi-tenant + Vercel AI SDK streaming + canonical KB graph + admin shell + 5 sister packages on Packagist |\n| **v4.1** | ✅ shipped 2026-05-03 | PII redactor v1.1 integrated at 4 chat / embedding / insights / detokenize touch-points (default-OFF) |\n| **v4.2** | ✅ shipped 2026-05-10 | Sister-package integration GA — laravel-flow v1.0 (9 Flow definitions) + eval-harness v1.2 RAG regression CI gate + 3 admin SPAs cross-mounted |\n| **v4.3** | ✅ shipped 2026-05-10 | Host-side hardening — PII at 11 persistence touch-points + React 19 host bump + `eval:nightly` LLM-as-judge cron (ADR 0005 + ADR 0006) |\n| **v4.4** | ✅ shipped 2026-05-11 | Tailwind v4 host migration + iframe→cross-mount of pii-redactor-admin + eval-harness-ui + adversarial nightly opt-in (ADR 0007) |\n| **v4.5** | ✅ shipped 2026-05-12 | Universal Connectors (Google Drive / Notion / Evernote / Fabric / OneDrive / Confluence / Jira) + admin OAuth SPA + source-aware ingestion (per-source chunker dispatch + Reranker Layer-4 + facets) + Vercel AI SDK UI Tier 1 + partial Tier 2 (suggested follow-ups). Stretch Tier 2 (tool-result render / streaming source parts / export / image attachments / artifact panel) deferred to v5.0 per ADR 0008 D4 |\n| **v4.6** | ✅ shipped 2026-05-12 | Connector package extraction — 7 inline connectors lifted to 8 standalone `padosoft/askmydocs-connector-*` packages (`-base` v1.1.1 + `-notion` v1.0.1 + `-google-drive` v1.0.1 + `-evernote` + `-fabric` + `-onedrive` + `-confluence` + `-jira` all v1.0.0) + `HostIngestionBridge` (binds `ConnectorIngestionContract`) + composer-extra auto-discovery + chunkers stay in host (ADR 0009) |\n| **v4.7** | ✅ shipped 2026-05-12 | Tabular Review + Workflows + AI-suggest — admin SPA list/show/create + SSE streaming extractor + workflow list / create / AI-suggest gallery + KB-sample-driven AI suggester + ~115 tests across PHPUnit / Vitest / Playwright. Workflow edit + share modal + use-as-template + Glide Data Grid migration deferred to v4.7.x per ADR 0010 |\n| **v5.0** | ✅ shipped 2026-05-13 | Agentic platform — MCP **client** framework: `McpToolCallingService` multi-turn orchestration + `McpServerRegistry` per-tenant + `McpToolAuthorizer` RBAC + `McpClientBridge` Node sidecar + immutable `mcp_tool_call_audit` trail + admin CRUD API + `AI_AGENTIC_ENABLED` master switch; OpenAI + OpenRouter tool-schema auto-wiring; +147 PHPUnit + 1 Playwright spec |\n| **v6.0** | ✅ shipped 2026-05-14 | AI Act compliance bundle — `padosoft/laravel-ai-act-compliance` v1.1.0 (9 modules: Disclosure / RiskRegister / DSAR / BiasMonitoring / HumanReviewTracker / Incident / Consent / Cybersecurity / ComplianceAttestation) + `padosoft/laravel-ai-act-compliance-admin` v1.1.0 (8 pixel-ported screens from Claude Design handoff) + AskMyDocs host depth: `TokenLevelExplainability` decorator over `streamReply()` writing into `chat_log_provenance`, `RagRefusalQualityMetric implements CohortParityMetric`, `ProvenanceChain::forChatLog()` joining chunks + documents (withTrashed) — ADR 0011 |\n| **v6.1** | ✅ shipped 2026-05-15 | AI Act compliance v1.2 → v1.5 catch-up wave — bumps `padosoft/laravel-ai-act-compliance` + `-admin` pins from `^1.1.3` to `^1.5.0` (skipping v1.2 → v1.5 in one hop). Layered capabilities arrive via the package upgrade: pluggable `CohortParityMetric` registry (DemographicParity / EqualizedOdds / Calibration), cohort-drift real-time alerting cascade (Slack → Discord → always-CC email with throttle + circuit breaker + severity-escalation bypass), EU AI Act regulatory-feed auto-flagger (RSS + Atom, XXE-safe), DPO multi-org tenant registry + per-tenant config overrides + cross-tenant overview. The companion admin SPA (already cross-mounted under `/admin/ai-act-compliance/` from v6.0) automatically surfaces three new screens (`/alerts`, `/regulatory`, `/tenants`) once the pin is bumped — no AskMyDocs-side route / middleware changes required. 1729/1729 PHPUnit on the bumped pin. |\n| **v6.1.1** | ✅ shipped 2026-05-15 | AI Act compliance host wiring — `bootstrap/app.php` registers `ai-act.tenant-context` middleware alias + scheduled `ai-act:regulatory-poll` daily 04:10 (env-gated); new `App\\Compliance\\TenantContextBridge` propagates host tenant id → package `Tenant` model; 18 new host-side end-to-end tests under `tests/Feature/AiAct/` (4 `AlertingCascadeFlowTest` + 2 `BiasMetricRegistryHostFlowTest` + 4 `RegulatoryFeedFlowTest` + 8 `TenantContextHostFlowTest`) prove every default-OFF v1.3 / v1.4 / v1.5 feature works when the opt-in flag is flipped; new `.env.example` AI Act section + junior-proof setup tutorial in README |\n| **v7.0/W1.A** | ✅ shipped 2026-05-15 | MCP client framework extraction — `padosoft/askmydocs-mcp-pack` v1.0.1 published on Packagist (6 contracts + multi-turn tool-calling orchestrator + stdio/HTTP transports + hash-only audit + RBAC hooks + 42 tests across 7 PHP × Laravel CI cells). Standalone, zero AskMyDocs dependencies; v5.0's inline `app/Mcp/Client/*` not yet replaced — the host integration is intentionally deferred until the package roadmap closes |\n| **v7.0/W2** | ✅ shipped 2026-05-15 | mcp-pack v1.1.0 — SSE transport (`SseJsonRpcTransport`) for remote HTTP+SSE gateways, JSON-RPC `resources/*` + `prompts/*` methods so the orchestrator can read from upstream resource catalogs and pre-prompt templates |\n| **v7.0/W3** | ✅ shipped 2026-05-15 | mcp-pack v1.2.0 — first-class server-side. The same package exposes a Laravel app AS an MCP server (stdio long-lived process via artisan command + HTTP+SSE route + JSON-RPC handler routing initialize / tools/list / tools/call to host-supplied tool catalog). Auth + RBAC integration with host gates |\n| **v7.0/W4** | ✅ shipped 2026-05-15 | mcp-pack v1.3.0 — production-hardening. Per-tool circuit breaker (open / half-open / closed states tracked in cache with TTL recovery) + adaptive retry budget (token-bucket per server per minute, exponential backoff on failure). Decorator over `ToolInvoker`; new config keys + telemetry events |\n| **v7.0/W5** | ✅ shipped 2026-05-15 | mcp-pack v1.4.0 — admin backend surface. Package registers REST routes under a configurable prefix (default `/api/admin/mcp-pack`): server CRUD, handshake action, tool catalog, paginated audit log, circuit-breaker state. Middleware-driven auth (host wires Sanctum / RBAC). OpenAPI 3.1 spec + Postman collection ship with the package. NO React/Vue code — this is the backend the standalone `-admin` SPA consumes in the post-v7.0 cycle |\n| **v7.0/W6** | ✅ shipped 2026-05-16 | Host integration over `padosoft/askmydocs-mcp-pack` v1.4 — closed across five sub-waves: PR #174 composer require, PR #175 `mcp_tool_call_audit` `input_hash`/`actor` coexistence + bulk CASE-WHEN backfill, PR #176 host adapters (`McpServerAdapter` / `EloquentMcpServerRegistry` / `McpToolAuthorizerAdapter` / `HostBridge`) bound via `AppServiceProvider::boot()` + `status` ENUM→`varchar(32)` + `user_id`/`result_hash` NULLABLE + `mcp_server_name` added, PR #177 Node sidecar fully retired (entire `mcp-client/` TypeScript project deleted, `ToolInvoker` + `McpHandshakeService` rewritten to drive `McpClient::forServer()` natively, `/api/mcp/credentials` decrypted-secret callback removed), PR #179 final sidecar-artefact retirement (`/api/mcp/internal-auth` probe + `MCP_INTERNAL_AUTH_TOKEN` env + `mcp.internal_auth_token` config + `McpInternalAuthController` all gone). DSAR coverage on actor-written rows, SPA contract aligned (`server_id` filter + `page`/`per_page` + `meta.*` pagination), `StatusPill` widened with `transport_error`. Inline orchestrator (`McpToolCallingService` + host registry + custom authorizer) keeps its surface — it already runs on native transports (PR #177 rewrote the invoker) and the consolidation is a refactor that's deferred to a post-v7.0 cycle (no capability gain, just translation-adapter work). See [`docs/v4-platform/STATUS-2026-05-16-v7-w6.md`](docs/v4-platform/STATUS-2026-05-16-v7-w6.md) for the full closure status. |\n| **v7.1** | ✅ shipped 2026-05-18 | mcp-pack v1.4→v1.5 + mcp-pack-admin v1.0→v1.1 live wire-up cycle. mcp-pack v1.5.0 ships the full 22-endpoint admin REST surface (+16 over v1.4) with BC-safe sub-interface extensions (`McpHostBridgeIdentityContract` + `McpServerMutableRegistryContract`), R21-atomic confirm-token protocol with host-owned mint/consume, OpenAPI 3.1 spec, 325 PHPUnit tests; mcp-pack-admin v1.1.0 wires the React SPA against the live surface end-to-end (23 hooks across read+write paths, R21 two-call with second-leg expired-token guard, SSE live-feed consumer, 154 Vitest specs); AskMyDocs host bumps `padosoft/askmydocs-mcp-pack` from `^1.4` to `^1.5` — zero breakage (1750 PHPUnit tests green: 613 Unit + 1137 Feature). 8 R36 iters across the cycle (mcp-pack v1.5: 4 PRs, mcp-pack-admin v1.1: 4 PRs). Full real-backend Playwright suite parked for v1.1.x patch (`docs/W5-E2E-REWRITE.md`). |\n| **v8.0** | ✅ shipped 2026-05-21 | Killer-features cycle closed (W1..W8). W1-W7 features shipped as planned (notifications core + channels/preferences, why-not-cited + counterfactual, decision-debt heatmap, living collections foundation+semantic, MCP-as-KB-debugger). **W8 Compliance Differential Pack v1** closed via PRs #217..#221: `compliance_reports` schema, report generator (delta + audit aggregate + tamper-evident hash), PDF/JSON export, `/app/admin/compliance/reports` SPA + verify endpoint, and tenant opt-in quarterly digest cron `compliance:digest-quarterly`. RC sequence completed (`v8.0.0-rc1`..`v8.0.0-rc4`), then GA. Plan: [`docs/v4-platform/PLAN-v8.0-killer-features.md`](docs/v4-platform/PLAN-v8.0-killer-features.md). ADRs: 0012..0018. |\n| **v8.0.1** | ✅ shipped 2026-05-22 | Deep-review hotfix (PR #223 — 12 R36 iterations). Six findings from a post-merge comparative review of `v8.0.0-rc1`..`rc3`: **F1 HIGH** project-membership gate on `KbChunkFeedbackController` (IDOR-class cross-project feedback), **F2 HIGH** atomic upsert replacing `updateOrCreate` race, **F3** retrieval correctness on `KbSearchService::fullTextSearch` (filter DTO now applied to hybrid FTS branch), **F4** R31 gate entry","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flopadova%2FAskMyDocs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flopadova%2FAskMyDocs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flopadova%2FAskMyDocs/lists"}