Data Design¶
How RCARS stores, relates, and scores content. Read this to understand the data model conceptually, then point an AI agent at the full schema reference at the bottom for implementation details.
Content Model¶
Everything in RCARS is a content entity -- a source-agnostic record representing a piece of RHDP content. The system stores labs, demos, sandboxes, and (in the future) architectures, interactive experiences, or content from non-Babylon sources.
content_entities babylon_items (1:1 extension)
┌──────────────────┐ ┌──────────────────────┐
│ content_id (PK) │◄─────│ content_id (PK, FK) │
│ source │ │ ci_name (UNIQUE) │
│ content_type │ │ stage, category │
│ display_name │ │ showroom_url/ref │
│ summary │ │ cloud_provider │
│ products_json │ │ scan_status │
│ retired_at │ │ ...30 more fields │
│ ... │ └──────────────────────┘
└──────────────────┘
Two-table design¶
content_entitiesholds fields that apply to any content source: identity (content_id,source,content_type), card-level metadata (display name, summary, products, topics, audience, difficulty), and lifecycle state (retired_at).babylon_itemsextendscontent_entities1:1 for Babylon-sourced content. It holds everything CRD-specific:ci_name, stage, category, Showroom URL/ref, cloud provider, OCP version, AgnosticD config, scan status, and infrastructure metadata.
Why this split¶
The old catalog_items table baked Babylon assumptions into the primary key (ci_name) and mixed source-specific fields with universal ones. The normalized model lets RCARS ingest content from other sources (Confluence, external labs, architecture docs) without schema changes to the core entity table -- just add a new extension table alongside babylon_items.
content_id¶
Every entity has a stable content_id as its primary key -- a text identifier with a source prefix:
- Babylon items:
babylon:<ci_name>(e.g.,babylon:ocp4-getting-started.prod)
All foreign keys throughout the schema reference content_entities.content_id. The old ci_name is preserved on babylon_items as a UNIQUE column for backward compatibility and CRD correlation, but it is never used as a foreign key.
Content types and sources¶
source: Where the content came from (babylon, and future values likeconfluence,external)content_type: What kind of content it is (lab,demo,sandbox, and future values likearchitecture,interactive_experience)
Soft-delete and retirement¶
Items that disappear from Babylon CRDs get content_entities.retired_at = NOW() instead of being deleted. All active-item queries filter on retired_at IS NULL. Items that reappear in a future scan are automatically un-retired (set retired_at = NULL).
Analysis and Embeddings¶
Content analysis¶
When a content entity has Showroom content (labs and demos), the scan pipeline clones the repo, reads the AsciiDoc, and sends it to an LLM for structured analysis. Results are stored in showroom_analysis (1:1 with content_entities):
- Structured fields: summary, products, topics, audience, difficulty, modules, learning objectives, duration estimate, format suitability, use cases
- Change detection:
content_hashtracks the SHA of the analyzed content;is_staleflags when the repo has new commits - Curator enrichment:
notes(free text),curated_duration_min(manual override),review_reasons(vocabulary flagging)
Embeddings¶
After analysis, RCARS generates 768-dimensional vector embeddings using the nomic-embed-text-v1.5 model, served by a dedicated vLLM HTTP server (OpenAI-compatible /v1/embeddings API).
Each content entity can have multiple embeddings in the embeddings table:
embed_type |
What it embeds | Used for |
|---|---|---|
summary |
The full analysis summary | Advisor vector search, content overlap |
module |
Individual module titles + descriptions | Fine-grained matching |
Key columns:
content_typeandsource: Enable filtered searches (e.g., only search labs, or only Babylon content)content_text: The original text that was embedded (for debugging and re-embedding)
Nomic requires task prefixes: search_document: when indexing content, search_query: when embedding a user query. RCARS applies these automatically.
Performance and Retirement¶
Performance data¶
Performance metrics flow from the RHDP Reporting MCP Server into two tables:
performance_channels: Raw metrics per (content_id, channel). One row per content entity per data channel (e.g.,rhdpfor provisioning data). Stores provisions, unique users, cost, pipeline touched/closed amounts, success ratios, andwindowed_metrics(JSONB with time-bucketed snapshots for 3m/6m/9m/12m views).performance_scores: Computed performance score per content entity. Stores the overallperformance_score(0-100 percentile, higher = better),score_breakdown(JSONB with component scores), andignored_until(curator mute).
This replaces the old monolithic reporting_metrics table, separating raw channel data from computed scores.
Retirement workflow¶
The retirement_workflow table tracks the lifecycle of content being retired, keyed by content_id:
Each step has step_<name>_at / step_<name>_by timestamps and actor fields. Additional fields track the approval reason, a snapshot of the item's state at approval time, Jira ticket linkage, and curator notes.
Supporting Tables¶
Workloads and ACL¶
babylon_item_workloads: Maps content entities to infrastructure workload roles (Ansible roles deployed by the content). Joined withworkload_mappingto resolve human-readable product names.babylon_item_acl_groups: Tracks which ACL groups have access to each content entity.workload_mapping: Reference table mappingworkload_role→product_namewith verification status.workload_aliases: Alternative names for workload products (for search flexibility).workload_scan_state: Tracks the last scanned commit SHA per Ansible collection (for incremental scanning).
Enrichment and curation¶
enrichment_tags: Curator-applied tags (tag_type + tag_value) on content entities -- audience, use-case, and custom classifications.content_similarity: Pairwise cosine similarity scores between content entities, computed from summary embeddings. Used by the content overlap analysis.
Sessions and jobs¶
advisor_sessions: Query history -- each row is one turn in an advisor conversation. Tracks the query, intent, results (including the full chat envelope and scope), and which recommendation the user chose (chosen_content_id).jobs: Async job tracking for long-running operations (scans, refreshes, queries). Stores status, progress, and results.
Auth and operational¶
api_keys: External API authentication -- hashed keys with prefixes, roles, scopes, and expiry.analysis_log: Audit trail of curator actions and system events.token_usage: LLM token consumption tracking per operation, model, and provider. Theprovidercolumn distinguishes between backends (e.g.,anthropic,litemaas).
Schema Management¶
The schema is defined as SCHEMA_SQL in src/api/rcars/db/database.py -- this is the single source of truth. There is no Alembic or external migration tool.
- Fresh installs: All tables use
CREATE TABLE IF NOT EXISTS - Adding columns: Append
ALTER TABLE <table> ADD COLUMN IF NOT EXISTS <col> <type>at the bottom ofSCHEMA_SQL - Structural changes: Run
rcars init-db --dropto drop and recreate all tables (destructive -- requires data re-population via pipelines)
rcars init-db runs create_schema() on every deploy. The entire block is idempotent.
Full Schema Reference¶
The complete CREATE TABLE statements from SCHEMA_SQL. This is what an AI agent should parse for column-level details.
-- ═══════════════════════════════════════════════════════════════════
-- content_entities — universal entity registry
-- ═══════════════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS content_entities (
content_id TEXT PRIMARY KEY,
source TEXT NOT NULL,
content_type TEXT NOT NULL,
is_hands_on BOOLEAN NOT NULL DEFAULT FALSE,
display_name TEXT NOT NULL,
summary TEXT,
products_json JSONB,
topics_json JSONB,
audience_json JSONB,
difficulty TEXT,
retired_at TIMESTAMPTZ,
retirement_reason TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- ═══════════════════════════════════════════════════════════════════
-- babylon_items — Babylon-specific extension (1:1 with content_entities)
-- ═══════════════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS babylon_items (
content_id TEXT PRIMARY KEY REFERENCES content_entities(content_id) ON DELETE CASCADE,
ci_name TEXT NOT NULL UNIQUE,
category TEXT,
stage TEXT,
catalog_namespace TEXT,
is_prod BOOLEAN DEFAULT FALSE,
is_published BOOLEAN DEFAULT FALSE,
published_ci_name TEXT,
base_ci_name TEXT,
showroom_url TEXT,
showroom_ref TEXT,
content_path TEXT,
showroom_url_override TEXT,
is_agd_v2 BOOLEAN DEFAULT FALSE,
agd_config TEXT,
cloud_provider TEXT,
ocp_version TEXT,
os_image TEXT,
worker_instance_count TEXT,
control_plane_instance_count TEXT,
instances_json JSONB,
keywords TEXT[],
description TEXT,
owners_json JSONB,
icon_url TEXT,
last_crd_update TIMESTAMPTZ,
last_refreshed TIMESTAMPTZ DEFAULT NOW(),
scan_status TEXT NOT NULL DEFAULT 'not_scanned',
scan_error_class TEXT,
scan_error TEXT,
scan_failed_at TIMESTAMPTZ
);
-- ═══════════════════════════════════════════════════════════════════
-- showroom_analysis — LLM-generated content analysis
-- ═══════════════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS showroom_analysis (
content_id TEXT PRIMARY KEY REFERENCES content_entities(content_id) ON DELETE CASCADE,
summary TEXT,
products_json JSONB,
topics_json JSONB,
audience_json JSONB,
difficulty TEXT,
content_hash TEXT,
last_analyzed TIMESTAMPTZ,
is_stale BOOLEAN DEFAULT FALSE,
stale_commit TEXT,
content_type TEXT,
modules_json JSONB,
learning_objectives_json JSONB,
estimated_duration_min INTEGER,
curated_duration_min INTEGER CHECK (curated_duration_min >= 0),
format_suitability_json JSONB,
use_cases_json JSONB,
last_repo_commit TEXT,
last_repo_updated TIMESTAMPTZ,
enrichment_review_needed BOOLEAN DEFAULT FALSE,
review_reasons JSONB,
notes TEXT
);
-- ═══════════════════════════════════════════════════════════════════
-- embeddings — 768-dim vectors from nomic-embed-text-v1.5 via vLLM
-- ═══════════════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS embeddings (
id SERIAL PRIMARY KEY,
content_id TEXT NOT NULL REFERENCES content_entities(content_id) ON DELETE CASCADE,
content_type TEXT NOT NULL,
source TEXT NOT NULL,
embed_type TEXT NOT NULL,
module_title TEXT,
content_text TEXT,
embedding vector(768)
);
-- ═══════════════════════════════════════════════════════════════════
-- performance_channels — per-channel performance metrics
-- ═══════════════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS performance_channels (
id SERIAL PRIMARY KEY,
content_id TEXT NOT NULL REFERENCES content_entities(content_id) ON DELETE CASCADE,
channel TEXT NOT NULL,
provisions INTEGER DEFAULT 0,
unique_users INTEGER DEFAULT 0,
requests INTEGER DEFAULT 0,
page_views INTEGER DEFAULT 0,
downloads INTEGER DEFAULT 0,
completions INTEGER DEFAULT 0,
pipeline_touched NUMERIC,
closed_amount NUMERIC,
marketing_spend NUMERIC,
total_cost NUMERIC,
avg_cost_per_provision NUMERIC,
success_ratio NUMERIC,
first_activity DATE,
last_activity DATE,
windowed_metrics JSONB DEFAULT '{}'::jsonb,
synced_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(content_id, channel)
);
-- ═══════════════════════════════════════════════════════════════════
-- performance_scores — computed retirement scores
-- ═══════════════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS performance_scores (
content_id TEXT PRIMARY KEY REFERENCES content_entities(content_id) ON DELETE CASCADE,
performance_score INTEGER NOT NULL DEFAULT 0,
score_breakdown JSONB,
channel_scores JSONB,
computed_at TIMESTAMPTZ DEFAULT NOW(),
ignored_until DATE
);
-- ═══════════════════════════════════════════════════════════════════
-- retirement_workflow — retirement lifecycle tracking
-- ═══════════════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS retirement_workflow (
content_id TEXT PRIMARY KEY REFERENCES content_entities(content_id) ON DELETE CASCADE,
status TEXT NOT NULL DEFAULT 'reviewed',
step_reviewed_at TIMESTAMPTZ,
step_reviewed_by TEXT,
step_approved_at TIMESTAMPTZ,
step_approved_by TEXT,
approval_reason TEXT,
approval_snapshot JSONB,
step_notified_at TIMESTAMPTZ,
step_notified_by TEXT,
step_started_at TIMESTAMPTZ,
step_started_by TEXT,
retirement_target_date DATE,
step_retired_at TIMESTAMPTZ,
replacement_ci TEXT,
replacement_name TEXT,
curator_notes TEXT,
jira_key TEXT,
jira_project TEXT NOT NULL DEFAULT 'RHDPCD',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- ═══════════════════════════════════════════════════════════════════
-- content_similarity — pairwise embedding similarity
-- ═══════════════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS content_similarity (
id SERIAL PRIMARY KEY,
content_id_a TEXT NOT NULL REFERENCES content_entities(content_id) ON DELETE CASCADE,
content_id_b TEXT NOT NULL REFERENCES content_entities(content_id) ON DELETE CASCADE,
similarity_score REAL NOT NULL,
relationship_type TEXT NOT NULL DEFAULT 'overlap',
computed_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(content_id_a, content_id_b)
);
-- ═══════════════════════════════════════════════════════════════════
-- babylon_item_workloads — workload role associations
-- ═══════════════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS babylon_item_workloads (
id SERIAL PRIMARY KEY,
content_id TEXT NOT NULL REFERENCES content_entities(content_id) ON DELETE CASCADE,
workload_fqcn TEXT NOT NULL,
workload_role TEXT NOT NULL,
workload_collection TEXT,
UNIQUE(content_id, workload_fqcn)
);
-- ═══════════════════════════════════════════════════════════════════
-- babylon_item_acl_groups — ACL group membership
-- ═══════════════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS babylon_item_acl_groups (
id SERIAL PRIMARY KEY,
content_id TEXT NOT NULL REFERENCES content_entities(content_id) ON DELETE CASCADE,
group_name TEXT NOT NULL,
UNIQUE(content_id, group_name)
);
-- ═══════════════════════════════════════════════════════════════════
-- Reference tables
-- ═══════════════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS workload_mapping (
id SERIAL PRIMARY KEY,
workload_role TEXT NOT NULL UNIQUE,
product_name TEXT NOT NULL,
description TEXT,
category TEXT,
source_collection TEXT,
verified BOOLEAN DEFAULT FALSE,
added_by TEXT,
added_at TIMESTAMPTZ DEFAULT NOW(),
verified_at TIMESTAMPTZ
);
CREATE TABLE IF NOT EXISTS workload_aliases (
id SERIAL PRIMARY KEY,
product_name TEXT NOT NULL,
alias TEXT NOT NULL UNIQUE,
added_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS workload_scan_state (
collection TEXT PRIMARY KEY,
last_sha TEXT,
last_scanned TIMESTAMPTZ DEFAULT NOW()
);
-- ═══════════════════════════════════════════════════════════════════
-- Enrichment and curation
-- ═══════════════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS enrichment_tags (
id SERIAL PRIMARY KEY,
content_id TEXT NOT NULL REFERENCES content_entities(content_id) ON DELETE CASCADE,
tag_type TEXT NOT NULL,
tag_value TEXT NOT NULL,
added_by TEXT,
added_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(content_id, tag_type, tag_value)
);
-- ═══════════════════════════════════════════════════════════════════
-- Operational tables
-- ═══════════════════════════════════════════════════════════════════
CREATE TABLE IF NOT EXISTS analysis_log (
id SERIAL PRIMARY KEY,
ci_name TEXT,
action TEXT NOT NULL,
user_id TEXT,
details TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS token_usage (
id SERIAL PRIMARY KEY,
operation TEXT NOT NULL,
model TEXT NOT NULL,
ci_name TEXT,
query_text TEXT,
input_tokens INTEGER NOT NULL DEFAULT 0,
output_tokens INTEGER NOT NULL DEFAULT 0,
provider TEXT DEFAULT 'anthropic',
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS jobs (
id TEXT PRIMARY KEY,
job_type TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'queued',
queue TEXT NOT NULL DEFAULT 'default',
created_by TEXT,
progress_json JSONB,
result_json JSONB,
error TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(),
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ
);
CREATE TABLE IF NOT EXISTS api_keys (
id SERIAL PRIMARY KEY,
key_hash TEXT NOT NULL UNIQUE,
key_prefix TEXT NOT NULL,
name TEXT NOT NULL,
created_by TEXT NOT NULL,
scopes TEXT[],
role TEXT NOT NULL DEFAULT 'user',
expires_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW(),
last_used_at TIMESTAMPTZ,
revoked_at TIMESTAMPTZ
);
CREATE TABLE IF NOT EXISTS advisor_sessions (
id SERIAL PRIMARY KEY,
session_id TEXT NOT NULL,
turn_index INTEGER NOT NULL,
user_email TEXT,
query_text TEXT,
event_url TEXT,
results_json JSONB,
overall_assessment TEXT,
chosen_ci_name TEXT,
chosen_content_id TEXT,
chosen_at TIMESTAMPTZ,
opted_out BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ DEFAULT NOW(),
intent TEXT,
envelope_json JSONB,
scope_json JSONB
);