Perception.Ambient context, never identity.
This page describes the perception layer end-to-end: the closed schema, the three gates between a visitor action and a stored count, the privacy invariants the architecture enforces, and the live aggregate the layer is currently producing. Opt-in default-off, aggregate- only, revocable in one click. Phase 6 lands its observers across five sub-PRs; this page is the single place every one of them is audit-able.
01 · Your consent
The toggle below sets a same-origin cookie named v5_perception_consent with a 14-day lifetime and a paired localStorage flag. The cookie is the single gate the edge endpoint checks; without it, any incoming event in any category other than “adoption” is dropped at the door without a KV write. Revoking clears both the cookie and the flag immediately.
02 · What can be collected
Six categories. Each one carries a closed set of bucket labels — the bucket is the level of detail the storage holds. Raw measurements (px/s, ms, scroll positions) are never persisted; the bucketization happens client-side before the event ever reaches the endpoint.
scroll-velocityHow fast the page scrollsAverage pixels per second over a sampling window. Bucketed into four coarse states — idle, browsing, scanning, skimming. The raw px/s number is never persisted.
idlebrowsingscanningskimmingdwell-timeHow long a page is kept openWall-clock milliseconds between page open and page close, bucketed into six progressively wider slots. Granularity widens with time because a 12s vs 14s difference is meaningless and a 12s vs 4min difference is.
0-10s10-30s30-60s60-180s180-600s600s+tab-visibilityHow long the tab spent backgroundedTime the visitor had this tab in the background between focus events. Three slots — short, medium, long. No record is kept of when the focus event fired.
shortmediumlongsection-engagementWhich on-page section drew attentionSection identifiers as they appear in the page source (e.g. hero, projects, contact). The bucket IS the section name; no scroll position, no time spent in section, no order.
(kebab-case section slug)navigation-flowWhich page-to-page transition firedOrdered pair of route slugs (e.g. home>about). Records the transition itself, not the visitor making it. Counts compose with one another into a Markov-shaped graph the operator can read; no individual visitor's path is reconstructible.
(from-slug>to-slug)cognition-signalInferred attention state at the moment of a navigationA three-state qualitative bucket derived from the per-session page counter — arrival on first navigation, exploring through 2-4 routes, engaged from 5 onward. The state never regresses within a session and is computed entirely client-side; only the bucket label reaches the endpoint.
arrivalexploringengagedpacing-transitionHow many navigations the session reached before backgroundingFired exactly once per session via sendBeacon when the tab is first backgrounded — captures the visitor's departure-time depth. Four progressively widening buckets (first / few / many / deep) match the cognition taxonomy's boundaries. No raw count is persisted; the bucket label is the level of detail.
firstfewmanydeepadoptionOpt-in / revoke / deny eventsThe three states the consent decision can take. Recorded WITHOUT a prior consent gate because the decision IS the consent. The only events the layer is allowed to record before consent.
opt_in_grantedopt_in_revokedopt_in_denied
03 · How it works
Every recorded count passes through three gates in a fixed order. The endpoint short-circuits at the first gate that fails — a request that crosses none of them still resolves with the same 204 No Content as a request that crosses all three, so the presence-or-absence of telemetry is never an attack surface a network observer can probe.
- Gate 1 · operator master switch
process.env[V5_PERCEPTION_ENABLED] === "1"Cheapest check. Without this env set on the deployment, the endpoint silently no-ops every event before reading the body. The operator can dark-launch the entire perception subsystem by leaving the variable unset, which is the default in production right now.
- Gate 2 · closed schema
isPerceptionCategory ∧ isValidBucketThe inbound
categorymust be one of the eight allow-listed values in section 02. The inboundbucketmust be either a member of that category's closed list (for fixed categories) or pass the kebab-case shape check (for dynamic categories like navigation-flow). Anything else drops at the door. - Gate 3 · visitor consent
Cookie: v5_perception_consent=grantedThe endpoint reads the perception consent cookie from the inbound
Cookieheader. Withoutgrantedthe event drops — except for theadoptioncategory, which bypasses this gate because recording the consent decision cannot itself require prior consent. The bypass is the only exception architecturally, and it's explicit in the endpoint source.
The flow
Every byte that lands in storage is one of the closed bucket labels documented in section 02. The HINCRBY primitive is atomic and stateless — there is no session reference, no timestamp, no identifier anywhere on the persistence path.
Two inference layers above storage
- Cognition signal
- A per-session counter in sessionStorage advances by one on every route mount the client observer sees. The counter maps to one of three states —
arrival(1),exploring(2-4),engaged(5+) — and fires one cognition-signal event per transition. The state never regresses within a session. - Pacing multiplier
- The cognition state plus the OS reduced-motion preference resolve to one of four duration multipliers —
FULL(1.0),MID(0.85),SNAPPY(0.65),STILL(0.0). Reduced-motion is an unconditional override. Animation consumers read the multiplier through a React Context; the layer ships no spring physics by type-system enforcement.
04 · What is never collected
- No IP address, no User-Agent, no Accept-Language, no Referer beyond what the platform logs at the edge.
- No mouse trails, no keystroke timings, no biometric- shaped signals. No session replay tooling.
- No identifier — anonymous or otherwise — minted by this layer. The Lumina session memory (separately documented at /lumina/brain) is the only place visitor state persists, and even that is anonymous + opt-out + a 14-day TTL (operator- configurable to 30 days; see the brain page for the live value).
- No timestamp on individual events. The aggregate hash holds a count, not a sequence.
- No cross-device linking. No third-party trackers. No analytics SDK beyond Vercel Analytics, which itself is cookieless and IP-anonymised at the platform layer.
05 · Privacy invariants
- Aggregate-only
- Every recorded event lands in a count keyed by a bucket label. The bucket is the level of detail the storage holds — nothing else. There is no per-visitor record, no session-scoped aggregate, no time-of-event field.
- No fingerprint
- The endpoint reads no IP, no User-Agent, no Accept-Language, no Referer beyond what Vercel logs at the platform layer. The only header consulted is Cookie, and only the perception consent token within it.
- No identity persistence
- No cross-session identifier is minted. The consent cookie expires after 14 days of inactivity. There is no linkage between this layer and the Lumina session memory (which is separately documented at /lumina/brain).
- Opt-in default-off
- The subsystem is dark unless the operator has flipped V5_PERCEPTION_ENABLED=1 AND the visitor has opted in via the toggle below. Either gate closed means no event records.
- No surfacing
- Nothing about the visitor's perception data is ever shown back to that visitor. The layer is invisible by construction — its output is a public aggregate snapshot that anyone can read, not a personalised message the visitor receives.
- Graceful no-op
- When KV is unavailable, every record helper returns silently and every read helper returns an empty object. The layer never blocks a page render or chat turn.
06 · Retention & aggregation
Aggregated counts live in six Vercel KV hashes — one per category. Each hash maps a bucket label to a count. There is no TTL on the hashes themselves; the counts are cumulative across the lifetime of the layer. The data persisted is, end-to-end, the count itself — nothing else.
Aggregation is monotonically additive. The endpoint increments a single field by 1 per qualifying event; no other write shape exists. There is no decrement, no re-attribution, no per-visitor bucketing. Removing a visitor's contribution to the aggregate is mathematically impossible — but the aggregate also contains no reference to which contributions came from whom, which is the point.
The consent cookie expires after 14 days of inactivity, at which point the visitor returns to the default-OFF state without action.
07 · Live aggregate snapshot
Read from KV at this page's hourly ISR cadence. The number against each bucket is the cumulative count since the perception layer was first enabled. Empty categories below mean no event of that kind has been recorded yet — which, at 6.1 foundation time, is every category that isn't the consent decision itself.
$ perception.snapshot
No events recorded yet.
The subsystem is either dark (V5_PERCEPTION_ENABLED unset), or no visitor has opted in since the layer began recording. Both are valid steady states for Phase 6.1.
08 · Why this exists
Subsequent V5 phases — temporal architecture playback, cinematic topology, operational digital twin — share a need to know the SHAPE of how visitors engage, not the identity of any one visitor. A page that loads fast for someone skimming should still feel cinematic for someone reading; the layer that distinguishes those modes is this one.
The site will never address the visitor about their perception data. There is no “we noticed you spent 8 minutes on architecture” greeting, no “your usual section” section, no implicit profile. The opt-in is a contribution to the aggregate — nothing more.
09 · Related transparency
The perception layer is one of two V5 surfaces that persist any visitor state. The other is the Lumina session memory — the conversation history that the chat embedded across this portfolio uses to maintain context across turns. Each has its own transparency page; together they describe every byte the platform stores about a visit.
10 · Source files
Every claim above is grounded in code. Click any row to read the file on GitHub.
- Schema + bucketslib/v5/perception/buckets.tsCategory allow-list, bucket allow-lists, and the raw → bucket helpers observers call. Eight categories live here as of Sub-PR 6.4.
- Consent resolutionlib/v5/perception/consent.tsEnv master switch + cookie + localStorage helpers. The single source of truth for whether the layer is allowed to record.
- KV record + readlib/v5/perception/telemetry.tsHINCRBY one bucket; HGETALL all eight categories. Graceful no-op when KV is unavailable.
- Edge event endpointapp/api/v5/perception/event/route.tsPOST { category, bucket }. Edge runtime, three gates (env / allow-list / consent), always 204.
- Cognition observercomponents/v5/CognitionAwareNavigationObserver.tsxSub-PR 6.2. Mounted in the root layout. Watches usePathname() and fires cognition-signal + navigation-flow events on transitions, gated on consent.
- Cognition inferencelib/v5/navigation/cognition.tsSub-PR 6.2. The three-state inference (arrival / exploring / engaged) derived from the per-session page counter.
- Pacing providercomponents/v5/PacingProvider.tsxSub-PR 6.3. React Context exposing the duration multiplier. Owns the visibility:hidden beacon that fires the pacing-transition event once per session via sendBeacon.
- Pacing inferencelib/v5/pacing/inference.tsSub-PR 6.3. The cognition + reduced-motion → multiplier resolver. Spring physics is type-system banned.
- Memory TTLlib/v5/memory/ttl.tsSub-PR 6.4. Operator-configurable Lumina session memory TTL (14-30 day range; default 14 = V4 parity).
- Memory adoptionlib/v5/memory/telemetry.tsSub-PR 6.4. The hit / miss / store / opt-out counters that drive the hit-rate tile visible on /lumina/brain.
- This pageapp/v5/perception/page.tsxThe transparency surface itself. The verbatim text above lives in this file; the snapshot in section 07 is read from KV at ISR time.
V5 · Phase 6 · Sensory Awakening·Opt-in default-off·Aggregate-only·Revocable in one click
Phase 6 closes with this page. Five sub-PRs landed the foundation: perception endpoint + schema (6.1), cognition observer (6.2), pacing engine (6.3), memory layer extensions (6.4), and this transparency page (6.5). A 60-90 day observation window now opens before Phase 7 (Temporal Architecture) begins; no further perception surface ships during that window.
