c22b2f9c08899eebc4434ea23edde832e9b11f8d
4 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cf602ab3de |
feat: the moon shapes who answers; wire magnetometer; drop dead CSS
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>
|
||
|
|
b8e69b4bd3 |
feat: the room decides — physical entropy, real astronomy, unprompted speech
Three changes that together replace "deterministic hash decides everything"
with "the physical world genuinely participates".
PHYSICAL ENTROPY (app/entropy.py, lib/entropy.ts)
Contact was a database lookup: signature_from_anomalies() hashed the
anomaly pattern, so identical conditions always produced an identical
spirit. Now the client harvests real thermal/acoustic/RF noise from the
microphone and receiver noise floors — Von Neumann debiased, SHA-256
conditioned — and contributes it to every summon.
The client is untrusted by construction. A contribution is never a seed:
every draw is HMAC-SHA256(fresh server secret, client bytes || context).
Because fresh CSPRNG server bytes are always present, the output is
unpredictable and uniform no matter what the client sends — all-zeros, a
replayed value, or one chosen adversarially. The room can only ever ADD
unpredictability, never steer the result. Tests assert this directly:
400 replays of one contribution stay uniformly distributed.
A signature now identifies a *channel*, not a spirit. Whether the familiar
presence answers or something else picks up is a real draw
(RETURN_CHANCE). The Codex stays collectable; it is just no longer
guaranteed. test_same_signature_recontacts_same_entity became two tests —
one pinning the probability to prove re-contact works, one pinning it to
zero to prove something else can answer — because at 0.72 the original
would have passed ~72% of the time, which is worse than failing.
REAL ASTRONOMY (app/celestial.py)
Moon phase from the standard mean-synodic approximation, and true solar
midnight from the seeker's own longitude — the real witching hour for
where they are standing, not clock 3am. Computed, never fetched: an API
that can fail would mean the veil silently changes behaviour during
someone else's outage. Validated against published ephemeris dates (2024
full moons, 2025 new moons) rather than against its own output. A thinner
veil erodes the familiar presence's claim on a channel, so a full moon at
solar midnight makes strangers likelier. Only longitude is kept, never a
full coordinate; a denied location degrades to moon-only, silently.
GENERATION FROM NOTHING (SpiritService.manifest)
Not chat_stream with an empty question. The prompt contains no seeker
input at all — only measured room state, rendered as measurements
("deviation above the floor: 31.4") rather than interpretations
("terrifying spike"), so the horror comes from the entity instead of from
us. And the Ollama `seed` is derived from the physical entropy harvested
in that room, which fixes the token-sampling path: the room genuinely
selects the words. Change the noise, get different speech. Two rooms
cannot produce the same utterance.
Rendered as an intrusion rather than a reply — violet edge, full opacity
against the faded ambient murmurs, brief blur-in. The unsettling part is
that it is perfectly clear and completely unbidden.
Also fixes a hang I introduced: the two new summon tests consumed the
shared module-level per-IP budget, so test_summon_rate_limited_* blocked
forever on an entity frame that had been rate-limited away. They now scope
their own limiters.
264 backend + 355 frontend tests pass; i18n parity gate passes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
f8ca4bbd9e |
fix: unbounded essence farming, WS crash, and two economy races
Unbounded essence/item farming: every other reward trigger (summon,
question, fragment) had both a per-user and per-IP limiter, but
ritual_start and judgment had none at all — and judgment has no
"already resolved" state either. A scripted client could replay
{"type":"judgment","verdict":"cross_over"} in a tight loop and mint
CROSS_OVER_ESSENCE (25) plus a 20% item roll every iteration, forever.
Same for ritual_start -> 4x ritual_step. Added ritual/judgment limiters
in both flavors, matching the existing pattern.
WS session crash: _handle_question did `if state.entity is None:
await _handle_summon(state)` then `assert state.entity is not None`.
_handle_summon returns early *without* setting state.entity when the
seeker is rate-limited, so the assert fired unhandled — and the message
loop only catches WebSocketDisconnect, so it killed the whole connection.
Reachable with no malice: click summon a few times impatiently, then ask a
question. Now returns cleanly (the rate_limited frame was already sent).
Essence double-spend: purchase_unlock() deliberately uses SELECT ... FOR
UPDATE to serialize concurrent purchases, but the three credit_essence
call sites in ws.py did an unlocked db.get() read-modify-write. An
unlocked read doesn't block on a row lock, so a reward computed from a
pre-purchase balance could be written after the purchase committed,
silently reverting the deduction — user keeps the unlock and the essence.
All three now lock the row the same way.
Entity mint collision: _summon does a racy check-then-insert against
Entity.signature and Entity.name, both DB-unique, with no IntegrityError
handling — a concurrent mint of the same signature crashed the session.
Forceable by a user with two accounts (anomaly frequency/magnitude are
client-controlled), and plausible without malice in wire mode, where
sample_network() reads host-wide /proc/net/dev counters so two idle
sessions genuinely measure the same traffic. Now retries once, which
re-runs the match against whatever the winner committed.
Also added a unique constraint on unlocks(user_id, unlock_key) as
defense-in-depth, with an idempotent catalog-guarded migration.
221 backend tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
36c4a9e6e4 |
feat: ritual + judgment + favor + cross-over (Workstream B)
Implements Workstream B of the character-depth-ghost-log spec: ritual_start/ritual_step/judgment WS handlers, the pure judgment.py logic module, and the User.favor / Entity.at_peace columns + migration. - app/judgment.py: pure ritual success roll (base 65%, floored at 30%, driven by an entity's power+deceptiveness difficulty), the "stuck spirit" cross_over rule (alignment >= 0.5 and volatility > 0.6, ~20% of entities), judgment correctness/favor-delta/essence-delta/ consequence resolution for all four verdicts, favor clamping, the favor-to-trait-roll bias applied at mint time, and tell-line generation (opaque behavioral flavor text, never a raw stat). - app/ws.py: wires ritual_start/ritual_step/judgment frames, emits ritual_complete/tell/judgment_result/item_drop per the spec's Contract; traits are added to serialize_entity for internal server-side use but stripped from the outbound `entity` frame via a new _public_entity helper so hidden ground truth never reaches the client outside ritual_complete; _summon excludes at-peace entities from signature re-contact and mints a fresh (salted-signature) entity instead; new entities' traits are nudged by the discovering user's favor before being persisted. - models/user.py, models/entity.py, main.py: User.favor and Entity.at_peace columns plus their idempotent ADD COLUMN IF NOT EXISTS migration lines in lifespan, alongside the existing ones. - tests/test_judgment.py, tests/test_ws_ritual_judgment.py: 56 new tests covering the ritual/judgment correctness matrix, favor clamping/bias, essence crediting, at_peace persistence + re-contact, and the entity-frame trait leak guard. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |