- 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>
413 lines
15 KiB
Markdown
413 lines
15 KiB
Markdown
# 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)
|