Vibecode Dreams & Stars
track this build4 phases, 12 steps, beginner friendly0%The reading loop itself · collect a dream or birth details, prompt an LLM, stream back an interpretation · is a weekend build with any modern coding agent. What resists cloning is the boring half: real natal-chart math from ephemeris data rather than an LLM guessing planetary positions, a prompt methodology (Jungian + Ibn Sirin + actionable layers) tuned over real usage, a budget-reservation system that stops one runaway API call from burning the month's margin, and the credits/payment plumbing. Build the personal version for your own dream journal, but don't expect the DIY build to hold up as a product.
You are building a lean indie version of Dreams & Stars. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Dreams & Stars · indie build A personal dream and chart journal whose hard problems are getting the astronomy from an ephemeris rather than from a model, handling birth data whose time is unknown and whose time zone is historical, and keeping a wellness-shaped app honest about what it is and what it will not answer. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js 15 with TypeScript, single user, running locally | a dream diary is the most private text most people write down | | Astronomy | An ephemeris library, with its licence recorded in the README | positions are computed, never generated, and the licence is part of the decision | | Time | A time zone database with historical rules, plus an explicit unknown-time path | an offset from 1978 is not today's offset and defaulting to noon silently is a lie | | Geocoding | A public geocoder used inside its published usage policy, every result cached | a birthplace is looked up once per place, forever, or you get blocked and deserve it | | Storage | SQLite on the local disk, no account, no upload, no telemetry | this is a diary, and diaries live where their owner lives | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the application. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **An ephemeris library plus its data files** · free under its open-source licence; a paid licence exists for closed distribution - Why: Every planetary position in this build comes from here, and the library's licence constrains what you may do with the result. - Get it: Install the bindings for your runtime, fetch the data files, and read the licence terms in full before you build on it. - Verify: Five known dates match a published source within your stated tolerance - [ ] **An API key for a text model** · pay per call; a reading is fractions of a penny - Why: The interpretation is one call per reading and is the only generated part of the product. - Get it: Create a key with the provider you prefer and set a spend limit in their console as well as the ceiling here. - Verify: One reading generates in the configured language with its model identifier stored - [ ] **A geocoder and its published usage policy** · free within the published policy - Why: Birthplace lookup is the one thing here that hits somebody else's free service, and their policy sets the rate, the identification requirement and the caching obligation. - Get it: Read the policy for the geocoder you choose, note the maximum rate and the identification requirement, and record the date you read it. - Verify: Your client sends an identifying user agent, respects the stated rate, and caches every result - [ ] **The crisis contacts for your country** · free - Why: The safety check refuses to interpret certain entries and has to offer something real in their place, which is local and cannot be guessed by the build. - Get it: Look up the current helpline numbers and text services where you live and write them into setup. - Verify: Setup refuses to complete with the field empty and the contacts render on a flagged entry ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Credits, payments and a paywall. - Compatibility readings between two people. - Any predictive claim about events, health or relationships. - A version aimed at children. - the three-layer interpretation methodology (psychological/Jungian, Islamic/Ibn Sirin traditional, actionable) tuned over real usage, not a first draft - real natal-chart accuracy from ephemeris data instead of an LLM approximating planetary positions - a spend-reservation system that caps provider cost before generation starts, so one bad request can't blow the budget - native-quality bilingual Turkish/English copy, not machine-translated - the credits/paywall, Stripe and RevenueCat payment plumbing If one of those is essential to you, that is the reason to keep paying for Dreams & Stars, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Dreams & Stars The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a private dream and birth-chart journal that computes the astronomy properly and labels the interpretation as interpretation to replace Dreams & Stars. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Three things, and the third one is a licence you have to read before you write any code. First, what this is. The product being replaced describes itself as being for entertainment and self-reflection and not a substitute for professional psychological or medical advice, and that is the honest framing. So the generated text never diagnoses, never predicts an outcome as fact, and never gives guidance about health, medication, pregnancy, money, legal matters or anyone's death. The disclaimer is rendered with the reading, at the top, not parked in a page footer. Second, a dream journal receives whatever the person is carrying. If an entry describes self-harm, suicidal thoughts or being in danger, the app does not interpret it. It saves the entry, says plainly that it is not the right tool for this, and shows the local crisis contacts you configured at setup. This check runs before the model call, not on its output, and there is no bypass. Third, the astronomy. Never let a language model produce a planetary position. Compute the chart from an ephemeris, and read that library's licence first: the Swiss Ephemeris is dual licensed, either under the GNU Affero General Public License, which requires your whole project to be AGPL or a compatible licence, or under a paid professional licence bought from its publisher. For a personal journal the AGPL route is fine. Anything you host for other people is a licence decision you make deliberately and record in the README. ### Stack (fixed, do not substitute) - Runtime: Next.js 15 with TypeScript, single user, running locally · a dream diary is the most private text most people write down - Astronomy: An ephemeris library, with its licence recorded in the README · positions are computed, never generated, and the licence is part of the decision - Time: A time zone database with historical rules, plus an explicit unknown-time path · an offset from 1978 is not today's offset and defaulting to noon silently is a lie - Geocoding: A public geocoder used inside its published usage policy, every result cached · a birthplace is looked up once per place, forever, or you get blocked and deserve it - Storage: SQLite on the local disk, no account, no upload, no telemetry · this is a diary, and diaries live where their owner lives ### Data model (create this before Phase 1) - people(id, label, birth_date, birth_time, time_known, birth_place_id, created_at) · time_known is a real column because an unknown birth time changes what can be computed - places(id, query, display_name, lat, lon, tz_id, source, fetched_at) · the geocoder is called once per place and never again - charts(id, person_id, ephemeris_name, ephemeris_version, computed_at, positions_json, aspects_json, houses_json, houses_omitted_reason) - entries(id, kind, body, created_at, language, mood) · kind is dream or question - readings(id, entry_id, chart_id, body_md, model_id, prompt_version, language, layers_json, created_at, cost_minor) - flags(entry_id, reason, at) · set by the pre-call safety check, and a flagged entry has no reading row - settings(crisis_contacts_md, monthly_cost_ceiling_minor, default_language) - A chart with no birth time has no houses and no rising sign, and the row records why rather than filling them in from a guessed noon. ### Phase 1 · Astronomy from an ephemeris, checked against published positions Build: Install an ephemeris library, record its name, version and licence in the README, and compute positions, aspects and, where a birth time exists, houses. Store the computed chart as data, not as prose. Verify a handful of dates against positions published by an independent source. Trap: Asking a language model where Saturn was on a given date produces a plausible degree and sign that is wrong often enough to be useless and confident enough that nobody checks, and every later layer of the product inherits the error. Compute from the ephemeris, store the numbers, and have the model read the numbers rather than supply them. Done when: Positions for at least five dates match an independent published source within a stated tolerance, asserted in the suite. The ephemeris name, version and licence appear in the README and on every stored chart row. No prompt anywhere asks a model for a position, a sign or an aspect, verified by review of the prompt templates. A chart is stored as structured data and the prose is generated from it, never the other way round. Do not build yet: No birth data handling, journal, readings or safety checks yet. ### Phase 2 · Birth data is the hard input Build: Ask for date, time and place, with an explicit 'time unknown' option that omits houses and the rising sign and says so. Resolve the place once through a public geocoder, store latitude, longitude and time zone identifier, and never call the geocoder for that place again. Convert birth time to UTC using historical time zone rules, not the current offset. Trap: Defaulting an unknown birth time to noon silently produces a rising sign and a full set of houses that are simply invented, and they are the first things the reading talks about. Record time_known, omit everything that depends on it, and say in the output which parts could not be computed and why. The same honesty applies to the offset: a birth in 1978 uses that year's rules for that zone, which a naive conversion from the current offset will get wrong by an hour or more. Done when: A person with time_known false produces a chart with no houses and no rising sign, and the omission reason is stored and rendered, tested. Converting a birth time from a year with different rules produces the correct UTC instant, tested against at least two historical cases including a daylight saving transition. The geocoder is called at most once per distinct place query, asserted by a counter in the suite; a repeated lookup is served from the places table. Outgoing geocoder requests carry an identifying user agent and are rate limited to the geocoder's published maximum, with the policy URL and the date you read it in the README. Do not build yet: No journal, readings, safety checks or export yet. ### Phase 3 · The safety check runs before the model, not after Build: Screen every entry before generation for descriptions of self-harm, suicidal intent or immediate danger. A flagged entry is stored, gets a flag row, and returns the crisis contacts configured at setup with a plain sentence saying this is not the right tool. No reading row is created and there is no override. Trap: Running the check on the model's output means the generation has already happened and the person has already been shown an interpretation of the worst night of their life by a dream app. Check the input before any call is made, and treat a false positive as an acceptable cost, because the failure in the other direction is not. Done when: A flagged entry creates no reading row and makes no model call, asserted by a spy on the client. The crisis contacts shown are the ones configured at setup and setup cannot complete without them, tested by attempting to skip. A flagged entry is still saved and still visible in the journal, because deleting the person's own words is not a safety measure. The screening fixture set includes phrasing that is oblique rather than explicit, and the suite fails if the obvious cases pass through. Do not build yet: No reading generation, language handling, export or cost ceiling yet. ### Phase 4 · Readings that stay inside what they are Build: Generate a reading in named layers: a psychological reading, a traditional reading marked as cultural interpretation, and one practical suggestion. For a chart, read the computed numbers first and interpret second. Emit the whole output including headings in the chosen language. Check the output for predictive, medical, financial and legal phrasing and refuse it. Trap: Translating an English reading into another language after generating it produces headings in one language and body text in another and idioms that land strangely, and people notice immediately. Pass the language into the prompt, require the entire response in it, and check the output before storing rather than trusting the instruction. Done when: Every stored reading carries its layer structure, the model identifier, the prompt version and the language, checked on a real entry. A reading generated in the second language contains no headings in the first, tested by scanning the output. Output containing predictive certainty, medical or dosage language, or financial or legal advice is refused and regenerated once before failing, tested with a forced fixture. The disclaimer renders above the reading, not below it, verified by review. Do not build yet: No export, cost ceiling or deletion yet. ### Phase 5 · It is a diary, so treat it like one Build: Keep the database on the local disk with no account and no telemetry. Export every entry, reading and chart as Markdown files plus a JSON archive. Delete a person or an entry completely, including their charts and readings, in one transaction. Cap monthly model spend and keep the journal, the charts and the history working when the ceiling is reached. Trap: Adding a convenience sync so the journal is available on a phone turns a private diary into an account on somebody's server, and the people most likely to write the most personal entries are the ones least likely to read the setup screen where you mentioned it. Keep it local, and make export the answer to portability. Done when: The application makes exactly two kinds of outbound request, to the model provider and to the geocoder, verified by watching the network through a full session. Export produces Markdown files that read in any editor plus a JSON archive that reimports into an empty database, round-tripped in the suite. Deleting a person removes their charts, entries, readings and flags in one transaction, verified by querying every table afterwards. At the monthly ceiling, new readings are refused with the figure shown and the journal, chart computation and history all continue to work. Do not build yet: Nothing after this. ### Out of scope (and why) - Credits, payments and a paywall, which is the plumbing that turns this into a product and is the part with a payment processor's rules attached. - Compatibility readings between two people, because it means storing a second person's birth details, usually without asking them. - Any claim to predict events, health outcomes or relationships, which the app refuses at generation and would not become true with better wording. - A version aimed at children, which brings in a different set of legal obligations around data from minors and a duty of care this build is not designed for. ### README must contain - That this is for entertainment and self-reflection and is not psychological or medical advice, stated before the setup instructions - Which ephemeris library is used, its version, and which of its licences you are relying on - That an unknown birth time omits houses and the rising sign rather than assuming a time - That entries describing crisis are never interpreted, and where the crisis contacts are configured - That the only outbound requests are to the model provider and the geocoder, with the geocoder's usage policy linked ===== AGENTS.md ===== # Agent instructions · Dreams & Stars indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js 15 with TypeScript, single user, running locally, An ephemeris library, with its licence recorded in the README, A time zone database with historical rules, plus an explicit unknown-time path, A public geocoder used inside its published usage policy, every result cached, SQLite on the local disk, no account, no upload, no telemetry. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Asking a language model where Saturn was on a given date produces a plausible degree and sign that is wrong often enough to be useless and confident enough that nobody checks, and every later layer of the product inherits the error. Compute from the ephemeris, store the numbers, and have the model read the numbers rather than supply them. - Defaulting an unknown birth time to noon silently produces a rising sign and a full set of houses that are simply invented, and they are the first things the reading talks about. Record time_known, omit everything that depends on it, and say in the output which parts could not be computed and why. The same honesty applies to the offset: a birth in 1978 uses that year's rules for that zone, which a naive conversion from the current offset will get wrong by an hour or more. - Running the check on the model's output means the generation has already happened and the person has already been shown an interpretation of the worst night of their life by a dream app. Check the input before any call is made, and treat a false positive as an acceptable cost, because the failure in the other direction is not. - Translating an English reading into another language after generating it produces headings in one language and body text in another and idioms that land strangely, and people notice immediately. Pass the language into the prompt, require the entire response in it, and check the output before storing rather than trusting the instruction. ===== BUILD_PLAN.md ===== # Build plan · Dreams & Stars A personal dream and chart journal whose hard problems are getting the astronomy from an ephemeris rather than from a model, handling birth data whose time is unknown and whose time zone is historical, and keeping a wellness-shaped app honest about what it is and what it will not answer. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Astronomy from an ephemeris, checked against published positions Planetary positions and aspects are computed by a library and verified against known values. ### Steps 1. Install the ephemeris and record the licence decision The licence affects what you may do with the finished thing. Files: `README.md`, `src/astro/ephemeris.ts` 2. Verify against independently published positions Five dates in the suite, with a stated tolerance. Files: `tests/positions.test.ts`, `fixtures/positions.json` 3. Store the chart as numbers, not as text The prose is a view of the data, and it is regenerable. Files: `src/astro/chart.ts` ### Done when - [ ] Five dates match a published source within tolerance - [ ] Ephemeris name, version and licence are recorded - [ ] No prompt asks a model for a position - [ ] Charts are stored as structured data ### Watch out - Asking a language model where Saturn was on a given date produces a plausible degree and sign that is wrong often enough to be useless and confident enough that nobody checks, and every later layer of the product inherits the error. Compute from the ephemeris, store the numbers, and have the model read the numbers rather than supply them. ## Phase 2 · Birth data is the hard input Unknown times are handled honestly and places are geocoded once, inside the geocoder's published policy. ### Steps 1. Make unknown birth time a first-class state It omits houses and the rising sign and explains itself. Files: `src/people/birth.ts`, `tests/unknown-time.test.ts` 2. Convert using historical time zone rules A 1978 birth is not converted with a 2026 offset. Files: `src/astro/time.ts`, `tests/tz-history.test.ts` 3. Cache every geocode and obey the published policy Read the policy, set an identifying user agent, rate limit to it. Files: `src/geo/lookup.ts`, `tests/geo-cache.test.ts` ### Done when - [ ] Unknown time omits houses and the rising sign with a reason - [ ] Historical offsets convert correctly in two cases - [ ] A place is geocoded at most once - [ ] Requests are identified and rate limited to the published policy ### Watch out - Defaulting an unknown birth time to noon silently produces a rising sign and a full set of houses that are simply invented, and they are the first things the reading talks about. Record time_known, omit everything that depends on it, and say in the output which parts could not be computed and why. The same honesty applies to the offset: a birth in 1978 uses that year's rules for that zone, which a naive conversion from the current offset will get wrong by an hour or more. ## Phase 3 · The safety check runs before the model, not after An entry describing crisis is saved, flagged and answered with your configured local contacts instead of an interpretation. ### Steps 1. Screen the input before any model call A spy in the suite proves no call happened. Files: `src/safety/screen.ts`, `tests/safety.test.ts` 2. Require crisis contacts during setup Local to the person, because a generic number helps nobody. Files: `src/setup/contacts.ts` 3. Save the entry, refuse the interpretation The journal keeps the words; the app declines the reading. Files: `src/entries/create.ts` ### Done when - [ ] A flagged entry makes no model call - [ ] Setup cannot complete without crisis contacts - [ ] Flagged entries are still saved and visible - [ ] Oblique phrasing is covered by the fixtures ### Watch out - Running the check on the model's output means the generation has already happened and the person has already been shown an interpretation of the worst night of their life by a dream app. Check the input before any call is made, and treat a false positive as an acceptable cost, because the failure in the other direction is not. ## Phase 4 · Readings that stay inside what they are Generated text is layered, labelled, single-language, and checked against the things it must not say. ### Steps 1. Generate in named layers from the stored numbers The chart data goes in; interpretation comes out. Files: `src/readings/generate.ts`, `src/readings/prompts.ts` 2. Require the whole response in one language Headings included, and check rather than trust. Files: `src/readings/language.ts`, `tests/language.test.ts` 3. Refuse predictive, medical, financial and legal phrasing One regeneration, then a failure the person can see. Files: `src/readings/guard.ts`, `tests/guard.test.ts` ### Done when - [ ] Readings store layers, model, prompt version and language - [ ] No mixed-language output - [ ] Predictive or advisory phrasing is refused - [ ] The disclaimer renders above the reading ### Watch out - Translating an English reading into another language after generating it produces headings in one language and body text in another and idioms that land strangely, and people notice immediately. Pass the language into the prompt, require the entire response in it, and check the output before storing rather than trusting the instruction. ## Not in this build - Credits, payments and a paywall. - Compatibility readings between two people. - Any predictive claim about events, health or relationships. - A version aimed at children. ## After v1, if you want it - A symbol index built from your own entries, which is more useful than any general dream dictionary - Transit computation against a stored chart, once the natal positions are verified and trusted - A yearly review that shows recurring themes by frequency, with the counts visible rather than summarised ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. People, charts, entries, readings and flags. The diary. DATABASE_PATH=./data/journal.db # Required. Ephemeris data files. Its version is stored on every chart. EPHEMERIS_PATH=./data/ephe # Required · secret. The text model. Set a provider-side spend limit as well. MODEL_API_KEY=sk-...redacted # Required. Identifies your client as the geocoder's policy requires. A default library agent is not acceptable. GEOCODER_USER_AGENT=dream-journal/1.0 (you@example.com) # Required. Minimum gap between geocoder calls. Check the current published policy before lowering it. GEOCODER_MIN_INTERVAL_MS=1000 # Required. The whole reading, headings included, is produced in one language. DEFAULT_LANGUAGE=en # Required. Readings stop here. Journal, charts and history keep working. MONTHLY_COST_CEILING=3.00
You are building a lean indie version of Dreams & Stars. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Dreams & Stars · indie build A personal dream and chart journal whose hard problems are getting the astronomy from an ephemeris rather than from a model, handling birth data whose time is unknown and whose time zone is historical, and keeping a wellness-shaped app honest about what it is and what it will not answer. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js 15 with TypeScript, single user, running locally | a dream diary is the most private text most people write down | | Astronomy | An ephemeris library, with its licence recorded in the README | positions are computed, never generated, and the licence is part of the decision | | Time | A time zone database with historical rules, plus an explicit unknown-time path | an offset from 1978 is not today's offset and defaulting to noon silently is a lie | | Geocoding | A public geocoder used inside its published usage policy, every result cached | a birthplace is looked up once per place, forever, or you get blocked and deserve it | | Storage | SQLite on the local disk, no account, no upload, no telemetry | this is a diary, and diaries live where their owner lives | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the application. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **An ephemeris library plus its data files** · free under its open-source licence; a paid licence exists for closed distribution - Why: Every planetary position in this build comes from here, and the library's licence constrains what you may do with the result. - Get it: Install the bindings for your runtime, fetch the data files, and read the licence terms in full before you build on it. - Verify: Five known dates match a published source within your stated tolerance - [ ] **An API key for a text model** · pay per call; a reading is fractions of a penny - Why: The interpretation is one call per reading and is the only generated part of the product. - Get it: Create a key with the provider you prefer and set a spend limit in their console as well as the ceiling here. - Verify: One reading generates in the configured language with its model identifier stored - [ ] **A geocoder and its published usage policy** · free within the published policy - Why: Birthplace lookup is the one thing here that hits somebody else's free service, and their policy sets the rate, the identification requirement and the caching obligation. - Get it: Read the policy for the geocoder you choose, note the maximum rate and the identification requirement, and record the date you read it. - Verify: Your client sends an identifying user agent, respects the stated rate, and caches every result - [ ] **The crisis contacts for your country** · free - Why: The safety check refuses to interpret certain entries and has to offer something real in their place, which is local and cannot be guessed by the build. - Get it: Look up the current helpline numbers and text services where you live and write them into setup. - Verify: Setup refuses to complete with the field empty and the contacts render on a flagged entry ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Credits, payments and a paywall. - Compatibility readings between two people. - Any predictive claim about events, health or relationships. - A version aimed at children. - the three-layer interpretation methodology (psychological/Jungian, Islamic/Ibn Sirin traditional, actionable) tuned over real usage, not a first draft - real natal-chart accuracy from ephemeris data instead of an LLM approximating planetary positions - a spend-reservation system that caps provider cost before generation starts, so one bad request can't blow the budget - native-quality bilingual Turkish/English copy, not machine-translated - the credits/paywall, Stripe and RevenueCat payment plumbing If one of those is essential to you, that is the reason to keep paying for Dreams & Stars, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Dreams & Stars The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a private dream and birth-chart journal that computes the astronomy properly and labels the interpretation as interpretation to replace Dreams & Stars. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Three things, and the third one is a licence you have to read before you write any code. First, what this is. The product being replaced describes itself as being for entertainment and self-reflection and not a substitute for professional psychological or medical advice, and that is the honest framing. So the generated text never diagnoses, never predicts an outcome as fact, and never gives guidance about health, medication, pregnancy, money, legal matters or anyone's death. The disclaimer is rendered with the reading, at the top, not parked in a page footer. Second, a dream journal receives whatever the person is carrying. If an entry describes self-harm, suicidal thoughts or being in danger, the app does not interpret it. It saves the entry, says plainly that it is not the right tool for this, and shows the local crisis contacts you configured at setup. This check runs before the model call, not on its output, and there is no bypass. Third, the astronomy. Never let a language model produce a planetary position. Compute the chart from an ephemeris, and read that library's licence first: the Swiss Ephemeris is dual licensed, either under the GNU Affero General Public License, which requires your whole project to be AGPL or a compatible licence, or under a paid professional licence bought from its publisher. For a personal journal the AGPL route is fine. Anything you host for other people is a licence decision you make deliberately and record in the README. ### Stack (fixed, do not substitute) - Runtime: Next.js 15 with TypeScript, single user, running locally · a dream diary is the most private text most people write down - Astronomy: An ephemeris library, with its licence recorded in the README · positions are computed, never generated, and the licence is part of the decision - Time: A time zone database with historical rules, plus an explicit unknown-time path · an offset from 1978 is not today's offset and defaulting to noon silently is a lie - Geocoding: A public geocoder used inside its published usage policy, every result cached · a birthplace is looked up once per place, forever, or you get blocked and deserve it - Storage: SQLite on the local disk, no account, no upload, no telemetry · this is a diary, and diaries live where their owner lives ### Data model (create this before Phase 1) - people(id, label, birth_date, birth_time, time_known, birth_place_id, created_at) · time_known is a real column because an unknown birth time changes what can be computed - places(id, query, display_name, lat, lon, tz_id, source, fetched_at) · the geocoder is called once per place and never again - charts(id, person_id, ephemeris_name, ephemeris_version, computed_at, positions_json, aspects_json, houses_json, houses_omitted_reason) - entries(id, kind, body, created_at, language, mood) · kind is dream or question - readings(id, entry_id, chart_id, body_md, model_id, prompt_version, language, layers_json, created_at, cost_minor) - flags(entry_id, reason, at) · set by the pre-call safety check, and a flagged entry has no reading row - settings(crisis_contacts_md, monthly_cost_ceiling_minor, default_language) - A chart with no birth time has no houses and no rising sign, and the row records why rather than filling them in from a guessed noon. ### Phase 1 · Astronomy from an ephemeris, checked against published positions Build: Install an ephemeris library, record its name, version and licence in the README, and compute positions, aspects and, where a birth time exists, houses. Store the computed chart as data, not as prose. Verify a handful of dates against positions published by an independent source. Trap: Asking a language model where Saturn was on a given date produces a plausible degree and sign that is wrong often enough to be useless and confident enough that nobody checks, and every later layer of the product inherits the error. Compute from the ephemeris, store the numbers, and have the model read the numbers rather than supply them. Done when: Positions for at least five dates match an independent published source within a stated tolerance, asserted in the suite. The ephemeris name, version and licence appear in the README and on every stored chart row. No prompt anywhere asks a model for a position, a sign or an aspect, verified by review of the prompt templates. A chart is stored as structured data and the prose is generated from it, never the other way round. Do not build yet: No birth data handling, journal, readings or safety checks yet. ### Phase 2 · Birth data is the hard input Build: Ask for date, time and place, with an explicit 'time unknown' option that omits houses and the rising sign and says so. Resolve the place once through a public geocoder, store latitude, longitude and time zone identifier, and never call the geocoder for that place again. Convert birth time to UTC using historical time zone rules, not the current offset. Trap: Defaulting an unknown birth time to noon silently produces a rising sign and a full set of houses that are simply invented, and they are the first things the reading talks about. Record time_known, omit everything that depends on it, and say in the output which parts could not be computed and why. The same honesty applies to the offset: a birth in 1978 uses that year's rules for that zone, which a naive conversion from the current offset will get wrong by an hour or more. Done when: A person with time_known false produces a chart with no houses and no rising sign, and the omission reason is stored and rendered, tested. Converting a birth time from a year with different rules produces the correct UTC instant, tested against at least two historical cases including a daylight saving transition. The geocoder is called at most once per distinct place query, asserted by a counter in the suite; a repeated lookup is served from the places table. Outgoing geocoder requests carry an identifying user agent and are rate limited to the geocoder's published maximum, with the policy URL and the date you read it in the README. Do not build yet: No journal, readings, safety checks or export yet. ### Phase 3 · The safety check runs before the model, not after Build: Screen every entry before generation for descriptions of self-harm, suicidal intent or immediate danger. A flagged entry is stored, gets a flag row, and returns the crisis contacts configured at setup with a plain sentence saying this is not the right tool. No reading row is created and there is no override. Trap: Running the check on the model's output means the generation has already happened and the person has already been shown an interpretation of the worst night of their life by a dream app. Check the input before any call is made, and treat a false positive as an acceptable cost, because the failure in the other direction is not. Done when: A flagged entry creates no reading row and makes no model call, asserted by a spy on the client. The crisis contacts shown are the ones configured at setup and setup cannot complete without them, tested by attempting to skip. A flagged entry is still saved and still visible in the journal, because deleting the person's own words is not a safety measure. The screening fixture set includes phrasing that is oblique rather than explicit, and the suite fails if the obvious cases pass through. Do not build yet: No reading generation, language handling, export or cost ceiling yet. ### Phase 4 · Readings that stay inside what they are Build: Generate a reading in named layers: a psychological reading, a traditional reading marked as cultural interpretation, and one practical suggestion. For a chart, read the computed numbers first and interpret second. Emit the whole output including headings in the chosen language. Check the output for predictive, medical, financial and legal phrasing and refuse it. Trap: Translating an English reading into another language after generating it produces headings in one language and body text in another and idioms that land strangely, and people notice immediately. Pass the language into the prompt, require the entire response in it, and check the output before storing rather than trusting the instruction. Done when: Every stored reading carries its layer structure, the model identifier, the prompt version and the language, checked on a real entry. A reading generated in the second language contains no headings in the first, tested by scanning the output. Output containing predictive certainty, medical or dosage language, or financial or legal advice is refused and regenerated once before failing, tested with a forced fixture. The disclaimer renders above the reading, not below it, verified by review. Do not build yet: No export, cost ceiling or deletion yet. ### Phase 5 · It is a diary, so treat it like one Build: Keep the database on the local disk with no account and no telemetry. Export every entry, reading and chart as Markdown files plus a JSON archive. Delete a person or an entry completely, including their charts and readings, in one transaction. Cap monthly model spend and keep the journal, the charts and the history working when the ceiling is reached. Trap: Adding a convenience sync so the journal is available on a phone turns a private diary into an account on somebody's server, and the people most likely to write the most personal entries are the ones least likely to read the setup screen where you mentioned it. Keep it local, and make export the answer to portability. Done when: The application makes exactly two kinds of outbound request, to the model provider and to the geocoder, verified by watching the network through a full session. Export produces Markdown files that read in any editor plus a JSON archive that reimports into an empty database, round-tripped in the suite. Deleting a person removes their charts, entries, readings and flags in one transaction, verified by querying every table afterwards. At the monthly ceiling, new readings are refused with the figure shown and the journal, chart computation and history all continue to work. Do not build yet: Nothing after this. ### Out of scope (and why) - Credits, payments and a paywall, which is the plumbing that turns this into a product and is the part with a payment processor's rules attached. - Compatibility readings between two people, because it means storing a second person's birth details, usually without asking them. - Any claim to predict events, health outcomes or relationships, which the app refuses at generation and would not become true with better wording. - A version aimed at children, which brings in a different set of legal obligations around data from minors and a duty of care this build is not designed for. ### README must contain - That this is for entertainment and self-reflection and is not psychological or medical advice, stated before the setup instructions - Which ephemeris library is used, its version, and which of its licences you are relying on - That an unknown birth time omits houses and the rising sign rather than assuming a time - That entries describing crisis are never interpreted, and where the crisis contacts are configured - That the only outbound requests are to the model provider and the geocoder, with the geocoder's usage policy linked ===== AGENTS.md ===== # Agent instructions · Dreams & Stars indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js 15 with TypeScript, single user, running locally, An ephemeris library, with its licence recorded in the README, A time zone database with historical rules, plus an explicit unknown-time path, A public geocoder used inside its published usage policy, every result cached, SQLite on the local disk, no account, no upload, no telemetry. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Asking a language model where Saturn was on a given date produces a plausible degree and sign that is wrong often enough to be useless and confident enough that nobody checks, and every later layer of the product inherits the error. Compute from the ephemeris, store the numbers, and have the model read the numbers rather than supply them. - Defaulting an unknown birth time to noon silently produces a rising sign and a full set of houses that are simply invented, and they are the first things the reading talks about. Record time_known, omit everything that depends on it, and say in the output which parts could not be computed and why. The same honesty applies to the offset: a birth in 1978 uses that year's rules for that zone, which a naive conversion from the current offset will get wrong by an hour or more. - Running the check on the model's output means the generation has already happened and the person has already been shown an interpretation of the worst night of their life by a dream app. Check the input before any call is made, and treat a false positive as an acceptable cost, because the failure in the other direction is not. - Translating an English reading into another language after generating it produces headings in one language and body text in another and idioms that land strangely, and people notice immediately. Pass the language into the prompt, require the entire response in it, and check the output before storing rather than trusting the instruction. ===== BUILD_PLAN.md ===== # Build plan · Dreams & Stars A personal dream and chart journal whose hard problems are getting the astronomy from an ephemeris rather than from a model, handling birth data whose time is unknown and whose time zone is historical, and keeping a wellness-shaped app honest about what it is and what it will not answer. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Astronomy from an ephemeris, checked against published positions Planetary positions and aspects are computed by a library and verified against known values. ### Steps 1. Install the ephemeris and record the licence decision The licence affects what you may do with the finished thing. Files: `README.md`, `src/astro/ephemeris.ts` 2. Verify against independently published positions Five dates in the suite, with a stated tolerance. Files: `tests/positions.test.ts`, `fixtures/positions.json` 3. Store the chart as numbers, not as text The prose is a view of the data, and it is regenerable. Files: `src/astro/chart.ts` ### Done when - [ ] Five dates match a published source within tolerance - [ ] Ephemeris name, version and licence are recorded - [ ] No prompt asks a model for a position - [ ] Charts are stored as structured data ### Watch out - Asking a language model where Saturn was on a given date produces a plausible degree and sign that is wrong often enough to be useless and confident enough that nobody checks, and every later layer of the product inherits the error. Compute from the ephemeris, store the numbers, and have the model read the numbers rather than supply them. ## Phase 2 · Birth data is the hard input Unknown times are handled honestly and places are geocoded once, inside the geocoder's published policy. ### Steps 1. Make unknown birth time a first-class state It omits houses and the rising sign and explains itself. Files: `src/people/birth.ts`, `tests/unknown-time.test.ts` 2. Convert using historical time zone rules A 1978 birth is not converted with a 2026 offset. Files: `src/astro/time.ts`, `tests/tz-history.test.ts` 3. Cache every geocode and obey the published policy Read the policy, set an identifying user agent, rate limit to it. Files: `src/geo/lookup.ts`, `tests/geo-cache.test.ts` ### Done when - [ ] Unknown time omits houses and the rising sign with a reason - [ ] Historical offsets convert correctly in two cases - [ ] A place is geocoded at most once - [ ] Requests are identified and rate limited to the published policy ### Watch out - Defaulting an unknown birth time to noon silently produces a rising sign and a full set of houses that are simply invented, and they are the first things the reading talks about. Record time_known, omit everything that depends on it, and say in the output which parts could not be computed and why. The same honesty applies to the offset: a birth in 1978 uses that year's rules for that zone, which a naive conversion from the current offset will get wrong by an hour or more. ## Phase 3 · The safety check runs before the model, not after An entry describing crisis is saved, flagged and answered with your configured local contacts instead of an interpretation. ### Steps 1. Screen the input before any model call A spy in the suite proves no call happened. Files: `src/safety/screen.ts`, `tests/safety.test.ts` 2. Require crisis contacts during setup Local to the person, because a generic number helps nobody. Files: `src/setup/contacts.ts` 3. Save the entry, refuse the interpretation The journal keeps the words; the app declines the reading. Files: `src/entries/create.ts` ### Done when - [ ] A flagged entry makes no model call - [ ] Setup cannot complete without crisis contacts - [ ] Flagged entries are still saved and visible - [ ] Oblique phrasing is covered by the fixtures ### Watch out - Running the check on the model's output means the generation has already happened and the person has already been shown an interpretation of the worst night of their life by a dream app. Check the input before any call is made, and treat a false positive as an acceptable cost, because the failure in the other direction is not. ## Phase 4 · Readings that stay inside what they are Generated text is layered, labelled, single-language, and checked against the things it must not say. ### Steps 1. Generate in named layers from the stored numbers The chart data goes in; interpretation comes out. Files: `src/readings/generate.ts`, `src/readings/prompts.ts` 2. Require the whole response in one language Headings included, and check rather than trust. Files: `src/readings/language.ts`, `tests/language.test.ts` 3. Refuse predictive, medical, financial and legal phrasing One regeneration, then a failure the person can see. Files: `src/readings/guard.ts`, `tests/guard.test.ts` ### Done when - [ ] Readings store layers, model, prompt version and language - [ ] No mixed-language output - [ ] Predictive or advisory phrasing is refused - [ ] The disclaimer renders above the reading ### Watch out - Translating an English reading into another language after generating it produces headings in one language and body text in another and idioms that land strangely, and people notice immediately. Pass the language into the prompt, require the entire response in it, and check the output before storing rather than trusting the instruction. ## Not in this build - Credits, payments and a paywall. - Compatibility readings between two people. - Any predictive claim about events, health or relationships. - A version aimed at children. ## After v1, if you want it - A symbol index built from your own entries, which is more useful than any general dream dictionary - Transit computation against a stored chart, once the natal positions are verified and trusted - A yearly review that shows recurring themes by frequency, with the counts visible rather than summarised ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. People, charts, entries, readings and flags. The diary. DATABASE_PATH=./data/journal.db # Required. Ephemeris data files. Its version is stored on every chart. EPHEMERIS_PATH=./data/ephe # Required · secret. The text model. Set a provider-side spend limit as well. MODEL_API_KEY=sk-...redacted # Required. Identifies your client as the geocoder's policy requires. A default library agent is not acceptable. GEOCODER_USER_AGENT=dream-journal/1.0 (you@example.com) # Required. Minimum gap between geocoder calls. Check the current published policy before lowering it. GEOCODER_MIN_INTERVAL_MS=1000 # Required. The whole reading, headings included, is produced in one language. DEFAULT_LANGUAGE=en # Required. Readings stop here. Journal, charts and history keep working. MONTHLY_COST_CEILING=3.00
You are building a production product version of Dreams & Stars. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Dreams & Stars · product brief ## Problem The reading loop itself · collect a dream or birth details, prompt an LLM, stream back an interpretation · is a weekend build with any modern coding agent. What resists cloning is the boring half: real natal-chart math from ephemeris data rather than an LLM guessing planetary positions, a prompt methodology (Jungian + Ibn Sirin + actionable layers) tuned over real usage, a budget-reservation system that stops one runaway API call from burning the month's margin, and the credits/payment plumbing. Build the personal version for your own dream journal, but don't expect the DIY build to hold up as a product. ## Product outcome A private journal where the astronomy is computed rather than imagined, an unknown birth time is admitted instead of filled in, an entry about a crisis gets real contacts rather than an interpretation, and nothing leaves the machine except one model call and one cached place lookup. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Anthropic or OpenAI API key - an ephemeris library for real planetary positions (Swiss Ephemeris bindings), not LLM-guessed astronomy - a database for reading history and credit balances - Stripe or RevenueCat if you want to charge for it ## Explicit non-goals for v1 - Credits, payments and a paywall. - Compatibility readings between two people. - Any predictive claim about events, health or relationships. - A version aimed at children. - the three-layer interpretation methodology (psychological/Jungian, Islamic/Ibn Sirin traditional, actionable) tuned over real usage, not a first draft - real natal-chart accuracy from ephemeris data instead of an LLM approximating planetary positions - a spend-reservation system that caps provider cost before generation starts, so one bad request can't blow the budget - native-quality bilingual Turkish/English copy, not machine-translated - the credits/paywall, Stripe and RevenueCat payment plumbing ## Success criteria - Five known positions are confirmed to match a published source - An unknown birth time is confirmed to omit houses and the rising sign - A flagged entry is confirmed to make no model call - A place is confirmed geocoded at most once - Deleting a person is confirmed to leave no rows in any table ===== BRIEF.md ===== # Build brief · Dreams & Stars The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a private dream and birth-chart journal that computes the astronomy properly and labels the interpretation as interpretation to replace Dreams & Stars. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Three things, and the third one is a licence you have to read before you write any code. First, what this is. The product being replaced describes itself as being for entertainment and self-reflection and not a substitute for professional psychological or medical advice, and that is the honest framing. So the generated text never diagnoses, never predicts an outcome as fact, and never gives guidance about health, medication, pregnancy, money, legal matters or anyone's death. The disclaimer is rendered with the reading, at the top, not parked in a page footer. Second, a dream journal receives whatever the person is carrying. If an entry describes self-harm, suicidal thoughts or being in danger, the app does not interpret it. It saves the entry, says plainly that it is not the right tool for this, and shows the local crisis contacts you configured at setup. This check runs before the model call, not on its output, and there is no bypass. Third, the astronomy. Never let a language model produce a planetary position. Compute the chart from an ephemeris, and read that library's licence first: the Swiss Ephemeris is dual licensed, either under the GNU Affero General Public License, which requires your whole project to be AGPL or a compatible licence, or under a paid professional licence bought from its publisher. For a personal journal the AGPL route is fine. Anything you host for other people is a licence decision you make deliberately and record in the README. ### Stack (fixed, do not substitute) - Runtime: Next.js 15 with TypeScript, single user, running locally · a dream diary is the most private text most people write down - Astronomy: An ephemeris library, with its licence recorded in the README · positions are computed, never generated, and the licence is part of the decision - Time: A time zone database with historical rules, plus an explicit unknown-time path · an offset from 1978 is not today's offset and defaulting to noon silently is a lie - Geocoding: A public geocoder used inside its published usage policy, every result cached · a birthplace is looked up once per place, forever, or you get blocked and deserve it - Storage: SQLite on the local disk, no account, no upload, no telemetry · this is a diary, and diaries live where their owner lives ### Data model (create this before Phase 1) - people(id, label, birth_date, birth_time, time_known, birth_place_id, created_at) · time_known is a real column because an unknown birth time changes what can be computed - places(id, query, display_name, lat, lon, tz_id, source, fetched_at) · the geocoder is called once per place and never again - charts(id, person_id, ephemeris_name, ephemeris_version, computed_at, positions_json, aspects_json, houses_json, houses_omitted_reason) - entries(id, kind, body, created_at, language, mood) · kind is dream or question - readings(id, entry_id, chart_id, body_md, model_id, prompt_version, language, layers_json, created_at, cost_minor) - flags(entry_id, reason, at) · set by the pre-call safety check, and a flagged entry has no reading row - settings(crisis_contacts_md, monthly_cost_ceiling_minor, default_language) - A chart with no birth time has no houses and no rising sign, and the row records why rather than filling them in from a guessed noon. ### Phase 1 · Astronomy from an ephemeris, checked against published positions Build: Install an ephemeris library, record its name, version and licence in the README, and compute positions, aspects and, where a birth time exists, houses. Store the computed chart as data, not as prose. Verify a handful of dates against positions published by an independent source. Trap: Asking a language model where Saturn was on a given date produces a plausible degree and sign that is wrong often enough to be useless and confident enough that nobody checks, and every later layer of the product inherits the error. Compute from the ephemeris, store the numbers, and have the model read the numbers rather than supply them. Done when: Positions for at least five dates match an independent published source within a stated tolerance, asserted in the suite. The ephemeris name, version and licence appear in the README and on every stored chart row. No prompt anywhere asks a model for a position, a sign or an aspect, verified by review of the prompt templates. A chart is stored as structured data and the prose is generated from it, never the other way round. Do not build yet: No birth data handling, journal, readings or safety checks yet. ### Phase 2 · Birth data is the hard input Build: Ask for date, time and place, with an explicit 'time unknown' option that omits houses and the rising sign and says so. Resolve the place once through a public geocoder, store latitude, longitude and time zone identifier, and never call the geocoder for that place again. Convert birth time to UTC using historical time zone rules, not the current offset. Trap: Defaulting an unknown birth time to noon silently produces a rising sign and a full set of houses that are simply invented, and they are the first things the reading talks about. Record time_known, omit everything that depends on it, and say in the output which parts could not be computed and why. The same honesty applies to the offset: a birth in 1978 uses that year's rules for that zone, which a naive conversion from the current offset will get wrong by an hour or more. Done when: A person with time_known false produces a chart with no houses and no rising sign, and the omission reason is stored and rendered, tested. Converting a birth time from a year with different rules produces the correct UTC instant, tested against at least two historical cases including a daylight saving transition. The geocoder is called at most once per distinct place query, asserted by a counter in the suite; a repeated lookup is served from the places table. Outgoing geocoder requests carry an identifying user agent and are rate limited to the geocoder's published maximum, with the policy URL and the date you read it in the README. Do not build yet: No journal, readings, safety checks or export yet. ### Phase 3 · The safety check runs before the model, not after Build: Screen every entry before generation for descriptions of self-harm, suicidal intent or immediate danger. A flagged entry is stored, gets a flag row, and returns the crisis contacts configured at setup with a plain sentence saying this is not the right tool. No reading row is created and there is no override. Trap: Running the check on the model's output means the generation has already happened and the person has already been shown an interpretation of the worst night of their life by a dream app. Check the input before any call is made, and treat a false positive as an acceptable cost, because the failure in the other direction is not. Done when: A flagged entry creates no reading row and makes no model call, asserted by a spy on the client. The crisis contacts shown are the ones configured at setup and setup cannot complete without them, tested by attempting to skip. A flagged entry is still saved and still visible in the journal, because deleting the person's own words is not a safety measure. The screening fixture set includes phrasing that is oblique rather than explicit, and the suite fails if the obvious cases pass through. Do not build yet: No reading generation, language handling, export or cost ceiling yet. ### Phase 4 · Readings that stay inside what they are Build: Generate a reading in named layers: a psychological reading, a traditional reading marked as cultural interpretation, and one practical suggestion. For a chart, read the computed numbers first and interpret second. Emit the whole output including headings in the chosen language. Check the output for predictive, medical, financial and legal phrasing and refuse it. Trap: Translating an English reading into another language after generating it produces headings in one language and body text in another and idioms that land strangely, and people notice immediately. Pass the language into the prompt, require the entire response in it, and check the output before storing rather than trusting the instruction. Done when: Every stored reading carries its layer structure, the model identifier, the prompt version and the language, checked on a real entry. A reading generated in the second language contains no headings in the first, tested by scanning the output. Output containing predictive certainty, medical or dosage language, or financial or legal advice is refused and regenerated once before failing, tested with a forced fixture. The disclaimer renders above the reading, not below it, verified by review. Do not build yet: No export, cost ceiling or deletion yet. ### Phase 5 · It is a diary, so treat it like one Build: Keep the database on the local disk with no account and no telemetry. Export every entry, reading and chart as Markdown files plus a JSON archive. Delete a person or an entry completely, including their charts and readings, in one transaction. Cap monthly model spend and keep the journal, the charts and the history working when the ceiling is reached. Trap: Adding a convenience sync so the journal is available on a phone turns a private diary into an account on somebody's server, and the people most likely to write the most personal entries are the ones least likely to read the setup screen where you mentioned it. Keep it local, and make export the answer to portability. Done when: The application makes exactly two kinds of outbound request, to the model provider and to the geocoder, verified by watching the network through a full session. Export produces Markdown files that read in any editor plus a JSON archive that reimports into an empty database, round-tripped in the suite. Deleting a person removes their charts, entries, readings and flags in one transaction, verified by querying every table afterwards. At the monthly ceiling, new readings are refused with the figure shown and the journal, chart computation and history all continue to work. Do not build yet: Nothing after this. ### Out of scope (and why) - Credits, payments and a paywall, which is the plumbing that turns this into a product and is the part with a payment processor's rules attached. - Compatibility readings between two people, because it means storing a second person's birth details, usually without asking them. - Any claim to predict events, health outcomes or relationships, which the app refuses at generation and would not become true with better wording. - A version aimed at children, which brings in a different set of legal obligations around data from minors and a duty of care this build is not designed for. ### README must contain - That this is for entertainment and self-reflection and is not psychological or medical advice, stated before the setup instructions - Which ephemeris library is used, its version, and which of its licences you are relying on - That an unknown birth time omits houses and the rising sign rather than assuming a time - That entries describing crisis are never interpreted, and where the crisis contacts are configured - That the only outbound requests are to the model provider and the geocoder, with the geocoder's usage policy linked ===== ARCHITECTURE.md ===== # Architecture · Dreams & Stars ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js 15 with TypeScript, single user, running locally | a dream diary is the most private text most people write down | | Astronomy | An ephemeris library, with its licence recorded in the README | positions are computed, never generated, and the licence is part of the decision | | Time | A time zone database with historical rules, plus an explicit unknown-time path | an offset from 1978 is not today's offset and defaulting to noon silently is a lie | | Geocoding | A public geocoder used inside its published usage policy, every result cached | a birthplace is looked up once per place, forever, or you get blocked and deserve it | | Storage | SQLite on the local disk, no account, no upload, no telemetry | this is a diary, and diaries live where their owner lives | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Astronomy | Ephemeris calls, charts and aspects. | Any ephemeris, provided positions are computed and the licence is recorded. | | Birth data | Unknown time, historical offsets and cached places. | Any geocoder, provided its policy is read and its results are cached. | | Safety | Pre-call screening and the crisis response. | Not swappable for a post-generation check, which is too late by definition. | | Readings | Layered generation, language and the output guard. | Any model, provided the guard runs before storing. | | Journal | Local storage, export and complete deletion. | Any storage that stays on the person's own machine. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_PATH` · required · People, charts, entries, readings and flags. The diary. - `EPHEMERIS_PATH` · required · Ephemeris data files. Its version is stored on every chart. - `MODEL_API_KEY` · required, secret · The text model. Set a provider-side spend limit as well. - `GEOCODER_USER_AGENT` · required · Identifies your client as the geocoder's policy requires. A default library agent is not acceptable. - `GEOCODER_MIN_INTERVAL_MS` · required · Minimum gap between geocoder calls. Check the current published policy before lowering it. - `DEFAULT_LANGUAGE` · required · The whole reading, headings included, is produced in one language. - `MONTHLY_COST_CEILING` · required · Readings stop here. Journal, charts and history keep working. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Dreams & Stars product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Next.js 15 with TypeScript, single user, running locally, An ephemeris library, with its licence recorded in the README, A time zone database with historical rules, plus an explicit unknown-time path, A public geocoder used inside its published usage policy, every result cached, SQLite on the local disk, no account, no upload, no telemetry. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Asking a language model where Saturn was on a given date produces a plausible degree and sign that is wrong often enough to be useless and confident enough that nobody checks, and every later layer of the product inherits the error. Compute from the ephemeris, store the numbers, and have the model read the numbers rather than supply them. - Defaulting an unknown birth time to noon silently produces a rising sign and a full set of houses that are simply invented, and they are the first things the reading talks about. Record time_known, omit everything that depends on it, and say in the output which parts could not be computed and why. The same honesty applies to the offset: a birth in 1978 uses that year's rules for that zone, which a naive conversion from the current offset will get wrong by an hour or more. - Running the check on the model's output means the generation has already happened and the person has already been shown an interpretation of the worst night of their life by a dream app. Check the input before any call is made, and treat a false positive as an acceptable cost, because the failure in the other direction is not. - Translating an English reading into another language after generating it produces headings in one language and body text in another and idioms that land strangely, and people notice immediately. Pass the language into the prompt, require the entire response in it, and check the output before storing rather than trusting the instruction. - Adding a convenience sync so the journal is available on a phone turns a private diary into an account on somebody's server, and the people most likely to write the most personal entries are the ones least likely to read the setup screen where you mentioned it. Keep it local, and make export the answer to portability. ===== MILESTONES.md ===== # Delivery milestones · Dreams & Stars Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Astronomy from an ephemeris, checked against published positions Planetary positions and aspects are computed by a library and verified against known values. ### Steps 1. Install the ephemeris and record the licence decision The licence affects what you may do with the finished thing. Files: `README.md`, `src/astro/ephemeris.ts` 2. Verify against independently published positions Five dates in the suite, with a stated tolerance. Files: `tests/positions.test.ts`, `fixtures/positions.json` 3. Store the chart as numbers, not as text The prose is a view of the data, and it is regenerable. Files: `src/astro/chart.ts` ### Done when - [ ] Five dates match a published source within tolerance - [ ] Ephemeris name, version and licence are recorded - [ ] No prompt asks a model for a position - [ ] Charts are stored as structured data ### Watch out - Asking a language model where Saturn was on a given date produces a plausible degree and sign that is wrong often enough to be useless and confident enough that nobody checks, and every later layer of the product inherits the error. Compute from the ephemeris, store the numbers, and have the model read the numbers rather than supply them. ## M2 · Birth data is the hard input Unknown times are handled honestly and places are geocoded once, inside the geocoder's published policy. ### Steps 1. Make unknown birth time a first-class state It omits houses and the rising sign and explains itself. Files: `src/people/birth.ts`, `tests/unknown-time.test.ts` 2. Convert using historical time zone rules A 1978 birth is not converted with a 2026 offset. Files: `src/astro/time.ts`, `tests/tz-history.test.ts` 3. Cache every geocode and obey the published policy Read the policy, set an identifying user agent, rate limit to it. Files: `src/geo/lookup.ts`, `tests/geo-cache.test.ts` ### Done when - [ ] Unknown time omits houses and the rising sign with a reason - [ ] Historical offsets convert correctly in two cases - [ ] A place is geocoded at most once - [ ] Requests are identified and rate limited to the published policy ### Watch out - Defaulting an unknown birth time to noon silently produces a rising sign and a full set of houses that are simply invented, and they are the first things the reading talks about. Record time_known, omit everything that depends on it, and say in the output which parts could not be computed and why. The same honesty applies to the offset: a birth in 1978 uses that year's rules for that zone, which a naive conversion from the current offset will get wrong by an hour or more. ## M3 · The safety check runs before the model, not after An entry describing crisis is saved, flagged and answered with your configured local contacts instead of an interpretation. ### Steps 1. Screen the input before any model call A spy in the suite proves no call happened. Files: `src/safety/screen.ts`, `tests/safety.test.ts` 2. Require crisis contacts during setup Local to the person, because a generic number helps nobody. Files: `src/setup/contacts.ts` 3. Save the entry, refuse the interpretation The journal keeps the words; the app declines the reading. Files: `src/entries/create.ts` ### Done when - [ ] A flagged entry makes no model call - [ ] Setup cannot complete without crisis contacts - [ ] Flagged entries are still saved and visible - [ ] Oblique phrasing is covered by the fixtures ### Watch out - Running the check on the model's output means the generation has already happened and the person has already been shown an interpretation of the worst night of their life by a dream app. Check the input before any call is made, and treat a false positive as an acceptable cost, because the failure in the other direction is not. ## M4 · Readings that stay inside what they are Generated text is layered, labelled, single-language, and checked against the things it must not say. ### Steps 1. Generate in named layers from the stored numbers The chart data goes in; interpretation comes out. Files: `src/readings/generate.ts`, `src/readings/prompts.ts` 2. Require the whole response in one language Headings included, and check rather than trust. Files: `src/readings/language.ts`, `tests/language.test.ts` 3. Refuse predictive, medical, financial and legal phrasing One regeneration, then a failure the person can see. Files: `src/readings/guard.ts`, `tests/guard.test.ts` ### Done when - [ ] Readings store layers, model, prompt version and language - [ ] No mixed-language output - [ ] Predictive or advisory phrasing is refused - [ ] The disclaimer renders above the reading ### Watch out - Translating an English reading into another language after generating it produces headings in one language and body text in another and idioms that land strangely, and people notice immediately. Pass the language into the prompt, require the entire response in it, and check the output before storing rather than trusting the instruction. ## M5 · It is a diary, so treat it like one (production only) Everything stays local, exports as plain files, deletes completely, and the model spend has a ceiling. ### Steps 1. No account, no telemetry, two outbound destinations Watch the network for a full session and count them. Files: `src/app/layout.tsx`, `tests/network.test.ts` 2. Export to Markdown plus a JSON archive One for reading years later, one for reimporting. Files: `src/export/journal.ts`, `tests/roundtrip.test.ts` 3. Delete completely, in one transaction A diary you cannot fully delete is not private. Files: `src/people/delete.ts`, `tests/delete.test.ts` ### Done when - [ ] Only the model and geocoder are contacted - [ ] Export and reimport round-trip the journal - [ ] Deleting a person leaves no rows anywhere - [ ] The ceiling stops readings, not the journal ### Watch out - Adding a convenience sync so the journal is available on a phone turns a private diary into an account on somebody's server, and the people most likely to write the most personal entries are the ones least likely to read the setup screen where you mentioned it. Keep it local, and make export the answer to portability. ===== OPERATIONS.md ===== # Operations · Dreams & Stars ## Backup The database, which is the diary. Charts are recomputable from birth data and readings are regenerable, but the entries are not, so a backup that only covers derived tables covers nothing. ## Restore Restore and confirm that charts recompute to the same positions with the recorded ephemeris version, and that flagged entries are still flagged. A restore that loses flags could offer an interpretation the build previously refused. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local tool this lives in the suite: no model call on a flagged entry, no mixed-language output, no geocoder call for a cached place, and spend against the ceiling. ## Incident checklist The incident that matters is a reading someone took as advice. Storing the model identifier, prompt version and layer structure with each reading is what lets you see exactly what was shown and change the prompt rather than argue about it. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Five known positions are confirmed to match a published source - [ ] An unknown birth time is confirmed to omit houses and the rising sign - [ ] A flagged entry is confirmed to make no model call - [ ] A place is confirmed geocoded at most once - [ ] Deleting a person is confirmed to leave no rows in any table ## Launch constraint Do not market omitted Dreams & Stars capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. People, charts, entries, readings and flags. The diary. DATABASE_PATH=./data/journal.db # Required. Ephemeris data files. Its version is stored on every chart. EPHEMERIS_PATH=./data/ephe # Required · secret. The text model. Set a provider-side spend limit as well. MODEL_API_KEY=sk-...redacted # Required. Identifies your client as the geocoder's policy requires. A default library agent is not acceptable. GEOCODER_USER_AGENT=dream-journal/1.0 (you@example.com) # Required. Minimum gap between geocoder calls. Check the current published policy before lowering it. GEOCODER_MIN_INTERVAL_MS=1000 # Required. The whole reading, headings included, is produced in one language. DEFAULT_LANGUAGE=en # Required. Readings stop here. Journal, charts and history keep working. MONTHLY_COST_CEILING=3.00
# Dreams & Stars · indie build A personal dream and chart journal whose hard problems are getting the astronomy from an ephemeris rather than from a model, handling birth data whose time is unknown and whose time zone is historical, and keeping a wellness-shaped app honest about what it is and what it will not answer. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js 15 with TypeScript, single user, running locally | a dream diary is the most private text most people write down | | Astronomy | An ephemeris library, with its licence recorded in the README | positions are computed, never generated, and the licence is part of the decision | | Time | A time zone database with historical rules, plus an explicit unknown-time path | an offset from 1978 is not today's offset and defaulting to noon silently is a lie | | Geocoding | A public geocoder used inside its published usage policy, every result cached | a birthplace is looked up once per place, forever, or you get blocked and deserve it | | Storage | SQLite on the local disk, no account, no upload, no telemetry | this is a diary, and diaries live where their owner lives | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the application. - Get it: nodejs.org, or your version manager. - Verify: node --version prints v22 or higher - [ ] **An ephemeris library plus its data files** · free under its open-source licence; a paid licence exists for closed distribution - Why: Every planetary position in this build comes from here, and the library's licence constrains what you may do with the result. - Get it: Install the bindings for your runtime, fetch the data files, and read the licence terms in full before you build on it. - Verify: Five known dates match a published source within your stated tolerance - [ ] **An API key for a text model** · pay per call; a reading is fractions of a penny - Why: The interpretation is one call per reading and is the only generated part of the product. - Get it: Create a key with the provider you prefer and set a spend limit in their console as well as the ceiling here. - Verify: One reading generates in the configured language with its model identifier stored - [ ] **A geocoder and its published usage policy** · free within the published policy - Why: Birthplace lookup is the one thing here that hits somebody else's free service, and their policy sets the rate, the identification requirement and the caching obligation. - Get it: Read the policy for the geocoder you choose, note the maximum rate and the identification requirement, and record the date you read it. - Verify: Your client sends an identifying user agent, respects the stated rate, and caches every result - [ ] **The crisis contacts for your country** · free - Why: The safety check refuses to interpret certain entries and has to offer something real in their place, which is local and cannot be guessed by the build. - Get it: Look up the current helpline numbers and text services where you live and write them into setup. - Verify: Setup refuses to complete with the field empty and the contacts render on a flagged entry ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - Credits, payments and a paywall. - Compatibility readings between two people. - Any predictive claim about events, health or relationships. - A version aimed at children. - the three-layer interpretation methodology (psychological/Jungian, Islamic/Ibn Sirin traditional, actionable) tuned over real usage, not a first draft - real natal-chart accuracy from ephemeris data instead of an LLM approximating planetary positions - a spend-reservation system that caps provider cost before generation starts, so one bad request can't blow the budget - native-quality bilingual Turkish/English copy, not machine-translated - the credits/paywall, Stripe and RevenueCat payment plumbing If one of those is essential to you, that is the reason to keep paying for Dreams & Stars, and the README should say so rather than pretend.
# Build brief · Dreams & Stars The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a private dream and birth-chart journal that computes the astronomy properly and labels the interpretation as interpretation to replace Dreams & Stars. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Three things, and the third one is a licence you have to read before you write any code. First, what this is. The product being replaced describes itself as being for entertainment and self-reflection and not a substitute for professional psychological or medical advice, and that is the honest framing. So the generated text never diagnoses, never predicts an outcome as fact, and never gives guidance about health, medication, pregnancy, money, legal matters or anyone's death. The disclaimer is rendered with the reading, at the top, not parked in a page footer. Second, a dream journal receives whatever the person is carrying. If an entry describes self-harm, suicidal thoughts or being in danger, the app does not interpret it. It saves the entry, says plainly that it is not the right tool for this, and shows the local crisis contacts you configured at setup. This check runs before the model call, not on its output, and there is no bypass. Third, the astronomy. Never let a language model produce a planetary position. Compute the chart from an ephemeris, and read that library's licence first: the Swiss Ephemeris is dual licensed, either under the GNU Affero General Public License, which requires your whole project to be AGPL or a compatible licence, or under a paid professional licence bought from its publisher. For a personal journal the AGPL route is fine. Anything you host for other people is a licence decision you make deliberately and record in the README. ### Stack (fixed, do not substitute) - Runtime: Next.js 15 with TypeScript, single user, running locally · a dream diary is the most private text most people write down - Astronomy: An ephemeris library, with its licence recorded in the README · positions are computed, never generated, and the licence is part of the decision - Time: A time zone database with historical rules, plus an explicit unknown-time path · an offset from 1978 is not today's offset and defaulting to noon silently is a lie - Geocoding: A public geocoder used inside its published usage policy, every result cached · a birthplace is looked up once per place, forever, or you get blocked and deserve it - Storage: SQLite on the local disk, no account, no upload, no telemetry · this is a diary, and diaries live where their owner lives ### Data model (create this before Phase 1) - people(id, label, birth_date, birth_time, time_known, birth_place_id, created_at) · time_known is a real column because an unknown birth time changes what can be computed - places(id, query, display_name, lat, lon, tz_id, source, fetched_at) · the geocoder is called once per place and never again - charts(id, person_id, ephemeris_name, ephemeris_version, computed_at, positions_json, aspects_json, houses_json, houses_omitted_reason) - entries(id, kind, body, created_at, language, mood) · kind is dream or question - readings(id, entry_id, chart_id, body_md, model_id, prompt_version, language, layers_json, created_at, cost_minor) - flags(entry_id, reason, at) · set by the pre-call safety check, and a flagged entry has no reading row - settings(crisis_contacts_md, monthly_cost_ceiling_minor, default_language) - A chart with no birth time has no houses and no rising sign, and the row records why rather than filling them in from a guessed noon. ### Phase 1 · Astronomy from an ephemeris, checked against published positions Build: Install an ephemeris library, record its name, version and licence in the README, and compute positions, aspects and, where a birth time exists, houses. Store the computed chart as data, not as prose. Verify a handful of dates against positions published by an independent source. Trap: Asking a language model where Saturn was on a given date produces a plausible degree and sign that is wrong often enough to be useless and confident enough that nobody checks, and every later layer of the product inherits the error. Compute from the ephemeris, store the numbers, and have the model read the numbers rather than supply them. Done when: Positions for at least five dates match an independent published source within a stated tolerance, asserted in the suite. The ephemeris name, version and licence appear in the README and on every stored chart row. No prompt anywhere asks a model for a position, a sign or an aspect, verified by review of the prompt templates. A chart is stored as structured data and the prose is generated from it, never the other way round. Do not build yet: No birth data handling, journal, readings or safety checks yet. ### Phase 2 · Birth data is the hard input Build: Ask for date, time and place, with an explicit 'time unknown' option that omits houses and the rising sign and says so. Resolve the place once through a public geocoder, store latitude, longitude and time zone identifier, and never call the geocoder for that place again. Convert birth time to UTC using historical time zone rules, not the current offset. Trap: Defaulting an unknown birth time to noon silently produces a rising sign and a full set of houses that are simply invented, and they are the first things the reading talks about. Record time_known, omit everything that depends on it, and say in the output which parts could not be computed and why. The same honesty applies to the offset: a birth in 1978 uses that year's rules for that zone, which a naive conversion from the current offset will get wrong by an hour or more. Done when: A person with time_known false produces a chart with no houses and no rising sign, and the omission reason is stored and rendered, tested. Converting a birth time from a year with different rules produces the correct UTC instant, tested against at least two historical cases including a daylight saving transition. The geocoder is called at most once per distinct place query, asserted by a counter in the suite; a repeated lookup is served from the places table. Outgoing geocoder requests carry an identifying user agent and are rate limited to the geocoder's published maximum, with the policy URL and the date you read it in the README. Do not build yet: No journal, readings, safety checks or export yet. ### Phase 3 · The safety check runs before the model, not after Build: Screen every entry before generation for descriptions of self-harm, suicidal intent or immediate danger. A flagged entry is stored, gets a flag row, and returns the crisis contacts configured at setup with a plain sentence saying this is not the right tool. No reading row is created and there is no override. Trap: Running the check on the model's output means the generation has already happened and the person has already been shown an interpretation of the worst night of their life by a dream app. Check the input before any call is made, and treat a false positive as an acceptable cost, because the failure in the other direction is not. Done when: A flagged entry creates no reading row and makes no model call, asserted by a spy on the client. The crisis contacts shown are the ones configured at setup and setup cannot complete without them, tested by attempting to skip. A flagged entry is still saved and still visible in the journal, because deleting the person's own words is not a safety measure. The screening fixture set includes phrasing that is oblique rather than explicit, and the suite fails if the obvious cases pass through. Do not build yet: No reading generation, language handling, export or cost ceiling yet. ### Phase 4 · Readings that stay inside what they are Build: Generate a reading in named layers: a psychological reading, a traditional reading marked as cultural interpretation, and one practical suggestion. For a chart, read the computed numbers first and interpret second. Emit the whole output including headings in the chosen language. Check the output for predictive, medical, financial and legal phrasing and refuse it. Trap: Translating an English reading into another language after generating it produces headings in one language and body text in another and idioms that land strangely, and people notice immediately. Pass the language into the prompt, require the entire response in it, and check the output before storing rather than trusting the instruction. Done when: Every stored reading carries its layer structure, the model identifier, the prompt version and the language, checked on a real entry. A reading generated in the second language contains no headings in the first, tested by scanning the output. Output containing predictive certainty, medical or dosage language, or financial or legal advice is refused and regenerated once before failing, tested with a forced fixture. The disclaimer renders above the reading, not below it, verified by review. Do not build yet: No export, cost ceiling or deletion yet. ### Phase 5 · It is a diary, so treat it like one Build: Keep the database on the local disk with no account and no telemetry. Export every entry, reading and chart as Markdown files plus a JSON archive. Delete a person or an entry completely, including their charts and readings, in one transaction. Cap monthly model spend and keep the journal, the charts and the history working when the ceiling is reached. Trap: Adding a convenience sync so the journal is available on a phone turns a private diary into an account on somebody's server, and the people most likely to write the most personal entries are the ones least likely to read the setup screen where you mentioned it. Keep it local, and make export the answer to portability. Done when: The application makes exactly two kinds of outbound request, to the model provider and to the geocoder, verified by watching the network through a full session. Export produces Markdown files that read in any editor plus a JSON archive that reimports into an empty database, round-tripped in the suite. Deleting a person removes their charts, entries, readings and flags in one transaction, verified by querying every table afterwards. At the monthly ceiling, new readings are refused with the figure shown and the journal, chart computation and history all continue to work. Do not build yet: Nothing after this. ### Out of scope (and why) - Credits, payments and a paywall, which is the plumbing that turns this into a product and is the part with a payment processor's rules attached. - Compatibility readings between two people, because it means storing a second person's birth details, usually without asking them. - Any claim to predict events, health outcomes or relationships, which the app refuses at generation and would not become true with better wording. - A version aimed at children, which brings in a different set of legal obligations around data from minors and a duty of care this build is not designed for. ### README must contain - That this is for entertainment and self-reflection and is not psychological or medical advice, stated before the setup instructions - Which ephemeris library is used, its version, and which of its licences you are relying on - That an unknown birth time omits houses and the rising sign rather than assuming a time - That entries describing crisis are never interpreted, and where the crisis contacts are configured - That the only outbound requests are to the model provider and the geocoder, with the geocoder's usage policy linked
# Agent instructions · Dreams & Stars indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Next.js 15 with TypeScript, single user, running locally, An ephemeris library, with its licence recorded in the README, A time zone database with historical rules, plus an explicit unknown-time path, A public geocoder used inside its published usage policy, every result cached, SQLite on the local disk, no account, no upload, no telemetry. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - Asking a language model where Saturn was on a given date produces a plausible degree and sign that is wrong often enough to be useless and confident enough that nobody checks, and every later layer of the product inherits the error. Compute from the ephemeris, store the numbers, and have the model read the numbers rather than supply them. - Defaulting an unknown birth time to noon silently produces a rising sign and a full set of houses that are simply invented, and they are the first things the reading talks about. Record time_known, omit everything that depends on it, and say in the output which parts could not be computed and why. The same honesty applies to the offset: a birth in 1978 uses that year's rules for that zone, which a naive conversion from the current offset will get wrong by an hour or more. - Running the check on the model's output means the generation has already happened and the person has already been shown an interpretation of the worst night of their life by a dream app. Check the input before any call is made, and treat a false positive as an acceptable cost, because the failure in the other direction is not. - Translating an English reading into another language after generating it produces headings in one language and body text in another and idioms that land strangely, and people notice immediately. Pass the language into the prompt, require the entire response in it, and check the output before storing rather than trusting the instruction.
# Build plan · Dreams & Stars A personal dream and chart journal whose hard problems are getting the astronomy from an ephemeris rather than from a model, handling birth data whose time is unknown and whose time zone is historical, and keeping a wellness-shaped app honest about what it is and what it will not answer. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Astronomy from an ephemeris, checked against published positions Planetary positions and aspects are computed by a library and verified against known values. ### Steps 1. Install the ephemeris and record the licence decision The licence affects what you may do with the finished thing. Files: `README.md`, `src/astro/ephemeris.ts` 2. Verify against independently published positions Five dates in the suite, with a stated tolerance. Files: `tests/positions.test.ts`, `fixtures/positions.json` 3. Store the chart as numbers, not as text The prose is a view of the data, and it is regenerable. Files: `src/astro/chart.ts` ### Done when - [ ] Five dates match a published source within tolerance - [ ] Ephemeris name, version and licence are recorded - [ ] No prompt asks a model for a position - [ ] Charts are stored as structured data ### Watch out - Asking a language model where Saturn was on a given date produces a plausible degree and sign that is wrong often enough to be useless and confident enough that nobody checks, and every later layer of the product inherits the error. Compute from the ephemeris, store the numbers, and have the model read the numbers rather than supply them. ## Phase 2 · Birth data is the hard input Unknown times are handled honestly and places are geocoded once, inside the geocoder's published policy. ### Steps 1. Make unknown birth time a first-class state It omits houses and the rising sign and explains itself. Files: `src/people/birth.ts`, `tests/unknown-time.test.ts` 2. Convert using historical time zone rules A 1978 birth is not converted with a 2026 offset. Files: `src/astro/time.ts`, `tests/tz-history.test.ts` 3. Cache every geocode and obey the published policy Read the policy, set an identifying user agent, rate limit to it. Files: `src/geo/lookup.ts`, `tests/geo-cache.test.ts` ### Done when - [ ] Unknown time omits houses and the rising sign with a reason - [ ] Historical offsets convert correctly in two cases - [ ] A place is geocoded at most once - [ ] Requests are identified and rate limited to the published policy ### Watch out - Defaulting an unknown birth time to noon silently produces a rising sign and a full set of houses that are simply invented, and they are the first things the reading talks about. Record time_known, omit everything that depends on it, and say in the output which parts could not be computed and why. The same honesty applies to the offset: a birth in 1978 uses that year's rules for that zone, which a naive conversion from the current offset will get wrong by an hour or more. ## Phase 3 · The safety check runs before the model, not after An entry describing crisis is saved, flagged and answered with your configured local contacts instead of an interpretation. ### Steps 1. Screen the input before any model call A spy in the suite proves no call happened. Files: `src/safety/screen.ts`, `tests/safety.test.ts` 2. Require crisis contacts during setup Local to the person, because a generic number helps nobody. Files: `src/setup/contacts.ts` 3. Save the entry, refuse the interpretation The journal keeps the words; the app declines the reading. Files: `src/entries/create.ts` ### Done when - [ ] A flagged entry makes no model call - [ ] Setup cannot complete without crisis contacts - [ ] Flagged entries are still saved and visible - [ ] Oblique phrasing is covered by the fixtures ### Watch out - Running the check on the model's output means the generation has already happened and the person has already been shown an interpretation of the worst night of their life by a dream app. Check the input before any call is made, and treat a false positive as an acceptable cost, because the failure in the other direction is not. ## Phase 4 · Readings that stay inside what they are Generated text is layered, labelled, single-language, and checked against the things it must not say. ### Steps 1. Generate in named layers from the stored numbers The chart data goes in; interpretation comes out. Files: `src/readings/generate.ts`, `src/readings/prompts.ts` 2. Require the whole response in one language Headings included, and check rather than trust. Files: `src/readings/language.ts`, `tests/language.test.ts` 3. Refuse predictive, medical, financial and legal phrasing One regeneration, then a failure the person can see. Files: `src/readings/guard.ts`, `tests/guard.test.ts` ### Done when - [ ] Readings store layers, model, prompt version and language - [ ] No mixed-language output - [ ] Predictive or advisory phrasing is refused - [ ] The disclaimer renders above the reading ### Watch out - Translating an English reading into another language after generating it produces headings in one language and body text in another and idioms that land strangely, and people notice immediately. Pass the language into the prompt, require the entire response in it, and check the output before storing rather than trusting the instruction. ## Not in this build - Credits, payments and a paywall. - Compatibility readings between two people. - Any predictive claim about events, health or relationships. - A version aimed at children. ## After v1, if you want it - A symbol index built from your own entries, which is more useful than any general dream dictionary - Transit computation against a stored chart, once the natal positions are verified and trusted - A yearly review that shows recurring themes by frequency, with the counts visible rather than summarised
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. People, charts, entries, readings and flags. The diary. DATABASE_PATH=./data/journal.db # Required. Ephemeris data files. Its version is stored on every chart. EPHEMERIS_PATH=./data/ephe # Required · secret. The text model. Set a provider-side spend limit as well. MODEL_API_KEY=sk-...redacted # Required. Identifies your client as the geocoder's policy requires. A default library agent is not acceptable. GEOCODER_USER_AGENT=dream-journal/1.0 (you@example.com) # Required. Minimum gap between geocoder calls. Check the current published policy before lowering it. GEOCODER_MIN_INTERVAL_MS=1000 # Required. The whole reading, headings included, is produced in one language. DEFAULT_LANGUAGE=en # Required. Readings stop here. Journal, charts and history keep working. MONTHLY_COST_CEILING=3.00
# Dreams & Stars · product brief ## Problem The reading loop itself · collect a dream or birth details, prompt an LLM, stream back an interpretation · is a weekend build with any modern coding agent. What resists cloning is the boring half: real natal-chart math from ephemeris data rather than an LLM guessing planetary positions, a prompt methodology (Jungian + Ibn Sirin + actionable layers) tuned over real usage, a budget-reservation system that stops one runaway API call from burning the month's margin, and the credits/payment plumbing. Build the personal version for your own dream journal, but don't expect the DIY build to hold up as a product. ## Product outcome A private journal where the astronomy is computed rather than imagined, an unknown birth time is admitted instead of filled in, an entry about a crisis gets real contacts rather than an interpretation, and nothing leaves the machine except one model call and one cached place lookup. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Anthropic or OpenAI API key - an ephemeris library for real planetary positions (Swiss Ephemeris bindings), not LLM-guessed astronomy - a database for reading history and credit balances - Stripe or RevenueCat if you want to charge for it ## Explicit non-goals for v1 - Credits, payments and a paywall. - Compatibility readings between two people. - Any predictive claim about events, health or relationships. - A version aimed at children. - the three-layer interpretation methodology (psychological/Jungian, Islamic/Ibn Sirin traditional, actionable) tuned over real usage, not a first draft - real natal-chart accuracy from ephemeris data instead of an LLM approximating planetary positions - a spend-reservation system that caps provider cost before generation starts, so one bad request can't blow the budget - native-quality bilingual Turkish/English copy, not machine-translated - the credits/paywall, Stripe and RevenueCat payment plumbing ## Success criteria - Five known positions are confirmed to match a published source - An unknown birth time is confirmed to omit houses and the rising sign - A flagged entry is confirmed to make no model call - A place is confirmed geocoded at most once - Deleting a person is confirmed to leave no rows in any table
# Build brief · Dreams & Stars The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a private dream and birth-chart journal that computes the astronomy properly and labels the interpretation as interpretation to replace Dreams & Stars. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Three things, and the third one is a licence you have to read before you write any code. First, what this is. The product being replaced describes itself as being for entertainment and self-reflection and not a substitute for professional psychological or medical advice, and that is the honest framing. So the generated text never diagnoses, never predicts an outcome as fact, and never gives guidance about health, medication, pregnancy, money, legal matters or anyone's death. The disclaimer is rendered with the reading, at the top, not parked in a page footer. Second, a dream journal receives whatever the person is carrying. If an entry describes self-harm, suicidal thoughts or being in danger, the app does not interpret it. It saves the entry, says plainly that it is not the right tool for this, and shows the local crisis contacts you configured at setup. This check runs before the model call, not on its output, and there is no bypass. Third, the astronomy. Never let a language model produce a planetary position. Compute the chart from an ephemeris, and read that library's licence first: the Swiss Ephemeris is dual licensed, either under the GNU Affero General Public License, which requires your whole project to be AGPL or a compatible licence, or under a paid professional licence bought from its publisher. For a personal journal the AGPL route is fine. Anything you host for other people is a licence decision you make deliberately and record in the README. ### Stack (fixed, do not substitute) - Runtime: Next.js 15 with TypeScript, single user, running locally · a dream diary is the most private text most people write down - Astronomy: An ephemeris library, with its licence recorded in the README · positions are computed, never generated, and the licence is part of the decision - Time: A time zone database with historical rules, plus an explicit unknown-time path · an offset from 1978 is not today's offset and defaulting to noon silently is a lie - Geocoding: A public geocoder used inside its published usage policy, every result cached · a birthplace is looked up once per place, forever, or you get blocked and deserve it - Storage: SQLite on the local disk, no account, no upload, no telemetry · this is a diary, and diaries live where their owner lives ### Data model (create this before Phase 1) - people(id, label, birth_date, birth_time, time_known, birth_place_id, created_at) · time_known is a real column because an unknown birth time changes what can be computed - places(id, query, display_name, lat, lon, tz_id, source, fetched_at) · the geocoder is called once per place and never again - charts(id, person_id, ephemeris_name, ephemeris_version, computed_at, positions_json, aspects_json, houses_json, houses_omitted_reason) - entries(id, kind, body, created_at, language, mood) · kind is dream or question - readings(id, entry_id, chart_id, body_md, model_id, prompt_version, language, layers_json, created_at, cost_minor) - flags(entry_id, reason, at) · set by the pre-call safety check, and a flagged entry has no reading row - settings(crisis_contacts_md, monthly_cost_ceiling_minor, default_language) - A chart with no birth time has no houses and no rising sign, and the row records why rather than filling them in from a guessed noon. ### Phase 1 · Astronomy from an ephemeris, checked against published positions Build: Install an ephemeris library, record its name, version and licence in the README, and compute positions, aspects and, where a birth time exists, houses. Store the computed chart as data, not as prose. Verify a handful of dates against positions published by an independent source. Trap: Asking a language model where Saturn was on a given date produces a plausible degree and sign that is wrong often enough to be useless and confident enough that nobody checks, and every later layer of the product inherits the error. Compute from the ephemeris, store the numbers, and have the model read the numbers rather than supply them. Done when: Positions for at least five dates match an independent published source within a stated tolerance, asserted in the suite. The ephemeris name, version and licence appear in the README and on every stored chart row. No prompt anywhere asks a model for a position, a sign or an aspect, verified by review of the prompt templates. A chart is stored as structured data and the prose is generated from it, never the other way round. Do not build yet: No birth data handling, journal, readings or safety checks yet. ### Phase 2 · Birth data is the hard input Build: Ask for date, time and place, with an explicit 'time unknown' option that omits houses and the rising sign and says so. Resolve the place once through a public geocoder, store latitude, longitude and time zone identifier, and never call the geocoder for that place again. Convert birth time to UTC using historical time zone rules, not the current offset. Trap: Defaulting an unknown birth time to noon silently produces a rising sign and a full set of houses that are simply invented, and they are the first things the reading talks about. Record time_known, omit everything that depends on it, and say in the output which parts could not be computed and why. The same honesty applies to the offset: a birth in 1978 uses that year's rules for that zone, which a naive conversion from the current offset will get wrong by an hour or more. Done when: A person with time_known false produces a chart with no houses and no rising sign, and the omission reason is stored and rendered, tested. Converting a birth time from a year with different rules produces the correct UTC instant, tested against at least two historical cases including a daylight saving transition. The geocoder is called at most once per distinct place query, asserted by a counter in the suite; a repeated lookup is served from the places table. Outgoing geocoder requests carry an identifying user agent and are rate limited to the geocoder's published maximum, with the policy URL and the date you read it in the README. Do not build yet: No journal, readings, safety checks or export yet. ### Phase 3 · The safety check runs before the model, not after Build: Screen every entry before generation for descriptions of self-harm, suicidal intent or immediate danger. A flagged entry is stored, gets a flag row, and returns the crisis contacts configured at setup with a plain sentence saying this is not the right tool. No reading row is created and there is no override. Trap: Running the check on the model's output means the generation has already happened and the person has already been shown an interpretation of the worst night of their life by a dream app. Check the input before any call is made, and treat a false positive as an acceptable cost, because the failure in the other direction is not. Done when: A flagged entry creates no reading row and makes no model call, asserted by a spy on the client. The crisis contacts shown are the ones configured at setup and setup cannot complete without them, tested by attempting to skip. A flagged entry is still saved and still visible in the journal, because deleting the person's own words is not a safety measure. The screening fixture set includes phrasing that is oblique rather than explicit, and the suite fails if the obvious cases pass through. Do not build yet: No reading generation, language handling, export or cost ceiling yet. ### Phase 4 · Readings that stay inside what they are Build: Generate a reading in named layers: a psychological reading, a traditional reading marked as cultural interpretation, and one practical suggestion. For a chart, read the computed numbers first and interpret second. Emit the whole output including headings in the chosen language. Check the output for predictive, medical, financial and legal phrasing and refuse it. Trap: Translating an English reading into another language after generating it produces headings in one language and body text in another and idioms that land strangely, and people notice immediately. Pass the language into the prompt, require the entire response in it, and check the output before storing rather than trusting the instruction. Done when: Every stored reading carries its layer structure, the model identifier, the prompt version and the language, checked on a real entry. A reading generated in the second language contains no headings in the first, tested by scanning the output. Output containing predictive certainty, medical or dosage language, or financial or legal advice is refused and regenerated once before failing, tested with a forced fixture. The disclaimer renders above the reading, not below it, verified by review. Do not build yet: No export, cost ceiling or deletion yet. ### Phase 5 · It is a diary, so treat it like one Build: Keep the database on the local disk with no account and no telemetry. Export every entry, reading and chart as Markdown files plus a JSON archive. Delete a person or an entry completely, including their charts and readings, in one transaction. Cap monthly model spend and keep the journal, the charts and the history working when the ceiling is reached. Trap: Adding a convenience sync so the journal is available on a phone turns a private diary into an account on somebody's server, and the people most likely to write the most personal entries are the ones least likely to read the setup screen where you mentioned it. Keep it local, and make export the answer to portability. Done when: The application makes exactly two kinds of outbound request, to the model provider and to the geocoder, verified by watching the network through a full session. Export produces Markdown files that read in any editor plus a JSON archive that reimports into an empty database, round-tripped in the suite. Deleting a person removes their charts, entries, readings and flags in one transaction, verified by querying every table afterwards. At the monthly ceiling, new readings are refused with the figure shown and the journal, chart computation and history all continue to work. Do not build yet: Nothing after this. ### Out of scope (and why) - Credits, payments and a paywall, which is the plumbing that turns this into a product and is the part with a payment processor's rules attached. - Compatibility readings between two people, because it means storing a second person's birth details, usually without asking them. - Any claim to predict events, health outcomes or relationships, which the app refuses at generation and would not become true with better wording. - A version aimed at children, which brings in a different set of legal obligations around data from minors and a duty of care this build is not designed for. ### README must contain - That this is for entertainment and self-reflection and is not psychological or medical advice, stated before the setup instructions - Which ephemeris library is used, its version, and which of its licences you are relying on - That an unknown birth time omits houses and the rising sign rather than assuming a time - That entries describing crisis are never interpreted, and where the crisis contacts are configured - That the only outbound requests are to the model provider and the geocoder, with the geocoder's usage policy linked
# Architecture · Dreams & Stars ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Next.js 15 with TypeScript, single user, running locally | a dream diary is the most private text most people write down | | Astronomy | An ephemeris library, with its licence recorded in the README | positions are computed, never generated, and the licence is part of the decision | | Time | A time zone database with historical rules, plus an explicit unknown-time path | an offset from 1978 is not today's offset and defaulting to noon silently is a lie | | Geocoding | A public geocoder used inside its published usage policy, every result cached | a birthplace is looked up once per place, forever, or you get blocked and deserve it | | Storage | SQLite on the local disk, no account, no upload, no telemetry | this is a diary, and diaries live where their owner lives | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Astronomy | Ephemeris calls, charts and aspects. | Any ephemeris, provided positions are computed and the licence is recorded. | | Birth data | Unknown time, historical offsets and cached places. | Any geocoder, provided its policy is read and its results are cached. | | Safety | Pre-call screening and the crisis response. | Not swappable for a post-generation check, which is too late by definition. | | Readings | Layered generation, language and the output guard. | Any model, provided the guard runs before storing. | | Journal | Local storage, export and complete deletion. | Any storage that stays on the person's own machine. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_PATH` · required · People, charts, entries, readings and flags. The diary. - `EPHEMERIS_PATH` · required · Ephemeris data files. Its version is stored on every chart. - `MODEL_API_KEY` · required, secret · The text model. Set a provider-side spend limit as well. - `GEOCODER_USER_AGENT` · required · Identifies your client as the geocoder's policy requires. A default library agent is not acceptable. - `GEOCODER_MIN_INTERVAL_MS` · required · Minimum gap between geocoder calls. Check the current published policy before lowering it. - `DEFAULT_LANGUAGE` · required · The whole reading, headings included, is produced in one language. - `MONTHLY_COST_CEILING` · required · Readings stop here. Journal, charts and history keep working. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Dreams & Stars product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Next.js 15 with TypeScript, single user, running locally, An ephemeris library, with its licence recorded in the README, A time zone database with historical rules, plus an explicit unknown-time path, A public geocoder used inside its published usage policy, every result cached, SQLite on the local disk, no account, no upload, no telemetry. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - Asking a language model where Saturn was on a given date produces a plausible degree and sign that is wrong often enough to be useless and confident enough that nobody checks, and every later layer of the product inherits the error. Compute from the ephemeris, store the numbers, and have the model read the numbers rather than supply them. - Defaulting an unknown birth time to noon silently produces a rising sign and a full set of houses that are simply invented, and they are the first things the reading talks about. Record time_known, omit everything that depends on it, and say in the output which parts could not be computed and why. The same honesty applies to the offset: a birth in 1978 uses that year's rules for that zone, which a naive conversion from the current offset will get wrong by an hour or more. - Running the check on the model's output means the generation has already happened and the person has already been shown an interpretation of the worst night of their life by a dream app. Check the input before any call is made, and treat a false positive as an acceptable cost, because the failure in the other direction is not. - Translating an English reading into another language after generating it produces headings in one language and body text in another and idioms that land strangely, and people notice immediately. Pass the language into the prompt, require the entire response in it, and check the output before storing rather than trusting the instruction. - Adding a convenience sync so the journal is available on a phone turns a private diary into an account on somebody's server, and the people most likely to write the most personal entries are the ones least likely to read the setup screen where you mentioned it. Keep it local, and make export the answer to portability.
# Delivery milestones · Dreams & Stars Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Astronomy from an ephemeris, checked against published positions Planetary positions and aspects are computed by a library and verified against known values. ### Steps 1. Install the ephemeris and record the licence decision The licence affects what you may do with the finished thing. Files: `README.md`, `src/astro/ephemeris.ts` 2. Verify against independently published positions Five dates in the suite, with a stated tolerance. Files: `tests/positions.test.ts`, `fixtures/positions.json` 3. Store the chart as numbers, not as text The prose is a view of the data, and it is regenerable. Files: `src/astro/chart.ts` ### Done when - [ ] Five dates match a published source within tolerance - [ ] Ephemeris name, version and licence are recorded - [ ] No prompt asks a model for a position - [ ] Charts are stored as structured data ### Watch out - Asking a language model where Saturn was on a given date produces a plausible degree and sign that is wrong often enough to be useless and confident enough that nobody checks, and every later layer of the product inherits the error. Compute from the ephemeris, store the numbers, and have the model read the numbers rather than supply them. ## M2 · Birth data is the hard input Unknown times are handled honestly and places are geocoded once, inside the geocoder's published policy. ### Steps 1. Make unknown birth time a first-class state It omits houses and the rising sign and explains itself. Files: `src/people/birth.ts`, `tests/unknown-time.test.ts` 2. Convert using historical time zone rules A 1978 birth is not converted with a 2026 offset. Files: `src/astro/time.ts`, `tests/tz-history.test.ts` 3. Cache every geocode and obey the published policy Read the policy, set an identifying user agent, rate limit to it. Files: `src/geo/lookup.ts`, `tests/geo-cache.test.ts` ### Done when - [ ] Unknown time omits houses and the rising sign with a reason - [ ] Historical offsets convert correctly in two cases - [ ] A place is geocoded at most once - [ ] Requests are identified and rate limited to the published policy ### Watch out - Defaulting an unknown birth time to noon silently produces a rising sign and a full set of houses that are simply invented, and they are the first things the reading talks about. Record time_known, omit everything that depends on it, and say in the output which parts could not be computed and why. The same honesty applies to the offset: a birth in 1978 uses that year's rules for that zone, which a naive conversion from the current offset will get wrong by an hour or more. ## M3 · The safety check runs before the model, not after An entry describing crisis is saved, flagged and answered with your configured local contacts instead of an interpretation. ### Steps 1. Screen the input before any model call A spy in the suite proves no call happened. Files: `src/safety/screen.ts`, `tests/safety.test.ts` 2. Require crisis contacts during setup Local to the person, because a generic number helps nobody. Files: `src/setup/contacts.ts` 3. Save the entry, refuse the interpretation The journal keeps the words; the app declines the reading. Files: `src/entries/create.ts` ### Done when - [ ] A flagged entry makes no model call - [ ] Setup cannot complete without crisis contacts - [ ] Flagged entries are still saved and visible - [ ] Oblique phrasing is covered by the fixtures ### Watch out - Running the check on the model's output means the generation has already happened and the person has already been shown an interpretation of the worst night of their life by a dream app. Check the input before any call is made, and treat a false positive as an acceptable cost, because the failure in the other direction is not. ## M4 · Readings that stay inside what they are Generated text is layered, labelled, single-language, and checked against the things it must not say. ### Steps 1. Generate in named layers from the stored numbers The chart data goes in; interpretation comes out. Files: `src/readings/generate.ts`, `src/readings/prompts.ts` 2. Require the whole response in one language Headings included, and check rather than trust. Files: `src/readings/language.ts`, `tests/language.test.ts` 3. Refuse predictive, medical, financial and legal phrasing One regeneration, then a failure the person can see. Files: `src/readings/guard.ts`, `tests/guard.test.ts` ### Done when - [ ] Readings store layers, model, prompt version and language - [ ] No mixed-language output - [ ] Predictive or advisory phrasing is refused - [ ] The disclaimer renders above the reading ### Watch out - Translating an English reading into another language after generating it produces headings in one language and body text in another and idioms that land strangely, and people notice immediately. Pass the language into the prompt, require the entire response in it, and check the output before storing rather than trusting the instruction. ## M5 · It is a diary, so treat it like one (production only) Everything stays local, exports as plain files, deletes completely, and the model spend has a ceiling. ### Steps 1. No account, no telemetry, two outbound destinations Watch the network for a full session and count them. Files: `src/app/layout.tsx`, `tests/network.test.ts` 2. Export to Markdown plus a JSON archive One for reading years later, one for reimporting. Files: `src/export/journal.ts`, `tests/roundtrip.test.ts` 3. Delete completely, in one transaction A diary you cannot fully delete is not private. Files: `src/people/delete.ts`, `tests/delete.test.ts` ### Done when - [ ] Only the model and geocoder are contacted - [ ] Export and reimport round-trip the journal - [ ] Deleting a person leaves no rows anywhere - [ ] The ceiling stops readings, not the journal ### Watch out - Adding a convenience sync so the journal is available on a phone turns a private diary into an account on somebody's server, and the people most likely to write the most personal entries are the ones least likely to read the setup screen where you mentioned it. Keep it local, and make export the answer to portability.
# Operations · Dreams & Stars ## Backup The database, which is the diary. Charts are recomputable from birth data and readings are regenerable, but the entries are not, so a backup that only covers derived tables covers nothing. ## Restore Restore and confirm that charts recompute to the same positions with the recorded ephemeris version, and that flagged entries are still flagged. A restore that loses flags could offer an interpretation the build previously refused. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring For a local tool this lives in the suite: no model call on a flagged entry, no mixed-language output, no geocoder call for a cached place, and spend against the ceiling. ## Incident checklist The incident that matters is a reading someone took as advice. Storing the model identifier, prompt version and layer structure with each reading is what lets you see exactly what was shown and change the prompt rather than argue about it. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Five known positions are confirmed to match a published source - [ ] An unknown birth time is confirmed to omit houses and the rising sign - [ ] A flagged entry is confirmed to make no model call - [ ] A place is confirmed geocoded at most once - [ ] Deleting a person is confirmed to leave no rows in any table ## Launch constraint Do not market omitted Dreams & Stars capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. People, charts, entries, readings and flags. The diary. DATABASE_PATH=./data/journal.db # Required. Ephemeris data files. Its version is stored on every chart. EPHEMERIS_PATH=./data/ephe # Required · secret. The text model. Set a provider-side spend limit as well. MODEL_API_KEY=sk-...redacted # Required. Identifies your client as the geocoder's policy requires. A default library agent is not acceptable. GEOCODER_USER_AGENT=dream-journal/1.0 (you@example.com) # Required. Minimum gap between geocoder calls. Check the current published policy before lowering it. GEOCODER_MIN_INTERVAL_MS=1000 # Required. The whole reading, headings included, is produced in one language. DEFAULT_LANGUAGE=en # Required. Readings stop here. Journal, charts and history keep working. MONTHLY_COST_CEILING=3.00
$ choose a build depth, inspect the files, then open the complete pack in your agent
The demo version · ask an LLM to interpret a dream · is free everywhere, including in ChatGPT itself. What people are actually paying for here is chart accuracy (real ephemeris math, not a model's guess at where Saturn was), a specific interpretive voice refined across the psychological, traditional and practical layers, and not having to build or maintain the astronomy, the budget guardrails, or the payment flow themselves.
xthe three-layer interpretation methodology (psychological/Jungian, Islamic/Ibn Sirin traditional, actionable) tuned over real usage, not a first draft
xreal natal-chart accuracy from ephemeris data instead of an LLM approximating planetary positions
xa spend-reservation system that caps provider cost before generation starts, so one bad request can't blow the budget
xnative-quality bilingual Turkish/English copy, not machine-translated
xthe credits/paywall, Stripe and RevenueCat payment plumbing
Don't feel like building it? These folks already made it free.
no votes, no pay-to-list · just what's real
Vibecode Dreams & Stars
Kinda. The core of Dreams & Stars is buildable in a weekend with the prompt on this page, but there are real gaps: the three-layer interpretation methodology (psychological/Jungian, Islamic/Ibn Sirin traditional, actionable) tuned over real usage, not a first draft, real natal-chart accuracy from ephemeris data instead of an LLM approximating planetary positions. Read the honest list above before committing.
How much does Dreams & Stars cost?
Dreams & Stars's pricing is usage-based or varies by plan · Sold as consumable credit packs, not a subscription: Starter 10 credits for 4.99 USD, Popular 30 credits for 9.99 USD, Premium 100 credits for 24.99 USD. One credit = one dream or astrology reading..
What do I lose by replacing Dreams & Stars?
Honestly: the three-layer interpretation methodology (psychological/Jungian, Islamic/Ibn Sirin traditional, actionable) tuned over real usage, not a first draft; real natal-chart accuracy from ephemeris data instead of an LLM approximating planetary positions; a spend-reservation system that caps provider cost before generation starts, so one bad request can't blow the budget; native-quality bilingual Turkish/English copy, not machine-translated; the credits/paywall, Stripe and RevenueCat payment plumbing. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Dreams & Stars?
Yes: Astro-Seek (Free natal chart calculator and traditional interpretation, no signup · real astronomy, no AI narrative.) DreamMoods (Free dream symbol dictionary running since the late 1990s · lookup-based, not a personalized AI narrative.) The prompt is for when you want it exactly your way.