Rich

Rich is the CLI’s presentation layer, and only that. It renders what the tdw subcommands in src/test_data_workbench/cli/ already decided to say; it makes no decisions of its own about what data to generate or how to classify a schema.

Console is shared, not re-created

Every CLI module (analyze.py, deploy.py, validate.py, team.py, demo.py) creates one module-level console = Console() and writes to it throughout the module, rather than constructing a new Console per function call. This matters for Rich specifically: a single Console instance is what lets a live-updating region (see below) coexist correctly with ordinary console.print calls around it, and it is what Rich itself recommends for consistent terminal-width detection across a run.

Tables for structured output

rich.table.Table is the default shape for anything list-like: discovered tables and their entity types, detected relationships, PII columns, and validation results. cli/analyze.py’s table output is representative:

table = Table(title="Discovered Tables")
table.add_column("Table Name", style="cyan")
table.add_column("Entity Type", style="magenta")
table.add_column("Columns", justify="right", style="green")
table.add_column("Estimated Rows", justify="right", style="yellow")

for table_info in schema_info.tables:
    table.add_row(
        table_info.name,
        table_info.entity_type.value,
        str(len(table_info.columns)),
        str(row_count) if row_count is not None and row_count > 0 else "Unknown",
    )

The "Unknown" fallback for an absent row count is not a Rich concern by itself, but it is exactly the kind of detail Rich’s tabular output makes easy to get right: a metadata-only analysis (Privacy and Data Access) leaves row_count as None, and the table renders that as an explicit word rather than a blank cell or a stray "None" string.

Panel for status and next-step framing

rich.panel.Panel wraps summary and outcome messages, analysis complete, deployment succeeded or failed, validation verdict, each with a title and color-coded content via Rich’s inline markup ([bold green]...[/bold green]). cli/deploy.py uses the border color itself as a signal, not just the text inside it:

console.print(Panel(
    f"[bold green]Deployment finished {status_word} target[/bold green]\n"
    f"Completed in {total_time:.1f} seconds\n...",
    title="Deployment complete",
    border_style="green" if status_word == "within" else "yellow",
))

Every command that ends successfully also prints a “Next steps” panel suggesting the next tdw command to run. That is a CLI design choice the docs are attributing correctly to the CLI, not to Rich, but Rich’s Panel is the mechanism that makes that guidance visually distinct from the data above it.

Progress and Live for the two operations that take real time

Schema analysis and deployment are the only two commands that can run long enough to need feedback mid-operation, and each uses a different Rich primitive suited to its shape. cli/analyze.py uses an indeterminate spinner, because there is nothing to measure progress against until the analysis finishes:

with Progress(SpinnerColumn(), TextColumn("[progress.description]{task.description}"),
              console=console) as progress:
    task = progress.add_task("Connecting to database...", total=None)
    ...
    schema_info = await analyzer.analyze_production_schema(...)

cli/deploy.py instead has three known phases with target times, so it uses rich.live.Live to keep re-rendering a phase-status table in place as each phase completes, rather than a spinner with no structure:

with Live(render_phase_table(), refresh_per_second=4, console=console) as live:
    ...
    live.update(render_phase_table(time.time() - start_time))

The choice between the two is deliberate: a spinner communicates “working, duration unknown”; a live-updating table communicates “here is the plan and here is where we are in it.” Using Live for the multi-phase deploy and Progress for the single-phase analyze matches the shape of what is actually happening in each command.

Confined to the CLI

Rich does not appear in core, adaptation, or api. The FastAPI layer (FastAPI) returns plain Pydantic models as JSON; formatting for a terminal is entirely the CLI’s concern, which keeps Rich out of any code path that does not actually print to a terminal.