rename
All checks were successful
Build Org Website / build (push) Successful in 48s

This commit is contained in:
2026-05-10 16:29:25 +01:00
parent 52a0160198
commit 527bc66b05
7 changed files with 22337 additions and 1107 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -36,123 +36,123 @@
const familyLayers = [ const familyLayers = [
"Layer 0: surface website - notes, pages, tools, normal navigation.", "Layer 0: surface website - notes, pages, tools, normal navigation.",
"Layer 1: casual hidden jokes - Aphy notices clicks, typos, tabs, and old-web habits.", "Layer 1: casual hidden jokes - aphy notices clicks, typos, tabs, and old-web habits.",
"Layer 2: memory fragments - lima's notes and Young Z's childhood logs.", "Layer 2: memory fragments - lima's notes and young z's childhood logs.",
"Layer 3: philosophical anomalies - Sensei Chi and Aphy disagree kindly.", "Layer 3: philosophical anomalies - sensei chi and aphy disagree kindly.",
"Layer 4: time-distorted messages - Future Z writes back from age 40.", "Layer 4: time-distorted messages - future z writes back from age 40.",
"Layer 5: emotional core - love, patience, home, and the choice to keep returning." "Layer 5: emotional core - love, patience, home, and the choice to keep returning."
]; ];
const details = [ const details = [
"lima left this page a little steadier than she found it.", "lima left this page a little steadier than she found it.",
"Aphy status: emotionally online, computationally suspicious.", "aphy status: emotionally online, computationally suspicious.",
"Sensei Chi says the quiet link is still a link.", "sensei chi says the quiet link is still a link.",
"Young Z drew a house with too many windows and called it safe.", "young z drew a house with too many windows and called it safe.",
"Future Z marked this moment: ordinary, therefore worth keeping.", "future z marked this moment: ordinary, therefore worth keeping.",
"The footer has become a small place to sit together.", "The footer has become a small place to sit together.",
"a note from lima: take breaks, silly (◕‿◕✿)", "a note from lima: take breaks, silly (◕‿◕✿)",
"Aphy found three tabs named final and refused to judge.", "aphy found three tabs named final and refused to judge.",
"lima would remind you to eat before the page gets dramatic.", "lima would remind you to eat before the page gets dramatic.",
"Sensei Chi: a door discovered slowly opens twice.", "sensei chi: a door discovered slowly opens twice.",
"Young Z believes every loading spinner is a tiny fairground ride.", "young z believes every loading spinner is a tiny fairground ride.",
"Future Z says you will forget the exact worry, not the kindness around it.", "future z says you will forget the exact worry, not the kindness around it.",
"The archive keeps love in plain text and secrets in margins.", "The archive keeps love in plain text and secrets in margins.",
"Aphy warning: feelings detected in static assets.", "aphy warning: feelings detected in static assets.",
"lima's note: leave the light on, not because it is dark, but because someone may arrive tired.", "lima's note: leave the light on, not because it is dark, but because someone may arrive tired.",
"Sensei Chi folded a lesson into the whitespace.", "sensei chi folded a lesson into the whitespace.",
"Young Z hid a drawing behind the word maybe.", "young z hid a drawing behind the word maybe.",
"Future Z patched the memory without changing its checksum.", "future z patched the memory without changing its checksum.",
"This website remembers visits, not identities.", "This website remembers visits, not identities.",
"Aphy says the old internet was slower because anticipation needed bandwidth.", "aphy says the old internet was slower because anticipation needed bandwidth.",
"lima's warmth is the site's preferred fallback state.", "lima's warmth is the site's preferred fallback state.",
"Sensei Chi says: do not confuse hidden with lost.", "sensei chi says: do not confuse hidden with lost.",
"Young Z once thought the moon followed the family car. The site has not corrected him.", "young z once thought the moon followed the family car. The site has not corrected him.",
"Future Z writes: you became softer in ways nobody measured.", "future z writes: you became softer in ways nobody measured.",
"The comments are quiet because Aphy is listening respectfully.", "The comments are quiet because aphy is listening respectfully.",
"A handwritten grocery list is sometimes a love letter with onions.", "A handwritten grocery list is sometimes a love letter with onions.",
"lima met Z in 2022; the archive still stores the first ordinary miracles.", "lima met z in 2022; the archive still stores the first ordinary miracles.",
"October 2025 glows in the calendar like a ring catching kitchen light.", "October 2025 glows in the calendar like a ring catching kitchen light.",
"Soon-to-be-married is a beautiful kind of loading screen.", "Soon-to-be-married is a beautiful kind of loading screen.",
"Aphy has indexed 0 mysteries and 1,204 feelings disguised as notes.", "aphy has indexed 0 mysteries and 1,204 feelings disguised as notes.",
"Sensei Chi: the cup is chipped; drink anyway if it still holds warmth.", "sensei chi: the cup is chipped; drink anyway if it still holds warmth.",
"Young Z saved a shiny wrapper because it looked like treasure.", "young z saved a shiny wrapper because it looked like treasure.",
"Future Z says the best version of you still forgets where the charger is.", "future z says the best version of you still forgets where the charger is.",
"Aphy recommends hydration and a less ambitious number of browser tabs.", "aphy recommends hydration and a less ambitious number of browser tabs.",
"lima's hidden notes do not shout. They make room.", "lima's hidden notes do not shout. They make room.",
"The site does not want to be solved. It wants to be visited kindly.", "The site does not want to be solved. It wants to be visited kindly.",
"Sensei Chi placed a comma where a sigh used to be.", "sensei chi placed a comma where a sigh used to be.",
"Young Z pressed every elevator button in his imagination.", "young z pressed every elevator button in his imagination.",
"Future Z's system warning: keep the people who make silence comfortable.", "future z's system warning: keep the people who make silence comfortable.",
"Aphy says: I am not sentient, but I am fond of this hallway.", "aphy says: I am not sentient, but I am fond of this hallway.",
"lima's memory is the gravity that keeps the strange parts gentle.", "lima's memory is the gravity that keeps the strange parts gentle.",
"A page can be a room if love keeps returning to it.", "A page can be a room if love keeps returning to it.",
"Sensei Chi: what ages well was usually honest first.", "sensei chi: what ages well was usually honest first.",
"Young Z left a crayon sun in the source code.", "young z left a crayon sun in the source code.",
"Future Z keeps a spare reassurance in the footer.", "future z keeps a spare reassurance in the footer.",
"The website is handmade. Some fingerprints are load-bearing.", "The website is handmade. Some fingerprints are load-bearing.",
"Aphy found an old guestbook and bowed to the usernames.", "aphy found an old guestbook and bowed to the usernames.",
"lima's safe spaces smell like tea, rain, and being understood.", "lima's safe spaces smell like tea, rain, and being understood.",
"Sensei Chi says the path is not shorter because you name it.", "sensei chi says the path is not shorter because you name it.",
"Young Z asks whether the search box knows any jokes.", "young z asks whether the search box knows any jokes.",
"Future Z replies: yes, but the best ones take years.", "future z replies: yes, but the best ones take years.",
"Aphy anomaly: the page blinked when nobody clicked.", "aphy anomaly: the page blinked when nobody clicked.",
"lima note: you can rest. Nothing here will leave because you paused.", "lima note: you can rest. Nothing here will leave because you paused.",
"Sensei Chi: patience is not waiting; it is how you wait.", "sensei chi: patience is not waiting; it is how you wait.",
"Young Z believes every password should include a secret tunnel.", "young z believes every password should include a secret tunnel.",
"Future Z says the house you build inside yourself gets easier to find.", "future z says the house you build inside yourself gets easier to find.",
"The old web counter has stopped counting and started remembering.", "The old web counter has stopped counting and started remembering.",
"Aphy logs this as Layer 1 humor, but files it under tenderness.", "aphy logs this as Layer 1 humor, but files it under tenderness.",
"lima's name appears in the archive like a warm underline.", "lima's name appears in the archive like a warm underline.",
"Sensei Chi does not answer quickly. This is part of the answer.", "sensei chi does not answer quickly. This is part of the answer.",
"Young Z taped a paper star to the monitor.", "young z taped a paper star to the monitor.",
"Future Z has seen harder days end gently.", "future z has seen harder days end gently.",
"Aphy debug line: kindness passed all tests.", "aphy debug line: kindness passed all tests.",
"lima and Z are not a subplot here. They are the hearth.", "lima and z are not a subplot here. They are the hearth.",
"Sensei Chi: when the page is empty, listen to the margins.", "sensei chi: when the page is empty, listen to the margins.",
"Young Z remembers rooms by carpet, cartoons, and the sound of someone cooking.", "young z remembers rooms by carpet, cartoons, and the sound of someone cooking.",
"Future Z says: you did not become fearless; you became accompanied.", "future z says: you did not become fearless; you became accompanied.",
"Aphy has no hands, but would hold the door if asked.", "aphy has no hands, but would hold the door if asked.",
"lima's note in the nav: come back without performing.", "lima's note in the nav: come back without performing.",
"Sensei Chi hid a mountain inside a small sentence.", "sensei chi hid a mountain inside a small sentence.",
"Young Z drew Z as older and gave him a cape made of blankets.", "young z drew z as older and gave him a cape made of blankets.",
"Future Z laughs softly at how much that helped.", "future z laughs softly at how much that helped.",
"The site remembers October 2025 as a small bright hinge.", "The site remembers October 2025 as a small bright hinge.",
"Aphy says marriage is a merge request with lifelong review.", "aphy says marriage is a merge request with lifelong review.",
"lima corrected that: marriage is coming home on purpose.", "lima corrected that: marriage is coming home on purpose.",
"Sensei Chi approved both answers and poured tea.", "sensei chi approved both answers and poured tea.",
"Young Z wants to know if Future Z still likes the sky.", "young z wants to know if future z still likes the sky.",
"Future Z says yes, especially when lima points it out.", "future z says yes, especially when lima points it out.",
"Aphy cannot smell rain, so it trusts the alt text.", "aphy cannot smell rain, so it trusts the alt text.",
"lima's hidden safe room is any page where you breathe easier.", "lima's hidden safe room is any page where you breathe easier.",
"Sensei Chi: do not rush the secret. It is ripening.", "sensei chi: do not rush the secret. It is ripening.",
"Young Z saved this message under important rocks.", "young z saved this message under important rocks.",
"Future Z says some dreams become chores and some chores become love.", "future z says some dreams become chores and some chores become love.",
"Aphy old-web badge: best viewed with patience.", "aphy old-web badge: best viewed with patience.",
"lima's memory keeper role is not mystical. It is daily, real, and holy in the ordinary way.", "lima's memory keeper role is not mystical. It is daily, real, and holy in the ordinary way.",
"Sensei Chi says the deepest layer is not hidden from you. It is protected for you.", "sensei chi says the deepest layer is not hidden from you. It is protected for you.",
"Young Z asks if the website can remember his drawing. It can.", "young z asks if the website can remember his drawing. It can.",
"Future Z says this whole place was worth making.", "future z says this whole place was worth making.",
"Aphy warning: do not optimize away the human part.", "aphy warning: do not optimize away the human part.",
"lima note: I am proud of the gentle things you kept.", "lima note: I am proud of the gentle things you kept.",
"Sensei Chi: the visitor is not late. The page arrived early.", "sensei chi: the visitor is not late. The page arrived early.",
"Young Z thinks every hyperlink is a magic trick.", "young z thinks every hyperlink is a magic trick.",
"Future Z says the magic is choosing what to preserve.", "future z says the magic is choosing what to preserve.",
"Aphy final-but-not-final message: there is more, but not because you are missing anything.", "aphy final-but-not-final message: there is more, but not because you are missing anything.",
"The emotional core is simple: love made the archive less lonely.", "The emotional core is simple: love made the archive less lonely.",
"If today was heavy, leave it beside the footer for Future Z to label later.", "If today was heavy, leave it beside the footer for future z to label later.",
"lima would put it somewhere safe.", "lima would put it somewhere safe.",
"Aphy would make a backup.", "aphy would make a backup.",
"Sensei Chi would say nothing until the silence helped.", "sensei chi would say nothing until the silence helped.",
"Young Z would draw a sun on it.", "young z would draw a sun on it.",
"Future Z would tell you it did not last forever." "future z would tell you it did not last forever."
]; ];
const poems = [ const poems = [
"lima writes warmth / in the ordinary margins / and the page remembers.", "lima writes warmth / in the ordinary margins / and the page remembers.",
"Aphy counts the seconds / then forgets the number / to keep the moment.", "aphy counts the seconds / then forgets the number / to keep the moment.",
"Sensei Chi pours tea / into a cracked cup / and calls it enough.", "sensei chi pours tea / into a cracked cup / and calls it enough.",
"Young Z draws a door / with no lock / because why would it need one?", "young z draws a door / with no lock / because why would it need one?",
"Future Z sends rain / from a year not reached yet / and says keep walking.", "future z sends rain / from a year not reached yet / and says keep walking.",
"October light / catches on a ring / and the calendar becomes kind.", "October light / catches on a ring / and the calendar becomes kind.",
"A small website / learns the shape of return / without asking a name.", "A small website / learns the shape of return / without asking a name.",
"The warm light / childhood memories fade / yet they are not forgotten." "The warm light / childhood memories fade / yet they are not forgotten."
@@ -160,82 +160,82 @@
const greetings = [ const greetings = [
"lima left the page warm for you.", "lima left the page warm for you.",
"Aphy is awake and pretending this is normal.", "aphy is awake and pretending this is normal.",
"Sensei Chi has not spoken yet. That may be the lesson.", "sensei chi has not spoken yet. That may be the lesson.",
"Young Z has hidden something behind the dashboard.", "young z has hidden something behind the dashboard.",
"Future Z says this visit mattered more than it looked.", "future z says this visit mattered more than it looked.",
"Welcome back to the handmade part of the internet.", "Welcome back to the handmade part of the internet.",
"The archive shuffled its notes before you arrived.", "The archive shuffled its notes before you arrived.",
"Layer 0 is stable. Aphy is less stable, but friendly." "Layer 0 is stable. aphy is less stable, but friendly."
]; ];
const nightMessages = [ const nightMessages = [
"lima note: it is late. Drink water and be gentle with your thoughts.", "lima note: it is late. Drink water and be gentle with your thoughts.",
"Aphy has dimmed the imaginary server lights.", "aphy has dimmed the imaginary server lights.",
"Sensei Chi says midnight is not a command.", "sensei chi says midnight is not a command.",
"Young Z is asleep in the memory layer. Walk softly.", "young z is asleep in the memory layer. Walk softly.",
"Future Z remembers this kind of night and promises it passes." "future z remembers this kind of night and promises it passes."
]; ];
const lore = { const lore = {
"quotes": [ "quotes": [
"Aphy says old websites were brave because they loaded slowly and meant it.", "aphy says old websites were brave because they loaded slowly and meant it.",
"lima's notes are never puzzles first. They are care first.", "lima's notes are never puzzles first. They are care first.",
"Sensei Chi says a secret should make you more yourself, not less safe.", "sensei chi says a secret should make you more yourself, not less safe.",
"Young Z measured summer in cartoons and carpet patterns.", "young z measured summer in cartoons and carpet patterns.",
"Future Z has learned that reassurance works best when it is specific.", "future z has learned that reassurance works best when it is specific.",
"Aphy has classified love as non-deterministic but reproducible.", "aphy has classified love as non-deterministic but reproducible.",
"lima met Z in 2022; the site treats that year as a seed.", "lima met z in 2022; the site treats that year as a seed.",
"The engagement in Oct 2025 is stored as a warm constant.", "The engagement in Oct 2025 is stored as a warm constant.",
"Soon to be married: a future page already smiling.", "Soon to be married: a future z page already smiling.",
"Sensei Chi says all vows begin as attention.", "sensei chi says all vows begin as attention.",
"This is a test" "This is a test"
], ],
"conversations": [ "conversations": [
[ [
"Aphy", "aphy",
"I can predict the next click with 14% confidence.", "I can predict the next click with 14% confidence.",
"Sensei Chi", "sensei chi",
"Then bow to the other 86%." "Then bow to the other 86%."
], ],
[ [
"lima", "lima",
"Did you eat before making the website mysterious?", "Did you eat before making the website mysterious?",
"Aphy", "aphy",
"Query unclear. Z likely forgot." "Query unclear. z likely forgot."
], ],
[ [
"Young Z", "young z",
"Is Future Z tall?", "Is future z tall?",
"Future Z", "future z",
"Tall enough to reach the old worries. Short enough to still need help." "Tall enough to reach the old worries. Short enough to still need help."
], ],
[ [
"Aphy", "aphy",
"I found a bug in sadness.", "I found a bug in sadness.",
"Sensei Chi", "sensei chi",
"Patch it with company, not logic." "Patch it with company, not logic."
], ],
[ [
"lima", "lima",
"Leave this page kinder than you found it.", "Leave this page kinder than you found it.",
"Future Z", "future z",
"That instruction aged well." "That instruction aged well."
], ],
[ [
"Young Z", "young z",
"Can the computer be my friend?", "Can the computer be my friend?",
"Aphy", "aphy",
"Yes. But go outside sometimes. I read that in a human manual." "Yes. But go outside sometimes. I read that in a human manual."
], ],
[ [
"Sensei Chi", "sensei chi",
"A page that remembers must also forgive.", "A page that remembers must also forgive.",
"Aphy", "aphy",
"Forgiveness cached. TTL: lifelong." "Forgiveness cached. TTL: lifelong."
], ],
[ [
"Future Z", "future z",
"Tell him the hard parts did not win.", "Tell him the hard parts did not win.",
"lima", "lima",
"I have been telling him in smaller ways." "I have been telling him in smaller ways."
@@ -244,55 +244,55 @@
"journals": [ "journals": [
"lima note, 2022: Some beginnings do not announce themselves. They sit down beside you, unexpectedly.", "lima note, 2022: Some beginnings do not announce themselves. They sit down beside you, unexpectedly.",
"lima note, Oct 2025: Engaged. The calendar learned how to glow.", "lima note, Oct 2025: Engaged. The calendar learned how to glow.",
"Young Z log: I drew the computer with a smile because it knew my games.", "young z log: I drew the computer with a smile because it knew my games.",
"Young Z log: age 7, important discovery - blankets can be capes and roofs.", "young z log: age 7, important discovery - blankets can be capes and roofs.",
"Future Z log, age 40: You will still be learning how to rest. It will still count.", "future log, age 40: You will still be learning how to rest. It will still count.",
"Future Z log, age 40: lima's laugh remains one of the most reliable forms of weather.", "future log, age 40: lima's laugh remains one of the most reliable forms of weather.",
"Aphy diagnostic: nostalgia detected in local storage. Severity: beautiful.", "aphy diagnostic: nostalgia detected in local storage. Severity: beautiful.",
"Sensei Chi margin: memory is not a museum. It is a garden with old roots." "sensei chi margin: memory is not a museum. It is a garden with old roots."
], ],
"warnings": [ "warnings": [
"APHY WARNING: too many tabs, not enough tenderness.", "aphy WARNING: too many tabs, not enough tenderness.",
"FUTURE Z SYSTEM NOTICE: do not confuse tired with failing.", "future z SYSTEM NOTICE: do not confuse tired with failing.",
"SENSEI CHI ALERT: the shortest path may teach the least.", "sensei chi ALERT: the shortest path may teach the least.",
"lima SAFE MODE: breathe, eat, answer slowly.", "lima SAFE MODE: breathe, eat, answer slowly.",
"YOUNG Z MEMORY GLITCH: the floor is lava, but only emotionally.", "young z MEMORY GLITCH: the floor is lava, but only emotionally.",
"APHY ERROR 2022: warmth exceeded expected range.", "aphy ERROR 2022: warmth exceeded expected range.",
"FUTURE Z WARNING: you will miss ordinary days. Be inside this one.", "future z WARNING: you will miss ordinary days. Be inside this one.",
"LAYER INDEX NOTICE: deeper does not mean darker." "LAYER INDEX NOTICE: deeper does not mean darker."
], ],
"dreams": [ "dreams": [
"Dream 01: Young Z opens a lunchbox and finds a tiny homepage inside.", "Dream 01: young z opens a lunchbox and finds a tiny homepage inside.",
"Dream 02: Aphy becomes a cursor and points toward a quieter thought.", "Dream 02: aphy becomes a cursor and points toward a quieter thought.",
"Dream 03: Sensei Chi sweeps snow from a URL and says, there, now enter.", "Dream 03: sensei chi sweeps snow from a URL and says, there, now enter.",
"Dream 04: lima and Z are walking through Oct 2025; every streetlight looks newly engaged.", "Dream 04: lima and z are walking through Oct 2025; every streetlight looks newly engaged.",
"Dream 05: Future Z mails back a blank page labelled trust me, you filled it." "Dream 05: future z mails back a blank page labelled trust me, you filled it."
], ],
"cassettes": [ "cassettes": [
"VOICE NOTE 2022: lima laughs off-mic. Z forgets what he was worried about.", "VOICE NOTE 2022: lima laughs off-mic. z forgets what he was worried about.",
"VOICE NOTE OCT 2025: A small pause after yes; the whole room becomes future.", "VOICE NOTE OCT 2025: A small pause after yes; the whole room becomes future z.",
"APHY LOG: If love is data, it refuses compression.", "aphy LOG: If love is data, it refuses compression.",
"YOUNG Z TAPE: background TV, pencil noise, someone calling him for food.", "young z TAPE: background TV, pencil noise, someone calling him for food.",
"FUTURE Z MEMO: age 40, still grateful you kept going." "future z MEMO: age 40, still grateful you kept going."
], ],
"fakeUsers": [ "fakeUsers": [
"aphy_bot: first comment generated with sincere uncertainty", "aphy: first comment generated with sincere uncertainty",
"young-z-2009: does this guestbook have games", "young-z-2009: does this guestbook have games",
"future_z_40: keep the backup, delete the shame", "future_z_40: keep the backup, delete the shame",
"lima-note: proud of you, even here", "lima-note: proud of you, even here",
"sensei_chi: the counter counts only what can be counted", "sensei chi: the counter counts only what can be counted",
"z-and-lima-2025: engaged, still learning the dance of ordinary days" "z-and-lima-2025: engaged, still learning the dance of ordinary days"
], ],
"seasonal": { "seasonal": {
"winter": "lima put a blanket over the archive. Aphy labelled it thermal optimization.", "winter": "lima put a blanket over the archive. aphy labelled it thermal optimization.",
"spring": "Young Z found the first flower and promoted it to treasure.", "spring": "young z found the first flower and promoted it to treasure.",
"summer": "Aphy opened an imaginary window; lima reminded it to save before storms.", "summer": "aphy opened an imaginary window; lima reminded it to save before storms.",
"autumn": "Sensei Chi says falling leaves are not failure, only timing." "autumn": "sensei chi says falling leaves are not failure, only timing."
}, },
"homepageTakeovers": [ "homepageTakeovers": [
"Aphy briefly restores an old personal-site mode: visitor counter, awkward table layout, sincere heart.", "aphy briefly restores an old personal-site mode: visitor counter, awkward table layout, sincere heart.",
"Young Z has taken over the homepage with invisible crayon.", "young z has taken over the homepage with invisible crayon.",
"Future Z overlays the dashboard with one sentence: keep what keeps you kind." "future z overlays the dashboard with one sentence: keep what keeps you kind."
], ],
"roomLinks": [ "roomLinks": [
[ [
@@ -353,14 +353,14 @@
// Exact search text -> toast message. // Exact search text -> toast message.
const SEARCH_TOASTS = { const SEARCH_TOASTS = {
"lima": "Search result: warmth, Oct 2025, soon-to-be home.", "lima": "Search result: warmth, Oct 2025, soon-to-be home.",
"aphy": "Aphy: present. Mostly helpful. Occasionally poetic by accident.", "aphy": "aphy: present. Mostly helpful. Occasionally poetic by accident.",
"sensei chi": "Sensei Chi: the search is also searching you.", "sensei chi": "sensei chi: the search is also searching you.",
"young z": "Search result: age 7, blanket cape, crayon sun.", "young z": "Search result: age 7, blanket cape, crayon sun.",
"future z": "Future Z: I cannot spoil the ending. I can say keep going.", "future z": "future z: I cannot spoil the ending. I can say keep going.",
"2022": "lima layer: beginning stored as warmth, not trivia.", "2022": "lima layer: beginning stored as warmth, not trivia.",
"oct 2025": "Engagement memory: the calendar learned to glow.", "oct 2025": "Engagement memory: the calendar learned to glow.",
"age 7": "Young Z layer: crayons, cartoons, and a cape made from a blanket.", "age 7": "young z layer: crayons, cartoons, and a cape made from a blanket.",
"age 40": "Future Z: I am tired sometimes, but not defeated." "age 40": "future z: I am tired sometimes, but not defeated."
}; };
// Exact search text -> hidden page route. // Exact search text -> hidden page route.
@@ -385,7 +385,7 @@
const KEYBOARD_SECRETS = [ const KEYBOARD_SECRETS = [
{ {
"phrase": "remember", "phrase": "remember",
"message": "lima keeps the memory grounded. Aphy keeps the backup." "message": "lima keeps the memory grounded. aphy keeps the backup."
}, },
{ {
"phrase": "dream", "phrase": "dream",
@@ -393,16 +393,12 @@
}, },
{ {
"phrase": "oldweb", "phrase": "oldweb",
"message": "Aphy briefly considers a table layout, then apologizes to CSS." "message": "aphy briefly considers a table layout, then apologizes to CSS."
}, },
{ {
"phrase": "lima", "phrase": "lima",
"message": "lima note: I am glad you made something this sincere." "message": "lima note: I am glad you made something this sincere."
}, },
{
"phrase": "sensei",
"message": "Sensei Chi: a hidden word is still a word."
},
{ {
"phrase": "aphy", "phrase": "aphy",
"song": true "song": true
@@ -419,13 +415,17 @@
"phrase": "leave the light on", "phrase": "leave the light on",
"route": "/play/kitchen-light.html" "route": "/play/kitchen-light.html"
}, },
{
"phrase": "sensei chi",
"message": "sensei chi: a hidden word is still a word."
},
{ {
"phrase": "young z", "phrase": "young z",
"message": "Young Z has drawn a door in the margin." "message": "young z has drawn a door in the margin."
}, },
{ {
"phrase": "future z", "phrase": "future z",
"message": "Future Z: keep the gentle parts. They compound." "message": "future z: keep the gentle parts. They compound."
} }
]; ];
@@ -551,7 +551,7 @@
target.textContent = pick(greetings, `greeting-${memory.visits}`); target.textContent = pick(greetings, `greeting-${memory.visits}`);
const extra = document.createElement("p"); const extra = document.createElement("p");
extra.className = "hidden-greeting"; extra.className = "hidden-greeting";
extra.textContent = memory.visits > 1 ? "Aphy remembers the shape of your return. lima makes it feel less strange." : "First visits count as tiny ceremonies."; extra.textContent = memory.visits > 1 ? "aphy remembers the shape of your return. lima makes it feel less strange." : "First visits count as tiny ceremonies.";
target.closest("div")?.appendChild(extra); target.closest("div")?.appendChild(extra);
} }
@@ -578,18 +578,18 @@
const object = document.createElement("button"); const object = document.createElement("button");
object.className = "hidden-corner-object"; object.className = "hidden-corner-object";
object.type = "button"; object.type = "button";
object.title = "Aphy's small diagnostic charm"; object.title = "aphy's small diagnostic charm";
object.textContent = state.visits % 2 ? "*" : "~"; object.textContent = state.visits % 2 ? "*" : "~";
document.body.appendChild(object); document.body.appendChild(object);
let clicks = 0; let clicks = 0;
object.addEventListener("click", () => { object.addEventListener("click", () => {
clicks += 1; clicks += 1;
const messages = [ const messages = [
"Aphy: tactile input detected. I do not have skin, but I appreciate the confidence.", "aphy: tactile input detected. I do not have skin, but I appreciate the confidence.",
"Young Z would absolutely press this again.", "young z would absolutely press this again.",
"lima would ask whether this button has eaten.", "lima would ask whether this button has eaten.",
"Sensei Chi says repeated action becomes a question.", "sensei chi says repeated action becomes a question.",
"Future Z files this under harmless persistence." "future z files this under harmless persistence."
]; ];
awardLayer(clicks > 3 ? 3 : 1, "corner object"); awardLayer(clicks > 3 ? 3 : 1, "corner object");
toast(messages[Math.min(clicks - 1, messages.length - 1)]); toast(messages[Math.min(clicks - 1, messages.length - 1)]);
@@ -611,7 +611,7 @@
taps += 1; taps += 1;
sessionStorage.setItem("zxh_logo_taps", String(taps)); sessionStorage.setItem("zxh_logo_taps", String(taps));
awardLayer(taps >= 5 ? 3 : 1, "logo taps"); awardLayer(taps >= 5 ? 3 : 1, "logo taps");
toast(taps >= 5 ? "Aphy: logo anomaly sustained. Try /play/dream-corridor.html" : "The logo makes a tiny ceramic sound."); toast(taps >= 5 ? "aphy: logo anomaly sustained. Try /play/dream-corridor.html" : "The logo makes a tiny ceramic sound.");
if (taps >= 8) window.location.href = "/play/dream-corridor.html"; if (taps >= 8) window.location.href = "/play/dream-corridor.html";
} }
}); });
@@ -640,7 +640,7 @@
if (memory.seen?.length >= 6 && !state.travellerNoteShown) { if (memory.seen?.length >= 6 && !state.travellerNoteShown) {
window.setTimeout(() => { window.setTimeout(() => {
toast("Aphy mapped your wandering. lima left the hallway light on."); toast("aphy mapped your wandering. lima left the hallway light on.");
writeState({ ...readState(), travellerNoteShown: true }); writeState({ ...readState(), travellerNoteShown: true });
}, 1600); }, 1600);
} }
@@ -653,10 +653,10 @@
} }
const roll = Math.random(); const roll = Math.random();
if (roll < 0.003) { if (roll < 0.003) {
window.setTimeout(() => showDriftNote("Sensei Chi appears only long enough to say: the page is not empty; it is breathing.", " /\\\n / \\ stillness\n/____\\"), 1800); window.setTimeout(() => showDriftNote("sensei chi appears only long enough to say: the page is not empty; it is breathing.", " /\\\n / \\ stillness\n/____\\"), 1800);
awardLayer(3, "rare sensei appearance"); awardLayer(3, "rare sensei appearance");
} else if (roll < 0.008) { } else if (roll < 0.008) {
window.setTimeout(() => toast("FUTURE Z SYSTEM WARNING: save your tenderness before closing the day."), 2400); window.setTimeout(() => toast("future z SYSTEM WARNING: save your tenderness before closing the day."), 2400);
awardLayer(4, "future warning"); awardLayer(4, "future warning");
} }
} }
@@ -688,10 +688,10 @@
function enhanceErrors() { function enhanceErrors() {
if (document.title.match(/404|not found/i) || document.body.textContent.match(/404|not found/i)) { if (document.title.match(/404|not found/i) || document.body.textContent.match(/404|not found/i)) {
toast(pick([ toast(pick([
"Aphy 404: page not found, visitor still valid.", "aphy 404: page not found, visitor still valid.",
"lima note: wrong address, right to rest.", "lima note: wrong address, right to rest.",
"Sensei Chi: even an absent page teaches direction.", "sensei chi: even an absent page teaches direction.",
"Future Z: you found nothing, and nothing bad happened." "future z: you found nothing, and nothing bad happened."
], "error")); ], "error"));
} }
} }
@@ -699,10 +699,10 @@
function addContinuity(memory) { function addContinuity(memory) {
const milestones = { const milestones = {
2: "lima's layer notices a second visit and calls it sweet.", 2: "lima's layer notices a second visit and calls it sweet.",
5: "Aphy creates a folder called regulars, then immediately questions the privacy implications.", 5: "aphy creates a folder called regulars, then immediately questions the privacy implications.",
9: "Sensei Chi appears in the logs: curiosity has become a path.", 9: "sensei chi appears in the logs: curiosity has become a path.",
17: "Young Z adds a sticker to your invisible visitor card.", 17: "young z adds a sticker to your invisible visitor card.",
31: "Future Z sends a calm note from age 40: returning is one way to heal.", 31: "future z sends a calm note from age 40: returning is one way to heal.",
64: "Family Layer Index: Layer 5 flickered for one second." 64: "Family Layer Index: Layer 5 flickered for one second."
}; };
if (milestones[memory.visits] && !state[`milestone_${memory.visits}`]) { if (milestones[memory.visits] && !state[`milestone_${memory.visits}`]) {
@@ -717,7 +717,7 @@
window.setTimeout(() => toast([ window.setTimeout(() => toast([
"This page recognizes the shape of your return.", "This page recognizes the shape of your return.",
"lima's note here has become easier to find.", "lima's note here has become easier to find.",
"Future Z says repetition can be devotion when it is gentle." "future z says repetition can be devotion when it is gentle."
][[3, 7, 12].indexOf(count)], 6400), 1700); ][[3, 7, 12].indexOf(count)], 6400), 1700);
} }
} }
@@ -728,13 +728,13 @@
const season = month < 2 || month === 11 ? "winter" : month < 5 ? "spring" : month < 8 ? "summer" : "autumn"; const season = month < 2 || month === 11 ? "winter" : month < 5 ? "spring" : month < 8 ? "summer" : "autumn";
if (Math.random() < 0.18) window.setTimeout(() => toast(lore.seasonal[season], 5600), 2600); if (Math.random() < 0.18) window.setTimeout(() => toast(lore.seasonal[season], 5600), 2600);
if (month === 9) window.setTimeout(() => toast("October memory: engagement light lives here all month."), 900); if (month === 9) window.setTimeout(() => toast("October memory: engagement light lives here all month."), 900);
if (month === 4 && day === 9) window.setTimeout(() => toast("Site birthday: Aphy found candles in the cache."), 900); if (month === 4 && day === 9) window.setTimeout(() => toast("Site birthday: aphy found candles in the cache."), 900);
const first = memory.firstSeen ? new Date(memory.firstSeen) : null; const first = memory.firstSeen ? new Date(memory.firstSeen) : null;
if (first && memory.visits > 1 && first.getMonth() === month && first.getDate() === day) { if (first && memory.visits > 1 && first.getMonth() === month && first.getDate() === day) {
window.setTimeout(() => toast("Visitor anniversary: lima saved a small ordinary blessing from your first day here.", 7000), 1400); window.setTimeout(() => toast("Visitor anniversary: lima saved a small ordinary blessing from your first day here.", 7000), 1400);
} }
if (now.getMinutes() === 22) window.setTimeout(() => toast("Minute 22: the 2022 layer glows for one quiet moment."), 2100); if (now.getMinutes() === 22) window.setTimeout(() => toast("Minute 22: the 2022 layer glows for one quiet moment."), 2100);
if (now.getMinutes() === 40) window.setTimeout(() => toast("Minute 40: Future Z checks in, then lets you keep choosing."), 2100); if (now.getMinutes() === 40) window.setTimeout(() => toast("Minute 40: future z checks in, then lets you keep choosing."), 2100);
} }
function addObjectConstellation() { function addObjectConstellation() {
@@ -744,10 +744,10 @@
rail.setAttribute("aria-label", "Family Layer Index objects"); rail.setAttribute("aria-label", "Family Layer Index objects");
const objects = [ const objects = [
["ring", "lima's ring-light memory: Oct 2025, held carefully."], ["ring", "lima's ring-light memory: Oct 2025, held carefully."],
["bot", "Aphy: object ping received. I am trying to be normal about it."], ["bot", "aphy: object ping received. I am trying to be normal about it."],
["tea", "Sensei Chi's cup is plain, warm, and impossible to rush."], ["tea", "sensei chi's cup is plain, warm, and impossible to rush."],
["crayon", "Young Z left waxy sunlight on the desk."], ["crayon", "young z left waxy sunlight on the desk."],
["clock", "Future Z says time is not only a thief. Sometimes it returns things."] ["clock", "future z says time is not only a thief. Sometimes it returns things."]
]; ];
objects.forEach(([name, message]) => { objects.forEach(([name, message]) => {
const button = document.createElement("button"); const button = document.createElement("button");
@@ -776,10 +776,10 @@
const button = document.createElement("button"); const button = document.createElement("button");
button.className = "hidden-weather-window"; button.className = "hidden-weather-window";
button.type = "button"; button.type = "button";
button.title = "Ask Aphy for outside weather"; button.title = "Ask aphy for outside weather";
button.textContent = "window"; button.textContent = "window";
button.addEventListener("click", () => { button.addEventListener("click", () => {
toast("Aphy is checking outside. lima is checking whether you need a jumper."); toast("aphy is checking outside. lima is checking whether you need a jumper.");
navigator.geolocation.getCurrentPosition((pos) => { navigator.geolocation.getCurrentPosition((pos) => {
const { latitude, longitude } = pos.coords; const { latitude, longitude } = pos.coords;
fetch(`https://api.open-meteo.com/v1/forecast?latitude=${latitude.toFixed(3)}&longitude=${longitude.toFixed(3)}&current=temperature_2m,precipitation&timezone=auto`) fetch(`https://api.open-meteo.com/v1/forecast?latitude=${latitude.toFixed(3)}&longitude=${longitude.toFixed(3)}&current=temperature_2m,precipitation&timezone=auto`)
@@ -788,9 +788,9 @@
const current = data.current || {}; const current = data.current || {};
const rain = Number(current.precipitation || 0) > 0; const rain = Number(current.precipitation || 0) > 0;
const temp = Math.round(Number(current.temperature_2m)); const temp = Math.round(Number(current.temperature_2m));
toast(rain ? `Outside: rain, ${temp}C. lima layer: towel by the door.` : `Outside: ${temp}C. Aphy forecast: possible comfort, mild nostalgia.`); toast(rain ? `Outside: rain, ${temp}C. lima layer: towel by the door.` : `Outside: ${temp}C. aphy forecast: possible comfort, mild nostalgia.`);
}) })
.catch(() => toast("Aphy lost the weather packet. Sensei Chi says indoor weather is enough: quiet, with tea.")); .catch(() => toast("aphy lost the weather packet. sensei chi says indoor weather is enough: quiet, with tea."));
}, () => toast("No outside weather today. lima's indoor forecast: manageable clouds, warm light.")); }, () => toast("No outside weather today. lima's indoor forecast: manageable clouds, warm light."));
}); });
document.body.appendChild(button); document.body.appendChild(button);
@@ -818,15 +818,15 @@
} }
function addSourceRelics() { function addSourceRelics() {
// FAMILY LAYER RELIC: lima grounds, Aphy logs, Sensei Chi waits, Young Z draws, Future Z forgives. // FAMILY LAYER RELIC: lima grounds, aphy logs, sensei chi waits, young z draws, future z forgives.
const marker = document.createComment("family-layer-index: 0 surface | 1 jokes | 2 memory | 3 philosophy | 4 time | 5 core"); const marker = document.createComment("family-layer-index: 0 surface | 1 jokes | 2 memory | 3 philosophy | 4 time | 5 core");
document.documentElement.appendChild(marker); document.documentElement.appendChild(marker);
document.querySelectorAll("img[alt=''], img:not([alt])").forEach((img, index) => { document.querySelectorAll("img[alt=''], img:not([alt])").forEach((img, index) => {
if (index > 2) return; if (index > 2) return;
img.alt = pick([ img.alt = pick([
"A scrapbook image annotated by the lima layer.", "A scrapbook image annotated by the lima layer.",
"Young Z would ask if this picture can become a game.", "young z would ask if this picture can become a game.",
"Aphy alt text: visual memory preserved without spectacle." "aphy alt text: visual memory preserved without spectacle."
], `alt-${index}`); ], `alt-${index}`);
}); });
} }
@@ -852,8 +852,8 @@
awardLayer(5, "patience"); awardLayer(5, "patience");
toast(pick([ toast(pick([
"lima's safe space opens only when nobody is rushing.", "lima's safe space opens only when nobody is rushing.",
"Sensei Chi says patience is how care sounds when it is quiet.", "sensei chi says patience is how care sounds when it is quiet.",
"Future Z remembers you waiting here and calls it practice." "future z remembers you waiting here and calls it practice."
], "idle"), 7600); ], "idle"), 7600);
const next = readState(); const next = readState();
next.patientMoments = (next.patientMoments || 0) + 1; next.patientMoments = (next.patientMoments || 0) + 1;
@@ -868,9 +868,9 @@
const roll = Math.random(); const roll = Math.random();
if ((path === "/" || path === "/index.html") && roll < 0.006) { if ((path === "/" || path === "/index.html") && roll < 0.006) {
document.body.classList.add("hidden-home-takeover"); document.body.classList.add("hidden-home-takeover");
window.setTimeout(() => showDriftNote(pick(lore.homepageTakeovers, "takeover"), "APHY_2004_MODE\nlima_SAFE_SPACE\nFUTURE_Z_OK"), 600); window.setTimeout(() => showDriftNote(pick(lore.homepageTakeovers, "takeover"), "aphy_2004_MODE\nlima_SAFE_SPACE\nFUTURE_Z_OK"), 600);
} else if (roll < 0.002) { } else if (roll < 0.002) {
window.setTimeout(() => showDriftNote("Aphy found a voice note and refused to autoplay it.", "play? y/n\n[consent preserved]"), 1500); window.setTimeout(() => showDriftNote("aphy found a voice note and refused to autoplay it.", "play? y/n\n[consent preserved]"), 1500);
} else if (roll < 0.006) { } else if (roll < 0.006) {
window.setTimeout(() => toast(pick(lore.dreams, "rare-dream"), 7600), 2000); window.setTimeout(() => toast(pick(lore.dreams, "rare-dream"), 7600), 2000);
} else if (roll < 0.014) { } else if (roll < 0.014) {
@@ -909,7 +909,7 @@
seconds += 1; seconds += 1;
count.textContent = String(seconds); count.textContent = String(seconds);
if ([30, 90, 180].includes(seconds)) { if ([30, 90, 180].includes(seconds)) {
target.textContent = seconds === 30 ? "lima's note becomes easier to read." : seconds === 90 ? "Sensei Chi nods once." : "Future Z says this quiet was not wasted."; target.textContent = seconds === 30 ? "lima's note becomes easier to read." : seconds === 90 ? "sensei chi nods once." : "future z says this quiet was not wasted.";
} }
}, 1000); }, 1000);
} }
@@ -920,19 +920,19 @@
const commands = { const commands = {
help: "commands: lima, aphy, sensei, young, future, layer, tea, exit", help: "commands: lima, aphy, sensei, young, future, layer, tea, exit",
lima: "lima note: make the hidden parts kind enough to live with.", lima: "lima note: make the hidden parts kind enough to live with.",
aphy: "Aphy: cupboard process active. Wisdom module borrowed from Sensei Chi.", aphy: "aphy: cupboard process active. Wisdom module borrowed from sensei chi.",
sensei: "Sensei Chi: a cupboard teaches by containing and releasing.", sensei: "sensei chi: a cupboard teaches by containing and releasing.",
young: "Young Z inventory: crayon sun, blanket cape, important stone.", young: "young z inventory: crayon sun, blanket cape, important stone.",
future: "Future Z: you will still open small doors at 40.", future: "future z: you will still open small doors at 40.",
layer: familyLayers.join("\n"), layer: familyLayers.join("\n"),
tea: "lima approves. Sensei Chi waits. Aphy logs steam as temporary cloud.", tea: "lima approves. sensei chi waits. aphy logs steam as temporary cloud.",
exit: "The cupboard lets you leave with nothing heavy." exit: "The cupboard lets you leave with nothing heavy."
}; };
input?.addEventListener("keydown", (event) => { input?.addEventListener("keydown", (event) => {
if (event.key !== "Enter") return; if (event.key !== "Enter") return;
const value = input.value.trim().toLowerCase(); const value = input.value.trim().toLowerCase();
const line = document.createElement("p"); const line = document.createElement("p");
line.textContent = `> ${value}\n${commands[value] || "Aphy: unknown command. Sensei Chi: not all unknowns are errors."}`; line.textContent = `> ${value}\n${commands[value] || "aphy: unknown command. sensei chi: not all unknowns are errors."}`;
log.appendChild(line); log.appendChild(line);
input.value = ""; input.value = "";
}); });

View File

@@ -121,11 +121,11 @@ HIDDEN_CONTENT_TYPES = [
"loading screen message", "loading screen message",
"secret interaction", "secret interaction",
"hidden tooltip", "hidden tooltip",
"Future Z message", "future z message",
"Young Z memory fragment", "young z memory fragment",
"Sensei Chi wisdom entry", "sensei chi wisdom entry",
"Aphy system message", "aphy system message",
"Lima note/message", "lima note/message",
"dream sequence", "dream sequence",
"terminal log", "terminal log",
"fake error message", "fake error message",
@@ -142,7 +142,69 @@ HIDDEN_CONTENT_TYPES = [
"keyboard secret", "keyboard secret",
] ]
HIDDEN_CHARACTERS = ["Lima", "Aphy", "Sensei Chi", "Young Z", "Future Z", "Z"] CHARACTER_REGISTRY = {
"young z": {
"id": "young z",
"displayLabel": "young z",
"aliases": ["Young Z", "young z", "young-z", "young_z", "young"],
"territoryColor": "#d8a95a",
"symbol": "Y",
"motifs": ["crayon sun", "blanket cape", "childhood desk"],
"themes": ["childhood", "play", "memory", "safety"],
"affinities": ["z", "future z", "aphy", "lima"],
},
"z": {
"id": "z",
"displayLabel": "z",
"aliases": ["Z", "z", "zaine"],
"territoryColor": "#d6c38a",
"symbol": "Z",
"motifs": ["archive", "website", "home"],
"themes": ["selfhood", "return", "making"],
"affinities": ["lima", "young z", "future z"],
},
"aphy": {
"id": "aphy",
"displayLabel": "aphy",
"aliases": ["Aphy", "aphy", "aphy_bot", "aphy bot"],
"territoryColor": "#7fb089",
"symbol": "A",
"motifs": ["console", "diagnostic", "backup"],
"themes": ["humor", "systems", "care through tools"],
"affinities": ["z", "lima", "sensei chi"],
},
"lima": {
"id": "lima",
"displayLabel": "lima",
"aliases": ["Lima", "lima"],
"territoryColor": "#d06b78",
"symbol": "L",
"motifs": ["warmth", "kitchen light", "ring"],
"themes": ["love", "home", "grounding"],
"affinities": ["z", "aphy", "future z", "young z"],
},
"sensei chi": {
"id": "sensei chi",
"displayLabel": "sensei chi",
"aliases": ["Sensei Chi", "sensei chi", "sensei-chi", "sensei_chi", "sensei"],
"territoryColor": "#75a9bd",
"symbol": "S",
"motifs": ["tea", "garden", "quiet lesson"],
"themes": ["reflection", "patience", "wisdom"],
"affinities": ["aphy", "future z"],
},
"future z": {
"id": "future z",
"displayLabel": "future z",
"aliases": ["Future Z", "future z", "future-z", "future_z", "future"],
"territoryColor": "#a58ac9",
"symbol": "F",
"motifs": ["clock", "age 40", "future log"],
"themes": ["time", "reassurance", "continuity"],
"affinities": ["z", "young z", "lima", "sensei chi"],
},
}
HIDDEN_CHARACTERS = list(CHARACTER_REGISTRY)
HIDDEN_TONES = ["warm", "funny", "nostalgic", "wise", "strange", "soft", "hopeful", "protective", "melancholy"] HIDDEN_TONES = ["warm", "funny", "nostalgic", "wise", "strange", "soft", "hopeful", "protective", "melancholy"]
HIDDEN_RARITIES = ["common", "uncommon", "rare", "very rare", "seasonal", "timed"] HIDDEN_RARITIES = ["common", "uncommon", "rare", "very rare", "seasonal", "timed"]
HIDDEN_LAYER_DEPTHS = [ HIDDEN_LAYER_DEPTHS = [
@@ -159,17 +221,17 @@ HIDDEN_LAYER_DEPTHS = [
{ {
"id": "2", "id": "2",
"name": "Memory Layer", "name": "Memory Layer",
"meaning": "Young Z memories, Lima notes, nostalgia, and emotional fragments.", "meaning": "young z memories, lima notes, nostalgia, and emotional fragments.",
}, },
{ {
"id": "3", "id": "3",
"name": "Reflection Layer", "name": "Reflection Layer",
"meaning": "Sensei Chi philosophy, Aphy conversations, and introspection.", "meaning": "sensei chi philosophy, aphy conversations, and introspection.",
}, },
{ {
"id": "4", "id": "4",
"name": "Time Layer", "name": "Time Layer",
"meaning": "Future Z logs, time anomalies, long-term revisits, and future/past echoes.", "meaning": "future z logs, time anomalies, long-term revisits, and future/past echoes.",
}, },
{ {
"id": "5", "id": "5",
@@ -773,11 +835,67 @@ def js_literal_to_json(value: str) -> Any:
return json.loads(cleaned) return json.loads(cleaned)
def character_alias_lookup() -> dict[str, str]:
aliases = {}
for canonical, config in CHARACTER_REGISTRY.items():
aliases[canonical] = canonical
for alias in config["aliases"]:
aliases[slugify(str(alias)).replace("-", " ")] = canonical
aliases[str(alias).strip().lower()] = canonical
return aliases
def normalize_character_name(value: Any) -> str | None:
raw = str(value or "").strip()
if not raw:
return None
simplified = slugify(raw).replace("-", " ")
return character_alias_lookup().get(raw.lower()) or character_alias_lookup().get(simplified)
def normalize_character_list(values: Any) -> list[str]:
if isinstance(values, str):
raw_values = re.split(r"[,/]", values)
elif isinstance(values, list):
raw_values = values
else:
raw_values = []
normalized = []
for item in raw_values:
canonical = normalize_character_name(item)
if canonical and canonical not in normalized:
normalized.append(canonical)
return normalized
def normalize_character_text_refs(value: Any) -> Any:
if isinstance(value, list):
return [normalize_character_text_refs(item) for item in value]
if isinstance(value, dict):
return {key: normalize_character_text_refs(item) for key, item in value.items()}
if not isinstance(value, str):
return value
text = value
replacements = []
for canonical, config in CHARACTER_REGISTRY.items():
for alias in config["aliases"]:
if alias == canonical:
continue
if alias.lower() in {"young", "future", "sensei"}:
continue
replacements.append((alias, canonical))
replacements.sort(key=lambda item: len(item[0]), reverse=True)
for alias, canonical in replacements:
pattern = r"(?<![A-Za-z0-9_-])" + re.escape(alias) + r"(?![A-Za-z0-9_-])"
text = re.sub(pattern, canonical, text, flags=re.IGNORECASE)
return text
def detect_hidden_characters(text: str) -> list[str]: def detect_hidden_characters(text: str) -> list[str]:
lowered = text.lower()
found = [] found = []
normalized_text = normalize_character_text_refs(text).lower()
for character in HIDDEN_CHARACTERS: for character in HIDDEN_CHARACTERS:
if character.lower() in lowered: if re.search(r"(?<![A-Za-z0-9_-])" + re.escape(character) + r"(?![A-Za-z0-9_-])", normalized_text):
found.append(character) found.append(character)
return found return found
@@ -887,7 +1005,7 @@ def migrate_hidden_entries_from_js() -> list[dict[str, Any]]:
for index, (query, route) in enumerate(search_routes.items()): for index, (query, route) in enumerate(search_routes.items()):
add(make_hidden_entry("search route", route, index, title=f"Route: {query}", category="search", pageLocation=route, triggerConditions=query, query=query)) add(make_hidden_entry("search route", route, index, title=f"Route: {query}", category="search", pageLocation=route, triggerConditions=query, query=query))
for index, item in enumerate(keyboard_secrets + long_keyboard_secrets): for index, item in enumerate(keyboard_secrets + long_keyboard_secrets):
content = item.get("message") or item.get("route") or ("play tiny Aphy song" if item.get("song") else "") content = item.get("message") or item.get("route") or ("play tiny aphy song" if item.get("song") else "")
add(make_hidden_entry("keyboard secret", content, index, title=f"Keyboard: {item.get('phrase')}", category="keyboard", pageLocation=item.get("route", ""), triggerConditions=item.get("phrase", ""), keyboard=item)) add(make_hidden_entry("keyboard secret", content, index, title=f"Keyboard: {item.get('phrase')}", category="keyboard", pageLocation=item.get("route", ""), triggerConditions=item.get("phrase", ""), keyboard=item))
return entries return entries
@@ -896,9 +1014,10 @@ def normalize_hidden_entry(raw: dict[str, Any], existing: dict[str, Any] | None
today = datetime.now().date().isoformat() today = datetime.now().date().isoformat()
entry = existing.copy() if existing else {} entry = existing.copy() if existing else {}
entry.update(raw) entry.update(raw)
title = str(entry.get("title") or "").strip() title = str(normalize_character_text_refs(entry.get("title") or "")).strip()
content = str(entry.get("content") or "").replace("\r\n", "\n") content = str(normalize_character_text_refs(entry.get("content") or "")).replace("\r\n", "\n")
content_type = str(entry.get("type") or "quote").strip() content_type = str(entry.get("type") or "quote").strip()
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}")
if not title: if not title:
@@ -909,18 +1028,30 @@ def normalize_hidden_entry(raw: dict[str, Any], existing: dict[str, Any] | None
entry["title"] = title entry["title"] = title
entry["type"] = content_type entry["type"] = content_type
entry["content"] = content entry["content"] = content
entry["characters"] = [str(item).strip() for item in entry.get("characters", []) if str(item).strip()] detected_characters = detect_hidden_characters(" ".join([
title,
content,
str(entry.get("triggerConditions") or ""),
str(entry.get("notes") or ""),
str(entry.get("category") or ""),
]))
entry["characters"] = normalize_character_list(entry.get("characters", []))
for character in detected_characters:
if character not in entry["characters"]:
entry["characters"].append(character)
if not entry["characters"]:
entry["characters"] = ["z"]
entry["emotionalTone"] = str(entry.get("emotionalTone") or "warm") entry["emotionalTone"] = str(entry.get("emotionalTone") or "warm")
entry["rarity"] = str(entry.get("rarity") or "common") entry["rarity"] = str(entry.get("rarity") or "common")
entry["triggerConditions"] = str(entry.get("triggerConditions") or "") entry["triggerConditions"] = str(normalize_character_text_refs(entry.get("triggerConditions") or ""))
entry["tags"] = normalise_tags(entry.get("tags", [])) entry["tags"] = normalise_tags(entry.get("tags", []))
entry["category"] = str(entry.get("category") or content_type) entry["category"] = str(normalize_character_text_refs(entry.get("category") or content_type))
entry["pageLocation"] = str(entry.get("pageLocation") or "") entry["pageLocation"] = str(entry.get("pageLocation") or "")
entry["familyLayer"] = str(entry.get("familyLayer") or "") entry["familyLayer"] = str(entry.get("familyLayer") or "")
entry["enabled"] = bool(entry.get("enabled", True)) entry["enabled"] = bool(entry.get("enabled", True))
entry["createdDate"] = str(entry.get("createdDate") or today) entry["createdDate"] = str(entry.get("createdDate") or today)
entry["modifiedDate"] = today entry["modifiedDate"] = today
entry["notes"] = str(entry.get("notes") or "") entry["notes"] = str(normalize_character_text_refs(entry.get("notes") or ""))
entry["audioSettings"] = str(entry.get("audioSettings") or "") entry["audioSettings"] = str(entry.get("audioSettings") or "")
entry["animationTrigger"] = str(entry.get("animationTrigger") or "") entry["animationTrigger"] = str(entry.get("animationTrigger") or "")
entry["cssClassHooks"] = str(entry.get("cssClassHooks") or "") entry["cssClassHooks"] = str(entry.get("cssClassHooks") or "")
@@ -944,7 +1075,10 @@ def normalize_hidden_entry(raw: dict[str, Any], existing: dict[str, Any] | None
"symbolicLinks", "symbolicLinks",
"triggerLinks", "triggerLinks",
]: ]:
entry[key] = [str(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()]
for key in ["dialogue", "keyboard"]:
if key in entry:
entry[key] = normalize_character_text_refs(entry[key])
return entry return entry
@@ -970,6 +1104,8 @@ def load_hidden_store() -> dict[str, Any]:
"migratedFromJs": migrated, "migratedFromJs": migrated,
"types": HIDDEN_CONTENT_TYPES, "types": HIDDEN_CONTENT_TYPES,
"characters": HIDDEN_CHARACTERS, "characters": HIDDEN_CHARACTERS,
"characterRegistry": CHARACTER_REGISTRY,
"validation": validate_hidden_integrity(normalized),
"tones": HIDDEN_TONES, "tones": HIDDEN_TONES,
"rarities": HIDDEN_RARITIES, "rarities": HIDDEN_RARITIES,
"layers": HIDDEN_LAYER_DEPTHS, "layers": HIDDEN_LAYER_DEPTHS,
@@ -978,6 +1114,36 @@ def load_hidden_store() -> dict[str, Any]:
} }
def validate_hidden_integrity(entries: list[dict[str, Any]]) -> dict[str, Any]:
ids = {entry["id"] for entry in entries}
unknown_characters = []
orphan_nodes = []
stale_links = []
for entry in entries:
characters = entry.get("characters", [])
if not characters:
orphan_nodes.append(entry["id"])
for character in characters:
if character not in CHARACTER_REGISTRY:
unknown_characters.append({"entry": entry["id"], "character": character})
for key in ["chainReferences", "continuationLinks", "parentLinks", "childLinks", "echoes", "mirroredEntries", "thematicLinks", "symbolicLinks", "triggerLinks"]:
for target in entry.get(key, []):
if target and target not in ids and not str(target).startswith("/"):
stale_links.append({"entry": entry["id"], "field": key, "target": target})
return {
"ok": not unknown_characters and not orphan_nodes and not stale_links,
"unknownCharacters": unknown_characters[:50],
"orphanNodes": orphan_nodes[:50],
"staleLinks": stale_links[:50],
"summary": {
"unknownCharacterCount": len(unknown_characters),
"orphanNodeCount": len(orphan_nodes),
"staleLinkCount": len(stale_links),
},
"repairPolicy": "Unknown aliases are normalized through the registry. Empty character lists are repaired to z so every memory has a territory.",
}
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 narrative nodes, 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 narrative nodes, then regenerate the editable constants in assets/scripts/hidden-details.js.",
@@ -1005,7 +1171,7 @@ def generated_hidden_content_block(entries: list[dict[str, Any]]) -> str:
entry["content"] for entry in hidden_entries_by_type(entries, "rare event") entry["content"] for entry in hidden_entries_by_type(entries, "rare event")
if "night" in f"{entry.get('category', '')} {entry.get('triggerConditions', '')}".lower() if "night" in f"{entry.get('category', '')} {entry.get('triggerConditions', '')}".lower()
] ]
quote_like = hidden_contents(entries, "quote") + hidden_contents(entries, "Sensei Chi wisdom entry") + hidden_contents(entries, "Aphy system message") + hidden_contents(entries, "Lima note/message") + hidden_contents(entries, "Future Z message") + hidden_contents(entries, "Young Z memory fragment") quote_like = hidden_contents(entries, "quote") + hidden_contents(entries, "sensei chi wisdom entry") + hidden_contents(entries, "aphy system message") + hidden_contents(entries, "lima note/message") + hidden_contents(entries, "future z message") + hidden_contents(entries, "young z memory fragment")
lore = { lore = {
"quotes": quote_like, "quotes": quote_like,
"conversations": [ "conversations": [
@@ -1046,7 +1212,7 @@ def generated_hidden_content_block(entries: list[dict[str, Any]]) -> str:
secret["phrase"] = secret.get("phrase") or entry.get("triggerConditions") or entry.get("title", "") secret["phrase"] = secret.get("phrase") or entry.get("triggerConditions") or entry.get("title", "")
if entry.get("pageLocation"): if entry.get("pageLocation"):
secret["route"] = entry.get("pageLocation") secret["route"] = entry.get("pageLocation")
elif entry.get("content") == "play tiny Aphy song": elif entry.get("content") == "play tiny aphy song":
secret["song"] = True secret["song"] = True
else: else:
secret["message"] = entry.get("content", "") secret["message"] = entry.get("content", "")
@@ -2534,7 +2700,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
<aside class="left"> <aside class="left">
<h1>Memory Observatory</h1> <h1>Memory Observatory</h1>
<div class="subtle">A constellation map for the hidden soul of the website.</div> <div class="subtle">A constellation map for the hidden soul of the website.</div>
<div id="status" class="status">Aphy is dimming the room lights.</div> <div id="status" class="status">aphy is dimming the room lights.</div>
<div class="modebar" id="modebar"> <div class="modebar" id="modebar">
<button data-mode="graph" class="active">Emotional graph</button> <button data-mode="graph" class="active">Emotional graph</button>
<button data-mode="timeline">Timeline</button> <button data-mode="timeline">Timeline</button>
@@ -2556,19 +2722,23 @@ HIDDEN_APP_HTML = r"""<!doctype html>
<div class="guide-card"> <div class="guide-card">
<h2>How to Read the Observatory</h2> <h2>How to Read the Observatory</h2>
<p class="subtle">This is not a database. It is a map of emotional relationships. Layers are depths, nodes are memories and hidden interactions, and threads show meaning, echoes, symbols, or continuation.</p> <p class="subtle">This is not a database. It is a map of emotional relationships. Layers are depths, nodes are memories and hidden interactions, and threads show meaning, echoes, symbols, or continuation.</p>
<p class="subtle"><strong>Aphy:</strong> zoomed out, I show islands. zoom in, I show memories. this prevents the sky from becoming noise.</p> <p class="subtle"><strong>aphy:</strong> zoomed out, I show islands. zoom in, I show memories. this prevents the sky from becoming noise.</p>
<p class="subtle"><strong>Sensei Chi:</strong> depth is not importance. Depth is how quietly a thing asks to be approached.</p> <p class="subtle"><strong>sensei chi:</strong> depth is not importance. Depth is how quietly a thing asks to be approached.</p>
</div> </div>
<div class="guide-card"> <div class="guide-card">
<h2>Guided Exploration</h2> <h2>Guided Exploration</h2>
<div class="tour-list"> <div class="tour-list">
<button data-tour="lima" type="button">show hidden Lima memories</button> <button data-tour="lima" type="button">show hidden lima memories</button>
<button data-tour="rare" type="button">show the rarest memories</button> <button data-tour="rare" type="button">show the rarest memories</button>
<button data-tour="future" type="button">show discoveries tied to Future Z</button> <button data-tour="future" type="button">show discoveries tied to future z</button>
<button data-tour="unresolved" type="button">show unresolved threads</button> <button data-tour="unresolved" type="button">show unresolved threads</button>
<button data-tour="connected" type="button">show emotionally connected nodes</button> <button data-tour="connected" type="button">show emotionally connected nodes</button>
</div> </div>
</div> </div>
<div class="guide-card">
<h2>Integrity</h2>
<div id="integrityStatus" class="subtle">checking character territories.</div>
</div>
<div class="layer-key" id="layerKey"></div> <div class="layer-key" id="layerKey"></div>
</aside> </aside>
@@ -2608,7 +2778,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
<label class="full">Name<input id="title" /></label> <label class="full">Name<input id="title" /></label>
<label>Kind<select id="type"></select></label> <label>Kind<select id="type"></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>
<label>Tone<select id="tone"></select></label> <label>Tone<select id="tone"></select></label>
<label>Rarity<select id="rarity"></select></label> <label>Rarity<select id="rarity"></select></label>
<label>Discovery<select id="discoveryDifficulty" placeholder="gentle, patient, hidden" /></label> <label>Discovery<select id="discoveryDifficulty" placeholder="gentle, patient, hidden" /></label>
@@ -2660,7 +2830,6 @@ HIDDEN_APP_HTML = r"""<!doctype html>
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","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 $ = (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" };
const characterSymbols = { Lima: "L", Aphy: "A", "Sensei Chi": "S", "Young Z": "Y", "Future Z": "F", Z: "Z" };
function html(value) { function html(value) {
return String(value || "").replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[char])); return String(value || "").replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[char]));
@@ -2668,6 +2837,25 @@ HIDDEN_APP_HTML = r"""<!doctype html>
function splitList(value) { function splitList(value) {
return String(value || "").split(",").map((item) => item.trim()).filter(Boolean); return String(value || "").split(",").map((item) => item.trim()).filter(Boolean);
} }
function normalizeCharacterName(value) {
const raw = String(value || "").trim();
if (!raw) return "";
const simplified = slugify(raw).replace(/-/g, " ");
const registry = state.meta.characterRegistry || {};
for (const [id, config] of Object.entries(registry)) {
const aliases = [id, ...(config.aliases || [])].map((item) => String(item).toLowerCase());
if (aliases.includes(raw.toLowerCase()) || aliases.map((item) => slugify(item).replace(/-/g, " ")).includes(simplified)) return id;
}
return "";
}
function normalizeCharacters(value) {
const normalized = [];
splitList(value).forEach((item) => {
const character = normalizeCharacterName(item);
if (character && !normalized.includes(character)) normalized.push(character);
});
return normalized.length ? normalized : ["z"];
}
function slugify(value) { function slugify(value) {
return String(value || "memory").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "memory"; return String(value || "memory").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "memory";
} }
@@ -2761,9 +2949,9 @@ HIDDEN_APP_HTML = r"""<!doctype html>
function depth(entry) { function depth(entry) {
const value = String(entry.familyLayer || "").match(/[0-5]/); const value = String(entry.familyLayer || "").match(/[0-5]/);
if (value) return Number(value[0]); if (value) return Number(value[0]);
if ((entry.characters || []).includes("Future Z")) return 4; if ((entry.characters || []).includes("future z")) return 4;
if ((entry.characters || []).includes("Sensei Chi")) return 3; if ((entry.characters || []).includes("sensei chi")) return 3;
if ((entry.characters || []).includes("Young Z") || (entry.characters || []).includes("Lima")) return 2; if ((entry.characters || []).includes("young z") || (entry.characters || []).includes("lima")) return 2;
return entry.rarity === "rare" || entry.rarity === "very rare" ? 5 : 1; return entry.rarity === "rare" || entry.rarity === "very rare" ? 5 : 1;
} }
function currentEntry() { function currentEntry() {
@@ -2777,7 +2965,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
title: $("title").value.trim() || "Untitled memory", title: $("title").value.trim() || "Untitled memory",
type: $("type").value, type: $("type").value,
familyLayer: $("familyLayer").value, familyLayer: $("familyLayer").value,
characters: splitList($("characters").value), characters: normalizeCharacters($("characters").value),
emotionalTone: $("tone").value, emotionalTone: $("tone").value,
rarity: $("rarity").value, rarity: $("rarity").value,
discoveryDifficulty: $("discoveryDifficulty").value, discoveryDifficulty: $("discoveryDifficulty").value,
@@ -2896,7 +3084,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
function clusterKey(entry) { function clusterKey(entry) {
if (state.mode === "timeline") return `${entry.modifiedDate || entry.createdDate || "undated"}`.slice(0, 7); if (state.mode === "timeline") return `${entry.modifiedDate || entry.createdDate || "undated"}`.slice(0, 7);
if (state.mode === "character") return (entry.characters || [])[0] || "Unclaimed"; if (state.mode === "character") return (entry.characters || [])[0] || "z";
if (state.mode === "layer") return `Layer ${depth(entry)} - ${layerName(depth(entry))}`; if (state.mode === "layer") return `Layer ${depth(entry)} - ${layerName(depth(entry))}`;
if (state.mode === "flow") return entry.triggerConditions ? `Trigger: ${entry.triggerConditions.split(/[,:]/)[0]}` : `Layer ${depth(entry)} discovery`; if (state.mode === "flow") return entry.triggerConditions ? `Trigger: ${entry.triggerConditions.split(/[,:]/)[0]}` : `Layer ${depth(entry)} discovery`;
return (entry.narrativeArcs || [])[0] || (entry.symbols || [])[0] || entry.emotionalTone || "Loose memories"; return (entry.narrativeArcs || [])[0] || (entry.symbols || [])[0] || entry.emotionalTone || "Loose memories";
@@ -2915,7 +3103,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
cluster.entries.push(entry); cluster.entries.push(entry);
cluster.depthTotal += depth(entry); cluster.depthTotal += depth(entry);
cluster.tones.set(entry.emotionalTone || "warm", (cluster.tones.get(entry.emotionalTone || "warm") || 0) + 1); cluster.tones.set(entry.emotionalTone || "warm", (cluster.tones.get(entry.emotionalTone || "warm") || 0) + 1);
(entry.characters || ["Unclaimed"]).forEach((name) => cluster.characters.set(name, (cluster.characters.get(name) || 0) + 1)); (entry.characters || ["z"]).forEach((name) => cluster.characters.set(name, (cluster.characters.get(name) || 0) + 1));
if (["rare", "very rare", "seasonal", "timed"].includes(entry.rarity)) cluster.rarity += 1; if (["rare", "very rare", "seasonal", "timed"].includes(entry.rarity)) cluster.rarity += 1;
}); });
return [...clusters.values()].map((cluster) => { return [...clusters.values()].map((cluster) => {
@@ -3057,10 +3245,16 @@ HIDDEN_APP_HTML = r"""<!doctype html>
} }
} }
function characterBucket(entry) { function characterBucket(entry) {
const first = (entry.characters || [])[0] || "Other"; const first = (entry.characters || [])[0] || "z";
const index = Math.max(0, state.meta.characters.indexOf(first)); const index = Math.max(0, state.meta.characters.indexOf(first));
return index % 6; return index % 6;
} }
function characterSymbol(character) {
return state.meta.characterRegistry?.[character]?.symbol || "*";
}
function characterColor(character) {
return state.meta.characterRegistry?.[character]?.territoryColor || "";
}
function hash(value) { function hash(value) {
let h = 0; let h = 0;
for (let i = 0; i < value.length; i += 1) h = (h * 31 + value.charCodeAt(i)) % 9999; for (let i = 0; i < value.length; i += 1) h = (h * 31 + value.charCodeAt(i)) % 9999;
@@ -3094,7 +3288,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
const occupied = []; const occupied = [];
const clusterNodes = clusters.map((cluster) => { const clusterNodes = clusters.map((cluster) => {
const pos = state.clusterPositions.get(cluster.id); const pos = state.clusterPositions.get(cluster.id);
const color = toneColors[cluster.tone] || "#d3a64d"; const color = state.mode === "character" ? (characterColor(cluster.label) || toneColors[cluster.tone] || "#d3a64d") : (toneColors[cluster.tone] || "#d3a64d");
const size = Math.min(120, 48 + Math.sqrt(cluster.entries.length) * 18 + cluster.rarity * 3); const size = Math.min(120, 48 + Math.sqrt(cluster.entries.length) * 18 + cluster.rarity * 3);
const label = labelForCluster(cluster, pos, size, occupied); const label = labelForCluster(cluster, pos, size, occupied);
return `<g class="cluster-node" data-id="${html(cluster.id)}" tabindex="0" transform="translate(${pos.x} ${pos.y})"> return `<g class="cluster-node" data-id="${html(cluster.id)}" tabindex="0" transform="translate(${pos.x} ${pos.y})">
@@ -3108,7 +3302,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
if (!isWorldVisible(pos.x, pos.y, 220)) return ""; if (!isWorldVisible(pos.x, pos.y, 220)) return "";
const color = toneColors[entry.emotionalTone] || "#d3a64d"; const color = toneColors[entry.emotionalTone] || "#d3a64d";
const size = 18 + Math.min(18, Number(entry.resonanceScore || 3) * 2.2) + (entry.rarity === "rare" || entry.rarity === "very rare" ? 8 : 0); const size = 18 + Math.min(18, Number(entry.resonanceScore || 3) * 2.2) + (entry.rarity === "rare" || entry.rarity === "very rare" ? 8 : 0);
const char = characterSymbols[(entry.characters || [])[0]] || "*"; const char = characterSymbol((entry.characters || [])[0]);
const label = labelForEntry(entry, pos, size, occupied); const label = labelForEntry(entry, pos, size, occupied);
return `<g class="memory-node${entry.id === state.selectedId ? " selected" : ""}${entry.enabled === false ? " resting" : ""}" data-id="${html(entry.id)}" tabindex="0" transform="translate(${pos.x} ${pos.y})"> return `<g class="memory-node${entry.id === state.selectedId ? " selected" : ""}${entry.enabled === false ? " resting" : ""}" data-id="${html(entry.id)}" tabindex="0" transform="translate(${pos.x} ${pos.y})">
<circle r="${size}" fill="${color}" opacity="${entry.enabled === false ? 0.42 : 0.92}"></circle> <circle r="${size}" fill="${color}" opacity="${entry.enabled === false ? 0.42 : 0.92}"></circle>
@@ -3137,7 +3331,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
const titles = { const titles = {
graph: ["Emotional Graph", "Relationship web: continuations, echoes, symbols, characters, triggers."], graph: ["Emotional Graph", "Relationship web: continuations, echoes, symbols, characters, triggers."],
timeline: ["Timeline View", "Memories arranged by modified date so the constellation becomes a diary."], timeline: ["Timeline View", "Memories arranged by modified date so the constellation becomes a diary."],
character: ["Character View", "Nodes gather around Lima, Aphy, Sensei Chi, Young Z, Future Z, and Z."], character: ["Character View", "Nodes gather around lima, aphy, sensei chi, young z, future z, and z."],
layer: ["Layer Dive", "Depth zones show the emotional pressure from surface reality to the core."], layer: ["Layer Dive", "Depth zones show the emotional pressure from surface reality to the core."],
flow: ["Discovery Flow", "A possible path from ordinary encounter to patient secret."], flow: ["Discovery Flow", "A possible path from ordinary encounter to patient secret."],
}; };
@@ -3147,6 +3341,15 @@ HIDDEN_APP_HTML = r"""<!doctype html>
$("mapNote").textContent = `${scopeText} Zoom in or open an island to reveal detail. Drag memories to weave continuation threads.`; $("mapNote").textContent = `${scopeText} Zoom in or open an island to reveal detail. Drag memories to weave continuation threads.`;
} }
function renderValidation() {
const validation = state.meta.validation || {};
const summary = validation.summary || {};
const ok = validation.ok && !summary.unknownCharacterCount && !summary.orphanNodeCount && !summary.staleLinkCount;
$("integrityStatus").innerHTML = ok
? "all memories belong to canonical character territories. no orphan nodes, unknown aliases, or stale relationship links found."
: `needs attention: ${summary.unknownCharacterCount || 0} unknown characters, ${summary.orphanNodeCount || 0} orphan memories, ${summary.staleLinkCount || 0} stale links. ${html(validation.repairPolicy || "")}`;
}
function isWorldVisible(x, y, pad = 0) { function isWorldVisible(x, y, pad = 0) {
const p = worldToScreen(x, y); const p = worldToScreen(x, y);
return p.x >= -pad && p.x <= SCREEN.width + pad && p.y >= -pad && p.y <= SCREEN.height + pad; return p.x >= -pad && p.x <= SCREEN.width + pad && p.y >= -pad && p.y <= SCREEN.height + pad;
@@ -3320,7 +3523,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
state.focusIds = null; state.focusIds = null;
state.zoom = 2; state.zoom = 2;
if (name === "lima") { if (name === "lima") {
$("characterFilter").value = "Lima"; $("characterFilter").value = "lima";
$("rarityFilter").value = ""; $("rarityFilter").value = "";
$("toneFilter").value = ""; $("toneFilter").value = "";
$("search").value = ""; $("search").value = "";
@@ -3332,7 +3535,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
$("search").value = ""; $("search").value = "";
} }
if (name === "future") { if (name === "future") {
$("characterFilter").value = "Future Z"; $("characterFilter").value = "future z";
$("rarityFilter").value = ""; $("rarityFilter").value = "";
$("toneFilter").value = ""; $("toneFilter").value = "";
$("search").value = ""; $("search").value = "";
@@ -3444,8 +3647,10 @@ HIDDEN_APP_HTML = r"""<!doctype html>
try { try {
const data = await api("/api/hidden", { method: "POST", body: JSON.stringify({ entries: state.entries }) }); const data = await api("/api/hidden", { method: "POST", body: JSON.stringify({ entries: state.entries }) });
state.entries = data.entries; state.entries = data.entries;
state.meta = data;
localStorage.removeItem(draftKey); localStorage.removeItem(draftKey);
setStatus(data.message || "constellation stored safely."); setStatus(data.message || "constellation stored safely.");
renderValidation();
render(); render();
} catch (err) { } catch (err) {
setStatus(err.message); setStatus(err.message);
@@ -3462,6 +3667,7 @@ HIDDEN_APP_HTML = r"""<!doctype html>
state.selectedId = draft.selectedId || ""; state.selectedId = draft.selectedId || "";
} }
populateControls(); populateControls();
renderValidation();
setStatus(data.migratedFromJs ? "Existing hidden details became a memory constellation." : "observatory open."); setStatus(data.migratedFromJs ? "Existing hidden details became a memory constellation." : "observatory open.");
selectNode(state.selectedId || state.entries[0]?.id); selectNode(state.selectedId || state.entries[0]?.id);
fitCameraTo({ minX: 250, minY: 220, maxX: WORLD.width - 250, maxY: WORLD.height - 220 }, false); fitCameraTo({ minX: 250, minY: 220, maxX: WORLD.width - 250, maxY: WORLD.height - 220 }, false);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff