Kansho/backend/writing_profile_schema.py
2026-08-25 13:57:23 +02:00

147 lines
4.6 KiB
Python

"""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
]