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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user