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.
§1 — Workspace table archaeology
Section titled “§1 — Workspace table archaeology”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:
| Column | Type | Notes |
|---|---|---|
id | uuid PK | gen_random_uuid() default |
name | text NOT NULL | — |
description | text NULL | — |
color | text NULL | Per-instance hex colour (e.g. #059669). Default applied at API layer (/api/workspaces POST: color ?? '#6366f1'). |
icon | text NULL | Per-instance lucide icon-key (e.g. globe). Default applied at API layer (icon ?? 'folder'). Restored S204 (see §1.2). |
type | text NOT NULL | DEFAULT '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). |
status | varchar(30) NULL | Currently holds bid state-machine values (draft, questions_extracted, …, withdrawn) when type='bid'; otherwise NULL. Conflict: shared column carries different semantics per type. |
domain_metadata | jsonb DEFAULT '{}' | Per-type metadata blob (BidMetadata shape when type='bid'). |
is_archived | boolean DEFAULT false | — |
created_by / updated_by / created_at / updated_at | audit | — |
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:statusenumerates the 10 BID_STATES (see §3 below) — NB the constraint name isprojects_status_check, an IMS-era artefact retained through the squash.
1.2 — color + icon archaeology
Section titled “1.2 — color + icon archaeology”supabase/migrations/20260417142044_fix_workspaces_and_storage.sql reveals the column history:
workspaces:
- S176 squash dropped
icon(used by app) and addedis_active(unused).- Both existed on old project? NO. Old had icon; squash invented is_active.
- Restore icon; drop is_active.
So:
iconcolumn 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 — notcolour, 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):
| File | Line | Usage |
|---|---|---|
components/workspace/workspace-card.tsx | 40 | ICON_MAP[workspace.icon as WorkspaceIconName] ?? Folder — renders per-workspace icon on launcher card |
components/workspace/workspace-card.tsx | 51, 65 | workspace.color → border-left + icon colour on launcher card |
components/workspace/workspace-selector.tsx | 155, 222 | workspace.color → coloured dot on the in-content assign-dropdown |
components/content/quick-assign-button.tsx | 174 | workspace.color → coloured dot in quick-assign menu |
components/browse/filter-panel.tsx | 566 | workspace.color → coloured dot in filter chip list |
app/api/items/[id]/workspaces/route.ts | 90 | color: parsed.data.color ?? '#6366f1' — default colour for new workspaces |
components/workspace/workspace-create-dialog.tsx | 39, 81 | User picks colour + icon in create-workspace dialog (per-instance) |
lib/workspace-types.ts | 78-79, 96-97, 115-116, 134-135 | Per-TYPE defaults — defaultColour + defaultIcon on each WorkspaceTypeConfig |
lib/workspace-types.ts | 38, 41 | defaultColour: string + defaultIcon: string — typed contract on the registry |
Verdict on the “top-level UI primitive” hypothesis:
The picture is mixed:
- 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.
- Per-type defaults also exist in
lib/workspace-types.ts:WORKSPACE_TYPE_REGISTRY. Each registered type hasdefaultColour+defaultIconthat pre-fill the create dialog. - Per-type icon overridden on launcher. The
/workspacespage (app/workspaces/workspaces-content.tsx:25-26) renders one card per registered TYPE usingwt.icon(the registry’s icon, not any instance’s). The launcher card uses the registry’s per-type icon —Briefcasefor bids,Newspaperfor intelligence,FileTextfor kb_section. - Per-instance icon overridden on workspace-card. The individual workspace cards (
workspace-card.tsx:40) useworkspace.iconfrom 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:
typecolumn carriesDEFAULT 'project'::"text"in the column definition while the CHECK restricts to 3 different values. This is a vestigial trace from when the table wasprojects(1-to-1 with bid) and is consistent with a historical reframing.- The
projects_status_checkCHECK constraint name preserves the original table name (projects) even though the table is nowworkspaces. 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.statusshares 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:
| Type | Row count | Colours used | Icons used |
|---|---|---|---|
intelligence | 4 | 3 of 4 set (#059669 × 3, NULL × 1) | 1 of 4 set (globe × 1, NULL × 3) |
bid | 0 | n/a | n/a |
kb_section | 0 | n/a | n/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”):
DEFAULT 'project'::"text"vestigial default suggests the table was once 1-to-1 with the concept of a project (bid).projects_status_checkconstraint name suggestsprojectsrename history (table ‘projects’ → ‘workspaces’ /project_id→workspace_idis still in flight per Q5.5).idx_workspaces_type_statuspartial indexWHERE type='bid'betrays a special-cased path for bid.workspaces.statusshared column carrying bid-state-machine values without a type-discriminated split is a code smell.color+iconper-row affords visual distinction across workspaces — useful when each workspace IS a project / application instance.
Evidence against:
lib/workspace-types.tsregisters types as categories with per-category config (defaults, route, available?, hasStatus, hasContentAssignment, hasDomainMetadata, hasCustomCreation). The registry is the de-facto application-type-config-table.app/workspaces/workspaces-content.tsxrenders 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.- 4 production rows ARE all instances (specific intelligence streams), not application-categories.
content_item_workspacesjunction 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
workspacestable = “any application instance” withtypetext discriminator - Code
WORKSPACE_TYPE_REGISTRY= “type-level config” (defaults, route, features) — in code only lib/workspace-types.tsexposesgetValidTypeValues()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_typeto Layer-1 CV (formalises Theme E). Addworkspaces.application_typeFK to the new vocabulary. Existingworkspaces.typebecomes a finer-grainedworkspace_subtypediscriminator…
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.
2.1 — Definitions (precise)
Section titled “2.1 — Definitions (precise)”To make the comparison rigorous, the labels need precision:
- Option (a) — Vocabulary-only: Add
application_typestable as a Layer-1 controlled vocabulary (table of allowed values + definitions). Addworkspaces.application_typeFK column →application_types.key. Per-application config (defaults, route, features) stays in code (lib/workspace-types.ts). - Option (b) — Instance table: Add
application_typesinstance 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_typesto a config-bearing table (Option b shape) but keep code as authoritative source for built-in types (aprovenancecolumn distinguishescorevsclient_defined, mirroringtaxonomy_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)).
2.2 — Dimensional comparison
Section titled “2.2 — Dimensional comparison”| Dimension | Option (a) Vocab-only | Option (b) Instance table | Option (c) Hybrid (provenance) |
|---|---|---|---|
| Migration cost | LOWEST. 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 overrides | LIMITED. 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 columns | KEEP 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 implications | RLS 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 fit | application_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. |
2.3 — What’s lost with each option
Section titled “2.3 — What’s lost with each option”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 haveprovenanceenum 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_typeanswers “what KIND of work happens here?” (bid response, intelligence monitoring, sales proposal, etc.).workspace_type(renamedworkspace_subtypein the original ratio) ostensibly answers “what stage / variant of that work?”.- But the onto-doc example confuses the levels — pairing
application_type=sales_proposalwithworkspace_type=draftmuddles application identity with state.
A coherent dual-axis would look like:
application_type=bid+workspace_type=tender_bidvsworkspace_type=framework_bid(two FLAVOURS of bid work).application_type=competitor_research+workspace_type=lightweight_monitorvsworkspace_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 — replacestype); OR- Defer until a real case for sub-flavours emerges. KISS.
2.5 — Recommendation + caveats
Section titled “2.5 — Recommendation + caveats”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.provenancealready 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.
§3 — Form-type vs application-type
Section titled “§3 — Form-type vs application-type”Liam’s prompt:
“Should we have
procurementas 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.
3.1 — Where each concept belongs
Section titled “3.1 — Where each concept belongs”| Concept | Best fit: application_type | Best fit: form_type | Best fit: both | Reasoning |
|---|---|---|---|---|
bid | YES | YES (a bid IS a form-type — the canonical “questions+answers” container) | YES — ambiguous | A 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. |
rfp | LIKELY NO | YES | NO | RFP 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. |
pqq | LIKELY NO | YES | NO | PQQ (Pre-Qualification Questionnaire) is a form-type. It’s bid-WORK at the qualification stage of a bid project. |
itt | LIKELY NO | YES | NO | ITT (Invitation to Tender) is a form-type, sub-form of procurement. Same workspace as bid. |
tender | NO (synonym) | YES | NO | ”Tender” is a synonym for ITT / bid form. Likely consolidates into form_type vocab as a synonym (Layer-4 thesaurus surface — SKOS-SYN). |
procurement | YES (umbrella) | NO | NO | Procurement 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_proposal | YES | YES (a sales proposal is also a form-with-response) | NO — application primary | Sales 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_research | YES | NO | NO | Competitor research is not Q&A-form-based per OQ-Q111-A. Application_type only. |
marketing_campaign | YES | LIKELY NO (not Q&A based) | NO | Marketing campaigns are not forms-with-response. Application_type only. |
product_guide | YES (app), YES (content_type=‘guide’) | NO | YES — but on different axes (per onto-doc §4.3 — orthogonal axes, not the same form/app axis) | See onto-doc §4.3 — orthogonal. |
checklist | NO | YES | NO | Form-type variant: short Q&A list with simple yes/no answers. Could be PQQ-style. |
questionnaire | NO | YES | NO | Generic form-type. Likely synonym surface with PQQ for some clients. |
sales_proposal_template | NO | YES (form-type) | NO | Form template variant — IS NOT an application type. |
framework | NO | YES (form-type — framework agreement bid) | NO | Form-type variant of bid. |
dps | NO | YES (Dynamic Purchasing System) | NO | Form-type variant of bid. |
gcloud | NO | YES (G-Cloud listing form) | NO | Form-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.
3.2 — State-machine mapping
Section titled “3.2 — State-machine mapping”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 | withdrawnquestions_extracted → matching | withdrawnmatching → drafting | withdrawndrafting → in_review | withdrawnin_review → ready_for_export | drafting | withdrawnready_for_export → submitted | in_review | withdrawnsubmitted → won | lost | in_review | withdrawnwon, lost, withdrawn → (terminal)Decomposition: which states are form-type-agnostic vs form-type-specific?
| State | Form-type-shared? | Reason |
|---|---|---|
draft | YES — all form types | Pre-extraction. |
questions_extracted | YES | Once extracted, anything with Q&A shape (bid, RFP, PQQ, ITT, checklist) lands here. |
matching | YES — for Q&A-shape forms | Match against q_a_pairs corpus. Same algorithm regardless of form_type. |
drafting | YES | Generate response. Different content guidance per form_type might be tuned via form_type config but core mechanic is shared. |
in_review | YES | Generic review state. |
ready_for_export | PARTIAL | Export format depends on form_format (DOCX vs XLSX vs PDF). Export logic divergence per format. |
submitted | YES | Generic post-export state. |
won / lost | YES 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. |
withdrawn | YES (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 (
ExtractByLlmprompt + Pydantic schema perphase-b-prerequisite-2-cocoindex-deep-diveRec 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; usesfeed_articles.passedboolean +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'actuallyapplication_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 atype='bid'workspace. Schema impact identical. - Or: keep current
type='bid'and reserveprocurementfor 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_workspacesjunction, 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_workspacesrows are forarticle,compliance,policycontent_types only — exclusively inintelligence-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 junctionq_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.”
4.3 — Mempalace Shape A/B angle
Section titled “4.3 — Mempalace Shape A/B angle”Per 0.9-intended-architecture.md §4.4 and §4.3:
- Shape A (mempalace temporal-edges) —
entity_relationshipsextended withvalid_from / valid_to / confidence / provenancecolumns. 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):
- Empirical: Today’s 395 production q_a_pair rows are unassigned to any workspace. They function as a corpus the bid matcher draws from.
- 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.
- 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.
- Workspace relevance via scope_tag, not FK: Per
0.9-intended-architecture.md§4.3 lines 386-387, q_a_pairs carryscope_tag+anti_scope_tagtext arrays. A workspace queriesWHERE 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. - 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 asource_workspace_idFK (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_idworks unchanged (per onto-doc §4.4 → polymorphic citations table holds the reference; workspace scope is onciting_entity_id’s row).question_matches(per OQ-Q113-C) also works unchanged — match links abid_question(workspace-scoped via its bid workspace) to aq_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 theworkspace - applicationfeedback 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”| Entity | Layer | Function |
|---|---|---|
source_documents | Layer 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_items | RETIRED (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_diffs | Layer 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_itemsrow 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’sREADME.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).
5.2 — Cardinality + direction of FK(s)
Section titled “5.2 — Cardinality + direction of FK(s)”Today (per S234 data inventory):
content_items.source_document_idFK →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_itemswith non-nullsource_document_id: 0source_documentstable 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 withsource_document_idset. - 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/itemsPOST): no source_document; content_item born from user input. - Bid library import (
scripts/qa-migration/): historical Phew DOCX → many content_items; plannedsource_document_idlinking. - MCP create (
/api/mcpcreate_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_documentsretains binary-identity + custody-trail rolecocoindex_source_keycolumn on content_items captures the cocoindex source-binding (orthogonal to source_document_id)- The dual fields make sense —
source_document_idis the KH-side typed entity;cocoindex_source_keyis the engine-side stable identifier
5.4 — Workspace scoping
Section titled “5.4 — Workspace scoping”Today:
source_documents.workspace_id(NOT NULL) → workspace direct FKcontent_items.source_document_id(nullable) → source_documentscontent_item_workspacesM: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:
- Via source_document.workspace_id (if applicable)
- 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_documentsretains its entity status (Layer 5 ontology class), but versioning cols retire (cocoindex ops-DB ledger absorbs version history).- Remaining
source_documentscarries:id, workspace_id, filename, mime_type, file_size, content_hash (md5), storage_path, status, uploaded_by, archived_at, archived_by. content_items.source_document_idFK remains (semantic: “this content was extracted from this binary”).content_items.cocoindex_source_keyADDED — 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_idSTAYS (binaries are uploaded to specific workspaces).- No change to content_items.source_document_id.
- The transitive
application_typeof a content_item can be derived via either path:content_item→source_document→workspace→application_typecontent_item→content_item_workspaces→workspace→application_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.
5.7 — Open question: should q_a_pairs also have a source_document_id link?
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_pairThis 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.
5.8 — Diagram
Section titled “5.8 — Diagram” ┌──────────────┐ │ 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)└───────────────┘5.9 — Open questions remaining
Section titled “5.9 — Open questions remaining”- Q5.1: Should
source_documents.workspace_idbecome 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_documentscarry anapplication_type_iddirectly (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 withsection,guide,subsection,content_layer,tags,status,version,change_history,ownerfields, 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:
- 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_domainsbaseline values (sectors / products / company / research) should be client-defined. - 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.
- 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.
- Sector-specific scope_tags (Liam’s
internal-it/production-infrastructure/application-layer/office-physical/data-centre-physicalfrom kh-client-feedback Item 3) are CLIENT-DEFINED. They emerged from the Bitdefender + physical-security false-positive examples — Phew-specific concerns.
6.2 — The classification table
Section titled “6.2 — The classification table”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.
| Item | Classification | Evidence (client-doc + line) | WP6 implication |
|---|---|---|---|
| Layer 1 vocabularies | |||
taxonomy_domains rows | HYBRID (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 rows | HYBRID (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 V2 | Product_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_items | CORE (closed) | Pipeline-side provenance — platform-internal concept. | CORE. |
lifecycle_type enum | CORE (closed) | Platform governance vocab. | CORE. |
dedup_status enum | CORE (closed) | Platform state-machine. | CORE. |
freshness enum | CORE (closed) but THRESHOLDS client-configurable | governance_config.freshness_decay_* columns hint at per-domain config. | CORE state vocab; THRESHOLDS per-domain via governance_config. |
governance_review_status enum | CORE (closed) | Platform state-machine. | CORE. |
publication_status enum | CORE (closed) | Per docs/specs/publication-lifecycle-state-machine-spec.md. | CORE. |
change_type enum on content_history | CORE (closed) | Platform audit vocab. | CORE. |
requirement_type enum on template_requirements | HYBRID (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 enum | CORE (closed) | Pipeline-side, platform-internal. | CORE. |
BID_STATES const tuple | CORE — re-namespace as procurement_workflow_states per §3 above | lib/bid/bid-state-machine.ts. Workflow is platform-shipped. | CORE (rename per §3). |
workspaces.type discriminator | CORE for the schema CHECK; HYBRID with provenance for application_types if Option b/c adopted | Current 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 v2 | Liam wants client-extensibility. Provenance pattern from taxonomy_domains applies. | Build with provenance from day 1 (Option c). |
form_type (NEEDED) | HYBRID | Procurement 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-DEFINED | kh-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 rows | HYBRID (category field is client/generic) | Existing column category ∈ {client, generic}. Already encodes this distinction. | Keep; rename category → provenance 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) | CORE | Platform-shipped. | CORE. |
| Descriptive metadata (title, content, brief, detail, reference, summary) | CORE shape; client-DEFINED VALUES | Every client supplies their own content. | Core schema; client content. |
| Administrative metadata (created_at, updated_at, owner, governance) | CORE | Platform-shipped. | CORE. |
| Application-served axis (content_items.application_type tag, per onto-doc §2.2) | HYBRID | Built-in app-types CORE; client-added app-types CLIENT. | Provenance per Option (c). |
| Layer 5 entities | |||
content_items table | CORE | Platform schema. | CORE. |
q_a_pairs table (planned) | CORE | Platform schema. | CORE. |
workspaces table | CORE | Platform schema. | CORE. |
bid_questions / bid_responses | CORE (procurement application) | Platform schema. | CORE. |
source_documents | CORE | Platform schema. | CORE. |
templates / form_templates (planned) | CORE schema; HYBRID instances | Schema 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_requirements | HYBRID | Platform 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_sections | HYBRID | Phew 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_articles | CLIENT-DEFINED INSTANCES, CORE SCHEMA | Each client configures their own RSS sources per Sector-Intelligence-Brief §1.2. | Client manages instances. |
entity_mentions / entity_relationships / entity_aliases | CORE SCHEMA; HYBRID INSTANCES | Entities extracted from content; baseline aliases shipped (per existing category=generic rows); client adds client-specific. | Same as today’s pattern. |
coverage_targets | HYBRID | Per-domain target values may be platform recommendations OR client-set. | Provenance per row. |
governance_config | CLIENT-DEFINED PER DOMAIN | Each client configures their governance posture per governance_config (posture, preset, reviewer, timeout). | Client manages. |
| Layer 6 / KG | |||
| Knowledge Map UI | CORE feature | CX.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 transitions | CORE (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 weights | HYBRID | lib/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) | HYBRID | Per-content_type default + per-item override. | Provenance per default row. |
6.3 — WP6 implication summary
Section titled “6.3 — WP6 implication summary”The Markdown ontology storage (WP6 — when we store the ontology) needs:
- A
provenancefield per CV term with valuescore/client/recommended. Mirrorstaxonomy_domains.provenance. - A
client_extensibleflag 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). - An
editable_viafield per CV —database_migration(closed schema CHECK),admin_ui(client adds via UI),seed_data(replaceable on platform upgrade but baseline-protected). - A
core_seed_pathfield 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_typeslayer: 1provenance_model: hybridclient_extensible: trueeditable_via: admin_uicore_seed_path: scripts/seed/baseline-application-types.sqlbaseline_values: - key: bid label: Procurement / Bid provenance: core - key: intelligence label: Sector Intelligence provenance: core - key: sales_proposal label: Sales Proposal provenance: corerelated_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:
- “Here’s the core platform vocabulary — you don’t need to think about it. It just works.”
- “Here’s the hybrid vocabulary — we’ve shipped sensible defaults; you can extend with your own.”
- “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.
§7 — kb_section retirement plan
Section titled “§7 — kb_section retirement plan”Liam: “The kb_section value in the DB was an error, and should be removed as part of schema cleanup/rework.”
7.1 — Where kb_section appears
Section titled “7.1 — Where kb_section appears”DB CHECK constraint:
supabase/migrations/20260416102457_pre_squash_reconciliation.sql:1966—workspaces_type_checkincludes'kb_section'.
Code references:
lib/workspace-types.ts:88— registered type withavailable: true, routenull, defaults colour#6366f1iconfolder.lib/mcp/tools/workspaces.ts:65-66— MCP tool mapsargs.type === 'content'to'kb_section'(UX rename in flight on MCP side).components/workspace/workspace-create-dialog.tsx:34— defaulttypeprop 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 4intelligenceworkspaces exist).
7.2 — Migration plan
Section titled “7.2 — Migration plan”Phase 1 — Schema migration (DDL):
-- Drop kb_section from CHECK constraintALTER 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_sectionregistration block inlib/workspace-types.ts(lines 87-103). - Change
app/api/workspaces/route.ts:80default 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:34default to whatever the calling context requires (most callers passtypeexplicitly). - Change
lib/mcp/tools/workspaces.ts:65-66to drop the ‘content’-to-‘kb_section’ mapping. - Change
hooks/use-library-bulk-actions.ts:193fallback.
Phase 3 — Data migration: none required (0 prod rows).
7.3 — Risk surface
Section titled “7.3 — Risk surface”- 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.tswill 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 havekb_sectiontest 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.
7.4 — Tie-in to broader rework
Section titled “7.4 — Tie-in to broader rework”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.
7.5 — Recommendation
Section titled “7.5 — Recommendation”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.
§8 — Open questions for Liam
Section titled “§8 — Open questions for Liam”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”| Q | Question | Why it matters |
|---|---|---|
| Q-OQR1-01 | Option (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-02 | Is 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-03 | If 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-04 | Single application_type discriminator (no sub-flavour workspace_subtype) vs dual axis? | KISS recommendation: single. Liam to confirm. |
| Q-OQR1-05 | Where does state-machine code live: per-app-type (lib/procurement/...) vs current lib/bid/...? Rename pre-launch with Q5.5? | Naming-cleanup PR scope. |
8.2 — On q_a_pair cardinality
Section titled “8.2 — On q_a_pair cardinality”| Q | Question | Why it matters |
|---|---|---|
| Q-OQR1-06 | Accept §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-07 | Add 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-08 | Q&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”| Q | Question | Why it matters |
|---|---|---|
| Q-OQR1-09 | Should 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-10 | Does multi-wing content (Mempalace Shape B) need explicit handling, or is “wing-per-assignment, content visible across multiple wings” implicit? | Wing-bridge semantics. |
8.4 — On the core/client dimension
Section titled “8.4 — On the core/client dimension”| Q | Question | Why it matters |
|---|---|---|
| Q-OQR1-11 | Adopt 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-12 | WP6 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-13 | Does 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. |
8.5 — On state-machine handling
Section titled “8.5 — On state-machine handling”| Q | Question | Why it matters |
|---|---|---|
| Q-OQR1-14 | Form_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-15 | When 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. |
8.6 — On schema cleanup PR
Section titled “8.6 — On schema cleanup PR”| Q | Question | Why it matters |
|---|---|---|
| Q-OQR1-16 | Combined 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-17 | Migration 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.
Application/workspace structure
Section titled “Application/workspace structure”| # | Recommendation | Chief trade-off |
|---|---|---|
| §2 Option choice | Option (c) hybrid with provenance | +3-5d migration cost; -code-fork risk for future tenants; +SMB positioning fit; +Talisman schema-as-product alignment. |
| §3 procurement rename | Rename bid → procurement pre-launch | One-time rename pain (~3-5d); lifelong clarity on RFP/PQQ/ITT/framework/DPS/G-Cloud as form_types-within-procurement. |
| §3 state-machine home | State-machine at application_type level; form_type policies via config injection | Slightly more abstraction; pays off when sales_proposal lands with overlapping-but-distinct workflow. |
Q&A pair cardinality (§4)
Section titled “Q&A pair cardinality (§4)”| # | Recommendation | Chief trade-off |
|---|---|---|
| Cardinality | Corpus-level q_a_pairs; no workspace FK; scope_tag-driven workspace relevance | Contradicts 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. |
| Provenance | Add nullable source_workspace_id to q_a_pairs (audit only) | One extra column; standard provenance pattern; doesn’t bind cardinality. |
| Privacy | Defer workspace-private q_a_pairs to v1.1 | Single Phew tenant means private/shared distinction not load-bearing for v1. |
content_items ↔ source_documents (§5)
Section titled “content_items ↔ source_documents (§5)”| # | Recommendation | Chief trade-off |
|---|---|---|
| Relationship | N:1 optional content_items → source_documents; KEEP both source_document_id FK AND cocoindex_source_key engine handle | Two semantically-distinct fields. Both useful. |
| Workspace scoping | Make source_documents.workspace_id nullable | Lets shared binaries serve multiple workspaces; small RLS adjustment. |
| Under Option (c) | No change to the FK relationship | application_type FK on workspaces is transparent to source_documents. |
Core/client dimension (§6)
Section titled “Core/client dimension (§6)”| # | Recommendation | Chief trade-off |
|---|---|---|
| Provenance pattern | Adopt provenance enum across all HYBRID vocabularies, mirroring taxonomy_domains.provenance | One pattern, well-precedented; cleanest mental model. |
| Admin UI scope | Defer client-managed application_types + scope_tags admin UI to v1.1 | v1 ships with Phew config; v1.1 unlocks generic SMB onboarding. |
| WP6 ontology fields | Add provenance_model, client_extensible, editable_via, core_seed_path to ontology Markdown frontmatter | Schema-as-product framing; minor doc-shape addition. |
kb_section retirement (§7)
Section titled “kb_section retirement (§7)”| # | Recommendation | Chief trade-off |
|---|---|---|
| Retirement | Retire kb_section in the combined schema-cleanup PR | Zero 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):
| Doc | Section(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 rows | Mark “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 order | Phase 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 row | Update 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 cardinality | Update 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.md | Q1.13 row | RESOLVED-SHAPE-B with application_type qualifier. |
0.9-decision-graph.md | Q3.11 row | Re-confirm form_templates rename target post-procurement-rename decision. |
lib/workspace-types.ts | Whole file | Becomes either seed data for application_types table (Option c) or stays as registry (Option a). |
| Migration sequence | New migration | application_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.