# STALGEN-AI — Resume & Interview Portfolio Document

## User-confirmed context — 2026-10-18

> Source: direct user answers in the portfolio review. These are user-confirmed statements, not independently rerun repository or production measurements.

- **Ownership:** independently built the entire tool.
- **Operational use:** a team lead runs it whenever a new specification arrives for testing, generating test cases for more than 20 testers. These testers are beneficiaries, not confirmed direct operators.
- **Timing:** manual preparation could take up to a week. Generation takes roughly 20 minutes depending on file size; the complete workflow, including human review and corrections, takes approximately half a day. These are user-reported estimates.
- **Review:** generated cases undergo human review and correction; generation time must not be presented as the complete preparation time.
- **Unmeasured:** numerical accuracy, schema-validity rates and compliance certification.

> Existing draft variants and unanswered placeholders are retained. This confirmed context governs personal ownership, adoption and outcomes; unresolved draft claims are not newly verified.


> Built from a review of the repository itself: source, prompts, configuration and git history (20 commits, Oct 2025 → Sep 2026). Every claim below points to a file in this repo.

---

## 1. High-Level Project Overview

### One-Line Pitch
**STALGEN (Stellantis–ALTEN Test Generation)** is a privacy-first AI tool that runs fully on-premise. It turns Word-based automotive Software Functional Documents (SFDs) into structured, traceable QA test scenarios and writes them into the client's macro-enabled Excel UAT template. Validation engineers on Stellantis CRONOS/OMS order-management projects use it.

### Problem & Solution

**Problem**
- Automotive QA teams read long SFD `.docx` files by hand (Business Need, Product Specs, many User Stories with Management Rules) and type macro scenarios and test cases into a fixed `.xlsm` UAT template. This work is slow and repetitive, and coverage varies from person to person.
- The documents are confidential client IP, so **cloud LLM APIs were not an option**.
- LLMs are unreliable for this job. They hallucinate requirements, break JSON schemas, wrap output in `<think>` tags, and fall into repetition loops.

**Solution: a 4-stage deterministic + generative pipeline** ([processing.py](stalgen/utils/processing.py))

```
.docx ──► [1] Page-filter + Pandoc → GFM Markdown      (parsers/word_parser_pandoc.py)
      ──► [2] Regex section/user-story segmentation    (extractors/data_extractor.py)
      ──► [3] Local LLM (Ollama) per user story → JSON (llm/…)
      ──► [4] openpyxl template population (.xlsm, VBA preserved) (converters/json_to_excel.py)
```

- **Deterministic work first.** Parsing, segmentation and Excel output are rule-based. The LLM only handles the part that needs language understanding, and it sees **one user story at a time** together with the shared Business Need and Product Specs. This keeps the context window small and the output traceable.
- **Local inference** through Ollama, so no document leaves the machine.
- **Defense-in-depth for LLM output.** Prompt-level constraints, Ollama JSON mode, response sanitization, schema normalization, heuristic hallucination detection, retries, and a deterministic fallback.

---

## 2. Comprehensive Tech Stack & Tooling

### Languages & Runtimes
| Item | Evidence |
|---|---|
| **Python 3.10+** (PEP 604 `str \| Path` unions, `from __future__ import annotations`) | `pyproject.toml` (`target-version = py310`), [word_parser_pandoc.py](stalgen/parsers/word_parser_pandoc.py) |
| **Bash** (cross-platform installer for macOS/Linux, using apt/yum/dnf/brew detection) | [setup.sh](setup.sh) |
| **Markdown as a configuration language** (prompt files loaded at runtime) | [docs/prompt/](docs/prompt/) |

### Frontend / UI
| Item | Usage |
|---|---|
| **Streamlit ≥1.48** | 3-tab single-page app (`Home`, `How It Works`, `Process Document`) plus a sidebar ([main.py](main.py), [ui.py](stalgen/ui/ui.py)) |
| **State management** | `st.session_state` keeps the selected model and processing results across Streamlit reruns |
| **Styling** | About 370 lines of custom injected CSS and a corporate design-token dictionary (`ALTEN_COLORS`) ([styling.py](stalgen/ui/styling.py), [config.py](stalgen/utils/config.py)) |
| **Reusable UI helpers** | `create_badge`, `create_divider`, `create_status_indicator`, `load_background` (Base64 background image) |
| **pandas ≥2.0** | Per-user-story results rendered as a `st.dataframe` |
| **UX features** | Live progress bar fed by callbacks, dynamic model picker, page-skip input, `.xlsm` download with the correct MIME type |

### Backend / AI / Processing
| Item | Usage |
|---|---|
| **Ollama Python SDK ≥0.5** | `ollama.list()`, `ollama.pull()`, `ollama.chat(format="json")` against the local Ollama REST server (port 11434) |
| **Supported local models** | `ministral-3:14b`, `gpt-oss:20b`, `gemma-3-12b (GGUF Q6_K_XL via HF)`, fallbacks `qwen2.5:14b`, `llama3.1:8b`, `mistral:7b` ([model_manager.py](stalgen/llm/ollama/model_manager.py)) |
| **Prompt-engineering techniques** | Role-separated system/user/assistant chains, few-shot pairs with embedded reasoning, an optional Chain-of-Thought template, a self-audit/validation prompt, and anti-hallucination instructions |
| **Pandoc + pypandoc ≥1.10** | DOCX → GitHub-Flavored Markdown (`--wrap=none`), with a fallback to a `subprocess` call |
| **python-docx ≥1.1** | Low-level OOXML traversal (`CT_P`, `CT_Tbl`, `w:br`) for page-break detection and building a filtered document |
| **Pydantic v2** | Typed output schema (`MacroScenario`, `ScenarioBundle`) with field constraints |
| **Protocols** | No public API. The app is a Streamlit server that acts as an HTTP client of the local Ollama REST API |

### Database & Storage
| Item | Usage |
|---|---|
| **No database.** The system is file-based by design | Per-request isolated temp workspace: `uploads/ → parsed/ → structured/ → json_outputs/` |
| **openpyxl ≥3.1** | Fills the `.xlsm` template with `keep_vba=True`, finds headers dynamically and merges cells |
| **In-memory cache** | MD5-keyed response cache on the LLM orchestrator |
| **Filesystem hygiene** | Stale-workspace garbage collection (older than 1 h), intermediate dirs removed after export, `rmtree` on failure |

### DevOps, Testing & Tooling
| Item | Status |
|---|---|
| **uv** (Astral) | Virtual-env and dependency installer used by `setup.sh` |
| **pytest + pytest-cov** | Configured in `pyproject.toml` (markers `unit/integration/slow`, HTML and term coverage). **No tests are committed yet** |
| **black** (line length 100), **isort** (black profile), **mypy** (`check_untyped_defs`, `strict_equality`, `no_implicit_optional`) | Configured in `pyproject.toml` |
| **Dependency pinning** | Upper-bounded semver ranges (`>=x,<next-major`) in [requirements.txt](requirements.txt) |
| **Docker / CI/CD** | **Not present.** Deployment is a scripted bare-metal install ([setup.sh](setup.sh)) |

---

## 3. Architecture & Technical Highlights

### 3.1 Modular, layered LLM subsystem (SRP + Facade + Factory)
`llm_system_refactored.py` (v3.0.0) is a **facade** (`AdvancedLLMPromptSystem`) over small single-purpose components:

| Component | Responsibility |
|---|---|
| `PromptLoader` ([prompt_loader.py](stalgen/llm/shared/prompt_loader.py)) | Loads and validates the 4 prompt files and parses few-shot examples out of Markdown |
| `ModelManager` ([model_manager.py](stalgen/llm/ollama/model_manager.py)) | Model registry, per-model hyperparameters, availability check, auto-pull, fallback chain |
| `MessageBuilder` ([message_builder.py](stalgen/llm/ollama/message_builder.py)) | Builds a role-separated message chain shaped by model capability flags |
| `OllamaClient` ([ollama_client.py](stalgen/llm/ollama/ollama_client.py)) | Transport: sampling options, seeded decoding, JSON-mode toggle |
| `ResponseProcessor` ([response_processor.py](stalgen/llm/shared/response_processor.py)) | Sanitizes, normalizes, quality-scores and builds the fallback |

The package is split into `llm/shared` (provider-agnostic) and `llm/ollama` (provider-specific). An `LLMProvider` enum lives in [config.py](stalgen/utils/config.py). Git history shows a Gemini provider was added and later removed (commit `43990de`), so the split was tested against a second backend. `create_llm_system()` and `generate_from_file()` are factory and convenience entry points.

### 3.2 Capability-aware prompt engineering ("prompts as configuration")
- **Per-model feature flags** (`use_few_shot`, `use_cot`, `json_mode`) together with sampling parameters (`temperature`, `top_p`, `num_predict`, `repeat_penalty`). Smaller or weaker models get a lighter context. Strong JSON models skip few-shot.
- **Fuzzy config resolution.** If there is no exact tag match, the manager matches on the base model name (`ministral-3:*`) before falling back to defaults.
- **Prompts live in versioned Markdown**, not in code: a domain system prompt with automotive glossary (CRONOS, OMS, CAR, CAF, NSC, MR, CUD…), a 4-phase CoT protocol, a 3-phase self-audit validation prompt, and 7 worked few-shot examples. Domain experts can tune generation without changing code. [config.py](stalgen/utils/config.py) **creates default prompt files on first run** if they are missing.
- **Few-shot examples carry their reasoning.** The loader extracts `REASONING_PROCESS` blocks and injects them as `<reasoning>…</reasoning>` inside assistant turns, so the model learns *how* to derive tests and not only the output format. It tries 3 heading regexes, 2 input, 2 reasoning and 4 output regexes so authors can format examples in different ways.
- **Anti-hallucination user prompt.** It requires traceability to input requirements, forbids invented features, enforces action-verb test-case phrasing, and caps output at 3–8 test cases per scenario.

### 3.3 Multi-layer LLM output reliability pipeline
[response_processor.py](stalgen/llm/shared/response_processor.py) + [llm_system_refactored.py](stalgen/llm/llm_system_refactored.py):
1. **Constrained decoding:** Ollama `format="json"`, `seed=42` for reproducibility, `repeat_penalty=1.1` against loops.
2. **Sanitization:** removes `<think>` and `<reasoning>` blocks, *extracts* `<answer>` content instead of dropping it, strips code fences, and pulls out the outermost JSON object or array with a regex.
3. **Schema normalization:** turns 4 different top-level shapes (`{"output":…}`, `{"output_json":…}`, bare list, bare object) into one canonical schema. Missing titles, scalar `test_cases` and empty strings are repaired in place.
4. **Heuristic hallucination detection:** min/max test cases per scenario (2/15), min test-case length (20 chars), a required action-verb whitelist, 3-consecutive-identical-word detection, and a sliding 40-char window (step 10) that flags any substring repeated more than twice. Issues are capped at 10 and returned as `quality_issues` metadata instead of being thrown.
5. **Bounded retries:** `MAX_RETRIES=3`. On `JSONDecodeError`, the last attempt returns a **deterministic fallback**: happy path, error handling and edge cases titled from the extracted story. Fallback output is marked `success=False` and `model_used="fallback"`. Other exceptions are raised with the original cause attached.

### 3.4 OOXML-level document processing
[word_parser_pandoc.py](stalgen/parsers/word_parser_pandoc.py):
- Walks the raw DOCX XML body (`CT_P` paragraphs and `CT_Tbl` tables, in document order) and detects page boundaries from `w:br` elements (`type=None|"page"`), including breaks nested inside table cells.
- **Rebuilds a filtered document** without the skipped pages (cover, TOC…). Section properties (`w:sectPr`) are kept, and paragraph style, bold, italic, underline, font name and size are copied. Pandoc then converts the result.
- **Two-path Pandoc integration:** `pypandoc` first, then a direct `subprocess` call (both for the install check, with a 5 s timeout, and for conversion). Install errors include instructions for each OS.
- Returns an immutable `@dataclass(frozen=True) ConversionResult`. The module also works as a CLI through `argparse`.

### 3.5 Robust, template-agnostic Excel generation
[json_to_excel.py](stalgen/converters/json_to_excel.py):
- Loads `.xlsm` with `keep_vba=True` so the client's **VBA macros survive**.
- **Finds columns dynamically** from header row 20 instead of hard-coding letters. A missing column raises an error that lists the headers that *were* found.
- Merges macro-scenario cells vertically across their test-case rows.
- Leftover template rows are cleared **by nulling values instead of `delete_rows()`**, because `delete_rows()` can corrupt embedded VBA in `.xlsm` files. This choice came from debugging that failure.
- Isolated per-file errors: one corrupt JSON is logged and skipped, and the rest of the batch still exports.

### 3.6 Resource management, input validation & observability
[processing.py](stalgen/utils/processing.py), [logging_config.py](stalgen/utils/logging_config.py):
- **Input validation:** `.docx` extension check and a **10 MB upload cap**.
- **Isolated per-run workspaces** (`tempfile.mkdtemp(prefix="sfd_processing_")`). Intermediates are deleted after a successful export, the whole tree is deleted on failure, and an **opportunistic GC** removes workspaces older than 1 h on each new run.
- **Partial-failure tolerance:** each user story records its own `error`. The run fails only if *zero* stories produce output.
- **Callback-based progress reporting** through 3 layers (UI → pipeline → LLM retries). The bar is split 10 % parse / 80 % LLM / 10 % export. A Python **late-binding closure bug** is avoided with default-argument binding (`_idx=idx, _story=user_story`).
- **Centralized logging:** root-logger setup, removal of duplicate handlers (needed because Streamlit reruns the script), an optional file handler, and quieter `httpx`/`urllib3` noise.
- **Model resilience:** the app finds installed models at runtime, auto-pulls a missing model, and walks a 3-model fallback chain. If Ollama is unreachable, the UI degrades gracefully.

---

## 4. Quantifiable Metrics & Scope (From the Codebase)

| Metric | Value | Source |
|---|---|---|
| Python source | **~3,400 LOC across 25 modules** in 7 sub-packages (`parsers`, `extractors`, `llm/shared`, `llm/ollama`, `converters`, `utils`, `ui`) | `git ls-files` |
| Classes / functions | **18 classes, 79 functions/methods** | static count |
| Prompt engineering assets | **4 prompt files (~600 lines)**, including **7 few-shot examples** and a 10-term domain glossary | `docs/prompt/` |
| Pipeline stages | **4** (parse → extract → generate → export) | `processing.py` |
| HTTP API endpoints | **0 exposed.** Streamlit app; consumes **3 Ollama API operations** (`list`, `pull`, `chat`) | `ollama_client.py`, `model_manager.py` |
| Database tables | **0** (file-based). **7 typed data models**: 4 dataclasses (`UserStoryData`, `SFDStructuredData`, `ConversionResult` [frozen], `PromptComponents`), 2 Pydantic models (`MacroScenario` → nested in `ScenarioBundle`), 1 Enum (`TestCaseType`, 6 categories) | `models.py`, `data_extractor.py`, `word_parser_pandoc.py` |
| UI | **3 tabs + sidebar**, **6 view-render functions**, **5 reusable styling helpers**, ~370 lines of custom CSS | `ui.py`, `styling.py` |
| LLM models configured | **3 tuned configs + 3 fallback models**; default `num_predict` 8,192 tokens | `model_manager.py` |
| Reliability constants | `MAX_RETRIES=3`, `seed=42`, `repeat_penalty=1.1`, temperature 0.1–0.2 for JSON models | `llm_system_refactored.py`, `ollama_client.py` |
| Quality thresholds | 2–15 test cases/scenario, 20–500 chars/test case, 5 action verbs, 40-char/step-10 repetition window, ≤10 issues reported | `response_processor.py` |
| Context budget control | Few-shot capped at **2 example pairs (4 messages)**; LLM sees **1 user story per call** | `message_builder.py`, `processing.py` |
| Input / resource limits | **10 MB** upload cap; **0–100** pages skippable; **3,600 s** stale-workspace TTL; **5 s** Pandoc probe timeout | `processing.py`, `ui.py`, `word_parser_pandoc.py` |
| Response parsing tolerance | **4** top-level JSON shapes normalized; **11** regex fallbacks for few-shot parsing; **3** user-story heading variants | `response_processor.py`, `prompt_loader.py`, `data_extractor.py` |
| Automated tests | **0 committed.** pytest/coverage/markers scaffolded in `pyproject.toml` | `pyproject.toml` |
| Platform support | macOS + Linux (apt / yum / dnf / brew) scripted install; Windows supported through Pandoc guidance | `setup.sh`, `word_parser_pandoc.py` |

---

## 5. Ready-to-Use Resume Bullet Points (Google XYZ format)

> Replace bracketed placeholders with real numbers from your team: hours saved, documents processed, review acceptance rate, and so on.

### Variation 1 — Full-Stack / General Software Engineering
1. **Independently built** an on-premise GenAI test-generation tool for Stellantis automotive QA. Preparation that could take **up to a week manually** now takes **approximately half a day including human review and corrections**, with generation taking **roughly 20 minutes**, depending on specification size. The pipeline combines Pandoc parsing, story segmentation, local Ollama inference and VBA-safe Excel export.
2. **Engineered** an LLM output-reliability layer that reached **[X%] schema-valid responses**. It combines JSON-mode decoding, seeded sampling, tag/fence sanitization, normalization of 4 response shapes, heuristic hallucination detection, 3-attempt retries and a deterministic fallback.
3. **Delivered** a Streamlit tool operated by a **team lead whenever a new specification arrives**, generating test cases for **20+ testers**. It provides model discovery, progress reporting, page filtering and `.xlsm` export. The number of SFDs processed remains **[N]+ SFDs** (unmeasured); 20+ testers are beneficiaries, not confirmed direct users.
4. **Refactored** a monolithic LLM script into a 5-component modular package (Facade, Factory, single-responsibility classes) with a provider-agnostic shared layer. This let the team **[test a second LLM provider / add N models] in [X] days** without touching the pipeline.

### Variation 2 — Backend / Systems / AI Engineering
5. **Designed** a capability-aware prompt-orchestration system with per-model feature flags (few-shot, CoT, JSON mode) and tuned sampling for 3+ local models. Accuracy improved by **[X%]** and per-request context dropped by **[Y] tokens** for smaller 8B–14B models.
6. **Built** OOXML-level document preprocessing with python-docx. It detects page breaks in paragraphs and nested tables and rebuilds formatting-preserving filtered documents, removing **[X]** irrelevant pages per document (cover/TOC) before Pandoc conversion.
7. **Hardened** pipeline resource handling with isolated per-run temp workspaces, a 10 MB input cap, cleanup on failure, and 1-hour stale-workspace garbage collection. This removed **[X] GB** of disk leakage on shared workstations and kept the batch running when individual stories failed.
8. **Kept data on-premise.** Zero client documents leave the network because all inference runs on **local Ollama models** with a self-healing model manager (auto-pull plus a 3-model fallback chain). This met **[client/ISO 26262 / data-confidentiality]** requirements.

### Variation 3 — Frontend / Product Engineering
9. **Shipped** a branded 3-tab Streamlit interface with ~370 lines of custom CSS design tokens and reusable UI helpers. Users went from upload to download in **[N] clicks** and onboarding time dropped by **[X%]**.
10. **Improved** perceived performance for **[Y]-minute** LLM batch jobs with real-time progress callbacks threaded across 3 layers (UI → pipeline → retry loop) and weighted 10/80/10 by stage. Per-story results and error reporting cut "is it stuck?" support requests by **[X%]**.
11. **Built a "prompts as configuration" workflow**: 4 versioned Markdown prompt files with 7 annotated few-shot examples that auto-create on first run. Non-engineering QA experts could tune output style and domain vocabulary **without code changes**, and iteration cycles shrank from **[X] days to [Y] hours**.

---

## 6. Interview Prep: Deep-Dive Talking Points

### Talking Point 1 — Making a non-deterministic LLM safe to plug into a deterministic pipeline
**Files:** [response_processor.py](stalgen/llm/shared/response_processor.py), [ollama_client.py](stalgen/llm/ollama/ollama_client.py), [llm_system_refactored.py](stalgen/llm/llm_system_refactored.py), [validation_prompt.md](docs/prompt/validation_prompt.md)

- **S:** The Excel exporter needs a strict `output → output_json → {macro_scenario, test_cases[]}` structure. Local 8B–20B models often produced `<think>` preambles, Markdown fences, other wrapper keys, nested test-case objects, or repetition loops. Any of these broke the export.
- **T:** Get reliable, schema-compliant output from small local models with no cloud model and no function-calling API.
- **A:** Several independent layers, so each one catches what the previous one misses. (1) The prompt states the schema and lists forbidden fields. (2) Ollama `format="json"` plus `seed=42` and `repeat_penalty=1.1`. (3) A sanitizer removes reasoning tags but **extracts** `<answer>` content (dropping it lost valid payloads) and isolates the outermost JSON. (4) A normalizer maps 4 different top-level shapes to one canonical schema and repairs fields in place. (5) A heuristic quality scorer looks for repetition, missing action verbs and size outliers. (6) Up to 3 retries and a deterministic fallback.
- **Result / trade-offs:** The pipeline *never crashes on bad JSON*. Quality problems are attached as metadata (`quality_issues`) and fallbacks are marked `success=False`, so reviewers know what to check. Be honest about the trade-offs: the fallback is generic, so it favors availability over accuracy. Quality checks warn instead of rejecting, to avoid false-positive rejections. With a fixed seed and identical messages, retries may return the same output, so a better design would vary the seed or temperature, or add the parse error to the next prompt. The Pydantic models exist but are not yet the runtime validation gate; moving validation into `ScenarioBundle.model_validate` is the natural next step.

### Talking Point 2 — Fitting long SFDs into small-model context windows without losing traceability
**Files:** [data_extractor.py](stalgen/extractors/data_extractor.py), [message_builder.py](stalgen/llm/ollama/message_builder.py), [model_manager.py](stalgen/llm/ollama/model_manager.py), [processing.py](stalgen/utils/processing.py)

- **S:** One SFD holds a Business Need, Product Specs and many User Stories. Sending the whole document to a 14B local model ran into context limits, made the model mix up stories, and made outputs impossible to map back to their source.
- **T:** Reduce per-call context while keeping enough global context for correct, traceable tests.
- **A:** **Map-style decomposition.** A regex extractor segments the Markdown into sections and user stories. It normalizes escaped Pandoc headings (`\<Update\> User Story:`) and avoids false positives on prefixed headings (latest commit `f3b286f`). Each story gets its own YAML-like file that repeats the *shared* context (`[SFD DESCRIPTION]` + `[USER STORY]`) with an ID (`US-001`…), and the LLM handles one story per call. On top of that, `MessageBuilder` includes few-shot examples (capped at 2 pairs) or the CoT template only when the model's config flags enable them.
- **Result / trade-offs:** Context per call is bounded, results are traced per story in the UI table, and one failed story doesn't sink the batch. The trade-off is that the shared context is repeated in every call (more tokens overall) and cross-story dependencies aren't modeled. Regex segmentation depends on the SFD template, and the extractor's constructor accepts pattern injection for that reason. Possible next steps: run stories in parallel (they are independent), or add a reduce step that removes duplicate scenarios across stories.

### Talking Point 3 — Writing into a client-owned macro-enabled Excel template without corrupting it
**Files:** [json_to_excel.py](stalgen/converters/json_to_excel.py), [processing.py](stalgen/utils/processing.py), [config.py](stalgen/utils/config.py)

- **S:** The deliverable had to be the client's own `UAT-NRM-CRONOS-OMS-XF_V2.2` `.xlsm` template, with embedded VBA, a header row at row 20 and merged scenario cells. Generic exports would not be accepted, and early versions corrupted the macros.
- **T:** Fill the template safely and repeatably, without breaking VBA or depending on column positions.
- **A:** Load with `openpyxl(keep_vba=True)`. Find target columns by **header text in row 20** rather than fixed letters, and list the available headers in the error if a lookup fails. Write test cases one row each and **vertically merge** the macro-scenario cell when a scenario has 2 or more test cases. Clear leftover template rows by **setting values to `None` instead of `ws.delete_rows()`**, because row deletion shifts the XML in ways that can corrupt VBA. Skip and log bad JSON files individually. Upstream, each run writes its output into an isolated temp workspace, deletes intermediates after export, and garbage-collects workspaces older than 1 h.
- **Result / trade-offs:** The output is a download-ready file in the client's template that survives template column changes and keeps macros intact. The trade-off is that template-specific constants (sheet name `Template`, header row 20) are still hard-coded, and moving them to config is the next step. Avoiding `delete_rows` can leave formatted but empty rows, which was accepted in exchange for VBA safety.

---

### Honest Gaps to Pre-empt in Interviews (and how to frame them)
| Gap | Framing |
|---|---|
| No automated tests committed | "pytest, coverage and markers are already scaffolded in `pyproject.toml`. My next step would be golden-file tests for the extractor and the response normalizer, since those are pure functions." |
| No Docker / CI | "Native Ollama on the host avoided GPU-in-container complexity. A Compose file with an Ollama sidecar plus a GitHub Actions lint/test job would be the next step." |
| Sequential LLM calls | "Stories are independent, so a bounded `ThreadPoolExecutor` is an easy win, limited by local GPU memory." |
| In-memory cache is per-instance | "It deduplicates identical stories within a run. A persistent content-hash cache (disk/SQLite) would let reruns of the same SFD skip inference." |
