docs: v1.5.0 planning - ADR-062, release plan, and design docs
- ADR-062: Timestamp-based slug format (supersedes ADR-007) - Updated v1.5.0 RELEASE.md with 6-phase plan - Updated BACKLOG.md with deferred N+1 query locations - Developer questions and architect responses for Phase 1 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
412
docs/design/v1.5.0/2025-12-16-architect-responses.md
Normal file
412
docs/design/v1.5.0/2025-12-16-architect-responses.md
Normal file
@@ -0,0 +1,412 @@
|
||||
# Architect Responses - v1.5.0 Developer Questions
|
||||
|
||||
**Date**: 2025-12-16
|
||||
**Architect**: StarPunk Architect Agent
|
||||
**In Response To**: `docs/design/v1.5.0/2025-12-16-developer-questions.md`
|
||||
|
||||
---
|
||||
|
||||
## Response Summary
|
||||
|
||||
All 8 questions have been addressed. The implementation can proceed immediately after reading this document. Key decisions:
|
||||
|
||||
1. **Q1**: Keep existing pattern (pass `existing_slugs: Set[str]`). ADR-062 pseudocode is conceptual.
|
||||
2. **Q2**: First note gets base slug, first collision gets `-1`. The table in ADR-062 is correct.
|
||||
3. **Q3**: Create new function in `slug_utils.py`. Migration path provided.
|
||||
4. **Q4**: Keep reserved slug validation for custom slugs only. Safe to skip for timestamp slugs.
|
||||
5. **Q5**: Tests should use pattern matching for timestamps. Test strategy provided.
|
||||
6. **Q6**: Phase 0 is a hard dependency. Cannot proceed with Phase 1 until all tests pass.
|
||||
7. **Q7**: Slug format is opaque. No format-aware code needed.
|
||||
8. **Q8**: Phases 2-4 can be parallelized. Commit each phase separately.
|
||||
|
||||
---
|
||||
|
||||
## Detailed Responses
|
||||
|
||||
### Q1: Collision handling function location and signature
|
||||
|
||||
**Decision**: Keep the existing pattern of accepting `existing_slugs: Set[str]` as a parameter.
|
||||
|
||||
**Rationale**:
|
||||
The ADR-062 pseudocode (`slug_exists(base_slug)`) is conceptual illustration only. The actual implementation should:
|
||||
|
||||
1. Follow the existing pattern in the codebase (passing pre-fetched slug sets)
|
||||
2. Avoid introducing database coupling into utility functions
|
||||
3. Prevent N+1 queries when checking multiple candidates
|
||||
|
||||
**Implementation Specification**:
|
||||
|
||||
Create a new function in `/home/phil/Projects/starpunk/starpunk/slug_utils.py`:
|
||||
|
||||
```python
|
||||
def generate_timestamp_slug(
|
||||
created_at: datetime = None,
|
||||
existing_slugs: Set[str] = None
|
||||
) -> str:
|
||||
"""Generate a timestamp-based slug with collision handling.
|
||||
|
||||
Per ADR-062: Default format is YYYYMMDDHHMMSS with sequential
|
||||
suffix (-1, -2, etc.) for collisions.
|
||||
|
||||
Args:
|
||||
created_at: Note creation timestamp (defaults to now)
|
||||
existing_slugs: Set of existing slugs to check for collisions
|
||||
|
||||
Returns:
|
||||
Unique timestamp-based slug
|
||||
"""
|
||||
if created_at is None:
|
||||
created_at = datetime.utcnow()
|
||||
|
||||
if existing_slugs is None:
|
||||
existing_slugs = set()
|
||||
|
||||
base_slug = created_at.strftime("%Y%m%d%H%M%S")
|
||||
|
||||
# If no collision, return base slug
|
||||
if base_slug not in existing_slugs:
|
||||
return base_slug
|
||||
|
||||
# Sequential suffix for collisions
|
||||
suffix = 1
|
||||
while f"{base_slug}-{suffix}" in existing_slugs:
|
||||
suffix += 1
|
||||
|
||||
return f"{base_slug}-{suffix}"
|
||||
```
|
||||
|
||||
**Location**: `/home/phil/Projects/starpunk/starpunk/slug_utils.py`
|
||||
|
||||
**Do NOT modify**: The `ensure_unique_slug()` function signature in ADR-062 is pseudocode. Do not create a function with database coupling.
|
||||
|
||||
---
|
||||
|
||||
### Q2: Sequential suffix starting number
|
||||
|
||||
**Decision**: First note gets base slug (no suffix). First collision gets `-1`.
|
||||
|
||||
**Clarification**: The examples table in ADR-062 is correct. The text "first collision: `20251216143052-1`" means the first **duplicate** (collision) gets `-1`, not the first note overall.
|
||||
|
||||
**Example Scenario**:
|
||||
```
|
||||
Note A created at 14:30:52 -> slug = "20251216143052" (base, no suffix)
|
||||
Note B created at 14:30:52 -> slug = "20251216143052-1" (first collision)
|
||||
Note C created at 14:30:52 -> slug = "20251216143052-2" (second collision)
|
||||
```
|
||||
|
||||
**Implementation**: Sequential suffix loop should start at `1`, not `2`:
|
||||
|
||||
```python
|
||||
suffix = 1
|
||||
while f"{base_slug}-{suffix}" in existing_slugs:
|
||||
suffix += 1
|
||||
return f"{base_slug}-{suffix}"
|
||||
```
|
||||
|
||||
**Note**: This differs from the existing `make_slug_unique_with_suffix()` function in `slug_utils.py` which starts at `-2`. The new timestamp slug function should start at `-1` per ADR-062.
|
||||
|
||||
---
|
||||
|
||||
### Q3: Two `generate_slug()` functions with different signatures
|
||||
|
||||
**Decision**: Create a new function in `slug_utils.py`. Do not modify or remove the existing function in `utils.py`.
|
||||
|
||||
**Migration Path**:
|
||||
|
||||
1. **Create new function** in `/home/phil/Projects/starpunk/starpunk/slug_utils.py`:
|
||||
- Name: `generate_timestamp_slug()` (as specified in Q1 response)
|
||||
- Do not name it `generate_slug()` to avoid confusion during migration
|
||||
|
||||
2. **Update `/home/phil/Projects/starpunk/starpunk/notes.py`** (lines 228-234):
|
||||
|
||||
**Current code** (line 228-234):
|
||||
```python
|
||||
else:
|
||||
# Generate base slug from content
|
||||
base_slug = generate_slug(content, created_at)
|
||||
|
||||
# Make unique if collision
|
||||
slug = make_slug_unique(base_slug, existing_slugs)
|
||||
```
|
||||
|
||||
**Replace with**:
|
||||
```python
|
||||
else:
|
||||
# Generate timestamp-based slug (ADR-062)
|
||||
from starpunk.slug_utils import generate_timestamp_slug
|
||||
slug = generate_timestamp_slug(created_at, existing_slugs)
|
||||
```
|
||||
|
||||
3. **Keep the existing function** in `utils.py` unchanged. It may be used elsewhere or for future content-based slug options.
|
||||
|
||||
4. **Update imports** in `notes.py`:
|
||||
- Remove `generate_slug` from the `utils` import line (line 34) if no longer needed
|
||||
- Verify no other callers use `generate_slug()` for default note creation
|
||||
|
||||
**Rationale**:
|
||||
- Creating a distinctly-named function makes the transition explicit
|
||||
- Preserves backward compatibility if content-based slugs are ever needed
|
||||
- Allows gradual migration with clear audit trail
|
||||
|
||||
---
|
||||
|
||||
### Q4: Reserved slug handling in timestamp context
|
||||
|
||||
**Decision**:
|
||||
- **Default timestamp slugs**: Safe to skip reserved slug validation (timestamps cannot collide with reserved words)
|
||||
- **Custom slugs**: Must retain reserved slug validation
|
||||
|
||||
**Implementation**:
|
||||
|
||||
The new `generate_timestamp_slug()` function does NOT need to check reserved slugs because:
|
||||
- Reserved slugs are alphabetic (`api`, `admin`, `feed`, etc.)
|
||||
- Timestamp slugs are purely numeric (`20251216143052`)
|
||||
- These sets are mutually exclusive by construction
|
||||
|
||||
**However**, the existing custom slug validation in `validate_and_sanitize_custom_slug()` must retain reserved slug checking because user-provided slugs could be anything.
|
||||
|
||||
**Code Impact**:
|
||||
- `generate_timestamp_slug()`: No reserved slug check needed
|
||||
- `validate_and_sanitize_custom_slug()`: Unchanged (already handles reserved slugs)
|
||||
- `validate_slug()` in `utils.py`: Unchanged (still validates reserved slugs for custom slugs)
|
||||
|
||||
**Simplification**: You may remove the defensive `validate_slug()` call after timestamp generation in `notes.py` (line 236-237) since timestamp slugs are guaranteed valid by construction:
|
||||
```python
|
||||
# This check is no longer needed for timestamp slugs:
|
||||
# if not validate_slug(slug):
|
||||
# raise InvalidNoteDataError(...)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Q5: Test update strategy for slug format changes
|
||||
|
||||
**Decision**: Use pattern matching for timestamp assertions. Preserve content-based tests in a separate test file or clearly marked section.
|
||||
|
||||
**Test Strategy**:
|
||||
|
||||
1. **For new timestamp slug tests**, use regex pattern matching:
|
||||
```python
|
||||
import re
|
||||
|
||||
TIMESTAMP_SLUG_PATTERN = re.compile(r'^\d{14}(-\d+)?$')
|
||||
|
||||
def test_default_slug_is_timestamp():
|
||||
note = create_note("Some content here")
|
||||
assert TIMESTAMP_SLUG_PATTERN.match(note.slug)
|
||||
|
||||
def test_slug_collision_adds_suffix():
|
||||
# Create two notes at same timestamp
|
||||
fixed_time = datetime(2025, 12, 16, 14, 30, 52)
|
||||
note1 = create_note("First note", created_at=fixed_time)
|
||||
note2 = create_note("Second note", created_at=fixed_time)
|
||||
|
||||
assert note1.slug == "20251216143052"
|
||||
assert note2.slug == "20251216143052-1"
|
||||
```
|
||||
|
||||
2. **For timestamp-specific assertions**, use fixed timestamps in tests:
|
||||
```python
|
||||
def test_timestamp_slug_format():
|
||||
"""Test that timestamp slug matches exact format."""
|
||||
fixed_time = datetime(2025, 12, 16, 14, 30, 52)
|
||||
slug = generate_timestamp_slug(fixed_time, set())
|
||||
assert slug == "20251216143052"
|
||||
```
|
||||
|
||||
3. **Content-based tests**: Move to a separate section or file named `test_legacy_slug_generation.py` with a comment:
|
||||
```python
|
||||
"""
|
||||
Legacy tests for content-based slug generation.
|
||||
|
||||
These test the generate_slug() function in utils.py which is
|
||||
preserved for backward compatibility. New notes use timestamp-based
|
||||
slugs per ADR-062.
|
||||
"""
|
||||
```
|
||||
|
||||
4. **Update existing tests that assert specific slug formats**:
|
||||
- Replace exact string assertions (`assert slug == "hello-world"`) with pattern matching
|
||||
- OR use fixed timestamps and assert exact expected values
|
||||
|
||||
**Affected Test Files** (estimate from your question):
|
||||
- `/home/phil/Projects/starpunk/tests/test_utils.py`: ~20 tests
|
||||
- `/home/phil/Projects/starpunk/tests/test_notes.py`: ~10 tests
|
||||
- `/home/phil/Projects/starpunk/tests/test_micropub.py`: ~10 tests
|
||||
|
||||
**Helper Function** (add to test utilities or conftest.py):
|
||||
```python
|
||||
def assert_valid_timestamp_slug(slug: str) -> None:
|
||||
"""Assert slug matches timestamp format per ADR-062."""
|
||||
pattern = re.compile(r'^\d{14}(-\d+)?$')
|
||||
assert pattern.match(slug), f"Slug '{slug}' does not match timestamp format"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Q6: Phase 0 priority - are tests blocking?
|
||||
|
||||
**Decision**: Phase 0 is a **hard dependency**. Phase 1 cannot proceed until all tests pass.
|
||||
|
||||
**Rationale**:
|
||||
|
||||
1. **Failing tests mask regressions**: If you implement Phase 1 (slug changes) while 19 tests are failing, you cannot distinguish between:
|
||||
- Pre-existing failures
|
||||
- New failures caused by your changes
|
||||
|
||||
2. **CI/CD integrity**: The project should maintain a green build. Committing Phase 1 work with known failing tests violates this principle.
|
||||
|
||||
3. **Debugging complexity**: If slug changes interact unexpectedly with the failing test areas (e.g., feed generation, content negotiation), debugging becomes significantly harder.
|
||||
|
||||
**Implementation Order**:
|
||||
|
||||
```
|
||||
Phase 0 (Test Fixes) <-- MUST COMPLETE FIRST
|
||||
|
|
||||
v
|
||||
+-- Phase 1 (Timestamp Slugs)
|
||||
|
|
||||
+-- Phase 2 (Debug Files) <-- Can run in parallel
|
||||
|
|
||||
+-- Phase 3 (N+1 Query Fix) <-- after Phase 0
|
||||
|
|
||||
+-- Phase 4 (Atomic Variants)
|
||||
|
|
||||
v
|
||||
Phase 5 (Test Coverage) <-- LAST
|
||||
```
|
||||
|
||||
**Exception**: If fixing a Phase 0 test reveals it requires slug-related changes, document the circular dependency and bring to architect attention.
|
||||
|
||||
**Recommendation**: Start Phase 0 immediately. Many of those 19 failures may be simple test maintenance issues (timeouts, assertions that need updating, etc.).
|
||||
|
||||
---
|
||||
|
||||
### Q7: Backwards compatibility for existing notes
|
||||
|
||||
**Decision**: Slug format is **opaque**. No code needs to be format-aware.
|
||||
|
||||
**Clarification**:
|
||||
|
||||
1. **No format detection needed**: Do not create an `is_timestamp_slug()` helper function. The system should treat all slugs identically regardless of format.
|
||||
|
||||
2. **Code paths are format-agnostic**:
|
||||
- Database queries use slug as opaque string key
|
||||
- URL routing uses slug as path parameter
|
||||
- Feed generation includes slug verbatim
|
||||
- Templates display slug without interpretation
|
||||
|
||||
3. **Coexistence is automatic**: Content-based slugs (`my-first-post`) and timestamp slugs (`20251216143052`) are both valid slug patterns. They coexist naturally.
|
||||
|
||||
4. **Sorting behavior**:
|
||||
- Database ORDER BY on `created_at` is unaffected
|
||||
- Alphabetical slug sorting would interleave formats (numbers sort before letters)
|
||||
- This is acceptable; no UI sorts by slug alphabetically
|
||||
|
||||
5. **Display considerations**:
|
||||
- Timestamps are less readable in URLs, but URLs are rarely human-read
|
||||
- UI displays note content, not slugs
|
||||
- RSS feeds include full URLs; format doesn't affect feed readers
|
||||
|
||||
**No audit required**: You do not need to audit slug usages. The design ensures format-agnosticism.
|
||||
|
||||
---
|
||||
|
||||
### Q8: Phase ordering rationale
|
||||
|
||||
**Decision**:
|
||||
- Phases 2, 3, and 4 **can be parallelized** after Phase 0 completes
|
||||
- Phase 1 (Timestamp Slugs) should be done **before** Phases 2-4 for cleaner testing
|
||||
- Each phase should be **committed separately**
|
||||
|
||||
**Recommended Order**:
|
||||
|
||||
```
|
||||
Day 1: Phase 0 (Test Fixes) - Get to green build
|
||||
Day 2: Phase 1 (Timestamp Slugs) - Quick win, user-visible improvement
|
||||
Day 3: Phase 2 (Debug Files) or Phase 3 (N+1) or Phase 4 (Atomic) - Any order
|
||||
Day 4: Remaining phases from above
|
||||
Day 5: Phase 5 (Test Coverage) - Must be last
|
||||
```
|
||||
|
||||
**Commit Strategy**:
|
||||
|
||||
Each phase gets its own commit or small series of commits:
|
||||
```
|
||||
feat(slugs): Implement timestamp-based slugs per ADR-062
|
||||
fix(tests): Resolve 19 failing tests in Phase 0
|
||||
feat(media): Add debug file cleanup and configuration
|
||||
perf(feed): Batch load media and tags to fix N+1 query
|
||||
feat(media): Make variant generation atomic with database
|
||||
test: Expand coverage to 90% for v1.5.0
|
||||
```
|
||||
|
||||
**Why Not Batch**: Batching commits makes bisecting regressions harder and obscures the changelog.
|
||||
|
||||
**Parallelization Note**: If you have capacity to work on multiple phases simultaneously (e.g., waiting for test runs), Phases 2-4 have no code overlap and can be developed in parallel branches:
|
||||
- `feature/v1.5.0-debug-files`
|
||||
- `feature/v1.5.0-n+1-fix`
|
||||
- `feature/v1.5.0-atomic-variants`
|
||||
|
||||
Then merge all into main before Phase 5.
|
||||
|
||||
---
|
||||
|
||||
## Observations Responses
|
||||
|
||||
### O1: Code duplication between `utils.py` and `slug_utils.py`
|
||||
|
||||
**Acknowledged**. The duplicate `RESERVED_SLUGS` definitions should be consolidated in a future cleanup. For v1.5.0:
|
||||
|
||||
- Use the more comprehensive list in `slug_utils.py` (12 items) as the canonical source
|
||||
- Do not consolidate in this release (scope creep)
|
||||
- Add to BACKLOG.md for v1.5.1 or v1.6.0
|
||||
|
||||
### O2: Timestamp format consistency
|
||||
|
||||
**Confirmed**: The hyphen removal in ADR-062 (`YYYYMMDDHHMMSS`) vs existing fallback (`YYYYMMDD-HHMMSS`) is intentional.
|
||||
|
||||
- ADR-062 format: `20251216143052` (14 characters, no separator)
|
||||
- Old fallback format: `20251216-143052` (15 characters, with hyphen)
|
||||
|
||||
**Rationale**:
|
||||
- One fewer character
|
||||
- Consistent with ISO 8601 compact format
|
||||
- No functional difference; purely aesthetic
|
||||
|
||||
**Action**: Use the unhyphenated format per ADR-062.
|
||||
|
||||
### O3: Test coverage gap mentioned in Phase 5
|
||||
|
||||
**Clarification**: This is intentional duplication for visibility.
|
||||
|
||||
- BACKLOG.md lists the gap for tracking
|
||||
- Phase 5 in RELEASE.md lists it for implementation planning
|
||||
- Both point to the same work item
|
||||
|
||||
No action needed; this is not an error.
|
||||
|
||||
---
|
||||
|
||||
## ADR-062 Updates Required
|
||||
|
||||
No updates to ADR-062 are required based on these questions. The ADR's pseudocode is understood to be conceptual, and all implementation details have been clarified in this response document.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
Based on these responses, the developer can now proceed with:
|
||||
|
||||
- [ ] Phase 0: Fix 19 failing tests
|
||||
- [ ] Create `generate_timestamp_slug()` in `slug_utils.py`
|
||||
- [ ] Update `notes.py` to use new timestamp slug function
|
||||
- [ ] Update tests with pattern matching strategy
|
||||
- [ ] Proceed with Phases 2-4 (order flexible)
|
||||
- [ ] Complete Phase 5 test coverage last
|
||||
|
||||
---
|
||||
|
||||
**Architect**: StarPunk Architect Agent
|
||||
**Status**: Questions Answered - Ready for Implementation
|
||||
**Next Action**: Developer to begin Phase 0 (Test Fixes)
|
||||
Reference in New Issue
Block a user