Skip to content

Knowledge Organisation — Technical Reference

Knowledge Organisation — Technical Reference

Section titled “Knowledge Organisation — Technical Reference”

Last verified: S514 (30/07/2026) — recorded the POST /api/entities/merge curation-pinning response (mentions_pinned / mentions_pin_error) and the persistence invariant the ingestion walk honours (id-400 / Inv-9). Prior: S223 (05/05/2026) — added Intelligence Ingestion sub-section

  • extended Validation row for S222 W3-A §2.3.4 native website scraping (FeedSourceCreateSchema async superRefine + parseBodyAsync helper + url-validation.ts HEAD pre-flight + RFC 7232 ETag/Last-Modified web parity + per-domain rate-limit). Prior: Session 210 (29 April 2026).

Knowledge organisation is the structural backbone of the knowledge base. It defines how content is classified, grouped, layered, tagged, and assessed for completeness. Six interconnected subsystems collaborate:

  1. Taxonomy — two-level structure (taxonomy_domainstaxonomy_subtopics), DB-driven via TaxonomyProvider context, admin- editable.
  2. Layers — content depth vocabulary (layer_vocabulary) defining audience slices: sales_brief, bid_detail, company_reference, research.
  3. Entities — extracted organisations, certifications, regulations, etc. stored on entity_mentions + entity_relationships.
  4. Tags — AI-generated keywords (content_items.ai_keywords) and user-applied tags (content_items.user_tags) with morphology canonicalisation and admin triage.
  5. Guides — completeness checklists (guides + guide_sections) defining expected content per domain or product.
  6. Coverage — gap analysis across taxonomy, templates, and guides; surfaced on the Coverage page and via MCP.

The taxonomy is DB-driven; the Python pipeline reads taxonomy from scripts/tests/fixtures/taxonomy_snapshot.json regenerated by bun run sync:taxonomy. App code consumes TaxonomyProvider via useTaxonomy(). lib/taxonomy/taxonomy.ts is now a 24-line re-export shim for content types and platforms only.

MethodRouteAuthRate LimitPurpose
GET/api/taxonomy/domainsAdmin onlyList all domains with subtopic counts
POST/api/taxonomy/domainsAdmin onlyCreate domain (auto display_order)
PATCH/api/taxonomy/domains/[id]Admin onlyUpdate domain (name, colour, key_signal, is_active, accepted_at)
MethodRouteAuthRate LimitPurpose
POST/api/taxonomy/subtopicsAdmin onlyCreate subtopic (validates domain exists)
PATCH/api/taxonomy/subtopics/[id]Admin onlyUpdate subtopic (name, description, display_order, is_active, accepted_at)
MethodRouteAuthRate LimitPurpose
POST/api/taxonomy/reorderAdmin onlyBatch update display_order for domains or subtopics

Subtopic reordering validates that all IDs belong to the specified domain_id before applying updates.

MethodRouteAuthRate LimitPurpose
GET/api/layersAdmin onlyList all layers (including inactive)
POST/api/layersAdmin onlyCreate layer (auto display_order)
PATCH/api/layers/[id]Admin onlyUpdate layer (label, description, display_order, is_active). Key is immutable.
DELETE/api/layers/[id]Admin onlyDelete layer (blocked if content items assigned)
PUT/api/layers/reorderAdmin onlyBatch update display_order for layers

Layer deletion is guarded: if any content_items reference the layer key, the route returns 409 with the count of affected items and advises deactivation instead.

MethodRouteAuthRate LimitPurpose
GET/api/tagsAll authed30/minList tag counts (legacy or filtered/paginated via RPC)
DELETE/api/tagsAdmin only10/minRemove a tag from all items
GET/api/tags/suggestAll authed60/minTag autocomplete by prefix
GET/api/tags/duplicatesAll authed20/minFind duplicate tag groups (case/plural variants)
GET/api/tags/by-domainAll authed20/minTags grouped by content primary_domain
POST/api/tags/renameAdmin only10/minRename a tag across all items (atomic via RPC)
POST/api/tags/mergeAdmin only10/minMerge source tag into target (atomic via RPC)
POST/api/tags/bulk-deleteAdmin only5/minRemove multiple tags from all items
POST/api/tags/bulk-mergeAdmin only5/minMerge multiple source tags into one target

GET /api/tags has two code paths: without query params it calls get_all_tag_counts (legacy); with ?type=ai&min_count=2&search=foo&limit=50&offset=0 it calls get_tag_counts_filtered for paginated/filtered results. Tag write schemas (TagRenameBodySchema, TagMergeBodySchema, TagBulkMergeBodySchema) call normaliseTag on the target value to apply morphology + proper-noun allowlisting at write time.

Tag Morphology Drift Flags (S196 §1.17 / S197 WP3)

Section titled “Tag Morphology Drift Flags (S196 §1.17 / S197 WP3)”
MethodRouteAuthPurpose
GET/api/admin/tag-morphology/flagsAdmin, EditorList flags filtered by decision (default pending); paginated
POST/api/admin/tag-morphology/flagsAdmin, EditorBulk insert flags from corpus regression eval (idempotent on UNIQUE)
PATCH/api/admin/tag-morphology/flags/[id]Admin, EditorDisposition a flag: accept, add_override, dismiss (records decided_by)

Backed by tag_morphology_drift_flags (migration 20260424222432). Backfill itself is performed separately by scripts/apply-tag-morphology-backfill.ts against accepted flag IDs — the API only records the disposition.

MethodRouteAuthRate LimitPurpose
GET/api/entitiesAdmin only30/minList entities with counts, variants, type conflicts (server-side via get_entity_list_aggregated RPC)
GET/api/entities/[canonical_name]Admin only30/minEntity detail: variants, content items, relationships, metadata
PATCH/api/entities/[canonical_name]/typeAdmin only20/minOverride entity type for all mentions (uses service client)
PATCH/api/entities/[canonical_name]/metadataEditor+Update entity metadata JSONB (shallow merge, expiry propagation)
POST/api/entities/mergeAdmin only10/minMerge entities into one canonical form (atomic via merge_entities RPC)
POST/api/entities/splitAdmin only10/minSplit entity by moving selected variants to new canonical name
GET/api/entities/co-occurrenceAll authedEntity co-occurrence pairs (via get_entity_co_occurrence RPC)

The metadata endpoint includes a reverse bridge: when expiry_date is updated on a certification, regulation, or standard entity, it propagates to linked content items’ expiry_date and sets lifecycle_type to date_bound (non-fatal — failures surface as warnings).

POST /api/entities/merge (id-400 / Inv-9 curation pinning) stamps metadata.curation_pinned = true on the surviving target rows after the merge_entities RPC commits and returns a mentions_pinned count in its response. A pin-step fault (read or per-row write) never 500s — the merge itself has already committed atomically — and is surfaced explicitly via an optional mentions_pin_error field (distinguishing “pin step failed” from an honest “0 mentions to pin”). The ingestion walk honours the pin at three sites — the Stage-5 write-back domain excludes pinned rows at SQL; a pinned cross-op key-holder wins the survivor rule unconditionally; the em-declare loop re-declares pinned rows verbatim — so an admin merge survives a later re-walk (including full_reprocess). See reference/cocoindex-pipeline.md for the pipeline-side mechanism.

MethodRouteAuthRate LimitPurpose
GET/api/guidesAll authedList guides (published only for non-admins; ?include=stats enriches with section counts via get_guide_coverage RPC)
POST/api/guidesEditor+20/minCreate guide
GET/api/guides/[slug]All authedGuide with content via get_guide_content RPC (sections grouped with content items)
PATCH/api/guides/[slug]Editor+30/minUpdate guide metadata
DELETE/api/guides/[slug]Admin onlyDelete guide
GET/api/guides/[slug]/sectionsAll authedList sections for a guide
POST/api/guides/[slug]/sectionsEditor+50/minCreate section within guide
PUT/api/guides/[slug]/sectionsEditor+20/minReorder sections (batch display_order update)
PATCH/api/guides/[slug]/sections/[sectionId]Editor+30/minUpdate section
DELETE/api/guides/[slug]/sections/[sectionId]Editor+Delete section

Section routes resolve the guide slug to a UUID before operating. The resolution distinguishes between “no row” (404) and “DB error” (500) to avoid misleading error messages during transient failures.

MethodRouteAuthRate LimitPurpose
GET/api/coverageAll authedCoverage matrix + summary (via get_coverage_matrix and get_coverage_summary RPCs)
GET/api/coverage/gapsAll authedUnified gaps from taxonomy, templates, and guides (60s in-memory cache)
GET/api/coverage/targetsAll authedFetch all coverage targets with domain names
PUT/api/coverage/targetsAdmin onlyUpsert coverage targets
GET/api/coverage/templatesAll authedCompute template coverage for a specific template
GET/api/coverage/templates/listAll authedList available templates
GET/api/coverage/guidesAll authed30/minGuide coverage data (section checklists with content counts and freshness)

/api/coverage/gaps aggregates three sources: taxonomy (empty subtopics), templates (unmet requirements), and guides (empty or fully stale required sections). Results are scored via scoreGap(), filterable by source, priority, or domain, and paginated. The 60-second in-memory cache reduces repeated computation but is per-process (not shared across Vercel instances).

Entity Management (components/entity-management/)

Section titled “Entity Management (components/entity-management/)”
ComponentFilePurpose
EntityListentity-list.tsxPaginated entity listing with filters; type-edit dialog
EntityDetailPanelentity-detail-panel.tsxEntity detail slide-out with variants, items, relationships, inline type-change Select (P1-22)
MergeModalmerge-modal.tsxMulti-entity merge dialog
SplitModalsplit-modal.tsxEntity variant split dialog

Eighteen components covering heatmap, gaps, targets, guide tab, and template coverage. Key surfaces:

ComponentPurpose
CoverageHeatmapViewDomain × subtopic heatmap visualisation
CoverageCell, CoverageGapCellIndividual heatmap cells (item count and gap-highlight variants)
CoverageDomainSectionCollapsible domain row in heatmap
CoverageSummaryCardsTop-level coverage statistics
CoverageTargetEditorAdmin UI for setting per-domain targets
CoverageTargetProgressProgress bar against admin-set targets
CoverageGuideTab, CoverageGuideCardGuide coverage tab content + per-guide cards
PriorityGapsTab, PriorityGapsFilters, PriorityGapsSummary, PriorityGapCardUnified gaps tab with source/priority/domain filters
TemplateCoverageContent, TemplateCoverageSection, TemplateCoverageRequirementTemplate coverage results display
TemplateUploadTemplate upload interface
CostEstimateDialogCoverage cost estimation dialog
ComponentPurpose
GuideProgressBarVisual progress indicator
GuideResearchFeedResearch feed within guide context
GuideSectionBannerSection header banner
GuideSectionEmptyEmpty section placeholder + CTA
GuideSectionSection content display
GuideTableOfContentsTable of contents navigation

Settings — Knowledge Organisation (components/settings/)

Section titled “Settings — Knowledge Organisation (components/settings/)”
ComponentPurpose
TaxonomySectionDomain/subtopic CRUD
TaxonomyDialogsAdd/edit dialogs for domains and subtopics
TaxonomyDriftBannerBanner warning when DB taxonomy and classification prompt are out of sync
DomainCardCollapsible domain card with subtopics
ContentOrganisationSectionWrapper for taxonomy + layers + tags
LayersSectionLayer CRUD and reorder
TagsSection2-tab container (S185 P1-17): Clean up + Browse all
TagsBrowseVirtual-scrolled tag list with per-tag CRUD
TagsCleanupDuplicates, domain view, bulk actions
TagBulkActionsBulk tag operations UI
TagDomainViewTags grouped by content domain
DuplicateReviewDuplicate tag group review and merge
TagMorphologySectionDrift flag triage queue (Pending / Accepted / Overrides / Dismissed / All) — admin/editor only (S197 §1.17)
EntitiesSectionEntity management entry — wraps entity-management/EntityList
GuidesSectionGuide management
GuideRow, GuideFormDialog, SectionFormDialogGuide and section CRUD UI
ContentOwnerManagementContent owner directory + service-account flag (S205 WP-A)
HookFileReturns / Notes
useTaxonomyAdminhooks/use-taxonomy-admin.tsDomain + subtopic CRUD via TanStack mutations; optimistic reorder; expand/collapse; deactivate/reactivate; accept/reject recommended; dialog state; SR announcements
useEntityDetailhooks/use-entity-detail.tsEntity detail fetcher; saveMetadata and changeType mutations (P1-22 inline type override with optimistic rollback); enabled flag
useCoverageTargetshooks/use-coverage-targets.tsTarget list fetcher + saveTargets upsert (returns { success, error? })
useLayerAdminhooks/use-layer-admin.tsLayer CRUD; deactivate/reactivate; optimistic reorder; dialog state; SR announcements
useTagsDatahooks/use-tags-data.tsTag counts, duplicates, domain groups; rename/merge/delete via mutationFetchJson
useTopicLayerContenthooks/use-topic-layer-content.tsSibling layer content for items sharing a topic_id; grouped by layer key

TaxonomyProvider (contexts/taxonomy-context.tsx)

Section titled “TaxonomyProvider (contexts/taxonomy-context.tsx)”

DB-driven taxonomy provider. Fetches active domains and subtopics client-side via TanStack Query (keys defined in lib/query/query-keys.ts). API:

  • domains — ordered list of active TaxonomyDomain rows
  • subtopics — ordered list of active TaxonomySubtopic rows
  • loading, error — async state
  • getDomainNames() — ordered array of domain name strings
  • getSubtopics(domainName) — subtopic names for a given domain
  • getDomainColourKey(domainName) — CSS colour key (--domain-{key}-*)
  • formatSubtopic(slug) — kebab-case → Title Case
  • formatDomainName(slug) — kebab-case → Title Case
  • refresh() — manual cache invalidation (called after admin mutations)

LayerVocabularyProvider (contexts/layer-vocabulary-context.tsx)

Section titled “LayerVocabularyProvider (contexts/layer-vocabulary-context.tsx)”

DB-driven layer vocabulary provider. Fetches active layers from Supabase. API:

  • layers — ordered list of LayerVocabulary rows
  • getLayerKeys() — ordered array of layer key strings
  • getLayerLabel(key) — human-readable label for a layer key
  • refresh() — manual cache invalidation

Falls back to FALLBACK_LAYERS from lib/client-config if the DB fetch fails.

Server-side layer validation (P1-36): API routes that accept a layer key (POST /api/guides/[slug]/sections, PATCH /api/guides/[slug]/sections/[sectionId], PATCH /api/items/[id]/metadata) call fetchActiveLayerKeys() from lib/validation/layer-schemas.ts to fetch live layer keys from the layer_vocabulary table. If the DB fetch fails or returns no active layers, these routes return 503 Service Unavailable rather than falling back to static keys.

Schema details and full column lists live in docs/reference/SCHEMA-QUICK-REFERENCE.md. Summary of knowledge-organisation tables:

TablePurposeKey Columns
taxonomy_domainsConfigurable domains (DB-driven taxonomy)id, name, display_name, display_order, colour, key_signal, provenance, accepted_at, is_active
taxonomy_subtopicsSubtopics within a domainid, domain_id FK, name, description, display_order, provenance, accepted_at, is_active
entity_mentionsEntities extracted by AI (vertex layer)id, content_item_id FK, entity_type, entity_name, canonical_name, entity_type_override, confidence, metadata JSONB, normalisation_version
entity_relationshipsEdges between entitiesid, source_entity, target_entity, relationship_type, source_item_id FK, confidence
entity_aliasesAlias resolution for classification (DB-driven)id, alias, canonical, category, is_active
layer_vocabularyDB-driven content layer definitionsid, key (UNIQUE), label, description, display_order, is_active
guidesContent completeness guidesid, name, slug (UNIQUE), description, guide_type, domain_filter, icon, color, display_order, is_published
guide_sectionsSections within a guideid, guide_id FK, section_name, description, content_type_filter, subtopic_filter, expected_layer, is_required, display_order
coverage_targetsPer-domain coverage thresholdsid, domain_id FK, metric_name, target_value
tag_morphology_drift_flagsTriage queue for normaliseTag disagreements (S196 §1.17)id, stored_tag, proposed_canonical, usage_count, affected_content_ids, decision (CHECK enum), decided_by, decided_at, decision_rationale

Entity types supported (12): organisation, certification, regulation, framework, capability, person, technology, project, sector, product, standard, methodology. Source of truth: docs/reference/entity-type-taxonomy-spec.md.

Relationship types (CHECK): holds, complies_with, delivers_to, uses, demonstrated_by, requires, part_of, supersedes, references, evidences.

Provenance values (CHECK): baseline (seeded), client (admin-created), recommended (AI-suggested).

Unique constraints:

  • entity_mentions(canonical_name, entity_type, content_item_id)
  • entity_relationships(source_entity, relationship_type, target_entity, source_item_id) with NULLS NOT DISTINCT (PG 17+)
  • taxonomy_subtopics(domain_id, name)
  • coverage_targets(domain_id, metric_name)
  • tag_morphology_drift_flags(stored_tag, proposed_canonical)
FilePurpose
taxonomy.ts24-line re-export shim — content types and platforms only (Python pipeline reads taxonomy from snapshot fixture)
taxonomy-format.tsformatSubtopic, formatDomainName, FALLBACK_COLOUR_MAP used by TaxonomyProvider
sync-trigger.tsHelpers for invoking the taxonomy sync chain
FilePurpose
entity-aliases.tsDB-driven alias resolution for classification (resolveAlias, loadAliases) with code fallback
entity-context.tsEntity context extraction utilities
entity-dedup.tsEntity deduplication and canonicalisation (canonicalise)
entity-metadata-bridge.tsBridge between entity metadata and content item fields (expiry propagation)
temporal-reconciliation.tsTemporal metadata reconciliation between entities and content
token-match.tsToken-based entity name matching
FilePurpose
coverage-heatmap.tsHeatmap data transformation and colour computation
gap-scoring.tsscoreGap algorithm. Assigns priority tiers (critical, high, medium, low) based on gap source, mandatory flags, targets
cost-estimation.tsCost estimation for coverage gap remediation
FilePurpose
template-coverage.tslistAvailableTemplates, fetchTemplateRequirements, fetchContentForMatching, computeTemplateCoverage, computeGapSummary. Shared between API routes and MCP tools.
template-auto-map.tsAutomatic mapping of content to template requirements
FilePurpose
layer-schemas.tsfetchActiveLayerKeys() fetches live keys from layer_vocabulary (P1-36). getLayerSchema() builds Zod enum from active keys. Falls back to FALLBACK_LAYERS for non-DB contexts.
schemas.tsnormaliseTag(tag) — tag canonicalisation with proper-noun allowlist + pluralize@8 morphology. ASCII-only whitespace regex for TS↔Python parity. toSingular short-word guard, whole-input override (TAG_PLURAL_LOOKING_SINGULARS), and compound last-token guard. FeedSourceCreateSchema (S222 W3-A §2.3.4) runs an async .superRefine invoking validateWebUrl for source_type='web'; consumers MUST use parseBodyAsync (lib/validation/index.ts:55+) — the synchronous parseBody throws “Encountered Promise during synchronous parse”.
index.tsparseBody<T> (sync) + parseBodyAsync<T> (async — for schemas with async .superRefine). Both return a typed Result. ESLint guard validation-sweep-safeparse-ban blocks inline .safeParse() in route files.

Intelligence Ingestion (lib/intelligence/, S222 W3-A §2.3.4)

Section titled “Intelligence Ingestion (lib/intelligence/, S222 W3-A §2.3.4)”
FilePurpose
url-validation.tsvalidateWebUrl(url) — HTTP HEAD pre-flight per RFC 7232 Option A. Rejects 4xx/5xx, captures redirect chain length, surfaces Content-Type mismatch. Used by FeedSourceCreateSchema.superRefine at insert time so a malformed source_type='web' row is rejected at the API boundary, not after a wasted polling cycle.
pipeline.tsPolling loop. Post-S222 W3-A: source_type='web' rows participate in the same conditional-request flow as RSS — pipeline writes etag + last_modified headers from each successful fetch and forwards them on subsequent requests for If-None-Match + If-Modified-Since (304 short-circuit). Per-domain rate-limit gate enforces N concurrent fetches per domain to respect crawl etiquette.

API consumers writing feed_sources:

  • POST /api/intelligence/workspaces/[id]/sources — admin/editor add a new source. Schema: FeedSourceCreateSchema. Web pre-flight runs at this call site.
  • PATCH /api/intelligence/workspaces/[id]/sources/[sourceId] — partial update via FeedSourceUpdateSchema (no async refinement — admins can toggle is_active without re-validating the URL).
  • POST /api/intelligence/workspaces/[id]/seed-starter-pack — seed a curated set of starter sources for a new workspace.

The downstream Sentry breadcrumb stream tags Firecrawl-credit usage per source so cost-monitoring dashboards can attribute spend per workspace + domain.

FilePurpose
unified-gap.tsTypeScript types for the unified gap system: UnifiedGap, TaxonomyGap, TemplateGap, GuideGap, PriorityTier, UnifiedGapSummary
taxonomy.tsShared taxonomy types: TaxonomyDomain, TaxonomySubtopic, TaxonomyProvenance

The classification pipeline holds the entity-quality logic. Notable components:

ComponentPurpose
EXCLUDED_PATTERNSRegex set for identifier patterns (UUIDs, emails, ticket IDs)
INTERNAL_DOCUMENT_SUFFIXESSuffix regex for policies/procedures/plans (excluded as entities)
GENERIC_CONCEPTS~70-name set of abstract concepts (e.g. information security, data protection, risk management) — never extracted as entities
_ISO_CERTIFICATION_OVERRIDESix-name set forcing iso 9001, iso 14001, iso 22301, iso 27001, iso 45001, iso 50001 to entity_type='certification' deterministically (S158A Iter 4)
deriveHolderMetadataTwo-pass holder derivation. Pass 1: canonical holds rels. Pass 2 (S196): synonym fallback accepting complies_with/evidences only when target is a certification, source is the client org or extracted org, and no canonical holds exists for that target. Mutates rows in place setting metadata.holder to 'self' or {holder: 'supplier', supplier_name}.
validateEntitiesPass 2 validation (Haiku, temperature: 0) with validate=true. Surgical bulk-cert rule with enumerated co-occurrence sets.

Python parity: scripts/kb_pipeline/classify.py::derive_holder_metadata mirrors the TS helper. _CLIENT_ORG_LOWER matches BRANDING.organisationName.toLowerCase(). TS↔Python normaliseTag parity is enforced by the cross-language fixture __tests__/fixtures/keyword-normalisation-cases.json (74 cases).

ToolTypeFilePurpose
list_guidesReadtools/guides.tsList guides with optional filters (type, domain, published_only)
get_guideReadtools/guides.tsFetch full guide content with sections and linked content
create_guideWritetools/guides.tsCreate guide + optional sections (validates layer keys)
update_guideWritetools/guides.tsUpdate guide metadata; optional sections array replaces all sections
get_entity_relationshipsReadtools/entities.tsQuery entity relationships and graph data
where_are_we_exposedReadtools/dashboard.tsFive-layer exposure report — absorbed the former get_certification_status (certification layer) and get_coverage_gaps (coverage-gap layer)
list_templatesReadtools/templates.tsList available templates
get_template_coverageReadtools/templates.tsCompute template coverage for a specific template
get_template_gapsReadtools/templates.tsFind unmet template requirements
show_coverage_matrixApptools/apps.tsRender interactive Coverage Matrix MCP App
suggest_content_creationReadtools/quality.tsGap-based content creation suggestions

Authoritative tool list: docs/generated/mcp-inventory.md (regenerate with bun run generate:mcp-inventory).

URITypePurpose
kb://coverageStaticCurrent taxonomy coverage state (domains + item counts)
kb://taxonomyStaticFull taxonomy of domains and subtopics
kb://entitiesStaticEntity overview: types, counts, top entities by mention count
ui://coverage-matrix/app.htmlAppInteractive Coverage Matrix MCP App HTML
PromptPurpose
coverage_analysisAnalyse coverage gaps and suggest content to create. Orchestrates where_are_we_exposed (the five-layer exposure tool that absorbed the former coverage-gap / quality / freshness reads) + suggest_content_creation.
SkillPurpose
knowledge-hub:classificationDomain taxonomy guidance — two-level structure, content type classification, confidence interpretation, when to trigger reclassification
knowledge-hub:guide-builder8-step conversational workflow for creating or updating guides — intent detection, metadata, section design, source validation, publish decision

Server-side RPC functions used by the knowledge-organisation routes (full list in docs/reference/SCHEMA-QUICK-REFERENCE.md §RPC):

FunctionPurpose
get_entity_list_aggregatedServer-side entity aggregation with filtering and pagination
get_entity_co_occurrenceEntity co-occurrence pair computation
get_entity_summaryEntity summary data for MCP tools
get_entity_relationships_rpcEntity relationship details
merge_entitiesAtomic entity merge (mentions + relationships + dedup)
get_coverage_matrixDomain × subtopic item count matrix
get_coverage_summaryCoverage summary statistics
get_guide_coverageGuide section content counts and freshness
get_guide_contentGuide content with sections and content items
get_topic_layersContent layers for a topic, ordered by layer_vocabulary
get_all_tag_countsAll tag counts (legacy)
get_tag_counts_filteredFiltered/paginated tag counts
delete_tag, rename_tag, merge_tagsAtomic tag mutations
bulk_delete_tags, bulk_merge_tagsBatch tag mutations
suggest_tagsTag autocomplete by prefix
find_duplicate_tagsFind duplicate tag groups (case + plural variants)
get_tags_by_domainTags grouped by content primary_domain
RoutePurposeAccess
/coverageCoverage matrix, gaps, guides, templates tabsEditor+ only (S189 P1-11 — viewers redirected to /browse)
/guide308 permanent redirect to /coverage?tab=guides (S188 P1-28)Editor+ via the coverage gate
/guide/[slug]Individual guide viewAll authed
/settingsTaxonomy, layers, tags, tag morphology, entities, guides sectionsAdmin (taxonomy/layers/tags/entities/guides), Admin+Editor (tag morphology)
SettingLocationPurpose
content_layers flaglib/client-config.ts CLIENT_FEATURESFeature gate for layer UI surfaces (LayerSwitcherNav, TopicLayerComparison, LayerSuggestionBanner). Enabled by default.
BRANDING.organisationNamelib/client-config.tsClient organisation name (camelCase). Used by deriveHolderMetadata for self/supplier holder attribution.
Layer keyslayer_vocabulary table + FALLBACK_LAYERS constActive layer keys at write sites; DB is single source of truth

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

Test FileCovers
__tests__/api/taxonomy/**/*.test.tsDomain/subtopic CRUD, reorder, validation
__tests__/api/entities/**/*.test.tsEntity list, detail, type override, metadata, merge, split, co-occurrence
__tests__/api/guides/**/*.test.tsGuide and section CRUD, slug resolution
__tests__/api/coverage/**/*.test.tsCoverage matrix, gaps, targets, templates, guides
__tests__/api/tags/**/*.test.tsTag CRUD, autocomplete, duplicates, by-domain, bulk operations
__tests__/api/admin/tag-morphology/**/*.test.tsDrift flag list/insert/disposition
__tests__/api/layers/**/*.test.tsLayer CRUD, deletion guard, reorder
__tests__/lib/coverage/**/*.test.tsGap scoring, heatmap colour computation
__tests__/lib/templates/template-coverage.test.tsTemplate coverage computation
__tests__/lib/validation/schemas.test.tsnormaliseTag, proper-noun allowlist, plural-looking singulars, compound-tag last-token guard
__tests__/fixtures/keyword-normalisation-cases.jsonCross-language fixture (74 cases) shared with Python scripts/tests/test_classify_normalise.py
__tests__/lib/ai/classify-derive-holder-metadata.test.tsTS holder derivation including synonym fallback
scripts/tests/test_classify_holder_rule.pyPython holder derivation
__tests__/contexts/taxonomy-context.test.tsxTaxonomyProvider behaviour and helpers
__tests__/contexts/layer-vocabulary-context.test.tsxLayerVocabularyProvider behaviour and fallback
  1. Taxonomy dual-source — App uses DB-driven taxonomy (contexts/taxonomy-context.tsx); Python pipeline reads scripts/tests/fixtures/taxonomy_snapshot.json. After taxonomy changes, bun run sync:taxonomy must be run to regenerate the classification prompt and plugin files. The DB is single source of truth but the chain is manual.
  2. Entity metadata is mention-level — The metadata JSONB update targets the first entity_mentions row for a canonical name, not a dedicated entity table. Entity-level properties (e.g. certification expiry) are stored on a mention row.
  3. No entity CRUD for manual creation — Entities are only created via AI classification during content ingestion.
  4. Coverage gaps cache is per-process — The 60-second in-memory cache in /api/coverage/gaps is not shared across Vercel instances.
  5. Layer key is immutable — The PATCH route for layers does not allow changing the key field, as this would break existing content item assignments.
  6. Guide section matching relies on subtopic_filter — Seeded sections often have subtopic_filter = NULL, which produces broad matches in get_guide_coverage.
  7. Tag operations are atomic via RPCs — Rename, merge, and delete use database functions for atomicity, but there is no undo mechanism.
  8. Tag morphology backfill is manual — Triage records the disposition; the actual UPDATE on content_items.ai_keywords runs out-of-band via scripts/apply-tag-morphology-backfill.ts against accepted flag IDs.
  9. Holder synonym fallback is conservativecomplies_with and evidences only count when the target is a certification entity AND the source is the client org or an extracted organisation in the same batch. This avoids garbage rels but misses some valid cert claims phrased indirectly.
DecisionRationaleAlternative Considered
DB-driven taxonomyAllows admin self-service without code deployments; AI can recommend new domains/subtopicsStatic code constants
Entity graph as entity_mentions + entity_relationshipsFlexible schema for varying entity types; JSONB metadata for type-specific propertiesSeparate table per entity type
Unified gap view across three sourcesSingle prioritised view reduces context switching; consistent scoring across taxonomy, template, and guide gapsSeparate gap views per source
Atomic entity merge via RPCSingle transaction prevents orphaned mentions/relationships during mergeMulti-step API calls
Provenance tracking on taxonomy itemsDistinguishes baseline (seeded), client (manual), and recommended (AI-suggested) itemsSimple active/inactive flag
Guide sections with filtersFlexible matching rules per section (content type, subtopic, layer) without rigid schemaFixed section templates
Tag RPCs for bulk operationsDatabase-level atomicity for tag rename/merge/delete across all content itemsApplication-level loops
Tag morphology library + carve-outspluralize@8 (TS) / inflect==7.5.0 (Python) cover 500+ irregular forms; explicit allowlists handle proper nouns + 11 -ics fields-of-study + Latin/Greek singularsHand-rolled suffix rules
Triage queue for morphology drift, not auto-applyHuman disposition required because backfill mutates production tags. Eval surfaces flags; admin/editor decide accept/override/dismiss; backfill runs separately.Auto-apply with rollback ability
ISO family deterministic overrideUK SMB usage is overwhelmingly certification-context; Pass 2 sometimes retypes to standard. Override at storage boundary eliminates cross-item flip-flop.Prompt-only guidance
Holder synonym fallback gated by entity-type contextAccepts complies_with/evidences only when target is a certification AND source is a known organisation. Prevents garbage rels like cert-complies-with-cert.Accept all synonyms, or accept none
expected_layer validated at app level, not DB CHECKLayer keys are admin-editable in DB. CHECK constraint would block dynamic vocabulary. App-level fetch returns 503 if vocabulary unavailable.DB CHECK enum