it already knows. you just have to ask.
A magic 8-ball that knows you. Enter your name and DOB once; optionally add birth time and city (autocompletes from a 53k-entry GeoNames subset; sets IANA timezone + lat + lng atomically) for rising sign and moon sign. Shake. The single-person sheet is completely free and unlimited (doctrine v0.71), and so is the dyad, the paired read (v0.81 added it as a paid surface; PAYWALL-REMOVE-01, 2026-09-14, made it free too — below). Five coordinates derive from the date of birth alone — tarot birth card, sun sign, public animal (year-pillar), life path, and the catalog numeral — rendered on a constant compartment specimen sheet. Every one of the fifteen sheet cells is clickable and keyboard-accessible. A resolved cell opens its value-specific meaning, followed by a separate contextual reading. Numerology preserves the master numbers: life path, name number, soul urge, personality, birthday, and maturity resolve to 1–9 or to 11, 22, or 33, and each resolved number is interpreted in its own role with the other numbers; a name with no contributing vowel or consonant shows an honest unresolved dash rather than a tenth value 0. Other coordinates retain the broader in this sheet context—for example, Earth stability as the material tempo working through Capricorn ambition and Snake discretion. An unresolved cell (missing optional birth time or city, for the handful of coordinates that need them) explains why no meaning can yet join the sheet without exposing a hidden value — a data-availability gap, never a sealed cell: nothing on the single sheet is tier-gated (doctrine v0.71, kept by v0.81). The catalog numeral remains separate and is not a detail trigger. The rest open when the name enters the math: rising sign, moon sign, five-element, private animal, name number, soul urge, personality, birthday, maturity, day pillar, hour pillar, and the written 144-card entry. Each explicit flip again rotates that entry through its three shipped note positions. The paired read — 8ball Dyad: two complete sheets, read beside each other, with the bounded relation layer between them — is free too (doctrine v0.81 made it the one paid surface; PAYWALL-REMOVE-01, 2026-09-14, retired that: no purchase, no license key, no account). Inside it the paired read is presented as a Pair Dossier — a compact three-finding pair signature (element cycle, combined life path, card pair) before the two full sheets, direction-explicit evidence below them — and, from a completed pair, an optional Pair Imprint: a narrow, on-device share PNG carrying only those three relation summaries and fixed product copy, never a name, DOB, or either sheet. /example shows the complete paired reading for one fixed synthetic pair; /activate and its Netlify function are retired (dead code, kept for one-commit revert) and now just redirect into the product. The second entry is never saved. Compact row labels are always visible on the host and both Pair sheets, and adjacent values remain separate when copied. Open any compartment to read its system name and derivation beside its meaning (the placard and atlas left the card in doctrine v0.74). Native disclosures explain how the written entry is chosen and how the symbolic associations are assembled; the associations and their counterpoint are explicitly not an assessment of aptitude or career suitability. The result rail reads full sheet, without implying every optional-input-dependent field has resolved. The catalog index is the card the (sun sign, public animal) pair selects from a 144-card grid (12 sun rows × 12 animals); life path anchors the first visible note position (low/mid/high) within a cell, not the index. All coordinates are surface-only — they never feed the catalog driver. Completed readings can be saved explicitly to Previous Readings in the same browser, then reopened, renamed, deleted, or cleared, or selected in pairs for a finite structural Concordance. Both entries are recalculated through the existing pipeline before relation lookup; there is no account, sync, or remote copy.
The card content ships in the public bundle. This source tree includes the calculations, the UI, the positional catalog map, and content/cards.v1.full.js — 144 entries with name/type/habit/note × low/mid/high brackets. Every render shows the full card content (free per doctrine v0.71; the tier-gated locked/unlocked split is lineage). The deck bytes are inspectable via View Source. Private authoring source is preserved at ~/dev/8ball-private/cards.v1.full.js.
npm run dev # serves on http://localhost:5173ES modules need an HTTP context — opening index.html directly via file:// will fail at the import lines. The dev script just starts a static server, no build.
npm test # vitest — file count: CLAUDE.md (canonical); case count: newest journal.md entrySix CI stages per DOCTRINE.md §7:
- Calculation contract + engine pipeline —
tests/profile.test.js,tests/rising.test.js,tests/cities.test.js,tests/countries.test.js,tests/numerology_display.test.js,tests/labels_reveal.test.js,tests/dob_validation.test.js,tests/payments_markup.test.js.tests/fixtures.jsonis the source of truth forcore/profile.js; the algorithm must match every fixture exactly. Changes need updates in lockstep (seeDOCTRINE.md §3). These files also cover thegetCardpipeline against the full positional catalog (12 sun × 12 animal = 144),resolveBracketcases, rising-sign math, the v0.22 deck scan, and the §1.G content-policy/completeness scans over immutablecontent/meanings.v1.jsand the activecontent/meanings.v5.jsregistry (v3/v4 carried unedited via re-export). - Privacy scan —
tests/privacy_scan.test.js. No unpermitted network calls (only DOCTRINE §5-permitted same-origin lazy loads and the §5.B user-initiated feedback POST; the dyad's commerce Buy Link redirect that doctrine v0.81 restored narrowly is retired again by PAYWALL-REMOVE-01, 2026-09-14 — the constant ships blank and no offer anchor ever gets an href; the signed?dyad=access link path is still wired but unreachable from any live UI, verified offline with Web Crypto, no call leaves the page); no third-party fonts or scripts; system fonts only. Saved Readings adds one doctrine-allow-listed local key;tests/readings.test.jslocks its minimal schema and lifecycle. Concordance adds no key or schema field;tests/concordance.test.jslocks its transient recomputation boundary and finite relation inventory. - PII scan —
tests/pii_scan.test.js. Operator-name leakage, SIRR cross-reference leakage, labeled-DOB leakage. - Dependency discipline —
tests/dependency_discipline.test.js. No card-content imports in the public engine; no runtime deps; devDependencies ≤ 5. - Single-file rule —
index.html≤ 1500 lines (CI-enforced; the current count lives in the newestjournal.mdentry, not here). - Payments state machine —
tests/payments_state.test.jsverifiescore/payments.js's retained legacy-registry compatibility machinery (isNewPair,nextShakeStaterender/render-idempotent,applyPaidReturnmonotonic tier write + pending render with no credit grant; replay-attack no-pending branch; same-profile idempotence) — since doctrine v0.71 (kept by v0.81),ui/payments.js's livegetRenderTier()never calls any of it: it answerst3for every device andt5only from a verified signed dyad access token (core/entitlement.js); this stage keeps the legacy registry's own contract correct for any pre-amendment stored state, not a live paid path.tests/facet_rotation.test.js(t3-only round-robin note rotation, owned and unfunded per §1.H v0.55, persistence), plustests/feedback_surface.test.js.
8ball/
├── index.html UI + boot markup/script (≤1500 LOC per §6; shell styles live in ui/shell.css since 2026-08-31)
├── core/ 14 pure-logic ES modules — no DOM
│ ├── profile.js sun, animals, numbers; aggregates birth card + day/hour pillars
│ ├── engine.js positional 144-card catalog + bracket resolution
│ ├── rising.js Meeus ascendant — DST + historical-tz aware
│ ├── birthcard.js Major Arcana birth card (digit-sum reduction) — v0.5.0
│ ├── pillars.js day + hour BaZi pillars (stem+branch+element) — surface-only
│ ├── countries.js legacy v0.2.1 fixed-offset entries (backward-compat for stored profiles)
│ ├── calendar.js Meeus lunar new year + solar terms, 1900–2100
│ ├── cities.js city autocomplete loader (lazy-loads assets/cities.json)
│ ├── math.js shared primitives: euclidean mod, sumDigits, normalizeDeg
│ ├── public.js public-reading resolution + disclosed master-mode bridge
│ ├── dyad.js pure two-profile relation calculation
│ ├── moon.js moon sign + moon placement context (§1.K)
│ ├── kua.js eight-trigram Kua registry lookup (renderer retired, table retained)
│ ├── payments.js pure state machines: new-profile reads + t3 facet rotation
│ └── entitlement.js dyad access-token verification (ECDSA P-256, offline) + configurable product url / public keys
├── ui/ 15 ES modules — init*UI({refs}, {hooks}) DI shape for DOM controllers; pure concordance lookup
│ ├── tiers.js compartment-card render + shareRowRefs + the provenance/atlas registries + density
│ ├── payments.js storage/status module (density resolver t3/t5 + dyad entitlement storage + facet storage)
│ ├── profile.js profile persistence + form helpers
│ ├── readings.js Saved Readings storage + previous/read/rename/delete/clear UI + closeActiveScreens screen-ownership hook (§1.J v0.83)
│ ├── concordance.js pure post-calculation relation lookup; no DOM/storage/network
│ ├── share.js free card → on-device PNG → Web Share / clipboard fallback
│ ├── labels.js intrinsic-height style + legacy label helpers (no live toggle)
│ ├── meanings.js all-cell value meaning + deterministic sheet context (§1.G v0.53)
│ ├── public.js symbolic-associations framing + master-mode bridge disclosure
│ ├── dyad.js second-profile entry + the Pair Dossier (heading/signature/direction-explicit evidence/failure state/completion flow, §1.J v0.83)
│ ├── pairShare.js the Pair Imprint — narrow allow-listed share PNG for the paired reading, via a model-first non-recursive view coordinator (§1.J/§5.D v0.83–v0.88)
│ ├── sheet.js shared sheet value mapping/render helpers
│ ├── result.js result-screen arrival + card-flip controller — transition state, face accessibility, arrival focus/announcement, shake-again
│ ├── modals.js about / forget controllers + escape-to-close + focus trap (§6 split)
│ └── citysearch.js city-autocomplete controller — debounce, race guard, polar mirror (§6 split)
├── content/ 16 versioned registry modules
│ ├── cards.v1.full.js 144-card deck (name/type/habit/note × low/mid/high) — HISTORICAL: JS-gated per §1 v0.22 behind a credits flag; current truth per doctrine v0.71's free amendment: every render shows the full card content, no gate
│ ├── meanings.v1.js 58 tradition-cited coordinate meanings (§1.G v0.44) — static, no network call
│ ├── meanings.v2.js element meanings + all-coordinate context roles (§1.G v0.53; superseded)
│ ├── meanings.v3.js twelve terminal values, masters reused from v1 (§1.G v0.62; superseded)
│ ├── meanings.v4.js + per-slot numerology lines, theme tensions (§1.G; superseded)
│ ├── meanings.v5.js + rising/private-animal placement lines (§1.G; superseded)
│ ├── meanings.v6.js ACTIVE registry — v5 unedited + moon placement family + moon context role (§1.K)
│ ├── concordance.v1.js immutable historical relation registry (§1.I v0.51)
│ ├── concordance.v2.js superseded registry for the strict 1–9 numerology cut (§1.I v0.54)
│ ├── concordance.v3.js ACTIVE registry — twelve-value domain + the three master links (§1.I v0.62)
│ ├── dyad.v1.js immutable two-profile relation tables (§1.J v0.61)
│ ├── dyad.v2.js ACTIVE dyad tables + master-preserving combined-path frame (§1.J v0.62)
│ ├── public.v1.js immutable public-reading mode registry (§1.D)
│ ├── public.v2.js superseded public-reading registry
│ ├── public.v3.js ACTIVE registry + declared master-to-base mode bridge (§1.D v0.62)
│ └── kua.v1.js eight-trigram registry — table retained, renderer retired (§1.D kua-retirement amendment)
├── agents/ agent role docs + platform constraints (per DOCTRINE §10 v0.24)
├── tests/ vitest files + fixtures.json — counts: CLAUDE.md + newest journal entry
│ ├── fixtures.json calculation contract — locked, hand-verified
│ ├── profile / rising / cities / countries / birthcard / pillars core calc + engine pipeline
│ ├── tiers / labels_reveal / numerology_display / prose_coordinate_count surface + tier render
│ ├── provenance / atlas / density CLP legibility surfaces (DOCTRINE §1.E / §1.F; placard + atlas live in the meaning panel since v0.74)
│ ├── meanings_content / meanings_ui coordinate meanings content policy + DI shape (DOCTRINE §1.G)
│ ├── share_surface / pair_share / readings / concordance / payments_markup / payments_state / facet_rotation / feedback_surface / modals UI surfaces + state
│ ├── dyad_surface / dyad / dyad_content / pair_readings_integration the paired reading + Pair Dossier hierarchy (DOCTRINE §1.J)
│ └── privacy_scan / pii_scan / dependency_discipline / dob_validation / rising_disclosure guards
├── audits/ release checklist + local PII audit + cross-model briefs
├── assets/ cities.json + favicons + og:image
├── .github/workflows/ci.yml CI gate (6 stages per §7)
├── netlify.toml headers + SPA redirect
├── DOCTRINE.md constitution — read every release
├── 8BALL.md canonical context, AI-readable
├── journal.md append-only release log
└── README.md this
Netlify, Pro plan (paid monthly — corrected 2026-07-10; earlier docs wrongly said free tier), main branch. Auto-deploys on push when the GitHub repo is connected.
First-time Netlify setup:
- https://app.netlify.com/start → "Import from GitHub" → pick this repo.
- Build command: leave blank.
- Publish directory:
.(or set to repo root). - Deploy. The first build will pick up
netlify.tomland apply security headers.
DOCTRINE.md is the constitution. The two load-bearing sections are:
- §3 Calculation contract — fixtures gate algorithm changes.
- §4 Content rules — what card lines (name/type/habit/note × low/mid/high) can and can't say.
Anything that contradicts the doctrine requires an explicit doctrine amendment in the same change.
MIT. See LICENSE.