From 7db60794a5bcbfcac6e48c2fd6a9e5ae4d61d7b8 Mon Sep 17 00:00:00 2001 From: shawshank-redemp <212104328+shawshank-redemp@users.noreply.github.com> Date: Wed, 2 Sep 2026 15:45:29 +0530 Subject: [PATCH] Centralize route event reading Signed-off-by: shawshank-redemp <212104328+shawshank-redemp@users.noreply.github.com> --- packages/server/src/capsule/causal-summary.ts | 3 +- packages/server/src/events/predicate-route.ts | 20 ++++++++----- packages/server/src/events/route-of-event.ts | 30 +++++++++++++++++++ packages/server/src/flows/flow-replay-run.ts | 5 ++-- packages/server/src/flows/flow-replay.ts | 11 ++++--- .../src/oracles/propose-consequences.ts | 4 +-- 6 files changed, 54 insertions(+), 19 deletions(-) create mode 100644 packages/server/src/events/route-of-event.ts diff --git a/packages/server/src/capsule/causal-summary.ts b/packages/server/src/capsule/causal-summary.ts index 188299011..c40e40117 100644 --- a/packages/server/src/capsule/causal-summary.ts +++ b/packages/server/src/capsule/causal-summary.ts @@ -1,4 +1,5 @@ import { EventType, PerfMetric, isDevToolingUrl, type ReticleEvent } from '@reticlehq/core'; +import { routeOfEvent } from '../events/route-of-event.js'; /** * The causal summary (Tier 1) — the bounded ~50–100 token block on EVERY act, green included: what the @@ -205,7 +206,7 @@ export function causalSummary( break; } case EventType.ROUTE_CHANGE: - if ('string' === typeof data['pathname']) route = data['pathname']; + route = routeOfEvent(event).routePath; break; case EventType.SIGNAL: pushUnique(signals, data['name']); diff --git a/packages/server/src/events/predicate-route.ts b/packages/server/src/events/predicate-route.ts index d2a1944bf..17c01c0d0 100644 --- a/packages/server/src/events/predicate-route.ts +++ b/packages/server/src/events/predicate-route.ts @@ -5,8 +5,8 @@ * app is right now — and keeping that reconciliation in one place is what makes it readable. */ import { EventType, PredicateKind, type ReticleEvent } from '@reticlehq/core'; -import { str, type EvalResult, type Predicate } from './predicate-eval.js'; - +import { type EvalResult, type Predicate } from './predicate-eval.js'; +import { routeOfEvent } from './route-of-event.js'; /** * Which fact answered a `route` predicate: a navigation inside the window, or where the app is now. * @@ -71,12 +71,16 @@ export function evalRoute( const last = routes.at(-1); const reading: RouteReading | undefined = last !== undefined - ? { - pathname: str(last.data['pathname']) ?? str(last.data['to']) ?? '', - full: `${str(last.data['pathname']) ?? str(last.data['to']) ?? ''}${str(last.data['search']) ?? ''}${str(last.data['hash']) ?? ''}`, - decidedBy: RouteDecidedBy.CHANGE, - data: { ...last.data, decidedBy: RouteDecidedBy.CHANGE }, - } + ? (() => { + const route = routeOfEvent(last); + + return { + pathname: route.routePath, + full: route.full, + decidedBy: RouteDecidedBy.CHANGE, + data: { ...last.data, decidedBy: RouteDecidedBy.CHANGE }, + }; + })() : currentUrl === undefined || 0 === currentUrl.length ? undefined : readCurrentRoute(currentUrl); diff --git a/packages/server/src/events/route-of-event.ts b/packages/server/src/events/route-of-event.ts new file mode 100644 index 000000000..6dc23c01f --- /dev/null +++ b/packages/server/src/events/route-of-event.ts @@ -0,0 +1,30 @@ +import { type ReticleEvent } from '@reticlehq/core'; //Bring me the event definitions from Reticle's core package +import { str } from './predicate-eval.js'; + +export interface RouteEventReading { + //blueprint of how my RouteEventReading would look like + routePath: string; + docPath: string; + hash: string; + search: string; + full: string; +} +//Create a function called routeOfEvent. It accepts one route-change event as input, +// and it will return route information in the form of routePath, docPath, hash, search, and full +export function routeOfEvent(event: ReticleEvent): RouteEventReading { + const pathname = str(event.data['pathname']) ?? str(event.data['to']) ?? ''; + const search = str(event.data['search']) ?? ''; + const hash = str(event.data['hash']) ?? ''; + + const docPath = pathname; + const routePath = hash.startsWith('#/') ? hash.slice(1) : pathname; + const full = `${docPath}${search}${hash}`; + + return { + routePath, + docPath, + hash, + search, + full, + }; +} diff --git a/packages/server/src/flows/flow-replay-run.ts b/packages/server/src/flows/flow-replay-run.ts index 8ce446408..3e0074b46 100644 --- a/packages/server/src/flows/flow-replay-run.ts +++ b/packages/server/src/flows/flow-replay-run.ts @@ -11,6 +11,8 @@ import { type FlowStepResult, type ReticleEvent, } from '@reticlehq/core'; + +import { routeOfEvent } from '../events/route-of-event.js'; import { asString } from '../tools/tools-helpers.js'; import { replayFlow } from './flow-replay.js'; import { assertSuccess, dynamicTestids, successLabel, SUCCESS_STEP_TOOL } from './flow-success.js'; @@ -122,8 +124,7 @@ export function startPathMismatchHint( if (startPath === undefined || 0 === startPath.length) return undefined; const routes = session.eventsSince(0).filter((e) => e.type === EventType.ROUTE_CHANGE); const last = routes.at(-1); - const data = last?.data ?? {}; - const current = asString(data['pathname']) ?? asString(data['to']); + const current = last === undefined ? undefined : routeOfEvent(last).full; if (current === undefined || current === startPath) return undefined; return `this flow's journey starts on ${startPath} but the tab is on ${current} — navigate there (reticle_navigate { url: "${startPath}" }), then replay`; } diff --git a/packages/server/src/flows/flow-replay.ts b/packages/server/src/flows/flow-replay.ts index 35d8023a1..6f3965129 100644 --- a/packages/server/src/flows/flow-replay.ts +++ b/packages/server/src/flows/flow-replay.ts @@ -1,4 +1,5 @@ import { span } from '../trace.js'; +import { routeOfEvent } from '../events/route-of-event.js'; import { AnchorKind, DriftReason, @@ -273,9 +274,8 @@ function currentRoute(session: FlowReplaySession): string | undefined { const routes = session.eventsSince(0).filter((e) => e.type === EventType.ROUTE_CHANGE); const last = routes.at(-1); if (last === undefined) return undefined; - const data = last.data ?? {}; - const pathname = asString(data['pathname']) ?? asString(data['to']); - return pathname !== undefined && pathname.length > 0 ? pathname : undefined; + const route = routeOfEvent(last); + return route.routePath.length > 0 ? route.routePath : undefined; } /** Pathname only (drop origin + query) so a net URL stays terse in the journey. */ @@ -296,9 +296,8 @@ function summarizeConsequence(events: ReticleEvent[]): string | undefined { const parts: string[] = []; const lastRoute = events.filter((e) => e.type === EventType.ROUTE_CHANGE).at(-1); if (lastRoute !== undefined) { - const data = lastRoute.data ?? {}; - const to = asString(data['pathname']) ?? asString(data['to']); - if (to !== undefined && to.length > 0) parts.push(`→ ${to}`); + const route = routeOfEvent(lastRoute); + if (route.routePath.length > 0) parts.push(`→ ${route.routePath}`); } const signals = new Set(); for (const e of events) { diff --git a/packages/server/src/oracles/propose-consequences.ts b/packages/server/src/oracles/propose-consequences.ts index beeda53f3..bb059ba22 100644 --- a/packages/server/src/oracles/propose-consequences.ts +++ b/packages/server/src/oracles/propose-consequences.ts @@ -1,5 +1,5 @@ import { EventType, type ReticleEvent, PredicateKind } from '@reticlehq/core'; - +import { routeOfEvent } from '../events/route-of-event.js'; /** * Self-generating oracles, v1. Given the window a recording captured, propose ranked mustHold * predicates — the practical answer to "you recorded a flow, now what should it assert?". Ranking @@ -87,7 +87,7 @@ export function proposeConsequences(events: readonly ReticleEvent[]): ProposedCo break; } case EventType.ROUTE_CHANGE: { - const to = data['pathname']; + const to = routeOfEvent(event).routePath; if ('string' === typeof to) { add(`route:${to}`, { predicate: { kind: PredicateKind.ROUTE, to },