MOON INFLUENCE ON SUMMONING
Astronomy previously only decided whether a channel's familiar spirit
returned. It now shapes *who comes through*:
- Rarity skews with real moon illumination. At full moon the rare and
mythic weights roughly triple while common recedes, so a mythic
summoning becomes a reason to go out on the right night rather than a
flat lottery. Deliberately a skew and never a gate — every tier stays
reachable on every night, because someone who can only play midweek
should not be locked out of the good spirits.
- Hidden traits take a small moonlit nudge: power and volatility rise,
alignment drifts slightly darker. Capped at 0.12 and clamped to [0,1],
so a full moon intensifies what a spirit already is instead of
rewriting it. Deceptiveness is untouched — whether a spirit lies is its
own nature, not the sky's doing.
- The mint prompt is told the phase, and explicitly told the entity must
never mention or seem aware of it. It shapes who they are, not their
dialogue; a ghost remarking on the moonlight would break the illusion
instantly.
Tests assert the outcomes shift in practice (mythic rate over 4000 draws,
rare-tier counts across 300 fallback profiles), not merely that the code
runs. test_mint_prompt_never_receives_traits now allows `sky` while still
forbidding `traits`: moon phase is public, observable state anyone can look
up, hidden ground truth is not.
GEOMAGNETIC (app/geomagnetic.py)
Real NOAA SWPC planetary K-index, verified against the live endpoint —
which caught a real bug: I had written the parser against an
array-of-arrays shape, and the actual feed serves a list of objects
(`estimated_kp` float, `kp_index` int, `kp` a display string with a letter
suffix). Fixed, and the tests now use the real captured shape. Cached,
never blocking, and a failed refresh keeps serving the last real value —
an hour-old genuine measurement beats nothing, and geomagnetic conditions
do not change fast enough for that to mislead.
MAGNETOMETER WIRED
MagnetometerListener existed but was never connected. The EMF panel now
runs it alongside the motion listener where the hardware exists, so the
"EMF meter" measures actual magnetic field in µT rather than only
inferring disturbance from movement. Additive: the motion path is
untouched and remains the only option on iOS. Its field jitter also feeds
the entropy pool.
DEAD CODE
Removed .evp-scope and .radio-waterfall, orphaned when both panels moved to
the shared SpectrumScope. Audited every other flagged export first and left
them alone — they are used internally, and "not imported elsewhere" is not
the same as dead.
Adds docs/CHANNELS.md recording what each channel measures and, honestly,
what has actually been verified against hardware versus only written
carefully.
311 backend + 355 frontend tests pass; i18n parity gate passes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
5.5 KiB
Channels to the dead — what is real, and how real
Every channel below measures something physically real. None of them simulate, interpolate, or invent a signal. This document exists so that claim stays honest as the app grows: if you add a channel, add a row, and be specific about what it actually measures and what has actually been verified.
The rule: the app may interpret a measurement however the fiction likes ("the air is thick tonight"), but it must never fabricate the measurement itself. A reading shown to a seeker is a reading something took.
Channels
| Channel | Measures | Platform | Verified? |
|---|---|---|---|
| Microphone (EVP) | Live FFT of room audio via AnalyserNode |
Everything incl. iOS | Yes — live |
| Motion (EMF) | Accelerometer + gyroscope via DeviceMotion | Everything incl. iOS | Yes — live |
| Magnetometer | True magnetic field, µT, via Generic Sensor API | Chromium/Android | Code + unit tests; no hardware test |
| Bluetooth | BLE advertisement RSSI | Chromium desktop/Android | Code + unit tests; no hardware test |
| RTL-SDR | RF spectrum 88–108 MHz via WebUSB | Chromium desktop/Android | Code + unit tests; unverified against a real dongle |
| Network (wire) | Real throughput/latency/jitter counters | Server-side | Yes — live |
| ESP32 node | Temperature, pressure, mic level, radar presence | LAN → HTTP | Firmware written; never flashed to hardware |
| Astronomy | Moon phase, true solar midnight | Computed, offline | Yes — validated against published ephemerides |
| Geomagnetic | NOAA planetary K-index | WAN → NOAA SWPC | Yes — verified against the live endpoint |
Platform walls that cannot be coded around
- iOS has no WebUSB — the RTL-SDR cannot work on iPhone. Not a bug, not a polyfill away. Apple does not ship the API and every iOS browser is WebKit underneath.
- iOS has no Web Bluetooth and no Generic Sensor API — same reason.
- No browser can scan WiFi. There is no
navigator.wifion any platform. Browsers deliberately withhold AP lists and RSSI because it is a location-inference vector. WiFi spectrum is only available from the ESP32.
On iOS the working channels are the microphone, motion, network, astronomy, geomagnetic, and anything the ESP32 reports over the LAN.
Randomness: the room decides
app/entropy.py + frontend/src/lib/entropy.ts
Contact used to be a database lookup — signature_from_anomalies() hashed
the anomaly pattern, so identical conditions always produced an identical
spirit. Now the client harvests real physical noise (microphone and RF
noise floors — thermal noise in the ADC, room acoustics, atmospheric RF),
Von Neumann debiases it, conditions it with SHA-256, and contributes it to
every summon.
The client is untrusted by construction. A contribution is never used as a seed. Every draw is:
HMAC-SHA256(fresh server secret, client bytes || context)
Because fresh CSPRNG server bytes are present on every single call, the
output is unpredictable and uniformly distributed no matter what the
client sends — all-zeros, a replayed value, or one chosen adversarially.
The room can only ever add unpredictability; it can never steer a
result. tests/test_entropy.py asserts this directly rather than assuming
it: 400 replays of one contribution stay uniformly distributed.
If you add a new draw, use veil_random(contribution, context) with a
distinct context string. Domain separation is why learning an entity's
visible rarity tells you nothing about its hidden alignment.
Generation from nothing
SpiritService.manifest()
Unprompted speech is deliberately not chat_stream with an empty
question. Two things make it generation from something rather than a
reply to something:
-
No seeker input in the prompt at all. The model's only stimulus is measured room state, rendered as measurements (
deviation above the floor: 31.4) rather than interpretations (terrifying spike). Putting the conclusion in the prompt would mean the horror came from us instead of from the entity. -
The sampling seed is physical. Ollama's
seedoption fixes the token-sampling path, and it is derived from entropy harvested in that room. The room genuinely selects the words. Change the noise, get different speech; two rooms cannot produce the same utterance.
Tuning knobs
These are taste calls, not derived constants. They are the first things to adjust if the feel is wrong:
| Constant | File | Meaning |
|---|---|---|
RETURN_CHANCE |
app/ws.py |
Odds a channel's familiar spirit answers (0.72) |
VEIL_THINNESS_PULL |
app/ws.py |
How much a thin veil favours strangers (0.45) |
MANIFEST_CHANCE_ON_ANOMALY |
app/ws.py |
Odds a room-shift pulls unprompted speech (0.28) |
VOICE_ARCHETYPES |
app/entities.py |
The eight throats a spirit can be drawn with |
If you add a channel
- Measure something real. If you cannot, do not add it.
- Put the pure maths in its own module with a rolling-baseline core, and
unit-test that core without any hardware object. See
lib/coldSpot.ts,lib/bluetooth.ts,lib/magnetometer.ts— they share one shape deliberately, because sensor intervals are irregular and a fixed per-sample alpha would weight a burst and a long gap identically. - Feed its frames into the entropy pool. More real noise is strictly better.
- Add a row to the table above, and be honest in the "Verified?" column. "Written carefully" is not "tested against hardware."