diff --git a/README.md b/README.md index d9b8b0b..706c1a8 100755 --- a/README.md +++ b/README.md @@ -30,3 +30,4 @@ Code layout: - `authoring_server.py` is the compatibility executable used by systemd. - `src/authoring_service/` contains the implementation modules. - `src/tests/` contains the unit tests. +- `docs/hidden-memory-architecture.md` documents the Fragment/Memory/Story model used by the Hidden Memory Observatory. diff --git a/docs/hidden-memory-architecture.md b/docs/hidden-memory-architecture.md new file mode 100644 index 0000000..b98c606 --- /dev/null +++ b/docs/hidden-memory-architecture.md @@ -0,0 +1,82 @@ +# Hidden Memory Architecture + +The hidden ecosystem has one friendly source of truth: + +- `assets/content/hidden-details.json` in `org_web` + +The live site still consumes generated constants in: + +- `assets/scripts/hidden-details.js` in `org_web` + +## Canonical Content + +The old `type` field is now treated as the runtime delivery kind. It answers: where does this need to go in the existing JavaScript arrays? + +The newer `contentClass` field answers: what is this thing conceptually? + +Use these classes: + +- `fragment`: tiny reusable language, such as tooltips, quotes, whispers, loading lines, and small dialogue. +- `memory`: a richer emotional moment or observation that can stand as an observatory node. +- `story material`: dialogue or sequence material that is primarily useful inside a story route. +- `interaction`: a trigger, route, keyboard secret, search response, seasonal event, or play-system behavior. +- `lore`: world material such as dreams, terminal logs, guestbook entries, or hidden archive objects. +- `system layer`: structural observatory material, such as the layer guide. + +## Stories + +Stories are curated routes. They should reference entries by id through `nodes`. + +Do not duplicate paragraphs inside a story when an existing fragment or memory can be referenced. A story gives order, title, tone, unlock conditions, and emotional shape. + +## Surfaces + +The `surfaces` field says where an entry is reusable: `tooltip`, `quote`, `story`, `observatory`, `search`, `keyboard`, `play`, `dream`, `temporal`, and similar surfaces. + +A tooltip fragment can appear in the footer without becoming a full story node. A memory can appear in the observatory and in a story without being forced into a rotating quote. An interaction can trigger a hidden route without pretending to be a narrative scene. + +## Observatory Roles + +Use `observatoryRole` to keep rendering calm: + +- `ambient`: small lights and flavor, usually fragments. +- `node`: substantial emotional points, usually memories or story material. +- `event`: triggered behavior, routes, search, keyboard, seasonal, and play content. +- `guide`: layer/system material. + +The observatory should render stories as the main territories, then reveal entries inside them. Zoomed-out views should favor routes and clusters; detailed views can show individual nodes, ambient fragments, and triggered events. + +## Examples + +`lima left this page a little steadier than she found it.` + +- Class: `fragment` +- Runtime kind: `hidden tooltip` +- Surfaces: `tooltip` +- Observatory role: `ambient` + +`July 2022, on the way to uni induction, lima pops into my life` + +- Class: `memory` +- Runtime kind: `journal entry` +- Surfaces: `story`, `observatory` +- Observatory role: `node` + +`search query "lima" opens a hidden route` + +- Class: `interaction` +- Runtime kind: `search route` +- Surfaces: `search`, `hidden route` +- Observatory role: `event` + +`The story of love between two souls` + +- Class: story record, not an entry class +- References: ordered entry ids in `nodes` +- Purpose: emotional route, not duplicated content + +## Authoring Rule + +Write the smallest canonical thing that is emotionally honest. + +If it is one line, make it a fragment. If it is a moment with weight, make it a memory. If it happens because of a trigger, make it an interaction. If it is a route through existing things, make it a story. diff --git a/src/authoring_service/constants.py b/src/authoring_service/constants.py index 6ae8ee2..11b343d 100755 --- a/src/authoring_service/constants.py +++ b/src/authoring_service/constants.py @@ -32,6 +32,64 @@ HIDDEN_CONTENT_TYPES = [ "keyboard secret", ] +HIDDEN_CONTENT_CLASSES = [ + "fragment", + "memory", + "story material", + "interaction", + "lore", + "system layer", +] + +HIDDEN_SURFACES = [ + "tooltip", + "quote", + "poem", + "story", + "observatory", + "hidden route", + "search", + "keyboard", + "play", + "dream", + "temporal", + "seasonal", + "loading", + "guestbook", + "terminal", + "layer guide", +] + +TYPE_ARCHITECTURE = { + "quote": {"contentClass": "fragment", "surfaces": ["quote", "observatory"], "observatory": "ambient"}, + "poem": {"contentClass": "fragment", "surfaces": ["poem", "observatory"], "observatory": "ambient"}, + "hidden dialogue": {"contentClass": "fragment", "surfaces": ["tooltip", "story", "observatory"], "observatory": "node"}, + "journal entry": {"contentClass": "memory", "surfaces": ["story", "observatory"], "observatory": "node"}, + "rare event": {"contentClass": "interaction", "surfaces": ["hidden route", "play", "observatory"], "observatory": "event"}, + "loading screen message": {"contentClass": "fragment", "surfaces": ["loading", "quote"], "observatory": "ambient"}, + "secret interaction": {"contentClass": "interaction", "surfaces": ["hidden route", "play"], "observatory": "event"}, + "hidden tooltip": {"contentClass": "fragment", "surfaces": ["tooltip"], "observatory": "ambient"}, + "future z message": {"contentClass": "memory", "surfaces": ["temporal", "story", "observatory"], "observatory": "node"}, + "young z memory fragment": {"contentClass": "memory", "surfaces": ["story", "observatory"], "observatory": "node"}, + "sensei chi wisdom entry": {"contentClass": "fragment", "surfaces": ["quote", "story", "observatory"], "observatory": "ambient"}, + "aphy system message": {"contentClass": "fragment", "surfaces": ["quote", "terminal"], "observatory": "ambient"}, + "lima note/message": {"contentClass": "fragment", "surfaces": ["quote", "story"], "observatory": "ambient"}, + "dream sequence": {"contentClass": "lore", "surfaces": ["dream", "play", "story", "observatory"], "observatory": "node"}, + "terminal log": {"contentClass": "lore", "surfaces": ["terminal", "play"], "observatory": "event"}, + "fake error message": {"contentClass": "interaction", "surfaces": ["play", "hidden route"], "observatory": "event"}, + "recurring joke": {"contentClass": "fragment", "surfaces": ["tooltip", "quote"], "observatory": "ambient"}, + "seasonal event": {"contentClass": "interaction", "surfaces": ["seasonal", "hidden route"], "observatory": "event"}, + "weather-based event": {"contentClass": "interaction", "surfaces": ["hidden route"], "observatory": "event"}, + "hover message": {"contentClass": "fragment", "surfaces": ["tooltip"], "observatory": "ambient"}, + "hidden achievement": {"contentClass": "interaction", "surfaces": ["play", "hidden route"], "observatory": "event"}, + "guestbook entry": {"contentClass": "lore", "surfaces": ["guestbook", "observatory"], "observatory": "ambient"}, + "hidden conversation": {"contentClass": "story material", "surfaces": ["story", "observatory"], "observatory": "node"}, + "family layer": {"contentClass": "system layer", "surfaces": ["layer guide"], "observatory": "guide"}, + "search toast": {"contentClass": "interaction", "surfaces": ["search"], "observatory": "event"}, + "search route": {"contentClass": "interaction", "surfaces": ["search", "hidden route"], "observatory": "event"}, + "keyboard secret": {"contentClass": "interaction", "surfaces": ["keyboard", "hidden route"], "observatory": "event"}, +} + CHARACTER_REGISTRY = { "young z": { "id": "young z", diff --git a/src/authoring_service/hidden.py b/src/authoring_service/hidden.py index 8ee4d83..49223a2 100755 --- a/src/authoring_service/hidden.py +++ b/src/authoring_service/hidden.py @@ -12,12 +12,15 @@ from .config import HIDDEN_BACKUP_DIR, HIDDEN_CONTENT_JSON, HIDDEN_DETAILS_JS, R from .constants import ( CHARACTER_REGISTRY, HIDDEN_CHARACTERS, + HIDDEN_CONTENT_CLASSES, HIDDEN_CONTENT_TYPES, HIDDEN_DISCOVERY_STYLES, HIDDEN_LAYER_DEPTHS, HIDDEN_RARITIES, + HIDDEN_SURFACES, HIDDEN_STORY_MARKERS, HIDDEN_TONES, + TYPE_ARCHITECTURE, ) from .utils import normalise_tags, slugify @@ -147,9 +150,11 @@ def make_hidden_entry(content_type: str, content: str, index: int, **extra: Any) title = str(extra.pop("title", "") or hidden_title(content_type, content, index)) characters = extra.pop("characters", None) or detect_hidden_characters(f"{title} {content}") tags = extra.pop("tags", None) or [slugify(character) for character in characters] + architecture = TYPE_ARCHITECTURE.get(content_type, TYPE_ARCHITECTURE["quote"]) entry = { "id": hidden_entry_id(content_type, index, title), "type": content_type, + "contentClass": extra.pop("contentClass", architecture["contentClass"]), "title": title, "content": content, "characters": characters, @@ -182,6 +187,9 @@ def make_hidden_entry(content_type: str, content: str, index: int, **extra: Any) "thematicLinks": extra.pop("thematicLinks", []), "symbolicLinks": extra.pop("symbolicLinks", []), "triggerLinks": extra.pop("triggerLinks", []), + "surfaces": extra.pop("surfaces", architecture["surfaces"]), + "observatoryRole": extra.pop("observatoryRole", architecture["observatory"]), + "canonicalUse": extra.pop("canonicalUse", ""), } entry.update(extra) return entry @@ -251,6 +259,7 @@ def normalize_hidden_entry(raw: dict[str, Any], existing: dict[str, Any] | None content_type = str(normalize_character_text_refs(content_type)) if content_type not in HIDDEN_CONTENT_TYPES: raise ValueError(f"Unsupported hidden content type: {content_type}") + architecture = TYPE_ARCHITECTURE.get(content_type, TYPE_ARCHITECTURE["quote"]) if not title: raise ValueError("Every hidden entry needs a title.") if not content and content_type not in {"search route"}: @@ -258,6 +267,10 @@ def normalize_hidden_entry(raw: dict[str, Any], existing: dict[str, Any] | None entry["id"] = slugify(str(entry.get("id") or title)) entry["title"] = title entry["type"] = content_type + content_class = str(entry.get("contentClass") or architecture["contentClass"]).strip() + if content_class not in HIDDEN_CONTENT_CLASSES: + content_class = architecture["contentClass"] + entry["contentClass"] = content_class entry["content"] = content detected_characters = detect_hidden_characters(" ".join([ title, @@ -305,8 +318,15 @@ def normalize_hidden_entry(raw: dict[str, Any], existing: dict[str, Any] | None "thematicLinks", "symbolicLinks", "triggerLinks", + "surfaces", ]: entry[key] = [str(normalize_character_text_refs(item)).strip() for item in entry.get(key, []) if str(item).strip()] + if not entry["surfaces"]: + entry["surfaces"] = list(architecture["surfaces"]) + entry["surfaces"] = [surface for surface in entry["surfaces"] if surface in HIDDEN_SURFACES] or list(architecture["surfaces"]) + observatory_role = str(entry.get("observatoryRole") or architecture["observatory"]).strip() + entry["observatoryRole"] = observatory_role if observatory_role in {"ambient", "node", "event", "guide"} else architecture["observatory"] + entry["canonicalUse"] = str(normalize_character_text_refs(entry.get("canonicalUse") or "")) for key in ["dialogue", "keyboard"]: if key in entry: entry[key] = normalize_character_text_refs(entry[key]) @@ -444,12 +464,15 @@ def load_hidden_store() -> dict[str, Any]: normalized_stories = migrate_hidden_stories(normalized) migrated_relationships = bool(normalized_stories) return { - "schemaVersion": 2, + "schemaVersion": 3, "source": HIDDEN_DETAILS_JS.relative_to(ROOT).as_posix(), "contentPath": HIDDEN_CONTENT_JSON.relative_to(ROOT).as_posix(), "migratedFromJs": migrated, "migratedRelationshipsToStories": migrated_relationships, "types": HIDDEN_CONTENT_TYPES, + "contentClasses": HIDDEN_CONTENT_CLASSES, + "surfaces": HIDDEN_SURFACES, + "typeArchitecture": TYPE_ARCHITECTURE, "characters": HIDDEN_CHARACTERS, "characterRegistry": CHARACTER_REGISTRY, "validation": validate_hidden_integrity(normalized, normalized_stories), @@ -506,11 +529,11 @@ def validate_hidden_integrity(entries: list[dict[str, Any]], stories: list[dict[ def hidden_architecture_recommendations() -> dict[str, Any]: return { - "storage": "Use assets/content/hidden-details.json as the friendly source of truth for memories and first-class stories, then regenerate the editable constants in assets/scripts/hidden-details.js.", + "storage": "Use assets/content/hidden-details.json as the friendly source of truth for canonical fragments, memories, interactions, lore, and first-class stories, then regenerate the editable constants in assets/scripts/hidden-details.js.", "backups": "Every save writes timestamped backups for both JSON and JS under backups/hidden-details/.", "versioning": "Commit the JSON and generated JS together so the live site and authoring history stay aligned.", - "collaboration": "For simultaneous editing, resolve conflicts by memory id and story id rather than by whole-file ownership.", - "scalability": "Stories are the primary emotional routes. Legacy relationship fields remain readable for migration, but the observatory should stay story-first.", + "collaboration": "For simultaneous editing, resolve conflicts by entry id and story id rather than by whole-file ownership.", + "scalability": "Stories are the primary emotional routes. Fragments remain reusable ambient language, memories become observatory nodes, and interactions describe triggers without pretending to be narrative scenes.", } @@ -640,7 +663,7 @@ def save_hidden_store(payload: dict[str, Any]) -> dict[str, Any]: HIDDEN_CONTENT_JSON.write_text( json.dumps( { - "schemaVersion": 2, + "schemaVersion": 3, "generatedAt": datetime.now().isoformat(timespec="seconds"), "entries": entries, "stories": stories, @@ -656,5 +679,3 @@ def save_hidden_store(payload: dict[str, Any]) -> dict[str, Any]: saved["message"] = "stored safely. future-you will probably smile at this one." saved["backupStamp"] = stamp return saved - - diff --git a/src/authoring_service/templates.py b/src/authoring_service/templates.py index 523f749..6811d5d 100755 --- a/src/authoring_service/templates.py +++ b/src/authoring_service/templates.py @@ -1609,7 +1609,7 @@ HIDDEN_APP_HTML = r"""
Story model: stories are first-class emotional routes. Memories can belong to multiple stories, but routes stay readable and authored in order.
+Canonical model: fragments are tiny reusable lines, memories are richer observatory moments, interactions are triggers, lore is world material, and stories are curated routes through them.
+Story model: stories are first-class emotional routes. Entries can belong to multiple stories, but routes stay readable and authored in order.
Legacy links: old continuations, echoes, and symbolic links are retained for migration only. New authoring happens through Story Builder.
Storage: the friendly graph lives in assets/content/hidden-details.json; the live site still receives generated constants in assets/scripts/hidden-details.js.