Skip to content

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).

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.


Per docs/reference/ai-integration-layers.md:

LayerSurfaceAudienceWhere it lives
Layer 0AI Service Layer + Skill FilesFoundation (consumed by all layers)lib/ai/, lib/ai/skills/, Supabase
Layer 1MCP Server (tools, resources, prompts)Any MCP clientlib/mcp/, app/api/mcp/[transport]/route.ts
Layer 2MCP Apps (visual cards inside Claude)Claude Desktop / Claude.ai usersmcp-apps/, lib/mcp/app-bundles.ts
Layer 3Knowledge Hub Plugin (commands + skills)Knowledge workers in Cowork/Claude.claude/plugins/knowledge-hub/1.0.0/, plugin-bundle.ts
Layer 4Claude 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.


MethodRouteAuthPurposeFile
POST/api/items/[id]/classifyEditor+Trigger classification for itemapp/api/items/[id]/classify/route.ts
POST/api/summaries/generateEditor+Generate AI summary for itemapp/api/summaries/generate/route.ts
POST/api/embedEditor+Generate embedding for arbitrary textapp/api/embed/route.ts
POST/api/extractEditor+Structured content extractionapp/api/extract/route.ts
POST/api/visionEditor+PDF/image analysisapp/api/vision/route.ts
POST/api/change-reports/generateEditor+Change digest generationapp/api/change-reports/generate/route.ts
POST/api/procurement/[id]/questions/extractEditor+Tender question extraction (PDF/DOCX)app/api/procurement/[id]/questions/extract/route.ts
POST/api/procurement/[id]/extract-metadataEditor+Tender metadata extractionapp/api/procurement/[id]/extract-metadata/route.ts
POST/api/procurement/[id]/matchEditor+KB matching with confidence assessmentapp/api/procurement/[id]/match/route.ts
POST/api/procurement/[id]/responses/draftEditor+3-pass form response drafting pipelineapp/api/procurement/[id]/responses/draft/route.ts
POST/api/mcp/mcpOAuthMCP Streamable HTTP transportapp/api/mcp/[transport]/route.ts
GET/api/provenance/item/[id]AdminPer-item provenance (admin-only)app/api/provenance/item/[id]/route.ts
GET/api/admin/provenance/pipeline-runsAdminPipeline Health (keyset-paginated)app/api/admin/provenance/pipeline-runs/route.ts
GET/api/admin/provenance/export/verification-historyAdminPDF audit exportapp/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.


13 modules centralise every AI integration point. API routes import from @/lib/ai rather than calling Anthropic/OpenAI SDKs directly.

ModuleFilePurposeSDK
classifylib/ai/classify.tsDomain, subtopic, keywords, summary, entities, relationshipsAnthropic (tool use)
summariselib/ai/summarise.tsExecutive / detailed / takeawaysAnthropic (tool use)
digestlib/ai/change-reports.tsChange digest generationAnthropic
embedlib/ai/embed.tsEmbeddings (1,024-dim, env-driven)OpenAI
matchlib/ai/match.tsKB matching + confidence assessmentOpenAI + Anthropic
draftlib/ai/draft.ts3-pass form response drafting pipelineAnthropic
extract-contentlib/ai/extract-content.tsStructured content extractionAnthropic
extract-questionslib/ai/extract-questions.tsPDF/DOCX tender question + metadata extractionAnthropic (tool use)
visionlib/ai/vision.tsPDF/image analysisAnthropic (vision)
quality-checklib/ai/quality-check.tsDeterministic + AI response quality checksAnthropic
pricinglib/ai/pricing.tsModel pricing constants (USD/M tokens)
errorslib/ai/errors.tsAIServiceError with HTTP status codes
skills/loaderlib/ai/skills/loader.tsIn-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.

ModuleFilePurpose
entity-deduplib/entities/entity-dedup.ts12-rule canonicalisation
entity-aliaseslib/entities/entity-aliases.tsDB-backed alias resolution with baseline fallback
entity-contextlib/entities/entity-context.tsContext snippet extraction (+/-80 chars)
entity-metadata-bridgelib/entities/entity-metadata-bridge.tsBridges temporal references to entity mention metadata
ModuleFilePurpose
layer-inferencelib/layer-inference.ts7-rule deterministic content layer suggestion
strip-markdownlib/content/strip-markdown.tsStrips markdown for plain text input to classifier
html-to-markdownlib/content/html-to-markdown.tsGFM-aware idempotent HTML→markdown bridge (S171)
client-configlib/client-config.tsClient-specific disambiguation + entity examples
validation/schemaslib/validation/schemas.tsTag normalisation (normaliseTag), proper noun allowlist
pipeline/record-runlib/pipeline/record-run.tsrecordPipelineRun() — audit row helper
provenance/pricinglib/provenance/pricing.tsPer-model token pricing for admin Provenance display
quality/quality-scorelib/quality/quality-score.tsComposite 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)”
export async function classifyContent(
params: ClassifyParams,
): Promise<ClassificationResult>;

Parameters (ClassifyParams):

ParamTypeRequiredPurpose
supabaseSupabaseClient<Database>YesSupabase client for DB reads/writes
itemIdstringYesUUID of the content item to classify
forcebooleanYesRe-classify even if already classified
userIdstringYesUUID of the user triggering classification
validatebooleanNoEnable 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.

The classification pipeline runs these steps sequentially:

  1. Fetch content item — Reads from content_items by itemId.
  2. Cache check — If classified_at is set and force=false, returns cached classification immediately.
  3. Load skill filesclassification.md (main prompt) and classification-entity-types.md (entity type reference) from lib/ai/skills/.
  4. Build taxonomy string — Fetches active domains and subtopics from taxonomy_domains and taxonomy_subtopics tables.
  5. Interpolate prompt — Substitutes {TAXONOMY}, {CLIENT_DISAMBIGUATION}, {CLIENT_ORGANISATION_NAME}, etc. Appends entity types reference and the payment gateway product anchor.
  6. Prepare content — Strips markdown via stripMarkdown(), then truncates to 5,000 characters for classification input.
  7. Pass 1: Claude API call — Calls Claude (default model: claude-sonnet-4-6, configurable via AI_SUMMARY_MODEL env var) with return_classification tool use. Max tokens: 2,500. Returns domain, subtopic, keywords, summary, entities, relationships, temporal references.
  8. Coerce subtopics — Empty/whitespace subtopics coerced to null via coerceSubtopic().
  9. Validate domainsvalidateDomain() matches AI output against active taxonomy slugs (fuzzy fallback).
  10. Normalise keywords — Handles Claude returning a string instead of an array, applies normaliseTag(), deduplicates.
  11. Generate embeddinggenerateEmbedding() on suggestedTitle + plainText, truncated to MAX_EMBEDDING_CHARS (24,000). Serialised as JSON.stringify(embedding) for Supabase RPC.
  12. Store temporal references — Merges ai_temporal_references into content_items.metadata JSONB.
  13. UPDATE content_items — Writes all classification fields + embedding + temporal refs.
  14. Delete existing entity mentions — Wipes entity_mentions for this content_item_id (clean-slate before re-insert).
  15. Entity processing pipeline (see below).
  16. Store entity relationships — Inserts into entity_relationships with canonicalised source/target names.
  17. Temporal-entity bridgebridgeTemporalReferencesToEntities() 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-filtershouldExcludeEntity() applies 10 categories of exclusion filters on the raw AI output (before canonicalisation):

FilterFunctionCatches
Identifier patternsisExcludedEntity()SIC codes, VAT numbers, DUNS numbers, pure numerics
Internal documentsisInternalDocument()Policies, procedures, plans, registers, agreements
Generic conceptsisGenericConcept()~80 terms: “information security”, “encryption”, etc.
Role titlesisRoleTitle()”Managing Director”, “DPO”, “CTO”, etc.
Protocols/formatsisProtocolOrFormat()HTTPS, SSH, PDF, AES-256, Python, etc.
Insurance/contractsisInsuranceOrContract()Professional indemnity, NDA, SLA, etc.
Management system acronymsisManagementSystemAcronym()ISMS, QMS, EMS (prefer the certification)
GDPR artefactsisGdprArtefact()DPIA, ROPA, lawful basis, consent, etc.
Framework lot numbersisFrameworkLot()”G-Cloud Lot 1”, etc.
Compound entitiesisCompoundEntity()”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 dedupdedupeEntityMentionRows() 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).


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.

PropertyPass 1Pass 2
Modelclaude-sonnet-4-6 (configurable)claude-haiku-4-5 (hardcoded)
PurposeClassification + entity extractionEntity quality validation
Max tokens2,5001,500
Temperature(default)0
Cost~$0.06/item~$0.003/item
MandatoryYesNo (opt-in via validate=true)

buildValidationPrompt() constructs a detailed prompt with:

  1. Five sequential tests per entity: Named Entity Test, External Reference Test, Role Title Test, Protocol/Format Test, Type Accuracy Test.
  2. Per-type diagnostic questions — e.g. for organisation: “Does it have a legal registration or government charter?”
  3. Common false positive patterns — ISMS/QMS, insurance products, GDPR artefacts, contract types, security principles, geographic regions.
  4. 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
  5. Payment Gateway Product Anchor — Six branded payment gateways that must be classified as product (not technology or organisation): Access PaySuite, Adalante Smartpay, Opayo, Pay360, WorldPay, Stripe.

Each entity receives one of three verdicts:

VerdictMeaningAction
confirmedPasses all tests with correct typeKeep as-is
retypedValid entity but wrong type assignedKeep with corrected type
removedFails one or more testsDrop from entity_mentions

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.


The entity type taxonomy is shared between TS and Python pipelines:

TypeDescriptionExample
organisationLegal entity with registration or government charterNHS Digital, Capita
certificationObtained by assessment with issuing body and renewalISO 27001, Cyber Essentials Plus
regulationNon-compliance carries legal penaltiesGDPR, Data Protection Act 2018
frameworkPublished guidance for voluntary adoptionNIST CSF, ITIL
capabilityService listed on company websitePenetration Testing Service
personSpecific named individual(redacted in KB context)
technologySpecific named platform with vendor and versionMicrosoft Azure, Jira
projectNamed piece of work with start, scope, and endNHS Spine Programme
sectorRecognised industry classificationHealthcare, Education
productNamed branded offering the organisation sellsAccess PaySuite, Opayo
standardNumbered document published by a standards bodyBS 10008, PAS 1192
methodologyNamed approach with its own body of knowledgePRINCE2, 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:

  1. Trim whitespace
  2. Convert slug-style names to proper case (“penetration-testing” becomes “Penetration Testing”)
  3. Normalise basic ISO formats (“ISO27001” becomes “ISO 27001”)
  4. Normalise extended ISO formats (“ISO/IEC 27001”, “ISO-27001” all become “ISO 27001”)
  5. Strip ISO version suffixes (“ISO 27001:2022” becomes “ISO 27001”)
  6. Normalise Cyber Essentials variants
  7. WCAG normalisation (“Wcag 2 1 Aa” becomes “WCAG 2.1 AA”)
  8. Company suffix normalisation (“Ltd” becomes “Limited”)
  9. Fix single-word abbreviations (“gdpr” becomes “GDPR”) — uses a 39-entry lookup
  10. Multi-word title case for all-lowercase inputs
  11. Plural normalisation — type-aware stripping of trailing ‘s’ for applicable types
  12. 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.


PropertyValue
ProviderOpenAI
Modeltext-embedding-3-large (configurable via AI_EMBEDDING_MODEL)
Dimensions1,024 (configurable via AI_EMBEDDING_DIMS, schema-locked to vector(1024))
Max input chars24,000 (MAX_EMBEDDING_CHARS)
CacheIn-memory, 500 entries, 1-hour TTL
DB column typevector(1024) (pgvector)
SerialisationJSON.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 pipelinebuild_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.


callSummaryAI() is a pure function (no Supabase dependency) suitable for both the service layer and batch scripts:

PropertyValue
Modelclaude-sonnet-4-6 (configurable via AI_SUMMARY_MODEL)
Max tokens2,000
Max content100,000 characters (truncated internally)
Toolreturn_summary
Output schema{ executive, detailed, takeaways }
ValidationZod via SummaryResponseSchema

Output structure (SummaryData):

FieldTypeDescription
executivestringSingle sentence summary (max 150 chars)
detailedstring2-3 paragraph detailed summary
takeawaysstring[]3-7 key takeaways
generated_atstringISO 8601 timestamp
modelstringModel used for generation
tokens_usednumberTotal input + output tokens

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_data as JSONB and syncs summary with the executive text (NOT ai_summaryfeed_articles.ai_summary is intentionally separate)

A pure, deterministic function with no AI calls and no database queries. Evaluates 7 rules in strict priority order and returns the first match.

#ConditionSuggested LayerConfidence
1isBidDiscovered === truebid_detailhigh
2ingestionSource === 'bid_library' AND contentType === 'q_a_pair'bid_detailhigh
3ahasReference === truecompany_referencehigh
3bhasDetail === true AND hasBrief === truebid_detailmedium
3chasBrief === true AND hasDetail === falsesales_briefmedium
4contentType in {policy, compliance, certification}company_referencemedium
4bcontentType === 'research'researchhigh
4ccontentType === 'case_study'bid_detailmedium
4dcontentType in {product_description, capability, methodology}bid_detailmedium
5Content length heuristics (Q&A pairs, short/long)varieslow
6ingestionSource === 'url_import'researchlow
7Defaultbid_detaillow
KeyMeaning
sales_briefShort positioning content for sales use
bid_detailDetailed content for form responses
company_referenceInternal reference material (policies, etc.)
researchExternal research and background material

These are defined in lib/client-config.ts and mirrored in scripts/kb_pipeline/layer_inference.py.


FilePurpose
app/api/mcp/[transport]/route.tsHTTP route handler; POST /api/mcp/mcp is primary
lib/mcp/tools/index.tsTool registration barrel — call order = discovery
lib/mcp/resources.tsResource + prompt registration
lib/mcp/auth.tsBearer-token auth, RLS-scoped client, role checks
lib/mcp/tools/shared.tsdefineTool 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.

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:

ConstantUse Case
READ_ONLY_ANNOTATIONSSearch, get, list, find, audit, suggest, show
SAFE_WRITE_ANNOTATIONSUpdate, assign, cite, classify, summarise (idempotent)
DESTRUCTIVE_WRITE_ANNOTATIONSDelete, supersede
NON_IDEMPOTENT_WRITE_ANNOTATIONSCreate (fresh UUID per call)
NON_IDEMPOTENT_OPEN_WORLD_WRITE_ANNOTATIONSExternal-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 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 fileTools
search.tsfind, find_duplicates
content.tsget, create_content_item, update_content_item, get_workspace_items, assign, get_document_versions, get_document_diff
procurement.tslist_active_procurement, get_procurement_detail, get_form_question, cite_content, get_content_effectiveness
dashboard.tsget_reorientation, where_are_we_exposed
quality.tssuggest_content_creation
ai.tsclassify_content, generate_summary
entities.tsget_entity_relationships
templates.tslist_templates, get_template_coverage, get_template_gaps
apps.tsshow_coverage_matrix, show_procurement_dashboard, show_reorient_me, show_intelligence_feed
governance.tsdelete_content_item, update_governance_status, update_publication_status, review_governance_item
supersession.tssupersede_content_item (S186)
review.tswhats_in_my_queue, create_review_assignment
intelligence.tsget_intelligence_summary, trigger_intelligence_poll
guides.tslist_guides, get_guide, create_guide, update_guide (S175)
change-report.tsget_change_report
workspaces.tslist_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.

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.

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.

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.

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 to source_documents) — persisted to typed columns on content_items.
  • Legacy metadata.source_document writes 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_runs row via recordPipelineRun({ 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-only pipeline_runs_insert RLS policy (so editor callers cannot silently lose audit rows).
  • S207 WP-A4 added a typed ingest_source column on content_items. The MCP create handler stamps ingest_source = 'mcp_create', and ensure_v1_history_at_commit() reads this column to set content_history.change_reason = 'initial_ingest'. Trigger is the single authority for v1 content_history rows; all five app-level v1 inserts have been removed.

S208 §5.5 Phase 4 (WP1):

  • The find tool’s chunk-granularity branch (formerly the standalone search_content_chunks tool, folded into find under ID-71) was widened with overdue_review: boolean and review_due_within_days: integer (1-365) Zod params. Migration 20260428212936_extend_search_content_chunks_review_filters.sql recreated the RPC with filter_overdue_review + filter_review_due_within_days (both DEFAULT NULL); RPC-level filter via existing JOIN to content_items — Option A, zero round-trip cost. Preserves LANGUAGE plpgsql STABLE SECURITY DEFINER + SET search_path = public, extensions
    • GRANTs to anon/authenticated/service_role.
  • whats_in_my_queue (the one faceted queue, facet: "governance" — formerly get_governance_queue) was widened with include_overdue: boolean (default true) + status_filter: enum('pending'|'review_overdue'|'all'). Query switches from .eq('governance_review_status', 'pending') to .in([...]) when include_overdue is true. Existing 4-arg callers unchanged (backwards-compat).

S186 supersession filter (still current):

  • Migration 20260421223339_add_include_superseded_to_search_rpcs.sql added include_superseded BOOLEAN DEFAULT false to hybrid_search and search_for_form_response (renamed from search_for_bid_response in 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 (the get tool, GET /api/items/:id) remains unchanged.

S208 §5.5 Phase 5 (WP2) — lib/quality/quality-score.ts:

  • Pure helper cadenceCompliancePenalty(nextReviewDate, now) returns 0/5-10/15/25/40 per spec §9.3 schedule:
    • >30d no penalty
    • 1-30d graduated linear up to -10
    • 1-14d overdue: -15
    • 15-30d overdue: -25
    • >30d overdue: -40
  • Boundary at daysUntilDue === 0 falls into overdue ≤14 tier (-15).
  • freshnessRaw() applies penalty only when nextReviewDate is non-null (preservation rule §9.4 — items without cadence produce IDENTICAL scores to pre-Phase-5).
  • QualityScoreInput extended with optional next_review_date?: string | null + review_cadence_days?: number | null.
  • Caller wires: components/content/content-card.tsx and app/api/cron/quality-score/route.ts pass new fields explicitly; components/item-detail/metadata-sidebar.tsx passes new fields to QualityScoreBreakdown so item-detail page aligns with content-card. lib/mcp/tools/quality.ts and components/shared/quality-badge.tsx not wired (read persisted scores; accept precomputed QualityScoreResult).

Four apps live under mcp-apps/. Each is a Vite single-file build inlined into lib/mcp/app-bundles.ts as a string constant.

AppTrigger ToolResource URIPurpose
Coverage Matrixshow_coverage_matrixui://coverage-matrix/app.htmlDomains × freshness grid, drill-down, heat map, gap cards (S72)
Procurement Dashboardshow_procurement_dashboardui://form-dashboard/app.htmlForm cards, urgency sorting, progress bars, drill-down to questions (S76+S81)
Reorient Meshow_reorient_meui://reorient-me/app.htmlPersonal briefing with 4-block layout (S84)
Intelligence Feedshow_intelligence_feedui://intelligence-feed/app.htmlSector intelligence digest cards scoped to workspace/period
  1. bun run build:mcp-apps builds each app to a single HTML file.
  2. scripts/bundle-mcp-apps.ts inlines each HTML file into lib/mcp/app-bundles.ts as a string constant.
  3. lib/mcp/app-bundles.ts is committed (Vercel deployment cannot read the filesystem reliably).
  4. Each app’s mcp-apps/{name}/src/types.ts must match the corresponding lib/mcp/formatters/*.ts interface — enforced by __tests__/mcp/mcp-app-contracts.test.ts.

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).

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.)

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 with content-governance (framework) as the active-workflow companion. Renamed from change-management to avoid collision with the Anthropic operations:change-management skill.
  • daily-briefing — 5-persona daily stand-up skill that composes with the Anthropic sales:daily-briefing via 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).
  • Taxonomy sync: scripts/sync-plugin-taxonomy.ts synchronises skill files with canonical docs/schemas via <!-- TAXONOMY_INJECT --> markers. Run via bun run sync:taxonomy.
  • Pre-bundle validation: scripts/bundle-plugin.ts enforces taxonomy validity before generating the Base64 ZIP bundle.
  • Reference-doc refresh: The kpf:refresh-reference-docs plugin command (S205C) periodically refreshes tracked reference docs (4 parallel agents); partition matches lib/docs/tracked-reference-docs.ts. AI integration documents in scope: ai-integration-layers.md, ai-integration-strategy.md.

The AI-Visibility Policy (docs/reference/ai-visibility-policy.md) is fully enforced across all user-facing surfaces.

Nothing AI-branded. Quality scores, freshness states, summaries, digests, and classification are presented as platform features.

  • Source Information accordion: classification_confidence rendered 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.

The admin Provenance route is the only surface where AI mechanism data is exposed. Five tabs (components/provenance/):

TabFilePurpose
Per-itemper-item-tab.tsxUUID lookup → classification, processing, drafting, Review Schedule (S207 §5.5 Phase 3 T4)
Pipeline Healthpipeline-health-tab.tsxKeyset-paginated pipeline_runs with time-range + kind filters, 20K truncation guard
Auditaudit-tab.tsx1:1 lift of ActivitySection, hosts PDF export
Cost (stub)cost-tab-stub.tsxpipeline_runs.cost aggregate; “Interim — Wave B”
Disputes (stub)disputes-tab-stub.tsxclassification_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().

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.

Three AI-drafting routes changed from null to PIPELINE_SYSTEM_USER_ID. The Per-item tab remaps this UUID to display name “Knowledge Hub”.

/activity/provenance?tab=audit; /settings?section=activity/provenance?tab=audit. Command palette entry relabelled “Provenance › Audit”.


Layer 2 was rolled into Layer 1 + 3 during implementation; live commands are L1 + L3 + L4.

LayerCommandFileChecksCoverage
L1bun run test:mcp-evalscripts/mcp-eval/protocol-compliance.ts42JSON-RPC framing, tool counts, annotation invariants
L3bun run test:mcp-eval:rqscripts/mcp-eval/response-quality.ts17Token efficiency, structured-content shape, error hints
L4bun run test:mcp-eval:fcscripts/mcp-eval/functional-correctness.ts37Live 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:

FlagModeDescriptionCost
--cachedCachedCompare against existing entity_mentions in DBFree
--live --confirmLiveRe-run classification, then compare~$0.06/item
--live --validate --confirmLive + Pass 2Re-run with two-pass validation~$0.063/item

Additional flags: --verbose, --json, --item <uuid>, --save-baseline.

PropertyValue
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.

MetricFormulaMinimum Threshold
Precisiontrue positives / total extracted40%
Recalltrue positives / total expected35%
F1 Scoreharmonic mean of precision and recall35%
Type Accuracycorrectly typed / total matched80%
Exclusion Compliancecorrectly excluded / total excluded expected50%
Cross-Item Type Consistencyentities with consistent type / total unique entities80%

All metrics also have a max_drop threshold (5% for most, 10% for exclusion and consistency) for regression detection against saved baselines.

ModuleFilePurpose
metricslib/eval/metrics.tsPure metric functions (precision, recall, F1, ROUGE-L, ROUGE-1, MRR, nDCG@k, P@k)
typeslib/eval/types.tsShared types (EvalResult, EvalBaseline, RegressionResult, gold standard interfaces)
baselinelib/eval/baseline.tsBaseline storage and regression detection (JSON files in __tests__/fixtures/eval-baselines/)
reporterlib/eval/reporter.tsConsole 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:

CheckWhat it detectsSeverity (re-ingest)
embedding_coverageItems with NULL embeddingmust-pass
entity_mention_coverageItems with zero entity mentionsmust-pass
entity_rel_coverageItems with zero entity relationshipsshould-pass
classified_domains_not_emptyItems with NULL primary_domainmust-pass
classified_but_no_confidenceItems with classified_at but NULL confidenceshould-pass
summary_coverageItems with NULL summaryshould-pass
chunk_coverageItems with zero chunksmust-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.


SettingLocationDefaultPurpose
ANTHROPIC_API_KEY.env.localAnthropic API authentication
OPENAI_API_KEY.env.localOpenAI API authentication
AI_SUMMARY_MODEL.env.localclaude-sonnet-4-6Model for classification and summaries
AI_CLASSIFICATION_MODEL.env.localclaude-opus-4-6Python classification model
AI_EMBEDDING_MODEL.env.localtext-embedding-3-largeEmbedding model
AI_EMBEDDING_DIMS.env.local1024Embedding dimensions
AI_ANALYSIS_MODEL.env.localclaude-sonnet-4-5Bid drafting analysis tier
AI_DRAFTING_MODEL.env.localclaude-opus-4-6Bid drafting response tier
AI_QUALITY_MODEL.env.localclaude-haiku-4-5Bid drafting quality check tier
SENTRY_*.env.localSentry 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)”
ModelInput ($/M)Output ($/M)Cache Read ($/M)Cache Write ($/M)
claude-opus-4-615.0075.001.5018.75
claude-sonnet-4-53.0015.000.303.75
claude-sonnet-4-63.0015.000.303.75
claude-haiku-4-50.804.000.081.00

TablePurposeKey ColumnsRLS
entity_mentionsStores extracted entity mentions per itemcontent_item_id, entity_type, entity_name, canonical_name, confidence, context_snippetRole-based via get_user_role()
entity_relationshipsStores relationships between entitiessource_entity, relationship_type, target_entity, source_item_id, confidenceRole-based
entity_aliasesMaps variant entity names to canonical formalias, canonical, is_activeRole-based
content_itemsAI-enriched fields on the main content tableembedding, primary_domain, primary_subtopic, ai_keywords, summary, summary_data, classification_confidence, classified_at, ingest_source, next_review_date, review_cadence_daysRole-based
content_chunksHeading-level chunks with per-chunk embeddingscontent_item_id, heading_text, heading_path, content, embedding, positionRole-based
pipeline_runsAudit log of every AI/ingest invocationpipeline_name, status, items_processed, items_created, result, costAdmin (insert via service-role)
classification_disputesPer-item classification dispute recordscontent_item_id, disputed_by, dispute_reason, resolved_at, resolution_notesAdmin

content_items.content_text_hash is GENERATED ALWAYS — never set explicitly; PG auto-computes via md5(normalised content). Omit from payloads.

  • entity_mentions: (canonical_name, entity_type, content_item_id) — the upsert conflict target.
  • entity_relationships: (source_entity, relationship_type, target_entity, source_item_id) with NULLS NOT DISTINCT (S183 WP1 G1; migration 20260421171520_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.

Defined in lib/ai/classify.ts tool schema:

holds, complies_with, delivers_to, uses, demonstrated_by, requires, part_of, supersedes, references, evidences.


The Python pipeline (scripts/kb_pipeline/) mirrors the TypeScript classification pipeline for CLI ingestion.

Python ModuleFileTS Equivalent
classifyscripts/kb_pipeline/classify.pylib/ai/classify.ts
embedscripts/kb_pipeline/embed.pylib/ai/embed.ts
chunkscripts/kb_pipeline/chunk.pylib/content/chunking.ts
layer_inferencescripts/kb_pipeline/layer_inference.pylib/layer-inference.ts
temporal_bridgescripts/kb_pipeline/temporal_bridge.pylib/entities/entity-metadata-bridge.ts
summarisescripts/kb_pipeline/summarise.pylib/ai/summarise.ts
post_insertscripts/kb_pipeline/post_insert.py(no single TS equivalent — steps are inline in each route)
supersedescripts/kb_pipeline/supersede.pylib/supersession/set.ts

These values must match between TS and Python. Verified by __tests__/validation/pipeline-parity.test.ts:

ConstantValue
Classification truncation limit5,000 chars
Entity types12 types (same set)
Excluded entity patterns5 regex patterns (same set)
Temporal entity typescertification, framework, regulation
Layer inference rules7 rules, same priority order
Layer keys4 keys (same set)
Canonicalisation rules12 rules, same order
Proper noun allowlist19 entries (same set)
Abbreviations lookup39 entries (matching)
MAX_EMBEDDING_CHARS24,000
DivergencePythonTypeScriptReason
Default classification modelclaude-opus-4-6claude-sonnet-4-6Python CLI is batch; cost is acceptable
Embedding text compositiontitle + summary + contentsuggestedTitle + contentPython includes summary (Plan D D5)
Entity context extractionNot implementedImplementedGap (not yet planned)
Taxonomy validationWarn-onlyAuto-correctGap (not yet planned)
Content truncation ellipsisAppends "..."No ellipsisCosmetic

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 former import_bid_library.py reference.


Test counts live in docs/generated/codebase-stats.md. Key files:

Test FileCovers
__tests__/lib/ai-classify-entities.test.tsEntity exclusion filters, shouldExcludeEntity()
__tests__/lib/ai-classify-skill.test.tsClassification skill loading
__tests__/lib/ai-classify-subtopic-coercion.test.tscoerceSubtopic() edge cases
__tests__/lib/ai-classify-filter-canonicalise.test.tsPost-canonicalise filter chain
__tests__/lib/classify-two-pass.test.tsPass 2 validation flow
__tests__/lib/classify-entity-dedup.test.tsEntity mention row deduplication
__tests__/lib/entity-dedup.test.tscanonicalise() rules
__tests__/lib/entity-aliases.test.tsAlias resolution and caching
__tests__/lib/entity-context-snippet.test.tsContext snippet extraction
__tests__/lib/entity-metadata-bridge.test.tsTemporal-to-entity bridging
__tests__/lib/entity-validation.test.tsPass 2 entity validation
__tests__/lib/embed-max-chars.test.tsMAX_EMBEDDING_CHARS enforcement
__tests__/lib/ai-parse.test.tsTool result extraction from Claude responses
__tests__/lib/quality-score.test.tsCadence-compliance penalty (S208 §5.5 Phase 5)
__tests__/mcp/formatters-ai.test.tsAI tool response formatting
__tests__/mcp/create_content_item.test.tsTyped provenance + pipeline_runs audit
__tests__/mcp/search-chunks-tool.test.tsReview-cadence filter pass-through (S208 Phase 4)
__tests__/mcp/tool-annotations-coverage.test.tsdefineTool annotation invariant guard (S172)
__tests__/mcp/mcp-app-contracts.test.tsApp types ↔ formatter interface parity
__tests__/validation/mcp-fixture-sync.test.tsTool/prompt list parity vs registrations
Test FileCovers
__tests__/eval/entity-classification-eval.test.tsEntity eval suite integration
__tests__/eval/classification-eval.test.tsDomain classification eval
__tests__/eval/search-eval.test.tsSearch quality eval
__tests__/validation/eval-fixture-sync.test.tsFixture integrity guard
__tests__/validation/pipeline-parity.test.tsTS/Python constant drift detection
Test FileCovers
__tests__/integration/classification-entity-certification-flow.test.tsEnd-to-end certification entity flow
__tests__/integration/certification-bridge-flow.integration.test.tsTemporal bridge integration
__tests__/integration/golden-path-real-db.integration.test.tsFull pipeline against real DB
__tests__/integration/ingest-source-fan-out.integration.test.tsS207 typed-column fan-out + trigger-sole-authority

  • 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_snippet is only populated by the TypeScript pipeline. Python-ingested items have null context 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 find tool’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.

DecisionRationaleAlternative 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 rulesDeterministic = 100% precision, 0 cost, instant. Prompt rules for entity exclusion regress easily.All exclusion in the LLM prompt
ISO certification type overrideEliminates 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_mentionsClean-slate ensures re-classification reflects CURRENT output, not stale rows from prior runs.Upsert with ignoreDuplicates
MAX_EMBEDDING_CHARS = 24,000Safety 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 classificationPython runs batch CLI (cost acceptable); TS runs real-time web requests (latency matters).Same model for both
Entity mention row dedup in application codePostgres 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 invariantCompile-time enforcement that every tool declares all four ToolAnnotations advisory fields.Runtime check / honour-system convention
Fresh McpServer per requestmcp-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-failEditor-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-scoreItems with active review obligations should de-prioritise quality when the cadence is missed.Standalone penalty / no penalty
Admin-only /provenance routeConcentrates 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_itemsReplace 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 triggerEliminates app-level v1-history inserts; trigger is sole authority. Prevents drift between code paths.Each entry point writes its own v1 row