that initial commit
This commit is contained in:
97
docs/decisions/ADR-001-python-web-framework.md
Normal file
97
docs/decisions/ADR-001-python-web-framework.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# ADR-001: Python Web Framework Selection
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
StarPunk requires a Python web framework to implement the API-first architecture with RESTful endpoints, Micropub support, IndieAuth integration, and web interface. The framework must support both API and server-side rendered HTML with minimal complexity.
|
||||
|
||||
## Decision
|
||||
Use **Flask** as the primary web framework.
|
||||
|
||||
## Rationale
|
||||
|
||||
### Simplicity Score: 9/10
|
||||
- Minimal boilerplate code required
|
||||
- Explicit routing and request handling
|
||||
- Easy to understand for newcomers
|
||||
- Core framework is ~1000 lines of code
|
||||
- Follows "micro-framework" philosophy aligned with StarPunk principles
|
||||
|
||||
### Fitness Score: 10/10
|
||||
- Perfect for single-user applications
|
||||
- Built-in development server
|
||||
- Excellent template engine (Jinja2) for HTML generation
|
||||
- Simple decorator-based routing
|
||||
- Easy integration with SQLite
|
||||
- Native support for both JSON APIs and HTML rendering
|
||||
- Werkzeug provides robust HTTP utilities
|
||||
- Blueprint support for code organization
|
||||
|
||||
### Maintenance Score: 9/10
|
||||
- Extremely mature (13+ years)
|
||||
- Large community and extensive documentation
|
||||
- Stable API with minimal breaking changes
|
||||
- Extensive ecosystem of well-tested extensions
|
||||
- Active development and security updates
|
||||
|
||||
### Standards Compliance: Pass
|
||||
- Standard WSGI interface
|
||||
- Full HTTP status code support
|
||||
- Proper content-type handling
|
||||
- Easy CORS implementation
|
||||
- Session management built-in
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- Minimal learning curve
|
||||
- Small dependency footprint
|
||||
- Easy to test (built-in test client)
|
||||
- Flexible enough for API-first architecture
|
||||
- Can render HTML templates for public interface
|
||||
- Easy deployment (WSGI compatible)
|
||||
|
||||
### Negative
|
||||
- No built-in ORM (but we're using raw SQLite, so this is actually positive)
|
||||
- Requires manual selection of extensions
|
||||
- Less opinionated than larger frameworks
|
||||
|
||||
### Mitigation
|
||||
- Extension selection will be minimal (see ADR-002 for extensions)
|
||||
- Lack of opinion allows us to stay minimal
|
||||
- Manual configuration gives us full control
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### FastAPI (Rejected)
|
||||
- **Simplicity**: 6/10 - Requires async/await understanding, Pydantic models
|
||||
- **Fitness**: 7/10 - Overkill for single-user CMS, async not needed
|
||||
- **Maintenance**: 8/10 - Newer framework, but growing
|
||||
- **Verdict**: Too complex for project needs, async unnecessary
|
||||
|
||||
### Django (Rejected)
|
||||
- **Simplicity**: 3/10 - Large framework with heavy abstractions
|
||||
- **Fitness**: 4/10 - Designed for multi-user applications, includes admin panel, ORM, and many features we don't need
|
||||
- **Maintenance**: 10/10 - Excellent maintenance and security
|
||||
- **Verdict**: Violates "minimal code" principle, too much unnecessary functionality
|
||||
|
||||
### Bottle (Considered)
|
||||
- **Simplicity**: 10/10 - Single file framework
|
||||
- **Fitness**: 7/10 - Very minimal, but perhaps too minimal
|
||||
- **Maintenance**: 6/10 - Smaller community, slower updates
|
||||
- **Verdict**: Close second, but Flask has better ecosystem for IndieAuth/Micropub
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Flask will be used with:
|
||||
- Jinja2 templates for HTML rendering (included with Flask)
|
||||
- Werkzeug for HTTP utilities (included with Flask)
|
||||
- Minimal extensions only (see ADR-002)
|
||||
- Standard WSGI deployment
|
||||
- Blueprint organization for clear separation of concerns
|
||||
|
||||
## References
|
||||
- Flask Documentation: https://flask.palletsprojects.com/
|
||||
- WSGI Specification: https://peps.python.org/pep-3333/
|
||||
- Flask Design Decisions: https://flask.palletsprojects.com/en/3.0.x/design/
|
||||
134
docs/decisions/ADR-002-flask-extensions.md
Normal file
134
docs/decisions/ADR-002-flask-extensions.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# ADR-002: Flask Extensions and Dependencies
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
Flask is intentionally minimal. We need to select only essential extensions that align with the "minimal code" philosophy while supporting required functionality.
|
||||
|
||||
## Decision
|
||||
Use the following minimal set of dependencies:
|
||||
- **Flask** - Core framework
|
||||
- **markdown** - Markdown to HTML conversion
|
||||
- **feedgen** - RSS feed generation
|
||||
- **httpx** - HTTP client for IndieAuth verification
|
||||
- **python-dotenv** - Environment configuration
|
||||
- **pytest** - Testing framework
|
||||
|
||||
**NO additional Flask extensions** will be used in V1.
|
||||
|
||||
## Rationale
|
||||
|
||||
### Core Dependencies
|
||||
|
||||
#### markdown
|
||||
- **Purpose**: Convert markdown notes to HTML
|
||||
- **Simplicity**: Pure Python, simple API
|
||||
- **Justification**: Core requirement for note rendering
|
||||
- **Alternative**: mistune (faster but less standard)
|
||||
- **Verdict**: markdown is more standard and sufficient for single-user
|
||||
|
||||
#### feedgen
|
||||
- **Purpose**: Generate valid RSS 2.0 feeds
|
||||
- **Simplicity**: High-level API, handles all RSS requirements
|
||||
- **Justification**: Ensures RSS 2.0 compliance without manual XML generation
|
||||
- **Alternative**: Manual XML generation (error-prone)
|
||||
- **Verdict**: feedgen guarantees valid RSS output
|
||||
|
||||
#### httpx
|
||||
- **Purpose**: HTTP client for IndieAuth endpoint verification
|
||||
- **Simplicity**: Modern, clean API
|
||||
- **Justification**: Need to verify IndieAuth endpoints and fetch client metadata
|
||||
- **Alternative**: requests (synchronous only), urllib (too low-level)
|
||||
- **Verdict**: httpx provides clean API and can be sync or async if needed later
|
||||
|
||||
#### python-dotenv
|
||||
- **Purpose**: Load environment variables from .env file
|
||||
- **Simplicity**: Single-purpose, simple API
|
||||
- **Justification**: Standard pattern for configuration management
|
||||
- **Alternative**: Manual environment variable handling
|
||||
- **Verdict**: Industry standard, minimal overhead
|
||||
|
||||
#### pytest
|
||||
- **Purpose**: Testing framework
|
||||
- **Simplicity**: Minimal boilerplate, clear assertions
|
||||
- **Justification**: Required for test coverage
|
||||
- **Alternative**: unittest (more verbose), nose2 (unmaintained)
|
||||
- **Verdict**: pytest is current Python testing standard
|
||||
|
||||
### Extensions REJECTED for V1
|
||||
|
||||
#### Flask-SQLAlchemy (Rejected)
|
||||
- **Reason**: Adds ORM abstraction we don't need
|
||||
- **Decision**: Use sqlite3 standard library directly
|
||||
- **Benefit**: Simpler code, explicit queries, no magic
|
||||
|
||||
#### Flask-Login (Rejected)
|
||||
- **Reason**: Session-based authentication, we need token-based
|
||||
- **Decision**: Implement simple token validation ourselves
|
||||
- **Benefit**: Full control over IndieAuth flow
|
||||
|
||||
#### Flask-CORS (Rejected)
|
||||
- **Reason**: Single function decorator, don't need extension
|
||||
- **Decision**: Use @after_request decorator for CORS headers
|
||||
- **Benefit**: 5 lines of code vs. another dependency
|
||||
|
||||
#### Flask-Limiter (Rejected for V1)
|
||||
- **Reason**: Rate limiting is nice-to-have, not critical for single-user
|
||||
- **Decision**: Defer to V2 or rely on reverse proxy
|
||||
- **Benefit**: Reduced complexity
|
||||
|
||||
#### Flask-WTF (Rejected)
|
||||
- **Reason**: Form handling for single form (note creation) is overkill
|
||||
- **Decision**: Simple HTML forms with manual validation
|
||||
- **Benefit**: No CSRF complexity in V1, manual validation is clear
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- Minimal dependency tree
|
||||
- Full control over implementation
|
||||
- Easy to understand codebase
|
||||
- Fast installation and startup
|
||||
- Reduced attack surface
|
||||
|
||||
### Negative
|
||||
- Must implement some features manually (token validation, CORS)
|
||||
- No form CSRF protection in V1 (acceptable for single-user)
|
||||
- Manual SQL queries required
|
||||
|
||||
### Mitigation
|
||||
- Document manual implementations clearly
|
||||
- Ensure manual code is well-tested
|
||||
- Keep manual implementations simple and obvious
|
||||
- Plan to add CSRF in V2 if needed
|
||||
|
||||
## Complete Dependency List
|
||||
|
||||
```
|
||||
Flask==3.0.*
|
||||
markdown==3.5.*
|
||||
feedgen==1.0.*
|
||||
httpx==0.27.*
|
||||
python-dotenv==1.0.*
|
||||
pytest==8.0.*
|
||||
```
|
||||
|
||||
## Development Dependencies
|
||||
```
|
||||
pytest-cov # Test coverage reporting
|
||||
black # Code formatting
|
||||
flake8 # Linting
|
||||
```
|
||||
|
||||
## Standards Compliance
|
||||
- All dependencies are pure Python or have minimal C extensions
|
||||
- All are actively maintained with security updates
|
||||
- All support Python 3.11+
|
||||
- Total dependency count: 6 direct dependencies (excluding dev tools)
|
||||
|
||||
## References
|
||||
- Flask Extensions: https://flask.palletsprojects.com/en/3.0.x/extensions/
|
||||
- Markdown Spec: https://daringfireball.net/projects/markdown/
|
||||
- RSS 2.0: https://www.rssboard.org/rss-specification
|
||||
- Python Packaging: https://packaging.python.org/
|
||||
289
docs/decisions/ADR-003-frontend-technology.md
Normal file
289
docs/decisions/ADR-003-frontend-technology.md
Normal file
@@ -0,0 +1,289 @@
|
||||
# ADR-003: Front-end Technology Stack
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
StarPunk requires a front-end for:
|
||||
1. Public interface (homepage, note permalinks) - Server-side rendered
|
||||
2. Admin interface (note creation/editing) - Requires some interactivity
|
||||
3. Progressive enhancement principle - Core functionality must work without JavaScript
|
||||
|
||||
The front-end must be minimal, elegant, and align with the "no client-side complexity" principle stated in CLAUDE.MD.
|
||||
|
||||
## Decision
|
||||
|
||||
### Public Interface: Server-Side Rendered HTML
|
||||
- **Template Engine**: Jinja2 (included with Flask)
|
||||
- **CSS**: Custom CSS (no framework)
|
||||
- **JavaScript**: None required for V1
|
||||
- **Build Tools**: None required
|
||||
|
||||
### Admin Interface: Enhanced Server-Side Rendering
|
||||
- **Template Engine**: Jinja2 (included with Flask)
|
||||
- **CSS**: Custom CSS (shared with public interface)
|
||||
- **JavaScript**: Minimal vanilla JavaScript for markdown preview only
|
||||
- **Build Tools**: None required
|
||||
|
||||
### Asset Management
|
||||
- **CSS**: Single stylesheet served statically
|
||||
- **JavaScript**: Single optional file for markdown preview
|
||||
- **No bundler**: Direct file serving
|
||||
- **No transpilation**: Modern browsers only (ES6+)
|
||||
|
||||
## Rationale
|
||||
|
||||
### Server-Side Rendering (SSR)
|
||||
**Simplicity Score: 10/10**
|
||||
- Zero build process
|
||||
- No JavaScript framework complexity
|
||||
- Direct Flask template rendering
|
||||
- Familiar Jinja2 syntax
|
||||
|
||||
**Fitness Score: 10/10**
|
||||
- Perfect for content-first site
|
||||
- Faster initial page load
|
||||
- Better SEO (though not critical for single-user)
|
||||
- Works without JavaScript
|
||||
- Easier to implement microformats
|
||||
|
||||
**Maintenance Score: 10/10**
|
||||
- Jinja2 is stable and mature
|
||||
- No framework version updates
|
||||
- No npm dependency hell
|
||||
- Templates are simple HTML
|
||||
|
||||
### No CSS Framework
|
||||
**Simplicity Score: 10/10**
|
||||
- Custom CSS is ~200 lines for entire site
|
||||
- No unused classes or styles
|
||||
- Full control over appearance
|
||||
- No framework learning curve
|
||||
|
||||
**Fitness Score: 9/10**
|
||||
- StarPunk needs minimal, elegant design
|
||||
- Single theme, no customization needed
|
||||
- Mobile-responsive can be achieved with simple media queries
|
||||
- No complex components needed
|
||||
|
||||
### Minimal JavaScript Approach
|
||||
**Simplicity Score: 9/10**
|
||||
- Vanilla JavaScript only (no React/Vue/Svelte)
|
||||
- Single purpose: markdown preview in admin
|
||||
- Optional progressive enhancement
|
||||
- No build step required
|
||||
|
||||
**Fitness Score: 10/10**
|
||||
- Markdown preview improves UX but isn't required
|
||||
- All functionality works without JavaScript
|
||||
- Can use fetch API for preview without library
|
||||
- Modern browser features are sufficient
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- Zero build time
|
||||
- No node_modules directory
|
||||
- Instant development setup
|
||||
- Fast page loads
|
||||
- Works with JavaScript disabled
|
||||
- Easy to understand and modify
|
||||
- Microformats implementation is straightforward
|
||||
- Complete control over HTML output
|
||||
|
||||
### Negative
|
||||
- No TypeScript type checking
|
||||
- No hot module replacement (but Flask auto-reload works)
|
||||
- Manual CSS organization required
|
||||
- Must write responsive CSS manually
|
||||
|
||||
### Mitigation
|
||||
- Keep JavaScript minimal and well-commented
|
||||
- Organize CSS with clear sections
|
||||
- Use CSS custom properties for theming
|
||||
- Test manually in multiple browsers
|
||||
- Validate HTML with W3C validator
|
||||
|
||||
## Frontend File Structure
|
||||
|
||||
```
|
||||
static/
|
||||
├── css/
|
||||
│ └── style.css # Single stylesheet for entire site
|
||||
└── js/
|
||||
└── preview.js # Optional markdown preview (admin only)
|
||||
|
||||
templates/
|
||||
├── base.html # Base template with HTML structure
|
||||
├── index.html # Homepage (note list)
|
||||
├── note.html # Single note permalink
|
||||
└── admin/
|
||||
├── base.html # Admin base template
|
||||
├── dashboard.html # Admin dashboard
|
||||
├── new.html # Create new note
|
||||
└── edit.html # Edit existing note
|
||||
```
|
||||
|
||||
## CSS Architecture
|
||||
|
||||
### Custom CSS Properties (Variables)
|
||||
```css
|
||||
:root {
|
||||
--color-text: #333;
|
||||
--color-bg: #fff;
|
||||
--color-link: #0066cc;
|
||||
--color-border: #ddd;
|
||||
--font-body: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
--font-mono: 'SF Mono', Monaco, monospace;
|
||||
--spacing-unit: 1rem;
|
||||
--max-width: 42rem;
|
||||
}
|
||||
```
|
||||
|
||||
### Mobile-First Responsive Design
|
||||
```css
|
||||
/* Base: Mobile styles */
|
||||
body { padding: 1rem; }
|
||||
|
||||
/* Tablet and up */
|
||||
@media (min-width: 768px) {
|
||||
body { padding: 2rem; }
|
||||
}
|
||||
```
|
||||
|
||||
## JavaScript Architecture
|
||||
|
||||
### Markdown Preview Implementation
|
||||
```javascript
|
||||
// static/js/preview.js
|
||||
// Simple markdown preview using marked.js CDN (no build step)
|
||||
// Progressive enhancement - form works without this
|
||||
```
|
||||
|
||||
**Decision**: Use marked.js from CDN for client-side preview
|
||||
- **Justification**: Same library as server-side (consistency)
|
||||
- **Simplicity**: No bundling required
|
||||
- **Reliability**: CDN delivers cached version
|
||||
- **Alternative**: No preview (acceptable fallback)
|
||||
|
||||
## Template Organization
|
||||
|
||||
### Jinja2 Template Strategy
|
||||
- **Inheritance**: Use base templates for common structure
|
||||
- **Blocks**: Define clear content blocks for overriding
|
||||
- **Macros**: Create reusable microformat snippets
|
||||
- **Filters**: Use Jinja2 filters for date formatting
|
||||
|
||||
### Example Base Template Structure
|
||||
```jinja2
|
||||
{# templates/base.html #}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}{{ site.title }}{% endblock %}</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||
<link rel="alternate" type="application/rss+xml" href="{{ url_for('feed') }}">
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% block content %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
## Microformats Integration
|
||||
|
||||
Server-side rendering makes microformats implementation straightforward:
|
||||
|
||||
```jinja2
|
||||
{# Macro for h-entry note rendering #}
|
||||
{% macro render_note(note) %}
|
||||
<article class="h-entry">
|
||||
<div class="e-content">
|
||||
{{ note.content_html | safe }}
|
||||
</div>
|
||||
<footer>
|
||||
<a class="u-url" href="{{ url_for('note', slug=note.slug) }}">
|
||||
<time class="dt-published" datetime="{{ note.created_at.isoformat() }}">
|
||||
{{ note.created_at.strftime('%B %d, %Y') }}
|
||||
</time>
|
||||
</a>
|
||||
</footer>
|
||||
</article>
|
||||
{% endmacro %}
|
||||
```
|
||||
|
||||
## Build and Development Workflow
|
||||
|
||||
### Development
|
||||
1. Run Flask development server: `flask run`
|
||||
2. Edit templates/CSS/JS directly
|
||||
3. Browser auto-refresh on template changes
|
||||
4. No build step required
|
||||
|
||||
### Production
|
||||
1. Copy static files to production
|
||||
2. Templates are rendered on-demand
|
||||
3. Optionally enable Flask caching for rendered HTML
|
||||
4. Serve static assets with nginx/Apache (optional)
|
||||
|
||||
## Browser Support
|
||||
- Modern browsers (Chrome 90+, Firefox 88+, Safari 14+, Edge 90+)
|
||||
- Mobile browsers (iOS Safari 14+, Chrome Android 90+)
|
||||
- Progressive enhancement ensures basic functionality on older browsers
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### React/Vue/Svelte (Rejected)
|
||||
- **Simplicity**: 2/10 - Requires build tools, npm, bundlers
|
||||
- **Fitness**: 3/10 - Massive overkill for content site
|
||||
- **Maintenance**: 5/10 - Constant framework updates
|
||||
- **Verdict**: Violates "no client-side complexity" principle
|
||||
|
||||
### htmx (Considered)
|
||||
- **Simplicity**: 8/10 - Single JavaScript file, declarative
|
||||
- **Fitness**: 6/10 - Useful for dynamic updates, but not needed in V1
|
||||
- **Maintenance**: 8/10 - Stable, minimal dependencies
|
||||
- **Verdict**: Interesting for V2, but V1 doesn't need dynamic updates
|
||||
|
||||
### Alpine.js (Considered)
|
||||
- **Simplicity**: 8/10 - Lightweight, declarative
|
||||
- **Fitness**: 5/10 - Good for small interactions, but we barely need any
|
||||
- **Maintenance**: 8/10 - Well maintained
|
||||
- **Verdict**: Too much for the minimal JS we need
|
||||
|
||||
### Tailwind CSS (Rejected)
|
||||
- **Simplicity**: 4/10 - Requires build process, large configuration
|
||||
- **Fitness**: 3/10 - Utility-first doesn't fit minimal design needs
|
||||
- **Maintenance**: 7/10 - Well maintained but heavy
|
||||
- **Verdict**: Build process violates simplicity; custom CSS is sufficient
|
||||
|
||||
### Bootstrap/Bulma (Rejected)
|
||||
- **Simplicity**: 5/10 - Large framework with many unused features
|
||||
- **Fitness**: 3/10 - Component-heavy, we need minimal custom design
|
||||
- **Maintenance**: 9/10 - Very stable
|
||||
- **Verdict**: Too much CSS for what we need
|
||||
|
||||
### PicoCSS/Water.css (Considered)
|
||||
- **Simplicity**: 9/10 - Classless CSS, just include and go
|
||||
- **Fitness**: 7/10 - Good starting point but may not match design vision
|
||||
- **Maintenance**: 8/10 - Maintained, simple
|
||||
- **Verdict**: Close consideration, but custom CSS gives full control
|
||||
|
||||
## Standards Compliance
|
||||
- Semantic HTML5 elements
|
||||
- Valid HTML (W3C validator)
|
||||
- Accessible forms and navigation
|
||||
- Proper heading hierarchy
|
||||
- ARIA labels where needed
|
||||
- Mobile-responsive (viewport meta tag)
|
||||
- Progressive enhancement (works without JS)
|
||||
|
||||
## References
|
||||
- Jinja2 Documentation: https://jinja.palletsprojects.com/
|
||||
- MDN Web Docs: https://developer.mozilla.org/
|
||||
- Microformats2: http://microformats.org/wiki/h-entry
|
||||
- Progressive Enhancement: https://developer.mozilla.org/en-US/docs/Glossary/Progressive_Enhancement
|
||||
- Semantic HTML: https://developer.mozilla.org/en-US/docs/Glossary/Semantics
|
||||
384
docs/decisions/ADR-004-file-based-note-storage.md
Normal file
384
docs/decisions/ADR-004-file-based-note-storage.md
Normal file
@@ -0,0 +1,384 @@
|
||||
# ADR-004: File-Based Note Storage Architecture
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
The user explicitly requires notes to be stored as files on disk rather than as database records. This is critical for:
|
||||
1. Data portability - notes can be backed up, moved, and read without the application
|
||||
2. User ownership - direct access to content in human-readable format
|
||||
3. Simplicity - text files are the simplest storage mechanism
|
||||
4. Future-proofing - markdown files will be readable forever
|
||||
|
||||
However, we also need SQLite for:
|
||||
- Metadata (timestamps, slugs, published status)
|
||||
- Authentication tokens
|
||||
- Fast querying and indexing
|
||||
- Relational data
|
||||
|
||||
The challenge is designing how file-based storage and database metadata work together efficiently.
|
||||
|
||||
## Decision
|
||||
|
||||
### Hybrid Architecture: Files + Database Metadata
|
||||
|
||||
**Notes Content**: Stored as markdown files on disk
|
||||
**Notes Metadata**: Stored in SQLite database
|
||||
**Source of Truth**: Files are authoritative for content; database is authoritative for metadata
|
||||
|
||||
### File Storage Strategy
|
||||
|
||||
#### Directory Structure
|
||||
```
|
||||
data/
|
||||
├── notes/
|
||||
│ ├── 2024/
|
||||
│ │ ├── 11/
|
||||
│ │ │ ├── my-first-note.md
|
||||
│ │ │ └── another-note.md
|
||||
│ │ └── 12/
|
||||
│ │ └── december-note.md
|
||||
│ └── 2025/
|
||||
│ └── 01/
|
||||
│ └── new-year-note.md
|
||||
├── starpunk.db # SQLite database
|
||||
└── .backups/ # Optional backup directory
|
||||
```
|
||||
|
||||
#### File Naming Convention
|
||||
- **Format**: `{slug}.md`
|
||||
- **Slug rules**: lowercase, alphanumeric, hyphens only, no spaces
|
||||
- **Example**: `my-first-note.md`
|
||||
- **Uniqueness**: Enforced by filesystem (can't have two files with same name in same directory)
|
||||
|
||||
#### File Organization
|
||||
- **Pattern**: Year/Month subdirectories (`YYYY/MM/`)
|
||||
- **Rationale**:
|
||||
- Keeps directories manageable (max ~30 files per month)
|
||||
- Easy chronological browsing
|
||||
- Matches natural mental model
|
||||
- Scalable to thousands of notes
|
||||
- **Example path**: `data/notes/2024/11/my-first-note.md`
|
||||
|
||||
### Database Schema
|
||||
|
||||
```sql
|
||||
CREATE TABLE notes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
slug TEXT UNIQUE NOT NULL, -- URL identifier
|
||||
file_path TEXT UNIQUE NOT NULL, -- Relative path from data/notes/
|
||||
published BOOLEAN DEFAULT 0, -- Publication status
|
||||
created_at TIMESTAMP NOT NULL, -- Creation timestamp
|
||||
updated_at TIMESTAMP NOT NULL, -- Last modification timestamp
|
||||
content_hash TEXT -- SHA-256 of file content for change detection
|
||||
);
|
||||
|
||||
CREATE INDEX idx_notes_created_at ON notes(created_at DESC);
|
||||
CREATE INDEX idx_notes_published ON notes(published);
|
||||
CREATE INDEX idx_notes_slug ON notes(slug);
|
||||
```
|
||||
|
||||
### File Format
|
||||
|
||||
#### Markdown File Structure
|
||||
```markdown
|
||||
[Content of the note in markdown format]
|
||||
```
|
||||
|
||||
**That's it.** No frontmatter, no metadata in file. Keep it pure.
|
||||
|
||||
**Rationale**:
|
||||
- Maximum portability
|
||||
- Readable by any markdown editor
|
||||
- No custom parsing required
|
||||
- Metadata belongs in database (timestamps, slugs, etc.)
|
||||
- User sees just their content when opening file
|
||||
|
||||
#### Optional Future Enhancement (V2+)
|
||||
If frontmatter becomes necessary, use standard YAML:
|
||||
```markdown
|
||||
---
|
||||
title: Optional Title
|
||||
tags: tag1, tag2
|
||||
---
|
||||
[Content here]
|
||||
```
|
||||
|
||||
But for V1: **NO frontmatter**.
|
||||
|
||||
## Rationale
|
||||
|
||||
### File Storage Benefits
|
||||
**Simplicity Score: 10/10**
|
||||
- Text files are the simplest storage
|
||||
- No binary formats
|
||||
- Human-readable
|
||||
- Easy to backup (rsync, git, Dropbox, etc.)
|
||||
|
||||
**Portability Score: 10/10**
|
||||
- Standard markdown format
|
||||
- Readable without application
|
||||
- Can be edited in any text editor
|
||||
- Easy to migrate to other systems
|
||||
|
||||
**Ownership Score: 10/10**
|
||||
- User has direct access to their content
|
||||
- No vendor lock-in
|
||||
- Can grep their own notes
|
||||
- Backup is simple file copy
|
||||
|
||||
### Hybrid Approach Benefits
|
||||
**Performance**: Database indexes enable fast queries
|
||||
**Flexibility**: Rich metadata without cluttering files
|
||||
**Integrity**: Database enforces uniqueness and relationships
|
||||
**Simplicity**: Each system does what it's best at
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- Notes are portable markdown files
|
||||
- User can edit notes directly in filesystem if desired
|
||||
- Easy backup (just copy data/ directory)
|
||||
- Database provides fast metadata queries
|
||||
- Can rebuild database from files if needed
|
||||
- Git-friendly (can version control notes)
|
||||
- Maximum data ownership
|
||||
|
||||
### Negative
|
||||
- Must keep file and database in sync
|
||||
- Potential for orphaned database records
|
||||
- Potential for orphaned files
|
||||
- File operations are slower than database queries
|
||||
- Must handle file system errors
|
||||
|
||||
### Mitigation Strategies
|
||||
|
||||
#### Sync Strategy
|
||||
1. **On note creation**: Write file FIRST, then database record
|
||||
2. **On note update**: Update file FIRST, then database record (update timestamp, content_hash)
|
||||
3. **On note delete**: Mark as deleted in database, optionally move file to .trash/
|
||||
4. **On startup**: Optional integrity check to detect orphans
|
||||
|
||||
#### Orphan Detection
|
||||
```python
|
||||
# Pseudo-code for integrity check
|
||||
def check_integrity():
|
||||
# Find database records without files
|
||||
for note in database.all_notes():
|
||||
if not file_exists(note.file_path):
|
||||
log_error(f"Orphaned database record: {note.slug}")
|
||||
|
||||
# Find files without database records
|
||||
for file in filesystem.all_markdown_files():
|
||||
if not database.has_note(file_path=file):
|
||||
log_error(f"Orphaned file: {file}")
|
||||
```
|
||||
|
||||
#### Content Hash Strategy
|
||||
- Calculate SHA-256 hash of file content on write
|
||||
- Store hash in database
|
||||
- On read, can verify content hasn't been externally modified
|
||||
- Enables change detection and cache invalidation
|
||||
|
||||
## Data Flow Patterns
|
||||
|
||||
### Creating a Note
|
||||
|
||||
1. Generate slug from content or timestamp
|
||||
2. Determine file path: `data/notes/{YYYY}/{MM}/{slug}.md`
|
||||
3. Create directories if needed
|
||||
4. Write markdown content to file
|
||||
5. Calculate content hash
|
||||
6. Insert record into database
|
||||
7. Return success
|
||||
|
||||
**Transaction Safety**: If database insert fails, delete file and raise error
|
||||
|
||||
### Reading a Note
|
||||
|
||||
**By Slug**:
|
||||
1. Query database for file_path by slug
|
||||
2. Read file content from disk
|
||||
3. Return content + metadata
|
||||
|
||||
**For List**:
|
||||
1. Query database for metadata (sorted, filtered)
|
||||
2. Optionally read file content for each note
|
||||
3. Return list with metadata and content
|
||||
|
||||
### Updating a Note
|
||||
|
||||
1. Query database for existing file_path
|
||||
2. Write new content to file (atomic write to temp, then rename)
|
||||
3. Calculate new content hash
|
||||
4. Update database record (timestamp, content_hash)
|
||||
5. Return success
|
||||
|
||||
**Transaction Safety**: Keep backup of original file until database update succeeds
|
||||
|
||||
### Deleting a Note
|
||||
|
||||
**Soft Delete (Recommended)**:
|
||||
1. Update database: set `deleted_at` timestamp
|
||||
2. Optionally move file to `.trash/` subdirectory
|
||||
3. Return success
|
||||
|
||||
**Hard Delete**:
|
||||
1. Delete database record
|
||||
2. Delete file from filesystem
|
||||
3. Return success
|
||||
|
||||
## File System Operations
|
||||
|
||||
### Atomic Writes
|
||||
```python
|
||||
# Pseudo-code for atomic file write
|
||||
def write_note_safely(path, content):
|
||||
temp_path = f"{path}.tmp"
|
||||
write(temp_path, content)
|
||||
atomic_rename(temp_path, path) # Atomic on POSIX systems
|
||||
```
|
||||
|
||||
### Directory Creation
|
||||
```python
|
||||
# Ensure directory exists before writing
|
||||
def ensure_note_directory(year, month):
|
||||
path = f"data/notes/{year}/{month}"
|
||||
makedirs(path, exist_ok=True)
|
||||
return path
|
||||
```
|
||||
|
||||
### Slug Generation
|
||||
```python
|
||||
# Generate URL-safe slug
|
||||
def generate_slug(content=None, timestamp=None):
|
||||
if content:
|
||||
# Extract first few words, normalize
|
||||
words = extract_first_words(content, max=5)
|
||||
slug = normalize(words) # lowercase, hyphens, no special chars
|
||||
else:
|
||||
# Fallback: timestamp-based
|
||||
slug = timestamp.strftime("%Y%m%d-%H%M%S")
|
||||
|
||||
# Ensure uniqueness
|
||||
if database.slug_exists(slug):
|
||||
slug = f"{slug}-{random_suffix()}"
|
||||
|
||||
return slug
|
||||
```
|
||||
|
||||
## Backup Strategy
|
||||
|
||||
### Simple Backup
|
||||
```bash
|
||||
# User can backup with simple copy
|
||||
cp -r data/ backup/
|
||||
|
||||
# Or with rsync
|
||||
rsync -av data/ backup/
|
||||
|
||||
# Or with git
|
||||
cd data/ && git add . && git commit -m "Backup"
|
||||
```
|
||||
|
||||
### Restore Strategy
|
||||
1. Copy data/ directory to new location
|
||||
2. Application reads database
|
||||
3. If database missing or corrupt, rebuild from files:
|
||||
```python
|
||||
def rebuild_database_from_files():
|
||||
for file_path in glob("data/notes/**/*.md"):
|
||||
content = read_file(file_path)
|
||||
metadata = extract_metadata_from_path(file_path)
|
||||
database.insert_note(
|
||||
slug=metadata.slug,
|
||||
file_path=file_path,
|
||||
created_at=file_stat.created,
|
||||
updated_at=file_stat.modified,
|
||||
content_hash=hash(content)
|
||||
)
|
||||
```
|
||||
|
||||
## Standards Compliance
|
||||
|
||||
### Markdown Standard
|
||||
- CommonMark specification
|
||||
- No custom extensions in V1
|
||||
- Standard markdown processors can read files
|
||||
|
||||
### File System Compatibility
|
||||
- ASCII-safe filenames
|
||||
- No special characters in paths
|
||||
- Maximum path length under 255 characters
|
||||
- POSIX-compatible directory structure
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### All-Database Storage (Rejected)
|
||||
- **Simplicity**: 8/10 - Simpler code, single source of truth
|
||||
- **Portability**: 2/10 - Requires database export
|
||||
- **Ownership**: 3/10 - User doesn't have direct access
|
||||
- **Verdict**: Violates user requirement for file-based storage
|
||||
|
||||
### Flat File Directory (Rejected)
|
||||
```
|
||||
data/notes/
|
||||
├── note-1.md
|
||||
├── note-2.md
|
||||
├── note-3.md
|
||||
...
|
||||
├── note-9999.md
|
||||
```
|
||||
- **Simplicity**: 10/10 - Simplest possible structure
|
||||
- **Scalability**: 3/10 - Thousands of files in one directory is slow
|
||||
- **Verdict**: Not scalable, poor performance with many notes
|
||||
|
||||
### Git-Based Storage (Rejected for V1)
|
||||
- **Simplicity**: 6/10 - Requires git integration
|
||||
- **Portability**: 9/10 - Excellent versioning
|
||||
- **Performance**: 7/10 - Git operations have overhead
|
||||
- **Verdict**: Interesting for V2, but adds complexity to V1
|
||||
|
||||
### Frontmatter in Files (Rejected for V1)
|
||||
```markdown
|
||||
---
|
||||
slug: my-note
|
||||
created: 2024-11-18
|
||||
published: true
|
||||
---
|
||||
Note content here
|
||||
```
|
||||
- **Simplicity**: 7/10 - Requires YAML parsing
|
||||
- **Portability**: 8/10 - Common pattern, but not pure markdown
|
||||
- **Single Source**: 10/10 - All data in one place
|
||||
- **Verdict**: Deferred to V2; V1 keeps files pure
|
||||
|
||||
### JSON Metadata Sidecar (Rejected)
|
||||
```
|
||||
notes/
|
||||
├── my-note.md
|
||||
├── my-note.json # Metadata
|
||||
```
|
||||
- **Simplicity**: 6/10 - Doubles number of files
|
||||
- **Portability**: 7/10 - Markdown still clean, but extra files
|
||||
- **Sync Issues**: 5/10 - Must keep two files in sync
|
||||
- **Verdict**: Database metadata is cleaner
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
- [ ] Create data/notes directory structure on initialization
|
||||
- [ ] Implement slug generation algorithm
|
||||
- [ ] Implement atomic file write operations
|
||||
- [ ] Implement content hash calculation
|
||||
- [ ] Create database schema with indexes
|
||||
- [ ] Implement sync between files and database
|
||||
- [ ] Implement orphan detection (optional for V1)
|
||||
- [ ] Add file system error handling
|
||||
- [ ] Create backup documentation for users
|
||||
- [ ] Test with thousands of notes for performance
|
||||
|
||||
## References
|
||||
- CommonMark Spec: https://spec.commonmark.org/
|
||||
- POSIX File Operations: https://pubs.opengroup.org/onlinepubs/9699919799/
|
||||
- File System Best Practices: https://www.pathname.com/fhs/
|
||||
- Atomic File Operations: https://lwn.net/Articles/457667/
|
||||
421
docs/decisions/ADR-005-indielogin-authentication.md
Normal file
421
docs/decisions/ADR-005-indielogin-authentication.md
Normal file
@@ -0,0 +1,421 @@
|
||||
# ADR-005: IndieLogin Authentication Integration
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
The user has explicitly required external IndieLogin authentication via indielogin.com for V1. This is different from implementing a full IndieAuth server (which CLAUDE.MD mentions). The distinction is important:
|
||||
|
||||
- **IndieAuth Server**: Host your own authentication endpoint (complex)
|
||||
- **IndieLogin Service**: Use indielogin.com as an external authentication provider (simple)
|
||||
|
||||
The user wants the simpler approach: delegate authentication to indielogin.com using their API (https://indielogin.com/api).
|
||||
|
||||
IndieLogin.com is a service that:
|
||||
1. Handles the OAuth 2.0 / IndieAuth flow
|
||||
2. Verifies user identity via their website
|
||||
3. Returns authenticated identity to our application
|
||||
4. Supports multiple authentication methods (RelMeAuth, email, etc.)
|
||||
|
||||
## Decision
|
||||
|
||||
### Use IndieLogin.com as External Authentication Provider
|
||||
|
||||
**Authentication Flow**: OAuth 2.0 Authorization Code flow via indielogin.com
|
||||
**API Endpoint**: https://indielogin.com/auth
|
||||
**Token Validation**: Server-side session tokens (not IndieAuth tokens)
|
||||
**User Identity**: URL (me parameter) verified by indielogin.com
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
User Browser → StarPunk → indielogin.com → User's Website
|
||||
↑ ↓
|
||||
└──────────────────────────────┘
|
||||
(Authenticated session)
|
||||
```
|
||||
|
||||
## Authentication Flow
|
||||
|
||||
### 1. Login Initiation
|
||||
```
|
||||
User clicks "Login"
|
||||
↓
|
||||
StarPunk generates state token (CSRF protection)
|
||||
↓
|
||||
Redirect to: https://indielogin.com/auth?
|
||||
- me={user_website}
|
||||
- client_id={starpunk_url}
|
||||
- redirect_uri={starpunk_url}/auth/callback
|
||||
- state={random_token}
|
||||
```
|
||||
|
||||
### 2. IndieLogin Processing
|
||||
```
|
||||
indielogin.com verifies user identity:
|
||||
- Checks for rel="me" links on user's website
|
||||
- Or sends email verification
|
||||
- Or uses other IndieAuth methods
|
||||
↓
|
||||
User authenticates via their chosen method
|
||||
↓
|
||||
indielogin.com redirects back to StarPunk
|
||||
```
|
||||
|
||||
### 3. Callback Verification
|
||||
```
|
||||
indielogin.com → StarPunk callback with:
|
||||
- code={authorization_code}
|
||||
- state={original_state}
|
||||
↓
|
||||
StarPunk verifies state matches
|
||||
↓
|
||||
StarPunk exchanges code for verified identity:
|
||||
POST https://indielogin.com/auth
|
||||
- code={authorization_code}
|
||||
- client_id={starpunk_url}
|
||||
- redirect_uri={starpunk_url}/auth/callback
|
||||
↓
|
||||
indielogin.com responds with:
|
||||
{ "me": "https://user-website.com" }
|
||||
↓
|
||||
StarPunk creates authenticated session
|
||||
```
|
||||
|
||||
### 4. Session Management
|
||||
```
|
||||
StarPunk stores session token in cookie
|
||||
↓
|
||||
Session token maps to authenticated user URL
|
||||
↓
|
||||
Admin routes check for valid session
|
||||
```
|
||||
|
||||
## Implementation Requirements
|
||||
|
||||
### Configuration Variables
|
||||
```
|
||||
SITE_URL=https://starpunk.example.com
|
||||
ADMIN_ME=https://your-website.com
|
||||
SESSION_SECRET=random_secret_key
|
||||
```
|
||||
|
||||
### Database Schema Addition
|
||||
```sql
|
||||
-- Add to existing schema
|
||||
CREATE TABLE sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
session_token TEXT UNIQUE NOT NULL,
|
||||
me TEXT NOT NULL, -- Authenticated user URL
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
last_used_at TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX idx_sessions_token ON sessions(session_token);
|
||||
CREATE INDEX idx_sessions_expires ON sessions(expires_at);
|
||||
|
||||
CREATE TABLE auth_state (
|
||||
state TEXT PRIMARY KEY,
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
expires_at TIMESTAMP NOT NULL -- Short-lived (5 minutes)
|
||||
);
|
||||
```
|
||||
|
||||
### HTTP Client for API Calls
|
||||
Use **httpx** (already selected in ADR-002) for:
|
||||
- POST to https://indielogin.com/auth to exchange code
|
||||
- Verify response contains valid "me" URL
|
||||
- Handle network errors gracefully
|
||||
|
||||
### Routes Required
|
||||
```
|
||||
GET /admin/login - Display login form
|
||||
POST /admin/login - Initiate IndieLogin flow
|
||||
GET /auth/callback - Handle IndieLogin redirect
|
||||
POST /admin/logout - Destroy session
|
||||
```
|
||||
|
||||
### Login Flow Implementation
|
||||
|
||||
#### Step 1: Login Form
|
||||
```python
|
||||
# /admin/login (GET)
|
||||
# Display simple form asking for user's website URL
|
||||
# Form submits to POST /admin/login with "me" parameter
|
||||
```
|
||||
|
||||
#### Step 2: Initiate Authentication
|
||||
```python
|
||||
# /admin/login (POST)
|
||||
def initiate_login(me_url):
|
||||
# Validate me_url format
|
||||
if not is_valid_url(me_url):
|
||||
return error("Invalid URL")
|
||||
|
||||
# Generate and store state token
|
||||
state = generate_random_token()
|
||||
store_state(state, expires_in_minutes=5)
|
||||
|
||||
# Build IndieLogin authorization URL
|
||||
params = {
|
||||
'me': me_url,
|
||||
'client_id': SITE_URL,
|
||||
'redirect_uri': f"{SITE_URL}/auth/callback",
|
||||
'state': state
|
||||
}
|
||||
|
||||
auth_url = f"https://indielogin.com/auth?{urlencode(params)}"
|
||||
|
||||
# Redirect user to IndieLogin
|
||||
return redirect(auth_url)
|
||||
```
|
||||
|
||||
#### Step 3: Handle Callback
|
||||
```python
|
||||
# /auth/callback (GET)
|
||||
def handle_callback(code, state):
|
||||
# Verify state token (CSRF protection)
|
||||
if not verify_state(state):
|
||||
return error("Invalid state")
|
||||
|
||||
# Exchange code for verified identity
|
||||
response = httpx.post('https://indielogin.com/auth', data={
|
||||
'code': code,
|
||||
'client_id': SITE_URL,
|
||||
'redirect_uri': f"{SITE_URL}/auth/callback"
|
||||
})
|
||||
|
||||
if response.status_code != 200:
|
||||
return error("Authentication failed")
|
||||
|
||||
data = response.json()
|
||||
me = data.get('me')
|
||||
|
||||
# Verify this is the authorized admin
|
||||
if me != ADMIN_ME:
|
||||
return error("Unauthorized user")
|
||||
|
||||
# Create session
|
||||
session_token = generate_random_token()
|
||||
create_session(session_token, me, expires_in_days=30)
|
||||
|
||||
# Set session cookie
|
||||
set_cookie('session', session_token, httponly=True, secure=True)
|
||||
|
||||
# Redirect to admin dashboard
|
||||
return redirect('/admin')
|
||||
```
|
||||
|
||||
#### Step 4: Session Validation
|
||||
```python
|
||||
# Decorator for protected routes
|
||||
def require_auth(f):
|
||||
def wrapper(*args, **kwargs):
|
||||
session_token = request.cookies.get('session')
|
||||
|
||||
if not session_token:
|
||||
return redirect('/admin/login')
|
||||
|
||||
session = get_session(session_token)
|
||||
|
||||
if not session or session.expired:
|
||||
return redirect('/admin/login')
|
||||
|
||||
# Update last_used_at
|
||||
update_session_activity(session_token)
|
||||
|
||||
# Store user info in request context
|
||||
g.user_me = session.me
|
||||
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
# Usage
|
||||
@app.route('/admin')
|
||||
@require_auth
|
||||
def admin_dashboard():
|
||||
return render_template('admin/dashboard.html')
|
||||
```
|
||||
|
||||
## Rationale
|
||||
|
||||
### Why IndieLogin.com Instead of Self-Hosted IndieAuth?
|
||||
|
||||
**Simplicity Score: 10/10 (IndieLogin) vs 4/10 (Self-hosted)**
|
||||
- IndieLogin.com handles all complexity of:
|
||||
- Discovering user's auth endpoints
|
||||
- Verifying user identity
|
||||
- Supporting multiple auth methods (RelMeAuth, email, etc.)
|
||||
- PKCE implementation
|
||||
- Self-hosted would require implementing full IndieAuth spec (complex)
|
||||
|
||||
**Fitness Score: 10/10**
|
||||
- Perfect for single-user system
|
||||
- User controls their identity via their own website
|
||||
- No password management needed
|
||||
- Aligns with IndieWeb principles
|
||||
|
||||
**Maintenance Score: 10/10**
|
||||
- indielogin.com is maintained by IndieWeb community
|
||||
- No auth code to maintain ourselves
|
||||
- Security updates handled externally
|
||||
- Well-tested service
|
||||
|
||||
**Standards Compliance: Pass**
|
||||
- Uses OAuth 2.0 / IndieAuth standards
|
||||
- Compatible with IndieWeb ecosystem
|
||||
- User identity is their URL (IndieWeb principle)
|
||||
|
||||
### Why Session Cookies Instead of Access Tokens?
|
||||
|
||||
For admin interface (not Micropub):
|
||||
- **Simpler**: Standard web session pattern
|
||||
- **Secure**: HttpOnly cookies prevent XSS
|
||||
- **Appropriate**: Admin is human using browser, not API client
|
||||
- **Note**: Micropub will still use access tokens (separate ADR needed)
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- Extremely simple implementation (< 100 lines of code)
|
||||
- No authentication code to maintain
|
||||
- Secure by default (delegated to trusted service)
|
||||
- True IndieWeb authentication (user owns identity)
|
||||
- No passwords to manage
|
||||
- Works immediately without setup
|
||||
- Community-maintained service
|
||||
|
||||
### Negative
|
||||
- Dependency on external service (indielogin.com)
|
||||
- Requires internet connection to authenticate
|
||||
- Single point of failure for login (mitigated: session stays valid)
|
||||
- User must have their own website/URL
|
||||
|
||||
### Mitigation
|
||||
- Sessions last 30 days, so brief indielogin.com outages don't lock out user
|
||||
- Document fallback: edit database to create session manually if needed
|
||||
- IndieLogin.com is stable, community-run service with good uptime
|
||||
- For V2: Consider optional email fallback or self-hosted IndieAuth
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### State Token (CSRF Protection)
|
||||
- Generate cryptographically random state token
|
||||
- Store in database with short expiry (5 minutes)
|
||||
- Verify state matches on callback
|
||||
- Delete state after use (single-use tokens)
|
||||
|
||||
### Session Token Security
|
||||
- Generate with secrets.token_urlsafe(32) or similar
|
||||
- Store hash in database (not plaintext)
|
||||
- Mark cookies as HttpOnly and Secure
|
||||
- Set SameSite=Lax for CSRF protection
|
||||
- Implement session expiry (30 days)
|
||||
- Support manual logout (session deletion)
|
||||
|
||||
### Identity Verification
|
||||
- Only allow ADMIN_ME URL to authenticate
|
||||
- Verify "me" URL from indielogin.com exactly matches config
|
||||
- Reject any other authenticated users
|
||||
- Log authentication attempts
|
||||
|
||||
### Network Security
|
||||
- Use HTTPS for all communication
|
||||
- Verify SSL certificates on httpx requests
|
||||
- Handle network timeouts gracefully
|
||||
- Log authentication failures
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
- State token generation and validation
|
||||
- Session creation and expiry
|
||||
- URL validation
|
||||
- Cookie handling
|
||||
|
||||
### Integration Tests
|
||||
- Mock indielogin.com API responses
|
||||
- Test full authentication flow
|
||||
- Test session expiry
|
||||
- Test unauthorized user rejection
|
||||
- Test CSRF protection (invalid state)
|
||||
|
||||
### Manual Testing
|
||||
- Authenticate with real indielogin.com
|
||||
- Verify session persistence
|
||||
- Test logout functionality
|
||||
- Test session expiry
|
||||
- Test with wrong "me" URL
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### Self-Hosted IndieAuth Server (Rejected)
|
||||
- **Complexity**: Must implement full IndieAuth spec
|
||||
- **Maintenance**: Security updates, endpoint discovery, token generation
|
||||
- **Verdict**: Too complex for V1, violates simplicity principle
|
||||
|
||||
### Password Authentication (Rejected)
|
||||
- **Security**: Must hash passwords, handle resets, prevent brute force
|
||||
- **IndieWeb**: Violates IndieWeb principle of URL-based identity
|
||||
- **Verdict**: Not aligned with project goals
|
||||
|
||||
### OAuth via GitHub/Google (Rejected)
|
||||
- **Simplicity**: Easy to implement
|
||||
- **IndieWeb**: Not IndieWeb-compatible, user doesn't own identity
|
||||
- **Verdict**: Violates IndieWeb requirements
|
||||
|
||||
### Email Magic Links (Rejected)
|
||||
- **Simplicity**: Requires email sending infrastructure
|
||||
- **IndieWeb**: Not standard IndieWeb authentication
|
||||
- **Verdict**: Deferred to V2 as fallback option
|
||||
|
||||
### Multi-User IndieAuth (Rejected for V1)
|
||||
- **Scope**: V1 is explicitly single-user
|
||||
- **Complexity**: Would require user management
|
||||
- **Verdict**: Out of scope, defer to V2
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
- [ ] Add SESSION_SECRET and ADMIN_ME to configuration
|
||||
- [ ] Create sessions and auth_state database tables
|
||||
- [ ] Implement state token generation and storage
|
||||
- [ ] Create login form template
|
||||
- [ ] Implement /admin/login routes (GET and POST)
|
||||
- [ ] Implement /auth/callback route
|
||||
- [ ] Implement session creation and validation
|
||||
- [ ] Create require_auth decorator
|
||||
- [ ] Implement logout functionality
|
||||
- [ ] Set secure cookie parameters
|
||||
- [ ] Add authentication error handling
|
||||
- [ ] Write unit tests for auth flow
|
||||
- [ ] Write integration tests with mocked indielogin.com
|
||||
- [ ] Test with real indielogin.com
|
||||
- [ ] Document setup process for users
|
||||
|
||||
## Configuration Example
|
||||
|
||||
```bash
|
||||
# .env file
|
||||
SITE_URL=https://starpunk.example.com
|
||||
ADMIN_ME=https://your-website.com
|
||||
SESSION_SECRET=your-random-secret-key-here
|
||||
```
|
||||
|
||||
## User Setup Documentation
|
||||
|
||||
1. Deploy StarPunk to your server at `https://starpunk.example.com`
|
||||
2. Configure `ADMIN_ME` to your personal website URL
|
||||
3. Visit `/admin/login`
|
||||
4. Enter your website URL (must match ADMIN_ME)
|
||||
5. indielogin.com will verify your identity
|
||||
6. Authenticate via your chosen method
|
||||
7. Redirected back to StarPunk admin interface
|
||||
|
||||
## References
|
||||
- IndieLogin.com: https://indielogin.com/
|
||||
- IndieLogin API Documentation: https://indielogin.com/api
|
||||
- IndieAuth Specification: https://indieauth.spec.indieweb.org/
|
||||
- OAuth 2.0 Spec: https://oauth.net/2/
|
||||
- Web Authentication Best Practices: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
|
||||
552
docs/decisions/ADR-006-python-virtual-environment-uv.md
Normal file
552
docs/decisions/ADR-006-python-virtual-environment-uv.md
Normal file
@@ -0,0 +1,552 @@
|
||||
# ADR-006: Python Virtual Environment Management with uv
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
StarPunk is a Python-based web application that requires dependency management and virtual environment isolation. Developer agents (AI assistants like Claude Code) need clear, unambiguous standards for:
|
||||
|
||||
- Creating and managing Python virtual environments
|
||||
- Installing and tracking dependencies
|
||||
- Ensuring reproducible development environments
|
||||
- Avoiding common pitfalls (polluting global Python, dependency conflicts)
|
||||
- Maintaining consistency across development and deployment
|
||||
|
||||
Traditional tools (pip, venv, virtualenv, poetry, pipenv) have various limitations:
|
||||
- **pip + venv**: Slow dependency resolution, manual requirements.txt management
|
||||
- **poetry**: Complex configuration, slow, dependency lock issues
|
||||
- **pipenv**: Abandoned maintenance, slow performance
|
||||
- **conda**: Heavyweight, non-standard for web development
|
||||
|
||||
We need a tool that is fast, simple, and provides excellent developer experience while maintaining compatibility with standard Python packaging.
|
||||
|
||||
## Decision
|
||||
Use **uv** for all Python virtual environment and dependency management in StarPunk.
|
||||
|
||||
uv will be the standard tool for:
|
||||
- Creating virtual environments
|
||||
- Installing dependencies
|
||||
- Managing requirements
|
||||
- Running Python commands in the virtual environment
|
||||
- Synchronizing dependencies
|
||||
|
||||
## Rationale
|
||||
|
||||
### Simplicity Score: 10/10
|
||||
- Single tool for all environment management
|
||||
- Simple command syntax (uv venv, uv pip install, uv run)
|
||||
- Drop-in replacement for pip and virtualenv
|
||||
- No complex configuration files
|
||||
- Works with standard requirements.txt
|
||||
- Written in Rust, installed as single binary
|
||||
|
||||
### Performance Score: 10/10
|
||||
- 10-100x faster than pip for dependency resolution
|
||||
- Parallel downloads and installations
|
||||
- Efficient caching mechanism
|
||||
- Near-instant virtual environment creation
|
||||
- Minimal overhead for running commands
|
||||
|
||||
### Fitness Score: 9/10
|
||||
- Perfect for small to medium Python projects
|
||||
- Excellent for single-developer projects
|
||||
- Works with standard Python packaging (PEP 517/518)
|
||||
- Compatible with requirements.txt workflow
|
||||
- Supports editable installs for development
|
||||
- Works seamlessly with Flask and all our dependencies
|
||||
|
||||
### Maintenance Score: 9/10
|
||||
- Actively developed by Astral (creators of ruff)
|
||||
- Strong community adoption
|
||||
- Excellent documentation
|
||||
- Regular updates and improvements
|
||||
- Modern codebase (Rust)
|
||||
- Backed by funding and commercial support
|
||||
|
||||
### Standards Compliance: Pass
|
||||
- Full compatibility with pip
|
||||
- Works with PyPI and all standard package indices
|
||||
- Supports PEP 440 version specifiers
|
||||
- Compatible with requirements.txt format
|
||||
- Works with standard Python virtual environments
|
||||
- No proprietary lock files (uses standard formats)
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### 1. Installation Standards
|
||||
|
||||
#### System-Level uv Installation
|
||||
Developer agents MUST ensure uv is installed before creating environments:
|
||||
|
||||
```bash
|
||||
# Check if uv is installed
|
||||
which uv
|
||||
|
||||
# If not installed, install via pip (fallback)
|
||||
pip install uv
|
||||
|
||||
# Or install via official installer (preferred on Linux/macOS)
|
||||
curl -LsSf https://astral.sh/uv/install.sh | sh
|
||||
```
|
||||
|
||||
#### Verification
|
||||
```bash
|
||||
# Verify uv installation
|
||||
uv --version
|
||||
# Expected output: uv 0.x.x (or newer)
|
||||
```
|
||||
|
||||
### 2. Virtual Environment Creation Standards
|
||||
|
||||
#### Location and Naming
|
||||
- **Standard location**: `/home/phil/Projects/starpunk/.venv`
|
||||
- **Name**: Always use `.venv` (hidden directory)
|
||||
- **DO NOT** use: `venv`, `env`, `virtualenv`, or custom names
|
||||
|
||||
#### Creation Command
|
||||
```bash
|
||||
# Create virtual environment with uv
|
||||
cd /home/phil/Projects/starpunk
|
||||
uv venv .venv
|
||||
|
||||
# Specify Python version (recommended)
|
||||
uv venv .venv --python 3.11
|
||||
```
|
||||
|
||||
#### Post-Creation Verification
|
||||
```bash
|
||||
# Verify .venv directory exists
|
||||
ls -la /home/phil/Projects/starpunk/.venv
|
||||
|
||||
# Verify Python executable
|
||||
/home/phil/Projects/starpunk/.venv/bin/python --version
|
||||
```
|
||||
|
||||
### 3. Dependency Installation Standards
|
||||
|
||||
#### Using requirements.txt (Primary Method)
|
||||
```bash
|
||||
# Install all dependencies from requirements.txt
|
||||
uv pip install -r /home/phil/Projects/starpunk/requirements.txt
|
||||
|
||||
# Verify installation
|
||||
uv pip list
|
||||
```
|
||||
|
||||
#### Installing Individual Packages
|
||||
```bash
|
||||
# Install a single package
|
||||
uv pip install flask==3.0.*
|
||||
|
||||
# Install multiple packages
|
||||
uv pip install flask markdown feedgen
|
||||
```
|
||||
|
||||
#### Development Dependencies
|
||||
```bash
|
||||
# Install dev dependencies (if requirements-dev.txt exists)
|
||||
uv pip install -r /home/phil/Projects/starpunk/requirements-dev.txt
|
||||
```
|
||||
|
||||
### 4. Running Commands in Virtual Environment
|
||||
|
||||
#### Using uv run (Recommended)
|
||||
```bash
|
||||
# Run Python script
|
||||
uv run /home/phil/Projects/starpunk/.venv/bin/python script.py
|
||||
|
||||
# Run Flask development server
|
||||
uv run /home/phil/Projects/starpunk/.venv/bin/flask run
|
||||
|
||||
# Run pytest
|
||||
uv run /home/phil/Projects/starpunk/.venv/bin/pytest
|
||||
|
||||
# Run Python REPL
|
||||
uv run /home/phil/Projects/starpunk/.venv/bin/python
|
||||
```
|
||||
|
||||
#### Direct Execution (Alternative)
|
||||
```bash
|
||||
# Execute using absolute path to venv Python
|
||||
/home/phil/Projects/starpunk/.venv/bin/python script.py
|
||||
/home/phil/Projects/starpunk/.venv/bin/flask run
|
||||
/home/phil/Projects/starpunk/.venv/bin/pytest
|
||||
```
|
||||
|
||||
### 5. Dependency Tracking Standards
|
||||
|
||||
#### Generating requirements.txt
|
||||
```bash
|
||||
# Freeze current environment to requirements.txt
|
||||
uv pip freeze > /home/phil/Projects/starpunk/requirements.txt
|
||||
|
||||
# Freeze with sorted output for consistency
|
||||
uv pip freeze | sort > /home/phil/Projects/starpunk/requirements.txt
|
||||
```
|
||||
|
||||
#### Adding New Dependencies
|
||||
When adding a new dependency:
|
||||
1. Install the package: `uv pip install package-name`
|
||||
2. Update requirements.txt: `uv pip freeze | sort > requirements.txt`
|
||||
3. Verify installation: `uv pip list | grep package-name`
|
||||
|
||||
### 6. Environment Updates and Maintenance
|
||||
|
||||
#### Updating Dependencies
|
||||
```bash
|
||||
# Update a specific package
|
||||
uv pip install --upgrade flask
|
||||
|
||||
# Update all packages (use with caution)
|
||||
uv pip install --upgrade -r requirements.txt
|
||||
|
||||
# Regenerate requirements.txt after updates
|
||||
uv pip freeze | sort > requirements.txt
|
||||
```
|
||||
|
||||
#### Cleaning and Rebuilding
|
||||
```bash
|
||||
# Remove virtual environment
|
||||
rm -rf /home/phil/Projects/starpunk/.venv
|
||||
|
||||
# Recreate from scratch
|
||||
uv venv .venv --python 3.11
|
||||
uv pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Developer Agent Standards
|
||||
|
||||
### Critical Rules for AI Assistants
|
||||
|
||||
#### Rule 1: ALWAYS Check for Existing Virtual Environment
|
||||
Before creating a new virtual environment, ALWAYS check:
|
||||
|
||||
```bash
|
||||
# Check if .venv exists
|
||||
if [ -d "/home/phil/Projects/starpunk/.venv" ]; then
|
||||
echo "Virtual environment exists"
|
||||
/home/phil/Projects/starpunk/.venv/bin/python --version
|
||||
else
|
||||
echo "Virtual environment does not exist"
|
||||
fi
|
||||
```
|
||||
|
||||
**NEVER** create a new virtual environment if one already exists without explicit user permission.
|
||||
|
||||
#### Rule 2: ALWAYS Use Absolute Paths
|
||||
Agent threads reset cwd between bash calls. ALWAYS use absolute paths:
|
||||
|
||||
**CORRECT:**
|
||||
```bash
|
||||
uv venv /home/phil/Projects/starpunk/.venv
|
||||
/home/phil/Projects/starpunk/.venv/bin/python script.py
|
||||
uv pip install -r /home/phil/Projects/starpunk/requirements.txt
|
||||
```
|
||||
|
||||
**INCORRECT:**
|
||||
```bash
|
||||
uv venv .venv # Relative path - WRONG
|
||||
./venv/bin/python script.py # Relative path - WRONG
|
||||
uv pip install -r requirements.txt # Relative path - WRONG
|
||||
```
|
||||
|
||||
#### Rule 3: Verify Before Executing
|
||||
Before running Python commands, verify the virtual environment:
|
||||
|
||||
```bash
|
||||
# Verification checklist
|
||||
[ -d "/home/phil/Projects/starpunk/.venv" ] && echo "✓ venv exists" || echo "✗ venv missing"
|
||||
[ -f "/home/phil/Projects/starpunk/.venv/bin/python" ] && echo "✓ Python exists" || echo "✗ Python missing"
|
||||
/home/phil/Projects/starpunk/.venv/bin/python --version
|
||||
```
|
||||
|
||||
#### Rule 4: Handle Errors Gracefully
|
||||
If virtual environment operations fail:
|
||||
|
||||
1. **Check uv installation**: `which uv`
|
||||
2. **Check Python version**: `python3 --version`
|
||||
3. **Check disk space**: `df -h /home/phil/Projects/starpunk`
|
||||
4. **Report specific error** to user with context
|
||||
5. **DO NOT** silently continue with global Python
|
||||
|
||||
#### Rule 5: Never Modify Global Python
|
||||
**NEVER** run these commands:
|
||||
```bash
|
||||
# FORBIDDEN - modifies global Python
|
||||
pip install package
|
||||
python3 -m pip install package
|
||||
sudo pip install package
|
||||
```
|
||||
|
||||
**ALWAYS** use virtual environment:
|
||||
```bash
|
||||
# CORRECT - uses virtual environment
|
||||
uv pip install package
|
||||
/home/phil/Projects/starpunk/.venv/bin/pip install package
|
||||
```
|
||||
|
||||
#### Rule 6: Track Dependency Changes
|
||||
After installing or removing packages:
|
||||
|
||||
1. Update requirements.txt: `uv pip freeze | sort > requirements.txt`
|
||||
2. Verify changes: `git diff requirements.txt` (if applicable)
|
||||
3. Inform user of changes made
|
||||
|
||||
### Standard Agent Workflow
|
||||
|
||||
#### Scenario 1: First-Time Setup
|
||||
```bash
|
||||
# 1. Check if venv exists
|
||||
if [ ! -d "/home/phil/Projects/starpunk/.venv" ]; then
|
||||
echo "Creating virtual environment..."
|
||||
uv venv /home/phil/Projects/starpunk/.venv --python 3.11
|
||||
fi
|
||||
|
||||
# 2. Verify creation
|
||||
/home/phil/Projects/starpunk/.venv/bin/python --version
|
||||
|
||||
# 3. Install dependencies (if requirements.txt exists)
|
||||
if [ -f "/home/phil/Projects/starpunk/requirements.txt" ]; then
|
||||
uv pip install -r /home/phil/Projects/starpunk/requirements.txt
|
||||
fi
|
||||
|
||||
# 4. Verify installation
|
||||
uv pip list
|
||||
```
|
||||
|
||||
#### Scenario 2: Running Development Server
|
||||
```bash
|
||||
# 1. Verify venv exists
|
||||
[ -d "/home/phil/Projects/starpunk/.venv" ] || echo "ERROR: Virtual environment missing"
|
||||
|
||||
# 2. Verify Flask is installed
|
||||
/home/phil/Projects/starpunk/.venv/bin/python -c "import flask; print(flask.__version__)"
|
||||
|
||||
# 3. Run Flask development server
|
||||
/home/phil/Projects/starpunk/.venv/bin/flask --app /home/phil/Projects/starpunk/app.py run
|
||||
```
|
||||
|
||||
#### Scenario 3: Adding New Dependency
|
||||
```bash
|
||||
# 1. Install package
|
||||
uv pip install httpx
|
||||
|
||||
# 2. Verify installation
|
||||
uv pip show httpx
|
||||
|
||||
# 3. Update requirements.txt
|
||||
uv pip freeze | sort > /home/phil/Projects/starpunk/requirements.txt
|
||||
|
||||
# 4. Confirm to user
|
||||
echo "Added httpx to project dependencies"
|
||||
```
|
||||
|
||||
#### Scenario 4: Running Tests
|
||||
```bash
|
||||
# 1. Verify pytest is installed
|
||||
/home/phil/Projects/starpunk/.venv/bin/python -c "import pytest; print(pytest.__version__)"
|
||||
|
||||
# 2. Run tests
|
||||
/home/phil/Projects/starpunk/.venv/bin/pytest /home/phil/Projects/starpunk/tests/
|
||||
|
||||
# 3. Run tests with coverage (if pytest-cov installed)
|
||||
/home/phil/Projects/starpunk/.venv/bin/pytest --cov=/home/phil/Projects/starpunk/src /home/phil/Projects/starpunk/tests/
|
||||
```
|
||||
|
||||
## Project-Specific Standards
|
||||
|
||||
### Python Version Requirements
|
||||
- **Minimum**: Python 3.11
|
||||
- **Recommended**: Python 3.11 or 3.12
|
||||
- **Rationale**: Modern Python features, improved performance, security updates
|
||||
|
||||
### Directory Structure
|
||||
```
|
||||
/home/phil/Projects/starpunk/
|
||||
├── .venv/ # Virtual environment (NEVER commit)
|
||||
├── requirements.txt # Production dependencies
|
||||
├── requirements-dev.txt # Development dependencies (optional)
|
||||
├── src/ # Application source code
|
||||
├── tests/ # Test files
|
||||
└── docs/ # Documentation
|
||||
```
|
||||
|
||||
### .gitignore Requirements
|
||||
The following MUST be in .gitignore:
|
||||
```
|
||||
# Virtual Environment
|
||||
.venv/
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
Use python-dotenv for configuration:
|
||||
```bash
|
||||
# .env file (NEVER commit to git)
|
||||
FLASK_APP=app.py
|
||||
FLASK_ENV=development
|
||||
SECRET_KEY=your-secret-key
|
||||
DATABASE_PATH=/home/phil/Projects/starpunk/data/starpunk.db
|
||||
```
|
||||
|
||||
Load in application:
|
||||
```python
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
```
|
||||
|
||||
### Requirements.txt Format
|
||||
Follow these conventions:
|
||||
```
|
||||
# Requirements.txt - StarPunk Dependencies
|
||||
# Generated: 2025-11-18
|
||||
|
||||
# Web Framework
|
||||
flask==3.0.*
|
||||
|
||||
# Content Processing
|
||||
markdown==3.5.*
|
||||
|
||||
# Feed Generation
|
||||
feedgen==1.0.*
|
||||
|
||||
# HTTP Client
|
||||
httpx==0.27.*
|
||||
|
||||
# Configuration
|
||||
python-dotenv==1.0.*
|
||||
```
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- **10-100x faster** dependency resolution and installation
|
||||
- **Consistent environments** across development and deployment
|
||||
- **Simple workflow** - one tool for all Python environment tasks
|
||||
- **No activation required** - uv run handles environment automatically
|
||||
- **Excellent caching** - faster subsequent installations
|
||||
- **Standard compatibility** - works with all existing Python tools
|
||||
- **Clear agent guidelines** - reduces errors in automated workflows
|
||||
- **Isolated dependencies** - no conflicts with system Python
|
||||
|
||||
### Negative
|
||||
- **Additional tool dependency** - requires uv installation
|
||||
- **Less familiar** - newer tool, smaller community than pip
|
||||
- **Rust dependency** - uv is written in Rust (but distributed as binary)
|
||||
|
||||
### Mitigation
|
||||
- uv is easy to install (single binary, no compilation needed)
|
||||
- uv is pip-compatible (drop-in replacement)
|
||||
- Fallback to pip + venv is always possible
|
||||
- Documentation and agent standards make adoption easy
|
||||
- Active development and growing adoption reduce risk
|
||||
|
||||
### Trade-offs Accepted
|
||||
- **uv vs poetry**: We chose simplicity over advanced features
|
||||
- **uv vs pipenv**: We chose active maintenance and speed
|
||||
- **uv vs pip**: We chose performance over ubiquity
|
||||
- **Single tool complexity**: Better than managing multiple tools
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
Before considering the environment correctly set up, verify:
|
||||
|
||||
- [ ] uv is installed and accessible: `which uv`
|
||||
- [ ] Virtual environment exists: `ls -la /home/phil/Projects/starpunk/.venv`
|
||||
- [ ] Python version is 3.11+: `/home/phil/Projects/starpunk/.venv/bin/python --version`
|
||||
- [ ] Dependencies installed: `uv pip list` shows Flask, markdown, feedgen, httpx
|
||||
- [ ] requirements.txt exists and is up to date
|
||||
- [ ] .venv is in .gitignore
|
||||
- [ ] Flask runs: `/home/phil/Projects/starpunk/.venv/bin/flask --version`
|
||||
|
||||
## Integration with Development Workflow
|
||||
|
||||
### Running Flask Application
|
||||
```bash
|
||||
# Development server
|
||||
/home/phil/Projects/starpunk/.venv/bin/flask --app app.py run --debug
|
||||
|
||||
# Production server (using gunicorn)
|
||||
/home/phil/Projects/starpunk/.venv/bin/gunicorn app:app
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
```bash
|
||||
# All tests
|
||||
/home/phil/Projects/starpunk/.venv/bin/pytest
|
||||
|
||||
# Specific test file
|
||||
/home/phil/Projects/starpunk/.venv/bin/pytest tests/test_api.py
|
||||
|
||||
# With coverage
|
||||
/home/phil/Projects/starpunk/.venv/bin/pytest --cov=src tests/
|
||||
```
|
||||
|
||||
### Code Quality Tools
|
||||
```bash
|
||||
# Format code with black
|
||||
/home/phil/Projects/starpunk/.venv/bin/black src/
|
||||
|
||||
# Lint with flake8
|
||||
/home/phil/Projects/starpunk/.venv/bin/flake8 src/
|
||||
|
||||
# Type checking with mypy (if added)
|
||||
/home/phil/Projects/starpunk/.venv/bin/mypy src/
|
||||
```
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### pip + venv (Rejected)
|
||||
- **Simplicity**: 8/10 - Standard Python tools, well-known
|
||||
- **Performance**: 4/10 - Very slow dependency resolution
|
||||
- **Fitness**: 7/10 - Works but painful for larger dependency trees
|
||||
- **Maintenance**: 10/10 - Built into Python, always maintained
|
||||
- **Verdict**: Too slow, poor developer experience, but acceptable fallback
|
||||
|
||||
### poetry (Rejected)
|
||||
- **Simplicity**: 5/10 - Complex pyproject.toml, lock file management
|
||||
- **Performance**: 5/10 - Slow dependency resolution
|
||||
- **Fitness**: 6/10 - Overkill for simple project, lock files add complexity
|
||||
- **Maintenance**: 7/10 - Maintained but has had reliability issues
|
||||
- **Verdict**: Too complex for "minimal code" philosophy
|
||||
|
||||
### pipenv (Rejected)
|
||||
- **Simplicity**: 6/10 - Simpler than poetry, but still adds abstraction
|
||||
- **Performance**: 4/10 - Known performance issues
|
||||
- **Fitness**: 5/10 - Previously recommended, now effectively abandoned
|
||||
- **Maintenance**: 2/10 - Minimal maintenance, community has moved on
|
||||
- **Verdict**: Dead project, poor performance
|
||||
|
||||
### conda (Rejected)
|
||||
- **Simplicity**: 3/10 - Heavy, complex environment management
|
||||
- **Performance**: 5/10 - Slower than uv, larger downloads
|
||||
- **Fitness**: 2/10 - Designed for data science, not web development
|
||||
- **Maintenance**: 9/10 - Well maintained, large ecosystem
|
||||
- **Verdict**: Wrong tool for web application development
|
||||
|
||||
### PDM (Considered)
|
||||
- **Simplicity**: 7/10 - Modern, PEP 582 support
|
||||
- **Performance**: 8/10 - Fast, but not as fast as uv
|
||||
- **Fitness**: 7/10 - Good for modern Python projects
|
||||
- **Maintenance**: 8/10 - Actively maintained, growing community
|
||||
- **Verdict**: Good alternative, but uv is faster and simpler
|
||||
|
||||
## References
|
||||
- uv Documentation: https://docs.astral.sh/uv/
|
||||
- uv GitHub: https://github.com/astral-sh/uv
|
||||
- Python Virtual Environments: https://docs.python.org/3/library/venv.html
|
||||
- PEP 405 (Python Virtual Environments): https://peps.python.org/pep-0405/
|
||||
- requirements.txt format: https://pip.pypa.io/en/stable/reference/requirements-file-format/
|
||||
- Astral (uv creators): https://astral.sh/
|
||||
|
||||
## Change Log
|
||||
- 2025-11-18: Initial version - Established uv as standard tool for StarPunk Python environment management
|
||||
487
docs/decisions/ADR-007-slug-generation-algorithm.md
Normal file
487
docs/decisions/ADR-007-slug-generation-algorithm.md
Normal file
@@ -0,0 +1,487 @@
|
||||
# ADR-007: Slug Generation Algorithm
|
||||
|
||||
## Status
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
Notes in StarPunk require URL-safe identifiers (slugs) for permalinks and file naming. The slug generation algorithm is critical because:
|
||||
|
||||
1. **User experience**: Slugs appear in URLs and should be readable/meaningful
|
||||
2. **SEO**: Descriptive slugs improve search engine optimization
|
||||
3. **File system**: Slugs become filenames, must be filesystem-safe
|
||||
4. **Uniqueness**: Slugs must be unique across all notes
|
||||
5. **Portability**: Slugs should work across different systems and browsers
|
||||
|
||||
The challenge is designing an algorithm that creates readable, unique, safe slugs automatically from note content.
|
||||
|
||||
## Decision
|
||||
|
||||
### Content-Based Slug Generation with Timestamp Fallback
|
||||
|
||||
**Primary Algorithm**: Extract first N words from content and normalize
|
||||
**Fallback**: Timestamp-based slug when content is insufficient
|
||||
**Uniqueness**: Random suffix when collision detected
|
||||
|
||||
### Algorithm Specification
|
||||
|
||||
#### Step 1: Extract Words
|
||||
```python
|
||||
# Extract first 5 words from content
|
||||
words = content.split()[:5]
|
||||
text = " ".join(words)
|
||||
```
|
||||
|
||||
#### Step 2: Normalize
|
||||
```python
|
||||
# Convert to lowercase
|
||||
text = text.lower()
|
||||
|
||||
# Replace spaces with hyphens
|
||||
text = text.replace(" ", "-")
|
||||
|
||||
# Remove all characters except a-z, 0-9, and hyphens
|
||||
text = re.sub(r'[^a-z0-9-]', '', text)
|
||||
|
||||
# Collapse multiple hyphens
|
||||
text = re.sub(r'-+', '-', text)
|
||||
|
||||
# Strip leading/trailing hyphens
|
||||
text = text.strip('-')
|
||||
```
|
||||
|
||||
#### Step 3: Validate Length
|
||||
```python
|
||||
# If slug too short or empty, use timestamp fallback
|
||||
if len(text) < 1:
|
||||
text = created_at.strftime("%Y%m%d-%H%M%S")
|
||||
```
|
||||
|
||||
#### Step 4: Truncate
|
||||
```python
|
||||
# Limit to 100 characters
|
||||
text = text[:100]
|
||||
```
|
||||
|
||||
#### Step 5: Check Uniqueness
|
||||
```python
|
||||
# If slug exists, add random 4-character suffix
|
||||
if slug_exists(text):
|
||||
text = f"{text}-{random_alphanumeric(4)}"
|
||||
```
|
||||
|
||||
### Character Set
|
||||
|
||||
**Allowed characters**: `a-z`, `0-9`, `-` (hyphen)
|
||||
|
||||
**Rationale**:
|
||||
- URL-safe without encoding
|
||||
- Filesystem-safe on all platforms (Windows, Linux, macOS)
|
||||
- Human-readable
|
||||
- No escaping required in HTML
|
||||
- Compatible with DNS hostnames (if ever used)
|
||||
|
||||
### Examples
|
||||
|
||||
| Input Content | Generated Slug |
|
||||
|--------------|----------------|
|
||||
| "Hello World! This is my first note." | `hello-world-this-is-my` |
|
||||
| "Testing... with special chars!@#" | `testing-with-special-chars` |
|
||||
| "2024-11-18 Daily Journal Entry" | `2024-11-18-daily-journal-entry` |
|
||||
| "A" (too short) | `20241118-143022` (timestamp) |
|
||||
| " " (whitespace only) | Error: ValueError |
|
||||
| "Hello World" (duplicate) | `hello-world-a7c9` (random suffix) |
|
||||
|
||||
### Slug Uniqueness Strategy
|
||||
|
||||
**Collision Detection**: Check database for existing slug before use
|
||||
|
||||
**Resolution**: Append random 4-character suffix
|
||||
- Character set: `a-z0-9` (36 characters)
|
||||
- Combinations: 36^4 = 1,679,616 possible suffixes
|
||||
- Collision probability: Negligible for reasonable note counts
|
||||
|
||||
**Example**:
|
||||
```
|
||||
Original: hello-world
|
||||
Collision: hello-world-a7c9
|
||||
Collision: hello-world-x3k2
|
||||
```
|
||||
|
||||
### Timestamp Fallback Format
|
||||
|
||||
**Pattern**: `YYYYMMDD-HHMMSS`
|
||||
**Example**: `20241118-143022`
|
||||
|
||||
**When Used**:
|
||||
- Content is empty or whitespace-only (raises error instead)
|
||||
- Normalized slug is empty (after removing special characters)
|
||||
- Normalized slug is too short (< 1 character)
|
||||
|
||||
**Rationale**:
|
||||
- Guaranteed unique (unless two notes created in same second)
|
||||
- Sortable chronologically
|
||||
- Still readable and meaningful
|
||||
- No special characters required
|
||||
|
||||
## Rationale
|
||||
|
||||
### Content-Based Generation (Score: 9/10)
|
||||
|
||||
**Pros**:
|
||||
- **Readability**: Users can understand URL meaning
|
||||
- **SEO**: Search engines prefer descriptive URLs
|
||||
- **Memorability**: Easier to remember and share
|
||||
- **Meaningful**: Reflects note content
|
||||
|
||||
**Cons**:
|
||||
- **Collisions**: Multiple notes might have similar titles
|
||||
- **Changes**: Editing note doesn't update slug (by design)
|
||||
|
||||
### First 5 Words (Score: 8/10)
|
||||
|
||||
**Pros**:
|
||||
- **Sufficient**: 5 words usually capture note topic
|
||||
- **Concise**: Keeps URLs short and readable
|
||||
- **Consistent**: Predictable slug length
|
||||
|
||||
**Cons**:
|
||||
- **Arbitrary**: 5 is somewhat arbitrary (could be 3-7)
|
||||
- **Language**: Assumes space-separated words (English-centric)
|
||||
|
||||
**Alternatives Considered**:
|
||||
- First 3 words: Too short, often not descriptive
|
||||
- First 10 words: Too long, URLs become unwieldy
|
||||
- First line: Could be very long, harder to normalize
|
||||
- First sentence: Variable length, complex to parse
|
||||
|
||||
**Decision**: 5 words is a good balance (configurable constant)
|
||||
|
||||
### Lowercase with Hyphens (Score: 10/10)
|
||||
|
||||
**Pros**:
|
||||
- **URL Standard**: Common pattern (github.com, stackoverflow.com)
|
||||
- **Readability**: Easier to read than underscores or camelCase
|
||||
- **Compatibility**: Works everywhere
|
||||
- **Simplicity**: One separator type only
|
||||
|
||||
**Cons**:
|
||||
- None significant
|
||||
|
||||
### Alphanumeric Only (Score: 10/10)
|
||||
|
||||
**Pros**:
|
||||
- **Safety**: No escaping required in URLs or filenames
|
||||
- **Portability**: Works on all filesystems (FAT32, NTFS, ext4, APFS)
|
||||
- **Predictability**: No ambiguity about character handling
|
||||
|
||||
**Cons**:
|
||||
- **Unicode Loss**: Non-ASCII characters stripped (acceptable trade-off)
|
||||
|
||||
### Random Suffix for Uniqueness (Score: 9/10)
|
||||
|
||||
**Pros**:
|
||||
- **Simplicity**: No complex conflict resolution
|
||||
- **Security**: Cryptographically secure random (secrets module)
|
||||
- **Scalability**: 1.6M possible suffixes per base slug
|
||||
|
||||
**Cons**:
|
||||
- **Ugliness**: Suffix looks less clean (but rare occurrence)
|
||||
- **Unpredictability**: User can't control suffix
|
||||
|
||||
**Alternatives Considered**:
|
||||
- Incrementing numbers (`hello-world-2`, `hello-world-3`): More predictable but reveals note count
|
||||
- Longer random suffix: More secure but uglier URLs
|
||||
- User-specified slug: More complex, deferred to V2
|
||||
|
||||
**Decision**: 4-character random suffix is good balance
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
1. **Automatic**: No user input required for slug
|
||||
2. **Readable**: Slugs are human-readable and meaningful
|
||||
3. **Safe**: Works on all platforms and browsers
|
||||
4. **Unique**: Collision resolution ensures uniqueness
|
||||
5. **SEO-friendly**: Descriptive URLs help search ranking
|
||||
6. **Predictable**: User can anticipate what slug will be
|
||||
7. **Simple**: Single, consistent algorithm
|
||||
|
||||
### Negative
|
||||
|
||||
1. **Not editable**: User can't customize slug in V1
|
||||
2. **English-biased**: Assumes space-separated words
|
||||
3. **Unicode stripped**: Non-ASCII content loses characters
|
||||
4. **Content-dependent**: Similar content = similar slugs
|
||||
5. **Timestamp fallback**: Short notes get ugly timestamp slugs
|
||||
|
||||
### Mitigations
|
||||
|
||||
**Non-editable slugs**:
|
||||
- V1 trade-off for simplicity
|
||||
- V2 can add custom slug support
|
||||
- Users can still reference notes by slug once created
|
||||
|
||||
**English-bias**:
|
||||
- Acceptable for V1 (English-first IndieWeb)
|
||||
- V2 can add Unicode slug support (requires more complex normalization)
|
||||
|
||||
**Unicode stripping**:
|
||||
- Markdown content can still contain Unicode (only slug is ASCII)
|
||||
- Timestamp fallback ensures note is still creatable
|
||||
- V2 can use Unicode normalization (transliteration)
|
||||
|
||||
**Timestamp fallback**:
|
||||
- Rare occurrence (most notes have >5 words)
|
||||
- Still functional and unique
|
||||
- V2 can improve (use first word if exists + timestamp)
|
||||
|
||||
## Standards Compliance
|
||||
|
||||
### URL Standards (RFC 3986)
|
||||
|
||||
Slugs comply with URL path segment requirements:
|
||||
- No percent-encoding required
|
||||
- No reserved characters (`/`, `?`, `#`, etc.)
|
||||
- Case-insensitive safe (always lowercase)
|
||||
|
||||
### Filesystem Standards
|
||||
|
||||
Slugs work on all major filesystems:
|
||||
- **FAT32**: Yes (no special chars, length OK)
|
||||
- **NTFS**: Yes
|
||||
- **ext4**: Yes
|
||||
- **APFS**: Yes
|
||||
- **HFS+**: Yes
|
||||
|
||||
**Reserved names**: None of our slugs conflict with OS reserved names (CON, PRN, etc.)
|
||||
|
||||
### IndieWeb Recommendations
|
||||
|
||||
Aligns with IndieWeb permalink best practices:
|
||||
- Descriptive URLs
|
||||
- No query parameters
|
||||
- Short and memorable
|
||||
- Permanent (don't change after creation)
|
||||
|
||||
## Implementation Requirements
|
||||
|
||||
### Validation Rules
|
||||
|
||||
```python
|
||||
# Valid slug pattern
|
||||
SLUG_PATTERN = r'^[a-z0-9]+(?:-[a-z0-9]+)*$'
|
||||
|
||||
# Constraints
|
||||
MIN_SLUG_LENGTH = 1
|
||||
MAX_SLUG_LENGTH = 100
|
||||
```
|
||||
|
||||
### Reserved Slugs
|
||||
|
||||
Certain slugs should be reserved for system routes:
|
||||
|
||||
**Reserved List** (reject these slugs):
|
||||
- `admin`
|
||||
- `api`
|
||||
- `static`
|
||||
- `auth`
|
||||
- `feed`
|
||||
- `login`
|
||||
- `logout`
|
||||
|
||||
Implementation:
|
||||
```python
|
||||
RESERVED_SLUGS = {'admin', 'api', 'static', 'auth', 'feed', 'login', 'logout'}
|
||||
|
||||
def is_slug_reserved(slug: str) -> bool:
|
||||
return slug in RESERVED_SLUGS
|
||||
```
|
||||
|
||||
### Error Cases
|
||||
|
||||
```python
|
||||
# Empty content
|
||||
generate_slug("") # Raises ValueError
|
||||
|
||||
# Whitespace only
|
||||
generate_slug(" ") # Raises ValueError
|
||||
|
||||
# Valid but short
|
||||
generate_slug("Hi") # Returns timestamp: "20241118-143022"
|
||||
|
||||
# Special characters only
|
||||
generate_slug("!@#$%") # Returns timestamp: "20241118-143022"
|
||||
```
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### UUID-based Slugs (Rejected)
|
||||
|
||||
```python
|
||||
slug = str(uuid.uuid4()) # "550e8400-e29b-41d4-a716-446655440000"
|
||||
```
|
||||
|
||||
**Pros**: Guaranteed unique, no collision checking
|
||||
**Cons**: Not human-readable, poor SEO, not memorable
|
||||
|
||||
**Verdict**: Violates principle of readable URLs
|
||||
|
||||
### Hash-based Slugs (Rejected)
|
||||
|
||||
```python
|
||||
slug = hashlib.sha256(content.encode()).hexdigest()[:12] # "a591a6d40bf4"
|
||||
```
|
||||
|
||||
**Pros**: Deterministic, unique
|
||||
**Cons**: Not human-readable, changes if content edited
|
||||
|
||||
**Verdict**: Not meaningful to users
|
||||
|
||||
### Title Extraction (Rejected for V1)
|
||||
|
||||
```python
|
||||
# Extract from # heading or first line
|
||||
title = extract_title_from_markdown(content)
|
||||
slug = normalize(title)
|
||||
```
|
||||
|
||||
**Pros**: More semantic, uses actual title
|
||||
**Cons**: Requires markdown parsing, more complex, title might not exist
|
||||
|
||||
**Verdict**: Deferred to V2 (V1 uses first N words which is simpler)
|
||||
|
||||
### User-Specified Slugs (Rejected for V1)
|
||||
|
||||
```python
|
||||
def create_note(content, custom_slug=None):
|
||||
if custom_slug:
|
||||
slug = validate_and_use(custom_slug)
|
||||
else:
|
||||
slug = generate_slug(content)
|
||||
```
|
||||
|
||||
**Pros**: Maximum user control, no surprises
|
||||
**Cons**: Requires UI input, validation complexity, user burden
|
||||
|
||||
**Verdict**: Deferred to V2 (V1 auto-generates for simplicity)
|
||||
|
||||
### Incrementing Numbers (Rejected)
|
||||
|
||||
```python
|
||||
# If collision, increment
|
||||
slug = "hello-world"
|
||||
slug = "hello-world-2" # Collision
|
||||
slug = "hello-world-3" # Collision
|
||||
```
|
||||
|
||||
**Pros**: Predictable, simple
|
||||
**Cons**: Reveals note count, enumeration attack vector, less random
|
||||
|
||||
**Verdict**: Random suffix is more secure and scales better
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Generation Speed
|
||||
|
||||
- Extract words: O(n) where n = content length (negligible, content is small)
|
||||
- Normalize: O(m) where m = extracted text length (< 100 chars)
|
||||
- Uniqueness check: O(1) database lookup with index
|
||||
- Random suffix: O(1) generation
|
||||
|
||||
**Target**: < 1ms per slug generation (easily achieved)
|
||||
|
||||
### Database Impact
|
||||
|
||||
- Index on `slug` column: O(log n) lookup
|
||||
- Collision rate: < 1% (most notes have unique first 5 words)
|
||||
- Random suffix retries: Nearly never (1.6M combinations)
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
### Test Cases
|
||||
|
||||
**Normal Cases**:
|
||||
- Standard English content → descriptive slug
|
||||
- Content with punctuation → punctuation removed
|
||||
- Content with numbers → numbers preserved
|
||||
- Content with hyphens → hyphens preserved
|
||||
|
||||
**Edge Cases**:
|
||||
- Very short content → timestamp fallback
|
||||
- Empty content → ValueError
|
||||
- Special characters only → timestamp fallback
|
||||
- Very long words → truncated to max length
|
||||
- Unicode content → stripped to ASCII
|
||||
|
||||
**Collision Cases**:
|
||||
- Duplicate slug → random suffix added
|
||||
- Multiple collisions → different random suffixes
|
||||
- Reserved slug → rejected
|
||||
|
||||
**Security Cases**:
|
||||
- Path traversal attempt (`../../../etc/passwd`)
|
||||
- Special characters (`<script>`, `%00`, etc.)
|
||||
- Very long input (>10,000 characters)
|
||||
|
||||
## Migration Path (V2)
|
||||
|
||||
Future enhancements that build on this foundation:
|
||||
|
||||
### Custom Slugs
|
||||
```python
|
||||
def create_note(content, custom_slug=None):
|
||||
slug = custom_slug or generate_slug(content)
|
||||
```
|
||||
|
||||
### Unicode Support
|
||||
```python
|
||||
def generate_unicode_slug(content):
|
||||
# Use Unicode normalization (NFKD)
|
||||
# Transliterate to ASCII (unidecode library)
|
||||
# Support CJK languages
|
||||
```
|
||||
|
||||
### Title Extraction
|
||||
```python
|
||||
def extract_title_from_content(content):
|
||||
# Check for # heading
|
||||
# Use first line if no heading
|
||||
# Fall back to first N words
|
||||
```
|
||||
|
||||
### Slug Editing
|
||||
```python
|
||||
def update_note_slug(note_id, new_slug):
|
||||
# Validate new slug
|
||||
# Update database
|
||||
# Rename file
|
||||
# Create redirect from old slug
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [RFC 3986 - URI Generic Syntax](https://www.rfc-editor.org/rfc/rfc3986)
|
||||
- [IndieWeb Permalink Design](https://indieweb.org/permalink)
|
||||
- [URL Slug Best Practices](https://moz.com/learn/seo/url)
|
||||
- [Python secrets Module](https://docs.python.org/3/library/secrets.html)
|
||||
- [ADR-004: File-Based Note Storage](/home/phil/Projects/starpunk/docs/decisions/ADR-004-file-based-note-storage.md)
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ] Slug generation creates valid, URL-safe slugs
|
||||
- [ ] Slugs are descriptive (use first 5 words)
|
||||
- [ ] Slugs are unique (collision detection + random suffix)
|
||||
- [ ] Slugs meet length constraints (1-100 characters)
|
||||
- [ ] Timestamp fallback works for short content
|
||||
- [ ] Reserved slugs are rejected
|
||||
- [ ] Unicode content is handled gracefully
|
||||
- [ ] All edge cases tested
|
||||
- [ ] Performance meets target (<1ms)
|
||||
- [ ] Code follows Python coding standards
|
||||
|
||||
---
|
||||
|
||||
**Approved**: 2024-11-18
|
||||
**Architect**: StarPunk Architect Agent
|
||||
457
docs/decisions/ADR-008-versioning-strategy.md
Normal file
457
docs/decisions/ADR-008-versioning-strategy.md
Normal file
@@ -0,0 +1,457 @@
|
||||
# ADR-008: Versioning Strategy
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
StarPunk is an IndieWeb CMS currently in active development, working toward its first production release. We need a comprehensive versioning strategy that:
|
||||
|
||||
1. **Communicates clearly** what type of changes each release contains
|
||||
2. **Works with Python ecosystem** tools (pip, uv, PyPI compatibility)
|
||||
3. **Aligns with IndieWeb values** of simplicity and sustainability
|
||||
4. **Supports the project lifecycle** from development through maintenance
|
||||
5. **Enables dependency management** for users who may build on StarPunk
|
||||
6. **Provides predictability** for users about what to expect in updates
|
||||
|
||||
### Current State
|
||||
|
||||
The project currently uses informal version terminology:
|
||||
- "V1" for the overall first release goal
|
||||
- "Phase 1.1", "Phase 1.2" for development milestones
|
||||
- "v1.1", "v2.0" for future iteration references
|
||||
|
||||
This works for internal planning but lacks the precision needed for:
|
||||
- Public releases
|
||||
- Dependency management
|
||||
- Communicating breaking changes
|
||||
- Git tagging and release management
|
||||
|
||||
### Requirements
|
||||
|
||||
1. **Version number format** that indicates change severity
|
||||
2. **Python ecosystem compliance** (PEP 440)
|
||||
3. **Git workflow integration** (tagging, branching)
|
||||
4. **Changelog format** for human-readable history
|
||||
5. **Pre-release versioning** for alphas/betas if needed
|
||||
6. **Upgrade communication** strategy
|
||||
7. **Simplicity** appropriate for an indie project
|
||||
|
||||
## Decision
|
||||
|
||||
We will adopt **Semantic Versioning 2.0.0 (SemVer)** with **Python PEP 440 compliance** for all StarPunk releases.
|
||||
|
||||
### Version Number Format
|
||||
|
||||
**Structure**: `MAJOR.MINOR.PATCH[-PRERELEASE]`
|
||||
|
||||
**Examples**:
|
||||
- `0.1.0` - Development version (Phase 1.1 complete)
|
||||
- `0.2.0` - Development version (Phase 1.2 complete)
|
||||
- `1.0.0` - First stable release (all V1 features complete)
|
||||
- `1.0.1` - Bug fix release
|
||||
- `1.1.0` - Feature release (backward compatible)
|
||||
- `2.0.0` - Major release (breaking changes)
|
||||
- `1.0.0a1` - Alpha pre-release (PEP 440 format)
|
||||
- `1.0.0b2` - Beta pre-release (PEP 440 format)
|
||||
- `1.0.0rc1` - Release candidate (PEP 440 format)
|
||||
|
||||
### Version Component Rules
|
||||
|
||||
**MAJOR version** - Increment when making incompatible changes:
|
||||
- Breaking API changes
|
||||
- Database schema changes requiring migration
|
||||
- Configuration file format changes requiring user intervention
|
||||
- Removal of deprecated features
|
||||
- Major architectural changes
|
||||
|
||||
**MINOR version** - Increment when adding functionality in backward-compatible manner:
|
||||
- New features
|
||||
- New API endpoints
|
||||
- Non-breaking enhancements
|
||||
- Optional new configuration parameters
|
||||
- Significant performance improvements
|
||||
|
||||
**PATCH version** - Increment for backward-compatible bug fixes:
|
||||
- Bug fixes
|
||||
- Security patches
|
||||
- Documentation corrections
|
||||
- Minor performance improvements
|
||||
- Dependency updates (without feature changes)
|
||||
|
||||
### Development Phase (0.x.y)
|
||||
|
||||
During development (pre-1.0), we use `0.MINOR.PATCH`:
|
||||
- MINOR increments for phase completions (Phase 1.1 → 0.1.0, Phase 1.2 → 0.2.0)
|
||||
- PATCH increments for bug fixes during development
|
||||
- Breaking changes are allowed without major version increment
|
||||
- Public API is not considered stable
|
||||
|
||||
### Git Tagging Convention
|
||||
|
||||
**Format**: `vMAJOR.MINOR.PATCH[-PRERELEASE]`
|
||||
|
||||
**Examples**:
|
||||
- `v0.1.0` - Development version tag
|
||||
- `v1.0.0` - Stable release tag
|
||||
- `v1.0.1` - Bug fix release tag
|
||||
- `v1.0.0-alpha.1` - Alpha pre-release tag (Git format)
|
||||
|
||||
**Tag type**: Annotated tags (not lightweight)
|
||||
- Contains tagger, date, message
|
||||
- Can include release notes
|
||||
- Can be GPG signed
|
||||
|
||||
### Version Storage
|
||||
|
||||
**Primary source of truth**: `starpunk/__init__.py`
|
||||
```python
|
||||
__version__ = "1.0.0"
|
||||
__version_info__ = (1, 0, 0)
|
||||
```
|
||||
|
||||
**Secondary locations**:
|
||||
- `pyproject.toml` - Package metadata (if used)
|
||||
- Git tags - Release markers
|
||||
- `CHANGELOG.md` - Human-readable history
|
||||
|
||||
**Synchronization**: Manual for V1 (simple, no automation dependencies)
|
||||
|
||||
### Changelog Format
|
||||
|
||||
**File**: `CHANGELOG.md`
|
||||
|
||||
**Format**: Based on [Keep a Changelog](https://keepachangelog.com/)
|
||||
|
||||
**Categories**:
|
||||
- Added - New features
|
||||
- Changed - Changes to existing functionality
|
||||
- Deprecated - Features that will be removed
|
||||
- Removed - Features that have been removed
|
||||
- Fixed - Bug fixes
|
||||
- Security - Security vulnerability fixes
|
||||
|
||||
**Example**:
|
||||
```markdown
|
||||
## [1.0.0] - 2024-11-18
|
||||
|
||||
### Added
|
||||
- IndieAuth authentication via IndieLogin
|
||||
- Micropub endpoint for publishing
|
||||
- RSS feed generation
|
||||
- File-based note storage
|
||||
|
||||
### Security
|
||||
- Implemented path traversal protection
|
||||
- Added CSRF protection for authentication flows
|
||||
```
|
||||
|
||||
### Pre-Release Versioning
|
||||
|
||||
**Format**: PEP 440 compliant
|
||||
- Alpha: `1.0.0a1`, `1.0.0a2`
|
||||
- Beta: `1.0.0b1`, `1.0.0b2`
|
||||
- Release Candidate: `1.0.0rc1`, `1.0.0rc2`
|
||||
|
||||
**Git tags use hyphen**: `v1.0.0-alpha.1` (for readability)
|
||||
**Python `__version__` uses PEP 440**: `1.0.0a1` (for pip compatibility)
|
||||
|
||||
### Phase-to-Version Mapping
|
||||
|
||||
**Implementation phases** (internal planning) map to **version numbers** (public releases):
|
||||
|
||||
```
|
||||
Phase 1.1 complete → Version 0.1.0
|
||||
Phase 1.2 complete → Version 0.2.0
|
||||
Phase 1.3 complete → Version 0.3.0
|
||||
Phase 2.1 complete → Version 0.4.0
|
||||
All V1 complete → Version 1.0.0
|
||||
V1.1 features → Version 1.1.0
|
||||
V2 features → Version 2.0.0
|
||||
```
|
||||
|
||||
**Clarification**:
|
||||
- "V1" refers to feature scope, not version number
|
||||
- Version 1.0.0 implements the "V1 feature set"
|
||||
- Phases are development milestones, versions are public releases
|
||||
|
||||
## Rationale
|
||||
|
||||
### Why Semantic Versioning?
|
||||
|
||||
1. **Industry Standard**: Used by Flask, Django, Requests, and most Python packages
|
||||
2. **Clear Communication**: Version number immediately conveys impact of changes
|
||||
3. **Predictable**: Users know what to expect from each version increment
|
||||
4. **Dependency Management**: Works seamlessly with pip version specifiers
|
||||
5. **Simple**: Easy to understand and apply without complex rules
|
||||
|
||||
### Why Not Calendar Versioning (CalVer)?
|
||||
|
||||
CalVer (e.g., `2024.11.18`) was considered but rejected:
|
||||
|
||||
**Pros**:
|
||||
- Shows when software was released
|
||||
- No ambiguity about version order
|
||||
|
||||
**Cons**:
|
||||
- Doesn't communicate impact of changes (patch vs breaking change)
|
||||
- Less common in Python ecosystem
|
||||
- Doesn't help users assess upgrade risk
|
||||
- Overkill for indie project release cadence
|
||||
|
||||
**Conclusion**: SemVer's semantic meaning is more valuable than date information
|
||||
|
||||
### Why Not ZeroVer (0.x forever)?
|
||||
|
||||
Some projects stay at 0.x.y indefinitely to signal "still evolving". Rejected because:
|
||||
|
||||
- Creates uncertainty about production readiness
|
||||
- Version 1.0.0 signals "ready for production use"
|
||||
- We have a clear 1.0 feature scope (V1)
|
||||
- Users deserve clarity about stability
|
||||
|
||||
### Why PEP 440 Compliance?
|
||||
|
||||
**PEP 440** is Python's version identification standard:
|
||||
|
||||
- Required for PyPI publication (if we ever publish)
|
||||
- Compatible with pip, uv, and all Python package managers
|
||||
- Slightly different pre-release format than SemVer (e.g., `1.0.0a1` vs `1.0.0-alpha.1`)
|
||||
- Used by all major Python frameworks
|
||||
|
||||
**Decision**: Use PEP 440 format in Python code, SemVer-style in Git tags (Git tags are more flexible)
|
||||
|
||||
### Why Manual Version Management (V1)?
|
||||
|
||||
Considered automation tools:
|
||||
- `bump2version` - Automates version bumping
|
||||
- `python-semantic-release` - Determines version from commit messages
|
||||
- `setuptools_scm` - Derives version from Git tags
|
||||
|
||||
**Decision**: Manual for V1 because:
|
||||
1. Simple - no extra dependencies
|
||||
2. Full control - explicit about versions
|
||||
3. Aligns with indie philosophy - minimal tooling
|
||||
4. Can add automation later if needed
|
||||
|
||||
### Why Annotated Tags?
|
||||
|
||||
**Annotated tags** vs lightweight tags:
|
||||
|
||||
Annotated tags:
|
||||
- Contain metadata (tagger, date, message)
|
||||
- Can include release notes
|
||||
- Can be GPG signed
|
||||
- Treated as full objects in Git
|
||||
|
||||
**Decision**: Always use annotated tags for releases
|
||||
|
||||
### Why CHANGELOG.md?
|
||||
|
||||
**Changelog** provides human-readable release history:
|
||||
- Users can quickly see what changed
|
||||
- Easier to read than Git commits
|
||||
- Standard location (`CHANGELOG.md`)
|
||||
- Standard format (Keep a Changelog)
|
||||
- Can be generated from commits or written manually
|
||||
|
||||
**Decision**: Maintain manually for V1 (precise control over messaging)
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
1. **Clear Communication**: Users know exactly what each version means
|
||||
2. **Ecosystem Compatibility**: Works with all Python tools
|
||||
3. **Predictable Upgrades**: Users can assess risk before upgrading
|
||||
4. **Professional Image**: Signals mature, well-maintained software
|
||||
5. **Dependency Management**: Other projects can depend on StarPunk versions
|
||||
6. **Git Integration**: Clean tagging and release workflow
|
||||
7. **Flexible**: Can add automation later without changing format
|
||||
8. **Standards-Based**: Uses established, documented standards
|
||||
|
||||
### Negative
|
||||
|
||||
1. **Manual Effort**: Requires discipline to update version in multiple places
|
||||
2. **Coordination**: Must remember to update version, changelog, and tag
|
||||
3. **Breaking Change Discipline**: Must carefully evaluate what constitutes breaking change
|
||||
4. **Learning Curve**: Contributors need to understand SemVer rules
|
||||
|
||||
### Neutral
|
||||
|
||||
1. **0.x Signals Development**: May discourage early adopters who want "1.0" stability
|
||||
2. **Commitment to Backward Compatibility**: Once at 1.0, breaking changes require major version
|
||||
3. **Changelog Maintenance**: Ongoing effort to document changes
|
||||
|
||||
### Mitigations
|
||||
|
||||
**For manual effort**:
|
||||
- Document clear release process in `versioning-strategy.md`
|
||||
- Create release checklist
|
||||
- Consider automation in V2+ if needed
|
||||
|
||||
**For breaking change discipline**:
|
||||
- Deprecate features one version before removal when possible
|
||||
- Document breaking changes prominently
|
||||
- Provide upgrade guides for major versions
|
||||
|
||||
**For 0.x concerns**:
|
||||
- Clearly communicate that 0.x is pre-production
|
||||
- Move to 1.0.0 once V1 features are complete and tested
|
||||
- Don't stay at 0.x longer than necessary
|
||||
|
||||
## Implementation
|
||||
|
||||
### Immediate Actions
|
||||
|
||||
1. **Set current version**: `0.1.0` (Phase 1.1 development)
|
||||
2. **Create `starpunk/__init__.py`**:
|
||||
```python
|
||||
__version__ = "0.1.0"
|
||||
__version_info__ = (0, 1, 0)
|
||||
```
|
||||
3. **Create `CHANGELOG.md`** with `[Unreleased]` section
|
||||
4. **Update README.md** with version information
|
||||
5. **Tag current state**: `git tag -a v0.1.0 -m "Development version 0.1.0"`
|
||||
|
||||
### Release Workflow
|
||||
|
||||
When ready to release:
|
||||
|
||||
1. Update `starpunk/__init__.py` with new version
|
||||
2. Update `CHANGELOG.md` with release date
|
||||
3. Commit: `git commit -m "Bump version to X.Y.Z"`
|
||||
4. Tag: `git tag -a vX.Y.Z -m "Release X.Y.Z: [description]"`
|
||||
5. Push: `git push origin main vX.Y.Z`
|
||||
|
||||
### Documentation
|
||||
|
||||
- Create comprehensive `docs/standards/versioning-strategy.md`
|
||||
- Document examples, decision tree, and FAQ
|
||||
- Include upgrade guide template
|
||||
- Reference in README.md
|
||||
|
||||
### Future Enhancements
|
||||
|
||||
**V2+ Considerations**:
|
||||
- Add `bump2version` for automation if manual process becomes tedious
|
||||
- Consider API versioning if need to support multiple incompatible API versions
|
||||
- Add database schema versioning if migrations become complex
|
||||
- Automated changelog generation from commit messages
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### Alternative 1: Calendar Versioning (CalVer)
|
||||
|
||||
**Format**: `YYYY.MM.PATCH` (e.g., `2024.11.0`)
|
||||
|
||||
**Pros**:
|
||||
- Shows release date
|
||||
- Clear chronological order
|
||||
- Used by Ubuntu, PyCharm
|
||||
|
||||
**Cons**:
|
||||
- Doesn't communicate change impact
|
||||
- Less common in Python web frameworks
|
||||
- Doesn't indicate breaking changes
|
||||
- Overkill for indie project cadence
|
||||
|
||||
**Rejected**: Semantic meaning more important than date
|
||||
|
||||
### Alternative 2: Simple Integer Versioning
|
||||
|
||||
**Format**: `1`, `2`, `3` (e.g., like TeX: 3.14159...)
|
||||
|
||||
**Pros**:
|
||||
- Extremely simple
|
||||
- No ambiguity
|
||||
|
||||
**Cons**:
|
||||
- No information about change severity
|
||||
- Doesn't work with Python dependency tools
|
||||
- Not standard in Python ecosystem
|
||||
- Too minimalist for practical use
|
||||
|
||||
**Rejected**: Too simplistic, poor ecosystem fit
|
||||
|
||||
### Alternative 3: Modified SemVer (Django-style)
|
||||
|
||||
**Format**: `MAJOR.FEATURE.PATCH` (e.g., Django's `4.2.7`)
|
||||
|
||||
**Pros**:
|
||||
- Used by Django
|
||||
- Separates features from bug fixes
|
||||
|
||||
**Cons**:
|
||||
- Non-standard SemVer interpretation
|
||||
- Confusing: when to increment feature vs major?
|
||||
- Doesn't clearly indicate breaking changes
|
||||
- Less predictable
|
||||
|
||||
**Rejected**: Standard SemVer is clearer
|
||||
|
||||
### Alternative 4: ZeroVer (0.x forever)
|
||||
|
||||
**Format**: Stay at `0.x.y` indefinitely
|
||||
|
||||
**Pros**:
|
||||
- Signals "always evolving"
|
||||
- No commitment to stability
|
||||
|
||||
**Cons**:
|
||||
- Users can't tell when production-ready
|
||||
- Doesn't signal stability improvements
|
||||
- Version 1.0.0 has meaning: "ready to use"
|
||||
|
||||
**Rejected**: We have clear 1.0 goals, should signal when achieved
|
||||
|
||||
### Alternative 5: Hybrid SemVer + CalVer
|
||||
|
||||
**Format**: `YYYY.MINOR.PATCH` (e.g., `2024.1.0`)
|
||||
|
||||
**Pros**:
|
||||
- Combines date and semantic information
|
||||
|
||||
**Cons**:
|
||||
- Unusual, confusing
|
||||
- Not standard
|
||||
- Year increments don't mean anything semantically
|
||||
|
||||
**Rejected**: Combines worst of both approaches
|
||||
|
||||
## References
|
||||
|
||||
### Standards
|
||||
|
||||
- [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html) - Official SemVer specification
|
||||
- [PEP 440 - Version Identification](https://peps.python.org/pep-0440/) - Python version numbering standard
|
||||
- [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Changelog format standard
|
||||
- [Calendar Versioning](https://calver.org/) - CalVer specification (considered but not adopted)
|
||||
|
||||
### Examples from Python Ecosystem
|
||||
|
||||
- [Flask Versioning](https://github.com/pallets/flask/releases) - Uses SemVer
|
||||
- [Django Release Process](https://docs.djangoproject.com/en/stable/internals/release-process/) - Modified SemVer
|
||||
- [Requests Versioning](https://github.com/psf/requests/releases) - Uses SemVer
|
||||
- [FastAPI Versioning](https://github.com/tiangolo/fastapi/releases) - Uses SemVer
|
||||
|
||||
### Tools
|
||||
|
||||
- [bump2version](https://github.com/c4urself/bump2version) - Version bump automation
|
||||
- [python-semantic-release](https://python-semantic-release.readthedocs.io/) - Automated semantic releases
|
||||
- [setuptools_scm](https://github.com/pypa/setuptools_scm) - SCM-based versioning
|
||||
|
||||
### Internal Documentation
|
||||
|
||||
- [Versioning Strategy](/home/phil/Projects/starpunk/docs/standards/versioning-strategy.md) - Complete versioning specification
|
||||
- [Architecture Overview](/home/phil/Projects/starpunk/docs/architecture/overview.md) - System architecture
|
||||
- [Development Setup](/home/phil/Projects/starpunk/docs/standards/development-setup.md) - Development workflow
|
||||
|
||||
---
|
||||
|
||||
**ADR**: 008
|
||||
**Date**: 2024-11-18
|
||||
**Status**: Accepted
|
||||
**Decision**: Adopt Semantic Versioning 2.0.0 with PEP 440 compliance
|
||||
**Supersedes**: None
|
||||
484
docs/decisions/ADR-009-git-branching-strategy.md
Normal file
484
docs/decisions/ADR-009-git-branching-strategy.md
Normal file
@@ -0,0 +1,484 @@
|
||||
# ADR-009: Git Branching Strategy
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
StarPunk needs a git branching strategy that supports:
|
||||
|
||||
1. **Semantic versioning workflow** - Moving from 0.x.y development through 1.0.0 stable release
|
||||
2. **Single developer initially** - Optimized for solo work but scalable to small teams
|
||||
3. **Clean release history** - Clear tags and versioning aligned with SemVer
|
||||
4. **Hotfix capability** - Ability to patch production releases
|
||||
5. **Minimal complexity** - Appropriate for indie project scale
|
||||
6. **Development discipline** - Structure without bureaucracy
|
||||
|
||||
### Current State
|
||||
|
||||
The project just renamed its primary branch from `master` to `main` to align with modern Git conventions and industry best practices. This is a brand new repository with no commits yet, currently at version 0.1.0 (development phase).
|
||||
|
||||
### Requirements
|
||||
|
||||
The branching strategy must:
|
||||
|
||||
1. **Support semantic versioning** releases (0.1.0 → 1.0.0 → 1.0.1 → 1.1.0 → 2.0.0)
|
||||
2. **Work for solo and team** development
|
||||
3. **Enable clean releases** with proper tagging
|
||||
4. **Allow hotfixes** to production versions
|
||||
5. **Keep main branch stable** - always in working state
|
||||
6. **Minimize long-lived branches** - integrate frequently
|
||||
7. **Align with IndieWeb values** - simplicity, no lock-in
|
||||
8. **Be well-documented** - clear workflows for common scenarios
|
||||
|
||||
## Decision
|
||||
|
||||
We will adopt a **simplified trunk-based development** strategy with feature branches and semantic versioning integration.
|
||||
|
||||
### Core Principles
|
||||
|
||||
1. **Single primary branch**: `main`
|
||||
2. **Branch name**: `main` (not `master`)
|
||||
3. **Feature branches**: Short-lived branches for features/fixes
|
||||
4. **Direct tagging**: Tag `main` for releases
|
||||
5. **Hotfix branches**: For production patches (post-1.0.0)
|
||||
6. **No long-lived branches**: Integrate to main frequently
|
||||
7. **Annotated tags**: Always use annotated tags for releases
|
||||
|
||||
### Branch Structure
|
||||
|
||||
**Primary branch**: `main`
|
||||
- Single source of truth
|
||||
- Always stable (tests pass)
|
||||
- Tagged for releases
|
||||
- Protected from force push
|
||||
- Never rewritten
|
||||
|
||||
**Feature branches**: `feature/<description>` or `<description>`
|
||||
- Branch from `main`
|
||||
- Merge into `main`
|
||||
- Short-lived (hours to days, not weeks)
|
||||
- Deleted after merge
|
||||
|
||||
**Fix branches**: `fix/<description>` or `bugfix/<description>`
|
||||
- Branch from `main`
|
||||
- Merge into `main`
|
||||
- Deleted after merge
|
||||
|
||||
**Hotfix branches**: `hotfix/<version>-<description>` (post-1.0.0 only)
|
||||
- Branch from release tag (e.g., `v1.0.0`)
|
||||
- Fix critical production bugs
|
||||
- Tagged as new patch release (e.g., `v1.0.1`)
|
||||
- Merged into `main`
|
||||
- Deleted after release
|
||||
|
||||
**Release branches**: `release/<version>` (optional, rarely used)
|
||||
- Only if release preparation requires multiple commits
|
||||
- For V1, likely unnecessary (prepare on `main` or feature branch)
|
||||
- Branch from `main`, merge back to `main`, tag, delete
|
||||
|
||||
### Branch Naming Conventions
|
||||
|
||||
**Preferred format**: `<type>/<description>`
|
||||
|
||||
**Types**:
|
||||
- `feature/` - New features
|
||||
- `fix/` or `bugfix/` - Bug fixes
|
||||
- `hotfix/` - Production hotfixes
|
||||
- `docs/` - Documentation only
|
||||
- `refactor/` - Code refactoring
|
||||
- `test/` - Test additions
|
||||
- `chore/` - Maintenance tasks
|
||||
|
||||
**Description**:
|
||||
- Lowercase with hyphens
|
||||
- Descriptive but concise
|
||||
- Example: `feature/micropub-endpoint`, `fix/rss-timezone`
|
||||
|
||||
**Alternative**: Simple description without prefix (e.g., `micropub-endpoint`)
|
||||
|
||||
### Tagging Strategy
|
||||
|
||||
**Format**: `vMAJOR.MINOR.PATCH[-PRERELEASE]`
|
||||
|
||||
**Type**: Annotated tags (always)
|
||||
|
||||
**Examples**:
|
||||
- `v0.1.0` - Development release
|
||||
- `v1.0.0` - First stable release
|
||||
- `v1.0.1` - Patch release
|
||||
- `v1.0.0-alpha.1` - Pre-release
|
||||
|
||||
**Tag message format**:
|
||||
```
|
||||
Release MAJOR.MINOR.PATCH: <Brief description>
|
||||
|
||||
[Optional details]
|
||||
```
|
||||
|
||||
### Workflows
|
||||
|
||||
**Development (0.x.y)**:
|
||||
1. Work on `main` or feature branches
|
||||
2. Commit frequently
|
||||
3. Tag development milestones (v0.1.0, v0.2.0)
|
||||
4. Breaking changes allowed
|
||||
|
||||
**Stable releases (1.0.0+)**:
|
||||
1. Prepare release on `main`
|
||||
2. Update version and changelog
|
||||
3. Commit version bump
|
||||
4. Create annotated tag
|
||||
5. Push main and tag
|
||||
|
||||
**Hotfixes (post-1.0.0)**:
|
||||
1. Branch from release tag
|
||||
2. Fix bug
|
||||
3. Update version and changelog
|
||||
4. Tag new patch version
|
||||
5. Merge to `main`
|
||||
6. Push main and tag
|
||||
7. Delete hotfix branch
|
||||
|
||||
## Rationale
|
||||
|
||||
### Why Simplified Trunk-Based Development?
|
||||
|
||||
**Trunk-based development** means developers integrate to a single branch (`main`) frequently, using short-lived feature branches.
|
||||
|
||||
**Pros**:
|
||||
1. **Simple** - One primary branch, minimal overhead
|
||||
2. **Scalable** - Works for solo and small teams
|
||||
3. **Fast integration** - Reduces merge conflicts
|
||||
4. **Always releasable** - Main stays stable
|
||||
5. **Aligns with CI/CD** - Easy to automate testing
|
||||
6. **Reduces complexity** - No long-lived branches to manage
|
||||
|
||||
**Fits StarPunk because**:
|
||||
- Personal project optimized for simplicity
|
||||
- Small codebase, infrequent releases
|
||||
- Solo developer initially
|
||||
- No need for complex branching
|
||||
|
||||
### Why Main Instead of Master?
|
||||
|
||||
**Decision**: Use `main` as primary branch name
|
||||
|
||||
**Rationale**:
|
||||
1. **Industry standard** - GitHub, GitLab default since 2020
|
||||
2. **Inclusive language** - Removes potentially offensive terminology
|
||||
3. **Aligns with modern practices** - Most new projects use `main`
|
||||
4. **Clear semantics** - "Main" clearly indicates primary branch
|
||||
5. **No functional difference** - Just a name, but better name
|
||||
|
||||
**Migration**:
|
||||
- Project just renamed `master` → `main`
|
||||
- All documentation uses `main` consistently
|
||||
|
||||
### Why Not Git Flow?
|
||||
|
||||
**Git Flow** is a popular branching model with `main`, `develop`, `release`, `hotfix`, and `feature` branches.
|
||||
|
||||
**Considered but rejected**:
|
||||
|
||||
**Pros**:
|
||||
- Well-defined process
|
||||
- Clear separation of development and production
|
||||
- Structured release process
|
||||
|
||||
**Cons**:
|
||||
- **Too complex** for indie project
|
||||
- **Overhead** - Multiple long-lived branches
|
||||
- **Slow integration** - Features merge to develop, not main
|
||||
- **Designed for** scheduled releases, not continuous delivery
|
||||
- **Overkill** for single developer
|
||||
|
||||
**Conclusion**: Git Flow's complexity doesn't justify benefits for StarPunk's scale
|
||||
|
||||
### Why Not GitHub Flow?
|
||||
|
||||
**GitHub Flow** is a simpler model: just `main` and feature branches, deploy from `main`.
|
||||
|
||||
**Very close to our choice**:
|
||||
|
||||
**Pros**:
|
||||
- Simple - only `main` + feature branches
|
||||
- Fast - deploy anytime
|
||||
- Works well with pull requests
|
||||
|
||||
**Differences from our approach**:
|
||||
- GitHub Flow deploys directly from `main`
|
||||
- We tag releases on `main` instead
|
||||
- We add hotfix branches for production patches
|
||||
|
||||
**Conclusion**: We essentially use GitHub Flow + semantic versioning + hotfix branches
|
||||
|
||||
### Why Annotated Tags?
|
||||
|
||||
**Annotated vs lightweight tags**:
|
||||
|
||||
**Annotated tags** (chosen):
|
||||
- Contain metadata (tagger, date, message)
|
||||
- Can include release notes
|
||||
- Can be GPG signed
|
||||
- Treated as full Git objects
|
||||
- Required: `git tag -a v1.0.0 -m "Release 1.0.0"`
|
||||
|
||||
**Lightweight tags** (rejected):
|
||||
- Just pointers to commits
|
||||
- No metadata
|
||||
- Created: `git tag v1.0.0`
|
||||
|
||||
**Decision**: Always use annotated tags for releases
|
||||
- Provides complete release history
|
||||
- Can include release notes in tag message
|
||||
- Better for professional releases
|
||||
|
||||
### Why Feature Branches?
|
||||
|
||||
**Alternatives**:
|
||||
1. **Direct commits to main** - Fast but risky
|
||||
2. **Feature branches** - Slight overhead but safer
|
||||
3. **Pull request workflow** - Most structured
|
||||
|
||||
**Decision**: Use feature branches with flexible merge approach
|
||||
|
||||
**For solo development**:
|
||||
- Feature branches optional for small changes
|
||||
- Required for larger features
|
||||
- Merge directly without pull request
|
||||
|
||||
**For team development**:
|
||||
- Feature branches required
|
||||
- Pull request review before merge
|
||||
- Delete branch after merge
|
||||
|
||||
**Benefits**:
|
||||
- Isolates work in progress
|
||||
- Enables experimentation
|
||||
- Keeps main stable
|
||||
- Scalable to team workflow
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
1. **Simple to understand** - One primary branch, clear workflows
|
||||
2. **Scalable** - Works solo, scales to small teams
|
||||
3. **Fast integration** - Short-lived branches reduce conflicts
|
||||
4. **Clean history** - Clear tags for every release
|
||||
5. **Semantic versioning alignment** - Tag strategy matches SemVer
|
||||
6. **Hotfix capability** - Can patch production releases
|
||||
7. **Low overhead** - No complex branch management
|
||||
8. **Standard practices** - Uses modern Git conventions
|
||||
9. **Well-documented** - Clear workflows for common scenarios
|
||||
10. **Flexible** - Can use pull requests or direct merges
|
||||
|
||||
### Negative
|
||||
|
||||
1. **Discipline required** - Main must stay stable (tests must pass)
|
||||
2. **Manual version management** - Must update version, changelog, tag (for V1)
|
||||
3. **Solo optimization** - Strategy favors individual over large team
|
||||
4. **No develop buffer** - Changes go directly to main (requires good testing)
|
||||
|
||||
### Neutral
|
||||
|
||||
1. **Feature branch discipline** - Must keep branches short-lived
|
||||
2. **Test coverage important** - Main stability depends on testing
|
||||
3. **Rebase vs merge** - Team must choose and be consistent
|
||||
|
||||
### Mitigations
|
||||
|
||||
**For main stability**:
|
||||
- Run tests before merging
|
||||
- Use pull requests for team development
|
||||
- Establish branch protection rules when team grows
|
||||
|
||||
**For version management**:
|
||||
- Document clear release process
|
||||
- Create release checklist
|
||||
- Consider automation in V2+ if needed
|
||||
|
||||
**For long-lived branches**:
|
||||
- Review open branches weekly
|
||||
- Delete stale branches
|
||||
- Encourage frequent integration
|
||||
|
||||
## Implementation
|
||||
|
||||
### Immediate Actions
|
||||
|
||||
1. **Primary branch**: Already renamed to `main`
|
||||
2. **Create documentation**: `docs/standards/git-branching-strategy.md`
|
||||
3. **Update all references**: Ensure docs use `main` consistently
|
||||
4. **Initial tag**: Tag current state as `v0.1.0`
|
||||
|
||||
### Branch Protection (Future)
|
||||
|
||||
When team grows or project matures, add GitHub branch protection:
|
||||
|
||||
```
|
||||
Settings → Branches → Add rule for main:
|
||||
- Require pull request reviews (1 approval)
|
||||
- Require status checks to pass
|
||||
- Prevent force push
|
||||
- Prevent deletion
|
||||
```
|
||||
|
||||
For solo development: Self-discipline instead of enforced rules
|
||||
|
||||
### Release Process
|
||||
|
||||
**Development releases (0.x.y)**:
|
||||
1. Update `starpunk/__init__.py` version
|
||||
2. Update `CHANGELOG.md`
|
||||
3. Commit: `git commit -m "Bump version to 0.x.y"`
|
||||
4. Tag: `git tag -a v0.x.y -m "Development release 0.x.y"`
|
||||
5. Push: `git push origin main v0.x.y`
|
||||
|
||||
**Stable releases (1.0.0+)**:
|
||||
1. Update version and changelog
|
||||
2. Commit version bump
|
||||
3. Tag: `git tag -a v1.0.0 -m "Release 1.0.0: First stable release"`
|
||||
4. Push: `git push origin main v1.0.0`
|
||||
|
||||
**Hotfixes**:
|
||||
1. Branch: `git checkout -b hotfix/1.0.1-fix v1.0.0`
|
||||
2. Fix bug and update version
|
||||
3. Tag: `git tag -a v1.0.1 -m "Hotfix 1.0.1: Bug fix"`
|
||||
4. Merge: `git checkout main && git merge hotfix/1.0.1-fix`
|
||||
5. Push: `git push origin main v1.0.1`
|
||||
6. Delete: `git branch -d hotfix/1.0.1-fix`
|
||||
|
||||
### Documentation
|
||||
|
||||
Created comprehensive documentation:
|
||||
- **Strategy document**: `docs/standards/git-branching-strategy.md`
|
||||
- Branch types and naming
|
||||
- Workflows and examples
|
||||
- Best practices
|
||||
- Troubleshooting
|
||||
- **This ADR**: `docs/decisions/ADR-009-git-branching-strategy.md`
|
||||
- Decision rationale
|
||||
- Alternatives considered
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
### Alternative 1: Git Flow
|
||||
|
||||
**Description**: Use full Git Flow with `main`, `develop`, `release`, `hotfix`, `feature` branches
|
||||
|
||||
**Pros**:
|
||||
- Well-established pattern
|
||||
- Clear separation of development and production
|
||||
- Structured release process
|
||||
- Good for scheduled releases
|
||||
|
||||
**Cons**:
|
||||
- Too complex for indie project
|
||||
- Multiple long-lived branches
|
||||
- Slower integration
|
||||
- More overhead
|
||||
- Designed for different release model
|
||||
|
||||
**Rejected**: Complexity doesn't match project scale
|
||||
|
||||
### Alternative 2: Trunk-Based Development (Pure)
|
||||
|
||||
**Description**: All commits directly to `main`, no feature branches
|
||||
|
||||
**Pros**:
|
||||
- Maximum simplicity
|
||||
- Fastest integration
|
||||
- No branch management
|
||||
|
||||
**Cons**:
|
||||
- Risky - broken commits go to main
|
||||
- No isolation for work in progress
|
||||
- Difficult for team collaboration
|
||||
- No experimentation space
|
||||
|
||||
**Rejected**: Too risky, doesn't scale to team
|
||||
|
||||
### Alternative 3: GitHub Flow (Pure)
|
||||
|
||||
**Description**: `main` + feature branches, deploy from `main` continuously
|
||||
|
||||
**Pros**:
|
||||
- Simple and well-documented
|
||||
- Works well with pull requests
|
||||
- Fast deployment
|
||||
|
||||
**Cons**:
|
||||
- Designed for continuous deployment
|
||||
- Doesn't emphasize versioned releases
|
||||
- No hotfix branch pattern
|
||||
|
||||
**Partially adopted**: We use this + semantic versioning + hotfix branches
|
||||
|
||||
### Alternative 4: Release Branches Primary
|
||||
|
||||
**Description**: Always use release branches, never tag `main` directly
|
||||
|
||||
**Pros**:
|
||||
- Clear release preparation phase
|
||||
- Can stabilize release while main continues
|
||||
|
||||
**Cons**:
|
||||
- Adds complexity
|
||||
- Creates long-lived branches
|
||||
- Overkill for small project
|
||||
|
||||
**Rejected**: Unnecessary complexity for V1
|
||||
|
||||
### Alternative 5: Keep Master Branch Name
|
||||
|
||||
**Description**: Continue using `master` instead of `main`
|
||||
|
||||
**Pros**:
|
||||
- Traditional name
|
||||
- No migration needed
|
||||
- No functional difference
|
||||
|
||||
**Cons**:
|
||||
- Outdated convention
|
||||
- Out of step with industry
|
||||
- Potentially offensive terminology
|
||||
- New projects use `main`
|
||||
|
||||
**Rejected**: Modern standard is `main`, no reason not to adopt
|
||||
|
||||
## References
|
||||
|
||||
### Git Branching Models
|
||||
|
||||
- [Trunk-Based Development](https://trunkbaseddevelopment.com/) - Pattern we primarily follow
|
||||
- [GitHub Flow](https://guides.github.com/introduction/flow/) - Simplified flow (close to ours)
|
||||
- [Git Flow](https://nvie.com/posts/a-successful-git-branching-model/) - More complex model (not adopted)
|
||||
- [GitLab Flow](https://docs.gitlab.com/ee/topics/gitlab_flow.html) - Hybrid approach
|
||||
|
||||
### Branch Naming
|
||||
|
||||
- [GitHub Renaming](https://github.com/github/renaming) - Main branch renaming initiative
|
||||
- [Git Branch Naming](https://deepsource.io/blog/git-branch-naming-conventions/) - Naming conventions
|
||||
|
||||
### Versioning Integration
|
||||
|
||||
- [Semantic Versioning](https://semver.org/) - Version numbering
|
||||
- [Git Tagging](https://git-scm.com/book/en/v2/Git-Basics-Tagging) - Tag documentation
|
||||
|
||||
### Internal Documentation
|
||||
|
||||
- [ADR-008: Versioning Strategy](/home/phil/Projects/starpunk/docs/decisions/ADR-008-versioning-strategy.md) - Semantic versioning decision
|
||||
- [Versioning Strategy](/home/phil/Projects/starpunk/docs/standards/versioning-strategy.md) - Complete versioning spec
|
||||
- [Git Branching Strategy](/home/phil/Projects/starpunk/docs/standards/git-branching-strategy.md) - Complete branching spec
|
||||
- [Development Setup](/home/phil/Projects/starpunk/docs/standards/development-setup.md) - Development workflow
|
||||
|
||||
---
|
||||
|
||||
**ADR**: 009
|
||||
**Date**: 2025-11-18
|
||||
**Status**: Accepted
|
||||
**Decision**: Adopt simplified trunk-based development with `main` branch, feature branches, semantic versioning tags, and hotfix capability
|
||||
**Supersedes**: None
|
||||
Reference in New Issue
Block a user