Skip to content

Content Management — User Journeys

Last verified: Session 215 W4 T9 (30 April 2026). S215 update: Journey 5 gains Markdown Batch Ingest UI sub-section (EP2 §1.11) + Publication Review tab sub-journey (§5.2-P4B); Entry Points table grows with two new rows (Markdown Batch Ingest + Publication Review). S210 update: refreshed for S195-S209. Added S198 §1.5 Q&A inline editor (Tiptap ContentEditor for answer_standard/answer_advanced), S197 §1.19 Source Information accordion (content-type-aware via SourceMetadata), S197 §1.20 browse persona prompt cards (discriminated-union: filter / search / chipComposite), S206 review cadence editor on metadata sidebar, publication_status lifecycle (§5.2). Prior baseline: S188 (22 April 2026).

Content management provides the primary interface for adding, viewing, editing, and organising knowledge base content. Users interact with content through browsing, creation, detail views, and reader modes. Role-based permissions determine which actions are available at each step.

Entry PointRouteAccessible ByPurpose
Browse/Content List/browseAll rolesFind and filter content
Create Content/item/newEditor, Admin4-tab content creation
Item Detail/item/[id]All rolesView and edit single item
Reader ViewVia item detailAll rolesDistraction-free reading
Source DocumentsVia item detailAll rolesView source file and versions
Batch Q&A Import/item/new (batch tab)Editor, AdminBulk question-answer import
Markdown Batch Ingest/item/new (upload tab; multi-.md)Editor, AdminEP2 §1.11 — drag-drop multiple .md files; pre-flight + import phases; Pattern E polling for progress
Publication Review/review?tab=publication-reviewEditor, Admin§5.2-P4B — triage publication_status='in_review' items (which EP2 markdown imports land in for final content)
MCP create toolClaude Desktop / .aiEditor, AdminProgrammatic create (typed provenance)

Actor: All roles Goal: Locate specific content items using filters, search, and persona prompt cards Preconditions: User is authenticated

  1. Navigate to browse view

    • Route: /browse
    • Components: components/browse/
    • What the user sees: Persona prompt cards (S197 §1.20), filter bar with domain/subtopic/content-type/platform/coverage-layer filters, sort controls, grid/list toggle, search bar.
  2. Cold-start: persona prompt cards (S197 §1.20)

    • Action: Click a prompt card on first visit (before any filters applied)
    • Card kinds — discriminated-union data model:
      • kind: 'filter' (8 cards) — applies filter URL params (domain, subtopic, content type, freshness, coverage layer)
      • kind: 'search' (4 cards) — fires semantic search with the card title and adds an entry via addRecentSearch
      • kind: 'chipComposite' (1 card — “Browse by domain”) — renders 3 example domain chips (top-3 by content_items.primary_domain count cached 24h via hooks/browse/use-top-domains.ts) plus a “more” action
    • Q&A card folds OPS-22 — clicking it IS the Q&A toggle
    • Result: List updates immediately based on the card kind
  3. Apply filters

    • Action: Select domain, subtopic, content type, freshness status, workspace, or tags via the filter panel
    • Components: filter-bar, filter-panel, filter-section, multi-select domain/subtopic/content-type/platform/coverage-layer filters
    • Hook: useBrowseFilters, useFilterDraft, useFilterPresets
    • Result: Content list updates to show matching items; filter badges summarise active filters
  4. Save / load filter preset

    • Action: Click Save Preset — preset name dialog opens (save-preset-dialog)
    • Result: Preset stored and shown in preset-bar; reusable via single click; manageable via manage-presets-dialog
  5. Select content item

    • Action: Click on a content card or row
    • Component: content-card (mounts FreshnessBadge, ReviewCadenceBadge (S206), GovernanceBadge, QualityBadge, DomainBadge, VerificationBadge, PriorityBadge, StarButton)
    • Result: Navigates to item detail view at /item/[id]
  • Viewer: Can browse and read but cannot edit or create. AI-mechanism fields (model names, reasoning, tokens) are not visible; classification confidence is not visible.
  • Editor: Sees inline edit affordances, QuickReviewActions, classify content button, classification confidence in the Source Information accordion (admin/editor-only carve-out).
  • Admin: Sees all editor actions plus delete, supersede, bulk-assign-owner, recalculate-all-freshness.
  • Empty filter results show a helpful empty state with suggestions.
  • Content with archived_at set is hidden from default browse; accessible via the archive filter.
  • Items with publication_status='draft' are excluded from default browse (visible only in the Drafts review filter).
  • Items with superseded_by != null are excluded from default search (MCP retrieval defaults include_superseded=false).

Actor: Editor, Admin Goal: Add a new content item to the knowledge base Preconditions: User has Editor or Admin role

  1. Start creation flow

    • Route: /item/new
    • Components: components/create-content/, app/item/new/new-item-tabs.tsx (4-tab container)
    • What the user sees: Tabbed interface with four tabs — Write (manual form), URL (web import), Upload (file), Batch (Q&A bulk). Write is the default.
  2. Select template (optional)

    • Action: Choose from available content templates
    • Component: template-selector; Hook: use-content-templates
    • Result: Form fields pre-populated based on template
  3. Fill in content fields

    • Action: Enter title, content body, content type, classification fieldset (domain/subtopic), provenance fieldset (source_url / source_file / source_document_id), progressive depth (brief/detail/reference).
    • Mobile: MobileStepIndicator provides step navigation on small screens
    • Validation: ItemCreateBodySchema (Zod) validates on submit. Optional fields include skip_dedup (admin-only override; silent-ignore for non-admin) and content_owner_id (admin override).
  4. Submit

    • Action: Click create
    • API: POST /api/items
    • Pipeline: validate → embed (synchronous for auto_embed=true) → dedup soft-block → INSERT (with typed ingest_source='manual', resolved content_owner_id, optionally publication_status='draft') → v1 history (DB trigger) → chunk (non-draft only) → classify → summarise → layer inference → quality score → topic suggestion → guide section suggestion
    • Result: 201 with id, warnings array, dedup stamp, suggested layer, topic suggestion, guide section suggestions.
  • Q&A content type: Shows additional fields for answer_standard and answer_advanced. The route populates answer_standard from content so the first PATCH edit doesn’t destroy creation content (Path 1 ss4.6 fix).
  • Save as draft: Toggling the Save-as-draft control sets publication_status='draft'. Drafts are excluded from search and only visible in the Drafts review filter. Embedding generation may be deferred via auto_embed=false.
  • Admin dedup override: Admin caller can set skip_dedup: true to bypass the suspected-duplicate stamp. Non-admin callers’ overrides are silently ignored (no 403).
  • Admin owner override: Admin caller can set content_owner_id to any UUID. Non-admin overrides silently force to caller’s userId via resolveContentOwnerId({ explicit, role, userId }).
  • Duplicate detection may stamp dedup_status='suspected_duplicate' (MD5 exact match). The insert still succeeds (soft-block).
  • Pipeline steps that fail non-fatally return in the warnings[] array rather than blocking creation.
  • content_text_hash is GENERATED ALWAYS — never write it explicitly (PG rejects with cannot insert a non-DEFAULT value into column).

Actor: All roles (view); Editor/Admin (edit) Goal: Review content details and make inline edits Preconditions: Content item exists

  1. Open item detail

    • Route: /item/[id]
    • Components: components/item-detail/
    • Hook: use-item-detail-data (master data hook)
    • What the user sees: ItemTitleSection, ItemActionBar (Star, Share, Supersede, Delete), ContentBody, MetadataSidebar (Quality Score Breakdown, Review Schedule sub-block via ReviewCadenceEditor, Domain/Subtopic, Owner, Layer/Topic), ContentTabs and CollapsibleSection providing tabbed/expandable sub-layout for Provenance, Relationships, Organisation panels, related-content section (RelatedContentSection, RelatedByTags, RelatedByEntities), EntityBadges, TemporalReferencesSection, TableOfContents, the reader-view panel, VerificationHistory, VersionHistory / VersionDiff panels, and the Source Information accordion at the bottom of the reader (S197 §1.19, content-type-aware).
  2. Edit content body (Editor/Admin only)

    • Action: Click Edit on the content body
    • Component: inline-content-editor (dynamically-imported Tiptap ContentEditor with GFM tables)
    • Hook: use-inline-field-edit
    • API: PATCH /api/items/[id] with { field, value, regenerate_embedding?, reclassify?, change_reason? }
    • Result: Content updates; embedding regenerated if requested; new content_history row created with author + change summary
  3. Edit Q&A answer fields (Editor/Admin only — S198 §1.5)

    • Action: Click Edit on the Standard or Advanced panel (qa-answer-display, stable testid qa-answer-panel-{standard,advanced})
    • Component: QAInlineEditor — mounts the same Tiptap ContentEditor with GFM tables (single-field-at-a-time)
    • Per-field save-safety guard: ≥20 % shrink against the per-field baseline blocks the save
    • Per-field regen-embedding checkbox in the editor footer threads regenerate_embedding: true into the PATCH body. The flag resets on startEdit / cancelEdit / successful save so it cannot leak across fields.
    • Result: PATCH preserves the Q: {question}\n\n prefix in the rebuilt content_items.content. Read mode renders both fields via QAPairRenderer → ContentRenderer.
  4. Edit a single metadata field (Editor/Admin only)

    • Action: Click an editable field in the sidebar (priority, layer, governance review status, owner, tags, lifecycle, etc.)
    • API: PATCH /api/items/[id] with { field, value }
    • Result: Field updates in place. Embedding is regenerated if a content- affecting field changes; reclassification is NOT auto-triggered (warns the user to call /classify separately).
  5. Edit publication status (Editor/Admin only)

    • Action: Use the publication status control to transition between draft, in_review, published, archived
    • API: PATCH /api/items/[id] with { field: 'publication_status', value, fromStatus }
    • Optimistic-concurrency guard: the route filters .eq('publication_status', fromStatus) and returns 409 PGRST116 on stale write
    • Allowed transitions are computed by lib/governance/publication-transitions.ts::computeAllowedTransitions( fromStatus, role) — the role’s allowed newStatus array is derived from fromStatus.
    • Side effects via applyTransitionSideEffects(...) — e.g. archiving populates archived_at/archived_by/archive_reason and the bidirectional enforce_archive_state_consistency trigger keeps the invariant in sync.
  6. Edit recurring review cadence (Editor/Admin only — S206)

    • Action: Open the Review Schedule sub-block in the metadata sidebar
    • Component: ReviewCadenceEditor<input type="date"> for next review date + 5-preset Select (No recurring review / Every 3 months (90) / Every 6 months (182) / Every 12 months (365) / Custom 1-1095)
    • API: PATCH /api/items/:id with field='next_review_date' or 'review_cadence_days'
    • Result: Updates persist; rollback on network failure restores the last-persisted value via lastPersistedDateRef.
    • ReaderView callers pass readOnly=true — the editor is hidden.
  7. View version history

    • Action: Open the History panel
    • API: GET /api/items/[id]/history
    • What the user sees: Chronological list of versions with change summaries, change reason (e.g. initial_ingest, auto_v1_on_insert, owner_change), and author.
  8. Rollback to previous version (Editor/Admin only)

    • Action: Select a version and confirm rollback
    • API: POST /api/items/[id]/rollback
    • Result: New version created with content from selected version; content_history entry with change_type='rollback'. Does not modify governance_review_status or publication_status.
  9. Mark superseded (Admin only)

    • Action: Open the Supersede dialog (supersede-content-dialog mounted on item-action-bar)
    • API: PATCH /api/items/:id with { field: 'superseded_by', value: uuid|null } — null clears the pointer (un-supersede)
    • Result: Old row is hidden from default search; superseded_by FK points to new row; dedup_status transitions to 'superseded'. Direct ID lookup still returns the row so the UI can surface “this item has been replaced by ”.
  • Read-only roles: Viewer sees content + read marks + reader view but no edit affordances. AI-mechanism fields are hidden across the entire detail page.
  • Keyboard shortcuts: use-item-detail-shortcuts enables power-user navigation.
  • Source Information accordion (S197 §1.19): Renders content-type-aware fields:
    • Q&A pairs → source_file raw + section_name + “Imported on DD/MM/YYYY”
    • Markdown items → ingestion-source label (“Markdown upload”, “URL import”, “File upload”, “Auto-split upload”, “Manual entry”, “Procurement library import”, “RSS feed”)
    • Feed articles → feed name + published date via feed_articles → feed_sources join
    • PDFs → PdfFields (page count + extraction quality)
    • Email → EmailFields
    • Default → GenericArticleFields
  • AI-visibility carve-out: Editors and admins see classification_confidence as a plain-text percentage row. Viewers see nothing. The accordion is collapsed by default and has no AI branding, colour coding, or accompanying mechanism fields.
  • PATCH updates that change content warn about potential classification staleness but do not auto-reclassify.
  • Concurrent edits: last write wins for most fields; publication_status uses optimistic concurrency (409 PGRST116 on stale write).
  • Saving a Q&A answer with a ≥20 % shrink against the baseline is blocked client-side.

Actor: All roles Goal: Read content in a distraction-free format Preconditions: Content item exists

  1. Open reader from item detail

    • Action: Click the reader view toggle on the item detail page
    • Components: components/reader/
    • What the user sees: Clean reading layout appropriate to content type
  2. Content-type-specific rendering

    • PDF: pdf-reader-view / pdf-viewer with image-gallery for extracted images
    • Web content: iframe-viewer for original source, reader-view for extracted markdown
    • Transcripts: transcript-reader for audio/video transcription content
    • All types: source-metadata panel renders the content-type-aware Source Information accordion (S197 §1.19)
  3. Mark as read

    • Action: Automatic or manual read mark
    • API: POST /api/read-marks (UPSERT idempotent)
    • Result: Read status tracked per user per item
  • Reader panel: reader-panel for side-by-side reading alongside other content. The floating overlay reader was removed in S183 P1-7 (react-rnd dependency dropped).

Actor: Editor, Admin Goal: Import content from files or URLs Preconditions: User has Editor or Admin role

Steps — File Upload (ingest_source='upload')

Section titled “Steps — File Upload (ingest_source='upload')”
  1. Select file

    • Route: /item/new (upload tab)
    • Component: file-upload
    • Hook: use-file-upload-pipeline
    • Supported: PDF, DOCX, MD, TXT (max 50MB)
  2. Upload and process

    • API: POST /api/upload (multipart form-data; admin can pass skip_dedup=true and/or content_owner_id UUID via form fields)
    • 6-step pipeline tracked in pipeline_runs: validate magic bytes → create item (with typed ingest_source='upload') → upload to storage → create source_documents row + link via source_document_id FK → extract text (PDF: unpdf; DOCX: mammoth → Turndown gfm; MD/TXT: direct read) → date extraction → dedup soft-block → embed → chunk → classify → summarise → quality score → layer + topic + guide section suggestions
    • Detect re-upload: detect_reupload(filename, uploaded_by, content_hash) RPC may flag match_type='identical' or 'new_version' (sets parent_id on the new source_documents row)
    • What the user sees: IngestionProgress indicator showing the 6 steps; IngestionSuccessCard on completion
  3. Review result

    • Result: New content item created with linked source document
    • Warnings: Dedup matches, extraction quality issues, expiry-date auto- detection (when found, sets expiry_date + lifecycle_type='date_bound')

Steps — Markdown Batch Ingest UI (EP2 §1.11; ingest_source='upload')

Section titled “Steps — Markdown Batch Ingest UI (EP2 §1.11; ingest_source='upload')”
  1. Drop multiple .md files

    • Route: /item/new (upload tab) — same tab as the EP3 single-file surface; routing diverges on file count + extension (all .md and count >1 → batch route; mixed types or single non-.md → EP3 route)
    • Component: extension to components/create-content/upload-tab-content.tsx (detect .md-only multi-file → call /api/ingest/markdown)
    • Limits: ≤10 files, ≤1 MB per file, ≤5 MB total batch, UTF-8 only
  2. Pre-flight analysis (analyse phase)

    • API: POST /api/ingest/markdown with phase=analyse
    • The orchestrator runs front-matter parse + title extraction + diff-marker scan + dedup pre-check + filename-collision check on each file. NO DB writes.
    • User sees an analysis table with per-row flags: front_matter parse status, computed title + provenance, dedupVerdict.{isDuplicate, ...}, sourceFileMatch, diff_markers.warning, draftOrFinalHeuristic, empty. Per-row checkboxes let editors override draft_or_final and mark files for excluded=true.
  3. Import (import phase)

    • User clicks “Import”; UI POST /api/ingest/markdown with phase=import and the per-row overrides JSON
    • Pattern E end-to-end: route blocks for the import duration (~80-100s for 10 files); UI fires GET /api/pipeline-runs/:id every 1-2s in parallel to surface mid-flight progress (“Processing foo-final.md…”)
    • Per-file pipeline (sequential): cleanMdxTags() → dedup pre-check → INSERT content_items with publication_status per D-A (draft/unknown → 'draft'; final → 'in_review') → classifyContent()regenerateChunks() → push to stored[] and conditionally dedup_flagged[]
  4. Review post-flight summary

    • Result: Per-file outcome card showing stored[], dedup_flagged[], superseded[] (forward-compat reserved), skipped_excluded[], errored[]. Note: dedup_flagged[] is a SUBSET of stored[] — soft-blocked rows ARE inserted with dedup_status='suspected_duplicate'.
    • Items flagged final land in publication_status='in_review' and are EXCLUDED from default hybrid_search until an admin/editor transitions them to 'published' via the publication-review tab on /review (Journey 5b below).

Steps — Publication Review tab (Journey 5b — admin/editor)

Section titled “Steps — Publication Review tab (Journey 5b — admin/editor)”

After EP2 markdown ingest with final-flagged content (or any other path that creates publication_status='in_review' items), admins and editors triage them via the publication-review tab:

  1. Navigate to /review?tab=publication-review (the 6th tab in the Radix Tabs surface; default tab is verified-review)
  2. Surface lists items where publication_status='in_review', newest first by default.
  3. Per-row actions: Approve & publish (PATCH publication_status='published', item joins default search/browse), Return to draft (PATCH 'draft', item stays out of default search), Open in editor (link to /item/[id]).
  4. Both transitions allowed for admin AND editor per lib/governance/publication-transitions.ts:75-80.

Steps — URL Import (ingest_source='url_import')

Section titled “Steps — URL Import (ingest_source='url_import')”
  1. Enter URL

    • Route: /item/new (URL tab)
    • Component: url-ingest-form
  2. Fetch and process

    • API: POST /api/ingest/url (10/min rate limit; admin can pass skip_dedup and/or content_owner_id)
    • Pipeline: SSRF validate → URL identity dup check (returns url_already_exists with existing item ref if matched) → fetch + extract via Readability + Turndown gfm → quality check (< 100 chars → 422; < 500 → warning) → embed → dedup soft-block → INSERT (with typed ingest_source='url_import', source_url, source_domain, platform=‘web’, thumbnail_url, author_name) → v1 history (trigger) → classify → summarise → quality → inference
    • Result: Content item created from web page content
  • MCP create (ingest_source='mcp_create'): Programmatic create via create_content_item tool. Supports the typed provenance triple (source_url, source_file, source_document_id); legacy metadata.source_document Zod input is rejected. Audit row emitted to pipeline_runs (pipeline_name='mcp_create_content_item') on success / partial / draft / auth-fail / catch-all paths via service-role client (S208 OPS-40).
  • Python URL ingest (ingest_source='python_url'): CLI python3 scripts/ingest.py <url> or --file urls.txt for batch.
  • Python markdown ingest (ingest_source='python_markdown'): CLI python3 scripts/ingest_markdown.py <dir> (supports --dry-run, --limit, --skip-existing, --tag, --author, --auto-supersede, --auto-supersede-dry-run).
  • SSRF validation blocks private/internal URLs (/api/ingest/url).
  • DOCX magic-byte validation rejects spoofed extensions (PK\x03\x04 ZIP signature check).
  • PDF files over 32MB can be uploaded but not sent to Anthropic Files API for vision analysis.
  • Re-upload of existing source documents triggers version detection via detect_reupload RPC and creates a new source_documents row with parent_id linking back to the previous version.
  • metadata.source_document is preserved on disk (legacy JSONB key) but new writes must go to typed columns.

Actor: Editor, Admin Goal: Import multiple question-answer pairs at once Preconditions: Q&A data prepared

  1. Enter batch data

    • Hook: use-batch-create
    • Component: batch-qa-preview-table
    • What the user sees: Batch input + preview table
  2. Submit batch

    • API: POST /api/items/batch (maxDuration 120s)
    • Auth: Single-use batch token prevents duplicate submissions
    • Pipeline per item: INSERT with typed ingest_source='upload_autosplit' → v1 history (trigger) → embed → classify → summarise → inference → quality
  3. Monitor progress

    • Result: Pipeline run tracked in pipeline_runs table (pipeline_name='batch_create')
    • What the user sees: Progress updates as items process sequentially

Steps — Python form-library import (ingest_source='qa_import')

Section titled “Steps — Python form-library import (ingest_source='qa_import')”

Stale (flagged for docubot lane): the scripts/kb_pipeline/ Python ingest referenced in this sub-journey (and the former import_bid_library.py entry point) has been superseded by the cocoindex pipeline (scripts/cocoindex_pipeline/); the per-script paths below predate that migration. The ingest_source='qa_import' key literal is unchanged.

  1. CLI invocation

    • The form-library (Q&A) import path reads .docx files via a Track-Changes-safe document opener and extracts Q&A tables from them.
  2. Pipeline

    • For each Q&A pair: dedup check via check_content_hash_duplicate() + normalise_title_for_dedup() (collapses leading articles for cross- file variant detection) → INSERT (typed ingest_source='qa_import') → v1 history (trigger) → post-insert helper (chunks, entity aliases, entity_mentions, entity_relationships, temporal references, layer inference)

Journey 7: Manage Source Document Versions

Section titled “Journey 7: Manage Source Document Versions”

Actor: Editor, Admin Goal: Compare document versions and review changes Preconditions: Source document with multiple versions exists

  1. View version chain

    • API: GET /api/source-documents/[id]/versions (via get_document_version_chain RPC)
    • Components: source-document-history, source-document-info
    • What the user sees: Version timeline with parent-child relationships (parent_id FK on source_documents)
  2. Compute and review diff

    • API: POST /api/source-documents/[id]/diff
    • Hook: use-diff-review
    • Component: source-document-diff-review, diff-highlighted-text
    • Diff modes: Q&A mode (Dice coefficient) or full-text (Myers algorithm)
    • What the user sees: Side-by-side or inline diff view
  3. Take action on diff

    • Actions: Applied, dismissed, or pending_review
    • API: PATCH /api/source-documents/[id]/diff
  4. Send to governance review (if needed)

    • API: POST /api/source-documents/[id]/send-to-review
    • Result: Hands off to quality-governance feature area; banner shown via reupload-banner

ActionViewerEditorAdmin
Browse contentYesYesYes
Read content / readerYesYesYes
View historyYesYesYes
View layersYesYesYes
View effectivenessYesYesYes
Mark as readYesYesYes
Batch fetch workspacesYesYesYes
See classification confidence (Source Information)NoYesYes
Create contentNoYesYes
Edit content (inline)NoYesYes
Edit Q&A answer fieldsNoYesYes
Set / change publication_statusNoYes (transitions per role)Yes
Set review cadenceNoYesYes
Upload / URL importNoYesYes
Batch Q&A importNoYesYes
Archive contentNoYesYes
Rollback versionNoYesYes
Classify / summariseNoYesYes
skip_dedup admin overrideNoNo (silent-ignored)Yes
content_owner_id overrideNoNo (silent-forced to userId)Yes
Delete contentNoNoYes
Supersede contentNoNoYes
Bulk assign ownersNoNoYes
Recalculate all freshnessNoNoYes
MCP update_publication_status to archived/publishedNoPer-transitionPer-transition
  1. No optimistic locking on most fields — concurrent edits use last-write- wins. publication_status is the exception (409 PGRST116 on stale write).
  2. Expiry date has no edit UI — two-store architecture with no synchronisation.
  3. Content templates are code constants, not yet configurable via the content_templates table.
  4. Batch review is limited to setting ‘pending’ status only.
  5. Q&A content field is auto-rebuilt from answer_standard + answer_advanced — direct edits to content for Q&A items are overwritten on the next save.
  6. Saved filter presets are scoped to the user; there is no shared/team preset surface.
  • Content Management — Technical Reference
  • Content Management — Workflows
  • Quality and governance workflows are documented in the quality-governance feature area
  • Classification and taxonomy are documented in the knowledge-organisation feature area
  • AI-visibility policy: docs/reference/ai-visibility-policy.md
  • Schema reference: docs/reference/SCHEMA-QUICK-REFERENCE.md