State of the Product — Canonical
Archived S504 (ruling R8): flagged stale/unused by both platform-context.md and platform-direction.md; DR-006 rules it not load-wired. Historical platform context only.
State of the Product — Canonical
Section titled “State of the Product — Canonical”⚠️ SUPERSEDED (S436, 2026-07-02): this doc is deliberately NOT load-wired (DR-006, retired — the rule is now carried by
platform-context.md’s known-stale list) — stale/unused by design. The load-at-session-start product anchor isreference/platform-context.md. Do not trust anything here without verifying againstreference/platform-context.md+reference/deployment-architecture.mdfirst.
Supersedes: ADS v1.0 (technical sections only; UX principles now in
docs/reference/ux-principles.md; ADS v1.0 archived to
.planning/.archive/.reference/ads-v1.md in S151) and the Track 1 “Knowledge
Platform — Product Design” brainstorm from 3 March 2026 (diffed and merged into
§0 below during S152B WP10; source archive to .planning/.archive/.reference/
is scheduled in S152B WP1 — its unique philosophical framing is now captured in
§0, the rest was superseded by the actual build).
Purpose: Accurate reference of what is actually built, the real tech stack,
and current feature state. Forward-looking work lives in
docs/reference/product-roadmap.md; parked/speculative items live in
docs/reference/product-backlog.md.
S152B WP10 split note: This document used to contain 53 “Session N
additions” blocks spanning S53 → S152A (total 2325 lines). Those blocks were
extracted out of this document into a separate session-history archive (since
retired). During the S186 refactor, per-phase re-ingestion detail (S153-S185)
was likewise removed, keeping this canonical reference focused on current state.
Session-specific deliverables live in docs/continuation-prompts/ and memory.
See also:
docs/reference/product-roadmap.md— forward-looking workdocs/reference/product-backlog.md— parked items.planning/.archive/.audits/si-gap-analysis-s149.md— SI gap analysis (S149, historical).planning/.archive/.audits/ai-eval-gap-analysis-s149.md— AI eval gap analysis (S149, historical)docs/reference/SCHEMA-QUICK-REFERENCE.mddocs/reference/classification-architecture.mddocs/reference/data-entry-points.mddocs/operations/taxonomy-change-runbook.mddocs/reference/field-consumer-dependency-map.mddocs/reference/ux-principles.md
0. Product Vision & Guiding Principles
Section titled “0. Product Vision & Guiding Principles”Extracted from the Track 1 Product Design brainstorm (3 March 2026, superseded but preserved here) and refined by the decisions made during Sessions 53-152.
0.1 Founding Insight
Section titled “0.1 Founding Insight”SMBs are not prepared for an AI-first future. Creating structured, high-quality knowledge bases as a single source of truth is a huge step towards preparedness. Claude provided with good data can support a wide variety of business use cases — bids, sales, compliance, training, and whatever comes next. The knowledge base is the product; bids are the first application, not the only one.
0.2 Wikipedia Principle — One Record, Many Views
Section titled “0.2 Wikipedia Principle — One Record, Many Views”Every piece of knowledge exists once. A single content entry has a canonical body, progressive depth layers (brief / detailed / reference sections within the same entry, not separate entries), AI-generated metadata (classification, embeddings, summary, keywords), user-applied context (tags, workspace assignments, notes), version history, and provenance. The UI shows the appropriate depth depending on the user’s context. Editing one version doesn’t risk another becoming stale; search returns one result, not three.
This principle is the reason progressive depth columns live on content_items
rather than as a separate table of layered records.
0.3 Observe and Intervene (Governance Philosophy)
Section titled “0.3 Observe and Intervene (Governance Philosophy)”Not “prevent and approve” (traditional publishing workflow), but “allow changes, track everything, surface for review”:
- Content can be updated freely — no blocking workflows by default.
- Every change is versioned — who changed what, when, with diff.
- Changes are surfaced via change reports (user-facing label; internal code now consistent across UI + code) — daily/weekly summary of what changed.
- Configurable posture per client:
- Open: Anyone with write access can edit. Change report shows changes.
- Review-on-change: Edits to tagged content (e.g., policies, certifications) trigger a review notification. Content is live immediately but flagged for review.
- Formal: Draft → Review → Publish workflow for designated content types. Content isn’t “live” until approved.
The freshness-to-governance and quality-to-governance bridges built in S114
implement the “surface for review” side of this model; the content_history
audit trail implements the “track everything” side.
0.4 Everyone Ends Up Working Within an LLM
Section titled “0.4 Everyone Ends Up Working Within an LLM”A strategic insight from the product owner: most users already work within Claude Desktop or similar LLM interfaces. Canonical’s job is to facilitate that — structuring data so AI can access it effectively, not competing for screen time. This motivates the MCP server (58 tools, 12 resources, 7 prompts, 4 MCP Apps exposing Canonical state through Claude.ai and Claude Desktop), the ClaudePromptButton contextual bridge (replaced CopilotKit in S109), and the “reorient me” pattern where AI reads system state and provides a personal briefing when a user returns.
0.5 The Library-to-Application Feedback Loop
Section titled “0.5 The Library-to-Application Feedback Loop”The circular value proposition: the KB feeds bids → winning bids strengthen the KB → a stronger KB produces better bids. Forward path is built (bid matching pulls from KB to draft responses). Reverse path is partially built (bid outcome dialog has “integrate to KB” checkbox; Phase 2 will surface refined winning responses for one-click promotion back to the Q&A library).
0.6 AI Is Infrastructure, Not a Feature
Section titled “0.6 AI Is Infrastructure, Not a Feature”“Helping you get organised, not learning from you.” AI is invisible plumbing
that classifies, summarises, embeds, and drafts — it is not a visible product
feature that the user is expected to notice, configure, or trust with personal
data. The UI visibility policy at docs/reference/ai-visibility-policy.md is
the operational rule for this principle.
AI visibility cleanup complete (S156-S158). All 37 findings (12C + 10M + 15
Minor) from the design critique audit resolved across 8 WPs: vocabulary sweep,
admin gating, badge/toggle rationalisation, change_reason wiring, client
telemetry helper (lib/client-telemetry.ts). Review check at
.claude/checks/ai-visibility.md. Audit:
docs/audits/design/design-critique-session-7.md.
0.7 Programmatic Where Possible
Section titled “0.7 Programmatic Where Possible”Save AI for response generation and classification. Use deterministic functions
for deterministic tasks. This keeps costs predictable, latency low, and errors
debuggable. The entity extraction filters (30+ deterministic rules in
lib/ai/classify.ts and scripts/kb_pipeline/classify.py) are the canonical
example — AI proposes entities, deterministic rules exclude false positives, AI
is only re-invoked for Pass 2 validation when validate: true is set.
1. Actual Technology Stack
Section titled “1. Actual Technology Stack”| Component | ADS v1.0 Planned | Actual Implementation |
|---|---|---|
| Frontend | Next.js + CopilotKit/AG-UI as sole UI protocol | Next.js 16 (CopilotKit removed S109 — ClaudePromptButton bridge for in-app AI entry points) |
| Backend | FastAPI (Python) | Next.js API routes (~159 route files across ~43 directories, +6 cron routes). No separate backend service. |
| Database | Turso per-tenant (LibSQL/SQLite) | Supabase (PostgreSQL + pgvector + Auth + Storage). One project per client. |
| Auth | SuperTokens (self-hosted) | Supabase Auth (email/password + magic link, invitation-only) |
| Deployment | Coolify on UK VPS (Docker) | Vercel |
| AI Reasoning | Anthropic SDK + Agent SDK (dual backend) | Anthropic SDK via AI service layer (lib/ai/). No Agent SDK. |
| Embeddings | Voyage AI voyage-4 (1024-dim) | OpenAI text-embedding-3-large (1024-dim, Matryoshka shortening) |
| Editor | Tiptap v3 + Tiptap AI Toolkit (paid) | Tiptap (open source core only). No AI Toolkit. |
| Export | Tiptap Conversion (DOCX) + LibreOffice (PDF/A) | docx npm + exceljs (DOCX/XLSX). No PDF/A. |
| Document Processing | Python worker on Railway + python-docx | Hybrid: Next.js API routes for extraction (unpdf + Claude) + Python scripts for bulk ingestion |
| Orchestration | LangGraph for state persistence | None — stateless API routes + Supabase for persistence |
| Client Branding | — | Build-time per-client config (NEXT_PUBLIC_CLIENT_ID), Zod-validated JSON, OKLCH contrast validation, CSS variable injection |
2. Architecture (As Built)
Section titled “2. Architecture (As Built)”Tenancy & entity hierarchy (canonical). Deployment is one Supabase project (database) per client — the DATABASE is the tenant boundary and the only cross-tenant boundary (no
tenant_idcolumn). A workspace is a container within that DB bound N:1 to oneapplication_type(workspaces.application_type_id); a client has many workspaces, soworkspace ≠ client/tenant. application_type is a use-case class (theapplication_typesreference table — not a container). q_a_pairs are corpus-level — one client’s shared corpus, not workspace-partitioned (source_workspace_id/source_form_response_idare nullable lineage only). Hence ID-120 Q&A dedup is INTRA-tenant (across the client’s workspaces & forms), never cross-tenant. Full glossary: platform-direction.md → “Core entity hierarchy (canonical glossary)“.
Next.js 16 on Vercel├── Frontend (React + shadcn/ui + Tiptap + TanStack Query v5 (40/40 hooks migrated) + @tanstack/eslint-plugin-query + ClaudePromptButton bridge)│ └── Client Branding (build-time per-client config via NEXT_PUBLIC_CLIENT_ID, CSS variable injection, BrandLogo with light/dark variants)├── API Routes (~159 route files across ~43 directories, +6 cron routes)│ ├── Content CRUD (/api/items/)│ ├── Bid workflow (/api/bids/)│ ├── Search (/api/search/)│ ├── AI operations (classify, extract, summaries, change-reports, vision)│ ├── Governance (/api/governance/, /api/review/)│ ├── Claude prompts (lib/claude-prompts.ts)│ ├── Activity feed (/api/activity/)│ ├── MCP server (/api/mcp/[transport]/)│ ├── OAuth decision (/api/oauth/decision/)│ ├── Plugin (/api/plugin/)│ └── Admin (/api/admin/)├── AI Service Layer (lib/ai/ — 12 modules)│ ├── classify, summarise, change-report, embed, match, draft│ ├── extract-content, extract-questions, vision, quality-check│ ├── errors (AIServiceError pattern), index (barrel exports)│ └── skills/ (5 Markdown skill files + loader)├── MCP Server (lib/mcp/ — 30+ .ts files across tools/, formatters/, and top-level modules)│ ├── tools/ (54 tools across 15 category files + index.ts + shared.ts)│ ├── formatters/ (14 files — Markdown formatters per domain, including intelligence)│ ├── resources.ts (12 resources + 7 prompts — +1 ui://intelligence-feed S144)│ ├── auth.ts (per-user Supabase client, role check)│ ├── app-bundles.ts (generated inline HTML for 4 MCP Apps — Vercel-safe)│ └── plugin-bundle.ts (generated ZIP for plugin download — Vercel-safe)├── OAuth & Protected Resource Metadata│ ├── app/.well-known/oauth-protected-resource/ (RFC 9728 via mcp-handler)│ └── app/oauth/consent/ (Warm Meridian consent page)├── Anthropic SDK (Claude claude-sonnet-4-6 for classification, summaries, drafting)├── OpenAI SDK (text-embedding-3-large for embeddings)└── Supabase client (server + browser)
Supabase (eu-west-2 London)├── PostgreSQL (42 tables, ~120 RLS policies, 1 squashed migration as of S176)│ ├── 75+ RPC functions (search, analytics, entity operations, intelligence feeds, bid matching)├── pgvector (HNSW indexes, cosine similarity)├── Auth (email/password, magic link, invitation-only)├── OAuth 2.1 Server (dynamic client registration, consent page)├── Storage (documents bucket for uploads)├── Auto-create viewer role trigger (on auth.users INSERT)└── Edge Functions (not used — all logic in Next.js API routes)
Python scripts (local execution, not deployed)├── kb_pipeline/ (ingest, classify, embed, store, dedup, layer_inference)├── import_bid_library.py (Q&A extraction from client DOCX, --entities flag)├── ingest_markdown.py (markdown ingestion with entity/relationship/temporal storage)└── extract_docx_tables.py (table extraction)3. Database Schema (40 Tables)
Section titled “3. Database Schema (40 Tables)”Core: content_items (KB content with progressive depth: brief/detail/reference
columns + answer_standard/answer_advanced for Q&A pairs + promoted columns:
citation_count, source_file, layer, starred — S117; ai_summary renamed to
summary per AI Visibility Policy — S164b), workspaces (containers — type:
bid|kb_section), content_item_workspaces (junction), user_roles
(admin/editor/viewer + display_name)
Bid: bid_questions (extracted tender questions), bid_responses (AI-drafted +
human-edited responses), bid_response_history (bid response version snapshots)
Governance: content_history (immutable version snapshots), governance_config
(review settings), notifications (user notifications), review_assignments
(scheduled review assignments with assignee, due date, priority — S114),
verification_history (immutable verification audit trail with optional notes —
S115), coverage_targets (per-domain coverage goals with extensible metric
model — S117)
Knowledge Graph: entity_mentions (entity mentions with metadata JSONB —
certification/framework/registration enrichment added S103),
entity_relationships (relationships between entities), content_citations
(citation links between content items)
Templates: template_requirements (tender template structure),
template_completions (filled template instances), templates and
template_fields (tender template structure definitions), content_templates
(content creation templates with pre-fill structure — S115)
Guides: guides (guide definitions), guide_sections (ordered section
structure per guide)
Multi-client: entity_aliases (DB-driven alias resolution), layer_vocabulary
(DB-driven layer definitions)
Source tracking: source_documents (uploaded file lineage with version chain
and re-upload detection), source_document_diffs (Q&A pair-level diffs between
document versions)
Sector Intelligence (S132-S149): company_profiles (per-workspace company
context with sectors/services/certifications + cached embedding column added
S149), feed_sources (RSS/Atom/Web sources with ETag and freshness tracking),
feed_prompts (versioned scoring prompts with rollback audit trail),
feed_articles (raw + filtered + passed articles with relevance scores and
extraction_method column added S149), feed_flags (user feedback on
classification), si_processing_queue (concurrency guard)
Support: read_marks, change_reports, pipeline_runs, processing_queue,
ingestion_quality_log, taxonomy_domains, taxonomy_subtopics
Taxonomy: 15 domains (12 baseline + 3 SI), 56 subtopics in DB and classification
prompt (S204 WP-D: corporate.financial merged into
corporate.financial-standing — PQQ-aligned canonical; 16 row-updates across 5
columns / 4 tables; soft-delete preserves audit trail;
lib/taxonomy/taxonomy.ts reduced to a 24-line re-export shim). provenance
column tracks origin (baseline/client/recommended). S128: slug normalisation +
display_name. S133: SI domains (legislation-policy, market-intelligence,
sector-news) + 19 subtopics. S134: full reclassification against 15-domain
taxonomy (251 items, 1,063 entities, 786 relationships).
1 squashed migration file (S176 squash: 43→1, reconciling entity_aliases table
- 14 column drift + 4 MCP-created functions + storage bucket/policy gaps).
Schema reconciliation report:
docs/operations/schema-reconciliation-report-s176.md. 90+ public RPC functions.
4. Navigation & Pages (As Built + Planned)
Section titled “4. Navigation & Pages (As Built + Planned)”Current Navigation (As Built)
Section titled “Current Navigation (As Built)”| Route | Purpose | Notes |
|---|---|---|
/ (Home) | Activity & Action Centre — Reorient Me (urgent items, active bids, recent work, team changes), Needs Attention, Active Bids, Quick Stats, Activity Feed. McpSetupNudge gated on KB ≥1 item (S177). S190 P0-4 first-run onboarding (DashboardFirstRunCard): for admin + editor on first login (isFirstLogin && !dismissed && role !== 'viewer') a dashed-border welcome card surfaces above ReorientSection with role-branched CTAs, dismissable via localStorage, plus an optional 3-link persona hint row that writes user_metadata.primary_focus (bid_writing / account_management / marketing) — same value is editable in Profile Settings dropdown. Viewers see an updated welcome one-liner inside ReorientSection instead. When the KB has zero content (isKBEmpty), 5 sections are suppressed (ContentPerformanceSection, QuickStatsStrip, ComplianceStatusSection, PipelineRunsPanel, Recent Activity) alongside McpSetupNudge’s existing gate. Shared signal helpers live in lib/dashboard-signals.ts. | Dashboard with search bar |
/browse | Filter-driven content browsing — grid/list views, multi-filter panel, infinite scroll, bulk actions | Excludes Q&A pairs by default (Session 54 decision) |
/search | Redirect to /browse (search consolidated into Browse, S111/S113) | Preserves ?q= param; API endpoint /api/search remains |
/workspaces | Workspace launcher — type selection (bid, kb_section) with mental model framing | S110: replaced /bid as nav entry point |
/bid | Bid listing and creation | Workspaces with type=bid |
/bid/[id] | Bid detail — questions, metadata, tender extraction, status | Question extraction, confidence postures, progress tracking. ReadinessChecklist with 7-criteria readiness badge (S113-S114) |
/bid/[id]/session | Working session — question-by-question drafting | Tiptap editor, Content Library drawer, ClaudePromptButton for AI-assisted drafting. Crash recovery (localStorage auto-save, DraftRecoveryDialog). “Browse for content” button for search-to-bid pipeline |
/item/[id] | Content item detail — split into ReaderView (viewers) + EditorView (editor+), shared data hooks, ContentTabs, metadata, version history. Consolidated to 2 edit paths: read-only (viewers) + inline edit via useInlineFieldEdit (editor+). DraftToggle routed through PATCH for audit trail (S177). | Content-type-aware: articles use progressive depth tabs, Q&A pairs get dedicated layout. Keyboard shortcuts: E→focus suggested_title, Escape→cancel edit. VerificationBadge with binary trust levels (S115). |
/item/new | Content creation — consolidated 4-tab surface (S178 P0-2): “Write content” (Tiptap + RHF + Zod, with fullwidth TemplateSelector zero-state gate), “Import from URL” (fetch + extract + classify), “Upload file” (folds former FileUploadDialog inline), “Batch Q&A” (folds former /item/new/batch). ?tab= deep links. Legacy /item/new/batch → 308 permanent-redirect to ?tab=batch. | S98-S99, S114-S115, S178 |
/review | Governance review queue — verify/flag items, undo toast, session counter, side panel, review cadence card, assignment manager | Editor+ only. Keyboard shortcuts. Server-side sort with confidence priority (S114). |
/change-reports | Change reports (formerly “digests” — renamed S246+S251 W1B) — generation + history. Reframed S115 | |
/documents/[id]/diff | Source document diff review — Q&A pair-level changes between document versions | S104-S106 |
/guide | 308 permanent redirect to /coverage?tab=guides (S188 P1-28 consolidation). The standalone guide listing route was removed; the guides tab in Coverage is the canonical surface and supports ?tab=guides&id=<slug> deep-links that scroll + highlight the matching card | S188 |
/guide/[slug] | Guide detail — ordered sections, content per section, progress tracking, research feed | Sector and product guides |
/intelligence | Intelligence hub — workspace grid, company profiles, pipeline status | S140: SI Phase 1b Waves 1-2 |
/intelligence/profiles | Company profile management — create/edit/delete profiles with sectors, services, certifications | S140 |
/intelligence/[workspaceId] | Workspace detail — overview with RSS panel, feed sources, articles, metrics, prompts (sub-nav: Overview/Sources/Articles/Metrics/Prompts) | S140-S142 |
/intelligence/[workspaceId]/metrics | Full metrics dashboard — filter ratio trend chart (SVG), prompt performance table, period/granularity selectors | S142 |
/api/feeds/[workspaceId]/rss | Public RSS 2.0 feed of passed articles (no auth, 15-min cache) | S142 |
/api/feeds/[workspaceId]/rss/filtered | Public RSS 2.0 feed of near-miss filtered articles (no auth) | S142 |
/intelligence/[workspaceId]/sources | Feed source management — add/edit/delete/test RSS/Atom/Web sources | S140 |
/settings | 9-section settings sidebar in 3 groups — Personal: Profile, Connections; Content Management: Content Organisation, Content Owners, Organisations & People, Guides; System: Team, Quality Review, Provenance (links to /provenance). Developer configuration collapsed into an admin-only “For developers” accordion inside Connections (S185 P1-20) | Role-adaptive sidebar |
/login | Supabase Auth login | Email/password + magic link |
Navigation (As Built — Current)
Section titled “Navigation (As Built — Current)”Browse | Q&A Library | Coverage | Change Reports | Workspaces | Intelligence | Review | SettingsS188: /guide nav entry removed (consolidated into Coverage → Guides tab);
/change-reports Change Reports added to NAV_LINKS with requiresEdit: false so
viewers/Tom can reach it (D-61 discoverability closure).
(Home is the logo link; Settings is shown for all roles; Review requires editor+)
| Route | Purpose | Session |
|---|---|---|
/library | Dedicated Q&A pair management — list, filter, copy, edit, bulk actions, grouping | Session 55-58 (Spec 5) |
/coverage | Coverage dashboard — taxonomy, template, and guide coverage (3 tabs). Freshness heatmap view (domain x subtopic grid with view toggle, S112). Editor+admin only (S189 P1-11 Option A) — viewers are redirected to /browse via client-side useUserRole().canEdit guard in coverage-tabs.tsx; /coverage nav link hidden for viewers (requiresEdit: true in NAV_LINKS). API routes remain auth-only (defence-in-depth deferred per spec OQ-3). | Session 58, 85, 88, 112, S189 P1-11 |
/bid | Bid listing and creation — workspaces with type=bid | Session 63 (nav redesign) |
/guide | 308 → /coverage?tab=guides (consolidation). Detail reader route /guide/[slug] preserved. | Session 81, S188 P1-28 |
Content Type Separation (Session 54 Decision)
Section titled “Content Type Separation (Session 54 Decision)”Q&A pairs (93% of content) and KB content (articles, research, policies) have fundamentally different interaction models. Session 54 investigation found:
- Progressive depth tabs are empty for all Q&A pairs
- AI summaries are useless import artifacts for Q&A pairs
- Q&A pairs need copy-to-clipboard as primary action, not read-and-learn
- Standard/Advanced answer variants are concatenated as unstructured text
Decision: Separate Q&A management into /library (see Spec 5). /browse is
for KB content. Both share the same content_items table and search index.
Page Overlap Analysis
Section titled “Page Overlap Analysis”Browse + Search (consolidated S111/S113): Browse now includes integrated
semantic search. Entering a query in the Browse search bar triggers hybrid
vector+keyword search with content-type-aware results (S113). The standalone
/search page redirects to /browse preserving ?q=. The /api/search
endpoint remains for the Content Library Drawer and MCP tools.
Projects + Bids → Workspaces: Same projects table with type
discriminator. Merging under “Workspaces” reinforces the KB-first framing:
knowledge base is the foundation, workspaces are where you act on it.
5. Feature State (What’s Actually Built)
Section titled “5. Feature State (What’s Actually Built)”Production counts: Content-item, test, MCP-tool, and migration counts drift
every session. docs/generated/codebase-stats.md and
docs/generated/mcp-inventory.md are the canonical source — this document no
longer tracks counts inline.
Content Library
Section titled “Content Library”Capability summary only. Session-by-session narrative is tracked in the session ledgers.
- Full CRUD (create, read, update, archive/delete)
- Edit-path consolidation: 2 paths (read-only for viewers, inline edit via
useInlineFieldEditfor editor+). 4 inline components incontent-tabs.tsx(InlineTitle,InlineSuggestedTitle,InlineContent,InlineAnswer). DraftToggle routed through PATCH (createscontent_historyaudit entry). Keyboard shortcuts: E focusessuggested_title(or cancels when editing), Escape cancels. - Single content creation entry point at
/item/new— 4-tab surface (Write / Import URL / Upload / Batch)./item/new/batchpermanent-redirects via HTTP 308 to/item/new?tab=batch. - Browse header strip — single
+ Newsplit-button dropdown, single “Display” dropdown, Sort. Cards show “SI” badge whenmetadata.source === 'intelligence_pipeline'. Source filter plumbed through URL param + Supabasemetadata->>sourceJSONB filter. - Browse FilterPanel — three-tier disclosure (Primary / Secondary / Advanced) with Domain + Subtopic + Content Type + Content Layer + Freshness primary. PresetBar above the panel. Saved filter presets via PresetBar / SavePresetDialog / ManagePresetsDialog /
useFilterPresets(localStorage). - Browse cold-start persona prompts —
<SearchPromptCards>grid renders when/browsehas no query, no filters, not unread-only, not loading,totalCount > 0. Three persona branches (bid_writing,account_management,marketing) read viausePrimaryFocus()fromuser_metadata.primary_focus+ viewer fallback. Discriminated-union card data model (kind: 'filter' | 'search' | 'chipComposite'); filter cards write URL params, search cards run semantic search, chipComposite cards render top-3 domain chips (hooks/browse/use-top-domains.ts, 24h-cached). 5-condition visibility gate inlib/browse-cold-start.tsshouldShowColdStartPrompts(). - Unified
SearchBar— three Browse search surfaces (hero / compact / inline) share one component. Live preview dropdown viaGET /api/search/preview+useDebouncedPreviewhook (TanStackuseQuery, AbortController,staleTime: 30_000). Inline dropdown layers Recent searches / preview / Popular topics witharia-live="polite", ArrowUp/Down keyboard navigation, “See all results” footer.from_bidURL param sticky through filter mutations. - Item detail Source Information accordion — content-type-aware via
SourceMetadata(components/reader/source-metadata.tsx) branching oncontent_type:QAPairFields,MarkdownFields(10 prod values viaINGESTION_SOURCE_LABELS),FeedArticleFields(feed name + published date viafeed_articles → feed_sourcesjoin). Role-gatedclassification_confidence(admin/editor only). - Q&A answer editing via dynamically-imported Tiptap
ContentEditorforanswer_standard/answer_advanced— single-field-at-a-time pattern, per-field save-safety guard (≥20% shrink blocks), per-field regen-embedding checkbox. PATCH preservesQ: {question}\n\nprefix in rebuiltcontent_items.content. - Inline body editing with Tiptap (outputs markdown via
@tiptap/markdown). GFM table node family registered (@tiptap/extension-table+ row/cell/header) — tables round-trip through edit/save without loss. Save-safety guard inlib/editor/save-safety.ts(SAVE_SAFETY_MIN_RATIO = 0.8) wired into Save-button path and Cmd/Ctrl+S path. - Markdown canonical format end-to-end. All 8 extraction paths (HTML/Readability, DOCX/mammoth, PDF/unpdf, SI Tier 1 RSS, SI Tier 2 fetch, Python trafilatura, Python PDF) produce markdown.
ContentRenderer(react-markdown) for full display;stripMarkdown()for snippets. Shared Turndown instance with GFM plugin for HTML→markdown.PipelineExtractionResultinterface for structured extraction metadata. - Heading-based content chunking —
content_chunkstable with per-chunkvector(1024)embeddings, populated bychunkByHeadings()inlib/content/chunking.ts(H2 boundaries with H1 fallback, sub-100-char merge, sub-500-char single-chunk). Chunks generated on file upload / URL ingest / web-form create / MCP create / content edit. Parallel Python implementation inscripts/kb_pipeline/chunk.py. Backfill CLI atscripts/backfill-chunks.ts. - Re-ingestion quality protocol — operator runbook at
docs/operations/re-ingestion-quality-protocol.md(6-step before-and-after protocol, embedding smoke-test, pipeline divergence assessment, DOCX pre-processing checklist, source-document inventory). - Markdown batch ingest UI (EP2, §1.11) — admin/editor drag-and-drop multi-file
.mdupload at/item/new“Upload file” tab routes to dedicatedapp/api/ingest/markdown/route.ts(maxDuration=300, batch ceiling 10 files / 5 MB total / 1 MB per file, UTF-8 only,.mdextension only). Two-phase orchestration vialib/ingest/markdown-orchestrator.ts:phase=analysereturns per-file analysis (front-matter parse + title extraction + diff-marker scan + dedup pre-check, no DB writes) so reviewers see flags before importing;phase=importruns the full pipeline (insert content_items withingest_source='upload'+content_owner_idresolved + draft/final → publication_status mapping per D-A:'draft'/'unknown' → 'draft','final' → 'in_review', then classify + chunk + embed). Pattern E end-to-end (server-sidepipeline_runswrites viarecordPipelineRun()+updatePipelineProgress()+ UI polling onGET /api/pipeline-runs/:idevery 1-2s during import); richresults_summaryshape (stored[]/dedup_flagged[]/superseded[]/skipped_excluded[]/errored[]) returned in the POST response and persisted topipeline_runs.result. Dedup is soft-block (matched rows still insert withdedup_status='suspected_duplicate'); adminskip_dedup=truebypasses, editor request silently ignored. EP3 single-file upload route untouched (maxDuration=60preserved); EP2 dedicated route is strictly additive. - Publication-review tab on
/review(§5.2-P4B + Radix Tabs refactor + §5.3 bulk-approve UX) — admin/editor surface for items inpublication_status='in_review'(the state EP2 markdown imports land in forfinal-flagged content). 6-tab Radix surface replaces the prior popover-button filter modes: Drafts / Pending changes / Verified content review / Verified (audit) / All / Awaiting publication (canonical order percomponents/review/review-tabs.tsxTAB_SPECS). Tab state lives in URL (?tab=publication-reviewetc., defaultverified-review); deep-links round-trip; back button skips tab clicks (router.replace, no history entry per click). Per-row actions on the publication-review tab: Approve & publish (PATCHespublication_status='published'), Return to draft (PATCHes'draft'), Open in editor (link to/item/[id]). Both transitions are role-allowed for admin and editor perlib/governance/publication-transitions.ts:75-80. Stats route widened to surfaceawaiting_publication: number; new routeapp/api/admin/content-dedup/...already covered by §1.7 narrative above. §5.3 bulk-approve UX (S219 + S220, fully shipped end-to-end): per-row checkboxes + sticky bulk action bar (<PublicationBulkActionBar>) mount above the queue when ≥1 row selected. Bar surfaces “N of M selected” live region, “Select all on page” master checkbox (indeterminate at partial selection), Clear, Approve selected, Return selected to draft. D-3 ratified cap = 50 client-side (action buttonsaria-disabled+ cap message past 50, mirroring server-side Zod cap); D-4 symmetric Radix AlertDialog confirmations on both bulk actions. Bulk endpointPOST /api/review/publication-bulk-actioniterates the per-row PATCH semantics sequentially with bulk-specificchange_reason='bulk_approve' | 'bulk_return_to_draft'audit literals (distinct from singleton-PATCH phrasing so audit queries differentiate); HTTP 200 always with{ totalRequested, successCount, failureCount, results[] }envelope and 5-status enum (success | conflict | forbidden | not_found | error); pre-loopfromStatus !== 'in_review'guard returns'conflict'regardless of role/action (D-10 defence-in-depth on top of.eq('publication_status','in_review')UPDATE filter so stale-queue selections cannot silently'published' → 'draft'). Partial-failure result dialog (<PublicationBulkResultDialog>) opens automatically whenfailureCount > 0, listing per-row outcomes viaitemTitleLookupMap (falls back to<UUID> (item no longer in queue)when title missing). Selection state lives inusePublicationReviewSelection()(page-scope ephemeral Set, clears on tab unmount). Spec archived to.planning/.archive/.specs/publication-approval-gate-spec.md. Shipped S215 W1+W3 (tab + per-row actions); §5.3 W1 endpoint S219 W1; §5.3 W2 UI + W3 tests S220. - Audit-of-verified surface (
/review?tab=verified-audit, S214 OQ1 + S215 T9 fold-in) — 4th tab “Verified (audit)” preserves the bulk re-verify and unverify capabilities on already-verified items. Admin and editor can mark a verified item as freshly checked (re-verify, refreshingverified_at) or push it back to the review queue (unverify). MCP toollib/mcp/tools/review.tsverifiedenum stays intact regardless of UI tabs (LLM-side surface unchanged). Capability previously undocumented in SoTP / roadmap / backlog; folded in S215 T9 per Liam OQ1 decision (kept as a tab rather than redirecting to/browse?review_status=verified).
Re-ingestion arc (S175-S182, cutover complete)
Section titled “Re-ingestion arc (S175-S182, cutover complete)”The blank-DB re-ingestion of Phew’s content onto a fresh Supabase project
(mgrmucazfiibsomdmndh, eu-west-2) spanned eight sessions across four phases:
prep (S175-S178, migration squash 43 to 1, restore matrix, runbook,
guide-regeneration prompts), execution (S179-S180, Stage 0 snapshot of 342
items, Stage 1 auto-ingest 291 items, Stage 2 client markdown 220 items, two
rounds of post-squash drift closure), cutover hardening (S181, full schema
reconciliation with 6 ADD COLUMN + 29 SET NOT NULL, chunk backfill,
skills-bundle build-time inlining), and cutover + close-out (S182, Path A+
project transfer with zero downtime, screenshot parity audit, 34 missing
embeddings + 78 empty-string classification cells backfilled). The consolidated
review doc is at docs/operations/re-ingestion-status-review-2026-04-21.md.
Post-cutover state
Section titled “Post-cutover state”Capability summary only. Session-by-session narrative is tracked in the session ledgers.
-
Production project (
rovrymhhffssilaftdwd): prod app reads from this project (Vercel + GitHub env vars flipped 22/04/2026; prod JS bundle carries the project ref). Sister project ‘M’ deleted via Supabase dashboard 23/04/2026; fullpg_dumpbackup archived atdocs/database/M_full_backup_20260423.sql(gitignored). -
Quality gate:
scripts/quality-gate.tswith 12 generic + 7 audit-content checks across 4 profiles (re-ingest / batch / onboarding / audit-content). CLI:--threshold | --profile | --format | --output | --fail-on | --include-check | --exclude-check. Config sidecars underscripts/config/quality-gate/. UUIDv7 run IDs, git_sha + timestamp + project_id fields in JSON envelope.excludeArtefacts()helper excludes[E2E%+[SUPERSEDE%titles from generic checks. -
Shared Python
post_inserthelper —scripts/kb_pipeline/post_insert.py::run_post_insert(...)runs 8 post-insert side-effects in canonical order (content_history v1 → chunks → entity aliases → entity_mentions → entity_relationships → metadata.ai_temporal_references → temporal-to-entity bridge → layer inference). Wired into all 4 ingest scripts (pipeline.py,ingest_markdown.py,ingest_stage2_markdown.py,import_bid_library.py). Step 1 (history) is a deliberate no-op — the DB trigger20260422060118is the single source of truth for v1 history writes. -
Supersession: content items can be marked superseded via admin UI (
supersede-content-dialog.tsx+PATCH /api/items/:idadmin-only double-gated), MCPsupersede_content_itemtool (defineToolwrapper withdestructiveHint), or Python CLI--auto-supersede/--auto-supersede-dry-run(mutually-exclusive). Superseded items hidden from search by default (include_superseded=false); undo re-exposes the row. Schema:superseded_byUUID FK oncontent_items+ partial index + self-reference CHECK;dedup_statusenum widened with'superseded'. -
v1 history guarantee: three-layer structural prevention — DB trigger backstop (migration
20260422060118,trg_content_items_ensure_v1_history, DEFERRABLE INITIALLY DEFERRED), app-level explicit writes in 5 of 8 TS/MCP insert paths, quality-gatehistory_v1_presentmonitoring, and guard test (__tests__/validation/content-items-v1-history-guard.test.ts) enforcing everycontent_itemsinsert pairs with a v1 write or sits on a documented allowlist. -
Dedup: content-hash soft-block across all 11 entry points (Python EP1/EP2/EP2b/EP8 + TS EP3/EP4/EP5/EP6/EP9/EP10/EP11). Admin-only
skip_dedupoverride. RSS D3 many-to-many viacontent_item_workspacesjunction. -
Admin dedup review surface (§1.7):
/admin/content-dedup/queue +/admin/content-dedup/[id]detail page (admin-only, route-group page; allow-listed inproxy.ts). Listsdedup_status='suspected_duplicate'items withmetadata.suspected_duplicate_ofcanonical reference; per-item actions confirm-duplicate (delegates toapp/api/items/[id]/archive/route.ts), confirm-unique (flips to'confirmed_unique'), and supersede (delegates tolib/supersession/set.ts::setSupersession()). Supersede supports both directions (subject→canonical default, canonical→subject reverse) viadirectionbody field; reverse path retires canonical AND flips subject to'confirmed_unique'so queue resolves either way (perdocs/specs/§1.7-admin-dedup-supersede-fix-spec.md). All five routes —GET /api/admin/content-dedup,GET /api/admin/content-dedup/[id],POST /api/admin/content-dedup/[id]/{confirm-duplicate,confirm-unique,supersede}— writecontent_historyrows with category-specificchange_reasonconstants; history insert failures uselogBestEffortWarn(). UI components incomponents/admin/content-dedup/(queue, row card, filter bar, detail, action buttons, empty state). Playwright E2E coverage on staging viae2e/tests/admin-dedup-queue.spec.ts(read-only — list, filter, detail compare, RBAC matrix) +e2e/tests/admin-dedup-actions.spec.ts(mutating — confirm-duplicate, confirm-unique, supersede A/B, SAME_ID guard, audit trail per action) — see Test Infrastructure §8. -
Near-duplicate merge dashboard (§1.9):
/admin/content-dedup/near-duplicateslist +/admin/content-dedup/near-duplicates/[pairId]detail page (admin-only). Pair-id is canonical lex-sorted${uuid}__${uuid}(helperlib/dedup/pair-id.ts). Lists embedding-cosine pairs fromfind_duplicate_pairsRPC; threshold slider default 0.95, range [0.85, 0.99], 300ms debounce; primary-domain filter; terminal-status pairs (merged/confirmed_unique/archived) excluded so resolved pairs stop re-surfacing. Detail view re-computes similarity at click time so reviewers see drift between flag-time and review-time scores. Two pair-level actions: merge (POST /api/admin/content-dedup/near-duplicates/[pairId]/merge, delegates tosetSupersession) — direction defaults to retain-newer-with-richer-content viadefaultMergeDirectionheuristic, admin can flip; confirm-unique (POST .../confirm-unique) — calls transactionalresolve_near_dup_confirm_uniquePL/pgSQL RPC that flips both pair members to'confirmed_unique'and writes bothcontent_historysnapshot rows in one transaction (idempotent re-flip; SECURITY INVOKER; anon REVOKEd). Both actions persist OQ2 audit context (metadata.similarity_at_resolution,metadata.threshold_at_resolution) so future audits can reconstruct flag-time score + threshold. Components incomponents/admin/content-dedup/near-duplicates/— pair list, filter bar, pair detail, action buttons, merge-direction dialog, empty state, pair row card (forked from §1.7 row card; types differ:left/rightpair members vssubject/canonicaldiscriminant). Playwright E2E coverage on staging viae2e/tests/admin-dedup-near-duplicates.spec.ts(read-only — pair list, threshold slider debounce, domain filter, pair detail, RBAC) +e2e/tests/admin-dedup-near-dup-actions.spec.ts(mutating — merge with OQ2 audit-context assertion, transactional confirm-both-unique, empty state) — see Test Infrastructure §8. -
Pipeline markdown parity:
lib/extraction/extraction-result.ts(TS) andscripts/kb_pipeline/extraction_result.py(Python) produce parityPipelineExtractionResultoutput; cross-language parity tests cover 8 fixtures × 8 assertions in each language. -
Progressive depth:
brief,detail,referencecolumns oncontent_items+content(full text) -
ContentTabs component with human-authored vs AI-generated toggle per depth level
-
SummaryData (executive/detailed/takeaways) via AI generation
-
Semantic + keyword hybrid search (superseded items excluded by default)
-
Browse with 12+ filter dimensions, saved filter presets (localStorage)
-
Content Library Drawer (Cmd+L in bid session) with search, copy, insert-with-citation
-
File upload to Supabase Storage
-
PDF/DOCX extraction to markdown (unpdf with
---page separators; mammoth HTML→Turndown two-step for table preservation) -
Anthropic Files API integration (upload-once, query-many)
Classification & AI
Section titled “Classification & AI”Capability summary only. Session-by-session narrative is tracked in the session ledgers.
- AI classification via Claude tool use (domain, subtopic, keywords, summary, confidence). Reclassification on demand (
force=true). Taxonomy loaded from DB (taxonomy_domains+taxonomy_subtopics, admin-editable). - AI summary generation (executive/detailed/takeaways). Embedding generation via OpenAI
text-embedding-3-large(1024-dim). Embedding truncation atMAX_EMBEDDING_CHARS = 24_000; TS+Python parity guard. - ClaudePromptButton bridge (contextual prompt generation, opens Claude directly).
- Classification skill (
lib/ai/skills/classification.md, 795 lines) as single source of truth for the classification prompt (v4.5). Loaded at runtime with 6 placeholders resolved fromCLIENT_CONFIG. - 30+ post-extraction entity filters across 9 categories in both TS and Python pipelines (full parity). Entity type taxonomy spec v1.1 (
docs/reference/entity-type-taxonomy-spec.md) — 12 entity types with diagnostic questions, disambiguation matrix, and 8 few-shot examples.CLIENT_DISAMBIGUATIONrules extracted tolib/client-config.tsfor multi-client readiness. - Two-pass entity validation (Pass 1 Sonnet + Pass 2 Haiku) controlled by
validateparameter. Pass 2temperature: 0. Surgical bulk-cert rule with enumerated co-occurrence sets (3 named groups). Architecture:docs/reference/two-pass-validation-architecture.md. Recommendation: batch-only. - Entity-mention dedup at upsert boundary — pre-upsert dedup by
(content_item_id, canonical_name, entity_type)eliminates Postgres error 21000. Mergesmatched_textarrays, sumsmention_count, takes maxconfidence. - Entity relationships UNIQUE constraint —
entity_relationships_unique_tuplecovers(source_entity, relationship_type, target_entity, source_item_id)withNULLS NOT DISTINCT(PG 17+).lib/ai/classify.tsrelationship insert uses.upsert({ ignoreDuplicates: true }); Pythonstore_relationshipstreats 409 as skipped. - Entity-management admin inline type-change — entity detail panel
<Select>+ entity listTypeEditDialogboth via TanStackuseMutationcalling admin-onlyPATCH /api/entities/[canonical_name]/type(writesentity_type_overrideonentity_mentions— citations preserved, no cascade).TYPE_COLOURScovers all 12 entity types. - Publish-from-draft AI processing — when a draft-created item is published and
classified_at IS NULL, the publish path re-runsclassifyContent+regenerateChunksagainst the service client so the item gains entity_mentions, entity_relationships, summary, and content_chunks before becoming live-searchable. Both paths log apublish_classifyentry topipeline_runs. Non-fatal — classify/chunk failure surfaces as a warning but does not un-publish. - Cert classifier holder disambiguation (full TS + Python parity) —
app/api/certifications/route.ts+ MCP toolget_certification_statusfilterholdsrelationships bysource_entity == BRANDING.organisationName.toLowerCase()AND requiremetadata.holderexplicitly set. Python classifier emitsmetadata={holder: 'self'|'supplier', supplier_name?}onentity_mentionswhen holder context is extractable. TSderiveHolderMetadatacarries the rule end-to-end. Synonym fallback acceptscomplies_with/evidencesfor certification targets only when (a) target is a certification entity, (b) source is the client org or extracted org entity, (c) no canonicalholdsrel exists for that target. - Tag canonicalisation at classify write-path —
pluralize@8(TS) +inflect==7.5.0(Python) forcontent_items.ai_keywordsmorphology. Domain carve-outs (11-icsfields-of-study,news/means/series/species, DUNS allowlist) run as a pre-pass override; compound last-token guard prevents Latin/Greek regressions. Admin triage UI for library-vs-actual disagreements lives at/settings?section=tag-morphology(tag_morphology_drift_flagstable, admin/editor RLS). Shared cross-language fixture__tests__/fixtures/keyword-normalisation-cases.json(74 cases) enforces TS↔Py parity. - Structured outputs for tender metadata extraction.
- Entity classification current metrics: precision 72.7%, recall 80.0%, F1 76.2%, type accuracy 94.8%, exclusion compliance 77.8%, cross-item consistency 96.6%. Gold standard fixture: 93 items. ISO family type override forces 6 ISO families to
certificationdeterministically. Iteration log:docs/audits/s154-entity-classification-iteration-log.md. - Temporal metadata bridge —
metadata.ai_temporal_referencesextracted at classify time then bridged to entity mentions viainferContextTypeheuristic +parseDuration(ISO 8601 time components incl. PT72H). 17 generic methodology/framework terms inGENERIC_CONCEPTS(TS+Python parity).
Procurement Workflow
Section titled “Procurement Workflow”Capability summary only. Session-by-session narrative is tracked in the session ledgers.
- Procurement creation with metadata (title, buyer, deadline).
ProcurementCreationWizardequal-weight Create Blank vs Create & Upload Tender (grid-cols-1 sm:grid-cols-2). - Tender upload (PDF/DOCX) with question extraction.
- KB matching with confidence postures (strong/partial/needs_sme/no_content).
- 3-pass AI response drafting (match → draft → stream).
- Tiptap editor with inline editing, citation markers.
- Response versioning (counter + history trigger).
- Procurement workflow state machine (
PROCUREMENT_WORKFLOW_STATES: draft → questions_extracted → drafting → in_review → ready_for_export → submitted → won/lost/withdrawn). Source:lib/procurement/procurement-workflow.ts. - DOCX + XLSX export (
lib/procurement/procurement-export-{docx,xlsx,data,types}.ts). Template completion (python-docx write-back). Tender metadata extraction (Claude tool use). - Procurement draft crash recovery (
useDraftRecovery,DraftRecoveryDialog— localStorage auto-save with session restore). - Readiness checklist (7-criteria API,
ReadinessChecklistcomponent,ReadinessBadge). - Search-to-procurement pipeline (
QuickAssignButton,useQuickAssign,batch-workspacesAPI,?from_bidURL context — URL param preserved for backward-link stability). - Overview tab lean layout —
NextActionCardis the single state-driven CTA; Submission Readiness remains as the reviewer-landing key card. - Citation panel default-open for admin + review-or-later states; collapsed by default for editor + drafting states. Per-question remount via
keyprop preserves the initial default on question switch. - URL-synced tab + filter state on procurement detail (
/procurement/[id]) —?tab=<id>&q=<search>&status=<filter>&sort=<field>drive active tab and question-list filters; deep-linkable, shareable between collaborators. - Q&A library DOCX extractor set —
lib/procurement-library-ingest/{docx-to-markdown,extract-qa-pairs}.tsprovides a pair-level Q&A extractor with markdown-preserving output via two-step mammoth → Turndown pipeline (GFM plugin for tables) keeping bold / italic / links / lists / nested tables intact. Python side hasscripts/docx_cell_to_markdown.py(pandoc subprocess wrapper) andimport_bid_library.pybuild_content_recordemitting canonicalQ: {q}\n\n{standard}\n\n {advanced}markdown. Cross-language parity test (KH_RUN_INTEGRATION=1-gated). Known limitation: Python cell converter preserves bold + italic only while TS preserves links / lists / nested tables. - S248 T4 procurement umbrella rename — code-side surface renamed end-to-end:
lib/bid/* → lib/procurement/*,lib/bid-library-ingest/ → lib/procurement-library-ingest/,components/bid/* → components/procurement/*,types/bid.ts → types/procurement.ts,hooks/bid/* → hooks/procurement/*, app URL paths/bid → /procurement+/api/bids/* → /api/procurement/*, MCP tool renames (show_bid_dashboard → show_procurement_dashboard,formatBidDashboard → formatProcurementDashboard,list_active_bids → list_active_procurement, etc.). Symbols:BID_STATES → PROCUREMENT_WORKFLOW_STATES,BidState → ProcurementWorkflowState,Bid → Procurementcascade across ~60 PascalCase + ~35 camelCase identifiers. CI guard__tests__/validation/no-bid-regression-guard.test.tsprevents regression. Deferred items tracked as backlog ID-21 (TanStack workspace-types retire — reframed S249 Path (c); still parked). ID-22 (RPC return-field rename) and ID-23 (defer-rename investigation forlib/mcp/formatters/bids.ts+lib/ai/skills/bid-writing.md) both closed in subsequent sessions: ID-23 done S249 (commit95b660ef—bids.ts → procurements.ts,bid-writing.md → procurement-writing.md), ID-22 done S250 (migration20260521100650— RPC return-shape rename + broken-body fix).
Governance & Review
Section titled “Governance & Review”Capability summary only. Session-by-session narrative is tracked in the session ledgers.
governance_review_statusoncontent_items: 5 values (pending,approved,reverted,changes_requested,review_overdue).'draft'removed post-publication-lifecycle split. Code-side guard atapp/api/governance/review/route.tsandlib/mcp/tools/governance.tsuses sharedALLOWED_REVIEW_INPUT_STATUSES = ['pending', 'review_overdue']allow-list (lib/governance/review-input-statuses.ts).- Recurring review cadence (§5.5 Phases 1-5 fully shipped).
content_items.next_review_date date NULL+content_items.review_cadence_days integer NULL(CHECK 1-1095). Partial indexidx_content_items_next_review_date(excludes superseded + archived). Daily cron atapp/api/cron/review-cadence/route.ts(03:45 UTC) flagsnext_review_date < CURRENT_DATEitems as'review_overdue'(only for items currentlyNULLor'approved'), writes 1 notification per recipient (idempotent within-day), uses batch summary at threshold 20, recordspipeline_runsrow withpipeline_name='review_cadence'. Auto-renewal in'approve'branch via sharedlib/governance/cadence-renewal.tscomputeNextReviewDate(...)helper. - Recurring review cadence — UI surfaces. “Overdue reviews” toggle in
components/review/review-filters.tsxwith count pill fromstats.overdue; routeapp/api/review/queue/route.tswidens default filter to OR clauseverified_at.is.null,governance_review_status.eq.review_overduewheninclude_overdue=true.components/shared/review-cadence-badge.tsxfour-band matrix: overdue (red) / due ≤14d (amber) / due ≤30d (muted) / >30d (no badge); pure helpercalculateReviewBand()exported for unit testing.components/content/review-cadence-editor.tsxadmin/editor cadence control with<input type="date">+ 5-preset Select (None / 90 / 182 / 365 / Custom 1-1095). Provenanceper-item-tab.tsxReview Schedule subsection renders Next review date / Review cadence / Last reviewed via canonicalformatDateUKhelper. - Recurring review cadence — MCP filter widening.
search_content_chunkswidened withoverdue_review: boolean+review_due_within_days: integer (1-365); RPC migration20260428212936_extend_search_content_chunks_review_filters.sql.get_governance_queuewidened withinclude_overdue: boolean+status_filter: enum('pending'|'review_overdue'|'all'). Existing 4-arg callers unchanged. - Cadence-compliance scorer.
lib/quality/quality-score.tsexports pure helpercadenceCompliancePenalty(nextReviewDate, now)returning0/5-10/15/25/40per spec §9.3 schedule (>30dno penalty /1-30dgraduated linear -10 /1-14doverdue -15 /15-30doverdue -25 />30doverdue -40).freshnessRaw()applies penalty only whennextReviewDateis non-null (preservation rule). Caller wires:content-card.tsx,app/api/cron/quality-score/route.ts,metadata-sidebar.tsxpassnext_review_date+review_cadence_daystoQualityScoreBreakdown. - Unified publication lifecycle (§5.2 — fully shipped).
content_items.publication_status(NOT NULL DEFAULT'published', CHECK enum{'draft', 'in_review', 'published', 'archived'}) is the four-state lifecycle column. Three partial indexes (idx_content_items_publication_status_published,idx_content_items_published_recent,idx_content_items_archived). Bidirectionalenforce_archive_state_consistencyPL/pgSQL trigger enforcesarchived_at <-> publication_status='archived'invariant across 4 directions. PATCH route atapp/api/items/[id]/route.tsacceptsfield='publication_status'with optimistic-concurrency guard (.eq('publication_status', fromStatus)→ 409 PGRST116 on stale write); MCPupdate_publication_statustool mirrors the route semantics; both consume shared helperlib/governance/publication-transitions.ts(VALID_PUBLICATION_STATUSES,computeAllowedTransitions(state, role),applyTransitionSideEffects(...)). All 6 production writers target the new column; AC6.5 grep guard test enforces zero write-position regressions. RPC visibility filter (Phase 3). Eight production RPCs gate visibility onpublication_status='published'by default —hybrid_search,search_for_bid_response,search_content_chunksaccept avisibility_filter VARCHAR DEFAULT 'default'parameter ('default'→ published-only,'all'→ exclude archived,'admin'→ all states); the four coverage/guide RPCs (get_coverage_matrix,get_coverage_summary,get_guide_content,get_guide_coverage) andget_review_breakdown_statsenforcepublication_status='published'in-body (no parameter). Migration20260430192325_widen_search_rpcs_visibility_filter.sql.get_filter_counts(browse-sidebar checkbox counts + Browse-by-domain chip composite) widened in S217 W1C to match — migration20260501173008_widen_get_filter_counts_publication_status.sqlswitches its three sub-aggregations fromarchived_at IS NULLtopublication_status='published'so counts and search results stay symmetric across the same surface. Admin UI opts intovisibility_filter='admin'to see drafts/in-review/archived; default user-facing search and browse return published rows only. Supersession + cron-exclusion (Phase 5).setSupersession()archives the OLD row in the same UPDATE — setspublication_status='archived',archived_at=now(),archived_by, andarchive_reason='Superseded by item ${newId}'(overridable via optionalarchiveReasonarg) in lockstep with the legacysuperseded_by/dedup_status='superseded'writes; the §6.6 trigger keepsarchived_atandpublication_statusin sync, but the explicit dual-write defends against trigger bypass.app/api/cron/review-cadence/route.tsadds.neq('publication_status', 'archived')belt-and-braces alongside the existingarchived_at IS NULLfilter to skip archived rows. Phase 4 UI surfaces (PublicationStatusBadge S212 + publication-review queue tab S215) shipped earlier — see §5 above. - Governance review API (approve, request_changes, revert).
- Review queue page with sticky progress bar, session counter, undo toast.
ReviewQueuePanelwith L-key toggle + item selection. Sort driven exclusively by filter popover’ssortURL param (confidence_asc, quality_score_asc, created_at desc). Review toolbar reduced to two pills (Session summary + Panel toggle) withReviewCadenceCardrendered unconditionally. - Content version history with immutable snapshots; version comparison and rollback.
- Governance config table with preset-based configuration (Light-touch / Strict presets;
presetcolumn ongovernance_config). Admin selects a preset per domain which populates concrete column values at save time. API accepts{ domain, preset }. - Quality-to-governance bridge — auto-triggers review when quality score drops below threshold.
- Freshness-to-governance bridge — auto-triggers review on freshness state transition.
- Classification confidence as review priority — low-confidence items sorted higher, server-side sort.
- Review assignment system —
review_assignmentstable,POST/GET /api/review/assignments,AssignmentManagerUI with assignee/due-date/priority. - Review cadence dashboard —
GET /api/review/cadence,ReviewCadenceCardshowing overdue/due-soon/completed stats. - Browse-review convergence —
useQuickReview,QuickReviewActionson browse cards/rows. - Send-to-review from browse — bulk action to flag items for governance review.
- Diff-to-review pipeline — inline diff highlighting (
DiffHighlightedText,diffWords) with side-by-side + card views. - Verification history — immutable audit trail in
verification_historytable, displayed on item detail. - Provenance via
content_history.change_reason— every version captures WHY it was created. Wired across all 9 TS write paths and Python ingest pipelines. Admin UI “Why change?” input on content-tab editors. Canonical vocabulary indocs/reference/data-entry-points.mdAppendix D. Structural guard test prevents regression. - Source document notifications (
lib/source-document-notifications.ts— notifies content owners on source document updates). - Content owner assignment —
ContentOwnerSelector, bulk assign, owner filter, freshness cron targeting. Single “Assign owner” dialog with a RadioGroup scope toggle (Unowned only / By domain).bulk_assign_ownerMCP tool — admin-only,NON_IDEMPOTENT_WRITE_ANNOTATIONS. Wrapsbulk_assign_content_ownerRPC with scope filter (domain / subtopic / content_type), dry-run preview, cursor pagination (opaque base64url of{last_id, scope_hash}with mid-pagination scope drift detection), skip-if-owned default plusforce_override: trueopt-in, notification control, and per-itemcontent_historyaudit write (change_reason: 'owner_change') viasb()+logBestEffortWarn(). - Notifications system (governance, quality, change-report types).
- AI-generated change reports (originally “digests” pre-KH; renamed S246 + S251 W1B completed full terminology cascade).
/change-reportsperiod-scoped grid with delta-focused counters (items added, items modified, freshness transitions); single period dropdown filter. Separate “Current KB Health” card carries fresh/aging/stale/expired state-of-the-world counts. Auto-gen cost guard: pre-flight item count; iftypedItems.length >= 150(CHANGE_REPORT_AUTO_GEN_MAX_ITEMS), the API returns HTTP 413 with structuredCHANGE_REPORT_TOO_MANY_ITEMSerror and the client renders a “KB too large for auto-summary” empty state.user_notification_prefs.auto_generate_change_reportsopt-out switch in Settings → Profile → Notification Preferences.AbortControllerdrives a cancel button. - Quality flagging (
ingestion_quality_log+ browse filter).
User Management
Section titled “User Management”Capability summary only. Session-by-session narrative is tracked in the session ledgers.
- Supabase Auth with invitation-only registration. 3 roles (admin, editor, viewer) via
user_rolestable. - Phew-domain auth hook.
public.hook_restrict_signup_to_phew_domain(event jsonb)wired to thebefore-user-createdauth event via Supabase dashboard gates sign-ups to@phew.org.ukonly; non-matching emails rejected with HTTP 403. Captured in migration20260424202806_capture_phew_domain_hook.sql(SET search_path = public, extensions+REVOKE EXECUTE ... FROM public, anon, authenticated, service_role+GRANT EXECUTE ... TO supabase_auth_admin). Dashboard wiring is NOT captured in SQL — must be re-configured after any project reset. Full audit:docs/reference/auth-hooks.md. Combined with viewer-default trigger (on_auth_user_created), new Phew users get least-privilege access on first magic-link request. Multi-client table-driven allowlist tracked as backlog 33; admin invite-only flow as OPS-28. display_namecolumn onuser_roles— explicit name field for UI display and review history attribution.- Role-adaptive UI (edit controls hidden for viewers).
- Team management in Settings (invite, role change, remove) — single responsive flex-row list (
TeamMemberRowwithrole="listitem"); inline ghostDeactivatebutton triggers AlertDialog confirm. - Display name resolution via
get_user_display_names(user_ids uuid[])Postgres function, consumed vialib/users/display-names.ts(replaces N+1auth.admin.getUserByIdpattern). - GoTrue NULL-token defensive stack. Runtime
BEFORE INSERT OR UPDATEtrigger onauth.userscoerces NULL →''on 8 GoTrue token columns, preventing upstream GoTrue bug (supabase/auth#1940) from causinglistUsersfailures. 5-layer defence: corrective migration, runtime filter, migration guard test, runtime trigger, integration witness test. Post-restore detection viaverifyPipelineUserShape()inscripts/seed-e2e-users.ts. Full investigation atdocs/audits/s156-gotrue-upstream-investigation.md. scripts/seed-e2e-users.tsCLI provisions E2E test users idempotently viaauth.admin.createUser(). Database rebuild runbook atdocs/operations/database-rebuild-runbook.md.- Settings sidebar consolidation.
tags-section.tsxis a 2-tab container wiringtags-cleanup.tsx(duplicates, domain-grouped, bulk-actions) +tags-browse.tsx(virtual-scrolled per-tag CRUD); auto-selects Clean-up when duplicates exist, Browse otherwise. “For developers” accordion insideConnectionsSectioncarries plugin download /.mcp.jsonconfig / Claude Code setup, gated byuseUserRole().canAdmin. 9-entry sidebar. - Organisation profile — first app-wide organisation-profile surface, promoting SI
company_profilestable to platform-wide grounding context. Schema widened withis_primary boolean NOT NULL DEFAULT false+idx_company_profiles_primary_singletonpartial unique index enforcing at most one primary active profile. Settings → Organisation section (admin + editor only) with 10 editable fields (name, description, website URL, sectors[], services[], certifications[], geographic_scope[], target_customers, value_proposition, key_topics[]).isComplete= name + ≥1 sector + ≥1 service. Slug auto-generates (kebab-case + numeric suffix on UNIQUE collision).GET/PUT /api/organisation/profileviagetAuthorisedClient(['admin','editor'])andsb().hooks/use-organisation-profile.tsreturnsOrganisationProfileStatus. Dashboard nudge renders when!isComplete. Phase 2 (grounding wiring) + Phase 3 (SI deprecation) deferred. - Notification preferences — first first-class settings surface.
NotificationPreferencesrenders as Card sub-section insideprofile-section.tsx(Personal group, all roles); no new sidebar entry. Three email toggles (Weekly Change Report, Review assignments, Owned content flags) all default ON (silence-first with release valve). Backed byuser_notification_prefstable (user_idPK →auth.userscascade, 3 booleans +auto_generate_change_reports, RLS users-own-own-row,update_user_notification_prefs_updated_attrigger,SET search_path = public, extensions). API:GET /api/notifications/preferencesreturns stored prefs or defaults;PUTvalidates withNotificationPreferencesPutBodySchema(strict, rejects empty / unknown / all-undefined) and upserts viasb(). TanStack Query viaqueryKeys.notifications.preferences. Email delivery itself out of scope — this surface ships the settings only; downstream cron jobs query the table directly.
Sector Intelligence
Section titled “Sector Intelligence”Capability summary only. Session-by-session narrative is tracked in the session ledgers.
A complete domain workspace for monitoring and curating sector news. The intelligence pipeline polls RSS/Atom/Web feeds on a 15-minute cron, filters articles for relevance against per-workspace company profiles using a two-stage scoring pipeline (embedding pre-filter + Claude Haiku LLM scoring with HIGH/MEDIUM/LOW/IRRELEVANT criteria), classifies passed articles, and exposes curated content via the KB, MCP tools, and public RSS feeds.
- Schema: 6 tables (
company_profiles,feed_sources,feed_prompts,feed_articles,feed_flags,si_processing_queue) + 3 SI taxonomy domains (legislation-policy,market-intelligence,sector-news) with 19 subtopics.get_due_feed_sourcesandget_filter_ratio_trendRPCs. - Pipeline: Feed poller (
lib/intelligence/feed-poller.ts) with ETag/If-Modified-Since handling on both RSS and web source types (web parity via HEAD pre-flight per RFC 7232 — 304 short-circuit skips Firecrawl credit), 4-tier content extraction (RSS → fetch → Firecrawl → summary fallback), URL normalisation, Google News URL resolution before dedup, feed title whitespace normalisation, concurrency guard viasi_processing_queue, per-domain rate limiting with exponential backoff (getGlobalRateLimiter().waitForDomain()applies to RSS and web equally).validateWebUrl()lives in leaf modulelib/intelligence/url-validation.tsand runs at source-creation time via asyncsuperRefineonFeedSourceCreateSchemaso admins get an immediate 400 for non-HTML URLs. Firecrawl-credit telemetry surfaces viaintelligence.web-source.firecrawl-callSentry breadcrumbs +WebPollResult.firecrawlCalledflag (per-pipeline_runs.resultaggregation deferred to roadmap §3.7.3). Company embedding cache avoids regeneration per run. Per-workspace pipeline-load hoisting.source_urlresolution prefers Firecrawl-resolved URLs (metadata.sourceURL) over raw feed URLs, preventing Google News redirect URLs from persisting oncontent_items. - Management UI: Intelligence hub (
/intelligence), company profile management (/intelligence/profiles), workspace detail with sub-nav (Overview / Sources / Articles / Metrics / Prompts), feed source CRUD with test polling, article review with passed/filtered tabs and flagging, prompt editor with version history and rollback. 11 API routes, 8 TanStack Query hooks. Quiet-week auto-collapse: when the current period has zero new passed articles, zero unresolved flags, and zero sources with errors, the overview’s low-signal sections wrap in a native<details>/<summary>disclosure (HealthPanel + Quick Actions remain always visible). - Output: Public RSS 2.0 feeds (
/api/feeds/[workspaceId]/rssand.../rss/filtered), metrics dashboard with filter ratio trend chart (SVG) and prompt performance table, guide auto-creation (lib/intelligence/guide-generator.ts) with hierarchical structure viaparent_section_idand topic-to-section mappings inlib/intelligence/topic-mappings.ts. UI rendering of hierarchical guides deferred. - MCP integration:
workspace_idfilter onsearch_knowledge_base,get_workspace_itemsandget_intelligence_summarytools, Intelligence Feed MCP App, intelligence formatters. - Prompt Refinement Skill (“Review and Refine”): end-to-end workflow for improving scoring prompts based on user flag patterns.
lib/intelligence/flag-analyser.tsuses Claude to cluster flag patterns and generate prompt recommendations. Three backend routes (analyse flags, preview re-scoring, resolve flags) with admin+editor auth, workspace access checks, and cross-workspace privilege escalation prevention. Rescoring preview uses cursor-based worker pool (concurrency cap 3) with partial-failure semantics viawarningsEnvelope(). UI: refinement panel as primary prompts page interface (6 reachable states), flag analysis view with collapsible pattern clusters, LCS-based prompt diff view, rescoring preview with direction icons (colour never the sole signal). Safety guards: minimum flag threshold warning + catastrophic prompt change warning (role="status").PromptEditorretained as “Advanced: edit prompt directly” disclosure. - Production hardening: Article cleanup cron (90-day retention), AI summariser for passed articles, pipeline health monitoring endpoint, workspace-level relevance threshold,
SOURCES_PER_INVOCATION = 10,coerceSubtopic()for empty-subtopic prevention. Backfill script (scripts/backfill-classify-content-items.ts) for unclassified workspace content. - Starter pack feed seeding: 4 sector-specific starter packs (Education, Safeguarding, Health & Social Care, Procurement) as importable feed-source configurations. Admin-gated seed endpoint (
POST /api/intelligence/workspaces/[id]/seed-starter-pack) with SELECT-before-INSERT idempotency,warningsEnvelope()response, TanStack Query mutation hook. 18 feeds across 4 packs. - Outstanding: Email change-report scheduling, multi-workspace aggregation, and database-level aggregation are SI Phase 2 (deferred). Hierarchical guide UI rendering deferred. See
docs/specs/si-hardening-implementation-plan-s154.mdanddocs/reference/product-roadmap.md§2 for remaining items.
AI Evaluation
Section titled “AI Evaluation”Capability summary only. Session-by-session narrative is tracked in the session ledgers.
A measurement-driven evaluation framework for the AI touchpoints. Built out of the recognition that the entity classification false-positive rate could only be improved with reliable measurement.
- Phase 1 — Evaluation Foundation: Shared eval infrastructure (
lib/eval/types.ts,lib/eval/metrics.tswith precision/recall/F1/ROUGE-L/MRR/NDCG/P@K,lib/eval/baseline.tsfor save/load/regression,lib/eval/reporter.tsfor console + JSON output). 4 gold-standard fixtures: classification (91 items, 11 domains), summarisation (36 items), bid drafting (24 synthetic items), entity classification (95 items, 12 entity types). 4 eval runners (scripts/eval-classification.ts,scripts/eval-summarisation.ts,scripts/eval-search.ts,scripts/eval-bid-drafting.ts). 5 gated Vitest eval test wrappers (EVAL=1). Fixture count guard test runs in the normal suite. Two-tier summarisation refactor (Tier 1 long-form evaluatessummary_data, Tier 2 short-form evaluatesai_summaryonly). Baselines: classification (domain 97.5%, subtopic 97.5%, secondary 84%, keywords 96.5%), summarisation (T1 ROUGE-L 36.1%, T2 47.1%, structural 100%), search (MRR 84.6%, P@5 56.8%, P@10 49.3%). Bid drafting baseline deferred (no real bid data yet).eval:allandeval:entitypackage.json scripts. - Phase 2a — Classification skill conversion: Classification prompt restructured into
lib/ai/skills/classification.md(795 lines) as single source of truth, loaded at runtime with 6 placeholders. - Phase 2b-2c — Filters + diagnostic questions: 30+ post-extraction entity filters across 9 categories (TS+Python parity). 12 entity types with diagnostic questions, disambiguation matrix, 8 few-shot examples. Entity types reference loaded at runtime.
- Phase 2d — Two-pass entity validation: Pass 1 (Sonnet) + Pass 2 (Haiku) validation. Controlled by
validateparameter. Architecture:docs/reference/two-pass-validation-architecture.md. Pass 2 trades -6.6 pp recall for +6.8 pp precision (net F1 -2.6 pp); recommendation batch-only. Pass 2temperature: 0(+2.5 pp cross-item type consistency). Surgical bulk-cert rule (3 enumerated co-occurrence sets) restored two-pass precision to 91.0%. Full audit:docs/audits/two-pass-cost-quality-measurement.md. - Wave 3 quick fixes: Entity eval migrated to shared infra with
--livemode. BERTScore integration. Gold-standard fixtures expanded (classification 79→91, entity 85→95 items).
Outstanding: AI Eval Phases 3-5 implementation not started (regression CI infrastructure, human-in-the-loop refinement, full coverage). Phase 3/4/5 specs implementation-ready. Bid drafting eval baseline blocked on real bid data. Pass 2 prompt hardening to close the recall regression remains scoped via roadmap §3.1. See docs/reference/product-roadmap.md §3 for remaining items.
6. Progressive Depth vs Content Layers — Current State
Section titled “6. Progressive Depth vs Content Layers — Current State”Progressive depth is built and working. Three columns on content_items:
brief— executive/sales-level summary (human-authored)detail— detailed explanation (human-authored)reference— technical/reference material (human-authored)content— canonical full text
Plus AI-generated equivalents in summary_data JSONB (executive, detailed,
takeaways). ContentTabs shows these as tabs with a human/AI toggle when both
exist.
Progressive-depth generation (S189). Automated progressive-depth generation
via scripts/kb_pipeline/progressive_depth.py, wired into post_insert.py for
all Python ingest paths. Generates brief and detail columns from content
text. 380-row backfill applied to production corpus. 44 Python tests.
Content layers are fully implemented (S100-S107). Items have a
metadata.layer value (sales_brief, bid_detail, company_reference, research)
managed via the layer_vocabulary table with admin UI in Settings. The
LayerSwitcherNav shows linked items sharing a topic_id,
TopicLayerComparison enables cross-layer comparison, and
LayerSuggestionBanner suggests layers post-creation. Feature-gated via
content_layers flag (enabled by default). 251 items have layer assignments.
This is a SEPARATE concept from progressive depth, though there is conceptual
overlap:
- Progressive depth = different levels of detail for the SAME content item
- Layers = different PURPOSE/AUDIENCE views of the same TOPIC across multiple items
These are orthogonal. A Sales Brief item about “SCP Market Overview” could have
its own brief/detail/reference depth. A Bid Detail item about the same topic
could have different brief/detail/reference content. The topic_id in metadata
links them as the same topic at different audience levels.
7. Governance vs Document Control — Current State
Section titled “7. Governance vs Document Control — Current State”Governance review (built): Tracks whether CHANGES to existing content have
been reviewed. Trigger: content is edited → status becomes pending → reviewer
approves/requests changes/reverts. This is change management.
Document control (not built): Would track whether content is READY FOR USE. Lifecycle: draft → in_review → published → archived. This is publication management.
The governance system does NOT gate visibility. All content is visible to all authenticated users regardless of governance_review_status. There is no concept of “draft content that only the creator can see.”
8. AI Integration Points (Current)
Section titled “8. AI Integration Points (Current)”AI Service Layer (lib/ai/)
Section titled “AI Service Layer (lib/ai/)”All AI operations are centralised in lib/ai/ with 12 modules. API routes
import from @/lib/ai rather than calling Anthropic/OpenAI SDKs directly. Key
modules:
| Module | Purpose | SDK |
|---|---|---|
classify | Domain, subtopic, keywords, summary, confidence | Anthropic (tool use) |
summarise | Executive/detailed/takeaways summaries | Anthropic |
change-reports | Change report generation | Anthropic |
embed | Embedding generation (1024-dim), env-var-driven model/dims | OpenAI |
match | KB matching with confidence assessment | OpenAI + Anthropic |
draft | 3-pass bid response drafting pipeline | Anthropic |
extract-content | Structured content extraction | Anthropic |
extract-questions | Tender question + metadata extraction | Anthropic (tool use) |
vision | PDF/image analysis | Anthropic (vision) |
quality-check | Deterministic + AI response quality checks | Anthropic |
errors | AIServiceError with HTTP status codes | — |
skills/ | 5 Markdown skill files + loader | — |
API Route Integration Points
Section titled “API Route Integration Points”| Integration | Route | AI Service Module |
|---|---|---|
| Classification | /api/items/[id]/classify | classifyContent() |
| Summary generation | /api/summaries/generate | generateSummary() |
| Bid question extraction | /api/bids/[id]/questions/extract | extractPDFQuestions() / extractDOCXQuestions() |
| Bid KB matching | /api/bids/[id]/match | generateQueryEmbedding() + assessConfidence() |
| Bid response drafting | /api/bids/[id]/responses/draft | runDraftingPipeline() |
/api/copilotkit | Removed — replaced by ClaudePromptButton bridge | |
| Content extraction | /api/extract | extractStructuredContent() |
| Tender metadata | /api/bids/[id]/extract-metadata | extractTenderMetadata() |
| Digest generation | /api/change-reports/generate | generateChangeReport() |
| PDF vision | /api/vision | analyseVision() |
MCP Server Integration
Section titled “MCP Server Integration”Capability summary only. Session-by-session narrative is tracked in the session ledgers.
The MCP server (lib/mcp/ — 30+ .ts files across tools/, formatters/, and
top-level modules; 58 tools, 12 resources, 7 prompts, 4 MCP Apps) provides
external AI client access to the same AI service layer:
| MCP Tool | AI Module Used | Role Required |
|---|---|---|
search_knowledge_base | generateEmbedding() | Any authenticated |
classify_content | classifyContent() | Editor+ |
generate_summary | generateSummary() | Editor+ |
create_content_item | generateEmbedding() | Editor+ |
search_qa_library | generateEmbedding() | Any authenticated |
find_similar_items | generateEmbedding() | Any authenticated |
find_duplicate_candidates | generateEmbedding() | Any authenticated |
search_content_chunks | generateEmbedding() | Any authenticated |
search_content_chunks returns heading-bounded chunks rather than whole
documents, with breadcrumb heading_path and similarity score.
get_content_item appends a Document Sections list summarising the item’s
chunks. create_content_item regenerates chunks for non-draft items on insert.
Other MCP tools query Supabase directly without AI module calls. Three tools
(#22-24) are app triggers that serve MCP App UIs (Coverage Matrix, Bid
Dashboard, Reorient Me). Tools #25-26 provide archival and duplicate management.
Tools #27-29 provide template coverage analysis. Tool #30 provides batch
governance status updates. Tool #33 provides source document version lineage.
Tool #34 (get_certification_status) provides
certification/framework/registration status and expiry data. Tool #35
(get_document_diff) provides Q&A pair-level diffs between source document
versions.
Capabilities live across the layer:
- HTML→markdown bridge in
lib/content/html-to-markdown.ts(GFM-aware, idempotent) — fixes the raw-HTML-in-markdown-payload class. defineToolwrapper + annotation invariant atlib/mcp/tools/shared.tswith five named annotation constants (READ_ONLY_ANNOTATIONS/SAFE_WRITE_ANNOTATIONS/DESTRUCTIVE_WRITE_ANNOTATIONS/NON_IDEMPOTENT_WRITE_ANNOTATIONS/NON_IDEMPOTENT_OPEN_WORLD_WRITE_ANNOTATIONS); all tools register through the wrappers withRequired<ToolAnnotations>enforced; regression test at__tests__/mcp/tool-annotations-coverage.test.ts;scripts/lib/mcp-parser.tsand the codebase-stats generator recognise both wrapper and legacy registration patterns.- Guide tools + builder skill — four tools (
create_guide,update_guide,get_guide,list_guides) atlib/mcp/tools/guides.ts, formatters atlib/mcp/formatters/guides.ts, schemas reused fromlib/validation/guide-schemas.ts; plugin skillknowledge-hub:guide-builderships an 8-step conversational workflow. create_content_itemtyped provenance +pipeline_runsaudit. Three typed optional fields (source_url,source_file,source_document_id) persisted to typed columns; legacymetadata.source_documentwrites return Zod errors. Every invocation emits exactly onepipeline_runsrow viarecordPipelineRun({ pipelineName: 'mcp_create_content_item', … }); audit coverage spans success / partial / draft / auth-fail / catch-all paths (lazy-imported service-role client where editor RLS would otherwise drop the row).content_owner_idauto-assign at all 6 ingest entry points vialib/auth/owner-default.ts::resolveContentOwnerId({ explicit, role, userId }). 5-of-6 ingest payload Zod schemas widened with optionalcontent_owner_idfield;KBIntegrationBodySchema(EP10) is hard-coded route-side. Service-account UUID list canonical:['a0000000-0000-4000-8000-000000000001'].- Ingest path consistency end-to-end.
content_items.ingest_sourcetyped column populated with one of 11 canonical values across all 8 INSERT-time entry points (4 TS + 4 Python).ensure_v1_history_at_commit()is the single authority for v1content_historyrows; app-level v1 inserts have been removed. Inverted guard at__tests__/validation/content-items-v1-history-guard.test.tsenforces the trigger-sole-authority contract. Canonicalpipeline_runs.pipeline_namelist lives indata-entry-points.mdAppendix G. outputSchemaruntime-validation scaffold on 5 highest-usage tools. ZodoutputSchemaregistered viadefineTool(...)config onsearch_knowledge_base+search_content_chunks(lib/mcp/tools/search.ts),get_governance_queue+review_governance_item(lib/mcp/tools/governance.ts), andget_change_report(lib/mcp/tools/change-report.ts). Ten Zod schemas exported fromlib/mcp/formatters/{search,governance,change-report}.tsmirroring each response interface field-for-field (nullability preserved). Smoke test__tests__/mcp/output-schema-smoke.test.tsexercisessafeParse()for known-good + known-bad payloads across the 5 tools. Full ~58-tool rollout deferred (R-WP22, Wave 7) pending main-track MCP cleanup. Source:docs/specs/id-16-ast-dataflow-tool/type-safety-pipeline/TECH.md§WP-E.
Admin Provenance Surface
Section titled “Admin Provenance Surface”Capability summary only. Session-by-session narrative is tracked in the session ledgers.
Admin-only route /provenance with 5 tabs, gated by useUserRole() on the
client and getAuthorisedClient(['admin']) on all API routes.
- Per-item tab — UUID lookup returning classification (confidence,
domain/subtopic, full reasoning), processing (model, tokens, cost via
lib/provenance/pricing.ts), and drafting (bid response attribution withPIPELINE_SYSTEM_USER_IDpromotion). API:GET /api/provenance/item/[id]. - Pipeline Health tab — keyset-paginated admin API at
GET /api/admin/provenance/pipeline-runswith server-side rollup, 20k truncation guard, time-range + kind filters, failure drawer. - Audit tab — 1:1 lift of
ActivitySectionfrom/settings; heading renamed to “Audit”; same filters andActivityFeedcomponent. Hosts the PDF export button. - Cost tab (stub) — queries
pipeline_runs.costaggregate; labelled “Interim — Wave B”. - Disputes tab (stub) — queries
classification_disputes; labelled “Interim — Wave C”. - PDF export —
GET /api/admin/provenance/export/verification-historygenerates a downloadable A4 PDF (via@react-pdf/renderer) with day-grouped verification events, UK date formatting, and access logging viarecordPipelineRun. - Schema:
classification_disputestable (RLS, 5 policies, 4 indexes, resolution-completeness CHECK) + 7 nullable cost/token columns oncontent_items. Twopipeline_runsindexes for Pipeline Health queries. drafted_bypromotion: three AI-drafting routes changed fromnulltoPIPELINE_SYSTEM_USER_ID. Per-item tab remaps to “Knowledge Hub”.- Policy: AI-visibility policy §2 amended with admin Provenance carve-out.
- Redirects:
/activity→/provenance?tab=audit;/settings?section=activity→/provenance?tab=audit; command palette entry relabelled “Provenance › Audit”.
AI-Visibility Enforcement
Section titled “AI-Visibility Enforcement”Capability summary only. Session-by-session narrative is tracked in the session ledgers.
The AI-visibility policy is fully enforced across all user-facing surfaces.
No model names, cost pills, token counts, classification reasoning, or raw
confidence percentages are visible to viewer roles. Admin Provenance carve-out
at /provenance?tab=per-item exposes all AI-mechanism data. Editors and admins
see classification_confidence as a plain-text percentage inside the
/item/[id] Source Information accordion only — accordion is collapsed by
default, has no AI branding or colour coding, and no accompanying mechanism
fields. Full policy: docs/reference/ai-visibility-policy.md.
Provenance E2E + MCP Eval Coverage
Section titled “Provenance E2E + MCP Eval Coverage”- 4 Provenance E2E specs (15 tests): admin access + role blocking, tab navigation + deep-links, redirects, Per-item UUID lookup, Pipeline Health filters, Audit tab, PDF export download.
- MCP guide tool eval L3: 4 checks (token efficiency + structural quality).
- MCP guide tool eval L4: 6 checks — full CRUD cycle + error paths with cleanup.
MCP Apps
Section titled “MCP Apps”Four MCP Apps built (Vite + vanilla TS single-file builds, inlined in
lib/mcp/app-bundles.ts):
| App | Trigger Tool | Resource | Purpose |
|---|---|---|---|
| Coverage Matrix | show_coverage_matrix | ui://coverage-matrix | Domains x freshness grid, drill-down, heat map, quality indicators |
| Bid Dashboard | show_bid_dashboard | ui://bid-dashboard | Bid cards, urgency sorting, progress bars, drill-down to detail |
| Reorient Me | show_reorient_me | ui://reorient-me | Personal briefing with urgent items, active bids, recent work, team changes |
| Intelligence Feed | show_intelligence_feed | ui://intelligence-feed | Sector intelligence dashboard with passed articles, filter ratios, prompts |
Plugin
Section titled “Plugin”Knowledge Hub plugin at .claude/plugins/knowledge-hub/1.0.0/ — 6 commands, 9
skills. Published to local marketplace. lib/mcp/plugin-bundle.ts is a
committed artefact (generated by bun run build:plugin).
Plugin skills:
procurement-writing,classification,content-creation,content-governance,guide-builder,knowledge-synthesis,search-strategy.governance-review— 9-step triage workflow for the governance queue, coexists withcontent-governance(framework) as the active-workflow companion. (Renamed fromchange-managementto avoid collision with the Anthropicoperations:change-managementskill.)daily-briefing— 5-persona daily stand-up skill that composes with the Anthropicsales:daily-briefingvia explicit skill-name delegation (not trigger-phrase routing), with a KB-only fallback when the Anthropic plugin is absent on the host. Useslist_user_workspacesMCP tool to resolve the intelligence workspace before invokingget_intelligence_summary.
Maintenance Layer:
- Taxonomy Sync:
scripts/sync-plugin-taxonomy.tssynchronises skill files with canonical docs/schemas via<!-- TAXONOMY_INJECT -->markers. - Consistency Tests:
__tests__/plugin-taxonomy-consistency.test.tsvalidates plugin-codebase alignment. - Pre-bundle Validation:
scripts/bundle-plugin.tsenforces taxonomy validity before generating the Base64 ZIP bundle.
Background Automation
Section titled “Background Automation”Five Vercel cron jobs implemented:
- Freshness transitions — daily scan for content state changes (fresh to ageing to stale to expired)
- Content gap alerts — weekly scan for template requirement gaps, creates notifications
- Classification quality — weekly audit of low-confidence classifications
- Coverage alerts — weekly domain coverage threshold checks
- Quality score — periodic quality score recalculation, triggers governance review when scores drop below threshold
Pipeline run tracking: recordPipelineRun() records every cron and
ingest outcome with sb() fail-fast + Sentry alerting. All 5 cron handlers
migrated. Admin dashboard tile shows last-24h pipeline status.
Background queue infrastructure (lib/queue/* + app/api/cron/process-queue + app/api/jobs/[id]/cancel):
End-to-end queue contract — chokepoint enqueue API, cron worker, backoff
window, visibility-timeout RPC, FIRST migration candidate bid_draft_all
shipped end-to-end, 14 W3 integration tests + 11 W4 unit tests + 12 W4
integration tests. Producer side:
QueueJobPayload<TBody> envelope type (envelope.ts) carries auth_context
(user_id + role + optional workspace_id) so workers can reconstruct the
caller’s authorisation state via createServiceClient() + user_roles
re-validation; buildIdempotencyKey({ jobType, scopedId, requestHash, dateUtc? })
enforces the spec §5.5 formula <job_type>:<scoped_id>:<YYYY-MM-DD>:<requestHash>
(date bucket mandatory to prevent cross-day dedup-forever). enqueueQueueJob
wraps the dedup SELECT + INSERT in tryQuery / sb and returns
{ jobId, deduplicated }. emitQueueSentry + emitQueueAnalytics publish
typed lifecycle events with PostHog-stub fallback via Sentry.addBreadcrumb({ category: 'queue.analytics' }). Worker side: app/api/cron/process-queue/route.ts
(GET, maxDuration=60 post-S224 W4 §5.4.1 D-3 ratification —
Vercel Pro plan; TIMEOUT_BUFFER_MS=50_000 keeping 10s headroom from the
60s function cap; * * * * * registered in vercel.json post-S224 W5
operational tweak — cron cadence flipped from */5 * * * * to 1-minute
cadence per Vercel Pro to cut worst-case queue-poll latency 5×) verifies
cron auth, calls reap_stuck_jobs(p_timeout_seconds) RPC (5-min default,
flips orphaned processing rows back to pending with atomic
attempts = attempts + 1), then loops calling claim_next_job() RPC
(WHERE status = 'pending' AND updated_at <= NOW() — backoff-window
gate so requeued rows are invisible until their backoff expires;
FOR UPDATE SKIP LOCKED concurrency-safe) within a 50s budget — for each
row, dispatches via runJobByType(job, supabase) switch — case 'bid_draft_all'
registered post-S224 W4, case 'batch_reclassify' registered post-S225 W1
(7 remaining historic job_types fall through to permanent-failure default
PermanentJobError('no_handler_registered: …') until each §5.4.x candidate
registers its own case). Failure classifier
handleJobFailure distinguishes transient (Anthropic 429/503, supabase 503,
embedding timeout, Firecrawl 5xx — retry with linear-with-jitter backoff
(attempts × 30s) + random(0..5000ms) written as future-dated updated_at)
from permanent ({permanent:true} duck-type on PermanentJobError —
status='failed', no retry; attempts still increments to record the
attempt that was made); transient at attempts ≥ max_attempts writes
status='dead_lettered'. Auth re-validation via
reValidateAuthContext(serviceClient, userId, enqueuedRole, requiredRole)
returns {ok:true} | {ok:false, reason} with verbatim spec §4.2 error
messages. User-initiated cancellation via PATCH /api/jobs/[id]/cancel
(admin/editor only, race-safe .in('status', ...) filter widened
post-S225 W1 to include 'processing' for opt-in job_types in
COOPERATIVELY_CANCELLABLE_JOB_TYPES allow-list at
lib/queue/cooperative-cancel.ts — currently ['batch_reclassify'];
non-opt-in types still return 409 on 'processing' or terminal,
preserving §5.4.1 hard-409 behaviour for bid_draft_all). processing_queue table extended with
idempotency_key text + partial UNIQUE index
WHERE status IN ('pending','processing','completed') + status enum
widened to include 'dead_lettered' + job_type CHECK enum widened to
11 values (8 historic + 'bid_draft_all' per S224 W4 §5.4.1 IMPL +
'batch_reclassify' per S225 W1 §5.4.2 IMPL + 'markdown_batch' per
S226 §5.4.4 IMPL) + RLS tightened to admin-only SELECT/UPDATE/DELETE +
editor+admin INSERT. Archive pg_cron job
archive-processing-queue runs weekly (Sunday 03:00 UTC) deleting
completed/failed/cancelled/dead-lettered rows older than 30 days
(supabase/migrations/20260506131535_s226_archive_processing_queue_pg_cron.sql
— closes infra spec §9 R4 archive-strategy gap that §5.4.1 + §5.4.2
each deferred).
Lifecycle + concurrency integration tests at
__tests__/integration/queue/{lifecycle,concurrency}.integration.test.ts
exercise all 12 ACs from W3 spec §8 through real Supabase staging
(S223 W3); §5.4.1 unit + integration + E2E tests at
__tests__/lib/queue/handlers/bid-draft-all.test.ts (11 cases) +
__tests__/integration/queue/bid-draft-all.integration.test.ts (12 cases
covering AC-1..AC-9 — env-gated, run via bun run test:integration) +
e2e/tests/bid-draft-all.spec.ts (Playwright click→queued→polling →
button-disable, S224 W4-C). All tests follow real-behaviour discipline:
no mocked supabase, no mocked queue lib; assertions only on observable
DB state transitions; mock external API boundaries only.
§5.4.1 batch-draft-all (FIRST migration candidate, SHIPPED S224 W4).
lib/queue/handlers/bid-draft-all.ts (404L) iterates bid_questions
ordered by (section_sequence, question_sequence), calls
runDraftingPipeline per question (continue-with-partial — D-2 ratified),
upserts bid_responses with drafted_response_ids: string[] tracked for
pipeline_runs.items_created, throws PermanentJobError on
bid-not-found / bid-not-draftable / zero-questions; case 'bid_draft_all'
in lib/queue/dispatch.ts validates envelope, calls
reValidateAuthContext(... 'editor') per D-1 ratification, dispatches
handler, then finalises caller-allocated pipeline_runs row via
DIRECT UPDATE (NOT recordPipelineRun() — INSERT-only helper would
create a 2nd row; drift documented inline; verifier confirmed
SELECT count(*) WHERE pipeline_run_id = … = 1). Producer
app/api/bids/[id]/responses/draft-all/route.ts refactored from sync
326-line loop to 175-line 202+enqueue (SHA-256 truncated requestHash,
crypto.randomUUID() pipelineRunId, pipeline_runs INSERT with
status='running', enqueueQueueJob chokepoint, returns
{job_id, pipeline_run_id, status: 'queued', deduplicated},
maxDuration=30 per D-6). UI hook hooks/bid/use-bid-actions.ts
mutation refactored from sync {drafted, skipped, failed} to async
polling via useQuery({refetchInterval: 3000}) against
/api/jobs/${jobId}/status, toast on click + dedup-aware copy + terminal
status branching (completed/completed_with_errors/failed/dead_lettered/
cancelled). Migration 20260505164817_s224_widen_job_type_check_bid_draft_all.sql
applied to staging + prod; idempotent re-affirm follow-up
20260505173311_s224_post_impl_schema_doc_sync_reaffirm.sql syncs
SCHEMA-QUICK-REFERENCE.md §8 row.
§5.4.2 batch-reclassify (SECOND migration candidate, SHIPPED S225 W1).
lib/queue/handlers/batch-reclassify.ts is the literal extraction of
the CLI’s L943-1242 main loop — taxonomy load, candidate filter,
per-item Anthropic classify + entity-extract + write logic — with the
dry-run branch removed (queued = always execute), stdout logging
swapped for result.results[] aggregation, and Anthropic SDK
instantiated at handler call-time (not module-load). Continue-with-
partial per item per D-2 ratified default; per-item Anthropic
permanent errors (content-policy refusal post-prompt-tightening eval)
record status: 'failed' and continue, escalating to a
PermanentJobError('eval_rule_regression: …') if the per-item failure
rate exceeds 80% of items processed (operator notice path per spec
§5.2). Cooperative cancellation via processing_queue.status poll
every 10 items: handler reads via createServiceClient() from
@/lib/supabase/server, breaks the loop on 'cancelled', returns
partial result with cancelled: true marker. case 'batch_reclassify'
in lib/queue/dispatch.ts validates envelope, calls
reValidateAuthContext(... 'editor') per D-1 ratified flip (matches
§5.4.1 D-1 ratified default for §5.4.x family symmetry), dispatches
handler, then finalises caller-allocated pipeline_runs row via
DIRECT UPDATE (NOT recordPipelineRun() — same drift as §5.4.1;
verifier confirmed count(*) = 1 end-to-end). Cancel-detected
finalisation writes status='completed_with_errors' with
error_message='cancelled mid-run after N/M items' per D-9.1 (no new
enum value). Producer route
app/api/admin/batch-reclassify/route.ts POST returns HTTP 202 with
{job_id, pipeline_run_id, status: 'queued', deduplicated} — auth via
getAuthorisedClient(['admin', 'editor']) per D-1 flip, body parse via
parseBody(BatchReclassifyBodyZodSchema, request), optionsHash =
SHA-256 hex truncated 16 char of canonical-JSON body (alphabetical
keys), idempotencyKey via
buildIdempotencyKey({ jobType: 'batch_reclassify', scopedId, requestHash: optionsHash })
(date bucket per §5.5), pre-allocated pipelineRunId,
pipeline_runs INSERT with status='running', enqueueQueueJob
chokepoint, maxDuration=30. UI page deferred per D-4 ratified flip
(CLI-only with future UI — producer endpoint exists for future UI to
call but no /admin/reclassify page in this candidate). CLI deprecation
banner per D-7 ratified default printed on every --execute
invocation routing operators to future UI; full deletion deferred to
follow-up §5.4.x. body.workspace_id body-only encoded per D-8
(client_id is non-UUID 'default'; envelope auth_context.workspace_id
omitted, handler reads body.workspace_id). Migration
20260505211806_s225_widen_job_type_check_batch_reclassify.sql
applied to staging + prod — paired with lib/queue/envelope.ts
JobType union widening + SCHEMA-QUICK-REFERENCE.md §8 row +
Last verified bump in same commit per
feedback_doc_freshness_guard_per_commit and
feedback_db_check_ts_union_paired_widening. AC-11 (UI flow E2E)
deferred per D-4 flip — 10 of 11 spec §8 ACs covered by 17 unit + 13
integration env-gated + 14 producer-route + 2 cancel-route
cooperative cases.
§5.4.4 EP2 markdown-batch (THIRD migration candidate, SHIPPED S226).
lib/queue/handlers/markdown-batch.ts is a thin wrapper around the
existing orchestrateMarkdownBatch({phase:'import', ...}) from
lib/ingest/markdown-orchestrator.ts — the orchestrator already
implements the full Pattern E lifecycle (at-start INSERT via
startPipelineRun, mid-flight updatePipelineProgress writes, terminal
UPDATE via finaliseRun) so the handler does NOT touch pipeline_runs
directly. KEY DIFFERENCE from §5.4.1 + §5.4.2: the dispatch case for
markdown_batch does NOT do an inline pipeline_runs.update —
orchestrator’s finaliseRun already wrote terminal status (avoiding
double-write). Cooperative cancellation cadence=1 per spec §10 D-8
ratified flip (poll BEFORE every file, NOT every-N-items as in §5.4.2 —
typical markdown_batch is 1-3 files, max ~10, so every-N would defeat
the purpose); allow-list extended to
['batch_reclassify', 'markdown_batch']. body.caller_user_id mismatch
defence per spec §4.3:783-784 throws
PermanentJobError('caller_user_id_mismatch'). Producer route
app/api/ingest/markdown/route.ts phase=import branch refactored from
sync await orchestrate(...) to 202+enqueue: pre-allocate
pipelineRunId, INSERT pipeline_runs with status='running', pipeline_name='upload_markdown_batch', compute fileSetHash =
SHA-256-hex truncated 16 char of sorted-by-filename
[{filename, contentSha256: sha256(content)}] JSON (per-file content
hash nested per D-9 ratified flip — NOT filenames+sizes, NOT
concatenated buffer — collision-resistant), call enqueueQueueJob,
return {job_id, pipeline_run_id, status: 'queued', deduplicated}.
maxDuration=60 per D-7 ratified flip (was 300 sync). phase=analyse
branch UNCHANGED — still returns sync 200 + {analysis}. Path B UPSERT
per D-11: lib/pipeline/start-run.ts switches .insert(...) to
.upsert(..., {onConflict:'id', ignoreDuplicates:true}) to absorb the
producer-pre-INSERT vs orchestrator-at-start-INSERT PK collision.
new Date(Date.now()).toISOString() symmetry pattern enables
deterministic vi.spyOn(Date, 'now') testing. UI surface:
components/create-content/upload-tab-content.tsx ships Cancel
batch button (mandatory per D-8 ratification — business users
won’t use API/CLI) visible whenever polled pipeline_runs.status === 'running'; deduplicated: true toast renders “Already importing —
joining the existing batch…”. Migrations:
20260506125704_s226_widen_job_type_check_markdown_batch.sql
(JobType CHECK 10→11) + 20260506131535_s226_archive_processing_queue_pg_cron.sql
(D-5 scope-in: weekly archive job — closes infra R4 gap). 12 ACs ship
(AC-12 added for archive cron); V_W1 PASS WITH NOTES, 0 HIGH/MEDIUM,
2 LOW (caller_user_id mismatch + auth-context rename — both fixed
fix-V_W1).
§5.4.5 queue-operational-dashboard spec v1 DRAFT at
docs/specs/queue-operational-dashboard-spec.md (S225 W3-A spec +
V_W3 fix — 10 sections / 6 ACs / 6 D-x / 7 risks; 6 D-x awaiting Liam
ratification covering surface choice, metric set, data source, alert
thresholds, refresh cadence, cost surface visibility — IMPL deferred
to a follow-up session post-ratification). Each §5.4.x candidate
registers its own case in dispatch + extends the JobType union via
paired DB-CHECK + TS-union widening per
feedback_db_check_ts_union_paired_widening.
Template-Driven Completeness
Section titled “Template-Driven Completeness”- Template requirements table with 2 catalogued templates (Standard SQ, Charnwood ITT)
- Template coverage analysis on Coverage page (Templates tab)
- Gap summary banner with requirement-type breakdown
- Auto-mapping of template fields to bid questions
- Template fill and download workflow
- Gap-driven content creation — content creation skill, cron-triggered gap alerts, notification types for coverage gaps
- Product Guide subtopic_filter wiring. Data migration
20260422174420populatessubtopic_filteron 57guide_sectionsrows (19 sections x 3 Product Guides), enabling section-to-content resolution. - Research Feed sections. Data migration
20260422174117adds a Research Feed section to each of the 3 Product Guides;guide_sectionstotal is 60.
Test Infrastructure
Section titled “Test Infrastructure”Capability summary only. Session-by-session narrative is tracked in the session ledgers.
- E2E critical coverage live. 9 Must-tier gaps deliberate-break verified across OAuth consent, MCP tool invocation, bid create, file/URL ingestion, viewer write enforcement, bid draft-stream, bid regen+restore plus conditional-assertion hardening on existing specs.
- React act() regression guard.
__tests__/setup.tsregisters aconsole.errorwrapper that throws on/wrapped into act|not wrapped in act|inside a test was not wrapped in act/— surfaces the offending test via Vitest’s stack trace at the exact call site rather than letting act warnings drift across CI shards. Composable with the 11 existing per-testvi.spyOn(console, 'error').mockImplementationoverlay-and-restore patterns. Backed by S37 W4 verification scan: 0 act-warning regressions across 12,481 tests at activation. Memoryfeedback_react_act_warning_classesdocuments the 3-class taxonomy (A bare dispatchEvent, B child useEffect fetch, C waitFor drain). - Guard tests:
claude-md-consistency.test.ts(107 checks),doc-freshness.test.ts(14 checks),mcp-fixture-sync.test.ts(20 checks),content-history-change-reason.test.ts(3 checks),reference-doc-edit-coupled-freshness.test.ts(12 checks; tracked-doc edits coupled to<!-- Last verified -->header bumps in the same commit, plus migration-coupled SCHEMA-QUICK-REF freshness with auto-skip for pure-DML),roadmap-no-shipped-rows.test.ts(3 checks; fails CI on Done/Shipped/Closed rows inproduct-roadmap.md),backlog-no-closed-rows.test.ts(7 checks; fails CI on strikethrough/closure-status rows in the active backlog table). - Silent-failure prevention.
sb()fail-fast wrapper +warningsEnvelope()+logBestEffortWarn()+tryQuery()Result-type helper from@/lib/supabase/safe. ESLint rulelocal/no-unchecked-supabase-erroraterrorlevel scoped toapp/api/**/*.ts+lib/**/*.ts+app/**/*.tsx(App Router Server Components included;**/*.test.tsx+**/__tests__/**/*.tsxexcluded). Client-side extension vialib/client-telemetry.ts::captureClientException()replacingconsole.erroranti-pattern. Spec:docs/specs/silent-failure-prevention-spec.md. - Supabase-result Record-cast prevention. ESLint rule
local/no-supabase-record-castaterrorlevel scoped tolib/**+app/api/**. Flagsas Record<string, unknown>/as Record<string, any>casts on Supabase result rows (direct chain, destructureddata, RPC.data). Four escape hatches: 22-entry JSONB column allowlist (mirroring type-safety-pipeline TECH.md §JSONB inventory), third-partyfetch()/axiosresponses, test/spec/e2e file paths, expliciteslint-disable-next-linewith justification. Source:docs/specs/id-16-ast-dataflow-tool/type-safety-pipeline/TECH.md§ESLint rule design. Test suite ateslint-rules/tests/no-supabase-record-cast.test.ts(20 RuleTester cases). Pairs with R-WP17 type-drift detector (.type-drift-report.md, gitignored at repo root; regenerated bybun run ast-dataflow type-drift-detect --pretty) — drift report names interfaces with no route annotation; ESLint rule prevents new Record casts on Supabase rows. - Knip integrated with deterministic baseline. Unused file/export/type
detection via
bun run knip+bun run ci:knip-check. Single canonical config atknip.config.ts(knip.json retired); Tailwind v4@plugin/@importCSS directives detected viasyncCompilers.cssregex (Jimmy Guzman pattern) so@tailwindcss/typographyno longer needs anignoreDependenciesshim. Baseline.knip-baseline.jsontracked in CI (“tighten or trace” discipline perdocs/runbooks/ci.md§6.3.1) — current{types: 285, exports: 57, dependencies: 0}. Triage doc atdocs/audits/kh-production-readiness-phase-1/research/19-knip-baseline-triage.mdenumerates the 4 buckets (BACKWARD-COMPAT-REEXPORT, RECENT-IMPL-ORPHAN-NEEDS-WIRING, GENUINELY-DEAD, PUBLIC-API-INTENTIONAL) and the path to OPS-50 ≤170 types target via Wave B (S25+ post-handover prep). - Test-DB wiring audited. 11 integration + 34 E2E specs are env-sourced
with zero hardcoded project IDs and zero write-without-cleanup patterns.
Audit:
docs/audits/test-db-wiring-s188.md. - Radix pointer shims centralised.
__tests__/helpers/radix-pointer-shims.ts::installRadixPointerShims()is the canonical shim set for Radix Select / cmdk in jsdom; adopted across the test base viabeforeEach. - Canonical MCP server-mock helper.
__tests__/helpers/mcp-server.ts::createMockMcpServer(overrides?: Partial<MockMcpServer>)is the canonical mock-server factory for__tests__/mcp/. Consolidated 28 duplicatedcreateMockMcpServer/createMockServer/createTestServervariants spread across MCP test files (kh-prod-readiness-S43 W-RA; -419 LOC net). Helper exports symmetric tool / prompt / resource handler registration plus a dual Record+Array tool exposure (mockServer.tools+mockServer.toolList) so files asserting tool counts (e.g.tool-annotations-coverage’s 58-tool guard) absorb cleanly.MockToolResult.structuredContenttypedanywith eslint-disable to preserve consumer ergonomics; strict consumers can pass an explicitMockToolResult<MyShape>wrapper. - Shared test-factory infrastructure.
__tests__/helpers/factories/is the canonical landing place for cross-file test-fixture builders. Four NEW helpers added kh-prod-readiness-S44 W2-RG:cron-request.ts(createMockCronRequest()— 6 cron-route consumers),file-upload.ts(File + Request mock builders for upload routes — 8 consumers),api-request.ts(NextRequest buildercreateMockApiRequest()— 3 consumers),components/item.ts(createMockItem+createMockQAItemprop fixtures — 6 component-test consumers).mock-supabase.tsextended withcreateMockSupabaseTable()per-table dispatch shape (5 lib consumers). Pattern-references W-RA: named exports, JSDoc preambles,overrides?: Partial<T>signatures, no barrel re-exports. - Chain-method assertion remediation. ~115 invocation-shape chain
asserts (
_chain.eq.toHaveBeenCalledWithand siblings) removed from api+lib unit tests (kh-prod-readiness-S44 W2-RD; 24 api files / 6 lib files). Replaced with observable-outcome assertions on response status + body shape (api) or function-return Result envelope (lib). Multi-tenant security-contract carve-outs (user_id-scoped DELETE/UPDATE chain asserts) migrate to integration tier via inlineremediation-plan.md §3.5NOTE breadcrumbs; W-RD’ integration-suite seeding pending. E2E false-pass hardening continues (provenance-pipeline-audit,provenance-per-item,settings-mutationsjoined the S43 W-RB.1 wave1-spec set; hardexpect(X).toBeVisible()replacingif (X.isVisible())). - Client branding system. Build-time per-client branding via
NEXT_PUBLIC_CLIENT_ID. Each client has a Zod-validated JSON config atlib/branding/clients/{id}.json(colours, logos, metadata). OKLCH contrast validation enforces WCAG 2.1 AA compliance (3:1 non-text warning, 4.5:1 text failure). CSS variables injected via inline<style>in root layout.BrandLogocomponent supports light/dark variants with SVG+PNG fallback. Phew is onboarded as the first client. Administration docs atdocs/product-functionality/administration/technical.md. 61+ unit/loader/component/integration tests. - Admin-dedup E2E fixture infrastructure. Deterministic 1024-d vector
helpers (
e2e/fixtures/admin-dedup-vectors.ts) generate pairs at controlled cosine similarity (perturbation by orthogonal-noise mix); seed/cleanup helpers (e2e/fixtures/admin-dedup-fixture-helpers.ts) produce the §1.7 + §1.9 dataset (26 rows / 12 queue + 14 near-dup pairs) with run-id-taggedmetadata->>'e2e_dedup_fixture_run_id'cleanup; a worker-scoped Playwright fixture (e2e/fixtures/admin-dedup-fixture.ts) wraps seed →verifySeededPairssmoke gate → finally-block cleanup; one-shot CLI (scripts/seed-admin-dedup-fixtures.ts) supports--tag/--cleanup/--cleanup-all/--dry-run/--yes/--helpwith interactive confirmation on destructive actions and exit codes 0/1/2/3. 47 unit tests (vectors 25 + helpers 22). Inline embedding write (single-stepJSON.stringify(vec)insert) +find_duplicate_pairsverifier; FK-safe cleanup order (clearsuperseded_by, then chunks → history → content_items). Decisions contract indocs/audits/s213b-admin-dedup-fixtures-design.md§9 (cleanup gating, run-id format, vector-seed determinism, cross-domain pair, embedding write path, overlap negative case).globalSetupruns a >2h orphan-sweep at session start;globalTeardownruns a tag-based safety-net sweep onmetadata->>'e2e_dedup_fixture_run_id'rows beyond the legacy[E2E-title-prefix sweep. Spec coverage: 4 spec files / 17 tests covering §1.7 ACs 1–8 + §1.9 ACs 1–7 + 9–10. Read-only specs consume the worker-scoped fixture; mutating specs seed per-test pairs with FK-safeafterEachteardown so the worker fixture stays read-only canonical. All specs run on chromium-desktop + chromium-mobile against staging. - Boot-time env validation + schema-split.
lib/env.tsexports Zod-parsedserverEnvand re-exportsclientEnvfromlib/env-client.ts(split keeps server-schema names out of client bundle — zero leaks across 118 client chunks). Required vars throw with field-level errors naming the offending key. Variable names align with Supabase→Vercel integration defaults (NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY,SUPABASE_SERVICE_ROLE_KEY, bareSENTRY_*). All production paths + Sentry config files +next.config.tsconsume the validated env. Vitest pattern documented inlib/env.tsJSDoc (parse-at-load +vi.stubEnv+vi.resetModules+ dynamic import). 8 dedicated tests at__tests__/lib/env.test.tscover happy path, required rejections, optional passthrough.
Observability & Build Chain
Section titled “Observability & Build Chain”Capability summary only. Session-by-session narrative is tracked in the session ledgers.
The production-readiness wave (Phase 1 — see
docs/audits/kh-production-readiness-phase-1/STATUS.md) hardened the
observability + build chain. Current capabilities:
- Sentry release tagging live —
withSentryConfigsetsrelease.name = process.env.VERCEL_GIT_COMMIT_SHA. Production stack traces map to deploy SHA; bareSENTRY_*env vars match the Vercel→Sentry integration default. Sentry Turbopack sourcemap upload de-silenced viasilent: !process.env.CI+release.create: false, finalize: false. - Error-boundary Sentry forwarding — 22 segment
app/**/error.tsx+ newapp/global-error.tsx(Next.js 16 root convention) all forward toSentry.captureException. Sentry SDK init reads DSN directly fromprocess.env(decoupled from theclientEnvZod gate so env-validation failures cannot silence the very tool meant to surface them). build:vercelchain wired —vercel.jsonbuildCommandinvokes thebun run generate:skills && bun run build:mcp-apps && next buildchain so production builds regenerate skill bundles.- Boot-time env validation operational —
lib/env.ts(serverEnv) +lib/env-client.ts(clientEnv) parse Zod schemas at module load, with literal-by-literalprocess.env.NEXT_PUBLIC_*accesses to preserve Next.js compile-time substitution. Build-output regression test (__tests__/build/env-substitution.test.ts, gatedRUN_BUILD_TESTS=1) scans.next/static/chunks/*.jsto guard against substitution-defeat. - Persistent staging Supabase branch operational —
turayklvaunphgbgscatat migration parity with prod, mapped to Vercel Preview; prod stays onrovrymhhffssilaftdwdmapped to Production. Schema sync viasupabase db push --linked(canonical migration mechanism).supabase/seed.sql- per-branch
[remotes.staging.db.seed]block establish the schema-vs-data boundary; data-bearing migrations gated with parent-row EXISTS checks for clean data-empty branch behaviour.vectorextension lives inextensionsschema (advisor-clean across both envs); EXT-FIX migration closes the squash divergence. Data-refresh strategy: Path (ii) reference-refresh + content fixtures (WP-CI.RES.7 shipped). Periodic refresh restricted to reference tables (taxonomies, layer_vocabulary, content_types, etc.) viascripts/staging-reference-refresh.shdriven by.github/workflows/staging-reference-refresh.yml(fortnightly cron + workflow_dispatch). Integration test fixtures live at__tests__/integration/fixtures/**(5 modules — staging-fixture-generator, reference-data-validator, content-fixtures, entity-fixtures, publication-fixtures) andscripts/seed-integration-fixtures.ts. The earlier pg_dump-based PII-scrub orchestrator +staging-live-mirror.ymlworkflow retired — Path A trigger-silencing was structurally exhausted (SUSET-gatedsession_replication_role+ lack of ownership on auth.* tables). Migration parity verified post-FK-corrective: prod + staging both at20260504225603_align_auth_user_indexes. Schema-parity guard (.github/workflows/schema-parity.yml) validates structural alignment between branches with managed-drift normalisation (auth.users staging-only indexes filtered as Auth-owned).
- per-branch
.env.localis the single source of truth for both TS and Python pipelines;.envretired. CLI scripts gain--env={prod,staging,auto}flag with assertion-only enforcement. Canonical guide:docs/runbooks/local-development.md.ci.ymlPR-blocking —qualitysplit intoquality-precheck(lint/format/knip/build/test:build/pytest,NODE_OPTIONS: --max-old-space-size=4096on Build step to avoid OOM) + 4-shardquality-testmatrix (bunx vitest run --shard=N/4, existing forks pool retained,fail-fast: false) → ~4 min CI wall-clock target vs ~8 min pre-split. Four additional CI gates ship in parallel:e2e-smoke(46@smoke-tagged Playwright tests onStagingenv),mcp-evaltopology built once via dedicatedmcp-buildjob (Staging-scoped) +mcp-eval-seedjob (deterministic Q&A corpus seed,MCP_EVAL_SEED_METADATA_FLAG=mcp_eval_seed, version-aware embedding regeneration) feeding L1/L3/L4 fan-out matrix downloading the shared build artifact (scripts/mcp-eval/seed-fixtures.ts+seed-data.ts; cleanup preserves seed rows viaisPersistentMcpEvalSeed()filter), andintegrationjob (Stagingenv scope, single shard, 25-min timeout, AI keys explicit + CRON_SECRET;continue-on-error: trueremoved per WP-CI.RES.7 Phase 2 cutover — integration job is hard-required). Branch protection enforced via GitHub rulesets (main-protection15785019;production-protection15785711 covering bothstaging+production-readiness). Schema parity validated nightly + on-push by.github/workflows/schema-parity.yml(managed-drift normalisation for Auth-owned indexes; PG 17pg_dumppinned). REVOKE-guard CI lint (migration-revoke-guard.yml) + Supabase advisor cron job baselines drift. Runbook:docs/runbooks/ci.md.- REVOKE-guard CI lint + cron audit —
.github/workflows/migration-revoke-guard.ymlscans every PR’ssupabase/migrations/diff and asserts that for eachCREATE FUNCTION public.<name>(<args>)there is a pairedREVOKE EXECUTE ON FUNCTION public.<name>(<args>) FROM PUBLIC, anonin the same migration file. Belt-and-braces backstop for the per-function REVOKE pattern (Supabasepg_default_aclauto-grants anon EXECUTE on every new public PL/pgSQL function —REVOKE FROM PUBLICalone is a no-op). Plus a periodic cron audit query against prod viascripts/check-revoke-guard.tsflagging anypublic.*function withhas_function_privilege('anon', ...) = TRUEoutside the intentional-anon enumerate-list (today:set_config). Documented at specdocs/audits/kh-production-readiness-phase-1/specs/wp-ops43.3-revoke-guard-spec.md. - Single-track local development (post-S71 collapse, ID-24) — local work is
single-track on
main; the former long-livedproduction-readinesstop-level worktree was retired (S261, ID-24.1 — branch ref retained onoriginfor history). Parallel work now uses transient worktree-isolated agents under.claude/worktrees/cherry-picked back ontomain(see CLAUDE.md § Development Workflow). Theoriginstaging/production-readinessbranches and their GitHub rulesets are unchanged by the local collapse. - Permanent git
stagingbranch + reference-refresh workflow (Path (ii)) — git branchstaginglives onoriginas the canonical pre-mainintegration point per the feature-branch / worktree →staging→mainflow codified indocs/runbooks/ci.md§3.4. Branch protection enforced via GitHub rulesets (production-protectionruleset 15785711 covers bothrefs/heads/staging+refs/heads/production-readiness;main-protectionruleset 15785019 coversrefs/heads/main)..github/workflows/staging-reference-refresh.yml(fortnightly cron +workflow_dispatch,environment: Staging) refreshes reference tables (taxonomies, layer_vocabulary, content_types, etc.) from prod viascripts/staging-reference-refresh.sh(Path (ii) orchestrator — ~50 LoC bash). Schema-level alignment is migration-driven viasupabase db push --linkedand verified by.github/workflows/schema-parity.ymlwith managed-drift normalisation. Auth-hook[remotes.staging.auth.hook.before_user_created]block insupabase/config.tomlapplies automatically via Supabase GitHub integration on PR-merge into mappedstaginggit branch (Hold (d-cont) closed). The earlierstaging-live-mirror.ymlworkflow +staging-mirror-and-scrub.ts+scrub-staging-pii.sql+verify-scrub.tsorchestration (Path A pg_dump- with-PII-scrub) was retired in favour of Path (ii) reference-only refresh + deterministic content fixtures, eliminating trigger-ordering, PII scrub, and auth.* exclusion failure classes simultaneously. Runbook §4.4 ofdocs/runbooks/staging-refresh.md. - Dependabot ecosystem live + reduced-signal CI for Dependabot PRs —
.github/dependabot.ymlruns thebun+pip+github_actionsecosystems so PRs update lockfiles + workflow action versions.claude-code-action@v1is excluded (Routines via Claude.ai seats instead). GitHub blockssecrets.*for Dependabot-triggered runs (env-scoped + repo-level both empty);ci.ymlcarries 9if: github.actor != 'dependabot[bot]'guards: 3 step-level insidequality-precheck(Build / build-output regression scan / pytest) plus 6 job-level onquality-test,e2e-smoke,mcp-build,mcp-eval-seed,mcp-eval,integration. Dependabot PRs retain lint + format-check + knip baseline guard +REVOKE-guard PR lintsignal — runtime-code bumps need manual local verify before merge perdocs/runbooks/ci.md§4.4. Branch-protection ruleset 15785019 requires onlyci-summary(single aggregator check).ci-summaryaggregator IMPL shipped S33 —.github/workflows/ci.ymlcarries anif: always()+needs: [quality-precheck, quality-test, e2e-smoke, mcp-build, mcp-eval-seed, mcp-eval, integration]job that fails iff any upstreamneeds.<job>.resultisfailure(treatsskipped+successas pass). All${{ needs.<job>.result }}routed throughenv:block per GHA injection-prevention guidance. Single fixed-nameci-summaryposts on every PR including Dependabot. Ruleset PUT applied kh-prod-readiness-S36 (replaces prior 2-check requirementQuality pre-check+E2E smokewith singleci-summary); other rules (deletion + non_fast_forward + bypass actors) preserved. OPS-58 closed. - GitHub Environments canonical — repo-level secrets removed; canonical
Production+Stagingenv-scoped slots populated. Capital-first convention; per-var type/value rules per env target documented atdocs/runbooks/github-environments.md. - GitHub repository hosted under
ai-solution-huborg — canonical URLhttps://github.com/ai-solution-hub/knowledge-hub. Migrated fromliam-jons/knowledge-hub29/04/2026 to access GH Team plan branch protection on private repos. Integrations rebound: Vercel (both projects), Supabase GitHub App, Sentry, CodeRabbit; Dependabot + GH Actions secrets/ vars auto-migrated via redirect. Long-lived worktrees (main, KPF, production-readiness) re-pointed viagit remote set-url. - Vercel × Supabase × env-var canonical mapping at
docs/audits/kh-production-readiness-phase-1/research/09-vercel-supabase-env-mapping-spec.mdwith Liam-action checklist; integration sync model documented inresearch/11-supabase-vercel-integration-sync-behaviour.md. - Structured logging Phase 1+2+3+4-app-api shipped — Pino root logger at
lib/logger/index.tswithAsyncLocalStoragerequest-context mixin, Sentry bridge forwardingwarn/error/fatal, PII-redacting serialisers (REDACT_PATHS). Proxy mintscrypto.randomUUID()per request and propagates viax-request-idheaders.lib/error.tsdeliberately stays client-safe (no logger import — pino +node:async_hookscan’t bundle for the browser; routes calllogger.error()direct in catch arms).withRequestContextoverloaded inlib/logger/request-context.tsto accept either value-form(ctx, fn)or decorator-form(handler)— the 6 highest-volume API routes (itemsPOST,items/[id],search,ingest/url,items/[id]/classify,freshness/{calculate,recalculate-all}) wrap their handlers with the decorator. Phase 3 prioritylib/modules (lib/ai/{classify,change-reports,summarise,extract-questions}.ts,lib/source-documents/document-diff.ts) emit throughlogger.*with structured op tags. Phase 4 long-tail migratedapp/api/**/route.tsconsole callsites — 262 calls (380→118 baseline drop, of which ~16 are intentional CLI sinks + sanctioned dev-mode debug). Phase 4 closure pending: the remaining 102 calls live inlib/modules and client components transitively imported by client bundles; closure needs alib/logger/cliententry point (console-backed) so shared chokepoints can import a logger interface without dragging Pino + AsyncLocalStorage into the Turbopack client bundle. Phase 5 (Python correlation, gated on WP-RUN provisioning) + Phase 6 (Axiom destination, D-5) carry forward perdocs/specs/structured-logging-spec.mdv1.1. public.user_profilesmirror table operational —auth.usersmirrored via consolidated trigger;count_auth_users()SECURITY DEFINER RPC for parity probe;get_user_display_names()LEFT JOINsuser_profiles; admin users route queriesuser_profiles + user_rolesvia PostgREST. OPS-1 closed.- GDPR data export runbook at
docs/handover/gdpr-data-export.mdwithscripts/export-user-data.tsenumerating 20 PII tables and producing SHA-256-manifested Article-15 + Article-20 bundles. First of 13 W5 handover artefacts shipped. - Cutover row-count diff utility —
scripts/db-row-count-diff.ts(CLI:bun run db:row-count-diff) compares per-table row counts source vs target with allowlist for known-empty staging tables; exit codes 0/1/2. Closes OPS-8. - Sentry project for handover (G6.2): single project
knowledge-hub-phew-design(orgtw-group-s3, region DE). - Cloud Run pipeline — RETIRED (S298). Cloud Run is fully decommissioned;
the
cloudrun/manifests and.github/workflows/cloud-run-deploy.ymlare deleted. The ingestion pipeline now deploys on-prem (IONOS VPS + Coolify) via.github/workflows/onprem-deploy.yml— seedocs/runbooks/onprem-b1-deploy.md. The detail below is retained as a point-in-time record of the (now superseded) Cloud Run deploy path. - Cloud Run pipeline operational on staging + prod (historical — superseded
S298; see above) — 4 per-tenant Knative
Job manifests
cloudrun/jobs/{prod,staging}-{phew,kpf}.yamldeployed across both projects with deploy-timeNEXT_PUBLIC_CLIENT_IDbaking per D-RUN-12.1. Build chain:cloudrun/cloudbuild.yamlproduces slim + eval image variants via Python buildpack (multi-tag publish viapack build --tag --publish). Deploy workflow.github/workflows/cloud-run-deploy.ymlruns WIF auth → Cloud Build → manifest replace → declarative--set-secretsmount of the 11 always-required Secret Manager secrets (Phase 2 / T2.2 —ANTHROPIC_API_KEY,OPENAI_API_KEY,SUPABASE_*,NEXT_PUBLIC_*,FIRECRAWL_API_KEY,CRON_SECRET,SENTRY_AUTH_TOKEN) → smoke verify → Vercel notify dispatch. Runtime per-tenant SAs ({phew,kpf}-pipeline-sa) hold project-scopedroles/secretmanager.secretAccessor(T2.3 — 4 bindings; project-scope chosen over per-secret since secrets are tenant-shared today). Firstinvoke verification GREEN on staging × 2 tenants viaKH_SMOKE_TEST=1env-var trigger inscripts/ingest.py:gcloud run jobs update --update-env-vars= KH_SMOKE_TEST=1thenexecute --waitreturns exit 0 with log lineKH_SMOKE_TEST=1: env + imports OK; exiting 0 without DB writes. The env-var path supersedes the unreachable--smoke-testargparse flag because Cloud Run buildpack launchers bake the entrypoint command at image-build time — runtime--argsoverrides do not reach the script (four override paths empirically verified ineffective:--argson execute/update,--commandoverride,GOOGLE_ENTRYPOINTenv var). The argparse flag remains for local-dev parity. All security-hardened with env-var indirection on${{ }}interpolations. Handover runbookdocs/runbooks/cloud-run-phase-1-handover.mdcovers T1 IAM/WIF/GH-vars setup + T2.2/T2.3 close-out (§8). Spec:docs/audits/kh-production-readiness-phase-1/specs/wp-run-cloud-run-provisioning-spec.mdv1.4 RATIFIED (§2.14.1-§2.14.5 4-entry Cloud Scheduler matrix + YAML schema + wrapper script + IAM pre-flight + OIDC audience pin; §2.13.1 NEW Cloud Build cache image plumbing —--cache-image=europe-west2-docker.pkg.dev/$PROJECT_ID/pipeline/kh-pipeline-{slim,eval}:cacheflag wired incloudrun/cloudbuild.yamlfor both build steps; AC-2.1 verifies:cachetag present post-first-build; cold rebuild ~14 min baseline, steady-state cache hit onscripts/**/*.py-only changes projected ~3-5 min, first cache-bearing build is cold + cache push ~15-16 min one-off; Phase 3 IMPL gated on Phase 2 T2.5/T2.6/T2.7pipeline_log.pywidening + cross-track sync). - CI prod-deploy gate via Vercel notify pattern —
vercel/repository-dispatch/actions/status@v1posts workflow status fromci.yml(ci-summary aggregator) to Vercel so prod deploys gate on that named check.if: always()ensures Vercel sees both success and failure signals. Vercel-side opt-in via Settings → Git → Required Checks. (Historicallycloud-run-deploy.ymlalso posted aVercel - knowledge-hub: cloud-run-deploycheck; that workflow is removed at S298 and no longer posts — drop it from Vercel Required Checks if it was opted in, or prod deploys hang.) Seedocs/runbooks/ci.md§2.2. - D-9 ESLint
no-consoleregression guard —eslint.config.mjsbansconsole.*inapp/**/*.{ts,tsx}+lib/**/*.{ts,tsx}with file-level allowlist for 4 documented intentional residuals (lib/logger/client.tschokepoint shim,lib/client-telemetry.tsdev-mode debug,lib/eval/reporter.tsCLI eval reporter,lib/mcp/app-bundles.tsautogen bundle). Subsumes the originally-proposed grep guard test. WP-G5.4 Phase 4 closure documented indocs/specs/structured-logging-spec.md§5. - Vercel function timeout audit complete — 12
maxDurationrows invercel.jsonaudited and classified: 9 STAY-ON-VERCEL (latency-sensitive user-triggered work, complementary Vercel cron per D-2), 2 AUDIT pending Sentry telemetry scan (responses/draft-allbulk-draft sustained-timeout risk,cron/intelligence-pollevery-15-min cadence at 120s ceiling), 0 unconditional MIGRATE-TO-CLOUD-RUN. WP-RUN.5 handoff list captures the 2 AUDIT entries as conditional candidates. Perdocs/audits/ kh-production-readiness-phase-1/wp-run-phase-1-5-vercel-audit.md.
10. Cross-References
Section titled “10. Cross-References”This document is the canonical record of what is built. Sister documents cover the rest of the product picture:
| Document | Purpose |
|---|---|
docs/reference/product-roadmap.md | Forward-looking only — what is coming next |
docs/reference/product-backlog.md | Parked, deferred, and speculative items |
.planning/.archive/.audits/si-gap-analysis-s149.md | Sector Intelligence gap analysis (S149, historical — roadmap is now source of truth) |
.planning/.archive/.audits/ai-eval-gap-analysis-s149.md | AI Evaluation gap analysis (S149, historical — roadmap is now source of truth) |
docs/reference/SCHEMA-QUICK-REFERENCE.md | Database schema canonical reference |
docs/reference/classification-architecture.md | Classification pipeline architecture |
docs/reference/classification-prompt.md | Current classification prompt v4.5 |
docs/reference/data-entry-points.md | All KB ingestion entry points |
docs/operations/taxonomy-change-runbook.md | 7-step taxonomy change chain |
docs/reference/field-consumer-dependency-map.md | Field-to-consumer dependency map (S139) |
docs/reference/entity-type-taxonomy-spec.md | Entity type taxonomy v1.1 (S140) |
docs/reference/two-pass-validation-architecture.md | Two-pass entity validation design (S149) |
docs/audits/two-pass-cost-quality-measurement.md | Task 15 measurement — batch-only recommendation (S168) |
docs/operations/re-ingestion-quality-protocol.md | Operator runbook for re-ingestion quality gate (S168) |
docs/operations/blank-db-restore-matrix.md | Blank-DB restore matrix — entry points + side tables (S175) |
docs/operations/two-stage-re-ingestion-runbook.md | Two-stage re-ingestion runbook — Stage 0→2 (S175) |
docs/operations/guide-regeneration-prompts.md | MCP guide/item recreation prompts (S175) |
docs/reference/sector-intelligence-pathway.md | Sector Intelligence product pathway |
docs/reference/ai-integration-strategy.md | AI integration strategy and visibility policy |
docs/reference/state-of-the-product.md | This document — canonical present-tense capability ledger |
docs/reference/ux-principles.md | UX principles (vision content from ADS v1.0) |
docs/reference/ai-visibility-policy.md | AI-as-infrastructure UI visibility policy |
docs/generated/codebase-stats.md | Auto-generated code statistics |
docs/generated/mcp-inventory.md | Auto-generated MCP tool/resource/prompt inventory |