# Data Flow

Five end-to-end user flows traced from initial action to final output.

---

## FLOW 1: Full Generation from a Text Prompt

```
═══════════════════════════════════════════════════════════════════
FLOW: uv run python main.py generate --prompt "A warehouse management system"
═══════════════════════════════════════════════════════════════════
```

```
┌──────────────────────────────────────────────────────────────────┐
│ STEP 1: User runs CLI command                                    │
├──────────────────────────────────────────────────────────────────┤
│ INPUT                                                             │
│   from:    shell / terminal                                       │
│   data:    argv = ["generate", "--prompt", "A warehouse..."]     │
│   via:     sys.argv                                               │
├──────────────────────────────────────────────────────────────────┤
│ PROCESS                                                           │
│   main() → _build_parser().parse_args() → args.command="generate"│
│   logging.basicConfig() configured.                              │
├──────────────────────────────────────────────────────────────────┤
│ OUTPUT                                                            │
│   returns: args.prompt = "A warehouse management system"         │
│   to:      cmd_generate(args) → Step 2                           │
│   triggers: cmd_generate called                                  │
├──────────────────────────────────────────────────────────────────┤
│ ERROR PATH                                                        │
│   throws:  argparse.error for invalid args                       │
│   caught:  argparse prints usage and exits                       │
│   shown:   stderr output                                         │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ STEP 2: cmd_generate() — main.py                                 │
├──────────────────────────────────────────────────────────────────┤
│ INPUT                                                             │
│   from:    Step 1                                                 │
│   data:    args.Namespace { prompt: str, output: None, name: None│
│              api_base_url: "http://localhost:8000" }              │
│   via:     parameter                                              │
├──────────────────────────────────────────────────────────────────┤
│ PROCESS                                                           │
│   Resolves output_dir = RUNS_DIR, calls generate_project().      │
│   Wraps in try/except for FileNotFoundError, ValueError.         │
├──────────────────────────────────────────────────────────────────┤
│ OUTPUT                                                            │
│   calls:   generate_project(user_prompt="A warehouse...",        │
│              images_dir=None, inputs_dir=None, ...)              │
│   triggers: Step 3                                               │
├──────────────────────────────────────────────────────────────────┤
│ ERROR PATH                                                        │
│   throws:  FileNotFoundError | ValueError → exit code 2          │
│            KeyboardInterrupt → exit code 130                     │
│   shown:   "ERROR: ..." printed to stderr                        │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ STEP 3: generate_project() — orchestrator.py                     │
├──────────────────────────────────────────────────────────────────┤
│ INPUT                                                             │
│   from:    Step 2                                                 │
│   data:    { user_prompt: str, images_dir: None,                 │
│              output_dir: Path, project_name: None }              │
├──────────────────────────────────────────────────────────────────┤
│ PROCESS                                                           │
│   run_id = new_run_id() → "20260508_143022"                      │
│   Creates all step output dirs, instantiates RunLog, set_active()│
│   Runs steps 01–05 in sequence under run_log.step() context.     │
├──────────────────────────────────────────────────────────────────┤
│ OUTPUT                                                            │
│   returns: project_root: Path  → Step 4 (file writing)           │
│   to:      cmd_generate() prints final paths                     │
├──────────────────────────────────────────────────────────────────┤
│ ERROR PATH                                                        │
│   finally: run_log.finalize(master_out) always called            │
│   throws:  propagates to cmd_generate()                          │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ STEP 4: ingest() — ingestion.py                                  │
├──────────────────────────────────────────────────────────────────┤
│ INPUT                                                             │
│   from:    Step 3 (orchestrator)                                  │
│   data:    { inputs_dir: INPUTS_DIR, user_prompt: "A warehouse...",
│              images_dir: None, output_dir: s01_out }             │
├──────────────────────────────────────────────────────────────────┤
│ PROCESS                                                           │
│   No images_dir provided. Checks INPUTS_DIR/images/ — empty.    │
│   images_dir remains None, image_paths = [].                     │
│   Writes s01_out/ingested_inputs.json.                           │
├──────────────────────────────────────────────────────────────────┤
│ OUTPUT                                                            │
│   returns: IngestedInputs(user_prompt="A warehouse...",          │
│              images_dir=None, image_paths=[])                    │
│   to:      Step 5                                                │
├──────────────────────────────────────────────────────────────────┤
│ ERROR PATH                                                        │
│   throws:  nothing in this path (no images_dir provided)         │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ STEP 5: generate_from_prompt() — prd_generator.py                │
├──────────────────────────────────────────────────────────────────┤
│ INPUT                                                             │
│   from:    Step 4 (IngestedInputs.user_prompt)                   │
│   data:    { user_prompt: "A warehouse management system",       │
│              output_dir: s02_out }                               │
├──────────────────────────────────────────────────────────────────┤
│ PROCESS                                                           │
│   llm = BedrockLLMClient()                                       │
│   Call 1: _llm_generate_prd(llm, user_prompt) → Bedrock          │
│   Call 2: _llm_generate_ddl(llm, prd_text, user_prompt) → Bedrock│
│   Writes full_prd.md + schema.sql + metadata.json                │
├──────────────────────────────────────────────────────────────────┤
│ OUTPUT                                                            │
│   returns: LoadedPRD(path=full_prd.md, text="# WMS...",          │
│              images=[], ddl_path=schema.sql)                     │
│   to:      Step 6 (backend generation)                           │
│   triggers: orchestrator validates ddl_path is not None          │
├──────────────────────────────────────────────────────────────────┤
│ ERROR PATH                                                        │
│   throws:  boto3 ClientError on Bedrock failure                  │
│   caught:  RunLog step-02 records failure                        │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ STEP 6: Orchestrator.run() — backend_gen/orchestrator.py         │
├──────────────────────────────────────────────────────────────────┤
│ INPUT                                                             │
│   from:    Step 5 (LoadedPRD.path, LoadedPRD.ddl_path)          │
│   data:    { ddl_file: "schema.sql", prd_file: "full_prd.md",   │
│              output_dir: backend_dir }                           │
├──────────────────────────────────────────────────────────────────┤
│ PROCESS                                                           │
│   9 sub-steps: analyze → consolidate → code-gen → manifest       │
│   → spec → readme → project-files → write → validate            │
│   Multiple Bedrock calls to claude-3-5-sonnet                    │
├──────────────────────────────────────────────────────────────────┤
│ OUTPUT                                                            │
│   returns: { system_name: "WarehouseManagement",                 │
│              api_manifest: {...}, endpoint_count: 23,            │
│              file_count: 12 }                                    │
│   written: project_root/backend/ (full FastAPI project)          │
│   written: project_root/api_manifest.json                        │
│   to:      Step 7 (IR generation)                                │
│   triggers: project_root renamed to final_slug                   │
├──────────────────────────────────────────────────────────────────┤
│ ERROR PATH                                                        │
│   throws:  SyntaxError (validation), boto3 ClientError           │
│   caught:  RunLog step-03                                        │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ STEP 7: generate_app_ir() — multi_page_service.py                │
├──────────────────────────────────────────────────────────────────┤
│ INPUT                                                             │
│   from:    Step 6 (api_manifest), Step 5 (prd_text, prd_images)  │
│   data:    { user_prompt: "A warehouse...",                      │
│              api_manifest: {...}, prd_text: "# WMS...",          │
│              prd_images: [], images_dir: None }                  │
├──────────────────────────────────────────────────────────────────┤
│ PROCESS                                                           │
│   No images → text+manifest-only mode.                           │
│   Bedrock call (Opus): detect_pages() → AppPlan                  │
│   For each page (N times): Bedrock call (Opus) → IRBundle        │
├──────────────────────────────────────────────────────────────────┤
│ OUTPUT                                                            │
│   returns: AppIRBundle { app_plan, ir_pages: [IRPage, ...] }    │
│   written: s04_out/ir/app_plan.json                              │
│            s04_out/ir/<page_id>_ir.json × N                     │
│   to:      Step 8 (React generation)                             │
├──────────────────────────────────────────────────────────────────┤
│ ERROR PATH                                                        │
│   throws:  pydantic.ValidationError on bad LLM JSON              │
│   caught:  RunLog step-04                                        │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ STEP 8: generate_react_pages() — react_gen.py                    │
├──────────────────────────────────────────────────────────────────┤
│ INPUT                                                             │
│   from:    Step 7 (AppIRBundle), Step 6 (api_manifest)           │
│   data:    { app_ir: AppIRBundle, api_manifest: {...} }          │
├──────────────────────────────────────────────────────────────────┤
│ PROCESS                                                           │
│   For each IRPage: Bedrock call (Sonnet) → TSX code              │
│   generate_router_code() → deterministic App.tsx                 │
│   generate_context_code() → deterministic AppContext.tsx         │
├──────────────────────────────────────────────────────────────────┤
│ OUTPUT                                                            │
│   returns: MultiPageBundle { pages, router_code, context_code }  │
│   to:      Step 9 (write files)                                  │
├──────────────────────────────────────────────────────────────────┤
│ ERROR PATH                                                        │
│   throws:  boto3 ClientError                                      │
│   caught:  RunLog step-05                                        │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ STEP 9: write_generated_files() — scaffolder.py                  │
├──────────────────────────────────────────────────────────────────┤
│ INPUT                                                             │
│   from:    Step 8 (MultiPageBundle)                               │
│   data:    { frontend_dir: Path, router_code, context_code,      │
│              pages: dict[filename, tsx_code] }                   │
├──────────────────────────────────────────────────────────────────┤
│ PROCESS                                                           │
│   Writes all TSX files to frontend_dir/src/.                     │
│   Writes IRBundle JSONs + app_plan.json to project_root/ir/.     │
│   Writes run summary to master_out/summary.json.                 │
├──────────────────────────────────────────────────────────────────┤
│ OUTPUT                                                            │
│   written: frontend_dir/src/App.tsx                              │
│            frontend_dir/src/AppContext.tsx                        │
│            frontend_dir/src/pages/<Name>Page.tsx × N             │
│   returns: project_root: Path → cmd_generate() prints next steps │
│   END: pipeline complete                                          │
├──────────────────────────────────────────────────────────────────┤
│ ERROR PATH                                                        │
│   throws:  IOError on disk write failure                          │
│   finally: run_log.finalize() always writes run_log.md           │
└──────────────────────────────────────────────────────────────────┘
```

---

## FLOW 2: LLM Call Lifecycle (any step)

```
═══════════════════════════════════════════════════════════════════
FLOW: A single LLM call via timed_llm_call() (step-04, step-05)
═══════════════════════════════════════════════════════════════════
```

```
┌──────────────────────────────────────────────────────────────────┐
│ STEP 1: timed_llm_call() enters — llm_logging.py                 │
├──────────────────────────────────────────────────────────────────┤
│ INPUT                                                             │
│   from:    ir_generation_service or react_generation_service      │
│   data:    { logger, step: str, model: str, page_id: str|None }  │
│   via:     context manager (with timed_llm_call(...) as stats:)  │
├──────────────────────────────────────────────────────────────────┤
│ PROCESS                                                           │
│   t0 = perf_counter(). Prints "calling..." to stdout.            │
│   Starts background spinner thread if stdout is a TTY.           │
├──────────────────────────────────────────────────────────────────┤
│ OUTPUT                                                            │
│   yields: stats: dict  (caller sets stats["response"] inside)    │
│   triggers: caller executes model.invoke(messages)               │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ STEP 2: ChatBedrockConverse.invoke() — LangChain / botocore      │
├──────────────────────────────────────────────────────────────────┤
│ INPUT                                                             │
│   from:    Step 1 caller body                                     │
│   data:    messages: list[HumanMessage | AIMessage]              │
│            (contains text + optional image blocks)               │
├──────────────────────────────────────────────────────────────────┤
│ PROCESS                                                           │
│   botocore sends HTTPS POST to Bedrock Converse API.             │
│   Retries up to BEDROCK_MAX_ATTEMPTS on throttling.              │
├──────────────────────────────────────────────────────────────────┤
│ OUTPUT (success)                                                  │
│   returns: AIMessage with content: str | list[dict]              │
│   stored in stats["response"]                                    │
│ OUTPUT (failure)                                                  │
│   raises:  botocore exception                                    │
│   triggers: Step 3 error path                                    │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ STEP 3: timed_llm_call() exits — llm_logging.py                  │
├──────────────────────────────────────────────────────────────────┤
│ INPUT                                                             │
│   from:    stats["response"] set by caller                       │
│   data:    response: AIMessage                                   │
├──────────────────────────────────────────────────────────────────┤
│ PROCESS (success)                                                 │
│   Stops spinner. Calls log_llm_response() → extracts token usage.│
│   Calls get_active().record_llm_call() to log to RunLog.         │
│   Sets stats["usage"] and stats["elapsed_ms"].                   │
│ PROCESS (failure)                                                 │
│   Stops spinner. Logs "[LLM] FAILED". Re-raises exception.       │
├──────────────────────────────────────────────────────────────────┤
│ OUTPUT                                                            │
│   stats["usage"] = { input_tokens, output_tokens, total_tokens } │
│   RunLog.record_llm_call() appended to active StepEvent          │
│   to:      caller reads stats["response"].content for parsing    │
├──────────────────────────────────────────────────────────────────┤
│ ERROR PATH                                                        │
│   throws:  botocore exception re-raised                          │
│   caught:  RunLog.step() context manager records failure         │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ STEP 4: coerce_message_content_to_text() — extractors.py         │
├──────────────────────────────────────────────────────────────────┤
│ INPUT                                                             │
│   from:    caller reads stats["response"].content                │
│   data:    content: str | list[dict]  (AIMessage.content)        │
├──────────────────────────────────────────────────────────────────┤
│ PROCESS                                                           │
│   If str: return as-is. If list: concatenate all "text" blocks.  │
├──────────────────────────────────────────────────────────────────┤
│ OUTPUT                                                            │
│   returns: str — raw text response from LLM                      │
│   to:      extract_json_object() or extract_code_block()         │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ STEP 5: extract_json_object() or extract_code_block() — extractors.py
├──────────────────────────────────────────────────────────────────┤
│ INPUT                                                             │
│   from:    Step 4 (raw text string)                              │
│   data:    text: str                                              │
├──────────────────────────────────────────────────────────────────┤
│ PROCESS (extract_json_object)                                     │
│   Finds first "{" and last "}" — extracts the substring.         │
│ PROCESS (extract_code_block)                                      │
│   Regex: ```tsx|jsx|typescript ... ``` → group 1.                │
├──────────────────────────────────────────────────────────────────┤
│ OUTPUT                                                            │
│   returns: str — JSON string or TSX code string                  │
│   to:      AppPlan.model_validate() or IRBundle.model_validate() │
│            or react_code stored in PageBundle                    │
├──────────────────────────────────────────────────────────────────┤
│ ERROR PATH                                                        │
│   throws:  JSONDecodeError if no JSON object found               │
│   caught:  caller → retry or propagate to RunLog step            │
└──────────────────────────────────────────────────────────────────┘
```

---

## FLOW 3: RunLog Telemetry Accumulation

```
═══════════════════════════════════════════════════════════════════
FLOW: Telemetry accumulation across the full pipeline run
═══════════════════════════════════════════════════════════════════
```

```
┌──────────────────────────────────────────────────────────────────┐
│ STEP 1: RunLog created and activated — orchestrator.py:121       │
├──────────────────────────────────────────────────────────────────┤
│ INPUT                                                             │
│   from:    generate_project()                                     │
│   data:    { run_id: "20260508_143022" }                         │
├──────────────────────────────────────────────────────────────────┤
│ PROCESS                                                           │
│   run_log = RunLog(run_id). set_active(run_log) sets module-level │
│   _active singleton. _t0 = perf_counter() starts global timer.   │
├──────────────────────────────────────────────────────────────────┤
│ OUTPUT                                                            │
│   _active: RunLog  (module-level in shared/run_log.py)           │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ STEP 2: Each pipeline step registers — orchestrator.py           │
├──────────────────────────────────────────────────────────────────┤
│ INPUT                                                             │
│   from:    orchestrator — each "with run_log.step(...)" block    │
│   data:    name: str, **notes: dict                              │
├──────────────────────────────────────────────────────────────────┤
│ PROCESS                                                           │
│   Creates StepEvent(name, started_at), appends to steps list,   │
│   pushes to _stack. On exit: records duration_ms + status.       │
├──────────────────────────────────────────────────────────────────┤
│ OUTPUT                                                            │
│   StepEvent added to RunLog.steps                                │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ STEP 3: LLM calls attach to active step — timed_llm_call()       │
├──────────────────────────────────────────────────────────────────┤
│ INPUT                                                             │
│   from:    timed_llm_call() or BedrockLLMClient.generate()       │
│   data:    { model, duration_ms, input_tokens, output_tokens }   │
├──────────────────────────────────────────────────────────────────┤
│ PROCESS                                                           │
│   get_active().record_llm_call() → appends LLMCall to           │
│   _stack[-1].llm_calls (top of stack = current step).            │
├──────────────────────────────────────────────────────────────────┤
│ OUTPUT                                                            │
│   StepEvent.llm_calls grows; RunLog.total_llm_calls increments  │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ STEP 4: run_log.finalize() — run_log.py:252                      │
├──────────────────────────────────────────────────────────────────┤
│ INPUT                                                             │
│   from:    orchestrator finally block (always runs)              │
│   data:    output_dir: Path (master_out)                         │
├──────────────────────────────────────────────────────────────────┤
│ PROCESS                                                           │
│   Sets duration_ms = (perf_counter() - _t0) * 1000.             │
│   Serializes to_dict() → JSON, to_markdown() → MD table.        │
├──────────────────────────────────────────────────────────────────┤
│ OUTPUT                                                            │
│   written: master_out/run_log.json                               │
│   written: master_out/run_log.md                                 │
│   to:      orchestrator prints summary stats to stdout           │
├──────────────────────────────────────────────────────────────────┤
│ ERROR PATH                                                        │
│   throws:  nothing — finalize() is robust                        │
│   shown:   N/A                                                   │
└──────────────────────────────────────────────────────────────────┘
```

---

## FLOW 4: Image-Based Generation (Wireframe Input Mode)

```
═══════════════════════════════════════════════════════════════════
FLOW: Images provided → PRD synthesized via Mantara sub-pipeline
      (currently disabled in main.py but fully wired in orchestrator)
═══════════════════════════════════════════════════════════════════
```

```
┌──────────────────────────────────────────────────────────────────┐
│ STEP 1: Images found in images_dir — ingestion.py                │
├──────────────────────────────────────────────────────────────────┤
│ INPUT                                                             │
│   from:    CLI / orchestrator                                     │
│   data:    images_dir: Path with .png/.jpg/.jpeg files           │
├──────────────────────────────────────────────────────────────────┤
│ OUTPUT                                                            │
│   returns: IngestedInputs(images_dir=Path, image_paths=[...])    │
│   to:      orchestrator → branches to generate_prd() not         │
│            generate_from_prompt()                                │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ STEP 2: generate_prd() — prd_generator.py                        │
├──────────────────────────────────────────────────────────────────┤
│ INPUT                                                             │
│   from:    IngestedInputs.images_dir                             │
│   data:    { images_dir: Path, user_prompt: str|None }           │
├──────────────────────────────────────────────────────────────────┤
│ PROCESS                                                           │
│   Calls _synthesize(images_dir) → loads mantara/main.py via     │
│   importlib.util. Mantara runs its own vision pipeline and       │
│   produces PRD.md + schema.sql in a run_dir.                     │
├──────────────────────────────────────────────────────────────────┤
│ OUTPUT                                                            │
│   returns: LoadedPRD(path=PRD.md, text=..., ddl_path=schema.sql) │
│   to:      orchestrator → same as FLOW 1 from step-03 onwards    │
├──────────────────────────────────────────────────────────────────┤
│ ERROR PATH                                                        │
│   throws:  FileNotFoundError if mantara/main.py missing          │
│            ValueError if mantara returns None                    │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ STEP 3: encode_image() × N — shared/media/images.py              │
├──────────────────────────────────────────────────────────────────┤
│ INPUT                                                             │
│   from:    multi_page_service.generate_app_ir()                  │
│   data:    paths: list[Path] from collect_image_paths()          │
├──────────────────────────────────────────────────────────────────┤
│ PROCESS                                                           │
│   Reads file bytes. Detects media type from file header.         │
│   base64-encodes. Returns EncodedImage dataclass.                │
├──────────────────────────────────────────────────────────────────┤
│ OUTPUT                                                            │
│   returns: list[EncodedImage { path, media_type, base64_data }] │
│   to:      detect_pages() and generate_ir_bundle_for_page()      │
│            both receive image content blocks in LLM messages     │
├──────────────────────────────────────────────────────────────────┤
│ ERROR PATH                                                        │
│   throws:  FileNotFoundError if image missing                    │
│            ValueError if media type unsupported                  │
│   caught:  propagates to multi_page_service → RunLog step-04     │
└──────────────────────────────────────────────────────────────────┘
```

---

## FLOW 5: AWS Credential Resolution

```
═══════════════════════════════════════════════════════════════════
FLOW: AWS credentials resolved before any Bedrock call
═══════════════════════════════════════════════════════════════════
```

```
┌──────────────────────────────────────────────────────────────────┐
│ STEP 1: .env loaded at startup — main.py:36                      │
├──────────────────────────────────────────────────────────────────┤
│ INPUT                                                             │
│   from:    filesystem                                             │
│   data:    .env file in ROOT_DIR                                 │
│   via:     python-dotenv load_dotenv(override=False)             │
├──────────────────────────────────────────────────────────────────┤
│ PROCESS                                                           │
│   Sets env vars into os.environ. override=False means existing   │
│   shell env vars win over .env values.                           │
├──────────────────────────────────────────────────────────────────┤
│ OUTPUT                                                            │
│   os.environ populated with AWS_ACCESS_KEY_ID, etc.             │
│   to:      all subsequent os.getenv() calls in shared/           │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ STEP 2: build_chat_model() resolves credentials — bedrock_client.py
├──────────────────────────────────────────────────────────────────┤
│ INPUT                                                             │
│   from:    step-04 / step-05 service files                       │
│   data:    os.environ keys: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY,
│            AWS_SESSION_TOKEN, AWS_PROFILE, BEDROCK_AWS_REGION    │
├──────────────────────────────────────────────────────────────────┤
│ PROCESS                                                           │
│   Priority: explicit keys > AWS_PROFILE > default chain.         │
│   boto3.Session created. session.get_credentials() checked.      │
│   ChatBedrockConverse instantiated with botocore.Config.         │
├──────────────────────────────────────────────────────────────────┤
│ OUTPUT                                                            │
│   returns: ChatBedrockConverse (ready to call invoke())          │
│   to:      detect_pages(), generate_ir_bundle_for_page(),        │
│            generate_page_react_code()                            │
├──────────────────────────────────────────────────────────────────┤
│ ERROR PATH                                                        │
│   throws:  RuntimeError("AWS credentials were not found")        │
│            if session.get_credentials() is None                  │
│   caught:  not caught — process exits at module load time        │
│   shown:   RuntimeError message printed to stderr                │
└──────────────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────────────┐
│ STEP 3: BedrockLLMClient.__init__() — bedrock_raw_client.py      │
├──────────────────────────────────────────────────────────────────┤
│ INPUT                                                             │
│   from:    create_llm_client() called by prd_generator / backend │
│   data:    os.environ: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY  │
├──────────────────────────────────────────────────────────────────┤
│ PROCESS                                                           │
│   boto3.client("bedrock-runtime", access_key, secret_key,        │
│     region, config=Config(read_timeout, connect_timeout, retry)) │
├──────────────────────────────────────────────────────────────────┤
│ OUTPUT                                                            │
│   self._client: boto3 bedrock-runtime client                     │
│   to:      self.generate() calls self._client.invoke_model()     │
├──────────────────────────────────────────────────────────────────┤
│ ERROR PATH                                                        │
│   throws:  boto3 NoCredentialsError if env vars missing          │
│   caught:  propagates to caller                                  │
│   shown:   printed to stderr; pipeline aborts                    │
└──────────────────────────────────────────────────────────────────┘
```

---

## Related source files

- [main.py](../main.py)
- [pipeline/master-pipeline/pipeline/orchestrator.py](../pipeline/master-pipeline/pipeline/orchestrator.py)
- [pipeline/step-01-input-ingestion/pipeline/ingestion.py](../pipeline/step-01-input-ingestion/pipeline/ingestion.py)
- [pipeline/step-02-prd-generation/pipeline/prd_generator.py](../pipeline/step-02-prd-generation/pipeline/prd_generator.py)
- [shared/bedrock_client.py](../shared/bedrock_client.py)
- [shared/bedrock_raw_client.py](../shared/bedrock_raw_client.py)
- [shared/llm_logging.py](../shared/llm_logging.py)
- [shared/run_log.py](../shared/run_log.py)
- [shared/extractors.py](../shared/extractors.py)
