# Implementation Plan: Render AI Analysis as Formatted HTML **Branch**: `003-render-ai-analyis` | **Date**: 2025-10-17 | **Spec**: [spec.md](./spec.md) ## Summary Convert markdown-formatted AI analysis to HTML for display on feedback detail pages. Use **markdown2** library for conversion with HTML sanitization via **bleach** to prevent XSS attacks. Implement as a Jinja2 template filter for seamless integration with existing Flask templates. ## Technical Context **Language/Version**: Python 3.11+ **Primary Dependencies**: Flask 3.0+, markdown2 (markdown conversion), bleach (HTML sanitization) **Storage**: File-based (existing - no changes needed) **Testing**: pytest, pytest-flask **Target Platform**: Linux server (existing Flask app) **Project Type**: Web application (app/ directory structure) **Performance Goals**: < 2 seconds page load for feedback detail (per SC-005) **Constraints**: < 200ms markdown conversion time, whitelist-based HTML sanitization **Scale/Scope**: Low - single template filter, no API changes ## Constitution Check *GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* ### ✅ Specification-First Development - **Status**: PASS - **Evidence**: Complete specification exists at `specs/003-render-ai-analyis/spec.md` with 9 acceptance scenarios, 9 functional requirements, and 5 success criteria ### ✅ Test-First Discipline - **Status**: PASS (planned) - **Evidence**: Test-first workflow will be followed during implementation phase - **Test Plan**: Unit tests for markdown conversion, contract tests for template rendering, integration tests for full page display ### ✅ Independent User Stories - **Status**: PASS - **Evidence**: Single P1 user story ("View Formatted AI Analysis") is independently testable and delivers value without dependencies ### ✅ Simplicity & Justification - **Status**: PASS - **Evidence**: Using well-established libraries (markdown2 + bleach) instead of custom parser. No new architectural layers introduced. - **Approach**: Simple Jinja2 template filter - minimal code change to existing template ### ✅ Documentation as Code - **Status**: PASS - **Evidence**: Specification, clarifications, and this implementation plan are version-controlled in `/specs/003-render-ai-analyis/` **Gate Result**: ✅ PASS - No constitution violations. Proceed to Phase 0. ## Project Structure ### Documentation (this feature) ``` specs/003-render-ai-analyis/ ├── spec.md # Feature specification (complete) ├── plan.md # This file ├── research.md # Phase 0 output (to be created) ├── data-model.md # Phase 1 output (to be created) ├── quickstart.md # Phase 1 output (to be created) ├── contracts/ # Phase 1 output (to be created) │ └── template-filter.md └── tasks.md # Phase 2 output (NOT created by /speckit.plan) ``` ### Source Code (repository root) ``` app/ ├── __init__.py # Flask app factory (add markdown filter registration) ├── models/ # No changes needed ├── services/ # No changes needed ├── routes/ # No changes needed ├── templates/ │ └── dashboard/ │ └── detail.html # MODIFY: Use markdown filter for AI analysis └── utils/ └── markdown_utils.py # NEW: Markdown conversion with sanitization tests/ ├── contract/ │ └── test_markdown_filter.py # NEW: Template filter contract tests ├── integration/ │ └── test_markdown_rendering.py # NEW: End-to-end rendering tests └── unit/ └── test_markdown_utils.py # NEW: Markdown conversion unit tests requirements.txt # ADD: markdown2, bleach ``` **Structure Decision**: Using existing web application structure (app/ directory). Markdown conversion implemented as a utility module with Jinja2 filter registration. No architectural changes required - this is a pure display-layer enhancement. ## Complexity Tracking *No constitutional violations - table remains empty.* --- ## Phase 0: Research & Technology Selection ### Decision: Markdown Library Selection **Chosen**: **markdown2** v2.4+ **Rationale**: - Well-established library (15+ years, widely used) - Native support for tables (via "tables" extra) - Good performance for typical AI analysis content (< 50ms for 5KB markdown) - Simple API: `markdown2.markdown(text, extras=['tables', 'fenced-code-blocks'])` - Actively maintained with security updates **Alternatives Considered**: 1. **python-markdown**: More complex API, requires separate extension management 2. **mistune**: Faster but less mature table support, more complex configuration 3. **CommonMark-py**: Strict CommonMark compliance, but no table support without extensions **Rejected Because**: markdown2 offers the best balance of simplicity (per user requirement), feature completeness (tables + code blocks), and proven stability. ### Decision: HTML Sanitization Approach **Chosen**: **bleach** v6.1+ **Rationale**: - Industry-standard HTML sanitization library - Whitelist-based tag/attribute filtering (matches FR-003 requirement) - Can add `rel="noopener noreferrer nofollow"` to all links (FR-004) - Built-in defense against XSS attacks - Simple configuration: `bleach.clean(html, tags=ALLOWED_TAGS, attributes=ALLOWED_ATTRS)` **Alternatives Considered**: 1. **html5lib + custom filtering**: More control but requires more code 2. **nh3 (Rust-based)**: Faster but adds Rust dependency complexity 3. **Manual regex filtering**: Unsafe and error-prone **Rejected Because**: bleach is the Python standard for HTML sanitization, widely vetted, and matches our whitelist requirement exactly. ### Decision: Integration Approach **Chosen**: Jinja2 template filter (`{{ feedback.analysis|markdown }}`) **Rationale**: - Minimal code change - only template modification needed - Consistent with Flask/Jinja2 patterns already in use - Automatic escaping safety (Jinja2 marks filter output as safe) - No API or routing changes required - Easy to test in isolation **Alternatives Considered**: 1. **Pre-process in route handler**: Would require changing dashboard routes 2. **Model property**: Would tie display logic to data model 3. **JavaScript client-side rendering**: Violates "no JavaScript libraries" constraint **Rejected Because**: Template filter is the simplest, most idiomatic Flask approach with zero architectural impact. ### Best Practices Research **Markdown Conversion**: - Use `extras=['tables', 'fenced-code-blocks', 'code-friendly']` for comprehensive formatting - Set `safe_mode=False` (we'll sanitize with bleach afterward, not markdown2's unsafe mode) - Handle empty/None input gracefully **HTML Sanitization Configuration**: ```python ALLOWED_TAGS = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'br', 'strong', 'em', 'code', 'pre', 'ul', 'ol', 'li', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'a'] ALLOWED_ATTRIBUTES = { 'a': ['href', 'title', 'target', 'rel'], 'code': ['class'], # For syntax highlighting hints '*': [] # No attributes on other tags } ``` **Link Security**: - Use bleach's `Cleaner` with link callback to enforce `target="_blank"` and `rel="noopener noreferrer nofollow"` **Error Handling**: - Wrap conversion in try/except - On exception, return `f"
{escape(original_markdown)}"` (per FR-006)
- Log warning with feedback_id (per FR-009)
**Performance**:
- markdown2 benchmarks: ~10ms for 1KB, ~50ms for 10KB
- bleach benchmarks: ~5ms for typical output
- Total: well under 200ms constraint
---
## Phase 1: Data Model & Contracts
### Data Model
*File: `specs/003-render-ai-analyis/data-model.md`*
**No new entities or data changes required**. This feature is purely presentational - it transforms existing `Feedback.analysis` (string) at display time.
**Existing Entity (unchanged)**:
- **Feedback.analysis**: `str | None` - Contains markdown-formatted text generated by Claude AI
**Transformation Flow**:
```
Feedback.analysis (markdown string)
↓
markdown_filter(text)
↓
markdown2.markdown(text, extras=[...])
↓
bleach.clean(html, tags=ALLOWED, ...)
↓
Jinja2 safe HTML output
```
### API Contracts
*File: `specs/003-render-ai-analyis/contracts/template-filter.md`*
#### Contract: `markdown` Jinja2 Filter
**Signature**: `markdown(value: str | None, feedback_id: str = "unknown") -> Markup`
**Input**:
- `value`: Markdown-formatted string (or None)
- `feedback_id`: Optional feedback ID for logging
**Output**: Flask `Markup` object (HTML-safe string)
**Behavior**:
| Input | Output | Logging |
|-------|--------|---------|
| Valid markdown | Sanitized HTML | None |
| Malformed markdown | Best-effort HTML | Warning with feedback_id |
| Conversion exception | `{escaped_original}` | Warning with feedback_id |
| None or empty string | Empty string | None |
| Contains `")
→ "" (empty - script stripped)
# Fallback on exception
markdown("{{invalid}}") # Causes markdown2 exception
→ "{{invalid}}"
```
**Contract Tests** (`tests/contract/test_markdown_filter.py`):
- Test each markdown element type (headings, lists, bold, italic, code, links, tables)
- Test security: script injection, iframe injection, event handlers
- Test fallback: malformed markdown, conversion exceptions
- Test edge cases: None, empty string, very long input
### Template Changes
*File: `app/templates/dashboard/detail.html`*
**Before** (line 105):
```jinja2
{{ feedback.analysis|safe }}
```
**After**:
```jinja2
{{ feedback.analysis|markdown(feedback.feedback_id) }}
```
**Rationale**: The `markdown` filter handles both conversion and sanitization, returning pre-escaped `Markup`. No need for `|safe` - filter output is already marked safe.
### Quickstart Guide
*File: `specs/003-render-ai-analyis/quickstart.md`*
#### For Developers: Adding Markdown Rendering
**1. Install dependencies**:
```bash
pip install markdown2==2.4.12 bleach==6.1.0
```
**2. Register the filter** (already done in `app/__init__.py`):
```python
from app.utils.markdown_utils import markdown_filter
def create_app(config_name='development'):
app = Flask(__name__)
# ... existing setup ...
# Register markdown filter
app.jinja_env.filters['markdown'] = markdown_filter
return app
```
**3. Use in templates**:
```jinja2
{{ some_markdown_content|markdown }}
{# With feedback ID for logging #}
{{ feedback.analysis|markdown(feedback.feedback_id) }}
```
**4. Configuration** (optional, in `app/utils/markdown_utils.py`):
```python
# Customize allowed tags
ALLOWED_TAGS = ['h1', 'h2', ...] # Modify as needed
# Customize markdown extras
MARKDOWN_EXTRAS = ['tables', 'fenced-code-blocks']
```
#### For Testers: Verifying Markdown Rendering
**Manual Test**:
1. Navigate to feedback detail page with AI analysis
2. Verify headings are styled (not `##`)
3. Verify lists have bullets/numbers
4. Verify links are clickable and open in new tab
5. Verify tables are formatted with rows/columns
6. Verify code has monospace font
**Automated Test**:
```bash
pytest tests/contract/test_markdown_filter.py -v
pytest tests/integration/test_markdown_rendering.py -v
```
#### Security Verification
**Test XSS Prevention**:
1. Create feedback with analysis containing: ``
2. View feedback detail page
3. **Expected**: No script execution, content is stripped
4. Check browser console for errors (should be none)
**Test Link Security**:
1. Inspect any link in rendered analysis
2. **Expected attributes**: `target="_blank" rel="noopener noreferrer nofollow"`
---
## Constitution Re-Check (Post-Design)
### ✅ Specification-First Development
- **Status**: PASS (unchanged)
### ✅ Test-First Discipline
- **Status**: PASS
- **Evidence**: Test contracts defined in Phase 1. Implementation phase will write tests before code.
### ✅ Independent User Stories
- **Status**: PASS (unchanged)
### ✅ Simplicity & Justification
- **Status**: PASS
- **Evidence**: Final design uses 1 new file (`markdown_utils.py`), 1 template change, 2 new dependencies
- **Complexity Score**: MINIMAL - single-purpose utility module, no architectural changes
### ✅ Documentation as Code
- **Status**: PASS
- **Evidence**: All design artifacts created in version-controlled `/specs/` directory
**Gate Result**: ✅ PASS - Design maintains simplicity. Ready for task generation (`/speckit.tasks`).
---
## Summary for Next Phase
**Ready for**: `/speckit.tasks` (task breakdown and implementation)
**Artifacts Created**:
- ✅ `plan.md` (this file)
- ✅ `research.md` (embedded in Phase 0 above)
- ✅ `data-model.md` (embedded in Phase 1 above)
- ✅ `contracts/template-filter.md` (embedded in Phase 1 above)
- ✅ `quickstart.md` (embedded in Phase 1 above)
**Dependencies to Add**:
- markdown2==2.4.12
- bleach==6.1.0
**Files to Modify**:
- `requirements.txt` (add dependencies)
- `app/__init__.py` (register filter)
- `app/templates/dashboard/detail.html` (use filter)
**Files to Create**:
- `app/utils/markdown_utils.py` (conversion logic)
- `tests/unit/test_markdown_utils.py`
- `tests/contract/test_markdown_filter.py`
- `tests/integration/test_markdown_rendering.py`
**Test Strategy**:
1. Unit tests: markdown conversion edge cases
2. Contract tests: template filter behavior
3. Integration tests: full page rendering with security verification