feat(journey): wire Receipt → centered Graph → auto-Cinema → Explore, all base-safe

The hero journey now flows through visible controls with cognitive context that
survives navigation (os-nav.ts gets its first real consumers):
- /dashboard/ redirects to /dashboard/palace (the home), base-safe (was a bare
  goto('/graph') that escaped the base + landed on the wrong organ).
- ReceiptCard 'Open in Cinema' uses osGoto with {memory, focus, receipt, cinema}
  — fixes the base-escape 404 AND carries the full evidence set.
- Graph consumes ?memory= (canonical; ?center= legacy alias), centers on it, and
  a Graph-OWNED one-shot bridge auto-launches the PROTECTED Cinema by clicking its
  own .cinema-launch control (MemoryCinema untouched).
- Graph 'Explore Connections' now carries the selected memory; Explore seeds the
  walk from THAT memory instead of re-seeding from the newest.
Verified live: /→palace, graph?memory=X&cinema=1 centers + auto-opens Cinema (0
errors), explore?memory=X seeds a walk. check 0/0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Sam Valladares 2026-07-13 11:31:26 +07:00
parent 3200f04de2
commit 4c6a24a35a
4 changed files with 71 additions and 18 deletions

View file

@ -8,9 +8,9 @@
// the receipt's primary memory, starting the (protected) Cinema flythrough
// over the exact memory set the receipt names.
// ═══════════════════════════════════════════════════════════════════════
import { goto } from '$app/navigation';
import Icon from './Icon.svelte';
import type { Receipt } from '$lib/stores/api';
import { osGoto } from '$lib/os-nav';
interface Props {
receipt: Receipt;
@ -24,11 +24,20 @@
high: '#f43f5e'
};
// Hero-journey handoff: open the exact receipt in the Graph, centered on its
// primary memory, with the full retrieved set focused, then AUTO-LAUNCH Cinema
// over that evidence. Base-safe via osGoto (a bare goto('/graph') escaped the
// /dashboard base and 404'd); the cognitive context (memory/focus/receipt/
// cinema) rides the URL so the whole set survives the navigation.
function openInCinema() {
const primary = receipt.retrieved[0];
if (!primary) return;
const focus = receipt.retrieved.join(',');
goto(`/graph?center=${encodeURIComponent(primary)}&focus=${encodeURIComponent(focus)}`);
osGoto('/graph', {
memory: primary,
focus: receipt.retrieved,
receipt: receipt.receipt_id,
cinema: true
});
}
</script>

View file

@ -52,9 +52,16 @@
// may apply results / clear the walking label. (GPT-5.6-sol cross-review.)
let loadGen = 0;
// A memory id passed from another organ (e.g. Graph's "Explore Connections")
// seeds the walk from THAT thought instead of re-seeding from the newest one,
// so the selected memory survives the Graph → Explore handoff.
let seedMemoryId: string | null = $state(null);
onMount(() => {
const params = new URLSearchParams(window.location.search);
searchQuery = params.get('q') ?? '';
// `memory` is the canonical cross-organ selection contract (center = legacy).
seedMemoryId = params.get('memory') ?? params.get('center');
void loadNeighbors();
});
@ -145,13 +152,20 @@
// immediately alive, and every click walks onward from a real thought.
if (!searchQuery.trim()) {
try {
const seed = await api.memories.list({ limit: '1' });
// If another organ handed us a memory (?memory=<id>), seed the walk
// from THAT thought so the selection survives the handoff. Otherwise
// auto-seed from the newest memory so landing here is always alive.
const seedMemory = seedMemoryId
? await api.memories.get(seedMemoryId).catch(() => null)
: null;
const start = seedMemory ?? (await api.memories.list({ limit: '1' })).memories?.[0] ?? null;
if (gen !== loadGen) return; // a newer walk superseded this load
const newest = seed.memories?.[0];
if (newest?.content) {
searchQuery = walkQuery(newest.content);
centerId = newest.id;
seededFrom = newest.id.slice(0, 8);
if (start?.content) {
searchQuery = walkQuery(start.content);
centerId = start.id;
seededFrom = start.id.slice(0, 8);
// Consume the one-shot seed so a later re-walk doesn't re-pin it.
seedMemoryId = null;
// The seeded expedition must be shareable/reproducible too — a
// reload mid-ingest would otherwise seed from a different memory.
syncQueryToUrl();

View file

@ -1,6 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { base } from '$app/paths';
import { osHref } from '$lib/os-nav';
import Graph3D from '$components/Graph3D.svelte';
import RetentionCurve from '$components/RetentionCurve.svelte';
import TimeSlider from '$components/TimeSlider.svelte';
@ -223,14 +224,37 @@
if (isColorMode(requestedMode)) {
colorMode = requestedMode;
}
// "Open receipt in Cinema" deep-links here with ?center=<memoryId>, so
// the graph loads centered on the receipt's primary memory and the
// (protected) Cinema flythrough starts from that exact node. We do not
// touch MemoryCinema itself — only seed the graph it renders.
const center = sp.get('center');
void loadGraph(undefined, center || undefined);
// The hero journey deep-links here carrying cognitive context: ?memory=<id>
// (canonical; ?center= is the legacy alias) centers the graph on the
// receipt's primary memory, and ?cinema=1 AUTO-LAUNCHES the (protected)
// Cinema flythrough over that exact evidence set. We NEVER touch
// MemoryCinema — a Graph-owned one-shot bridge just clicks its own launch
// control once the graph has loaded, so the component stays untouched.
const center = sp.get('memory') ?? sp.get('center');
const autoCinema = sp.get('cinema') === '1';
void loadGraph(undefined, center || undefined).then(() => {
if (autoCinema) launchCinemaOnce();
});
});
// One-shot bridge: after the graph has real nodes, programmatically trigger
// the protected Cinema's own launch button exactly once. Read-only w.r.t.
// MemoryCinema — we click its rendered control, never import/modify its state.
function launchCinemaOnce() {
let tries = 0;
const timer = setInterval(() => {
tries += 1;
const btn = document.querySelector<HTMLButtonElement>('.cinema-launch');
if (btn) {
btn.click();
clearInterval(timer);
} else if (tries > 40) {
// ~6s: nodes never materialized (empty/errored graph) — give up quietly.
clearInterval(timer);
}
}, 150);
}
function isColorMode(value: string | null): value is ColorMode {
return value === 'type' || value === 'state' || value === 'ahagraph';
}
@ -708,9 +732,10 @@ disown</code>
</button>
</div>
<!-- Explore from this node -->
<!-- Explore from this node — carries the selected memory so the walk
starts from THIS thought, not a re-seed from the newest memory. -->
<a
href="{base}/explore"
href={osHref('/explore', { memory: selectedMemory.id })}
class="flex items-center justify-center gap-2 px-3 py-2 rounded-xl bg-dream/10 text-dream-glow text-xs hover:bg-dream/20 transition border border-dream/20"
>
<Icon name="explore" size={14} /> Explore Connections

View file

@ -1,6 +1,11 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
import { base } from '$app/paths';
import { HOME_ROUTE } from '$lib/os-routes';
onMount(() => goto('/graph', { replaceState: true }));
// The VestigeOS home is the Palace (the spatial launcher), NOT /graph. Use the
// base-safe path so /dashboard/ lands on /dashboard/palace — a bare
// goto('/graph') escapes the configured base and 404s on the deployed site.
onMount(() => goto(`${base}${HOME_ROUTE}`, { replaceState: true }));
</script>