Knowledge Organisation — Workflows
Knowledge Organisation — Workflows
Section titled “Knowledge Organisation — Workflows”Last verified: Session 210 (29 April 2026) Pending updates: None
Overview
Section titled “Overview”Knowledge organisation workflows cover the data flows for taxonomy management, entity lifecycle, layer vocabulary, tag governance, tag morphology triage, guide configuration, and coverage gap analysis. Key automated processes include entity extraction during content classification, coverage matrix computation, unified gap aggregation, entity metadata propagation, and the four-step taxonomy sync chain.
Workflow 1: Taxonomy Domain + Subtopic CRUD
Section titled “Workflow 1: Taxonomy Domain + Subtopic CRUD”Trigger: Admin submits POST / PATCH to /api/taxonomy/domains or
/api/taxonomy/subtopics Owner: app/api/taxonomy/domains/*/route.ts +
app/api/taxonomy/subtopics/*/route.ts
Flow — Domain Create
Section titled “Flow — Domain Create”Validate (TaxonomyDomainCreateSchema) → Auto-assign display_order if not provided → Insert taxonomy_domains (provenance='client', is_active=true) → Return 201Flow — Subtopic Create
Section titled “Flow — Subtopic Create”Validate (TaxonomySubtopicCreateSchema) → Verify domain_id exists → Auto-assign display_order → Insert taxonomy_subtopics → Return 201Common Patterns
Section titled “Common Patterns”- Auto-assign
display_order— query max existing + 10 when omitted - Provenance — created rows always get
provenance='client'; the AI pipeline writesprovenance='recommended'directly, never via this route - Unique constraints — domains by
name, subtopics by(domain_id, name); both return 409 on duplicate - Update path —
PATCHonly updates the changed fields (name, colour, description, display_order, is_active, key_signal, accepted_at) - Optional
descriptionfield on subtopics added in P1-37 (S185)
Error Handling
Section titled “Error Handling”| Error Condition | Handling | User Feedback |
|---|---|---|
| Validation failure | 400 returned | Field-level errors |
| Domain not found (subtopic create) | 400 returned | ”Domain not found” |
| Duplicate name | 409 returned | ”already exists” message |
| DB error | 500 returned | Generic error message |
Workflow 2: Taxonomy Reorder
Section titled “Workflow 2: Taxonomy Reorder”Trigger: Admin submits POST /api/taxonomy/reorder Owner:
app/api/taxonomy/reorder/route.ts
Validate (TaxonomyReorderSchema) → Validate subtopic ownership (if type=subtopic) → Update display_order for each item → Return success countDetailed Steps
Section titled “Detailed Steps”- Validate input — Fields:
type(domainorsubtopic),domain_id(for subtopics),items(array of{id, display_order}). - Validate subtopic ownership — For subtopic reordering, verify all IDs belong to the specified domain. Returns 400 if any subtopic belongs to a different domain.
- Apply updates — Update each item’s
display_orderindividually; return count of successfully updated items.
Optimistic Update Pattern
Section titled “Optimistic Update Pattern”useTaxonomyAdmin implements optimistic updates for reordering:
- Cancel any in-flight queries for the affected data
- Snapshot current data as rollback target
- Apply new
display_ordervalues to the cache - Sort cached data by
display_order - On success: invalidate queries to refetch
- On error: restore snapshot, show error toast
Workflow 3: Entity Merge
Section titled “Workflow 3: Entity Merge”Trigger: Admin submits POST /api/entities/merge Owner:
app/api/entities/merge/route.ts
Validate (EntityMergeBodySchema) → Call merge_entities RPC (atomic transaction) → Return merge resultsDetailed Steps — RPC merge_entities
Section titled “Detailed Steps — RPC merge_entities”The merge is atomic via the merge_entities database function:
- Update canonical_name on all
entity_mentionsrows for source entities to the target name - Set
entity_type_overrideto the chosen entity type - Update
entity_relationships— bothsource_entityandtarget_entityreferences updated from source names to target name - Remove duplicate mention rows where
(canonical_name, entity_type, content_item_id)collides after the merge
Error Handling
Section titled “Error Handling”| Error Condition | Handling | User Feedback |
|---|---|---|
| Validation failure | 400 returned | Field-level errors |
| RPC failure | 500 returned | Merge error message |
| Rate limit | 429 returned (10/min) | Rate limit response |
Workflow 4: Entity Split
Section titled “Workflow 4: Entity Split”Trigger: Admin submits POST /api/entities/split Owner:
app/api/entities/split/route.ts
Validate (EntitySplitBodySchema) → Validate names differ → Update entity_mentions canonical_name for selected variants → Check if all mentions moved → Conditionally update entity_relationships → Return split resultsDetailed Steps
Section titled “Detailed Steps”- Validate input — Fields:
canonical_name,variant_names(array),new_canonical_name. Rejects identical old/new canonical names. - Move selected variant mentions — Update
canonical_nameonentity_mentionsrows matching both the old canonical name AND the specified variant names. Uses service client to bypass RLS. - Check remaining mentions — Query whether any mentions still have the old canonical name.
- Update relationships — Only when ALL mentions were moved. Updates
source_entityandtarget_entityonentity_relationshipsfrom old to new canonical name.
State Transitions
Section titled “State Transitions”| Condition | Relationship Update | Rationale |
|---|---|---|
| Some mentions remain | No update | Old entity still exists |
| All mentions moved | Update references | Old entity has been fully renamed |
Workflow 5: Entity Type Override
Section titled “Workflow 5: Entity Type Override”Trigger: Admin submits PATCH /api/entities/[canonical_name]/type —
either from the entity list type-edit dialog or inline via the entity detail
panel type-change <Select> (P1-22). Owner:
app/api/entities/[canonical_name]/type/route.ts
Validate (EntityTypeOverrideBodySchema) → Update entity_type_override on all mentions → Return count of updated mentionsBehaviour
Section titled “Behaviour”- Sets
entity_type_overrideon allentity_mentionsrows matching the canonical name - Override takes precedence via
COALESCE(entity_type_override, entity_type)in all queries - Uses service client for atomic update
- Returns 404 if no mentions found
UI Paths
Section titled “UI Paths”| Path | Component | Hook |
|---|---|---|
| Entity list type dialog | EntityList | Local useMutation |
| Entity detail inline (P1-22) | EntityDetailPanel | useEntityDetail.changeType |
Both call the same API endpoint. The detail panel path uses optimistic update with rollback; the list path refreshes the list on success.
Workflow 6: Entity Metadata Update with Reverse Bridge
Section titled “Workflow 6: Entity Metadata Update with Reverse Bridge”Trigger: Editor or admin submits
PATCH /api/entities/[canonical_name]/metadata Owner:
app/api/entities/[canonical_name]/metadata/route.ts
Validate (EntityMetadataUpdateSchema) → Find first mention → Shallow merge metadata → Update mention → Reverse bridge: propagate expiry_date to content items → Return updated metadata + warningsDetailed Steps
Section titled “Detailed Steps”- Find mention — Query
entity_mentionsfor the canonical name, limit 1. 404 if not found. - Merge metadata — Shallow merge: new keys override existing, existing keys preserved.
- Reverse bridge propagation (conditional)
- Only when
expiry_dateis present in the merged metadata - Only for entity types:
certification,regulation,standard - Finds all content items linked via
entity_mentions.content_item_id - Updates
expiry_dateand setslifecycle_typetodate_boundon those content items - Non-fatal: failures added to warnings array, not blocking
- Only when
Error Handling
Section titled “Error Handling”| Error Condition | Handling | User Feedback |
|---|---|---|
| Entity not found | 404 returned | ”Entity not found” |
| Metadata update fails | 500 returned | Error message |
| Reverse bridge fails | Warning added, metadata still saved | Warning in response |
Workflow 7: Holder Metadata Derivation (Classification Pipeline)
Section titled “Workflow 7: Holder Metadata Derivation (Classification Pipeline)”Trigger: Content classification — runs at the end of every successful
classification with validate=true or in batch reclassification. Owner:
lib/ai/classify.ts::deriveHolderMetadata (TS) +
scripts/kb_pipeline/classify.py::derive_holder_metadata (Python).
Flow — Two-pass derivation (S196 fix)
Section titled “Flow — Two-pass derivation (S196 fix)”Pass 1: scan relationships for `holds` → build holdsRelsByTarget mapPass 2 (synonym fallback): scan relationships for `complies_with`/`evidences` → only accept when target is certification AND source is client org or extracted org AND no canonical `holds` exists for that targetFor each certification mention: if holdsRelsByTarget has a source for canonical_name → set metadata.holder = 'self' (if source == client org lower) or {holder: 'supplier', supplier_name: source}Why two passes?
Section titled “Why two passes?”The classifier sometimes emits complies_with or evidences when the
content phrases a certification differently (e.g. “our ISO 27001 compliance”,
“evidenced by our DBS check”). Both are valid relationship-type enum members,
but the holder rule was originally only acting on holds. The S196 fix adds
a synonym fallback that is conservative:
- (a) target must be a certification entity (preserves semantic meaning of
complies_with/evidencesin non-cert contexts — e.g. “our org complies_with GDPR” where GDPR is a regulation) - (b) source must be the client organisation OR an extracted organisation entity in this batch (prevents garbage rels like “ISO 27001 complies_with Cyber Essentials Plus” from being mis-derived as cert-held-by-cert)
- (c) no canonical
holdsrel already exists for that target (holds wins over synonyms on tie)
Output
Section titled “Output”Each certification row that matched a holds (or eligible synonym) writes
metadata JSONB:
| Source matches client org? | metadata payload |
|---|---|
| Yes | { "holder": "self" } |
| No | { "holder": "supplier", "supplier_name": <source> } |
Consumers:
app/api/certifications/route.tsfilters certs byholder == 'self'AND source matchesBRANDING.organisationName.toLowerCase()- The MCP
where_are_we_exposedtool (certification layer — formerly the standaloneget_certification_statustool) applies the same filter
Cross-language parity:
- TS test:
__tests__/lib/ai/classify-derive-holder-metadata.test.ts - Python test:
scripts/tests/test_classify_holder_rule.py - Eval:
scripts/kb_pipeline/eval_holder_rule.py
Workflow 8: ISO Family Type Override (Classification Pipeline)
Section titled “Workflow 8: ISO Family Type Override (Classification Pipeline)”Trigger: Storage step in classification — runs after canonicalise + alias
- filter, before upsert. Owner:
lib/ai/classify.tsStep 15b (and Python parity inscripts/kb_pipeline/classify.py).
For each filtered entity row → If canonical_name (lowercased) ∈ {iso 9001, iso 14001, iso 22301, iso 27001, iso 45001, iso 50001} AND entity_type != 'certification' → Force entity_type = 'certification' (log override)Rationale
Section titled “Rationale”UK SMB form-response libraries overwhelmingly mention these six ISO families in
certification-context (i.e. “we hold ISO 27001”), but classifier Pass 2
sometimes retypes them to standard when the document discusses the
published document. The override eliminates cross-item type flip-flop that
broke entity dedup. CREST is deliberately NOT in the set (genuinely
ambiguous between professional body and credential).
Source-of-truth for entity types: docs/reference/entity-type-taxonomy-spec.md.
Workflow 9: Tag Morphology Drift Flag Disposition
Section titled “Workflow 9: Tag Morphology Drift Flag Disposition”Trigger: Admin or editor submits
PATCH /api/admin/tag-morphology/flags/[id] (S196 §1.17) Owner:
app/api/admin/tag-morphology/flags/[id]/route.ts
Validate (TagMorphologyFlagDecisionSchema) → Update tag_morphology_drift_flags row with decision + decided_by + decided_at + rationale → Return updated rowDisposition options
Section titled “Disposition options”| Decision | Effect | Follow-up |
|---|---|---|
accept | Library output is correct; tag should be backfilled to proposed_canonical | Admin runs scripts/apply-tag-morphology-backfill.ts against accepted IDs |
add_override | Stored form should be preserved; library should NOT apply this transformation | Add to TAG_PLURAL_LOOKING_SINGULARS or TAG_PROPER_NOUN_ALLOWLIST |
dismiss | Library agrees, or noise — no action | None |
Why triage, not auto-apply?
Section titled “Why triage, not auto-apply?”Backfill mutates production tags across content_items.ai_keywords. Liam’s
spec decision (recorded in docs/specs/p1-tag-morphology-library-adoption-spec.md
§3.5.3) is that humans must accept each disagreement before backfill runs.
This avoids regressing existing curated tags when the library’s morphology
output diverges from established usage.
Database Operations
Section titled “Database Operations”| Operation | Table | Auth |
|---|---|---|
| UPDATE | tag_morphology_drift_flags | Admin, Editor |
Eval feed
Section titled “Eval feed”Drift flags are populated by scripts/eval-tag-morphology-adoption.ts. The
eval iterates content_items.ai_keywords, applies normaliseTag() to every
stored value, and inserts (or updates the usage_count of) a flag for each
disagreement via POST /api/admin/tag-morphology/flags. UNIQUE on
(stored_tag, proposed_canonical) makes the bulk-insert idempotent.
Workflow 10: Tag Mutations (Rename / Merge / Delete / Bulk)
Section titled “Workflow 10: Tag Mutations (Rename / Merge / Delete / Bulk)”Trigger: Admin submits to one of the /api/tags/* write endpoints
Owner: app/api/tags/*/route.ts
Atomicity
Section titled “Atomicity”All tag mutations route through DB RPCs to keep the operation atomic across
all rows in content_items.ai_keywords / user_tags:
| API Route | RPC |
|---|---|
DELETE /api/tags | delete_tag |
POST /api/tags/rename | rename_tag |
POST /api/tags/merge | merge_tags |
POST /api/tags/bulk-delete | bulk_delete_tags |
POST /api/tags/bulk-merge | bulk_merge_tags |
Write-time normalisation
Section titled “Write-time normalisation”Targets in TagRenameBodySchema, TagMergeBodySchema, and
TagBulkMergeBodySchema pass through normaliseTag before reaching the RPC.
This applies the proper-noun allowlist (ISO 27001, GDPR, etc.) and
plural canonicalisation (pluralize@8) at the write boundary, so the stored
form is canonical regardless of admin input casing.
Workflow 11: Unified Gap Computation
Section titled “Workflow 11: Unified Gap Computation”Access note: The
/coveragepage is editor+admin only since S189 P1-11 (viewers redirected to/browse). API routes remain auth-only without defence-in-depth role checks.
Trigger: GET /api/coverage/gaps Owner:
app/api/coverage/gaps/route.ts
Check cache → Fetch coverage matrix + targets + guide coverage + templates in parallel → Build taxonomy gaps → Build template gaps → Build guide gaps → Score all gaps → Filter/paginate → Cache result → ReturnDetailed Steps
Section titled “Detailed Steps”- Cache check — 60-second in-memory cache keyed by sorted query parameters. Returns cached result if still valid.
- Parallel data fetch
get_coverage_matrix(taxonomy item counts)coverage_targets(admin-set thresholds)get_guide_coverage(guide section status)listAvailableTemplates(template list from DB)
- Build taxonomy gaps — Empty subtopics (
item_count = 0) from coverage matrix. Scored higher if domain has zero items overall, or if domain has unmet coverage targets. - Build template gaps — For each template, fetches requirements and
content items. Computes coverage via
computeTemplateCoverage(). Extracts requirements withcoverage_status: 'gap'. Mandatory requirements scored higher. - Build guide gaps — Empty sections (
content_count = 0) and stale sections (stale_count > 0, fresh_count = 0) from guide coverage. Required sections scored higher than optional. - Score and aggregate — All gaps via
scoreGap()fromlib/coverage/gap-scoring.ts. Assigned priority tier:critical,high,medium,low. Filtered by source/priority/domain. Sorted bypriority_scoredesc. Paginated.
Gap Priority Scoring
Section titled “Gap Priority Scoring”| Source | Factor | Effect on Score |
|---|---|---|
| Taxonomy | Domain has zero items | Significant boost |
| Taxonomy | Domain has unmet coverage target | Moderate boost |
| Template | Requirement is mandatory | Moderate boost |
| Guide | Section is required | Moderate boost |
| Guide | Section is stale (not empty) | Lower than empty sections |
Workflow 12: Coverage Matrix Computation
Section titled “Workflow 12: Coverage Matrix Computation”Trigger: GET /api/coverage Owner: app/api/coverage/route.ts
Fetch coverage matrix + summary in parallel → Return combined resultRPC calls
Section titled “RPC calls”| RPC | Purpose |
|---|---|
get_coverage_matrix | Domain × subtopic item count grid |
get_coverage_summary | Aggregated coverage statistics |
The p_layer parameter optionally filters the matrix to a specific content
layer.
Workflow 13: Guide Coverage Computation
Section titled “Workflow 13: Guide Coverage Computation”Trigger: GET /api/coverage/guides Owner:
app/api/coverage/guides/route.ts
Call get_guide_coverage RPC → Group rows by guide → Compute per-guide section stats → Build summary → ReturnSection Status Derivation
Section titled “Section Status Derivation”| Content Count | Fresh Count | Stale Count | Derived Status |
|---|---|---|---|
| 0 | — | — | empty |
| > 0 | > 0 | — | populated |
| > 0 | 0 | > 0 | stale |
| > 0 | 0 | 0 | populated |
Guide-Level Aggregation
Section titled “Guide-Level Aggregation”| Metric | Computation |
|---|---|
total_sections | Count of all sections |
populated_sections | Sections with non-empty status |
required_sections | Sections where is_required = true |
populated_required | Required sections that are populated |
fresh_sections | Sections with fresh_count > 0 |
stale_sections | Sections with stale_count > 0 and fresh_count = 0 |
Summary
Section titled “Summary”| Metric | Computation |
|---|---|
fully_populated | Guides where populated_sections = total_sections |
empty | Guides where populated_sections = 0 |
partially_populated | All others |
Workflow 14: Template Coverage Computation
Section titled “Workflow 14: Template Coverage Computation”Trigger: GET /api/coverage/templates Owner:
app/api/coverage/templates/route.ts
Validate template_name param → Fetch requirements + content items in parallel → Compute coverage → Return sections with requirements and coverage statusKey Functions (all in lib/templates/template-coverage.ts)
Section titled “Key Functions (all in lib/templates/template-coverage.ts)”| Function | Purpose |
|---|---|
listAvailableTemplates | Query template_requirements for distinct templates |
fetchTemplateRequirements | Get all requirements for a template (with version) |
fetchContentForMatching | Get content items with fields needed for matching |
computeTemplateCoverage | Match requirements against content, produce coverage report |
computeGapSummary | Aggregate gap counts across multiple templates |
Coverage Status Values
Section titled “Coverage Status Values”| Status | Meaning |
|---|---|
covered | Requirement fully met by one or more content items |
partial | Requirement partially met (some but not all criteria) |
gap | No matching content found for this requirement |
Workflow 15: Tag Autocomplete
Section titled “Workflow 15: Tag Autocomplete”Trigger: User types in tag input field Owner:
app/api/tags/suggest/route.ts
Validate prefix + type params → Call suggest_tags RPC → Return up to 10 matching tags ordered by frequencyRate limited at 60/min per user to handle rapid typing.
Workflow 16: Layer Lifecycle
Section titled “Workflow 16: Layer Lifecycle”Trigger: Admin CRUD operations on layers Owner: app/api/layers/
routes
Create Flow
Section titled “Create Flow”Validate (LayerCreateSchema) → Auto-assign display_order → Insert into layer_vocabulary → Return 201Delete Guard Flow
Section titled “Delete Guard Flow”Look up layer key → Count content_items with this layer → If count > 0: return 409 with count → If count = 0: delete layer → Return 204Key Constraint
Section titled “Key Constraint”The key field is immutable after creation. Content items reference layers
by key string (not UUID), so changing the key would orphan existing
assignments.
DB-Driven Validation (P1-36)
Section titled “DB-Driven Validation (P1-36)”API routes that accept a layer key now validate against live layer_vocabulary
rows via fetchActiveLayerKeys() from lib/validation/layer-schemas.ts:
| Route | Purpose |
|---|---|
POST /api/guides/[slug]/sections | Guide section creation |
PATCH /api/guides/[slug]/sections/[sectionId] | Guide section update |
PATCH /api/items/[id]/metadata | Content item metadata update |
If fetchActiveLayerKeys() throws (DB unavailable or no active layers), the
route returns 503 Service Unavailable. MCP tool layer validation was loosened
accordingly to accept any string, delegating validation to the API layer.
Workflow 17: Taxonomy Sync Chain
Section titled “Workflow 17: Taxonomy Sync Chain”Trigger: bun run sync:taxonomy (manual, after taxonomy changes)
Owner: package.json script
Flow (4-step chain)
Section titled “Flow (4-step chain)”generate-classification-prompt-taxonomy.ts → generate-taxonomy-snapshot.ts → sync-plugin-taxonomy.ts → build:pluginStep Detail
Section titled “Step Detail”scripts/generate-classification-prompt-taxonomy.ts— Reads taxonomy from DB; regenerates the taxonomy section oflib/ai/skills/classification.md(the classification prompt single source of truth, v4.5).scripts/generate-taxonomy-snapshot.ts— Writesscripts/tests/fixtures/taxonomy_snapshot.jsonconsumed by the Python pipeline. The snapshot is the Python pipeline’s view of the canonical taxonomy.scripts/sync-plugin-taxonomy.ts— Readstaxonomy_domains/taxonomy_subtopicsfrom DB; rewrites the plugin classification skill, search-strategy skill, and settings template (.claude/plugins/knowledge-hub/1.0.0/...).bun run build:plugin— Regenerateslib/mcp/plugin-bundle.ts(the committed base64 plugin ZIP).
Manual Steps Outside the Chain
Section titled “Manual Steps Outside the Chain”Two facts the script cannot derive from the DB:
- Key signals — short summary phrases per domain (currently embedded in the classification prompt template; require manual updates if domain semantics shift).
- Guide definitions — guide rows + guide_sections live in DB but the initial seed of guides for a new client is a manual product decision.
Drift Surfacing
Section titled “Drift Surfacing”TaxonomyDriftBanner (in components/settings/) compares the live DB
taxonomy hash against the prompt’s taxonomy hash and warns the admin if the
two diverge. The banner does NOT block operation — admins must remember to
run bun run sync:taxonomy after taxonomy edits.
Workflow 18: Tag Morphology Drift Flag Bulk Insert
Section titled “Workflow 18: Tag Morphology Drift Flag Bulk Insert”Trigger: POST /api/admin/tag-morphology/flags (called by
scripts/eval-tag-morphology-adoption.ts) Owner:
app/api/admin/tag-morphology/flags/route.ts
Validate (TagMorphologyFlagsBulkInsertSchema) → Upsert each flag (UNIQUE on stored_tag + proposed_canonical) → Return countThe eval script iterates the corpus, applies normaliseTag() to every stored
tag in content_items.ai_keywords, and posts each disagreement as a flag.
UNIQUE constraint makes re-runs idempotent — usage_count and
affected_content_ids are updated, but already-decided flags are left alone.
Automated Processes
Section titled “Automated Processes”| Process | Trigger | Route/Module | Purpose |
|---|---|---|---|
| Entity extraction | Content creation/update | Part of classification pipeline | Extract entities + relationships into entity_mentions + entity_relationships |
| Holder metadata derivation | Classification pipeline | lib/ai/classify.ts | Set metadata.holder on certification mentions (TS+Python parity) |
| ISO family type override | Classification pipeline | lib/ai/classify.ts Step 15b | Force six ISO families to entity_type='certification' |
| Coverage matrix computation | On-demand | /api/coverage | Domain-subtopic item count grid |
| Unified gap aggregation | On-demand (60s cache) | /api/coverage/gaps | Scored gaps from taxonomy, templates, guides |
| Guide coverage computation | On-demand | /api/coverage/guides | Section-level content counts and freshness |
| Template coverage computation | On-demand | /api/coverage/templates | Requirement-level coverage matching |
| Reverse bridge propagation | Entity metadata update | /api/entities/[name]/metadata | Expiry date propagation to content items |
| Tag suggestion | User typing | /api/tags/suggest (RPC) | Prefix-based tag autocomplete |
| Tag morphology eval | Manual cron-style | scripts/eval-tag-morphology-adoption.ts | Surfaces drift between stored and library-canonical tag forms |
Integration Points
Section titled “Integration Points”| External System | Direction | Protocol | Purpose |
|---|---|---|---|
| Supabase | Read/Write | REST + RPC | Primary data store; RPCs for atomic mutations and aggregation |
| Claude API | Request | HTTP | Entity extraction during content classification |
| MCP Clients | Read+Write | MCP | List/get/create/update guides; entity relationships; certification status; coverage gaps |
Handoff Points to Other Feature Areas
Section titled “Handoff Points to Other Feature Areas”| Target Feature Area | Mechanism |
|---|---|
| Content Management | Classification populates primary_domain/primary_subtopic on content items; entity extraction runs during creation pipeline; layer assignment via content_items.layer referencing layer_vocabulary.key |
| Quality Governance | Coverage targets inform governance review prioritisation; guide section freshness feeds into quality assessment |
| Search | Taxonomy domains/subtopics used as search filters; entity relationships queryable via MCP |
| Completing Forms (Procurement) | Template coverage maps form requirements to KB content; certification status reports serve form responses |
| Administration | content_owner_id (S205 WP-A) integrates with knowledge organisation by tracking ownership of curated structural items (guides, taxonomy entries) for accountability |
Taxonomy Provenance — State Machine
Section titled “Taxonomy Provenance — State Machine”Valid values for provenance on taxonomy_domains and taxonomy_subtopics:
| Provenance | Meaning |
|---|---|
baseline | Seeded during initial setup |
client | Manually created by admin |
recommended | AI-suggested (e.g. from classification pipeline) |
Transition Rules
Section titled “Transition Rules”baseline → (edit) → baseline (provenance unchanged, fields updated)recommended → (accept) → recommended (provenance unchanged, accepted_at set, is_active=true)recommended → (reject) → recommended (provenance unchanged, is_active=false)client → (deactivate) → client (is_active=false)client → (reactivate) → client (is_active=true)The provenance field does not change after creation. Only is_active,
accepted_at, and editable fields change. The accepted_at timestamp
indicates when a recommended item was promoted into active use.
Entity Type Override — Precedence
Section titled “Entity Type Override — Precedence”Entity types resolve via COALESCE semantics:
effective_type = COALESCE(entity_type_override, entity_type)entity_type | entity_type_override | effective_type | Scenario |
|---|---|---|---|
| organisation | NULL | organisation | No override applied |
| person | organisation | organisation | Admin corrected mistyped entity |
| certification | NULL | certification | Correct extraction |
| standard | certification | certification | ISO-family override (Workflow 9) |
The has_type_conflict flag is set when types_seen contains more than one
distinct type across all mentions for a canonical name.
Tag Morphology Drift Flag — State Machine
Section titled “Tag Morphology Drift Flag — State Machine”Valid values for decision on tag_morphology_drift_flags:
| Decision | Meaning |
|---|---|
pending | Awaiting admin/editor disposition (default on insert) |
accept | Library output is correct; backfill proposed_canonical |
add_override | Preserve current stored form; library should not transform this |
dismiss | Library agrees, or noise — no action |
Transition Rules
Section titled “Transition Rules”pending → (admin/editor patches /api/admin/tag-morphology/flags/[id]) → accept | add_override | dismiss(no transitions back from terminal states; re-running the eval inserts new rows for new disagreements but does not reset existing decisions)After accept is set, the admin runs
scripts/apply-tag-morphology-backfill.ts to perform the actual UPDATE on
content_items.ai_keywords. The triage UI itself does not mutate tags.
Current Limitations
Section titled “Current Limitations”- Entity extraction is coupled to the content classification pipeline — there is no standalone entity extraction endpoint
- Coverage gap computation fetches all template requirements and content items on each uncached request, which can be expensive for large knowledge bases
- The taxonomy sync chain (
bun run sync:taxonomy) is manual and must be run after any taxonomy changes; key signals + guide seeds are manual outside the chain - Entity merge is one-way — there is no unmerge operation
- Tag operations are atomic but have no audit trail of who performed them
(separate from the tag morphology triage flow which DOES record
decided_by) - Guide section matching is filter-based (
content_type_filter,subtopic_filter,expected_layer) which can produce broad matches when filters are NULL - Holder synonym fallback is conservative — it intentionally rejects valid
complies_with/evidencesclaims when the source is not a known organisation, to prevent garbage rels. Some legitimate cert claims are missed in exchange for higher precision. - Tag morphology drift backfill is out-of-band — the API records the
disposition, but
scripts/apply-tag-morphology-backfill.tsmust be run manually against accepted flag IDs to apply the actual UPDATE.