Skip to content

Phase B Prereq 1 — Ontology Pipeline Feedback Investigation

Phase B Prereq 1 — Ontology Pipeline Feedback Investigation

Section titled “Phase B Prereq 1 — Ontology Pipeline Feedback Investigation”

Audit date: 13/05/2026 (S235 Wave 1) Inputs: Liam’s review feedback on phase-b-prerequisite-1-onthology-pipeline.md §4.2 / §4.4 / §4.5 / §5 + client-doc original-requirements review + workspaces table column archaeology. Output type: Research findings. No edits to existing docs. Recommendations + open questions ONLY. Liam ratifies; cascade lands in WP2.

Reading order: §1 (workspace archaeology) → §2 (a/b/c comparison) → §3 (form-type vs application-type) → §4 (q_a_pair cardinality) → §5 (content_items ↔ source_documents) → §6 (core/client dimension) → §7 (kb_section retirement) → §8 (open questions for Liam) → §9 (one-line recommendation summary).

Critical context on scope: the post-prereq rollup in feedback-findings-review.md §5 records the §4.2 / §4.4 / §4.5 / §5 recommendations as RESOLVED. Liam’s feedback effectively REOPENS them. The investigation below honestly re-evaluates rather than defending the prior closures.


Liam’s prompt: “If you review workspaces table, you’ll see how this previously served a different function — it includes colour & icon columns, from a previous UI component, which would suggest that the table was considered ‘top-level’, with unique workspace types.”

This section investigates that hypothesis empirically.

1.1 — Current workspaces schema (verified)

Section titled “1.1 — Current workspaces schema (verified)”

Per supabase/types/database.types.ts:2857 + migration 20260416102457_pre_squash_reconciliation.sql:1951-1967:

ColumnTypeNotes
iduuid PKgen_random_uuid() default
nametext NOT NULL
descriptiontext NULL
colortext NULLPer-instance hex colour (e.g. #059669). Default applied at API layer (/api/workspaces POST: color ?? '#6366f1').
icontext NULLPer-instance lucide icon-key (e.g. globe). Default applied at API layer (icon ?? 'folder'). Restored S204 (see §1.2).
typetext NOT NULLDEFAULT 'project'::"text" in column definition — but a CHECK constraint restricts allowable values to ('bid', 'kb_section', 'intelligence'). The 'project' default is a vestigial trace (see §1.3).
statusvarchar(30) NULLCurrently holds bid state-machine values (draft, questions_extracted, …, withdrawn) when type='bid'; otherwise NULL. Conflict: shared column carries different semantics per type.
domain_metadatajsonb DEFAULT '{}'Per-type metadata blob (BidMetadata shape when type='bid').
is_archivedboolean DEFAULT false
created_by / updated_by / created_at / updated_ataudit

Two FK indexes (idx_workspaces_type, idx_workspaces_type_archived, idx_workspaces_type_status — last one is a partial index WHERE type='bid').

CHECK constraints:

  • workspaces_type_check: type ∈ ('bid', 'kb_section', 'intelligence')
  • projects_status_check: status enumerates the 10 BID_STATES (see §3 below) — NB the constraint name is projects_status_check, an IMS-era artefact retained through the squash.

supabase/migrations/20260417142044_fix_workspaces_and_storage.sql reveals the column history:

workspaces:

  • S176 squash dropped icon (used by app) and added is_active (unused).
  • Both existed on old project? NO. Old had icon; squash invented is_active.
  • Restore icon; drop is_active.

So:

  • icon column existed before the S176 squash, was lost in the squash, and was restored in S~204 (file date 17/04/2026).
  • color (note US spelling — not colour, breaking project convention) was retained through the squash.
  • The fix-up migration explicitly says “icon … used by app” — confirming a UI component depended on it pre-squash.

Code grep for workspace.color / workspace.icon consumers (relative to repo root):

FileLineUsage
components/workspace/workspace-card.tsx40ICON_MAP[workspace.icon as WorkspaceIconName] ?? Folder — renders per-workspace icon on launcher card
components/workspace/workspace-card.tsx51, 65workspace.color → border-left + icon colour on launcher card
components/workspace/workspace-selector.tsx155, 222workspace.color → coloured dot on the in-content assign-dropdown
components/content/quick-assign-button.tsx174workspace.color → coloured dot in quick-assign menu
components/browse/filter-panel.tsx566workspace.color → coloured dot in filter chip list
app/api/items/[id]/workspaces/route.ts90color: parsed.data.color ?? '#6366f1' — default colour for new workspaces
components/workspace/workspace-create-dialog.tsx39, 81User picks colour + icon in create-workspace dialog (per-instance)
lib/workspace-types.ts78-79, 96-97, 115-116, 134-135Per-TYPE defaultsdefaultColour + defaultIcon on each WorkspaceTypeConfig
lib/workspace-types.ts38, 41defaultColour: string + defaultIcon: string — typed contract on the registry

Verdict on the “top-level UI primitive” hypothesis:

The picture is mixed:

  1. Per-instance color/icon exist as user-customisable fields. UI flows let any user creating a workspace pick a colour + icon to make it distinctive in lists. This is genuine instance-level metadata.
  2. Per-type defaults also exist in lib/workspace-types.ts:WORKSPACE_TYPE_REGISTRY. Each registered type has defaultColour + defaultIcon that pre-fill the create dialog.
  3. Per-type icon overridden on launcher. The /workspaces page (app/workspaces/workspaces-content.tsx:25-26) renders one card per registered TYPE using wt.icon (the registry’s icon, not any instance’s). The launcher card uses the registry’s per-type icon — Briefcase for bids, Newspaper for intelligence, FileText for kb_section.
  4. Per-instance icon overridden on workspace-card. The individual workspace cards (workspace-card.tsx:40) use workspace.icon from the row — the user’s per-instance choice.

So the columns serve a per-instance purpose, not a per-type purpose. Per-type styling is layered on top via the code registry. The columns themselves don’t validate the “table was top-level” hypothesis — they validate “each workspace instance is paintable for user distinguishability.”

However, two other signals DO support Liam’s intuition:

  • type column carries DEFAULT 'project'::"text" in the column definition while the CHECK restricts to 3 different values. This is a vestigial trace from when the table was projects (1-to-1 with bid) and is consistent with a historical reframing.
  • The projects_status_check CHECK constraint name preserves the original table name (projects) even though the table is now workspaces. The status enum (10 values) is the BID state machine, suggesting the table was originally bid-specific (one row = one bid project) and was renamed/repurposed without normalising the status column.
  • workspaces.status shares a single column with bid-state-machine values, which is a tell that early design was bid-only (the column wasn’t typed/separated for other types).

1.3 — Current 3 type values + row counts (live prod data)

Section titled “1.3 — Current 3 type values + row counts (live prod data)”

Queried rovrymhhffssilaftdwd (prod) 13/05/2026:

TypeRow countColours usedIcons used
intelligence43 of 4 set (#059669 × 3, NULL × 1)1 of 4 set (globe × 1, NULL × 3)
bid0n/an/a
kb_section0n/an/a

Workspace names: Education Sector Monitor, [SI-GNEWS-DEDUP-1775716569992] GNews Dedup Workspace (test/scratch), MAT Auditing, NHS Digital Cyber alerts. All 4 are intelligence workspaces — bid workflow not yet exercised in prod. kb_section has zero rows in prod, consistent with Liam’s “the value was an error” framing.

content_item_workspaces: 81 assignments across 2 workspaces (Education Sector Monitor 48 + MAT Auditing 33). Content types assigned: article, compliance, policy — NO q_a_pair rows. This is critical for §4 below — empirically, the 395 prod q_a_pair rows are unassigned to any workspace today.

Multi-workspace content items: 10+ items are assigned to 2 workspaces (Education + MAT). M:N is empirically used at low cardinality.

1.4 — Theory: was workspaces originally designed as application-type-level?

Section titled “1.4 — Theory: was workspaces originally designed as application-type-level?”

Evidence in favour (“workspaces was top-level”):

  1. DEFAULT 'project'::"text" vestigial default suggests the table was once 1-to-1 with the concept of a project (bid).
  2. projects_status_check constraint name suggests projects rename history (table ‘projects’ → ‘workspaces’ / project_idworkspace_id is still in flight per Q5.5).
  3. idx_workspaces_type_status partial index WHERE type='bid' betrays a special-cased path for bid.
  4. workspaces.status shared column carrying bid-state-machine values without a type-discriminated split is a code smell.
  5. color + icon per-row affords visual distinction across workspaces — useful when each workspace IS a project / application instance.

Evidence against:

  1. lib/workspace-types.ts registers types as categories with per-category config (defaults, route, available?, hasStatus, hasContentAssignment, hasDomainMetadata, hasCustomCreation). The registry is the de-facto application-type-config-table.
  2. app/workspaces/workspaces-content.tsx renders one card per type, then /bid/, /intelligence/, etc. as top-level type-routes. Each route lists instances of that type. This UX pattern matches the “top-level workspace-type page → specific bid / RFP / proposal” mental model Liam described — already partially in place.
  3. 4 production rows ARE all instances (specific intelligence streams), not application-categories.
  4. content_item_workspaces junction is M:N (instance-level), not type-level — content assignment is per-instance.

Verdict: the table was likely originally bid-instance-level (projects table holding one row per bid), got generalised to “container for any application instance” (workspaces with type discriminator). The code registry in lib/workspace-types.ts retroactively added the application-TYPE concept on top, without ever materialising it as a DB table. Today’s state:

  • DB workspaces table = “any application instance” with type text discriminator
  • Code WORKSPACE_TYPE_REGISTRY = “type-level config” (defaults, route, features) — in code only
  • lib/workspace-types.ts exposes getValidTypeValues() to Zod for validation — so the registry IS source-of-truth for valid types, even though the DB CHECK is decoupled

This explains Liam’s “struggling to understand” — the registry was bolted on later and never reconciled with the table that may once have been the application-level primitive.


§2 — Option (a) vs Option (b) — honest comparison

Section titled “§2 — Option (a) vs Option (b) — honest comparison”

The onto-doc §4.2 (line 352) recommended:

Recommendation: Option (a). Add application_type to Layer-1 CV (formalises Theme E). Add workspaces.application_type FK to the new vocabulary. Existing workspaces.type becomes a finer-grained workspace_subtype discriminator…

Liam’s response (verbatim):

“What do we lose by not adopting Option b?” “This seems like something we would want to adopt: ‘all bid workspaces share this template set’” “This doesn’t look right: ‘application_type=bid + workspace_type=bid; application_type=sales_proposal + workspace_type=draft’ ” “I would expect as a user to be able to navigate to a top-level workspace-type (application?) page, and then see and take action on a specific bid, sales proposal, RFP, competitor research item…”

This section honestly compares the options. The onto-doc original presentation under-weighted Option (b)‘s benefits.

To make the comparison rigorous, the labels need precision:

  • Option (a) — Vocabulary-only: Add application_types table as a Layer-1 controlled vocabulary (table of allowed values + definitions). Add workspaces.application_type FK column → application_types.key. Per-application config (defaults, route, features) stays in code (lib/workspace-types.ts).
  • Option (b) — Instance table: Add application_types instance table with rich config columns (label, icon, route, defaults, features, RLS hints, satellite-table name, state-machine name). Workspaces FK to applications. App-level config moves from code to DB; clients/admins can add new application types via UI without code change.
  • Option (c) — Hybrid: Promote application_types to a config-bearing table (Option b shape) but keep code as authoritative source for built-in types (a provenance column distinguishes core vs client_defined, mirroring taxonomy_domains.provenance). Code-registered types seed the table; client-added types live alongside.

The onto-doc presented (a) without grappling with what Liam means by “share this template set” (an app-level config primitive) or “user can navigate to a top-level workspace-type page” (already in place via lib/workspace-types.ts registry — so that benefit isn’t unique to (b)).

DimensionOption (a) Vocab-onlyOption (b) Instance tableOption (c) Hybrid (provenance)
Migration costLOWEST. New application_types vocab table (5-10 rows). New FK column on workspaces. Backfill: application_type = type for existing 4 rows. ~1d work.MEDIUM. New application_types table with ~15 columns. New FK column on workspaces. Backfill. Migrate lib/workspace-types.ts config to seed-data. Add admin UI to manage. ~3-5d work.MEDIUM. As (b) but with provenance column + seed-vs-runtime separation. ~3-5d. Marginal extra cost over (b).
”All bid workspaces share this template set” (Liam’s example)FAIL. Template-set binding lives in code or in lib/workspace-types.ts (which is global). Cannot per-tenant override without code change.PASS. application_types.default_form_template_ids[] or application_types_to_form_templates junction. Client can add/remove templates per their bid workspaces via admin UI.PASS (same as b). Plus: core-provided templates marked as core-provenance; client-added ones marked as client.
User mental model (“top-level page → specific bid / RFP / proposal”)PARTIAL. /workspaces (launcher of TYPES) + /bid/ (list of bid INSTANCES) already exists via lib/workspace-types.ts. So this mental model IS supported under (a), the registry is the source. But code-driven registry means adding new application types requires code deploy, not config.FULL. New app-types can be added in admin UI; immediately get a launcher card + (with a registered route) a list page. Faster client-onboarding feedback loop.FULL (same as b).
State-management per workspace(a) has workspaces.status column carrying bid-state-machine values today — schema-implicit per-type semantics. Each new app-type with its own state-machine inherits this conflated shared column.(b) makes state-machine a per-app-type concept (application_types.state_machine_name or similar). Different state-machines per app-type explicit + discoverable.(b)‘s shape + core/client distinction (clients can register custom state-machines vs platform-native ones).
Multi-tenancy / per-client overridesLIMITED. Configuration is global per-deployment (code-level). One Phew tenant + one Knowledge Hub client today; future tenants would need code-fork-level config.STRONG. Each tenant can customise their application types in admin UI. Direct alignment with “SMB cleanup-up data via configurable platform” wider goal.STRONGEST. Tenant clones core types, marks customisations as client-provenance, retains upgrade compatibility (core-provenance rows update on platform upgrade; client-provenance untouched).
Reuse of existing color/icon/type columnsKEEP per-instance color/icon on workspaces (user picks per workspace). KEEP type as the application-type FK key. KEEP workspace_type (sub-type) — but cleanly require it to NOT duplicate the application type (Liam’s correct objection to onto-doc’s “type=bid + workspace_type=bid” example).KEEP per-instance color/icon on workspaces. workspaces.application_type_id FK replaces workspaces.type text column. Per-application defaults move to application_types.default_color + .default_icon. Existing per-type defaults from lib/workspace-types.ts seed the table.Same as (b).
Complexity for new application types (sales_proposal, competitor_research, marketing_campaign)NEW TYPE = (i) ALTER TABLE to widen CHECK + (ii) code change in lib/workspace-types.ts + (iii) optional new satellite table + (iv) deploy. Friction is mostly DB CHECK alteration.NEW TYPE = (i) INSERT row in application_types + (ii) optional new satellite + (iii) optional route component. Configuration via admin UI. Lower friction.Same as (b).
RLS implicationsRLS still flows through workspace_id (existing pattern OQ-Q113-B). application_type itself doesn’t carry per-row ACLs.RLS on application_types rows themselves possible (e.g. some types tenant-restricted). Marginal complexity if multi-tenancy is one-Supabase-per-client per the existing Q2.8 decision.(b)‘s shape + can mark types client_visible vs internal_only (some app-types could be admin-only).
Knowledge Map (CX.32) substrate fitapplication_type as a Layer-1 vocab is a node label in the KG. Works.application_type instance becomes a first-class graph node with rich properties (templates, state-machines, features). Strictly richer KG.(b)‘s shape + core/client provenance marker as edge property. Useful for “what did Phew customise vs base platform.”
Q&A pair scoping (“which app-type uses this Q&A?”)Same as today — Q&A pairs scope via workspace_id (and thus indirectly to the workspace.type).Q&A pairs scope via workspace_id + workspaces.application_type_id → indirect application_type binding. Same path; just one extra hop through application_types.Same as (b).
Schema-as-product (Talisman framing)Layer-1 CV is consolidated as a register but per-app config stays in code.Per-app config IS the schema (DB-driven). Stronger Talisman ROI — schema describes “what an application IS.”Same as (b) with provenance dimension.

Lost with Option (a):

  • App-level config that clients can customise without code change (the “all bid workspaces share this template set” benefit Liam called out).
  • Decoupling of platform deploys from app-type evolution.
  • Materialisation of application-types as first-class KG entities.
  • Multi-tenant configurability for the SMB “data cleanup” angle.

Lost with Option (b):

  • Speed-of-build (3-5d vs 1d).
  • Simplicity (one fewer table; one fewer join in every workspace fetch).
  • The reassurance of code-as-source-of-truth — Option (b) lets clients break things by mis-configuring application types.

Lost with Option (c):

  • Same as (b), and: extra complexity of dual-source (provenance) handling. But this complexity is well-precedented in taxonomy_domains + taxonomy_subtopics (both have provenance enum and a documented baseline/client/recommended cycle).

2.4 — On the onto-doc’s confusing example

Section titled “2.4 — On the onto-doc’s confusing example”

Liam called out the onto-doc line 352:

“application_type=bid + workspace_type=bid; application_type=sales_proposal + workspace_type=draft”

This was a poor example. The reason:

  • application_type answers “what KIND of work happens here?” (bid response, intelligence monitoring, sales proposal, etc.).
  • workspace_type (renamed workspace_subtype in the original ratio) ostensibly answers “what stage / variant of that work?”.
  • But the onto-doc example confuses the levels — pairing application_type=sales_proposal with workspace_type=draft muddles application identity with state.

A coherent dual-axis would look like:

  • application_type=bid + workspace_type=tender_bid vs workspace_type=framework_bid (two FLAVOURS of bid work).
  • application_type=competitor_research + workspace_type=lightweight_monitor vs workspace_type=deep_dive_analysis.

But this is over-engineering for today’s needs. Each existing workspace TYPE is its own application; no clear case yet for needing sub-flavours within an app-type. Recommendation: collapse to a single discriminator. Either:

  • workspaces.application_type (single column, Option a or b — replaces type); OR
  • Defer until a real case for sub-flavours emerges. KISS.

The investigation does not lock a decision — Liam rules. That said, the findings strongly suggest:

  • Option (c) hybrid is the strongest fit given:
    • Liam wants client-configurable application config (“share this template set”)
    • The SMB data-cleanup positioning needs schema-as-product
    • taxonomy_domains.provenance already proves the core/client provenance pattern is viable in KH
    • Cost over Option (b) is marginal
  • Option (a) is acceptable as a v1 stepping-stone IF the team agrees to revisit pre-launch (i.e. before any other tenant joins). Otherwise migration debt accumulates.
  • Option (b) without provenance is functionally close to (c) but loses the ability to distinguish “platform-shipped” from “client-extended” types. Worth (c) for the modest extra cost.

Caveat: if multi-tenant ambition is genuinely off the table for the next 12 months (one tenant = Phew), then Option (a) is fine and (c) is gold-plating. Liam should rule.


Liam’s prompt:

“Should we have procurement as an application type? Or something else — understand how we should be handling form entry for all form types, and considering that each will have state-management, will likely impact decisions here e.g., RFP, PQQ etc., will presumably all follow the same state-management as bids.”

This section maps form-types to application-types + traces state-machine implications.

ConceptBest fit: application_typeBest fit: form_typeBest fit: bothReasoning
bidYESYES (a bid IS a form-type — the canonical “questions+answers” container)YES — ambiguousA bid is both: (i) an app-level WORKSPACE in which bid work happens, (ii) the FORM-TYPE that gets extracted, matched, drafted, returned. The application is the workspace; the form is the content being processed.
rfpLIKELY NOYESNORFP is a form-type variant (Request for Proposal — slightly different evaluation criteria than a bid). It’s still bid-WORK happening in a bid WORKSPACE.
pqqLIKELY NOYESNOPQQ (Pre-Qualification Questionnaire) is a form-type. It’s bid-WORK at the qualification stage of a bid project.
ittLIKELY NOYESNOITT (Invitation to Tender) is a form-type, sub-form of procurement. Same workspace as bid.
tenderNO (synonym)YESNO”Tender” is a synonym for ITT / bid form. Likely consolidates into form_type vocab as a synonym (Layer-4 thesaurus surface — SKOS-SYN).
procurementYES (umbrella)NONOProcurement is the workflow CATEGORY (umbrella of bid + RFP + PQQ + ITT + framework etc.). It could be an application_type='procurement' that has a sub-form-type vocab (form_type ∈ {bid, rfp, pqq, itt, framework, dps}). OR — the current application_type='bid' is in fact named badly and should be procurement, with form_type discriminating bid vs RFP vs PQQ within it.
sales_proposalYESYES (a sales proposal is also a form-with-response)NO — application primarySales proposals are a distinct WORKFLOW (different state-machine semantics from bid — no “won/lost” outcome bid has; different submission semantics) but the document itself is a form-type. Application_type primary; form_type carries the format/structure.
competitor_researchYESNONOCompetitor research is not Q&A-form-based per OQ-Q111-A. Application_type only.
marketing_campaignYESLIKELY NO (not Q&A based)NOMarketing campaigns are not forms-with-response. Application_type only.
product_guideYES (app), YES (content_type=‘guide’)NOYES — but on different axes (per onto-doc §4.3 — orthogonal axes, not the same form/app axis)See onto-doc §4.3 — orthogonal.
checklistNOYESNOForm-type variant: short Q&A list with simple yes/no answers. Could be PQQ-style.
questionnaireNOYESNOGeneric form-type. Likely synonym surface with PQQ for some clients.
sales_proposal_templateNOYES (form-type)NOForm template variant — IS NOT an application type.
frameworkNOYES (form-type — framework agreement bid)NOForm-type variant of bid.
dpsNOYES (Dynamic Purchasing System)NOForm-type variant of bid.
gcloudNOYES (G-Cloud listing form)NOForm-type variant of bid (form to apply to G-Cloud framework).

Pattern that emerges:

An application_type is a workspace where a TYPE of work happens (bid responses, intelligence monitoring, sales proposing, etc.). A form_type is the structural variant of the content being processed inside that workspace. Multiple form_types can exist within a single application_type (a bid workspace processes bids, RFPs, PQQs, ITTs).

This is what the onto-doc was hinting at but didn’t make crisp enough.

Critical question for Liam: is the current application_type='bid' actually application_type='procurement'? If so, the rename is appropriate now (pre-launch) — and form_type clearly discriminates bid vs RFP vs PQQ vs ITT vs framework vs DPS vs G-Cloud within the procurement application. Each procurement workspace processes a specific tender, with one or more form-instances of varying form_types attached to it.

BID_STATES (verbatim from types/bid.ts:3-14):

export const BID_STATES = [
'draft',
'questions_extracted',
'matching',
'drafting',
'in_review',
'ready_for_export',
'submitted',
'won',
'lost',
'withdrawn',
] as const;

Transitions (verbatim from lib/bid/bid-state-machine.ts:48-59):

draft → questions_extracted | withdrawn
questions_extracted → matching | withdrawn
matching → drafting | withdrawn
drafting → in_review | withdrawn
in_review → ready_for_export | drafting | withdrawn
ready_for_export → submitted | in_review | withdrawn
submitted → won | lost | in_review | withdrawn
won, lost, withdrawn → (terminal)

Decomposition: which states are form-type-agnostic vs form-type-specific?

StateForm-type-shared?Reason
draftYES — all form typesPre-extraction.
questions_extractedYESOnce extracted, anything with Q&A shape (bid, RFP, PQQ, ITT, checklist) lands here.
matchingYES — for Q&A-shape formsMatch against q_a_pairs corpus. Same algorithm regardless of form_type.
draftingYESGenerate response. Different content guidance per form_type might be tuned via form_type config but core mechanic is shared.
in_reviewYESGeneric review state.
ready_for_exportPARTIALExport format depends on form_format (DOCX vs XLSX vs PDF). Export logic divergence per format.
submittedYESGeneric post-export state.
won / lostYES for procurement; NO for non-procurement applications (e.g. competitor_research workspace doesn’t have win/lose outcomes — has “report published” or “monitoring complete” outcomes)App-type-specific terminal states.
withdrawnYES (procurement) — NO (sector intelligence: workspaces aren’t “withdrawn”, they’re “paused” or “archived”)App-type-specific.

Hypothesis (recommended position):

The BID_STATES tuple is misnamed — it’s the procurement workflow state-machine. It applies to ANY procurement form (bid, RFP, PQQ, ITT, framework, DPS, G-Cloud). Form_type-specific differences are:

  • Extraction policy — which extractor (ExtractByLlm prompt + Pydantic schema per phase-b-prerequisite-2-cocoindex-deep-dive Rec 1) targets which form_type. Lives at form_type level, not app_type level.
  • Match policy — which q_a_pair scope_tags + anti_scope_tags are relevant. Configurable per form_type within the procurement app_type.
  • Export format — DOCX exporter for DOCX form_format; XLSX exporter for XLSX form_format. Lives at form_FORMAT level (orthogonal to form_type).
  • Outcome semantics — won/lost for tender-form; “framework awarded” / “framework rejected” for framework-bid; “shortlisted” / “not shortlisted” for PQQ. These are sub-state semantics, not extra states.

For non-procurement application_types:

  • application_type='intelligence' — has its own state-machine (none today; uses feed_articles.passed boolean + relevance_category). No 10-state pipeline; uses different lifecycle.
  • application_type='sales_proposal' — likely shares MOST of procurement’s pipeline (draft → matching → drafting → review → export → submitted) but differs at terminal (won/lost outcome shape is different — e.g. closed_won / closed_lost / nurture / no_decision).
  • application_type='competitor_research' — non-Q&A-form-based per OQ-Q111-A. Probably uses a simpler state-machine (draft → published → archived) or no state-machine at all (continuous monitoring).
  • application_type='training' / marketing_campaign — TBD.

Recommendation: state-machine sits at application_type level, with per-form_type policy injection.

Implementation pattern (under Option (c)):

application_types
- key
- state_machine_name (e.g. 'procurement_workflow' for bid, 'intelligence_lifecycle' for intelligence)
- state_machine_config (jsonb — overrides per app-type)
form_types
- key
- applicable_application_types (text[]) — e.g. {procurement} for RFP/PQQ/ITT; {sales_proposal} for proposal templates
- extraction_policy_id (FK to extraction_policies, per Theme B form pipeline)
- match_policy_config (jsonb)
workspaces
- id
- application_type_id (FK)
- state (text — values constrained by application_types.state_machine_name)

The state-machine logic itself stays in code (lib/bid/bid-state-machine.ts becomes lib/procurement/procurement-workflow.ts — Q5.5 rename territory) but the per-app-type DISPATCH is data-driven.

For Liam to rule:

  • Is application_type='bid' actually application_type='procurement'? If yes, rename now (pre-launch) is the right move.
  • If kept as bid, then RFP/PQQ/ITT live as form_type discriminators within a type='bid' workspace. Schema impact identical.
  • Or: keep current type='bid' and reserve procurement for the future-multi-form case. Pragmatic but adds a rename to the v1.1 backlog.

§4 — q_a_pair cardinality reasoning rebuild

Section titled “§4 — q_a_pair cardinality reasoning rebuild”

Onto-doc §4.4 (line 392):

Pair → workspace … Recommendation: new q_a_pair_workspaces junction, because q_a_pairs are no longer subtypes of content_items — they’re a peer class.

Liam: “same for new q_a_pair_workspaces junction, because q_a_pairs are no longer subtypes of content_items — they're a peer class.

Liam is rejecting the REASONING, not necessarily the conclusion. The reasoning (“peer class therefore needs separate junction”) doesn’t follow logically — “peer class to content_items” implies it’s at the same Layer-5 ontology level, but doesn’t tell us anything about cardinality to workspaces. This section rebuilds the cardinality argument from scratch on three axes.

4.1 — Identity angle: do q_a_pairs have workspace-independent identity?

Section titled “4.1 — Identity angle: do q_a_pairs have workspace-independent identity?”

Question: if the same question “What is your data centre’s certification?” is answered identically in two different bid workspaces, is it ONE q_a_pair (re-used) or TWO q_a_pairs (one per workspace)?

Today’s evidence (verified on prod):

  • 395 q_a_pair rows in content_items (S234 verified)
  • ZERO are assigned to any workspace (verified by SQL query: SELECT COUNT(*) FROM content_item_workspaces ciw JOIN content_items ci ON ci.id = ciw.content_item_id WHERE ci.content_type = 'q_a_pair' returns 0)
  • The 81 content_item_workspaces rows are for article, compliance, policy content_types only — exclusively in intelligence-type workspaces

This is a strong signal: q_a_pairs in production today are corpus-level, NOT workspace-scoped. They represent reusable “company-level Q&A” (capability statements, policy answers, evidence Q&As, etc.) that any bid workspace’s matcher would draw on.

Architectural intent (from 0.9-intended-architecture.md §4.3, lines 358-414):

CREATE TABLE q_a_pairs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
workspace_id UUID NOT NULL REFERENCES workspaces(id),
...

The architecture sketch has q_a_pairs as N:1 → workspaces (workspace_id is NOT NULL on the table). This contradicts the onto-doc §4.4’s “new q_a_pair_workspaces junction” recommendation.

So we have a contradiction inside the existing planning docs:

  • 0.9-intended-architecture.md §4.3 says N:1 (workspace_id NOT NULL FK on q_a_pairs)
  • phase-b-prerequisite-1-onthology-pipeline.md §4.4 says M:N (new junction q_a_pair_workspaces)

Identity answer: A q_a_pair has workspace-independent identity at the corpus level but may also have workspace-relevance flags. The question’s text and answer are atomic — they don’t change per workspace. What changes per workspace is applicability (is this Q&A relevant to THIS workspace?).

4.2 — Usage angle: does the same Q&A serve multiple workspaces?

Section titled “4.2 — Usage angle: does the same Q&A serve multiple workspaces?”

Empirically — across Phew’s 4 bid library files (Phew-Bid-Library, LMS_Bid_Library, Website_Bid_Library, Advanced_Audits_Bid_Library) — Q&A pairs about company-level things (ISO certifications, data protection, business continuity, social value) get re-used across every bid response. Bid-specific Q&A pairs (e.g. “Describe your LMS user training programme”) might be reused across all LMS-specific bids but not Websites-specific bids.

This is consistent with M:N in concept but with a tilt: most Q&A pairs are corpus-level (relevant to “all” or “many” workspaces); some are workspace-scoped to a particular client or sector.

Three plausible patterns:

A. Pure N:1 (one workspace per Q&A pair) — every Q&A is owned by exactly one workspace. Cross-workspace reuse requires duplication (anti-pattern per “one record, many views”).

B. Pure M:N (q_a_pair_workspaces junction) — every Q&A pair can be assigned to any number of workspaces. Default cardinality TBD per pair.

C. Hybrid: corpus + scope (no workspace FK on q_a_pair) — Q&A pair has no direct workspace FK at all; relevance to a workspace is computed via scope_tag / anti_scope_tag matching (per 0.9-intended-architecture.md §4.3 has scope_tag TEXT[] + anti_scope_tag TEXT[] on q_a_pairs). Workspace matcher queries: “give me q_a_pairs whose scope_tag overlaps with my workspace’s scope_tag and anti_scope_tag does not include my workspace’s scope_tag.”

Per 0.9-intended-architecture.md §4.4 and §4.3:

  • Shape A (mempalace temporal-edges) — entity_relationships extended with valid_from / valid_to / confidence / provenance columns. Used for KG temporal edges.
  • Shape B (per-workspace memory partition; mempalace wing = workspace_id per Finding 03 Q4.5) — wing-scoped memory.

For q_a_pairs:

  • Shape A applicability: temporal validity on q_a_pair (valid_from / valid_to) ALREADY in the architecture sketch (§4.3 lines 390-391). q_a_pairs ARE temporal entities (“our position on PCI-DSS as of Q1 2026”).
  • Shape B applicability: if q_a_pairs are workspace-partitioned (each wing has its own pairs), then N:1 → workspace_id FK on q_a_pair holds. But mempalace’s wing concept is for AGENT memory (drawer + tunnel storage), not for corpus content. Corpus content (content_items, q_a_pairs) is workspace-scoped only insofar as a workspace is the unit of authorisation — not necessarily of partitioning.

Argument for N:1 from Shape B:

  • If every q_a_pair belongs to exactly one workspace (i.e. exists in exactly one wing), Shape B partitioning aligns with q_a_pair workspace_id.
  • But: the empirical reality is q_a_pairs DON’T have workspace_id today (zero workspace assignments). Production q_a_pairs are corpus-level.

Argument for M:N from Shape B:

  • Shape B doesn’t BIND q_a_pairs to a single wing; it provides memory partition that can be queried alongside corpus content.

4.4 — Cardinality recommendation + reasoning

Section titled “4.4 — Cardinality recommendation + reasoning”

Recommendation: Option C from §4.2 — q_a_pairs as corpus-level + scope-tag-driven workspace relevance.

Rebuilt reasoning (NOT the rejected “peer class” rationale):

  1. Empirical: Today’s 395 production q_a_pair rows are unassigned to any workspace. They function as a corpus the bid matcher draws from.
  2. Mental model: A bid workspace’s job is to RESPOND to a tender. The corpus of Q&A pairs is the evidence base the response is built from. Evidence doesn’t “belong to” a single workspace; evidence is referenced by many.
  3. One record, many views (CLAUDE.md): A Q&A pair about “ISO 27001 certification status” should be one record. Citing it from a bid workspace, a sales proposal workspace, AND a competitor-research workspace’s competitor-comparison report should all reference the same Q&A pair.
  4. Workspace relevance via scope_tag, not FK: Per 0.9-intended-architecture.md §4.3 lines 386-387, q_a_pairs carry scope_tag + anti_scope_tag text arrays. A workspace queries WHERE q_a_pair.scope_tag && workspace.scope_tag AND NOT (q_a_pair.anti_scope_tag && workspace.scope_tag). This is looser than M:N junction — relevance is computed, not declared.
  5. Provenance via origin_kind: Some Q&A pairs ARE tied to a specific workspace (e.g. origin_kind='derived_from_bid_response' where a curator promoted a specific bid response into the corpus). For those, capture the originating workspace in a source_workspace_id FK (nullable, soft pointer for provenance). This is NOT a relevance binding; it’s an audit trail.

So the schema should be:

CREATE TABLE q_a_pairs (
id UUID PRIMARY KEY,
-- NO workspace_id FK directly (corpus-level)
source_workspace_id UUID NULL REFERENCES workspaces(id), -- provenance only, nullable
question_text TEXT NOT NULL,
answer_standard TEXT NOT NULL,
...
scope_tag TEXT[],
anti_scope_tag TEXT[],
origin_kind TEXT NOT NULL CHECK (...),
...
);

Cardinality verdict: scope-tag-driven N:M (not declared M:N junction). Workspace ↔ q_a_pair relationship is computed at query time via tag overlap; no junction table needed (q_a_pair_workspaces is unnecessary). A source_workspace_id FK (nullable) tracks provenance for promoted pairs.

Exception/edge case: if Liam wants to allow EXPLICIT workspace-scoping (“this Q&A pair is private to this client / not part of the shared corpus”), add a private_to_workspace_id UUID NULL column that, when set, scopes visibility. This is rare and likely deferred to post-v1 unless multi-tenant Q&A privacy is a launch requirement.

This rebuilt reasoning supersedes the onto-doc §4.4 “peer class → needs junction” non-sequitur.

4.5 — Implications for citations / question_matches

Section titled “4.5 — Implications for citations / question_matches”

If q_a_pairs are corpus-level (no workspace FK):

  • citations.cited_q_a_pair_id works unchanged (per onto-doc §4.4 → polymorphic citations table holds the reference; workspace scope is on citing_entity_id’s row).
  • question_matches (per OQ-Q113-C) also works unchanged — match links a bid_question (workspace-scoped via its bid workspace) to a q_a_pair (corpus-level). Scope filtering happens at match-time via scope_tag, not at FK level.

§5 — content_items ↔ source_documents relationship mapping

Section titled “§5 — content_items ↔ source_documents relationship mapping”

Liam’s prompt:

“I may have missed this in the onthology pipeline document, but what is the dynamic/relationship between content_items -> source_documents, including how any changes based on the workspace - application feedback would impact this.”

Liam is correct that the onto-doc didn’t map this explicitly. Filling the gap:

5.1 — Definitions per Talisman framework

Section titled “5.1 — Definitions per Talisman framework”
EntityLayerFunction
source_documentsLayer 5 ontology class”Binary identity + custody trail” — represents a physical/digital binary file (DOCX, PDF, XLSX), its versions, its storage location, and the workspace it belongs to.
content_itemsLayer 5 ontology classRETIRED (ID-133 BI-9). Row preserved verbatim below for historical record; content_items was DROPPED at ID-131 M6 (S450 GO) and no longer exists on any env. Pre-ID-131 text: “Knowledge artefact” — represents a unit of usable knowledge (article, capability statement, Q&A pair, etc.). May be derived from a source_document (e.g. extracted from a Phew bid library DOCX) or born digital (e.g. RSS feed article, manual input).
source_document_diffsLayer 5 ontology class”Cross-version Q&A diff” — pairs Q&A items across two versions of the same source_document. Currently planned to retire under cocoindex Scenario A.

ID-133 amendment (retired). The content_items row above is retired. The single “Knowledge artefact” Layer-5 role it described now splits three ways per the Decision-A / three-layer-model re-alignment (ID-133 BI-1/BI-3, ratified after this investigation was authored): a vetted question-and-answer pair → q_a_pairs; a classified, chunked document body → source_documents (+ classification columns) + content_chunks; a distilled, non-record concept → an L-concept in the client OKF bundle (36-three-layer-model.md). See the ontology register’s README.md “Where does new data live?” for the current routing rule. The rest of this §5 investigation (§5.2 onward) is retained as historical record of the original cardinality/lifecycle analysis and is not re-derived here — it predates the ID-131 table drop and the ID-133 split (BI-9 prose-sweep scope was this row only, not a full §5 rewrite).

Today (per S234 data inventory):

  • content_items.source_document_id FK → source_documents.id (SET NULL on source_documents delete). Nullable: a content_item may be born digital (no source document) OR derived from one.
  • N:1 direction: many content_items can be derived from one source_document (think: a Phew bid library DOCX → 100+ Q&A pair content_items).

Empirical state (verified 13/05/2026):

  • content_items with non-null source_document_id: 0
  • source_documents table rows: 0

So today this relationship is specced but unused.

5.3 — Lifecycle (does source_doc create content_item? always? optionally?)

Section titled “5.3 — Lifecycle (does source_doc create content_item? always? optionally?)”

Per 0.9-intended-architecture.md §4 + Finding 01 + Phase B Prereq 2 cocoindex evaluation:

  • Manual upload path (app/api/upload/route.ts): source_document inserted → cocoindex (planned) extracts → content_items created with source_document_id set.
  • URL ingest path (/api/ingest/url): no source_document; content_item born from URL fetch.
  • RSS feed path (feed_articles): no source_document; content_item born from feed.
  • Manual content creation (/api/items POST): no source_document; content_item born from user input.
  • Bid library import (scripts/qa-migration/): historical Phew DOCX → many content_items; planned source_document_id linking.
  • MCP create (/api/mcp create_content): no source_document; content_item born from AI / MCP client.

Conclusion: the relationship is N:1 optional — content_items MAY have source_document_id but often won’t. Under cocoindex Scenario A (Option α slim source_documents — per onto-doc §5):

  • source_documents retains binary-identity + custody-trail role
  • cocoindex_source_key column on content_items captures the cocoindex source-binding (orthogonal to source_document_id)
  • The dual fields make sense — source_document_id is the KH-side typed entity; cocoindex_source_key is the engine-side stable identifier

Today:

  • source_documents.workspace_id (NOT NULL) → workspace direct FK
  • content_items.source_document_id (nullable) → source_documents
  • content_item_workspaces M:N → workspaces (content_items don’t have direct workspace FK)

So a content_item derived from a source_document inherits the workspace via two indirect paths:

  1. Via source_document.workspace_id (if applicable)
  2. Via content_item_workspaces M:N assignments (one or many)

Potential inconsistency — under current shape, a content_item can be assigned to workspaces DIFFERENT from its source_document’s workspace. This is semantically meaningful (corpus content reused across workspaces) but could create governance confusion (“which workspace OWNS this content for review purposes?”). Resolution: content_owner_id FK answers ownership orthogonally to workspace.

5.5 — Under Option α (slim source_documents) — what becomes of this relationship?

Section titled “5.5 — Under Option α (slim source_documents) — what becomes of this relationship?”

Per onto-doc §5 line 453 (RESOLVED-DIRECTIONAL: Option α — slim-and-keep):

  • source_documents retains its entity status (Layer 5 ontology class), but versioning cols retire (cocoindex ops-DB ledger absorbs version history).
  • Remaining source_documents carries: id, workspace_id, filename, mime_type, file_size, content_hash (md5), storage_path, status, uploaded_by, archived_at, archived_by.
  • content_items.source_document_id FK remains (semantic: “this content was extracted from this binary”).
  • content_items.cocoindex_source_key ADDED — engine-side identifier (per Q1.9 Scenario A).

The two FKs serve different purposes:

  • source_document_id = “what binary did this come from?” (KH-side typed manifest)
  • cocoindex_source_key = “what is the engine’s stable handle for re-runs?”

They’re both useful; they don’t conflict.

5.6 — Under workspace=application-instance changes — does the relationship shift?

Section titled “5.6 — Under workspace=application-instance changes — does the relationship shift?”

If Option (b)/(c) from §2 is adopted (application_types table):

  • source_documents.workspace_id STAYS (binaries are uploaded to specific workspaces).
  • No change to content_items.source_document_id.
  • The transitive application_type of a content_item can be derived via either path:
    • content_itemsource_documentworkspaceapplication_type
    • content_itemcontent_item_workspacesworkspaceapplication_type

Both paths agree under normal circumstances. If they disagree (e.g. a content_item is reassigned to a different workspace from where its source_document was uploaded), this is a soft warning condition — content_owner_id provides governance routing.

Section titled “5.7 — Open question: should q_a_pairs also have a source_document_id link?”

Per 0.9-intended-architecture.md §4.3, q_a_pairs has source_content_item_id UUID REFERENCES content_items(id) and source_chunk_id. The chain is:

source_document → content_item → content_chunk → q_a_pair

This is correct — q_a_pair provenance reaches back through content_items, not directly to source_documents. The chain is intact whether content_items.source_document_id is set or not.

┌──────────────┐
│ workspaces │ (1 row per application-instance;
│ │ application_type FK if Option b/c)
└──────┬───────┘
┌───────────────────┼─────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────────┐ ┌──────────────────────┐
│source_ │ │content_item_ │ │feed_articles, │
│documents │ │workspaces M:N │ │bid_questions, │
│(binary │ │(content-to- │ │templates, etc. │
│ manifest) │ │ workspace │ │(application-specific │
│ │ │ assignment) │ │ entities, direct FK) │
└──────┬───────┘ └────────┬─────────┘ └──────────────────────┘
│ │
│ N:1 │ M:N
▼ ▼
┌─────────────────────────────────┐
│ content_items │
│ - source_document_id (nullable) │
│ - cocoindex_source_key (text) │
│ - workspace assignments via │
│ content_item_workspaces │
└──────┬───────────────────────────┘
│ 1:N
┌──────────────┐
│content_chunks│
└──────┬───────┘
│ provenance (via q_a_pairs.source_chunk_id)
┌───────────────┐
│ q_a_pairs │ (corpus-level, no direct workspace FK;
│ │ scope_tag-driven relevance per §4)
└───────────────┘
  • Q5.1: Should source_documents.workspace_id become nullable in Option α? Rationale: some uploads might be admin/shared rather than workspace-bound (e.g. a Phew bid library DOCX uploaded once, used across all bid workspaces).
  • Q5.2: When a content_item is in multiple workspaces, which workspace does Mempalace’s wing-partition (Shape B) bind to? Currently 1:1 wing=workspace_id, but content can be in many wings. Implies multi-wing-per-content reads.
  • Q5.3: Should source_documents carry an application_type_id directly (for “this binary belongs to a procurement workspace” vs “this binary belongs to a sales-proposal workspace”) OR should we always traverse via workspace? The traverse path is cleaner; an explicit FK would be premature optimisation.

§6 — Core KH platform vs client-defined data dimension

Section titled “§6 — Core KH platform vs client-defined data dimension”

Liam’s prompt (verbatim):

“When we store the data for each layer (WP6), it would also be helpful to understand what is core KH platform versus what can be defined by clients.” “This is extremely relevant to our wider platform goals of helping SMBs understand how to work with/cleanup-up their current data…”

Cross-references every CV + Layer-5 entity + Layer-2 metadata in the onto-doc against the original-requirements client docs (Product_KB_Dev_Brief, Sector-Intelligence-Brief, gap-analysis trio, kh-client-feedback) to classify what’s CORE vs CLIENT vs HYBRID. KH already has a partial precedent in taxonomy_domains.provenance enum (baseline / client / recommended).

6.1 — Cross-cutting findings before the table

Section titled “6.1 — Cross-cutting findings before the table”

Original requirements docs analysed:

  • Product_KB_Dev_Brief.md — Initial brief, defines 4 top-level sections (Sector Guides / Product Guides / Company-Corporate / Research), Sales Brief / Bid Detail / Company Reference content layer model, db schema sketch with section, guide, subsection, content_layer, tags, status, version, change_history, owner fields, user roles Admin/BidWriter/Sales.
  • Sector-Intelligence-Brief-Liam-Final.md — Sector intelligence brief: AI-filtered RSS, team-editable prompts, feedback loop (false +/-), RSS output feeds, content-tree categories.
  • kb-hub-gap-analysis-liam.md — Section 2: KB Hub vs IMS boundary. Output workspaces: bids, sales proposals, renewal packs. IMS owns policies/processes; KB Hub owns external content.
  • kh-client-feedback.md — list_user_workspaces blocker, search underperforming, scope tags + citations as required guard rails.

Strong client-doc signals on core/client split:

  1. The 4-section structure (Sectors / Products / Company-Corporate / Research) is CLIENT-DEFINED for Phew. Sectors (SCP, SAB, MATs, Education Safeguarding) are Phew-specific. Other clients have different sectors. So taxonomy_domains baseline values (sectors / products / company / research) should be client-defined.
  2. Content Layer (Sales Brief / Bid Detail / Company Reference) is HYBRID. The 3-tier layered model is core platform-shaped (the “layered content prevents duplication” architectural principle). But specific layer labels per client may diverge — some clients may want different layer names or a 4-tier model.
  3. Output workspaces (bids, sales proposals, renewal packs) are CORE application_types. All clients with similar use cases (SMBs doing bid management + sales) will use these.
  4. Sector-specific scope_tags (Liam’s internal-it / production-infrastructure / application-layer / office-physical / data-centre-physical from kh-client-feedback Item 3) are CLIENT-DEFINED. They emerged from the Bitdefender + physical-security false-positive examples — Phew-specific concerns.

Legend: Core = ships with platform, can’t be deleted; Client-defined = empty by default, clients add; Hybrid = ships with baseline values that clients can override/extend.

ItemClassificationEvidence (client-doc + line)WP6 implication
Layer 1 vocabularies
taxonomy_domains rowsHYBRID (provenance=baseline / client / recommended)Product_KB_Dev_Brief §“Section 1: Sector Guides” defines Phew-specific sectors; SI brief §2.1 implies sector verticalisation. taxonomy_domains.provenance enum ALREADY exists with baseline/client/recommended.WP6 ontology storage carries provenance per CV term. UI exposes core/client distinction.
taxonomy_subtopics rowsHYBRID (same as domains)Product_KB_Dev_Brief §“Sales Playbook Zone” defines per-product subsections (Product Overview, Key Features, …); clearly client-extensible.Same as domains.
content_type enum (15 values)CORE (closed enum) with client-extension RESEARCH AT V2Product_KB_Dev_Brief §“Database Schema” lists section as Enum (closed). Today 15 values are platform-shipped.Mark as CORE; defer client-extensible content_type to v2. Adding new content types requires schema migration.
platform enum on content_itemsCORE (closed)Pipeline-side provenance — platform-internal concept.CORE.
lifecycle_type enumCORE (closed)Platform governance vocab.CORE.
dedup_status enumCORE (closed)Platform state-machine.CORE.
freshness enumCORE (closed) but THRESHOLDS client-configurablegovernance_config.freshness_decay_* columns hint at per-domain config.CORE state vocab; THRESHOLDS per-domain via governance_config.
governance_review_status enumCORE (closed)Platform state-machine.CORE.
publication_status enumCORE (closed)Per docs/specs/publication-lifecycle-state-machine-spec.md.CORE.
change_type enum on content_historyCORE (closed)Platform audit vocab.CORE.
requirement_type enum on template_requirementsHYBRID (core baseline + client-extension)Theme A form-type generalisation implies clients may add domain-specific requirement types.Core 7 values ship; clients may add via admin UI (deferred to v1.1).
ingest_source enumCORE (closed)Pipeline-side, platform-internal.CORE.
BID_STATES const tupleCORE — re-namespace as procurement_workflow_states per §3 abovelib/bid/bid-state-machine.ts. Workflow is platform-shipped.CORE (rename per §3).
workspaces.type discriminatorCORE for the schema CHECK; HYBRID with provenance for application_types if Option b/c adoptedCurrent CHECK has (bid, kb_section, intelligence). Future: application_types table with provenance.If Option (c) — application_types carries provenance baseline/client/recommended. CORE app-types: bid (procurement) + intelligence + sales_proposal. Client app-types: marketing_campaign, custom workflows.
application_type (NEEDED)CORE — closed list for v1, HYBRID with provenance for v2Liam wants client-extensibility. Provenance pattern from taxonomy_domains applies.Build with provenance from day 1 (Option c).
form_type (NEEDED)HYBRIDProcurement forms (bid, RFP, PQQ, ITT) are CORE for procurement application; client may add custom forms (questionnaire-X).Provenance baseline/client.
form_format (NEEDED)CORE (closed)docx/xlsx/pdf/html/md — closed by what platform supports.CORE.
scope_tag / anti_scope_tag (PLANNED)CLIENT-DEFINEDkh-client-feedback Item 3 (Liam suggested internal-it, production-infrastructure, etc. — these are PHEW examples). Each client’s scope universe differs.Client manages via admin UI; no baseline values.
entity_aliases rowsHYBRID (category field is client/generic)Existing column category ∈ {client, generic}. Already encodes this distinction.Keep; rename categoryprovenance to align with taxonomy.
chunk_kind (PLANNED)CORE (closed)Platform internals.CORE.
origin_kind on q_a_pairs (PLANNED)CORE (closed)Pipeline-side.CORE.
extractor_kind on q_a_extractions (PLANNED)CORE (closed)Pipeline-side.CORE.
citation_kind (PLANNED)CORE (closed)Platform vocab.CORE.
change_report_category (NEEDED)CORE (closed)Platform vocab.CORE.
cocoindex_source_kind (PLANNED)CORE (closed)Pipeline-side; closed by what cocoindex adapter supports.CORE.
edit_intent (PLANNED)CORE (closed)Theme C state-machine.CORE.
Layer 2 metadata standards
Structural metadata (FK, embedding, content_text_hash, pipeline_run_id)COREPlatform-shipped.CORE.
Descriptive metadata (title, content, brief, detail, reference, summary)CORE shape; client-DEFINED VALUESEvery client supplies their own content.Core schema; client content.
Administrative metadata (created_at, updated_at, owner, governance)COREPlatform-shipped.CORE.
Application-served axis (content_items.application_type tag, per onto-doc §2.2)HYBRIDBuilt-in app-types CORE; client-added app-types CLIENT.Provenance per Option (c).
Layer 5 entities
content_items tableCOREPlatform schema.CORE.
q_a_pairs table (planned)COREPlatform schema.CORE.
workspaces tableCOREPlatform schema.CORE.
bid_questions / bid_responsesCORE (procurement application)Platform schema.CORE.
source_documentsCOREPlatform schema.CORE.
templates / form_templates (planned)CORE schema; HYBRID instancesSchema is platform; templates uploaded by client. Some templates may be platform-shipped (SSQ, government bid templates) — these are baseline templates with provenance.Provenance per instance: baseline/client.
form_template_requirementsHYBRIDPlatform may ship baseline SSQ-style requirement library (96 prod rows already in template_requirements); clients extend with their own. template_requirements.is_current already exists; add provenance.Provenance per row.
guides / guide_sectionsHYBRIDPhew has Phew-specific guides (LMS, Websites, Audits); platform may ship baseline “Company / Corporate” guide template applicable to all SMBs.Provenance per guide.
feed_sources / feed_prompts / feed_articlesCLIENT-DEFINED INSTANCES, CORE SCHEMAEach client configures their own RSS sources per Sector-Intelligence-Brief §1.2.Client manages instances.
entity_mentions / entity_relationships / entity_aliasesCORE SCHEMA; HYBRID INSTANCESEntities extracted from content; baseline aliases shipped (per existing category=generic rows); client adds client-specific.Same as today’s pattern.
coverage_targetsHYBRIDPer-domain target values may be platform recommendations OR client-set.Provenance per row.
governance_configCLIENT-DEFINED PER DOMAINEach client configures their governance posture per governance_config (posture, preset, reviewer, timeout).Client manages.
Layer 6 / KG
Knowledge Map UICORE featureCX.32 platform feature.CORE.
KG entity types (12-value entity_type enum)CORE (closed)entity-type-taxonomy-spec.md source of truth.CORE.
entity_relationships.relationship_type (10-value enum)CORE (closed)Platform vocab.CORE.
Workflows / behaviour
bid-state-machine transitionsCORE (procurement workflow)lib/bid/bid-state-machine.ts.CORE.
Freshness decay rates (per lifecycle_type)HYBRID (baseline shipped; client-tunable)governance_config.freshness_decay_* (implied per §4.4 of onto-doc).Per-domain override.
Quality scoring weightsHYBRIDlib/quality/quality-score.ts — likely shipped with weights; could be client-tunable in admin UI.Defer to v1.1 (out-of-scope for ontology pipeline).
Review cadence default (per content_type)HYBRIDPer-content_type default + per-item override.Provenance per default row.

The Markdown ontology storage (WP6 — when we store the ontology) needs:

  1. A provenance field per CV term with values core / client / recommended. Mirrors taxonomy_domains.provenance.
  2. A client_extensible flag per CV — distinguishes closed enums (e.g. content_type, change_type — clients can’t add) from open vocabularies (e.g. scope_tag, custom application_types).
  3. An editable_via field per CVdatabase_migration (closed schema CHECK), admin_ui (client adds via UI), seed_data (replaceable on platform upgrade but baseline-protected).
  4. A core_seed_path field for hybrid vocabularies — pointer to where the platform’s default values live (e.g. scripts/seed/baseline-taxonomy-domains.sql).

Concretely, the WP6 ontology Markdown might look like:

---
cv_name: application_types
layer: 1
provenance_model: hybrid
client_extensible: true
editable_via: admin_ui
core_seed_path: scripts/seed/baseline-application-types.sql
baseline_values:
- key: bid
label: Procurement / Bid
provenance: core
- key: intelligence
label: Sector Intelligence
provenance: core
- key: sales_proposal
label: Sales Proposal
provenance: core
related_layers: [3, 5]
---

This pattern lets the platform ship a known core while letting Phew (and future clients) extend without code change — directly serving the “SMB data cleanup” positioning Liam cited.

6.4 — Implication for the SMB positioning

Section titled “6.4 — Implication for the SMB positioning”

Liam’s note:

“This is extremely relevant to our wider platform goals of helping SMBs understand how to work with/cleanup-up their current data, to better improve the outputs they can achieve when working with AI/AI agents.”

The core/client/hybrid dimension is a product feature, not just a schema concern. Each new SMB tenant onboarding can be guided through:

  1. “Here’s the core platform vocabulary — you don’t need to think about it. It just works.”
  2. “Here’s the hybrid vocabulary — we’ve shipped sensible defaults; you can extend with your own.”
  3. “Here’s the client vocabulary you need to fill in — your sectors, your products, your scope tags.”

This onboarding/disambiguation step is exactly what the onto-doc Layer-1 + Layer-2 disambiguation work targets. The WP6 ontology storage shape should make this product story explicit.


Liam: “The kb_section value in the DB was an error, and should be removed as part of schema cleanup/rework.”

DB CHECK constraint:

  • supabase/migrations/20260416102457_pre_squash_reconciliation.sql:1966workspaces_type_check includes 'kb_section'.

Code references:

  • lib/workspace-types.ts:88 — registered type with available: true, route null, defaults colour #6366f1 icon folder.
  • lib/mcp/tools/workspaces.ts:65-66 — MCP tool maps args.type === 'content' to 'kb_section' (UX rename in flight on MCP side).
  • components/workspace/workspace-create-dialog.tsx:34 — default type prop value is 'kb_section'.
  • app/api/workspaces/route.ts:80 — default insert value is 'kb_section' if no type specified.
  • hooks/use-library-bulk-actions.ts:193 — bulk fallback type.

Prod data:

  • 0 rows with type='kb_section' (verified via SQL query — only 4 intelligence workspaces exist).

Phase 1 — Schema migration (DDL):

-- Drop kb_section from CHECK constraint
ALTER TABLE public.workspaces DROP CONSTRAINT workspaces_type_check;
ALTER TABLE public.workspaces ADD CONSTRAINT workspaces_type_check
CHECK (type = ANY (ARRAY['bid', 'intelligence']));
-- If pre-emptive sales_proposal landing, include it:
-- CHECK (type = ANY (ARRAY['bid', 'intelligence', 'sales_proposal']));

(If Option b/c adopted, the CHECK is replaced by an FK to application_types.)

Phase 2 — Code cleanup:

  • Delete kb_section registration block in lib/workspace-types.ts (lines 87-103).
  • Change app/api/workspaces/route.ts:80 default from 'kb_section' to a sensible alternative or require explicit type at API surface (recommended — reject inserts without an explicit type rather than defaulting).
  • Change components/workspace/workspace-create-dialog.tsx:34 default to whatever the calling context requires (most callers pass type explicitly).
  • Change lib/mcp/tools/workspaces.ts:65-66 to drop the ‘content’-to-‘kb_section’ mapping.
  • Change hooks/use-library-bulk-actions.ts:193 fallback.

Phase 3 — Data migration: none required (0 prod rows).

  • Application breakage: if the create-workspace flow doesn’t pass a type, it falls back to kb_section. After removal, this fallback fails CHECK. Fix all call-sites first; CHECK migration runs last.
  • Test fixtures: __tests__/lib/workspace-types.test.ts will break. Update fixtures.
  • MCP regression: the 'content' MCP filter mapping (workspaces.ts:65) becomes dead code; tools/eval suite needs update.
  • Staging-mirror drift: staging branch (per CLAUDE.md turayklvaunphgbgscat) may have kb_section test rows — clean those before main migration.
  • Backward-compatible client API: if any external integration creates workspaces with type='kb_section', this is breaking. No evidence of external integration; KH MCP server is the only external surface.

If Option (b)/(c) from §2 adopted, the kb_section retirement folds into the bigger application_types migration. If Option (a) only, the retirement is a small standalone migration plus code cleanup.

Retire kb_section in the same PR as the Q5.5 project_id → workspace_id rename + the application_types introduction (whether via Option a/b/c). Pre-launch is the cheapest time. Land as part of the “schema-restructure PR” the onto-doc §4.6 names.


The investigation surfaced the following open questions that need Liam ruling before WP2 cascades.

8.1 — On application/workspace structure

Section titled “8.1 — On application/workspace structure”
QQuestionWhy it matters
Q-OQR1-01Option (a) vocabulary-only / Option (b) instance-table / Option (c) hybrid with provenance — which to adopt?Determines WP2 schema scope. 1d vs 3-5d work. Affects SMB multi-tenant positioning.
Q-OQR1-02Is application_type='bid' actually application_type='procurement'? (Rename now, pre-launch, vs defer.)Lets RFP/PQQ/ITT live cleanly as form_type within application_type='procurement'.
Q-OQR1-03If Option (c), what application_types ship as baseline core-provenance? Proposed list: bid (procurement), intelligence, sales_proposal, product_guide, competitor_research, training_onboarding.Determines seed-data shape.
Q-OQR1-04Single application_type discriminator (no sub-flavour workspace_subtype) vs dual axis?KISS recommendation: single. Liam to confirm.
Q-OQR1-05Where does state-machine code live: per-app-type (lib/procurement/...) vs current lib/bid/...? Rename pre-launch with Q5.5?Naming-cleanup PR scope.
QQuestionWhy it matters
Q-OQR1-06Accept §4 recommendation (q_a_pairs corpus-level + scope-tag relevance, no direct workspace FK)?Direct contradiction of two prior planning docs (0.9-intended-architecture.md §4.3 says N:1 FK; onto-doc §4.4 says M:N junction). Need explicit ruling.
Q-OQR1-07Add source_workspace_id (nullable, provenance only) to q_a_pairs vs leave provenance via source_content_item_id?If derived_from_bid_response origin, do we capture originating workspace for audit?
Q-OQR1-08Q&A pair privacy — does any client need workspace-private q_a_pairs (visible only to one workspace)?If yes, add private_to_workspace_id column; if no, defer.

8.3 — On content_items ↔ source_documents

Section titled “8.3 — On content_items ↔ source_documents”
QQuestionWhy it matters
Q-OQR1-09Should source_documents.workspace_id become nullable (admin-shared binaries)?Currently NOT NULL; would let one Phew bid library DOCX serve all bid workspaces.
Q-OQR1-10Does multi-wing content (Mempalace Shape B) need explicit handling, or is “wing-per-assignment, content visible across multiple wings” implicit?Wing-bridge semantics.
QQuestionWhy it matters
Q-OQR1-11Adopt the §6 provenance pattern across ALL hybrid vocabularies (not just taxonomy_domains)?Big lift but enables the SMB onboarding-cleanup product story. Liam ruling on scope.
Q-OQR1-12WP6 ontology markdown schema — does it carry the provenance_model + client_extensible + editable_via + core_seed_path fields per §6.3?Schema-as-product framing.
Q-OQR1-13Does the “admin UI to manage client vocabularies” land in v1 or v1.1?v1 lift estimate ~5-10d for the admin surfaces; v1.1 acceptable if only Phew tenant for v1.
QQuestionWhy it matters
Q-OQR1-14Form_type-specific behaviour (extraction policy, match policy, export policy) — code-driven or data-driven?Code-driven = simpler; data-driven = client-configurable. Likely code-driven for v1, data-driven for v2.
Q-OQR1-15When won / lost / withdrawn terminal states semantics diverge per application_type, where does the renaming/mapping happen?Per-app-type state vocabulary table vs shared with per-app-type label override.
QQuestionWhy it matters
Q-OQR1-16Combined PR scope: kb_section retire + application_types introduce + project_id→workspace_id rename + digests→change_reports rename — one PR or sequential?Per OQ-Q55-A previously DECIDED “combined”. Verify that holds given the additional application_types scope from this investigation.
Q-OQR1-17Migration sequencing: data-migration before/after CHECK migration? Drop-then-add-CHECK vs ALTER-TYPE-USING?Standard pattern; deferred to migration writing time.

§9 — Recommendations summary (one-line per question)

Section titled “§9 — Recommendations summary (one-line per question)”

Liam decides. The investigation states the option + the chief trade-off.

#RecommendationChief trade-off
§2 Option choiceOption (c) hybrid with provenance+3-5d migration cost; -code-fork risk for future tenants; +SMB positioning fit; +Talisman schema-as-product alignment.
§3 procurement renameRename bidprocurement pre-launchOne-time rename pain (~3-5d); lifelong clarity on RFP/PQQ/ITT/framework/DPS/G-Cloud as form_types-within-procurement.
§3 state-machine homeState-machine at application_type level; form_type policies via config injectionSlightly more abstraction; pays off when sales_proposal lands with overlapping-but-distinct workflow.
#RecommendationChief trade-off
CardinalityCorpus-level q_a_pairs; no workspace FK; scope_tag-driven workspace relevanceContradicts both 0.9-intended-architecture.md §4.3 (N:1) and onto-doc §4.4 (M:N junction). Requires explicit Liam ratification + retro-update of both docs.
ProvenanceAdd nullable source_workspace_id to q_a_pairs (audit only)One extra column; standard provenance pattern; doesn’t bind cardinality.
PrivacyDefer workspace-private q_a_pairs to v1.1Single Phew tenant means private/shared distinction not load-bearing for v1.
#RecommendationChief trade-off
RelationshipN:1 optional content_items → source_documents; KEEP both source_document_id FK AND cocoindex_source_key engine handleTwo semantically-distinct fields. Both useful.
Workspace scopingMake source_documents.workspace_id nullableLets shared binaries serve multiple workspaces; small RLS adjustment.
Under Option (c)No change to the FK relationshipapplication_type FK on workspaces is transparent to source_documents.
#RecommendationChief trade-off
Provenance patternAdopt provenance enum across all HYBRID vocabularies, mirroring taxonomy_domains.provenanceOne pattern, well-precedented; cleanest mental model.
Admin UI scopeDefer client-managed application_types + scope_tags admin UI to v1.1v1 ships with Phew config; v1.1 unlocks generic SMB onboarding.
WP6 ontology fieldsAdd provenance_model, client_extensible, editable_via, core_seed_path to ontology Markdown frontmatterSchema-as-product framing; minor doc-shape addition.
#RecommendationChief trade-off
RetirementRetire kb_section in the combined schema-cleanup PRZero prod rows; only friction is code cleanup + test fixture updates.

§10 — What this doc does NOT settle (and why)

Section titled “§10 — What this doc does NOT settle (and why)”

Items deferred for Liam-only operational ruling, not addressable via this investigation:

  • Multi-tenant launch readiness — whether Knowledge Hub launches at v1 with one tenant (Phew) or multi-tenant. The Option (a)/(b)/(c) decision tilts on this; explicit Liam ruling needed.
  • State-machine code reorganisation (Q5.5 rename + procurement rename) — touches ~50 code files; Liam needs to confirm scope/sequencing fits the combined schema-cleanup PR.
  • Form-type pipeline gating on Docling sidecar architecture — per Phase B Prereq 2’s Cloud Run sidecar architecture (Docling 1.8 GB). Settled by Prereq 2d Docling spike but operational kickoff dependent on Liam Cloud Run approval.
  • Admin UI for client-vocabulary management — full UI shape outside ontology pipeline scope. WP4 06-mcp-tooling or dedicated WP for admin surfaces.

§11 — Reading-back-to-existing-docs (cross-reference cleanup)

Section titled “§11 — Reading-back-to-existing-docs (cross-reference cleanup)”

If Liam ratifies the recommendations in §9, the following existing docs need updating in WP2 (Liam directs):

DocSection(s)Update needed
phase-b-prerequisite-1-onthology-pipeline.md§4.2 (line 348-352)Re-author with Option (a/b/c) honest comparison + Liam’s choice.
phase-b-prerequisite-1-onthology-pipeline.md§4.4 (line 392)Re-author q_a_pair cardinality with the rebuilt reasoning per §4 of this doc (scope-tag relevance, NOT peer-class-junction).
phase-b-prerequisite-1-onthology-pipeline.md§4.5 (line 410)Add procurement/form_type clarification per §3.
phase-b-prerequisite-1-onthology-pipeline.md§5 RESOLVED rowsMark “Applications layer above workspaces”, “Per-type satellite registry pattern”, “form-type schema” as RE-OPENED → RESOLVED-PER-WP-ONTO-R1.
phase-b-prerequisite-1-onthology-pipeline.md§6 build orderPhase 1 CV consolidation gets application_types (per Option c) instead of “vocabulary-only”.
phase-b-prerequisite-1-onthology-pipeline.md§2.1 (line 58)Update workspaces.type discriminator line — current values + retirement note for kb_section.
feedback-findings-review.md§5.1 Theme E rowUpdate from RESOLVED-OPTION-A to RESOLVED-OPTION-? per Liam.
feedback-findings-review.md§5.2.3 (Q1.13 Shape B retention)Confirm Shape B holds; clarify application_type vs workspace.type semantics.
feedback-findings-review.md§5.2.6 q_a_pair cardinalityUpdate with §4 reasoning.
0.9-intended-architecture.md§4.3 q_a_pairs schema sketch (line 361)Change workspace_id UUID NOT NULL → reconsider per §4.
0.9-intended-architecture.md§4.5 workspaces extension (line 588)Add application_type FK if Option (b/c) adopted.
0.9-decision-graph.mdQ1.13 rowRESOLVED-SHAPE-B with application_type qualifier.
0.9-decision-graph.mdQ3.11 rowRe-confirm form_templates rename target post-procurement-rename decision.
lib/workspace-types.tsWhole fileBecomes either seed data for application_types table (Option c) or stays as registry (Option a).
Migration sequenceNew migrationapplication_types table + seed + workspaces FK + drop kb_section + CHECK update.

End of WP-ONTO-R1 investigation. This is research-only. No existing docs touched. Main session + Liam to ratify; cascade in WP2.