## Codebase Patterns
- 4QF+STRIDE stage modules live in `frameworks/stride_4q/`; tests in `tests/stride_4q/`. Same patterns as PASTA stages: `STAGE_PROMPT` constant + `build_prompt(context, output_dir)` using `.replace()` for placeholders.
- **No circular imports**: Pack builders (`frameworks/pasta/_pack.py`) import from `frameworks.types` directly (sibling module), not from `frameworks/__init__`. This keeps the import graph a DAG: `frameworks/__init__` → `_built_in` → `frameworks.pasta._pack` → `frameworks.types`.
- `evaluate_reference_conditions(reference_config, prior_outputs)` in `frameworks/references/conditions.py` (re-exported from `frameworks/references/__init__.py`) is the canonical utility for conditional reference injection. Each entry in `reference_config` is a dict with `"condition"` and `"reference"` keys. Conditions: `always`, `api_detected`, `llm_detected`. The function searches all values in `prior_outputs` (case-insensitively combined) for keyword matches.
- Keyword constants `API_KEYWORDS` and `LLM_KEYWORDS` in `frameworks/references/conditions.py` are the authoritative keyword lists for framework pack reference injection conditions.
- OWASP constants live in `frameworks/references/owasp.py`; scanner snippet constants live in `frameworks/references/scanner_snippets.py`.
- The `prompts/` package has been deleted entirely — do not create or import from `threatsmith.prompts`.

- `frameworks/_built_in.py` registers packs for all four frameworks at import time; `frameworks/__init__.py` imports it to trigger registration. PASTA uses `build_pasta_pack()` from `frameworks.pasta`; other frameworks use placeholder stubs until their stories are implemented.
- `list_frameworks()` returns `list(_REGISTRY.values())` — order follows insertion order (dict in Python 3.7+).

- Framework data model and `StageContext` live in `frameworks/types.py`; `frameworks/__init__.py` re-exports the public API. Keep `__init__.py` thin.
- `_REGISTRY` is a module-level dict in `types.py`; tests use the `autouse` fixture to snapshot/restore it so registrations don't leak between tests.
- `ruff` enforces E731 (no lambda assignments) — use `def` for named callables in tests.
- All `build_prompt` functions use the generic `StageContext` dataclass — no per-stage typed contexts.
---

## 2026-03-23 - PASTA E2E test consolidation
- Merged `tests/test_e2e.py` and `tests/test_e2e_pasta_regression.py` into `tests/test_e2e_pasta.py`
- Deleted both source files; combined metadata assertions (required fields + `framework="pasta"` value) into one test; 10 tests total
- Files changed: `tests/test_e2e_pasta.py` (new), `tests/test_e2e.py` (deleted), `tests/test_e2e_pasta_regression.py` (deleted)
---

## 2026-03-23 - US-F37
- Created `tests/test_e2e_pasta_regression.py` with 9 regression tests for the full PASTA pipeline
- Tests cover: all 8 deliverable filenames, Stage 4 OWASP Web Top 10 (always), OWASP API Top 10 (conditional on api_detected), OWASP LLM Top 10 (conditional on llm_detected), Stage 5 scanner snippets with patched `detect_scanners`, metadata `framework: "pasta"`
- Stage 4 conditional tests work by writing API/LLM keywords into Stage 1 mock output; `evaluate_reference_conditions` searches all prior_outputs values combined
- Scanner snippet tests patch `threatsmith.main.detect_scanners` to return a controlled `{"available": [...], "unavailable": [...]}` dict
- Files changed: `tests/test_e2e_pasta_regression.py` (new)
- **Learnings for future iterations:**
  - To test OWASP conditional injection at stage N, write trigger keywords (e.g. "rest", "llm") into earlier stage mock outputs — the assembler combines all prior_outputs values for keyword detection
  - Patch `threatsmith.main.detect_scanners` (not the module-level import) to control scanner availability in E2E tests
---

## 2026-03-23 - US-F34
- Created `tests/test_e2e_stride_4q.py` with 4 E2E tests for the full 4QF+STRIDE pipeline
- Tests cover: all 5 deliverable files created, Stage 2 prompt contains STRIDE categories, metadata `framework: "stride-4q"`, context accumulation across all 4 analysis stages + report
- Pattern mirrors `tests/test_e2e.py` (PASTA) with `_make_writing_engine` using `call_count = {"n": 0}` closure dict
- Files changed: `tests/test_e2e_stride_4q.py` (new)
- **Learnings for future iterations:**
  - E2E tests for new frameworks follow the same `_make_writing_engine` + `call_count` dict pattern as PASTA test_e2e.py
  - Mock engine `execute_side_effect` signature must include `output_dir: str` as third parameter (matches actual Engine.execute signature)
  - Stage 2 STRIDE reference test: check for both "STRIDE" and "Spoofing" (the STRIDE_CATEGORIES constant contains both)
---

## 2026-03-19 - US-F33
- Added `framework_display_name: str` and `stages_completed: int` to `ThreatSmithMetadata`
- Changed `generate_metadata` signature: `framework_name: str` → `framework: FrameworkPack`; added `stages_completed: int = 0` param
- Added `stages_completed` property to `Orchestrator` (returns `len(self._prior_outputs)`)
- Moved metadata write in `main.py` to AFTER `orchestrator.run()` so `stages_completed` reflects actual completed stages
- Updated test for ordering: `test_metadata_written_before_pipeline` → `test_metadata_written_after_pipeline`
- Deferred: framework mismatch warning when resuming/re-running (per user instruction)
- Files changed: `src/threatsmith/utils/metadata.py`, `src/threatsmith/orchestrator.py`, `src/threatsmith/main.py`, `tests/test_metadata.py`, `tests/test_cli.py`, `tests/test_e2e.py`
- **Learnings for future iterations:**
  - `generate_metadata` now imports `FrameworkPack` from `frameworks.types` — no circular import since `frameworks.types` doesn't import `utils.metadata`
  - `stages_completed` is derived from `len(orchestrator._prior_outputs)` via the `stages_completed` property — one entry per successfully completed stage
  - Metadata is written AFTER the pipeline run (not before) to capture accurate `stages_completed`
---

## 2026-03-19 - US-F32
- Added `--framework` option to CLI (default `stride-4q`, resolved from `.threatsmith.yml` config when not explicitly passed)
- Added `--list-frameworks` flag that prints all registered framework names/descriptions and exits 0
- Added `--rerun-stage` option with validation against `len(pack.stages) + 1` (analysis + report); exits 1 with informative error including valid range
- `_load_config(path)` utility reads `.threatsmith.yml` from target repo as simple key:value YAML (no PyYAML dependency)
- `generate_metadata` now accepts `framework_name: str` and stores it as `framework` field in `ThreatSmithMetadata`
- Updated `tests/test_e2e.py` to pass `--framework pasta` (default changed from pasta to stride-4q)
- Added 7 new CLI tests in `tests/test_cli.py` covering all acceptance criteria
- Files changed: `src/threatsmith/main.py`, `src/threatsmith/utils/metadata.py`, `tests/test_cli.py`, `tests/test_metadata.py`, `tests/test_e2e.py`
- **Learnings for future iterations:**
  - Use `framework: str | None = None` (not `= "stride-4q"`) for the CLI option so the config file can supply the default; resolve via `framework or config.get("framework", "stride-4q")`
  - `.threatsmith.yml` is parsed with a trivial line-by-line parser (no PyYAML) since the project has no runtime deps beyond typer
  - `--list-frameworks` uses `is_eager=True` in `typer.Option` so Typer doesn't require PATH argument when the flag is set; combined with `path: str | None = None` default
  - Total stage count for rerun-stage validation: `len(pack.stages) + 1` (not just `len(pack.stages)`)
---

## 2026-03-19 - US-F16
- Created `src/threatsmith/frameworks/stride_4q/_pack.py` with `build_stride_4q_pack()` function
- Created `src/threatsmith/frameworks/stride_4q/__init__.py` exporting `build_stride_4q_pack` (following PASTA pattern: import stage submodules first, then import from `_pack`)
- Updated `src/threatsmith/frameworks/_built_in.py` to call `build_stride_4q_pack()` instead of placeholder stub
- Pack: name="stride-4q", display_name="4QF + STRIDE", 4 analysis stages + report, scanner_stages=[2]
- reference_sets on stage 2: STRIDE_CATEGORIES (always), OWASP_WEB_TOP_10 (always), OWASP_API_TOP_10 (api_detected), OWASP_LLM_TOP_10 (llm_detected), OWASP_MOBILE_TOP_10 (mobile_detected)
- Created `tests/stride_4q/test_pack.py` with 20 tests: name, display_name, description, stage count, report stage, stage numbers, output files, scanner_stages, reference_sets entry count and conditions for all 5 references, callable build_prompt, no other reference sets
- Files changed: `src/threatsmith/frameworks/stride_4q/_pack.py` (new), `src/threatsmith/frameworks/stride_4q/__init__.py` (updated), `src/threatsmith/frameworks/_built_in.py` (updated), `tests/stride_4q/test_pack.py` (new)
- **Learnings for future iterations:**
  - Pack `__init__.py` must import stage submodules before importing `_pack` — this ensures submodules are in `sys.modules` when `_pack.py` does `from package import stage_XX`, avoiding AttributeError on partial package initialization
  - When replacing a placeholder in `_built_in.py`, import the pack builder and call it directly; remove the placeholder stub only if no other placeholders need it
---

## 2026-03-19 - US-F15
- Created `src/threatsmith/frameworks/stride_4q/stage_05_report.py` with `STAGE_PROMPT` constant and `build_prompt` function
- Prompt consolidates Stages 1–4 into a single executive report with no new analysis
- Prompt preserves Mermaid diagrams, threat tables, mitigation recommendations, gap assessments, accepted risks
- Prompt includes executive summary with scope, critical findings, top risks, key mitigations, validation outcome, overall risk posture
- Prompt includes content cleanup rules (conversational artifacts, heading normalization, deduplication)
- Prompt writes output to `threatmodel/05-report.md`
- Injects Stages 1–4 via `{prior_stages_section}` placeholder with XML-delimited tags: `<stage_01_system_model>`, `<stage_02_threat_identification>`, `<stage_03_mitigations>`, `<stage_04_validation>`
- Created `tests/stride_4q/test_stage_05_report.py` with 30 tests: all four stages injected, each stage independently, absent/empty prior outputs, output file path, custom output dir, trailing slash normalization, placeholder absence, no-new-analysis, consolidation, stage names, heading normalization, content preservation, 4QF+STRIDE mention
- Files changed: `src/threatsmith/frameworks/stride_4q/stage_05_report.py` (new), `tests/stride_4q/test_stage_05_report.py` (new)
- **Learnings for future iterations:**
  - Report stages follow the PASTA stage_08_report pattern: no references or scanners, only `{prior_stages_section}` and `{output_dir}` placeholders
  - Report stages use `any([...])` pattern (not `if x or y or z`) for checking multiple prior outputs
  - XML tag names in report stages match the stage names (e.g. `<stage_04_validation>` not `<stage_04_output>`)
---

## 2026-03-19 - US-F14
- Created `src/threatsmith/frameworks/stride_4q/stage_04_validation.py` with `STAGE_PROMPT` constant and `build_prompt` function
- Prompt validates completeness and quality of the threat model from Stages 1–3
- Prompt covers: component coverage verification, STRIDE category coverage verification, mitigation completeness for high-priority threats, remaining gaps, accepted risks with justification, recommended next steps and review cadence
- Prompt references the Four Question Framework ("Did we do a good job?")
- Prompt injects Stages 1–3 outputs via `{prior_stages_section}` placeholder with XML-delimited `<stage_01_system_model>`, `<stage_02_threat_identification>`, and `<stage_03_mitigations>` tags
- Prompt writes output to `threatmodel/04-validation.md`
- Created `tests/stride_4q/test_stage_04_validation.py` with 25 tests: all three prior stages injected, each stage independently, absent/empty prior outputs, only stage 01/03 present, output file path, custom output dir, trailing slash normalization, placeholder absence, 4QF context, content coverage (component coverage, STRIDE coverage, mitigation completeness, remaining gaps, accepted risks, review cadence, next steps)
- Files changed: `src/threatsmith/frameworks/stride_4q/stage_04_validation.py` (new), `tests/stride_4q/test_stage_04_validation.py` (new)
- **Learnings for future iterations:**
  - Stage 4 uses only `{prior_stages_section}` and `{output_dir}` placeholders — no references or scanners (same as Stage 3)
  - Three-stage prior output injection: each of the three stages gets its own XML tag within `<prior_stages>`, each independently optional
---

## 2026-03-19 - US-F13
- Created `src/threatsmith/frameworks/stride_4q/stage_03_mitigations.py` with `STAGE_PROMPT` constant and `build_prompt` function
- Prompt covers countermeasure identification per threat, existing controls assessment, gap analysis, implementation recommendations with effort estimates (Low/Medium/High), priority ranking (P0–P3), and residual risk assessment
- Prompt references the Four Question Framework ("What are we going to do about it?")
- Prompt injects Stages 1–2 outputs via `{prior_stages_section}` placeholder with XML-delimited `<stage_01_system_model>` and `<stage_02_threat_identification>` tags
- Prompt writes output to `threatmodel/03-mitigations.md`
- Created `tests/stride_4q/test_stage_03_mitigations.py` with 20 tests: both prior stages injected, each stage independently, absent/empty prior outputs, output file path, custom output dir, trailing slash normalization, placeholder absence, 4QF context, content coverage (countermeasures, existing controls, gap analysis, effort estimates, residual risk)
- Files changed: `src/threatsmith/frameworks/stride_4q/stage_03_mitigations.py` (new), `tests/stride_4q/test_stage_03_mitigations.py` (new)
- **Learnings for future iterations:**
  - Stage 3 uses only `{prior_stages_section}` and `{output_dir}` placeholders — no references or scanners (unlike Stage 2)
  - Multi-stage prior output injection pattern: each stage output gets its own XML tag within `<prior_stages>`, and each is independently optional (only included when present and non-empty)
---

## 2026-03-19 - US-F12
- Created `src/threatsmith/frameworks/stride_4q/stage_02_threat_identification.py` with `STAGE_PROMPT` constant and `build_prompt` function
- Prompt performs systematic STRIDE analysis across all Stage 1 components with attacker motivation and capability context
- Prompt references OWASP Top 10 as a coverage checklist via `context.references`
- Prompt integrates scanner results via `context.scanners_available` when present
- Prompt references the Four Question Framework ("What can go wrong?")
- Prompt writes output to `threatmodel/02-threat-identification.md`
- Created `tests/stride_4q/test_stage_02_threat_identification.py` with 20 tests: all context types, STRIDE reference injection, OWASP reference injection, scanner present/absent/empty, prior stage injection/absent/empty, references absent/empty, output file path, custom output dir, trailing slash normalization, placeholder absence, 4QF context, multiple scanners
- Files changed: `src/threatsmith/frameworks/stride_4q/stage_02_threat_identification.py` (new), `tests/stride_4q/test_stage_02_threat_identification.py` (new)
- **Learnings for future iterations:**
  - Stage 2 uses three placeholders: `{prior_stages_section}`, `{references_section}`, `{scanner_section}` — follows same `.replace()` pattern as PASTA stages
  - References are injected via `context.references` (pre-resolved by assembler); scanners via `context.scanners_available` with `SCANNER_SNIPPETS` lookup — same pattern as PASTA stage 5
  - Stage 2 is the scanner stage for 4QF+STRIDE (`scanner_stages=[2]` in pack config US-F16)
---

## 2026-03-19 - US-F11
- Created `src/threatsmith/frameworks/stride_4q/stage_01_system_model.py` with `STAGE_PROMPT` constant and `build_prompt` function
- Prompt covers all 8 analysis areas from the acceptance criteria: application purpose/scope, technology stack, data flows, actors/assets, trust boundaries, entry points, external dependencies, deployment context
- Prompt requires at least one Mermaid DFD with example syntax
- Prompt references the Four Question Framework ("What are we working on?")
- Created `tests/stride_4q/test_stage_01_system_model.py` with 15 tests: objectives injection (with/without/both/none/empty), output file path, custom output dir, trailing slash normalization, placeholder absence, Mermaid instruction, 4QF context
- Created `src/threatsmith/frameworks/stride_4q/__init__.py` (empty, package marker)
- Files changed: `src/threatsmith/frameworks/stride_4q/__init__.py` (new), `src/threatsmith/frameworks/stride_4q/stage_01_system_model.py` (new), `tests/stride_4q/__init__.py` (new), `tests/stride_4q/test_stage_01_system_model.py` (new)
- **Learnings for future iterations:**
  - The `stride_4q/__init__.py` starts empty; `build_stride_4q_pack()` will be added later (US-F16)
  - 4QF+STRIDE stages follow the exact same `build_prompt` pattern as PASTA stages — copy the structure and adapt content
  - Stage 1 is the only 4QF+STRIDE stage with user objectives injection; stages 2-4 use prior outputs instead
---

## 2026-03-19 - US-F09 and US-F10
- Both orchestrator and assembler were already fully implemented from the Deliverable A restructure
- Missing: test coverage for "mock 3-stage pack" (US-F09) and "framework-agnostic assembly with non-PASTA pack" (US-F10)
- Added to `tests/test_orchestrator.py`: `_build_mock_prompt`, `_make_mock_pack`, `test_mock_3stage_pack_full_pipeline_success`, `test_mock_pack_output_validation_uses_framework_filenames`, `test_mock_pack_context_accumulates`
- Added to `tests/test_assembler.py`: `TestFrameworkAgnosticAssembly` class (5 tests) using a custom `FrameworkPack` with inline `build_prompt` closures to verify scanner injection, non-scanner exclusion, always/api_detected/llm_detected reference conditions
- Files changed: `tests/test_orchestrator.py`, `tests/test_assembler.py`
- **Learnings for future iterations:**
  - Use a dict `{"n": 0}` (not a bare int) for mutable call counters in `execute_side_effect` closures — Python closures can't rebind outer int variables with `=`, but can mutate dict values
  - Mock pack `build_prompt` functions must use `def` (not lambda) to satisfy ruff E731
  - The orchestrator key format is `stage_{number:02d}_output` (e.g. `stage_01_output`), NOT `stage.output_file` — this is what `build_prompt` functions expect when looking up prior context
---

## 2026-03-19 - PASTA test reorganization
- Moved all 8 PASTA stage test files from `tests/` root into `tests/pasta/` subdirectory
- Created `tests/pasta/__init__.py` to make it a proper package
- Updated `CLAUDE.md` testing patterns to document the `tests/<framework>/` convention
- Fixed stale `prompts/references/stride_categories.py` path in progress.txt US-F05 entry (correct path: `frameworks/references/`)
- All 299 tests pass
- **Convention established**: framework-specific stage tests go in `tests/<framework>/`; top-level `tests/` is for cross-cutting tests (assembler, orchestrator, CLI, e2e, etc.)
---

## 2026-03-19 - Architecture Restructure (Deliverable A)
- Eliminated `src/threatsmith/prompts/` package entirely — all framework code now under `frameworks/`
- Added `StageContext` generic dataclass to `frameworks/types.py`, replacing 8 per-stage typed context dataclasses
- Migrated all 8 PASTA `build_prompt` functions from per-stage typed contexts to `StageContext`
- Centralized OWASP reference injection: moved keyword detection from PASTA stage_04's `build_prompt` to the assembler via `evaluate_reference_conditions()` + `pack.reference_sets`
- Moved assembler to `src/threatsmith/assembler.py` (top-level), rewrote as framework-agnostic: accepts `StageSpec` + `FrameworkPack`
- Made orchestrator framework-agnostic: accepts `pack: FrameworkPack`, iterates `pack.stages + [pack.report_stage]`
- Moved `prompts/references/` → `frameworks/references/`, `prompts/pasta/` → `frameworks/pasta/`
- Updated `main.py` to call `get_framework("pasta")` and pass pack to orchestrator
- Eliminated circular import between `frameworks/` and `prompts/` — clean DAG import graph
- Updated all test imports to use `frameworks.*` paths
- All 299 tests pass, lint and format clean
- Files deleted: entire `src/threatsmith/prompts/` package (including `contexts.py`, `assembler.py`, `__init__.py`, all subpackages)
- Files created: `src/threatsmith/assembler.py`, `src/threatsmith/frameworks/references/` (copied from prompts/references/), `src/threatsmith/frameworks/pasta/` (moved from prompts/pasta/)
- **Learnings for future iterations:**
  - Pack builders (`_pack.py`) import from `frameworks.types` directly (sibling), not from `frameworks/__init__` — this avoids cycles
  - `StageContext.references` is a `list[str]` of pre-resolved reference strings; the assembler populates it from `pack.reference_sets` via `evaluate_reference_conditions()`
  - No backward-compat layer for `prompts/` — clean break since this is pre-release
---

## 2026-03-19 - US-F05
- Created `src/threatsmith/frameworks/references/stride_categories.py` exporting `STRIDE_CATEGORIES` string constant
- Markdown table with bold full category names (e.g. `**Spoofing**`) — not split-bold like `**S**poofing`, which breaks substring tests
- Added 7 tests in `tests/test_stride_categories.py` verifying all six category names and all six initial letters
- Files changed: `src/threatsmith/frameworks/references/stride_categories.py` (new), `tests/test_stride_categories.py` (new)
- **Learnings for future iterations:**
  - Use `**FullName**` markdown bold in table cells — splitting bold on the first letter (`**S**poofing`) breaks `assert "Spoofing" in constant` tests since the word is no longer a contiguous string
---

## 2026-03-19 - US-F08
- Moved all 8 PASTA stage modules from `prompts/` to `prompts/pasta/`
- Implemented `build_pasta_pack()` in `prompts/pasta/__init__.py` with 7 analysis stages + report stage, `scanner_stages=[5]`, and `reference_sets` for stage 4 (OWASP Web always, API api_detected, LLM llm_detected)
- Added backward-compat re-exports in `prompts/__init__.py` so `from threatsmith.prompts import stage_XX` still works
- Updated `frameworks/_built_in.py` to call `build_pasta_pack()` instead of using placeholder stub
- Updated test imports for all stage test files (`from threatsmith.prompts.pasta.stage_XX import ...`)
- Files changed: `prompts/pasta/__init__.py`, `prompts/__init__.py`, `frameworks/_built_in.py`, all `prompts/pasta/stage_0*.py` (moved), all `tests/test_stage_*.py` (import paths updated)
- **Learnings for future iterations:**
  - `build_pasta_pack()` uses lazy imports for `FrameworkPack`/`StageSpec` inside the function body — if those imports were at module level, they'd trigger a circular import: `prompts.pasta` → `frameworks.models` → `frameworks.__init__` → `_built_in` → `prompts.pasta`
  - `prompts/__init__.py` re-exports submodules as attributes; `from pkg import submodule` works if the module is an attribute, but `from pkg.submodule import X` requires the actual file path — backward compat re-exports only work for the `from pkg import submodule` form
---

## 2026-03-18 - US-F04
- Implemented `evaluate_reference_conditions(reference_config, prior_outputs)` in `src/threatsmith/prompts/references/__init__.py`
- Supports three conditions: `always` (unconditional), `api_detected` (keyword match), `llm_detected` (keyword match)
- `API_KEYWORDS` and `LLM_KEYWORDS` are module-level constants per acceptance criteria
- Added 12 tests in `tests/test_references.py`
- Files changed: `src/threatsmith/prompts/references/__init__.py`, `tests/test_references.py` (new)
- **Learnings for future iterations:**
  - `reference_config` is a `list[dict]` with `"condition"` and `"reference"` keys — this is the dict shape expected by `evaluate_reference_conditions`
  - The function combines all `prior_outputs` values (joined by space, lowercased) for a single keyword scan pass — efficient for multiple conditions
  - `mobile_detected` is NOT implemented here (not in acceptance criteria); PASTA stage 4 still handles mobile detection internally with its own `_MOBILE_KEYWORDS`

---

## 2026-03-18 - US-F03
- Created `src/threatsmith/prompts/references/` package with `__init__.py`, `owasp.py`, `scanner_snippets.py`
- Created empty `__init__.py` packages for `prompts/stride_4q/`, `prompts/pasta/`, `prompts/linddun/`, `prompts/maestro/`
- Updated `stage_04_threat_analysis.py` to import from `threatsmith.prompts.references.owasp`
- Updated `stage_05_vulnerability.py` to import from `threatsmith.prompts.references.scanner_snippets`
- Removed `prompts/owasp_references.py` and `prompts/scanner_snippets.py`
- Files changed: `src/threatsmith/prompts/references/__init__.py` (new), `src/threatsmith/prompts/references/owasp.py` (new), `src/threatsmith/prompts/references/scanner_snippets.py` (new), `src/threatsmith/prompts/stride_4q/__init__.py` (new), `src/threatsmith/prompts/pasta/__init__.py` (new), `src/threatsmith/prompts/linddun/__init__.py` (new), `src/threatsmith/prompts/maestro/__init__.py` (new), `src/threatsmith/prompts/stage_04_threat_analysis.py`, `src/threatsmith/prompts/stage_05_vulnerability.py`
- **Learnings for future iterations:**
  - The new `prompts/references/` package is the canonical home for all shared reference constants (OWASP, scanner snippets, and future STRIDE/LINDDUN/ATLAS constants)
  - Framework subdirectory `__init__.py` files start empty; pack builder functions are added later (US-F08, F16, F23, F31)
---

## 2026-03-18 - US-F02
- Added `list_frameworks() -> list[FrameworkPack]` to `frameworks/models.py`
- Created `frameworks/_built_in.py` with placeholder `FrameworkPack` registrations for all four frameworks (stride-4q, pasta, linddun, maestro)
- Updated `frameworks/__init__.py` to import `_built_in` (triggers registration) and re-export `list_frameworks`
- Added 5 tests in `TestListFrameworks` class to `tests/test_frameworks.py`
- Files changed: `src/threatsmith/frameworks/models.py`, `src/threatsmith/frameworks/__init__.py`, `src/threatsmith/frameworks/_built_in.py` (new), `tests/test_frameworks.py`
- **Learnings for future iterations:**
  - `_built_in.py` uses side-effect import pattern — importing the module registers packs. The `clean_registry` fixture snapshots/restores the registry so these registrations don't interfere with tests that manually register their own packs.
  - When replacing placeholder packs (US-F08, F16, F23, F31), update `_built_in.py` to import from the prompts subdirectory pack builders instead of defining inline stubs.
---

## 2026-03-18 - US-F01
- Implemented framework pack data model
- Created `src/threatsmith/frameworks/models.py` with `StageSpec`, `FrameworkPack` dataclasses, `_REGISTRY` dict, `register_framework()`, and `get_framework()`
- Created `src/threatsmith/frameworks/__init__.py` re-exporting the public API
- Created `tests/test_frameworks.py` with 11 tests covering data model construction, `get_framework` for all four valid names, and invalid name error
- **Learnings for future iterations:**
  - Put data model in a named module (`models.py`) rather than `__init__.py` for readability; `__init__.py` just re-exports
  - Registry fixture pattern: `autouse` fixture snapshots `_REGISTRY` before each test and restores it after — prevents test pollution when registering stubs
  - `get_framework` error message lists available frameworks alphabetically using `sorted(_REGISTRY.keys())`
---
