feat(slugs): Implement timestamp-based slugs per ADR-062

Replaces content-based slug generation with timestamp format YYYYMMDDHHMMSS.
Simplifies slug generation and improves privacy by not exposing note content in URLs.

Changes:
- Add generate_timestamp_slug() to slug_utils.py
- Update notes.py to use timestamp slugs for default generation
- Sequential collision suffix (-1, -2) instead of random
- Custom slugs via mp-slug continue to work unchanged
- 892 tests passing (+18 new timestamp slug tests)

Per ADR-062 and v1.5.0 Phase 1 specification.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-17 09:49:30 -07:00
parent 92e7bdd342
commit 3f1f82a749
6 changed files with 580 additions and 20 deletions

View File

@@ -226,15 +226,9 @@ def create_note(
if not success:
raise InvalidNoteDataError("slug", custom_slug, error)
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)
# Validate final slug (defensive check)
if not validate_slug(slug):
raise InvalidNoteDataError("slug", slug, f"Generated slug is invalid: {slug}")
# Generate timestamp-based slug (ADR-062)
from starpunk.slug_utils import generate_timestamp_slug
slug = generate_timestamp_slug(created_at, existing_slugs)
# 4. GENERATE FILE PATH
note_path = generate_note_path(slug, created_at, data_dir)

View File

@@ -241,6 +241,53 @@ def make_slug_unique_with_suffix(base_slug: str, existing_slugs: Set[str], max_a
)
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
Examples:
>>> generate_timestamp_slug(datetime(2025, 12, 16, 14, 30, 52), set())
'20251216143052'
>>> generate_timestamp_slug(datetime(2025, 12, 16, 14, 30, 52), {'20251216143052'})
'20251216143052-1'
>>> generate_timestamp_slug(datetime(2025, 12, 16, 14, 30, 52), {'20251216143052', '20251216143052-1'})
'20251216143052-2'
"""
if created_at is None:
created_at = datetime.utcnow()
if existing_slugs is None:
existing_slugs = set()
# Generate base timestamp slug (YYYYMMDDHHMMSS per ADR-062)
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 (starts at -1 per ADR-062)
suffix = 1
while f"{base_slug}-{suffix}" in existing_slugs:
suffix += 1
return f"{base_slug}-{suffix}"
def validate_and_sanitize_custom_slug(custom_slug: str, existing_slugs: Set[str]) -> tuple[bool, Optional[str], Optional[str]]:
"""
Validate and sanitize a custom slug from Micropub