"""Stable Writing-Profile shell. Seed keys are ordering help, not a closed trait schema.""" from __future__ import annotations import re from datetime import datetime, timezone SLUG = re.compile(r"^[a-z][a-z0-9_]{1,62}$") LIFECYCLE = ("uninitialized", "initial_pending", "confirmed") LAYERS = ("core", "context", "output") TRAIT_STATUS = ("active", "merged", "split", "retired") REF_ROLES = ("evidence", "exemplar") ORIGINS = ("manual", "imported", "accepted_suggestion", "initial_build") CONTEXT_HINTS = { "vacation_diary": "autobiographical_journal", "travel_journal": "autobiographical_journal", "autobiographical_journal": "autobiographical_journal", "journal": "autobiographical_journal", "reflective": "reflective", } FACET_ALIASES = {"journal": "autobiographical_journal"} SEED_FACETS = { "core": {"layer": "core", "label": "Globaler Schreibkern"}, "autobiographical_journal": {"layer": "context", "label": "Autobiografisches Journaling"}, "reflective": {"layer": "context", "label": "Tiefer reflektierende Texte"}, } SEED_TRAIT_HINTS = ( {"slug": "rhythm", "label": "Satzlänge / Rhythmus"}, {"slug": "detail", "label": "Detailgrad"}, {"slug": "chronology", "label": "Erzählweise"}, {"slug": "lexicon", "label": "Wortwahl / typische Formulierungen"}, {"slug": "humor", "label": "Humor"}, {"slug": "emotional_directness", "label": "Emotionale Direktheit"}, {"slug": "transitions", "label": "Typische Übergänge"}, {"slug": "concreteness", "label": "Zeiten, Orte, Namen"}, {"slug": "event_vs_reflection", "label": "Ereignis / Reflexion"}, ) LEGACY_STYLE_KEYS = {item["slug"] for item in SEED_TRAIT_HINTS} EXISTING_BEFORE_NEW = ( "bestehenden Trait bestätigen", "präzisieren", "Scope ändern", "zusammenführen oder aufteilen", "erst dann einen neuen Trait anlegen", ) TRAIT_ACTIONS = ("confirm", "precisify", "rescope", "merge", "split", "create", "keep", "update") ACTION_ALIASES = {"keep": "confirm", "update": "precisify"} def normalize_facet_key(key: str | None) -> str: raw = (key or "").strip() return FACET_ALIASES.get(raw, raw) def facet_label(key: str) -> str: item = SEED_FACETS.get(normalize_facet_key(key)) if item: return item["label"] return key.replace("_", " ") def facet_layer(key: str, fallback: str = "context") -> str: item = SEED_FACETS.get(normalize_facet_key(key)) if item: return item["layer"] return fallback if fallback in LAYERS else "context" def valid_slug(value: str | None) -> bool: return bool(value and SLUG.match(value)) def coerce_slug(value: str, fallback: str = "trait") -> str: raw = (value or "").strip().lower().replace("-", "_").replace(" ", "_") cleaned = re.sub(r"[^a-z0-9_]", "", raw) if valid_slug(cleaned): return cleaned return fallback def hint_for_context(context_hint: str | None) -> str: raw = (context_hint or "").strip().lower() return CONTEXT_HINTS.get(raw, raw) def parse_when(raw: str | None) -> datetime | None: text = (raw or "").strip() if not text: return None try: stamp = datetime.fromisoformat(text.replace("Z", "+00:00")) except ValueError: try: stamp = datetime.strptime(text[:10], "%Y-%m-%d") except ValueError: return None if stamp.tzinfo is None: stamp = stamp.replace(tzinfo=timezone.utc) return stamp def recency_weight(occurred_at: str | None, now: datetime | None = None) -> float: """Newer texts weigh more for current expression; older texts stay as long-term evidence.""" stamp = parse_when(occurred_at) if not stamp: return 1.0 current = now or datetime.now(timezone.utc) days = max((current - stamp).days, 0) if days <= 90: return 1.25 if days <= 365: return 1.0 if days <= 1100: return 0.75 return 0.55 def recency_role(occurred_at: str | None, now: datetime | None = None) -> str: stamp = parse_when(occurred_at) if not stamp: return "undated" current = now or datetime.now(timezone.utc) days = max((current - stamp).days, 0) if days <= 365: return "current_expression" return "long_term" def seed_catalog() -> list[dict]: return [ { "kind": "facet", "key": key, "layer": item["layer"], "label": item["label"], "role": "ordering_help", } for key, item in SEED_FACETS.items() ] + [ { "kind": "trait_hint", "slug": item["slug"], "label": item["label"], "role": "ordering_help", } for item in SEED_TRAIT_HINTS ]