AI Integration — Technical Reference
AI Integration — Technical Reference
Section titled “AI Integration — Technical Reference”Last verified: Session 210 (29 April 2026). Refresh covers S195-S209 + production-readiness S10-S13. Tool, resource, and prompt counts are auto-generated — see
docs/generated/mcp-inventory.md.Reference companions:
docs/reference/ai-integration-layers.md(4-layer architecture map),docs/reference/ai-integration-strategy.md(vision + decision log),docs/reference/classification-architecture.md,docs/reference/classification-prompt.md,docs/reference/data-entry-points.md(per-entry-point pipeline coverage),docs/reference/ai-visibility-policy.md(admin Provenance carve-out).
Overview
Section titled “Overview”The AI integration layer provides classification, entity extraction, embedding, summarisation, drafting, vision, and evaluation infrastructure that powers Canonical’s data quality pipeline plus the MCP server that exposes the KB to external AI clients. AI is invisible infrastructure — it enriches content at ingest time and powers retrieval, but is never surfaced as a user-facing feature outside the admin Provenance route.
The layer spans three languages (TypeScript for the web app and MCP server,
Python for CLI ingestion pipelines) with strict parity constants enforced by
__tests__/validation/pipeline-parity.test.ts. All AI calls are gated by
environment variables for model selection and pricing transparency.
5-Layer AI Architecture
Section titled “5-Layer AI Architecture”Per docs/reference/ai-integration-layers.md:
| Layer | Surface | Audience | Where it lives |
|---|---|---|---|
| Layer 0 | AI Service Layer + Skill Files | Foundation (consumed by all layers) | lib/ai/, lib/ai/skills/, Supabase |
| Layer 1 | MCP Server (tools, resources, prompts) | Any MCP client | lib/mcp/, app/api/mcp/[transport]/route.ts |
| Layer 2 | MCP Apps (visual cards inside Claude) | Claude Desktop / Claude.ai users | mcp-apps/, lib/mcp/app-bundles.ts |
| Layer 3 | Knowledge Hub Plugin (commands + skills) | Knowledge workers in Cowork/Claude | .claude/plugins/knowledge-hub/1.0.0/, plugin-bundle.ts |
| Layer 4 | Claude Code Plugin (dev workflow) | Developers | .claude/, CLAUDE.md, .claude/checks/ |
Each layer adds distinct value. Layer 1 gives Claude the ability to access data; Layer 3 gives Claude the expertise to use that data well. The foundation (Layer 0) is shared across every higher layer.
API Routes
Section titled “API Routes”| Method | Route | Auth | Purpose | File |
|---|---|---|---|---|
| POST | /api/items/[id]/classify | Editor+ | Trigger classification for item | app/api/items/[id]/classify/route.ts |
| POST | /api/summaries/generate | Editor+ | Generate AI summary for item | app/api/summaries/generate/route.ts |
| POST | /api/embed | Editor+ | Generate embedding for arbitrary text | app/api/embed/route.ts |
| POST | /api/extract | Editor+ | Structured content extraction | app/api/extract/route.ts |
| POST | /api/vision | Editor+ | PDF/image analysis | app/api/vision/route.ts |
| POST | /api/change-reports/generate | Editor+ | Change digest generation | app/api/change-reports/generate/route.ts |
| POST | /api/procurement/[id]/questions/extract | Editor+ | Tender question extraction (PDF/DOCX) | app/api/procurement/[id]/questions/extract/route.ts |
| POST | /api/procurement/[id]/extract-metadata | Editor+ | Tender metadata extraction | app/api/procurement/[id]/extract-metadata/route.ts |
| POST | /api/procurement/[id]/match | Editor+ | KB matching with confidence assessment | app/api/procurement/[id]/match/route.ts |
| POST | /api/procurement/[id]/responses/draft | Editor+ | 3-pass form response drafting pipeline | app/api/procurement/[id]/responses/draft/route.ts |
| POST | /api/mcp/mcp | OAuth | MCP Streamable HTTP transport | app/api/mcp/[transport]/route.ts |
| GET | /api/provenance/item/[id] | Admin | Per-item provenance (admin-only) | app/api/provenance/item/[id]/route.ts |
| GET | /api/admin/provenance/pipeline-runs | Admin | Pipeline Health (keyset-paginated) | app/api/admin/provenance/pipeline-runs/route.ts |
| GET | /api/admin/provenance/export/verification-history | Admin | PDF audit export | app/api/admin/provenance/export/verification-history/route.ts |
These routes delegate to the library modules documented below. AI processing
also runs implicitly during content ingestion — see
docs/reference/data-entry-points.md for the full coverage matrix across
all 11 ingestion entry points.
AI Service Layer (lib/ai/)
Section titled “AI Service Layer (lib/ai/)”13 modules centralise every AI integration point. API routes import from
@/lib/ai rather than calling Anthropic/OpenAI SDKs directly.
| Module | File | Purpose | SDK |
|---|---|---|---|
classify | lib/ai/classify.ts | Domain, subtopic, keywords, summary, entities, relationships | Anthropic (tool use) |
summarise | lib/ai/summarise.ts | Executive / detailed / takeaways | Anthropic (tool use) |
digest | lib/ai/change-reports.ts | Change digest generation | Anthropic |
embed | lib/ai/embed.ts | Embeddings (1,024-dim, env-driven) | OpenAI |
match | lib/ai/match.ts | KB matching + confidence assessment | OpenAI + Anthropic |
draft | lib/ai/draft.ts | 3-pass form response drafting pipeline | Anthropic |
extract-content | lib/ai/extract-content.ts | Structured content extraction | Anthropic |
extract-questions | lib/ai/extract-questions.ts | PDF/DOCX tender question + metadata extraction | Anthropic (tool use) |
vision | lib/ai/vision.ts | PDF/image analysis | Anthropic (vision) |
quality-check | lib/ai/quality-check.ts | Deterministic + AI response quality checks | Anthropic |
pricing | lib/ai/pricing.ts | Model pricing constants (USD/M tokens) | — |
errors | lib/ai/errors.ts | AIServiceError with HTTP status codes | — |
skills/loader | lib/ai/skills/loader.ts | In-memory skill lookup against inlined.generated.ts | — |
Skill files (5 markdown files): classification.md,
classification-entity-types.md, plus three drafting/governance skills.
lib/ai/skills/inlined.generated.ts is the AUTO-GENERATED string map of all
skill files; regenerate via bun run generate:skills (runs as prebuild +
predev). Committed for safety so Vercel deployments do not require
filesystem reads.
Entity Modules
Section titled “Entity Modules”| Module | File | Purpose |
|---|---|---|
entity-dedup | lib/entities/entity-dedup.ts | 12-rule canonicalisation |
entity-aliases | lib/entities/entity-aliases.ts | DB-backed alias resolution with baseline fallback |
entity-context | lib/entities/entity-context.ts | Context snippet extraction (+/-80 chars) |
entity-metadata-bridge | lib/entities/entity-metadata-bridge.ts | Bridges temporal references to entity mention metadata |
Other Supporting Modules
Section titled “Other Supporting Modules”| Module | File | Purpose |
|---|---|---|
layer-inference | lib/layer-inference.ts | 7-rule deterministic content layer suggestion |
strip-markdown | lib/content/strip-markdown.ts | Strips markdown for plain text input to classifier |
html-to-markdown | lib/content/html-to-markdown.ts | GFM-aware idempotent HTML→markdown bridge (S171) |
client-config | lib/client-config.ts | Client-specific disambiguation + entity examples |
validation/schemas | lib/validation/schemas.ts | Tag normalisation (normaliseTag), proper noun allowlist |
pipeline/record-run | lib/pipeline/record-run.ts | recordPipelineRun() — audit row helper |
provenance/pricing | lib/provenance/pricing.ts | Per-model token pricing for admin Provenance display |
quality/quality-score | lib/quality/quality-score.ts | Composite quality score (5-component) + cadence-compliance penalty (S208 §5.5 Phase 5) |
Classification Pipeline (lib/ai/classify.ts)
Section titled “Classification Pipeline (lib/ai/classify.ts)”Function Signature
Section titled “Function Signature”export async function classifyContent( params: ClassifyParams,): Promise<ClassificationResult>;Parameters (ClassifyParams):
| Param | Type | Required | Purpose |
|---|---|---|---|
supabase | SupabaseClient<Database> | Yes | Supabase client for DB reads/writes |
itemId | string | Yes | UUID of the content item to classify |
force | boolean | Yes | Re-classify even if already classified |
userId | string | Yes | UUID of the user triggering classification |
validate | boolean | No | Enable Pass 2 entity validation (default: false) |
userId must be a valid UUID (the content_items.updated_by column is
uuid type). For pipeline/batch scripts, use the pipeline service account:
a0000000-0000-4000-8000-000000000001.
Processing Steps (19 canonical)
Section titled “Processing Steps (19 canonical)”The classification pipeline runs these steps sequentially:
- Fetch content item — Reads from
content_itemsbyitemId. - Cache check — If
classified_atis set andforce=false, returns cached classification immediately. - Load skill files —
classification.md(main prompt) andclassification-entity-types.md(entity type reference) fromlib/ai/skills/. - Build taxonomy string — Fetches active domains and subtopics from
taxonomy_domainsandtaxonomy_subtopicstables. - Interpolate prompt — Substitutes
{TAXONOMY},{CLIENT_DISAMBIGUATION},{CLIENT_ORGANISATION_NAME}, etc. Appends entity types reference and the payment gateway product anchor. - Prepare content — Strips markdown via
stripMarkdown(), then truncates to 5,000 characters for classification input. - Pass 1: Claude API call — Calls Claude (default model:
claude-sonnet-4-6, configurable viaAI_SUMMARY_MODELenv var) withreturn_classificationtool use. Max tokens: 2,500. Returns domain, subtopic, keywords, summary, entities, relationships, temporal references. - Coerce subtopics — Empty/whitespace subtopics coerced to
nullviacoerceSubtopic(). - Validate domains —
validateDomain()matches AI output against active taxonomy slugs (fuzzy fallback). - Normalise keywords — Handles Claude returning a string instead of an
array, applies
normaliseTag(), deduplicates. - Generate embedding —
generateEmbedding()onsuggestedTitle + plainText, truncated toMAX_EMBEDDING_CHARS(24,000). Serialised asJSON.stringify(embedding)for Supabase RPC. - Store temporal references — Merges
ai_temporal_referencesintocontent_items.metadataJSONB. - UPDATE content_items — Writes all classification fields + embedding + temporal refs.
- Delete existing entity mentions — Wipes
entity_mentionsfor thiscontent_item_id(clean-slate before re-insert). - Entity processing pipeline (see below).
- Store entity relationships — Inserts into
entity_relationshipswith canonicalised source/target names. - Temporal-entity bridge —
bridgeTemporalReferencesToEntities()links temporal references to entity mention metadata.
Entity Processing Sub-Pipeline (Steps 14-15)
Section titled “Entity Processing Sub-Pipeline (Steps 14-15)”Entity extraction quality is the most complex part of the pipeline. After Pass 1 returns raw entities, they go through a multi-stage refinement chain:
Step 14a: Deterministic pre-filter — shouldExcludeEntity() applies 10
categories of exclusion filters on the raw AI output (before canonicalisation):
| Filter | Function | Catches |
|---|---|---|
| Identifier patterns | isExcludedEntity() | SIC codes, VAT numbers, DUNS numbers, pure numerics |
| Internal documents | isInternalDocument() | Policies, procedures, plans, registers, agreements |
| Generic concepts | isGenericConcept() | ~80 terms: “information security”, “encryption”, etc. |
| Role titles | isRoleTitle() | ”Managing Director”, “DPO”, “CTO”, etc. |
| Protocols/formats | isProtocolOrFormat() | HTTPS, SSH, PDF, AES-256, Python, etc. |
| Insurance/contracts | isInsuranceOrContract() | Professional indemnity, NDA, SLA, etc. |
| Management system acronyms | isManagementSystemAcronym() | ISMS, QMS, EMS (prefer the certification) |
| GDPR artefacts | isGdprArtefact() | DPIA, ROPA, lawful basis, consent, etc. |
| Framework lot numbers | isFrameworkLot() | ”G-Cloud Lot 1”, etc. |
| Compound entities | isCompoundEntity() | ”ISO 27001/ISO 9001” (slash-separated) |
A STATUTORY_ALLOWLIST prevents false exclusion of real statutory documents
(e.g. “Wales Safeguarding Procedure”, “Keeping Children Safe in Education”).
Step 14b: Pass 2 validation (optional, when validate=true) — see next
section.
Step 15: Storage preparation:
- Strip parenthetical descriptors from person names via
stripPersonDescriptors(). - Canonicalise names via
canonicalise()(12-rule normalisation). - Resolve aliases via
resolveAlias()(DB-backed with baseline fallback). - Lowercase canonical names for storage.
- Extract context snippets via
extractEntityContext()(+/-80 chars).
Step 15a: Post-canonicalise filter — Re-runs shouldExcludeEntity() on
the canonicalised form. This catches entities that slipped through Step 14a
because the pre-canonicalise name didn’t match exclusion patterns (e.g.
“encryption processes” canonicalises to “Encryption Process” which then
matches the generic concepts filter). Dropped entities are logged via
logBestEffortWarn.
Step 15b: ISO certification type override — Forces six ISO certification
families (iso 9001, iso 14001, iso 22301, iso 27001, iso 45001,
iso 50001) to entity type certification, regardless of what Claude
returned. Eliminates cross-item type flip-flop between certification and
standard (S158A Iteration 4).
Step 15c: Mention row dedup — dedupeEntityMentionRows() collapses
duplicate (content_item_id, canonical_name, entity_type) triples that arise
when canonicalise/alias/lowercase collapses two distinct Pass 1 outputs (e.g.
“ISO 27001” and “ISO27001”) onto the same key. Merge rules: max confidence,
first entity_name, first non-null context_snippet. Prevents Postgres error
21000 on upsert.
Step 15d: Upsert — Upserts into entity_mentions with conflict target
(canonical_name, entity_type, content_item_id).
Pass 2: Entity Validation
Section titled “Pass 2: Entity Validation”Architecture
Section titled “Architecture”The two-pass system adds a second LLM call to validate entities that survive
the deterministic filters. It is optional (validate parameter) and designed
for batch quality improvement, not real-time ingestion.
| Property | Pass 1 | Pass 2 |
|---|---|---|
| Model | claude-sonnet-4-6 (configurable) | claude-haiku-4-5 (hardcoded) |
| Purpose | Classification + entity extraction | Entity quality validation |
| Max tokens | 2,500 | 1,500 |
| Temperature | (default) | 0 |
| Cost | ~$0.06/item | ~$0.003/item |
| Mandatory | Yes | No (opt-in via validate=true) |
Validation Prompt
Section titled “Validation Prompt”buildValidationPrompt() constructs a detailed prompt with:
- Five sequential tests per entity: Named Entity Test, External Reference Test, Role Title Test, Protocol/Format Test, Type Accuracy Test.
- Per-type diagnostic questions — e.g. for
organisation: “Does it have a legal registration or government charter?” - Common false positive patterns — ISMS/QMS, insurance products, GDPR artefacts, contract types, security principles, geographic regions.
- Named Bulk Certification Sets — Three specific certification bundles
that trigger atomic preservation when ALL members co-occur in source text:
- Data-centre accreditation: ISO 27001 + BS 10008 + PCI-DSS
- Security posture: ISO 27001 + Cyber Essentials Plus + SOC 2
- ESG stack: ISO 14001 + ISO 50001 + ISO 45001
- Payment Gateway Product Anchor — Six branded payment gateways that
must be classified as
product(nottechnologyororganisation): Access PaySuite, Adalante Smartpay, Opayo, Pay360, WorldPay, Stripe.
Verdict Schema
Section titled “Verdict Schema”Each entity receives one of three verdicts:
| Verdict | Meaning | Action |
|---|---|---|
confirmed | Passes all tests with correct type | Keep as-is |
retyped | Valid entity but wrong type assigned | Keep with corrected type |
removed | Fails one or more tests | Drop from entity_mentions |
Graceful Degradation
Section titled “Graceful Degradation”If Pass 2 fails (API error, timeout), the pipeline falls back to the deterministically-filtered entities from Step 14a. Pass 2 failure never breaks classification.
Entity Extraction
Section titled “Entity Extraction”12 Entity Types
Section titled “12 Entity Types”The entity type taxonomy is shared between TS and Python pipelines:
| Type | Description | Example |
|---|---|---|
organisation | Legal entity with registration or government charter | NHS Digital, Capita |
certification | Obtained by assessment with issuing body and renewal | ISO 27001, Cyber Essentials Plus |
regulation | Non-compliance carries legal penalties | GDPR, Data Protection Act 2018 |
framework | Published guidance for voluntary adoption | NIST CSF, ITIL |
capability | Service listed on company website | Penetration Testing Service |
person | Specific named individual | (redacted in KB context) |
technology | Specific named platform with vendor and version | Microsoft Azure, Jira |
project | Named piece of work with start, scope, and end | NHS Spine Programme |
sector | Recognised industry classification | Healthcare, Education |
product | Named branded offering the organisation sells | Access PaySuite, Opayo |
standard | Numbered document published by a standards body | BS 10008, PAS 1192 |
methodology | Named approach with its own body of knowledge | PRINCE2, Agile, DevOps |
The canonical list is defined in lib/ai/classify.ts (tool schema) and
mirrored in scripts/kb_pipeline/classify.py VALID_ENTITY_TYPES.
Canonicalisation (lib/entities/entity-dedup.ts)
Section titled “Canonicalisation (lib/entities/entity-dedup.ts)”The canonicalise() function applies 12 rules in order:
- Trim whitespace
- Convert slug-style names to proper case (“penetration-testing” becomes “Penetration Testing”)
- Normalise basic ISO formats (“ISO27001” becomes “ISO 27001”)
- Normalise extended ISO formats (“ISO/IEC 27001”, “ISO-27001” all become “ISO 27001”)
- Strip ISO version suffixes (“ISO 27001:2022” becomes “ISO 27001”)
- Normalise Cyber Essentials variants
- WCAG normalisation (“Wcag 2 1 Aa” becomes “WCAG 2.1 AA”)
- Company suffix normalisation (“Ltd” becomes “Limited”)
- Fix single-word abbreviations (“gdpr” becomes “GDPR”) — uses a 39-entry lookup
- Multi-word title case for all-lowercase inputs
- Plural normalisation — type-aware stripping of trailing ‘s’ for applicable types
- Strip trailing periods
Alias Resolution (lib/entities/entity-aliases.ts)
Section titled “Alias Resolution (lib/entities/entity-aliases.ts)”Aliases are loaded from the entity_aliases DB table with a 5-minute TTL
in-memory cache. A BASELINE_ALIASES constant (11 entries) provides fallback
when the DB is unreachable. DB entries take precedence over baseline on
conflict.
Context Extraction (lib/entities/entity-context.ts)
Section titled “Context Extraction (lib/entities/entity-context.ts)”extractEntityContext() finds the first occurrence of the entity name
(case-insensitive) in the plain text and returns +/-80 characters of
surrounding text with ellipsis markers at truncation boundaries. Stored in
the entity_mentions.context_snippet column.
Embedding Generation (lib/ai/embed.ts)
Section titled “Embedding Generation (lib/ai/embed.ts)”| Property | Value |
|---|---|
| Provider | OpenAI |
| Model | text-embedding-3-large (configurable via AI_EMBEDDING_MODEL) |
| Dimensions | 1,024 (configurable via AI_EMBEDDING_DIMS, schema-locked to vector(1024)) |
| Max input chars | 24,000 (MAX_EMBEDDING_CHARS) |
| Cache | In-memory, 500 entries, 1-hour TTL |
| DB column type | vector(1024) (pgvector) |
| Serialisation | JSON.stringify(embedding) for Supabase RPC params |
Why 1,024 dimensions? OpenAI text-embedding-3-large natively produces
3,072-dim vectors; Matryoshka shortening lets us store the lower-dimensional
projection at significant index/storage savings without measurable retrieval
quality loss for KB content.
The generateEmbedding() function checks the in-memory cache first, then
calls OpenAI. The cache uses an LRU-style eviction (expired entries removed
first, then oldest if over capacity).
Embedding input for classification — During classifyContent(), the
embedding text is suggestedTitle + '\n\n' + plainText, truncated to
MAX_EMBEDDING_CHARS. If truncation occurs, a
classify.embedding.input_truncated warning is logged via Sentry.
Embedding input in Python pipeline — build_embedding_text() in
scripts/kb_pipeline/embed.py constructs title + summary + content[:24000].
The summary inclusion is an intentional divergence (Plan D D5); the
AI-generated summary provides useful semantic signal.
Summary Generation (lib/ai/summarise.ts)
Section titled “Summary Generation (lib/ai/summarise.ts)”Pure AI Function
Section titled “Pure AI Function”callSummaryAI() is a pure function (no Supabase dependency) suitable for
both the service layer and batch scripts:
| Property | Value |
|---|---|
| Model | claude-sonnet-4-6 (configurable via AI_SUMMARY_MODEL) |
| Max tokens | 2,000 |
| Max content | 100,000 characters (truncated internally) |
| Tool | return_summary |
| Output schema | { executive, detailed, takeaways } |
| Validation | Zod via SummaryResponseSchema |
Output structure (SummaryData):
| Field | Type | Description |
|---|---|---|
executive | string | Single sentence summary (max 150 chars) |
detailed | string | 2-3 paragraph detailed summary |
takeaways | string[] | 3-7 key takeaways |
generated_at | string | ISO 8601 timestamp |
model | string | Model used for generation |
tokens_used | number | Total input + output tokens |
Service-Layer Function
Section titled “Service-Layer Function”generateSummary() wraps callSummaryAI() with Supabase fetch/store:
- Returns HTTP 404 if item not found
- Returns HTTP 409 if summary exists and
force=false - Returns HTTP 400 if content is empty
- Returns HTTP 413 if response was truncated (max_tokens hit)
- Stores
summary_dataas JSONB and syncssummarywith the executive text (NOTai_summary—feed_articles.ai_summaryis intentionally separate)
Layer Inference (lib/layer-inference.ts)
Section titled “Layer Inference (lib/layer-inference.ts)”A pure, deterministic function with no AI calls and no database queries. Evaluates 7 rules in strict priority order and returns the first match.
Rules (priority order)
Section titled “Rules (priority order)”| # | Condition | Suggested Layer | Confidence |
|---|---|---|---|
| 1 | isBidDiscovered === true | bid_detail | high |
| 2 | ingestionSource === 'bid_library' AND contentType === 'q_a_pair' | bid_detail | high |
| 3a | hasReference === true | company_reference | high |
| 3b | hasDetail === true AND hasBrief === true | bid_detail | medium |
| 3c | hasBrief === true AND hasDetail === false | sales_brief | medium |
| 4 | contentType in {policy, compliance, certification} | company_reference | medium |
| 4b | contentType === 'research' | research | high |
| 4c | contentType === 'case_study' | bid_detail | medium |
| 4d | contentType in {product_description, capability, methodology} | bid_detail | medium |
| 5 | Content length heuristics (Q&A pairs, short/long) | varies | low |
| 6 | ingestionSource === 'url_import' | research | low |
| 7 | Default | bid_detail | low |
Layer Keys
Section titled “Layer Keys”| Key | Meaning |
|---|---|
sales_brief | Short positioning content for sales use |
bid_detail | Detailed content for form responses |
company_reference | Internal reference material (policies, etc.) |
research | External research and background material |
These are defined in lib/client-config.ts and mirrored in
scripts/kb_pipeline/layer_inference.py.
MCP Server (Layer 1)
Section titled “MCP Server (Layer 1)”Entry Point
Section titled “Entry Point”| File | Purpose |
|---|---|
app/api/mcp/[transport]/route.ts | HTTP route handler; POST /api/mcp/mcp is primary |
lib/mcp/tools/index.ts | Tool registration barrel — call order = discovery |
lib/mcp/resources.ts | Resource + prompt registration |
lib/mcp/auth.ts | Bearer-token auth, RLS-scoped client, role checks |
lib/mcp/tools/shared.ts | defineTool wrapper, annotation constants, lazy imports |
The MCP server uses the SDK’s WebStandardStreamableHTTPServerTransport
directly (not mcp-handler, which has shared-state corruption on Vercel
warm instances). A fresh McpServer and transport are created per request.
Tool Registration Convention
Section titled “Tool Registration Convention”defineTool( server, 'snake_case_name', { title: 'Human Title', description: '...', inputSchema: { param: z.string().describe('...') }, annotations: READ_ONLY_ANNOTATIONS, }, async (args, extra: ToolExtra) => { ... });defineTool enforces all four ToolAnnotations advisory fields at compile
time via RequiredToolAnnotations (S172). Five named annotation constants
encode the policy-approved combinations:
| Constant | Use Case |
|---|---|
READ_ONLY_ANNOTATIONS | Search, get, list, find, audit, suggest, show |
SAFE_WRITE_ANNOTATIONS | Update, assign, cite, classify, summarise (idempotent) |
DESTRUCTIVE_WRITE_ANNOTATIONS | Delete, supersede |
NON_IDEMPOTENT_WRITE_ANNOTATIONS | Create (fresh UUID per call) |
NON_IDEMPOTENT_OPEN_WORLD_WRITE_ANNOTATIONS | External-API-touching pipelines (e.g. RSS poll) |
destructiveHint defaults to true in the MCP spec — picking the right
constant prevents read-only tools from rendering as destructive in clients.
Tool Categories
Section titled “Tool Categories”Tool registrations live in 16 category files under lib/mcp/tools/. Each
category file exports a register{Category}Tools(server) function called by
lib/mcp/tools/index.ts in discovery order. The surface is outcome-grouped
(ID-71): several former single-purpose tools collapsed into one parameterised
entry per outcome — find (retrieval), where_are_we_exposed (five-layer
exposure), whats_in_my_queue (the one faceted queue), get / assign /
find_duplicates (one-or-many).
| Category file | Tools |
|---|---|
search.ts | find, find_duplicates |
content.ts | get, create_content_item, update_content_item, get_workspace_items, assign, get_document_versions, get_document_diff |
procurement.ts | list_active_procurement, get_procurement_detail, get_form_question, cite_content, get_content_effectiveness |
dashboard.ts | get_reorientation, where_are_we_exposed |
quality.ts | suggest_content_creation |
ai.ts | classify_content, generate_summary |
entities.ts | get_entity_relationships |
templates.ts | list_templates, get_template_coverage, get_template_gaps |
apps.ts | show_coverage_matrix, show_procurement_dashboard, show_reorient_me, show_intelligence_feed |
governance.ts | delete_content_item, update_governance_status, update_publication_status, review_governance_item |
supersession.ts | supersede_content_item (S186) |
review.ts | whats_in_my_queue, create_review_assignment |
intelligence.ts | get_intelligence_summary, trigger_intelligence_poll |
guides.ts | list_guides, get_guide, create_guide, update_guide (S175) |
change-report.ts | get_change_report |
workspaces.ts | list_user_workspaces |
The former search trio (search_knowledge_base / search_qa_library /
search_content_chunks) + find_similar_items collapsed into find
(type / scope / granularity / similar_to params); the freshness /
quality / coverage-gap / certification reads collapsed into
where_are_we_exposed; the content-review and governance queues collapsed
into whats_in_my_queue (facet param); the single+batch pairs
(get_content_item/_items, assign_content_owner/bulk_assign_owner,
find_duplicate_candidates/find_all_duplicates) collapsed into the
one-or-many get / assign / find_duplicates. The
former bids.ts is now procurement.ts (procurement is the first form type)
and get_bid_question is now get_form_question.
For the canonical, auto-generated tool/resource/prompt list (with parameters,
annotations, and counts), see docs/generated/mcp-inventory.md.
Auth Flow
Section titled “Auth Flow”createMcpClient(extra.authInfo) returns an RLS-scoped Supabase client
created from the OAuth bearer token. Write tools additionally call
checkMcpRole(extra.authInfo, ['admin', 'editor']). A DB error during role
lookup must NOT silently downgrade — the auth attempt is rejected and the
error logged server-side only.
Lazy Imports
Section titled “Lazy Imports”Heavy modules (AI, dashboard, form/procurement queries, ext-apps) are loaded on-demand
via wrapper functions in lib/mcp/tools/shared.ts (getClassifyContent,
getGenerateSummary, getDashboardModule, etc.) to prevent Vercel cold
start crashes at module evaluation time.
Response Format
Section titled “Response Format”Every tool returns dual content:
return { content: [{ type: 'text' as const, text: markdown }], structuredContent: toStructuredContent(dataObject),};Markdown truncated to 10,000 chars via truncateResponse(). Errors use
isError: true with actionable hint text.
toStructuredContent performs a JSON round-trip cast to satisfy the SDK’s
[x: string]: unknown index signature.
Formatters (lib/mcp/formatters/)
Section titled “Formatters (lib/mcp/formatters/)”Parallel structure to tools/ — one formatter file per category. Each
formatter exports TypeScript interfaces for structured-content shapes plus
format* functions that produce the Markdown rendering. Dates use
formatDateUK (DD/MM/YYYY).
create_content_item Typed Provenance + Pipeline Audit
Section titled “create_content_item Typed Provenance + Pipeline Audit”S205 WP-A1 + S206 WP4 + S207 OPS-39 + OPS-40 (§1.21):
- Three typed optional fields —
source_url(Zod URL, max 2048),source_file(max 500),source_document_id(UUID FK tosource_documents) — persisted to typed columns oncontent_items. - Legacy
metadata.source_documentwrites return Zod errors advertising the three typed replacements (Zod 4 strip mode bypassed via.refine()so unknown keys are not silently dropped). - Every invocation emits exactly one
pipeline_runsrow viarecordPipelineRun({ pipelineName: 'mcp_create_content_item', … }). Audit coverage spans success / partial / draft / auth-fail / catch-all paths; auth-fail and outer-catch lazy-import the service-role client to bypass the admin-onlypipeline_runs_insertRLS policy (so editor callers cannot silently lose audit rows). - S207 WP-A4 added a typed
ingest_sourcecolumn oncontent_items. The MCP create handler stampsingest_source = 'mcp_create', andensure_v1_history_at_commit()reads this column to setcontent_history.change_reason = 'initial_ingest'. Trigger is the single authority for v1content_historyrows; all five app-level v1 inserts have been removed.
Search RPC + MCP Filter Widening
Section titled “Search RPC + MCP Filter Widening”S208 §5.5 Phase 4 (WP1):
- The
findtool’s chunk-granularity branch (formerly the standalonesearch_content_chunkstool, folded intofindunder ID-71) was widened withoverdue_review: booleanandreview_due_within_days: integer (1-365)Zod params. Migration20260428212936_extend_search_content_chunks_review_filters.sqlrecreated the RPC withfilter_overdue_review+filter_review_due_within_days(bothDEFAULT NULL); RPC-level filter via existing JOIN tocontent_items— Option A, zero round-trip cost. PreservesLANGUAGE plpgsql STABLE SECURITY DEFINER+SET search_path = public, extensions- GRANTs to
anon/authenticated/service_role.
- GRANTs to
whats_in_my_queue(the one faceted queue,facet: "governance"— formerlyget_governance_queue) was widened withinclude_overdue: boolean(default true) +status_filter: enum('pending'|'review_overdue'|'all'). Query switches from.eq('governance_review_status', 'pending')to.in([...])wheninclude_overdueis true. Existing 4-arg callers unchanged (backwards-compat).
S186 supersession filter (still current):
- Migration
20260421223339_add_include_superseded_to_search_rpcs.sqladdedinclude_superseded BOOLEAN DEFAULT falsetohybrid_searchandsearch_for_form_response(renamed fromsearch_for_bid_responsein the bid→forms rename). The WHERE clause(include_superseded OR ci.superseded_by IS NULL)excludes superseded rows from MCP retrieval by default. Direct ID lookup (thegettool,GET /api/items/:id) remains unchanged.
Cadence-Compliance Quality Score Modifier
Section titled “Cadence-Compliance Quality Score Modifier”S208 §5.5 Phase 5 (WP2) — lib/quality/quality-score.ts:
- Pure helper
cadenceCompliancePenalty(nextReviewDate, now)returns0/5-10/15/25/40per spec §9.3 schedule:>30dno penalty1-30dgraduated linear up to -101-14doverdue: -1515-30doverdue: -25>30doverdue: -40
- Boundary at
daysUntilDue === 0falls into overdue ≤14 tier (-15). freshnessRaw()applies penalty only whennextReviewDateis non-null (preservation rule §9.4 — items without cadence produce IDENTICAL scores to pre-Phase-5).QualityScoreInputextended with optionalnext_review_date?: string | null+review_cadence_days?: number | null.- Caller wires:
components/content/content-card.tsxandapp/api/cron/quality-score/route.tspass new fields explicitly;components/item-detail/metadata-sidebar.tsxpasses new fields toQualityScoreBreakdownso item-detail page aligns with content-card.lib/mcp/tools/quality.tsandcomponents/shared/quality-badge.tsxnot wired (read persisted scores; accept precomputedQualityScoreResult).
MCP Apps (Layer 2)
Section titled “MCP Apps (Layer 2)”Four apps live under mcp-apps/. Each is a Vite single-file build inlined
into lib/mcp/app-bundles.ts as a string constant.
| App | Trigger Tool | Resource URI | Purpose |
|---|---|---|---|
| Coverage Matrix | show_coverage_matrix | ui://coverage-matrix/app.html | Domains × freshness grid, drill-down, heat map, gap cards (S72) |
| Procurement Dashboard | show_procurement_dashboard | ui://form-dashboard/app.html | Form cards, urgency sorting, progress bars, drill-down to questions (S76+S81) |
| Reorient Me | show_reorient_me | ui://reorient-me/app.html | Personal briefing with 4-block layout (S84) |
| Intelligence Feed | show_intelligence_feed | ui://intelligence-feed/app.html | Sector intelligence digest cards scoped to workspace/period |
Build Pipeline
Section titled “Build Pipeline”bun run build:mcp-appsbuilds each app to a single HTML file.scripts/bundle-mcp-apps.tsinlines each HTML file intolib/mcp/app-bundles.tsas a string constant.lib/mcp/app-bundles.tsis committed (Vercel deployment cannot read the filesystem reliably).- Each app’s
mcp-apps/{name}/src/types.tsmust match the correspondinglib/mcp/formatters/*.tsinterface — enforced by__tests__/mcp/mcp-app-contracts.test.ts.
Knowledge Hub Plugin (Layer 3)
Section titled “Knowledge Hub Plugin (Layer 3)”Located at .claude/plugins/knowledge-hub/1.0.0/. The plugin is published to
the local marketplace and bundled via bun run build:plugin (regenerates
lib/mcp/plugin-bundle.ts, a base64-encoded ZIP).
Commands (8)
Section titled “Commands (8)”form-pipeline-review, form-status, briefing, coverage, digest,
draft-response, search, sector-briefing. Files under
.claude/plugins/knowledge-hub/1.0.0/commands/. (form-pipeline-review and
form-status were renamed from bid-pipeline-review / bid-status in the
bid→forms rename.)
Skills (9)
Section titled “Skills (9)”classification, completing-forms, content-creation, content-governance,
daily-briefing, governance-review, guide-builder,
knowledge-synthesis, search-strategy. Files under
.claude/plugins/knowledge-hub/1.0.0/skills/. (completing-forms is the
generalisation of the former bid-writing skill — procurement is the first
form type.)
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.guide-builder(S175) — 8-step conversational workflow for the four guide tools (create_guide,update_guide,get_guide,list_guides).
Maintenance Tooling
Section titled “Maintenance Tooling”- Taxonomy sync:
scripts/sync-plugin-taxonomy.tssynchronises skill files with canonical docs/schemas via<!-- TAXONOMY_INJECT -->markers. Run viabun run sync:taxonomy. - Pre-bundle validation:
scripts/bundle-plugin.tsenforces taxonomy validity before generating the Base64 ZIP bundle. - Reference-doc refresh: The
kpf:refresh-reference-docsplugin command (S205C) periodically refreshes tracked reference docs (4 parallel agents); partition matcheslib/docs/tracked-reference-docs.ts. AI integration documents in scope:ai-integration-layers.md,ai-integration-strategy.md.
AI-Visibility Enforcement
Section titled “AI-Visibility Enforcement”The AI-Visibility Policy (docs/reference/ai-visibility-policy.md) is fully
enforced across all user-facing surfaces.
What Viewers See
Section titled “What Viewers See”Nothing AI-branded. Quality scores, freshness states, summaries, digests, and classification are presented as platform features.
What Editors and Admins See on /item/[id]
Section titled “What Editors and Admins See on /item/[id]”- Source Information accordion:
classification_confidencerendered as a plain text percentage (S173 / S176 / S183 baseline; S197 amendment). Accordion is collapsed by default, has no AI branding, no colour-coded badge, no Sparkles icon, and no accompanying mechanism fields (model names, reasoning, tokens, cost). MUST NOT be shown to viewers.
What Admins See on /provenance
Section titled “What Admins See on /provenance”The admin Provenance route is the only surface where AI mechanism data is
exposed. Five tabs (components/provenance/):
| Tab | File | Purpose |
|---|---|---|
| Per-item | per-item-tab.tsx | UUID lookup → classification, processing, drafting, Review Schedule (S207 §5.5 Phase 3 T4) |
| Pipeline Health | pipeline-health-tab.tsx | Keyset-paginated pipeline_runs with time-range + kind filters, 20K truncation guard |
| Audit | audit-tab.tsx | 1:1 lift of ActivitySection, hosts PDF export |
| Cost (stub) | cost-tab-stub.tsx | pipeline_runs.cost aggregate; “Interim — Wave B” |
| Disputes (stub) | disputes-tab-stub.tsx | classification_disputes rows; “Interim — Wave C” |
Role gate: useUserRole().canAdmin client-side; getAuthorisedClient(['admin'])
on every API route.
PDF export: GET /api/admin/provenance/export/verification-history
generates an A4 PDF (via @react-pdf/renderer) with day-grouped verification
events. The export call itself is logged via recordPipelineRun().
Per-item Schema
Section titled “Per-item Schema”classification_disputes table (RLS, 5 policies, 4 indexes,
resolution-completeness CHECK) plus 7 nullable cost/token columns on
content_items. Two pipeline_runs indexes for Pipeline Health queries.
drafted_by Promotion
Section titled “drafted_by Promotion”Three AI-drafting routes changed from null to PIPELINE_SYSTEM_USER_ID.
The Per-item tab remaps this UUID to display name “Knowledge Hub”.
Redirects
Section titled “Redirects”/activity → /provenance?tab=audit; /settings?section=activity →
/provenance?tab=audit. Command palette entry relabelled
“Provenance › Audit”.
Eval Infrastructure
Section titled “Eval Infrastructure”MCP Eval Pipeline (Layers 1, 3, 4)
Section titled “MCP Eval Pipeline (Layers 1, 3, 4)”Layer 2 was rolled into Layer 1 + 3 during implementation; live commands are L1 + L3 + L4.
| Layer | Command | File | Checks | Coverage |
|---|---|---|---|---|
| L1 | bun run test:mcp-eval | scripts/mcp-eval/protocol-compliance.ts | 42 | JSON-RPC framing, tool counts, annotation invariants |
| L3 | bun run test:mcp-eval:rq | scripts/mcp-eval/response-quality.ts | 17 | Token efficiency, structured-content shape, error hints |
| L4 | bun run test:mcp-eval:fc | scripts/mcp-eval/functional-correctness.ts | 37 | Live DB CRUD cycles, error paths with cleanup |
scripts/mcp-eval/fixtures.ts is the canonical source for current
tool/prompt lists (CANONICAL_TOOL_NAMES, CANONICAL_PROMPT_NAMES). Tool
counts in the eval spec are stale; fixtures are authoritative.
CI integration: .github/workflows/ci.yml runs the eval matrix [l1, l3, l4] with AI-spend gating. Currently continue-on-error: true until
staging PII-scrubbed live-mirror lands (production-readiness §9.16.10).
Entity Classification Eval (scripts/eval-entity-classification.ts)
Section titled “Entity Classification Eval (scripts/eval-entity-classification.ts)”Measures entity extraction quality against a hand-labelled gold standard.
Modes:
| Flag | Mode | Description | Cost |
|---|---|---|---|
--cached | Cached | Compare against existing entity_mentions in DB | Free |
--live --confirm | Live | Re-run classification, then compare | ~$0.06/item |
--live --validate --confirm | Live + Pass 2 | Re-run with two-pass validation | ~$0.063/item |
Additional flags: --verbose, --json, --item <uuid>, --save-baseline.
Gold Standard Fixture
Section titled “Gold Standard Fixture”| Property | Value |
|---|---|
| File | __tests__/fixtures/entity-eval-gold-standard.json |
| Provenance | __tests__/fixtures/entity-eval-gold-standard.README.md |
| Baseline | __tests__/fixtures/eval-baselines/entity-classification.baseline.json |
Each gold standard item contains content_item_id, expected_entities,
excluded_entities. Item count tracked in fixture; minimum count (≥ 60)
enforced by __tests__/validation/eval-fixture-sync.test.ts.
Metrics
Section titled “Metrics”| Metric | Formula | Minimum Threshold |
|---|---|---|
| Precision | true positives / total extracted | 40% |
| Recall | true positives / total expected | 35% |
| F1 Score | harmonic mean of precision and recall | 35% |
| Type Accuracy | correctly typed / total matched | 80% |
| Exclusion Compliance | correctly excluded / total excluded expected | 50% |
| Cross-Item Type Consistency | entities with consistent type / total unique entities | 80% |
All metrics also have a max_drop threshold (5% for most, 10% for exclusion
and consistency) for regression detection against saved baselines.
Shared Eval Framework (lib/eval/)
Section titled “Shared Eval Framework (lib/eval/)”| Module | File | Purpose |
|---|---|---|
metrics | lib/eval/metrics.ts | Pure metric functions (precision, recall, F1, ROUGE-L, ROUGE-1, MRR, nDCG@k, P@k) |
types | lib/eval/types.ts | Shared types (EvalResult, EvalBaseline, RegressionResult, gold standard interfaces) |
baseline | lib/eval/baseline.ts | Baseline storage and regression detection (JSON files in __tests__/fixtures/eval-baselines/) |
reporter | lib/eval/reporter.ts | Console and JSON report formatting |
Quality Gate — AI-Adjacent Checks (S185)
Section titled “Quality Gate — AI-Adjacent Checks (S185)”scripts/quality-gate.ts is a read-only observer that inspects DB state
after ingestion and emits pass/fail per a configurable profile. Several of
its 12 generic checks surface AI-adjacent gaps:
| Check | What it detects | Severity (re-ingest) |
|---|---|---|
embedding_coverage | Items with NULL embedding | must-pass |
entity_mention_coverage | Items with zero entity mentions | must-pass |
entity_rel_coverage | Items with zero entity relationships | should-pass |
classified_domains_not_empty | Items with NULL primary_domain | must-pass |
classified_but_no_confidence | Items with classified_at but NULL confidence | should-pass |
summary_coverage | Items with NULL summary | should-pass |
chunk_coverage | Items with zero chunks | must-pass |
The audit-content companion profile adds 7 further checks calibrated to the client corpus.
CLI: bun run scripts/quality-gate.ts --threshold=re-ingest. Config sidecars
under scripts/config/quality-gate/. 4 profiles: re-ingest, batch,
onboarding, audit-content.
Configuration
Section titled “Configuration”| Setting | Location | Default | Purpose |
|---|---|---|---|
ANTHROPIC_API_KEY | .env.local | — | Anthropic API authentication |
OPENAI_API_KEY | .env.local | — | OpenAI API authentication |
AI_SUMMARY_MODEL | .env.local | claude-sonnet-4-6 | Model for classification and summaries |
AI_CLASSIFICATION_MODEL | .env.local | claude-opus-4-6 | Python classification model |
AI_EMBEDDING_MODEL | .env.local | text-embedding-3-large | Embedding model |
AI_EMBEDDING_DIMS | .env.local | 1024 | Embedding dimensions |
AI_ANALYSIS_MODEL | .env.local | claude-sonnet-4-5 | Bid drafting analysis tier |
AI_DRAFTING_MODEL | .env.local | claude-opus-4-6 | Bid drafting response tier |
AI_QUALITY_MODEL | .env.local | claude-haiku-4-5 | Bid drafting quality check tier |
SENTRY_* | .env.local | — | Sentry release tagging (S10 prod-readiness; bare SENTRY_* per Vercel→Sentry integration default) |
Env vars are validated at boot via lib/env.ts (serverEnv) +
lib/env-client.ts (clientEnv). Sentry SDK init reads DSN directly from
process.env (decoupled from the clientEnv Zod gate so env-validation
failures cannot silence the very tool meant to surface them).
Model Pricing Constants (lib/ai/pricing.ts)
Section titled “Model Pricing Constants (lib/ai/pricing.ts)”| Model | Input ($/M) | Output ($/M) | Cache Read ($/M) | Cache Write ($/M) |
|---|---|---|---|---|
claude-opus-4-6 | 15.00 | 75.00 | 1.50 | 18.75 |
claude-sonnet-4-5 | 3.00 | 15.00 | 0.30 | 3.75 |
claude-sonnet-4-6 | 3.00 | 15.00 | 0.30 | 3.75 |
claude-haiku-4-5 | 0.80 | 4.00 | 0.08 | 1.00 |
Database Tables
Section titled “Database Tables”| Table | Purpose | Key Columns | RLS |
|---|---|---|---|
entity_mentions | Stores extracted entity mentions per item | content_item_id, entity_type, entity_name, canonical_name, confidence, context_snippet | Role-based via get_user_role() |
entity_relationships | Stores relationships between entities | source_entity, relationship_type, target_entity, source_item_id, confidence | Role-based |
entity_aliases | Maps variant entity names to canonical form | alias, canonical, is_active | Role-based |
content_items | AI-enriched fields on the main content table | embedding, primary_domain, primary_subtopic, ai_keywords, summary, summary_data, classification_confidence, classified_at, ingest_source, next_review_date, review_cadence_days | Role-based |
content_chunks | Heading-level chunks with per-chunk embeddings | content_item_id, heading_text, heading_path, content, embedding, position | Role-based |
pipeline_runs | Audit log of every AI/ingest invocation | pipeline_name, status, items_processed, items_created, result, cost | Admin (insert via service-role) |
classification_disputes | Per-item classification dispute records | content_item_id, disputed_by, dispute_reason, resolved_at, resolution_notes | Admin |
content_items.content_text_hash is GENERATED ALWAYS — never set
explicitly; PG auto-computes via md5(normalised content). Omit from
payloads.
Key Unique Constraints
Section titled “Key Unique Constraints”entity_mentions:(canonical_name, entity_type, content_item_id)— the upsert conflict target.entity_relationships:(source_entity, relationship_type, target_entity, source_item_id)withNULLS NOT DISTINCT(S183 WP1 G1; migration20260421171520_entity_relationships_unique_tuple_constraint.sql). Deduped 472 to 454 rows. Callsites use.upsert({ ignoreDuplicates: true }).entity_aliases:(alias)— each alias maps to exactly one canonical form.
Relationship Types (10)
Section titled “Relationship Types (10)”Defined in lib/ai/classify.ts tool schema:
holds, complies_with, delivers_to, uses, demonstrated_by, requires,
part_of, supersedes, references, evidences.
Python Pipeline Parity
Section titled “Python Pipeline Parity”The Python pipeline (scripts/kb_pipeline/) mirrors the TypeScript
classification pipeline for CLI ingestion.
| Python Module | File | TS Equivalent |
|---|---|---|
classify | scripts/kb_pipeline/classify.py | lib/ai/classify.ts |
embed | scripts/kb_pipeline/embed.py | lib/ai/embed.ts |
chunk | scripts/kb_pipeline/chunk.py | lib/content/chunking.ts |
layer_inference | scripts/kb_pipeline/layer_inference.py | lib/layer-inference.ts |
temporal_bridge | scripts/kb_pipeline/temporal_bridge.py | lib/entities/entity-metadata-bridge.ts |
summarise | scripts/kb_pipeline/summarise.py | lib/ai/summarise.ts |
post_insert | scripts/kb_pipeline/post_insert.py | (no single TS equivalent — steps are inline in each route) |
supersede | scripts/kb_pipeline/supersede.py | lib/supersession/set.ts |
Parity Constants
Section titled “Parity Constants”These values must match between TS and Python. Verified by
__tests__/validation/pipeline-parity.test.ts:
| Constant | Value |
|---|---|
| Classification truncation limit | 5,000 chars |
| Entity types | 12 types (same set) |
| Excluded entity patterns | 5 regex patterns (same set) |
| Temporal entity types | certification, framework, regulation |
| Layer inference rules | 7 rules, same priority order |
| Layer keys | 4 keys (same set) |
| Canonicalisation rules | 12 rules, same order |
| Proper noun allowlist | 19 entries (same set) |
| Abbreviations lookup | 39 entries (matching) |
| MAX_EMBEDDING_CHARS | 24,000 |
Key Divergences (intentional)
Section titled “Key Divergences (intentional)”| Divergence | Python | TypeScript | Reason |
|---|---|---|---|
| Default classification model | claude-opus-4-6 | claude-sonnet-4-6 | Python CLI is batch; cost is acceptable |
| Embedding text composition | title + summary + content | suggestedTitle + content | Python includes summary (Plan D D5) |
| Entity context extraction | Not implemented | Implemented | Gap (not yet planned) |
| Taxonomy validation | Warn-only | Auto-correct | Gap (not yet planned) |
| Content truncation ellipsis | Appends "..." | No ellipsis | Cosmetic |
Python Post-Insert Helper (S185)
Section titled “Python Post-Insert Helper (S185)”scripts/kb_pipeline/post_insert.py::run_post_insert() consolidates 8
post-insert side-effects into a single shared function with canonical
ordering. Wired into 4 scripts:
scripts/kb_pipeline/pipeline.py (EP1), scripts/ingest_markdown.py (EP2),
scripts/ingest_stage2_markdown.py (EP2b), and the form-library import path
(EP8, formerly import_bid_library.py). All steps are best-effort — errors
accumulate on PostInsertResult.errors rather than raising. Closes the S181
chunk-skip regression on EP2b.
Stale (flagged for docubot lane): the
scripts/kb_pipeline/Python ingest described here has been superseded by the cocoindex pipeline (scripts/cocoindex_pipeline/); the script paths predate that migration. The bid→forms rename only updates the formerimport_bid_library.pyreference.
Testing
Section titled “Testing”Unit Tests (AI-specific)
Section titled “Unit Tests (AI-specific)”Test counts live in docs/generated/codebase-stats.md. Key files:
| Test File | Covers |
|---|---|
__tests__/lib/ai-classify-entities.test.ts | Entity exclusion filters, shouldExcludeEntity() |
__tests__/lib/ai-classify-skill.test.ts | Classification skill loading |
__tests__/lib/ai-classify-subtopic-coercion.test.ts | coerceSubtopic() edge cases |
__tests__/lib/ai-classify-filter-canonicalise.test.ts | Post-canonicalise filter chain |
__tests__/lib/classify-two-pass.test.ts | Pass 2 validation flow |
__tests__/lib/classify-entity-dedup.test.ts | Entity mention row deduplication |
__tests__/lib/entity-dedup.test.ts | canonicalise() rules |
__tests__/lib/entity-aliases.test.ts | Alias resolution and caching |
__tests__/lib/entity-context-snippet.test.ts | Context snippet extraction |
__tests__/lib/entity-metadata-bridge.test.ts | Temporal-to-entity bridging |
__tests__/lib/entity-validation.test.ts | Pass 2 entity validation |
__tests__/lib/embed-max-chars.test.ts | MAX_EMBEDDING_CHARS enforcement |
__tests__/lib/ai-parse.test.ts | Tool result extraction from Claude responses |
__tests__/lib/quality-score.test.ts | Cadence-compliance penalty (S208 §5.5 Phase 5) |
__tests__/mcp/formatters-ai.test.ts | AI tool response formatting |
__tests__/mcp/create_content_item.test.ts | Typed provenance + pipeline_runs audit |
__tests__/mcp/search-chunks-tool.test.ts | Review-cadence filter pass-through (S208 Phase 4) |
__tests__/mcp/tool-annotations-coverage.test.ts | defineTool annotation invariant guard (S172) |
__tests__/mcp/mcp-app-contracts.test.ts | App types ↔ formatter interface parity |
__tests__/validation/mcp-fixture-sync.test.ts | Tool/prompt list parity vs registrations |
Eval Tests
Section titled “Eval Tests”| Test File | Covers |
|---|---|
__tests__/eval/entity-classification-eval.test.ts | Entity eval suite integration |
__tests__/eval/classification-eval.test.ts | Domain classification eval |
__tests__/eval/search-eval.test.ts | Search quality eval |
__tests__/validation/eval-fixture-sync.test.ts | Fixture integrity guard |
__tests__/validation/pipeline-parity.test.ts | TS/Python constant drift detection |
Integration Tests
Section titled “Integration Tests”| Test File | Covers |
|---|---|
__tests__/integration/classification-entity-certification-flow.test.ts | End-to-end certification entity flow |
__tests__/integration/certification-bridge-flow.integration.test.ts | Temporal bridge integration |
__tests__/integration/golden-path-real-db.integration.test.ts | Full pipeline against real DB |
__tests__/integration/ingest-source-fan-out.integration.test.ts | S207 typed-column fan-out + trigger-sole-authority |
Current Limitations
Section titled “Current Limitations”- Pass 2 is not enabled by default. The two-pass system must be opted in
via
validate=true. Real-time ingestion paths (upload, URL ingest, MCP create) do not enable it due to latency and cost concerns. - Entity context extraction missing in Python pipeline.
context_snippetis only populated by the TypeScript pipeline. Python-ingested items havenullcontext snippets. - Markdown chunking gaps. Python-side closed S185. TS-side gaps remain
at batch item creation (EP6) and form-outcome integration (EP10) — items
from those paths are not surfaced via the
findtool’s chunk-granularity branch. - Gold standard fixture coverage. Items cover common entity types but not all 12 types evenly. Follow-up expansion planned from real ingested content.
- No streaming classification. Classification is fully synchronous; long documents block the request until completion.
- Single-model classification. Pass 1 uses a single model for all content types. No content-type-specific prompt variants.
- Embedding cache is per-process. The in-memory embedding cache
(
lib/ai/embed.ts) is not shared across Vercel serverless invocations. Cache hit rate is low in production. - MCP eval CI gates are
continue-on-error. Hard-fail enforcement blocked on the staging PII-scrubbed live-mirror (production-readiness §9.16.10). - Cloud Run NL recommendation outstanding. The Cloud Run “natural language” recommendation from production-readiness S10 (WP-RUN.1) is research-only at present; no execution layer attached.
Architecture Decisions
Section titled “Architecture Decisions”| Decision | Rationale | Alternative Considered |
|---|---|---|
| Two-pass validation (Pass 1 + Pass 2) | Deterministic filters are free and fast; LLM validation is accurate but expensive. Layering both maximises quality. | Single-pass with more aggressive prompt engineering |
| Haiku for Pass 2 (not Sonnet) | Validation is a simpler task than extraction; Haiku is 4× cheaper with acceptable quality. | Sonnet for both passes |
| Deterministic entity filters over prompt rules | Deterministic = 100% precision, 0 cost, instant. Prompt rules for entity exclusion regress easily. | All exclusion in the LLM prompt |
| ISO certification type override | Eliminates cross-item type flip-flop for the 6 most common ISO families. Taxonomy spec backs it. | Per-item Pass 2 retyping only |
| Delete-before-insert for entity_mentions | Clean-slate ensures re-classification reflects CURRENT output, not stale rows from prior runs. | Upsert with ignoreDuplicates |
MAX_EMBEDDING_CHARS = 24,000 | Safety margin for non-English text (3 chars/token) within OpenAI’s 8,192-token limit. | 28,000 (original; exceeded cap on non-English) |
| Python uses Opus, TS uses Sonnet for classification | Python runs batch CLI (cost acceptable); TS runs real-time web requests (latency matters). | Same model for both |
| Entity mention row dedup in application code | Postgres upsert cannot update the same conflict target twice in one statement (error 21000). | Multiple single-row upserts |
| Skill files as markdown (not hardcoded prompts) | Separates prompt engineering from code; easier iteration and version control. | Inline prompt strings |
defineTool wrapper + annotation invariant | Compile-time enforcement that every tool declares all four ToolAnnotations advisory fields. | Runtime check / honour-system convention |
Fresh McpServer per request | mcp-handler shared transport corrupts on Vercel warm instances. SDK transport directly avoids the bug. | mcp-handler library (broken on Vercel) |
pipeline_runs audit on auth-fail | Editor-RLS would silently lose audit rows. Lazy-imported service-role client preserves audit completeness. | Skip audit on auth fail (silent gap) |
| Cadence-compliance penalty on freshness sub-score | Items with active review obligations should de-prioritise quality when the cadence is missed. | Standalone penalty / no penalty |
Admin-only /provenance route | Concentrates AI mechanism data in one place under explicit admin gate; preserves invisibility for editors+viewers. | Per-item drawer surfaced to editors |
Typed provenance columns on content_items | Replace JSONB metadata.source_document with typed source_url / source_file / source_document_id columns. | JSONB blob with Zod validation |
Single-source ensure_v1_history_at_commit trigger | Eliminates app-level v1-history inserts; trigger is sole authority. Prevents drift between code paths. | Each entry point writes its own v1 row |