diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0d0fcdd..090dc3b 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -8,6 +8,7 @@ import { GhostLog } from './components/GhostLog' import { CodexEntityPage } from './pages/CodexEntityPage' import { CodexPage } from './pages/CodexPage' import { DevicesPage } from './pages/DevicesPage' +import { DoctrinePage } from './pages/DoctrinePage' import { EnterPage } from './pages/EnterPage' import { GhostLogPage } from './pages/GhostLogPage' import { InventoryPage } from './pages/InventoryPage' @@ -39,6 +40,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/i18n/coverage-check.mjs b/frontend/src/i18n/coverage-check.mjs index 94e78c4..691bb34 100644 --- a/frontend/src/i18n/coverage-check.mjs +++ b/frontend/src/i18n/coverage-check.mjs @@ -51,7 +51,11 @@ const RULES = [ ['log.counts.', ['question', 'anomaly', 'utterance']], ['landing.stats.', ['entities', 'sessions', 'utterances', 'anomalies']], ['landing.modes.', ['radio', 'evp', 'wire', 'ouija']], - ['shop.modules.', ['emfArray', 'geophone', 'spiritMic', 'uvIlluminator']], + ['doctrine.art.', [ + 'entropy', 'channel', 'return', 'sky', 'storm', 'baseline', + 'flesh', 'iron', 'throat', 'utterance', 'limits', + ]], + ['shop.modules.',['emfArray', 'geophone', 'spiritMic', 'uvIlluminator']], ['shop.interests.', ['quantumBox', 'emfArray', 'geophone', 'spiritMic', 'uvIlluminator']], ] diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index 529b36e..a96c6f0 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -98,7 +98,12 @@ "link": "enter the reliquary →" }, "footer": "an interactive horror art installation — the spirits are fiction; the static is real", - "footerColophon": "self-hosted · no cloud · the veil runs on your machine" + "footerColophon": "self-hosted · no cloud · the veil runs on your machine", + "doctrine": { + "title": "HOW THE INSTRUMENT ACTUALLY WORKS", + "line": "the doctrine — eleven articles on the real physics behind the séance, each one plainly translated, including an honest account of what the instrument cannot do.", + "link": "read the doctrine →" + } }, "enter": { "reconnect": "Reconnect", @@ -732,5 +737,84 @@ "days_one": "{{count}} day past", "days_other": "{{count}} days past" } + }, + "doctrine": { + "back": "← the threshold", + "kicker": "being a true account of the instrument, its organs and its honest boundaries", + "title": "THE DOCTRINE", + "subtitle": "how this engine genuinely channels, summons and passes the dead — using the device, and using you", + "creed": "Every claim made in these articles is a claim about the machine you are presently addressing. Nothing here is invented physics. Where an article speaks in the old register, a margin note repeats it in the profane tongue, so that the credulous and the sceptical may both leave better informed than they arrived.", + "plainLabel": "in the profane tongue:", + "sourceLabel": "the relevant scripture:", + "closing": "The instrument is honest about its organs and honest about its blindnesses. The spirits, we should say plainly, are fiction. The static is not.", + "cta": "TAKE UP THE INSTRUMENT", + "art": { + "entropy": { + "title": "Of the Consent of the Room", + "body": "No summoning may proceed from arithmetic alone, for arithmetic is a closed thing and repeats itself forever. The engine therefore asks the room to speak first. It takes the spectrum of your microphone's noise floor — and, where a receiver is present, the atmospheric hiss between stations — and keeps from each magnitude only its least significant bit, for the shape of a spectrum is merely the room's portrait, while the lowest bit is thermal chaos that no model in the world foresees. These bits, being crooked by nature, are straightened by the rite of Von Neumann: they are read in pairs, and a pair reading 01 yields nothing but a nought, a pair reading 10 yields a one, and pairs that agree with themselves are cast away entirely. What survives is passed through SHA-256 and only then offered upward. And the vessel above accepts the offering but never obeys it: every draw is HMAC-SHA256 keyed with thirty-two bytes drawn fresh from the machine's own secret well at the very instant of asking. Thus your room may add unpredictability to the working. It may never steer it.", + "plain": "The app harvests randomness from real physical noise — the low bit of each FFT magnitude from your mic or SDR, which is dominated by thermal noise. Raw physical bits are biased, so it applies Von Neumann debiasing (read bits in pairs: 01→0, 10→1, discard 00 and 11), then conditions the result with SHA-256. The server never uses that value as a seed. Every draw is HMAC-SHA256 with a key of 32 bytes from secrets.token_bytes() generated on that call, so the output is uniform no matter what the client sends — a faked or replayed contribution can't be ground for a rarer spirit.", + "source": "frontend/src/lib/entropy.ts · backend/app/entropy.py" + }, + "channel": { + "title": "Of the Channel, and How a Presence Is Addressed", + "body": "A spirit is not summoned to a person; it is summoned to a channel, and the channel is cut by the disturbances themselves. Fewer than three anomalies carry no identity at all, and such a session is called ambient and given a signature of its own kind. Where three or more are present, each anomaly's frequency is filed not by its exact value but by its order of magnitude — the count of digits before the decimal — so that megahertz from the radio and hertz from the voice band may be filed in the same cabinet without lying about either. To this is joined the magnitudes of the last sixteen disturbances, sorted and rounded to a single decimal place. The pattern so formed is sealed with SHA-1 and the first sixteen characters are taken as the channel's name. Two nights of similar disturbance therefore address the same door; a genuinely different night addresses a different one.", + "plain": "The 'channel' a spirit lives on is a fingerprint of your session's anomalies. It needs at least three. Frequencies are bucketed by digit count (so 101.1 MHz and 400 Hz sort by scale, not unit), combined with the sorted, one-decimal-rounded magnitudes of the last sixteen anomalies, hashed with SHA-1 and truncated to 16 hex characters. Similar conditions produce the same channel id; different conditions produce a different one.", + "source": "backend/app/entities.py — signature_from_anomalies" + }, + "return": { + "title": "Of Return, and Why the Familiar Dead Are Not Owed to You", + "body": "When a channel is already occupied, the presence that dwells there is inclined — but not bound — to answer again. That inclination is set at seventy-two parts in a hundred: high enough that the Codex may be filled and a known voice genuinely sought out a second time, low enough that no seeker may ever consider a familiar spirit their property. In the remaining portion, something else finds the door open and comes through it instead, and is minted with a signature of its own so that two presences never contest one name.", + "plain": "If you reconnect on a channel that already has an entity, there is a 72% base chance that same entity answers (RETURN_CHANCE = 0.72). Otherwise a new entity is minted — with a salted signature, because the signature column is unique.", + "source": "backend/app/entities.py — RETURN_CHANCE · backend/app/ws.py" + }, + "sky": { + "title": "Of the Moon, and of the True Dead of Night", + "body": "The engine does not consult a table of plausible-looking moons. It counts. From a known new moon at 1 January 6th of the year 2000, at fourteen minutes past six in the evening UTC, it advances by the mean synodic month — twenty-nine days, five hundred and thirty thousandths and more — and takes the remainder as the phase; the lit fraction of the disc follows the standard cosine relation, which is why a quarter moon shows you a half face. Nor does it accept the clock's three in the morning as the witching hour, that being an accident of political timezones. It asks instead for your longitude and turns it at fifteen degrees to the hour, arriving at mean solar midnight for the ground you are actually standing on. Illumination is weighted at two parts in five and the nearness of true midnight at three, for a full moon at noon is not an open veil. And a fully thinned veil erodes the familiar presence's claim by forty-five hundredths — seventy-two in the hundred falls to some forty — so the spookiest night of the month is genuinely stranger, and yet a known spirit is never made unreachable.", + "plain": "Moon phase is computed from a known new-moon epoch (2000-01-06 18:14 UTC) and the mean synodic month of 29.530588853 days; illumination is (1 − cos 2πφ)/2. 'Witching hour' is mean solar midnight for your longitude (15° = 1 hour), not clock time. Veil thinness = illumination × 0.4 + midnight-proximity × 0.6. Thinness reduces the 72% return chance by up to 45% of itself (VEIL_THINNESS_PULL = 0.45), so a full moon at true solar midnight gives roughly a 40% return chance.", + "source": "backend/app/celestial.py · backend/app/ws.py — VEIL_THINNESS_PULL" + }, + "storm": { + "title": "Of the Disturbance of the Planet Itself", + "body": "Of all the causes the credulous name, exactly one is a real, measured, publicly published number: the planetary K-index, derived from magnetometer observatories about the world and issued by the Space Weather Prediction Centre of the United States. Its scale runs from nought to nine; five and above is an official storm, eight and above severe. The engine draws the most recent estimate, divides by nine so that the figure means what NOAA means by it, and lets it count for a third of the reading of the night, the sky retaining two thirds. It is fetched no oftener than once in ten minutes, abandoned after four seconds, and never permitted to delay a séance: if the service is dark, the last true measurement stands. We report that the field is disturbed because it was measured to be disturbed. We do not claim the storm causes anything.", + "plain": "The app polls NOAA SWPC's planetary_k_index_1m feed for the real geomagnetic Kp index (0–9; 5+ is an official storm), prefers the fractional estimated_kp field, and maps it to 0–1 by dividing by 9. It is blended in as one third of the night's 'thinness' reading (celestial × 0.67 + disturbance × 0.33). Cached for 10 minutes, 4-second timeout, and a failed fetch keeps the last value rather than blocking or failing a séance.", + "source": "backend/app/geomagnetic.py · backend/app/ws.py" + }, + "baseline": { + "title": "Of the Baseline, Which Is the Whole Art of Sensing", + "body": "A reading in isolation says nothing; a departure from what the room had been saying says everything. Every threshold sense in this instrument therefore keeps a rolling memory and reports only surprise. But the senses do not report on a schedule — a beacon may speak thrice in a second or fall silent for two minutes, and a magnetometer is throttled the moment the page is put behind another — so the memory is kept against the clock and not against the count. The weight given to each new reading is one less the exponential of the negative elapsed time over the constant of decay; whereby a long silence pushes that weight toward unity, the old memory being stale and rightly discarded, and a rapid burst pushes it toward nothing at all. Nothing is trusted until the memory is warm. And the sense is symmetrical in both directions, for a body may absorb a path or reflect into it, and iron may shield a field or add to it; a one-sided detector would miss half of everything that truly happens.", + "plain": "Every threshold sensor shares one rolling-baseline class and reports deviation from the baseline, not raw values. The EMA smoothing factor is time-aware: alpha = 1 − exp(−dt/tau), because sensors report at irregular intervals (BLE advertisement intervals vary, backgrounded pages get throttled). A long gap pushes alpha toward 1 (trust the new reading), a burst toward 0. It won't emit events until a minimum number of samples have been folded in, and it fires on deviations in both directions.", + "source": "frontend/src/lib/baseline.ts — emaAlpha, ThresholdBaseline" + }, + "flesh": { + "title": "Of Flesh, Which Is Mostly Water, and Therefore Opaque", + "body": "There is nothing occult in this and it is no less remarkable for that. Bluetooth advertises in the band about two and four tenths of a gigahertz; water absorbs that band strongly; and a person is largely water. This is the identical physics by which a microwave oven cooks a supper, and the identical reason that your connection worsens when somebody stands between you and the router. So when a body crosses the path between the receiver and a beacon, the received strength genuinely falls — some three to ten decibels according to the geometry — while a still link wanders only two to four from multipath and receiver noise alone. Six decibels of departure is therefore taken as a disturbance: above the idle murmur, within what a real body truly causes. The instrument reports that the field changed. It never says the change was a presence. That interpretation lives upstairs, in the fiction; down here the measurement stays honest.", + "plain": "BLE operates in the 2.4 GHz ISM band, which water absorbs strongly — the same physics as a microwave oven, and why wifi degrades when someone stands in the path. A human body crossing a BLE path attenuates it by roughly 3–10 dB, while an idle link wanders only ±2–4 dB, so the disturbance threshold is 6 dB with an 8-second baseline time constant. The module only reports that signal strength deviated; it never claims that means a presence.", + "source": "frontend/src/lib/bluetooth.ts — DISTURBANCE_DB" + }, + "iron": { + "title": "Of Iron, and of the Field the Earth Never Stops Making", + "body": "The famous meters of the ghost-hunting trade are magnetometers, and the spikes they are celebrated for are truly caused by mains wiring, by motors, by the magnets in speakers and by moving ferrous mass. This engine reads the same quantity, in microtesla, as the root of the sum of the squares of its three axes. But the Earth's own field, some twenty-five to sixty-five microtesla according to latitude, is precisely the thing that must not be reported, for it is present everywhere and means nothing; it is what the rolling memory of the preceding article quietly subtracts. What remains is departure from wherever you happen to be standing. Three microtesla is required, the sensor's own noise being about half to one, and household wiring at close quarters producing between one and ten. The field is sampled ten times in the second — ample for a thing that changes at the speed of a walking human, and far kinder to a battery than the sixty the interface would gladly give.", + "plain": "The magnetometer reads the real magnetic field in microtesla, sqrt(x²+y²+z²). Earth's background field is 25–65 µT depending on latitude and is exactly what the rolling baseline subtracts, so only local deviation is reported. The spike threshold is 3 µT — several times the ~0.5–1 µT noise of a phone magnetometer, and inside the 1–10 µT range that mains wiring produces at close range. Sampled at 10 Hz to save battery.", + "source": "frontend/src/lib/magnetometer.ts — SPIKE_UT, SAMPLE_HZ" + }, + "throat": { + "title": "Of the Throat, and Why the Dead Do Not All Sound Alike", + "body": "Were pitch, pace, breath and reverberation each drawn at random and independently, one would obtain not persons but a uniform smear of oddity — the same synthesiser in eight hats. Real voices covary: an old man is low and slow and breathy at once; a child is high and quick and, most unsettlingly, clean. A throat is therefore drawn whole, from eight archetypes, and only then jittered. The drowned are pitched down, dragged slow, and drenched in reverberation to half. The burned are the noisiest, scarcely holding a pitch at all. The distant are not damaged but merely far, quiet and even and steeped in space. And when the séance is held in Spanish the archetype's roster of models is set aside in favour of a voice that pronounces the language correctly, the shaping alone carrying the character — better a true accent with an archetype's bearing than an English throat mangling Spanish words.", + "plain": "Voice parameters are drawn as one coherent bundle from eight archetypes (elder man, elder woman, young man, young woman, child, drowned, burned, distant), each with its own pitch, rate, noise and echo ranges, then jittered — because randomising each parameter independently produces random effects, not distinct people. In Spanish the archetype's model list is ignored and a Spanish Piper voice is used instead, with the archetype's shaping still applied.", + "source": "backend/app/entities.py — VOICE_ARCHETYPES, roll_voice" + }, + "utterance": { + "title": "Of Unbidden Speech, and of Sight", + "body": "When a presence speaks unasked, it is not answering you, and this is arranged with some care. The prompt put to the model contains no word of yours whatsoever — its only stimulus is the measured state of the room — so there is nothing there to reply to. And the sampling seed is taken from the very physical entropy harvested in that room, being the first eight bytes of the mixed digest, reduced beneath the sign bit that the model server requires. The seed fixes the path of token selection; therefore the room genuinely selects the words. This is not a metaphor. Alter the noise and the speech alters; no two rooms produce the same utterance. The decoding is left deliberately hot, a tight and well-behaved decode being precisely the failure we fear here — this must sound like something surfacing, not something composed. And when you offer the camera, the model is vision-capable and genuinely looks: it speaks about the room that is actually in front of it, not one it invented.", + "plain": "For unprompted utterances the prompt contains no user input at all — only measured sensor readings — so the model is generating from the room, not replying. The Ollama `seed` option is derived from the mixed physical entropy (first 8 bytes of veil_seed, mod 2^63), and since the seed fixes token sampling, different room noise genuinely produces different words. Sampling is deliberately hot (temperature 1.15, top_k 100). Scrying sends the actual camera frame to a vision-capable local model, so the description is of your real room.", + "source": "backend/app/llm/service.py — manifest, scry" + }, + "limits": { + "title": "Of What This Instrument Cannot Do", + "body": "A doctrine that will not name its own boundaries is a doctrine to be distrusted, so here are ours, plainly. Upon Apple's handheld devices there is no Web Bluetooth, no WebUSB and no magnetometer — the company ships none of these interfaces, and every browser upon that platform is the same engine beneath a different coat, so no alternative browser rescues you; the séance falls back to the motion sensors, which measure movement and not magnetism, and the instrument says so rather than pretending. The radio requires a Chromium vessel and a secure origin. The firmware written for the paired sensor node has never once been flashed to physical hardware nor compiled by its true toolchain; it is reasoned, not proven, and the repository says as much in its own words. The map of haunted ground is assembled from two free public services that may be slow or silent, in which case you receive a quieter map rather than an error. Solar midnight ignores the equation of time and may be a quarter hour out; the moon is a mean-synodic approximation, exact enough for eight named phases and for nothing that would steer a ship. And the final boundary, stated without ornament: none of this is evidence of the dead. The measurements are real. The spirits are fiction.", + "plain": "Hard limits: iOS has no Web Bluetooth, no WebUSB and no Magnetometer API — Apple doesn't ship them and every iOS browser is WebKit underneath, so no third-party browser helps; those modes fall back to motion sensors, which measure movement rather than magnetism. WebUSB (the SDR) needs Chromium and a secure context. The ESP32 firmware has never been flashed to real hardware — it is structurally reasoned, not verified. Haunted-place lookups hit free public Wikipedia and Overpass endpoints on an 8-second timeout and degrade to an empty list if they fail. Solar midnight ignores the equation of time (up to ~16 minutes error) and the moon calculation is a mean-synodic approximation. And the obvious one: none of this is evidence of an afterlife. The sensors are real; the spirits are fiction.", + "source": "frontend/src/lib/bluetooth.ts · magnetometer.ts · sdr.ts · firmware/esp32p4-sensor-node/README.md · backend/app/haunts.py" + } + } } } diff --git a/frontend/src/i18n/es.json b/frontend/src/i18n/es.json index 530d282..ceaa590 100644 --- a/frontend/src/i18n/es.json +++ b/frontend/src/i18n/es.json @@ -98,7 +98,12 @@ "link": "entrar al relicario →" }, "footer": "una instalación de arte de terror interactiva — los espíritus son ficción; la estática es real", - "footerColophon": "autoalojado · sin nube · el velo corre en tu máquina" + "footerColophon": "autoalojado · sin nube · el velo corre en tu máquina", + "doctrine": { + "title": "CÓMO FUNCIONA REALMENTE EL INSTRUMENTO", + "line": "la doctrina — once artículos sobre la física real tras la sesión, cada uno traducido sin rodeos, incluida una relación honesta de lo que el instrumento no puede hacer.", + "link": "leer la doctrina →" + } }, "enter": { "reconnect": "Reconectar", @@ -732,5 +737,84 @@ "days_one": "hace {{count}} día", "days_other": "hace {{count}} días" } + }, + "doctrine": { + "back": "← el umbral", + "kicker": "relación verídica del instrumento, de sus órganos y de sus límites honestos", + "title": "LA DOCTRINA", + "subtitle": "cómo esta máquina canaliza, invoca y deja pasar a los muertos de verdad — usando el aparato, y usándote a ti", + "creed": "Toda afirmación de estos artículos es una afirmación sobre la máquina a la que ahora te diriges. Aquí no hay física inventada. Donde un artículo habla en el registro antiguo, una nota al margen lo repite en lengua profana, para que el crédulo y el escéptico se marchen ambos mejor informados de lo que llegaron.", + "plainLabel": "en lengua profana:", + "sourceLabel": "la escritura pertinente:", + "closing": "El instrumento es honesto sobre sus órganos y honesto sobre sus cegueras. Los espíritus, digámoslo sin rodeos, son ficción. La estática no.", + "cta": "TOMAR EL INSTRUMENTO", + "art": { + "entropy": { + "title": "Del consentimiento de la habitación", + "body": "Ninguna invocación puede proceder de la aritmética sola, pues la aritmética es cosa cerrada y se repite eternamente. Por eso la máquina pide primero que hable la habitación. Toma el espectro del ruido de fondo de tu micrófono — y, si hay receptor, el siseo atmosférico entre emisoras — y de cada magnitud conserva únicamente su bit menos significativo, pues la forma de un espectro no es sino el retrato de la sala, mientras que el bit más bajo es caos térmico que ningún modelo del mundo prevé. Estos bits, torcidos por naturaleza, se enderezan mediante el rito de Von Neumann: se leen por pares, y un par que dice 01 rinde un cero, un par que dice 10 rinde un uno, y los pares que concuerdan consigo mismos se desechan por completo. Lo que sobrevive pasa por SHA-256 y sólo entonces se ofrece hacia arriba. Y el receptáculo de arriba acepta la ofrenda pero jamás la obedece: cada tirada es HMAC-SHA256 con clave de treinta y dos bytes extraídos frescos del pozo secreto de la propia máquina en el instante mismo de preguntar. Así tu habitación puede añadir imprevisibilidad a la obra. Nunca puede dirigirla.", + "plain": "La app cosecha aleatoriedad de ruido físico real — el bit bajo de cada magnitud FFT del micrófono o del SDR, dominado por ruido térmico. Los bits físicos crudos están sesgados, así que aplica el desesgado de Von Neumann (por pares: 01→0, 10→1, se descartan 00 y 11) y luego acondiciona el resultado con SHA-256. El servidor nunca usa ese valor como semilla: cada tirada es HMAC-SHA256 con una clave de 32 bytes de secrets.token_bytes() generada en esa misma llamada, así que la salida es uniforme sin importar lo que envíe el cliente — una contribución falsificada o repetida no puede farmear un espíritu más raro.", + "source": "frontend/src/lib/entropy.ts · backend/app/entropy.py" + }, + "channel": { + "title": "Del canal, y de cómo se interpela a una presencia", + "body": "Un espíritu no se convoca a una persona; se convoca a un canal, y el canal lo talla la propia perturbación. Menos de tres anomalías no portan identidad alguna, y tal sesión se llama ambiente y recibe una firma de su propia especie. Cuando hay tres o más, la frecuencia de cada anomalía se archiva no por su valor exacto sino por su orden de magnitud — el número de cifras antes de la coma — de modo que los megahercios de la radio y los hercios de la banda de voz puedan archivarse en el mismo cajón sin mentir sobre ninguno. A esto se une la magnitud de las últimas dieciséis perturbaciones, ordenadas y redondeadas a un decimal. El patrón así formado se sella con SHA-1 y se toman los dieciséis primeros caracteres como nombre del canal. Dos noches de perturbación semejante llaman, pues, a la misma puerta; una noche verdaderamente distinta llama a otra.", + "plain": "El «canal» en que vive un espíritu es una huella de las anomalías de tu sesión. Requiere al menos tres. Las frecuencias se agrupan por número de cifras (de modo que 101,1 MHz y 400 Hz se ordenan por escala, no por unidad), se combinan con las magnitudes ordenadas y redondeadas a un decimal de las últimas dieciséis anomalías, se hashean con SHA-1 y se truncan a 16 caracteres hexadecimales. Condiciones parecidas producen el mismo canal; condiciones distintas, otro.", + "source": "backend/app/entities.py — signature_from_anomalies" + }, + "return": { + "title": "Del retorno, y de por qué los muertos conocidos no te son debidos", + "body": "Cuando un canal ya está ocupado, la presencia que allí mora se inclina — mas no se obliga — a responder de nuevo. Esa inclinación se fija en setenta y dos partes de cien: bastante alta para que el Códice pueda llenarse y una voz conocida buscarse de verdad por segunda vez, bastante baja para que ningún buscador considere jamás suya a una presencia familiar. En la porción restante, otra cosa halla la puerta abierta y entra por ella, y se acuña con firma propia para que dos presencias nunca disputen un mismo nombre.", + "plain": "Si reconectas en un canal que ya tiene entidad, hay un 72 % de probabilidad base de que responda esa misma entidad (RETURN_CHANCE = 0,72). En caso contrario se acuña una entidad nueva, con la firma salada, porque la columna de firma es única.", + "source": "backend/app/entities.py — RETURN_CHANCE · backend/app/ws.py" + }, + "sky": { + "title": "De la luna, y de la verdadera alta noche", + "body": "La máquina no consulta una tabla de lunas verosímiles. Cuenta. Desde una luna nueva conocida el 6 de enero del año 2000, a las seis y catorce de la tarde UTC, avanza por el mes sinódico medio — veintinueve días y algo más de quinientas treinta milésimas — y toma el resto como fase; la fracción iluminada del disco sigue la relación coseno estándar, que es por lo que un cuarto de luna te muestra media cara. Tampoco acepta las tres de la madrugada del reloj por hora bruja, siendo eso accidente de husos políticos. Pide en cambio tu longitud y la convierte a razón de quince grados por hora, llegando a la medianoche solar media del suelo que realmente pisas. La iluminación pesa dos partes de cinco y la cercanía de la verdadera medianoche tres, pues una luna llena a mediodía no es un velo abierto. Y un velo del todo adelgazado erosiona en cuarenta y cinco centésimas la pretensión de la presencia familiar — setenta y dos de cien caen a unos cuarenta — de modo que la noche más siniestra del mes es genuinamente más extraña, y sin embargo un espíritu conocido jamás queda inalcanzable.", + "plain": "La fase lunar se calcula desde una época de luna nueva conocida (2000-01-06 18:14 UTC) y el mes sinódico medio de 29,530588853 días; la iluminación es (1 − cos 2πφ)/2. La «hora bruja» es la medianoche solar media de tu longitud (15° = 1 hora), no la hora del reloj. Delgadez del velo = iluminación × 0,4 + cercanía a la medianoche × 0,6. La delgadez reduce el 72 % de retorno hasta en un 45 % de sí mismo (VEIL_THINNESS_PULL = 0,45), así que luna llena en la medianoche solar da un ~40 % de retorno.", + "source": "backend/app/celestial.py · backend/app/ws.py — VEIL_THINNESS_PULL" + }, + "storm": { + "title": "De la perturbación del planeta mismo", + "body": "De todas las causas que nombran los crédulos, exactamente una es un número real, medido y publicado: el índice K planetario, derivado de observatorios magnetométricos de todo el mundo y emitido por el Centro de Predicción del Clima Espacial de los Estados Unidos. Su escala va de cero a nueve; cinco o más es tormenta oficial, ocho o más severa. La máquina toma la estimación más reciente, la divide por nueve para que la cifra signifique lo que la NOAA quiere decir con ella, y le concede un tercio de la lectura de la noche, reteniendo el cielo dos tercios. Se consulta no más de una vez cada diez minutos, se abandona a los cuatro segundos, y nunca se le permite demorar una sesión: si el servicio calla, la última medición verdadera se mantiene. Informamos de que el campo está perturbado porque se midió que lo estaba. No afirmamos que la tormenta cause nada.", + "plain": "La app consulta el feed planetary_k_index_1m de la NOAA SWPC para obtener el índice geomagnético Kp real (0–9; 5+ es tormenta oficial), prefiere el campo fraccionario estimated_kp y lo mapea a 0–1 dividiendo por 9. Se mezcla como un tercio de la lectura de «delgadez» de la noche (celeste × 0,67 + perturbación × 0,33). Cacheado 10 minutos, timeout de 4 segundos, y un fallo conserva el último valor en lugar de bloquear o romper una sesión.", + "source": "backend/app/geomagnetic.py · backend/app/ws.py" + }, + "baseline": { + "title": "De la línea base, que es todo el arte de sentir", + "body": "Una lectura aislada no dice nada; una desviación de lo que la sala venía diciendo lo dice todo. Por eso todo sentido de umbral de este instrumento guarda una memoria rodante y sólo informa de la sorpresa. Pero los sentidos no informan con horario — una baliza puede hablar tres veces en un segundo o callar dos minutos, y un magnetómetro se estrangula en cuanto la página queda detrás de otra — así que la memoria se lleva contra el reloj y no contra el recuento. El peso dado a cada lectura nueva es uno menos la exponencial del tiempo transcurrido negativo sobre la constante de decaimiento; por lo cual un largo silencio empuja ese peso hacia la unidad, siendo rancia la memoria vieja y con razón descartada, y una ráfaga rápida lo empuja hacia la nada. Nada se cree hasta que la memoria está templada. Y el sentido es simétrico en ambas direcciones, pues un cuerpo puede absorber un camino o reflejar en él, y el hierro puede apantallar un campo o sumarse a él; un detector de un solo lado perdería la mitad de cuanto de verdad ocurre.", + "plain": "Todos los sensores de umbral comparten una misma clase de línea base rodante y reportan desviación, no valores crudos. El factor de suavizado EMA es consciente del tiempo: alpha = 1 − exp(−dt/tau), porque los sensores informan a intervalos irregulares (los intervalos de anuncio BLE varían, las páginas en segundo plano se estrangulan). Un hueco largo lleva alpha hacia 1 (fíate de la lectura nueva), una ráfaga hacia 0. No emite eventos hasta acumular un mínimo de muestras y dispara en ambas direcciones.", + "source": "frontend/src/lib/baseline.ts — emaAlpha, ThresholdBaseline" + }, + "flesh": { + "title": "De la carne, que es casi toda agua y por tanto opaca", + "body": "Nada hay de oculto en esto y no por ello resulta menos notable. El Bluetooth anuncia en la banda de dos coma cuatro gigahercios; el agua absorbe con fuerza esa banda; y una persona es en gran medida agua. Es la misma física por la que un microondas cocina una cena, y la misma razón por la que tu conexión empeora cuando alguien se interpone entre tú y el router. Así que cuando un cuerpo cruza el camino entre el receptor y una baliza, la potencia recibida cae de verdad — unos tres a diez decibelios según la geometría — mientras que un enlace quieto vaga sólo dos a cuatro por multitrayecto y ruido del receptor. Seis decibelios de desviación se toman, pues, por perturbación: por encima del murmullo en reposo, dentro de lo que un cuerpo real causa de verdad. El instrumento informa de que el campo cambió. Nunca dice que el cambio fuera una presencia. Esa interpretación vive arriba, en la ficción; aquí abajo la medición sigue siendo honesta.", + "plain": "BLE opera en la banda ISM de 2,4 GHz, que el agua absorbe con fuerza — la misma física del microondas, y por lo que el wifi empeora si alguien se interpone. Un cuerpo humano que cruza un enlace BLE lo atenúa unos 3–10 dB, mientras que un enlace en reposo vaga sólo ±2–4 dB, de ahí el umbral de perturbación de 6 dB con constante de tiempo de línea base de 8 segundos. El módulo sólo informa de que la potencia se desvió; nunca afirma que eso sea una presencia.", + "source": "frontend/src/lib/bluetooth.ts — DISTURBANCE_DB" + }, + "iron": { + "title": "Del hierro, y del campo que la Tierra no deja de hacer", + "body": "Los célebres medidores del oficio cazafantasmas son magnetómetros, y los picos por los que se los celebra los causan de verdad el cableado de la red, los motores, los imanes de los altavoces y la masa ferrosa en movimiento. Esta máquina lee la misma magnitud, en microteslas, como la raíz de la suma de los cuadrados de sus tres ejes. Pero el campo propio de la Tierra, de unos veinticinco a sesenta y cinco microteslas según la latitud, es precisamente lo que no debe informarse, pues está en todas partes y no significa nada; es lo que la memoria rodante del artículo anterior resta en silencio. Lo que queda es la desviación respecto de donde estés parado. Se exigen tres microteslas, siendo el ruido propio del sensor de medio a uno, y produciendo el cableado doméstico a corta distancia entre uno y diez. El campo se muestrea diez veces por segundo — de sobra para algo que cambia a la velocidad de un humano andando, y mucho más benévolo con la batería que los sesenta que la interfaz daría de buen grado.", + "plain": "El magnetómetro lee el campo magnético real en microteslas, sqrt(x²+y²+z²). El campo de fondo terrestre es de 25–65 µT según la latitud y es exactamente lo que la línea base rodante resta, de modo que sólo se reporta la desviación local. El umbral de pico es 3 µT — varias veces el ruido de ~0,5–1 µT de un magnetómetro de móvil, y dentro del rango de 1–10 µT que produce el cableado de la red a corta distancia. Se muestrea a 10 Hz para ahorrar batería.", + "source": "frontend/src/lib/magnetometer.ts — SPIKE_UT, SAMPLE_HZ" + }, + "throat": { + "title": "De la garganta, y de por qué los muertos no suenan todos igual", + "body": "Si el tono, el ritmo, el aliento y la reverberación se sortearan cada uno por separado, no se obtendrían personas sino una mancha uniforme de rareza — el mismo sintetizador con ocho sombreros. Las voces reales covarían: un anciano es grave y lento y jadeante a la vez; un niño es agudo, rápido y, lo más inquietante, limpio. Por eso la garganta se sortea entera, de entre ocho arquetipos, y sólo después se agita levemente. Los ahogados van bajados de tono, arrastrados y empapados de reverberación hasta la mitad. Los quemados son los más ruidosos, apenas sosteniendo un tono. Los distantes no están dañados sino sólo lejos: quedos, parejos y macerados en espacio. Y cuando la sesión se celebra en español se deja de lado la lista de modelos del arquetipo en favor de una voz que pronuncie bien la lengua, cargando la forma sola con el carácter — mejor un acento verdadero con el porte de un arquetipo que una garganta inglesa destrozando palabras españolas.", + "plain": "Los parámetros de voz se sortean como un paquete coherente de entre ocho arquetipos (anciano, anciana, joven, joven mujer, niño, ahogado, quemado, distante), cada uno con sus rangos de tono, ritmo, ruido y eco, y luego se agitan — porque aleatorizar cada parámetro por separado produce efectos aleatorios, no personas distintas. En español se ignora la lista de modelos del arquetipo y se usa una voz Piper española, aplicando igualmente la forma del arquetipo.", + "source": "backend/app/entities.py — VOICE_ARCHETYPES, roll_voice" + }, + "utterance": { + "title": "Del habla no pedida, y de la vista", + "body": "Cuando una presencia habla sin que se le pregunte, no te está respondiendo, y eso se dispone con cuidado. La instrucción entregada al modelo no contiene palabra tuya alguna — su único estímulo es el estado medido de la sala — de suerte que no hay allí nada a lo que replicar. Y la semilla de muestreo se toma de la mismísima entropía física cosechada en esa sala, siendo los ocho primeros bytes del compendio mezclado, reducidos por debajo del bit de signo que el servidor del modelo exige. La semilla fija el camino de la selección de tokens; por tanto la sala elige de verdad las palabras. Esto no es metáfora. Altera el ruido y el habla se altera; no hay dos salas que produzcan el mismo enunciado. La decodificación se deja adrede caliente, siendo una decodificación estrecha y bien educada precisamente el fracaso que aquí tememos — esto debe sonar a algo que aflora, no a algo compuesto. Y cuando ofreces la cámara, el modelo tiene visión y mira de verdad: habla de la sala que realmente tiene delante, no de una que inventó.", + "plain": "Para los enunciados espontáneos la instrucción no contiene ninguna entrada del usuario — sólo lecturas de sensores medidas — así que el modelo genera a partir de la sala, no responde. La opción `seed` de Ollama se deriva de la entropía física mezclada (los 8 primeros bytes de veil_seed, mod 2^63) y, como la semilla fija el muestreo de tokens, un ruido de sala distinto produce realmente palabras distintas. El muestreo es deliberadamente caliente (temperatura 1,15, top_k 100). El escrutinio envía el fotograma real de tu cámara a un modelo local con visión, así que la descripción es de tu habitación real.", + "source": "backend/app/llm/service.py — manifest, scry" + }, + "limits": { + "title": "De lo que este instrumento no puede hacer", + "body": "Una doctrina que no nombra sus propias fronteras es doctrina de la que desconfiar, así que aquí están las nuestras, sin adorno. En los dispositivos de mano de Apple no hay Web Bluetooth, ni WebUSB, ni magnetómetro — la compañía no envía ninguna de esas interfaces, y todo navegador de esa plataforma es el mismo motor bajo otro abrigo, de modo que ningún navegador alternativo te salva; la sesión recae en los sensores de movimiento, que miden movimiento y no magnetismo, y el instrumento lo dice en vez de fingir. La radio exige un receptáculo Chromium y un origen seguro. El firmware escrito para el nodo de sensores emparejado jamás se ha grabado en hardware físico ni compilado con su verdadera cadena de herramientas; está razonado, no probado, y el repositorio lo declara con sus propias palabras. El mapa de suelo embrujado se arma con dos servicios públicos gratuitos que pueden ir lentos o callar, en cuyo caso recibes un mapa más silencioso y no un error. La medianoche solar ignora la ecuación del tiempo y puede errar un cuarto de hora; la luna es una aproximación sinódica media, exacta bastante para ocho fases con nombre y para nada que gobierne un barco. Y la frontera final, dicha sin ornamento: nada de esto es prueba de los muertos. Las mediciones son reales. Los espíritus son ficción.", + "plain": "Límites duros: iOS no tiene Web Bluetooth, ni WebUSB, ni API de Magnetómetro — Apple no los envía y todo navegador de iOS es WebKit por debajo, así que ningún navegador de terceros ayuda; esos modos recaen en sensores de movimiento, que miden movimiento y no magnetismo. WebUSB (el SDR) requiere Chromium y contexto seguro. El firmware ESP32 nunca se ha grabado en hardware real: está razonado estructuralmente, no verificado. Las consultas de lugares embrujados golpean endpoints públicos gratuitos de Wikipedia y Overpass con timeout de 8 segundos y degradan a lista vacía si fallan. La medianoche solar ignora la ecuación del tiempo (hasta ~16 minutos de error) y el cálculo lunar es una aproximación sinódica media. Y la evidente: nada de esto es prueba de un más allá. Los sensores son reales; los espíritus son ficción.", + "source": "frontend/src/lib/bluetooth.ts · magnetometer.ts · sdr.ts · firmware/esp32p4-sensor-node/README.md · backend/app/haunts.py" + } + } } } diff --git a/frontend/src/pages/DoctrinePage.css b/frontend/src/pages/DoctrinePage.css new file mode 100644 index 0000000..7f14430 --- /dev/null +++ b/frontend/src/pages/DoctrinePage.css @@ -0,0 +1,219 @@ +/* The Doctrine — a long-form reading page. + * + * Deliberately quieter than the séance chrome: this is the one page meant to + * be *read*, so it optimises for line length, contrast and scanning rather + * than atmosphere. The occult register lives in the words; the layout stays + * out of the way. + * + * Layout: single column at 390px, with the plain-language margin note sitting + * beneath its article. On wide screens the numeral moves into the gutter so + * the articles read as a numbered codex rather than a list of cards. + */ + +.doctrine-page { + min-height: 100vh; + padding: 1.5rem 1rem 4rem; + color: #d9d4c8; + background: #07070a; + font-size: 1rem; + line-height: 1.7; +} + +.doctrine-header { + max-width: 40rem; + margin: 0 auto 2.5rem; +} + +.doctrine-back { + display: inline-flex; + align-items: center; + min-height: 44px; + color: #8b8577; + text-decoration: none; + letter-spacing: 0.08em; + font-size: 0.85rem; +} + +.doctrine-back:hover, +.doctrine-back:focus-visible { + color: #cfc7b2; +} + +.doctrine-kicker { + margin: 0.75rem 0 0; + color: #6f6a5e; + font-size: 0.8rem; + font-style: italic; + letter-spacing: 0.04em; +} + +.doctrine-title { + margin: 0.35rem 0 0.75rem; + font-size: clamp(2rem, 9vw, 3.25rem); + letter-spacing: 0.14em; + line-height: 1.1; + color: #efe8d6; +} + +.doctrine-subtitle { + margin: 0 0 1.25rem; + color: #a49d8c; + font-size: 1.05rem; +} + +.doctrine-creed { + margin: 0; + padding: 0.9rem 1rem; + border-left: 2px solid #3a4a3e; + background: rgba(255, 255, 255, 0.02); + color: #b7b0a0; + font-size: 0.92rem; +} + +.doctrine-articles { + max-width: 40rem; + margin: 0 auto; + padding: 0; + list-style: none; + counter-reset: doctrine; +} + +.doctrine-article { + position: relative; + margin: 0 0 3rem; + padding-top: 1.75rem; + border-top: 1px solid #1d1d24; +} + +.doctrine-numeral { + font-size: 0.9rem; + letter-spacing: 0.3em; + color: #5d7a63; +} + +/* The visible numeral is decorative (aria-hidden); this carries it to + * screen readers inside the heading instead, where it belongs. */ +.doctrine-numeral-sr { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip: rect(0 0 0 0); + white-space: nowrap; + border: 0; +} + +.doctrine-article-title { + margin: 0.4rem 0 0.9rem; + font-size: clamp(1.25rem, 5vw, 1.6rem); + line-height: 1.25; + color: #e7dfc9; +} + +.doctrine-body { + margin: 0 0 1.1rem; + color: #cdc6b6; +} + +.doctrine-margin { + margin: 0 0 0.9rem; + padding: 0.85rem 1rem; + border-left: 2px solid #4a5f7a; + background: rgba(74, 95, 122, 0.08); +} + +.doctrine-margin-label { + display: block; + margin-bottom: 0.35rem; + color: #8fa8c4; + font-size: 0.75rem; + letter-spacing: 0.14em; + text-transform: uppercase; +} + +.doctrine-margin-text { + margin: 0; + color: #c3cbd6; + font-size: 0.94rem; + line-height: 1.65; +} + +.doctrine-source { + margin: 0; + color: #6b665b; + font-size: 0.78rem; + /* Paths are long and must not push the page sideways at 390px. */ + overflow-wrap: anywhere; +} + +.doctrine-source-label { + letter-spacing: 0.1em; + text-transform: uppercase; +} + +.doctrine-source code { + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + color: #7f8f7c; +} + +.doctrine-footer { + max-width: 40rem; + margin: 0 auto; + padding-top: 1.5rem; + border-top: 1px solid #1d1d24; + text-align: center; +} + +.doctrine-footer p { + margin: 0 0 1.25rem; + color: #9a9384; + font-style: italic; +} + +.doctrine-cta { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 44px; + padding: 0 1.5rem; + border: 1px solid #4d6b52; + color: #cfe3cf; + text-decoration: none; + letter-spacing: 0.16em; + font-size: 0.9rem; + transition: background-color 160ms ease, border-color 160ms ease; +} + +.doctrine-cta:hover, +.doctrine-cta:focus-visible { + background: rgba(77, 107, 82, 0.18); + border-color: #7fa886; +} + +@media (min-width: 56rem) { + .doctrine-articles, + .doctrine-header, + .doctrine-footer { + max-width: 46rem; + } + + .doctrine-article { + padding-left: 4.5rem; + } + + .doctrine-numeral { + position: absolute; + top: 1.75rem; + left: 0; + width: 3.5rem; + text-align: right; + } +} + +@media (prefers-reduced-motion: reduce) { + .doctrine-cta { + transition: none; + } +} diff --git a/frontend/src/pages/DoctrinePage.tsx b/frontend/src/pages/DoctrinePage.tsx new file mode 100644 index 0000000..f0e10de --- /dev/null +++ b/frontend/src/pages/DoctrinePage.tsx @@ -0,0 +1,109 @@ +// The Doctrine — /doctrine. +// +// A public exposition of how the instrument actually works, written in the +// register of an insufferably erudite occultist. The hard rule this page is +// built on: every arcane claim must be TRUE of the code in this repository. +// Each article therefore carries a plain-language margin note stating the +// same mechanism without ornament, so the page is genuinely educational and +// nobody mistakes the fiction for invented physics. +// +// Article ids double as i18n key segments (doctrine.art..*); the +// coverage checker has a domain rule for that prefix. + +import { Link } from 'react-router-dom' +import { useTranslation } from 'react-i18next' +import { PUBLIC_ORIGIN, usePageMeta } from '../lib/pageMeta' +import './DoctrinePage.css' + +/** Ordered articles. Keep in sync with the `doctrine.art.` rule in + * src/i18n/coverage-check.mjs. */ +const ARTICLES = [ + 'entropy', + 'channel', + 'return', + 'sky', + 'storm', + 'baseline', + 'flesh', + 'iron', + 'throat', + 'utterance', + 'limits', +] as const + +const ROMAN = [ + 'I', + 'II', + 'III', + 'IV', + 'V', + 'VI', + 'VII', + 'VIII', + 'IX', + 'X', + 'XI', +] as const + +export function DoctrinePage() { + const { t } = useTranslation() + + usePageMeta({ + title: 'The Doctrine', + description: + 'How Quantumancy actually channels the dead: Von Neumann debiasing of microphone noise, HMAC-SHA256 mixing, anomaly fingerprints, the mean synodic month, NOAA planetary K-index, time-aware EMA baselines and 2.4GHz body absorption — every arcane claim is a real mechanism, plainly explained.', + path: '/doctrine', + structuredData: { + '@context': 'https://schema.org', + '@type': 'Article', + headline: 'The Doctrine', + url: `${PUBLIC_ORIGIN}/doctrine`, + about: + 'The real physics and measurement behind Quantumancy: entropy harvesting, sensor baselines, astronomy, space weather and locally-run language models.', + }, + }) + + return ( +
+
+ + {t('doctrine.back')} + +

{t('doctrine.kicker')}

+

{t('doctrine.title')}

+

{t('doctrine.subtitle')}

+

{t('doctrine.creed')}

+
+ +
    + {ARTICLES.map((id, i) => ( +
  1. + +

    + {ROMAN[i]}.{' '} + {t(`doctrine.art.${id}.title`)} +

    +

    {t(`doctrine.art.${id}.body`)}

    + +

    + {t('doctrine.sourceLabel')}{' '} + {t(`doctrine.art.${id}.source`)} +

    +
  2. + ))} +
+ +
+

{t('doctrine.closing')}

+ + {t('doctrine.cta')} + +
+
+ ) +} diff --git a/frontend/src/pages/LandingPage.css b/frontend/src/pages/LandingPage.css index 7cef8f2..845dc07 100644 --- a/frontend/src/pages/LandingPage.css +++ b/frontend/src/pages/LandingPage.css @@ -445,11 +445,18 @@ gap: 1.6rem; } -.landing-section.reliquary-strip { +.landing-section.reliquary-strip, +.landing-section.doctrine-strip { padding-top: 2.5rem; padding-bottom: 2.5rem; } +/* The doctrine strip reuses the reliquary strip's shape but a colder edge — + * it leads to an explanation, not to a shop. */ +.landing-section.doctrine-strip .reliquary-strip-inner { + border-color: rgba(120, 190, 160, 0.35); +} + .reliquary-strip-inner { display: flex; flex-wrap: wrap; diff --git a/frontend/src/pages/LandingPage.tsx b/frontend/src/pages/LandingPage.tsx index 4af7aff..c8fbd02 100644 --- a/frontend/src/pages/LandingPage.tsx +++ b/frontend/src/pages/LandingPage.tsx @@ -366,6 +366,18 @@ export function LandingPage() { )} + {/* The doctrine: the one page that explains the real mechanisms behind + every mode above, in plain language alongside the fiction. */} +
+
+

{t('landing.doctrine.title')}

+

{t('landing.doctrine.line')}

+ + {t('landing.doctrine.link')} + +
+
+