147 lines
4.6 KiB
Python
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
|
|
]
|