memory observatory redesign
All checks were successful
Build Authoring Service / build (push) Successful in 8s
All checks were successful
Build Authoring Service / build (push) Successful in 8s
This commit is contained in:
@@ -30,3 +30,4 @@ Code layout:
|
|||||||
- `authoring_server.py` is the compatibility executable used by systemd.
|
- `authoring_server.py` is the compatibility executable used by systemd.
|
||||||
- `src/authoring_service/` contains the implementation modules.
|
- `src/authoring_service/` contains the implementation modules.
|
||||||
- `src/tests/` contains the unit tests.
|
- `src/tests/` contains the unit tests.
|
||||||
|
- `docs/hidden-memory-architecture.md` documents the Fragment/Memory/Story model used by the Hidden Memory Observatory.
|
||||||
|
|||||||
82
docs/hidden-memory-architecture.md
Normal file
82
docs/hidden-memory-architecture.md
Normal file
@@ -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.
|
||||||
@@ -32,6 +32,64 @@ HIDDEN_CONTENT_TYPES = [
|
|||||||
"keyboard secret",
|
"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 = {
|
CHARACTER_REGISTRY = {
|
||||||
"young z": {
|
"young z": {
|
||||||
"id": "young z",
|
"id": "young z",
|
||||||
|
|||||||
@@ -12,12 +12,15 @@ from .config import HIDDEN_BACKUP_DIR, HIDDEN_CONTENT_JSON, HIDDEN_DETAILS_JS, R
|
|||||||
from .constants import (
|
from .constants import (
|
||||||
CHARACTER_REGISTRY,
|
CHARACTER_REGISTRY,
|
||||||
HIDDEN_CHARACTERS,
|
HIDDEN_CHARACTERS,
|
||||||
|
HIDDEN_CONTENT_CLASSES,
|
||||||
HIDDEN_CONTENT_TYPES,
|
HIDDEN_CONTENT_TYPES,
|
||||||
HIDDEN_DISCOVERY_STYLES,
|
HIDDEN_DISCOVERY_STYLES,
|
||||||
HIDDEN_LAYER_DEPTHS,
|
HIDDEN_LAYER_DEPTHS,
|
||||||
HIDDEN_RARITIES,
|
HIDDEN_RARITIES,
|
||||||
|
HIDDEN_SURFACES,
|
||||||
HIDDEN_STORY_MARKERS,
|
HIDDEN_STORY_MARKERS,
|
||||||
HIDDEN_TONES,
|
HIDDEN_TONES,
|
||||||
|
TYPE_ARCHITECTURE,
|
||||||
)
|
)
|
||||||
from .utils import normalise_tags, slugify
|
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))
|
title = str(extra.pop("title", "") or hidden_title(content_type, content, index))
|
||||||
characters = extra.pop("characters", None) or detect_hidden_characters(f"{title} {content}")
|
characters = extra.pop("characters", None) or detect_hidden_characters(f"{title} {content}")
|
||||||
tags = extra.pop("tags", None) or [slugify(character) for character in characters]
|
tags = extra.pop("tags", None) or [slugify(character) for character in characters]
|
||||||
|
architecture = TYPE_ARCHITECTURE.get(content_type, TYPE_ARCHITECTURE["quote"])
|
||||||
entry = {
|
entry = {
|
||||||
"id": hidden_entry_id(content_type, index, title),
|
"id": hidden_entry_id(content_type, index, title),
|
||||||
"type": content_type,
|
"type": content_type,
|
||||||
|
"contentClass": extra.pop("contentClass", architecture["contentClass"]),
|
||||||
"title": title,
|
"title": title,
|
||||||
"content": content,
|
"content": content,
|
||||||
"characters": characters,
|
"characters": characters,
|
||||||
@@ -182,6 +187,9 @@ def make_hidden_entry(content_type: str, content: str, index: int, **extra: Any)
|
|||||||
"thematicLinks": extra.pop("thematicLinks", []),
|
"thematicLinks": extra.pop("thematicLinks", []),
|
||||||
"symbolicLinks": extra.pop("symbolicLinks", []),
|
"symbolicLinks": extra.pop("symbolicLinks", []),
|
||||||
"triggerLinks": extra.pop("triggerLinks", []),
|
"triggerLinks": extra.pop("triggerLinks", []),
|
||||||
|
"surfaces": extra.pop("surfaces", architecture["surfaces"]),
|
||||||
|
"observatoryRole": extra.pop("observatoryRole", architecture["observatory"]),
|
||||||
|
"canonicalUse": extra.pop("canonicalUse", ""),
|
||||||
}
|
}
|
||||||
entry.update(extra)
|
entry.update(extra)
|
||||||
return entry
|
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))
|
content_type = str(normalize_character_text_refs(content_type))
|
||||||
if content_type not in HIDDEN_CONTENT_TYPES:
|
if content_type not in HIDDEN_CONTENT_TYPES:
|
||||||
raise ValueError(f"Unsupported hidden content type: {content_type}")
|
raise ValueError(f"Unsupported hidden content type: {content_type}")
|
||||||
|
architecture = TYPE_ARCHITECTURE.get(content_type, TYPE_ARCHITECTURE["quote"])
|
||||||
if not title:
|
if not title:
|
||||||
raise ValueError("Every hidden entry needs a title.")
|
raise ValueError("Every hidden entry needs a title.")
|
||||||
if not content and content_type not in {"search route"}:
|
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["id"] = slugify(str(entry.get("id") or title))
|
||||||
entry["title"] = title
|
entry["title"] = title
|
||||||
entry["type"] = content_type
|
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
|
entry["content"] = content
|
||||||
detected_characters = detect_hidden_characters(" ".join([
|
detected_characters = detect_hidden_characters(" ".join([
|
||||||
title,
|
title,
|
||||||
@@ -305,8 +318,15 @@ def normalize_hidden_entry(raw: dict[str, Any], existing: dict[str, Any] | None
|
|||||||
"thematicLinks",
|
"thematicLinks",
|
||||||
"symbolicLinks",
|
"symbolicLinks",
|
||||||
"triggerLinks",
|
"triggerLinks",
|
||||||
|
"surfaces",
|
||||||
]:
|
]:
|
||||||
entry[key] = [str(normalize_character_text_refs(item)).strip() for item in entry.get(key, []) if str(item).strip()]
|
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"]:
|
for key in ["dialogue", "keyboard"]:
|
||||||
if key in entry:
|
if key in entry:
|
||||||
entry[key] = normalize_character_text_refs(entry[key])
|
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)
|
normalized_stories = migrate_hidden_stories(normalized)
|
||||||
migrated_relationships = bool(normalized_stories)
|
migrated_relationships = bool(normalized_stories)
|
||||||
return {
|
return {
|
||||||
"schemaVersion": 2,
|
"schemaVersion": 3,
|
||||||
"source": HIDDEN_DETAILS_JS.relative_to(ROOT).as_posix(),
|
"source": HIDDEN_DETAILS_JS.relative_to(ROOT).as_posix(),
|
||||||
"contentPath": HIDDEN_CONTENT_JSON.relative_to(ROOT).as_posix(),
|
"contentPath": HIDDEN_CONTENT_JSON.relative_to(ROOT).as_posix(),
|
||||||
"migratedFromJs": migrated,
|
"migratedFromJs": migrated,
|
||||||
"migratedRelationshipsToStories": migrated_relationships,
|
"migratedRelationshipsToStories": migrated_relationships,
|
||||||
"types": HIDDEN_CONTENT_TYPES,
|
"types": HIDDEN_CONTENT_TYPES,
|
||||||
|
"contentClasses": HIDDEN_CONTENT_CLASSES,
|
||||||
|
"surfaces": HIDDEN_SURFACES,
|
||||||
|
"typeArchitecture": TYPE_ARCHITECTURE,
|
||||||
"characters": HIDDEN_CHARACTERS,
|
"characters": HIDDEN_CHARACTERS,
|
||||||
"characterRegistry": CHARACTER_REGISTRY,
|
"characterRegistry": CHARACTER_REGISTRY,
|
||||||
"validation": validate_hidden_integrity(normalized, normalized_stories),
|
"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]:
|
def hidden_architecture_recommendations() -> dict[str, Any]:
|
||||||
return {
|
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/.",
|
"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.",
|
"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.",
|
"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. Legacy relationship fields remain readable for migration, but the observatory should stay story-first.",
|
"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(
|
HIDDEN_CONTENT_JSON.write_text(
|
||||||
json.dumps(
|
json.dumps(
|
||||||
{
|
{
|
||||||
"schemaVersion": 2,
|
"schemaVersion": 3,
|
||||||
"generatedAt": datetime.now().isoformat(timespec="seconds"),
|
"generatedAt": datetime.now().isoformat(timespec="seconds"),
|
||||||
"entries": entries,
|
"entries": entries,
|
||||||
"stories": stories,
|
"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["message"] = "stored safely. future-you will probably smile at this one."
|
||||||
saved["backupStamp"] = stamp
|
saved["backupStamp"] = stamp
|
||||||
return saved
|
return saved
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1609,7 +1609,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
<select id="rarityFilter"></select>
|
<select id="rarityFilter"></select>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button id="newBtn" type="button">New memory</button>
|
<button id="newBtn" type="button">New fragment</button>
|
||||||
<button id="saveBtn" class="primary" type="button">Save constellation</button>
|
<button id="saveBtn" class="primary" type="button">Save constellation</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="actions" style="margin-top:8px">
|
<div class="actions" style="margin-top:8px">
|
||||||
@@ -1715,7 +1715,8 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
<h2>Memory editor</h2>
|
<h2>Memory editor</h2>
|
||||||
<div class="editor-grid">
|
<div class="editor-grid">
|
||||||
<label class="full">Name<input id="title" /></label>
|
<label class="full">Name<input id="title" /></label>
|
||||||
<label>Kind<select id="type"></select></label>
|
<label>Runtime kind<select id="type"></select></label>
|
||||||
|
<label>Canonical class<select id="contentClass"></select></label>
|
||||||
<label>Depth<select id="familyLayer"></select></label>
|
<label>Depth<select id="familyLayer"></select></label>
|
||||||
<label>Characters<input id="characters" placeholder="lima, aphy" /></label>
|
<label>Characters<input id="characters" placeholder="lima, aphy" /></label>
|
||||||
<div id="characterTags" class="full pills"></div>
|
<div id="characterTags" class="full pills"></div>
|
||||||
@@ -1727,7 +1728,10 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
<label>Resonance<input id="resonanceScore" type="number" min="1" max="10" /></label>
|
<label>Resonance<input id="resonanceScore" type="number" min="1" max="10" /></label>
|
||||||
<label>Where it appears<input id="pageLocation" placeholder="/play/lima-note.html" /></label>
|
<label>Where it appears<input id="pageLocation" placeholder="/play/lima-note.html" /></label>
|
||||||
<label class="full">Trigger path<input id="triggerConditions" placeholder="search: lima, hover, idle 90 seconds" /></label>
|
<label class="full">Trigger path<input id="triggerConditions" placeholder="search: lima, hover, idle 90 seconds" /></label>
|
||||||
<label class="full">Memory text<textarea id="content" spellcheck="true"></textarea></label>
|
<label class="full">Content text<textarea id="content" spellcheck="true"></textarea></label>
|
||||||
|
<label>Reusable surfaces<input id="surfaces" placeholder="tooltip, story, observatory" /></label>
|
||||||
|
<label>Observatory role<select id="observatoryRole"><option value="ambient">Ambient light</option><option value="node">Node</option><option value="event">Triggered event</option><option value="guide">Layer guide</option></select></label>
|
||||||
|
<label class="full">Canonical use<input id="canonicalUse" placeholder="short reusable whisper, observatory scene, search trigger..." /></label>
|
||||||
<label>Symbols<input id="symbols" placeholder="ring, tea, crayon sun" /></label>
|
<label>Symbols<input id="symbols" placeholder="ring, tea, crayon sun" /></label>
|
||||||
<label>Story hint<input id="narrativeArcs" placeholder="optional legacy arc note" /></label>
|
<label>Story hint<input id="narrativeArcs" placeholder="optional legacy arc note" /></label>
|
||||||
<label>Tags<input id="tags" placeholder="warm, october" /></label>
|
<label>Tags<input id="tags" placeholder="warm, october" /></label>
|
||||||
@@ -1757,7 +1761,8 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
<section class="panel">
|
<section class="panel">
|
||||||
<h2>Architecture</h2>
|
<h2>Architecture</h2>
|
||||||
<div class="subtle">
|
<div class="subtle">
|
||||||
<p><strong>Story model:</strong> stories are first-class emotional routes. Memories can belong to multiple stories, but routes stay readable and authored in order.</p>
|
<p><strong>Canonical model:</strong> fragments are tiny reusable lines, memories are richer observatory moments, interactions are triggers, lore is world material, and stories are curated routes through them.</p>
|
||||||
|
<p><strong>Story model:</strong> stories are first-class emotional routes. Entries can belong to multiple stories, but routes stay readable and authored in order.</p>
|
||||||
<p><strong>Legacy links:</strong> old continuations, echoes, and symbolic links are retained for migration only. New authoring happens through Story Builder.</p>
|
<p><strong>Legacy links:</strong> old continuations, echoes, and symbolic links are retained for migration only. New authoring happens through Story Builder.</p>
|
||||||
<p><strong>Storage:</strong> the friendly graph lives in <code>assets/content/hidden-details.json</code>; the live site still receives generated constants in <code>assets/scripts/hidden-details.js</code>.</p>
|
<p><strong>Storage:</strong> the friendly graph lives in <code>assets/content/hidden-details.json</code>; the live site still receives generated constants in <code>assets/scripts/hidden-details.js</code>.</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -1771,7 +1776,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
const WORLD = { width: 4600, height: 3300, cx: 2300, cy: 1650 };
|
const WORLD = { width: 4600, height: 3300, cx: 2300, cy: 1650 };
|
||||||
const SCREEN = { width: 1000, height: 760 };
|
const SCREEN = { width: 1000, height: 760 };
|
||||||
const state = { entries: [], stories: [], meta: {}, mode: "graph", selectedId: "", selectedStoryId: "", undo: [], redo: [], draggingId: "", positions: new Map(), clusterPositions: new Map(), zoom: 0, focusCluster: "", focusKind: "", focusIds: null, focusLabel: "", history: [], explorationUndo: [], explorationRedo: [], camera: { x: 0, y: 0, k: 0.22 }, pan: null, drag: null, renderTimer: 0 };
|
const state = { entries: [], stories: [], meta: {}, mode: "graph", selectedId: "", selectedStoryId: "", undo: [], redo: [], draggingId: "", positions: new Map(), clusterPositions: new Map(), zoom: 0, focusCluster: "", focusKind: "", focusIds: null, focusLabel: "", history: [], explorationUndo: [], explorationRedo: [], camera: { x: 0, y: 0, k: 0.22 }, pan: null, drag: null, renderTimer: 0 };
|
||||||
const fields = ["title","type","familyLayer","characters","tone","rarity","discoveryDifficulty","mysteryLevel","resonanceScore","pageLocation","triggerConditions","content","symbols","narrativeArcs","tags","emotionalRole","continuationLinks","echoes","thematicLinks","symbolicLinks","triggerLinks","parentLinks","childLinks","mirroredEntries","enabled","cssClassHooks","audioSettings","animationTrigger","notes"];
|
const fields = ["title","type","contentClass","familyLayer","characters","tone","rarity","discoveryDifficulty","mysteryLevel","resonanceScore","pageLocation","triggerConditions","content","surfaces","observatoryRole","canonicalUse","symbols","narrativeArcs","tags","emotionalRole","continuationLinks","echoes","thematicLinks","symbolicLinks","triggerLinks","parentLinks","childLinks","mirroredEntries","enabled","cssClassHooks","audioSettings","animationTrigger","notes"];
|
||||||
const storyFields = ["storyTitle","storyDescription","storyTone","storyDiscovery","storyCharacters","storySymbols","storyMarkers","storyLayers","storyUnlock","storyHidden"];
|
const storyFields = ["storyTitle","storyDescription","storyTone","storyDiscovery","storyCharacters","storySymbols","storyMarkers","storyLayers","storyUnlock","storyHidden"];
|
||||||
const $ = (id) => document.getElementById(id);
|
const $ = (id) => document.getElementById(id);
|
||||||
const toneColors = { warm: "#d3a64d", funny: "#7fb089", nostalgic: "#d06b78", wise: "#75a9bd", strange: "#a58ac9", soft: "#e6bd8c", hopeful: "#9fcf9f", protective: "#d98f71", melancholy: "#8fa7c6" };
|
const toneColors = { warm: "#d3a64d", funny: "#7fb089", nostalgic: "#d06b78", wise: "#75a9bd", strange: "#a58ac9", soft: "#e6bd8c", hopeful: "#9fcf9f", protective: "#d98f71", melancholy: "#8fa7c6" };
|
||||||
@@ -1838,11 +1843,14 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
entry.title,
|
entry.title,
|
||||||
entry.content,
|
entry.content,
|
||||||
entry.type,
|
entry.type,
|
||||||
|
entry.contentClass,
|
||||||
entry.triggerConditions,
|
entry.triggerConditions,
|
||||||
entry.pageLocation,
|
entry.pageLocation,
|
||||||
|
entry.canonicalUse,
|
||||||
entry.emotionalRole,
|
entry.emotionalRole,
|
||||||
entry.mysteryLevel,
|
entry.mysteryLevel,
|
||||||
...(entry.tags || []),
|
...(entry.tags || []),
|
||||||
|
...(entry.surfaces || []),
|
||||||
...(entry.symbols || []),
|
...(entry.symbols || []),
|
||||||
...(entry.narrativeArcs || []),
|
...(entry.narrativeArcs || []),
|
||||||
].join(" ").toLowerCase();
|
].join(" ").toLowerCase();
|
||||||
@@ -2065,8 +2073,9 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
return {
|
return {
|
||||||
...current,
|
...current,
|
||||||
id: current.id || slugify($("title").value),
|
id: current.id || slugify($("title").value),
|
||||||
title: $("title").value.trim() || "Untitled memory",
|
title: $("title").value.trim() || "Untitled fragment",
|
||||||
type: $("type").value,
|
type: $("type").value,
|
||||||
|
contentClass: $("contentClass").value,
|
||||||
familyLayer: $("familyLayer").value,
|
familyLayer: $("familyLayer").value,
|
||||||
characters: normalizeCharacters($("characters").value),
|
characters: normalizeCharacters($("characters").value),
|
||||||
emotionalTone: $("tone").value,
|
emotionalTone: $("tone").value,
|
||||||
@@ -2077,6 +2086,9 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
pageLocation: $("pageLocation").value,
|
pageLocation: $("pageLocation").value,
|
||||||
triggerConditions: $("triggerConditions").value,
|
triggerConditions: $("triggerConditions").value,
|
||||||
content: $("content").value,
|
content: $("content").value,
|
||||||
|
surfaces: splitList($("surfaces").value),
|
||||||
|
observatoryRole: $("observatoryRole").value,
|
||||||
|
canonicalUse: $("canonicalUse").value,
|
||||||
symbols: splitList($("symbols").value),
|
symbols: splitList($("symbols").value),
|
||||||
narrativeArcs: splitList($("narrativeArcs").value),
|
narrativeArcs: splitList($("narrativeArcs").value),
|
||||||
tags: splitList($("tags").value),
|
tags: splitList($("tags").value),
|
||||||
@@ -2096,6 +2108,12 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
notes: $("notes").value,
|
notes: $("notes").value,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
function applyTypeDefaults() {
|
||||||
|
const architecture = state.meta.typeArchitecture?.[$("type").value] || {};
|
||||||
|
if (architecture.contentClass) $("contentClass").value = architecture.contentClass;
|
||||||
|
if (architecture.surfaces) $("surfaces").value = architecture.surfaces.join(", ");
|
||||||
|
if (architecture.observatory) $("observatoryRole").value = architecture.observatory;
|
||||||
|
}
|
||||||
function applyFormToState() {
|
function applyFormToState() {
|
||||||
if (!state.selectedId) return;
|
if (!state.selectedId) return;
|
||||||
const index = state.entries.findIndex((entry) => entry.id === state.selectedId);
|
const index = state.entries.findIndex((entry) => entry.id === state.selectedId);
|
||||||
@@ -2158,9 +2176,10 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
function fillForm(entry) {
|
function fillForm(entry) {
|
||||||
if (!entry) return;
|
if (!entry) return;
|
||||||
$("drawerTitle").textContent = entry.title || "Untitled memory";
|
$("drawerTitle").textContent = entry.title || "Untitled memory";
|
||||||
$("drawerSub").textContent = `${layerName(depth(entry))} / ${entry.type || "memory"}`;
|
$("drawerSub").textContent = `${layerName(depth(entry))} / ${entry.contentClass || "fragment"} / ${entry.type || "entry"}`;
|
||||||
$("title").value = entry.title || "";
|
$("title").value = entry.title || "";
|
||||||
$("type").value = entry.type || "quote";
|
$("type").value = entry.type || "quote";
|
||||||
|
$("contentClass").value = entry.contentClass || state.meta.typeArchitecture?.[entry.type]?.contentClass || "fragment";
|
||||||
$("familyLayer").value = String(depth(entry));
|
$("familyLayer").value = String(depth(entry));
|
||||||
$("characters").value = (entry.characters || []).join(", ");
|
$("characters").value = (entry.characters || []).join(", ");
|
||||||
$("tone").value = entry.emotionalTone || "warm";
|
$("tone").value = entry.emotionalTone || "warm";
|
||||||
@@ -2171,6 +2190,9 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
$("pageLocation").value = entry.pageLocation || "";
|
$("pageLocation").value = entry.pageLocation || "";
|
||||||
$("triggerConditions").value = entry.triggerConditions || "";
|
$("triggerConditions").value = entry.triggerConditions || "";
|
||||||
$("content").value = entry.content || "";
|
$("content").value = entry.content || "";
|
||||||
|
$("surfaces").value = (entry.surfaces || []).join(", ");
|
||||||
|
$("observatoryRole").value = entry.observatoryRole || state.meta.typeArchitecture?.[entry.type]?.observatory || "ambient";
|
||||||
|
$("canonicalUse").value = entry.canonicalUse || "";
|
||||||
$("symbols").value = (entry.symbols || []).join(", ");
|
$("symbols").value = (entry.symbols || []).join(", ");
|
||||||
$("narrativeArcs").value = (entry.narrativeArcs || []).join(", ");
|
$("narrativeArcs").value = (entry.narrativeArcs || []).join(", ");
|
||||||
$("tags").value = (entry.tags || []).join(", ");
|
$("tags").value = (entry.tags || []).join(", ");
|
||||||
@@ -2206,7 +2228,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
function filteredEntries() {
|
function filteredEntries() {
|
||||||
const query = $("search").value.toLowerCase();
|
const query = $("search").value.toLowerCase();
|
||||||
return state.entries.filter((entry) => {
|
return state.entries.filter((entry) => {
|
||||||
const haystack = [entry.title, entry.content, entry.type, entry.triggerConditions, entry.pageLocation, entry.emotionalRole, entry.mysteryLevel, ...(entry.characters || []), ...(entry.tags || []), ...(entry.symbols || []), ...(entry.narrativeArcs || [])].join(" ").toLowerCase();
|
const haystack = [entry.title, entry.content, entry.type, entry.contentClass, entry.triggerConditions, entry.pageLocation, entry.canonicalUse, entry.emotionalRole, entry.mysteryLevel, ...(entry.surfaces || []), ...(entry.characters || []), ...(entry.tags || []), ...(entry.symbols || []), ...(entry.narrativeArcs || [])].join(" ").toLowerCase();
|
||||||
return (!query || haystack.includes(query))
|
return (!query || haystack.includes(query))
|
||||||
&& (!$("layerFilter").value || String(depth(entry)) === $("layerFilter").value)
|
&& (!$("layerFilter").value || String(depth(entry)) === $("layerFilter").value)
|
||||||
&& (!$("characterFilter").value || (entry.characters || []).includes($("characterFilter").value))
|
&& (!$("characterFilter").value || (entry.characters || []).includes($("characterFilter").value))
|
||||||
@@ -2619,7 +2641,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
const scale = labelScale();
|
const scale = labelScale();
|
||||||
const deep = state.camera.k > 1.25 || selected;
|
const deep = state.camera.k > 1.25 || selected;
|
||||||
const text = truncate(entry.title, deep ? 72 : state.camera.k > 0.85 ? 42 : 24);
|
const text = truncate(entry.title, deep ? 72 : state.camera.k > 0.85 ? 42 : 24);
|
||||||
const meta = `${entry.type || "memory"} / ${entry.emotionalTone || "warm"}`;
|
const meta = `${entry.contentClass || "fragment"} / ${entry.type || "entry"} / ${entry.emotionalTone || "warm"}`;
|
||||||
const width = Math.max(190, Math.min(420, text.length * 8 + 42));
|
const width = Math.max(190, Math.min(420, text.length * 8 + 42));
|
||||||
const height = deep ? 92 : 44;
|
const height = deep ? 92 : 44;
|
||||||
const labelPos = { x: pos.x + size + 18, y: pos.y - height * scale / 2 };
|
const labelPos = { x: pos.x + size + 18, y: pos.y - height * scale / 2 };
|
||||||
@@ -3135,7 +3157,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
function renderPreview(entry) {
|
function renderPreview(entry) {
|
||||||
const body = html(entry.content || "").replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
const body = html(entry.content || "").replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
||||||
const avatarPills = charactersForEntry(entry).map((character) => `<span class="avatar-pill pill" style="--avatar-glow:${html(avatarGlow(character))}">${avatarImg(character, "tiny", avatarExpressionForEntry(entry))}${html(characterLabel(character))}</span>`).join("");
|
const avatarPills = charactersForEntry(entry).map((character) => `<span class="avatar-pill pill" style="--avatar-glow:${html(avatarGlow(character))}">${avatarImg(character, "tiny", avatarExpressionForEntry(entry))}${html(characterLabel(character))}</span>`).join("");
|
||||||
$("preview").innerHTML = `${body || "<span class='subtle'>This memory is waiting for words.</span>"}<div class="pills" style="margin-top:10px">${avatarPills}<span class="pill">${html(layerName(depth(entry)))}</span><span class="pill">${html(entry.emotionalTone)}</span><span class="pill">${html(entry.rarity)}</span>${(entry.symbols || []).map((item) => `<span class="pill">${html(item)}</span>`).join("")}</div>`;
|
$("preview").innerHTML = `${body || "<span class='subtle'>This entry is waiting for words.</span>"}<div class="pills" style="margin-top:10px">${avatarPills}<span class="pill">${html(entry.contentClass || "fragment")}</span><span class="pill">${html(entry.observatoryRole || "ambient")}</span><span class="pill">${html(layerName(depth(entry)))}</span><span class="pill">${html(entry.emotionalTone)}</span><span class="pill">${html(entry.rarity)}</span>${(entry.surfaces || []).map((item) => `<span class="pill">${html(item)}</span>`).join("")}${(entry.symbols || []).map((item) => `<span class="pill">${html(item)}</span>`).join("")}</div>`;
|
||||||
}
|
}
|
||||||
function renderConnections(entry) {
|
function renderConnections(entry) {
|
||||||
const stories = storiesForEntry(entry.id);
|
const stories = storiesForEntry(entry.id);
|
||||||
@@ -3201,13 +3223,17 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
const now = new Date().toISOString().slice(0, 10);
|
const now = new Date().toISOString().slice(0, 10);
|
||||||
const entry = {
|
const entry = {
|
||||||
id: `memory-${Date.now()}`,
|
id: `memory-${Date.now()}`,
|
||||||
title: "Untitled memory",
|
title: "Untitled fragment",
|
||||||
type,
|
type,
|
||||||
|
contentClass: state.meta.typeArchitecture?.[type]?.contentClass || "fragment",
|
||||||
content: "",
|
content: "",
|
||||||
characters: [],
|
characters: [],
|
||||||
emotionalTone: "warm",
|
emotionalTone: "warm",
|
||||||
rarity: "common",
|
rarity: "common",
|
||||||
triggerConditions: "",
|
triggerConditions: "",
|
||||||
|
surfaces: state.meta.typeArchitecture?.[type]?.surfaces || ["quote"],
|
||||||
|
observatoryRole: state.meta.typeArchitecture?.[type]?.observatory || "ambient",
|
||||||
|
canonicalUse: "",
|
||||||
tags: [],
|
tags: [],
|
||||||
category: type,
|
category: type,
|
||||||
pageLocation: "",
|
pageLocation: "",
|
||||||
@@ -3340,6 +3366,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
function populateControls() {
|
function populateControls() {
|
||||||
const opt = (value, label = value) => `<option value="${html(value)}">${html(label || "All")}</option>`;
|
const opt = (value, label = value) => `<option value="${html(value)}">${html(label || "All")}</option>`;
|
||||||
$("type").innerHTML = state.meta.types.map((value) => opt(value)).join("");
|
$("type").innerHTML = state.meta.types.map((value) => opt(value)).join("");
|
||||||
|
$("contentClass").innerHTML = (state.meta.contentClasses || ["fragment","memory"]).map((value) => opt(value)).join("");
|
||||||
$("tone").innerHTML = state.meta.tones.map((value) => opt(value)).join("");
|
$("tone").innerHTML = state.meta.tones.map((value) => opt(value)).join("");
|
||||||
$("storyTone").innerHTML = state.meta.tones.map((value) => opt(value)).join("");
|
$("storyTone").innerHTML = state.meta.tones.map((value) => opt(value)).join("");
|
||||||
$("storyDiscovery").innerHTML = (state.meta.discoveryStyles || ["gradual"]).map((value) => opt(value)).join("");
|
$("storyDiscovery").innerHTML = (state.meta.discoveryStyles || ["gradual"]).map((value) => opt(value)).join("");
|
||||||
@@ -3491,6 +3518,10 @@ HIDDEN_APP_HTML = r"""<!doctype html>
|
|||||||
$(id).addEventListener("input", applyFormToState);
|
$(id).addEventListener("input", applyFormToState);
|
||||||
$(id).addEventListener("change", () => { snapshot(); applyFormToState(); });
|
$(id).addEventListener("change", () => { snapshot(); applyFormToState(); });
|
||||||
});
|
});
|
||||||
|
$("type").addEventListener("change", () => {
|
||||||
|
applyTypeDefaults();
|
||||||
|
applyFormToState();
|
||||||
|
});
|
||||||
storyFields.forEach((id) => {
|
storyFields.forEach((id) => {
|
||||||
$(id).addEventListener("input", applyStoryFormToState);
|
$(id).addEventListener("input", applyStoryFormToState);
|
||||||
$(id).addEventListener("change", () => { snapshot(); applyStoryFormToState(); });
|
$(id).addEventListener("change", () => { snapshot(); applyStoryFormToState(); });
|
||||||
|
|||||||
Reference in New Issue
Block a user