Metadata-Version: 2.4
Name: shamash-core
Version: 2026.3.0
Summary: A Toolbox for creating a physically modelled world for worldbuilding purposes, based on Artifexian's Worldsmith.
License-Expression: MIT
Project-URL: Source, https://codeberg.org/schobernoise/shamash-core
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pyyaml
Requires-Dist: rich
Requires-Dist: naturedynamics>=1.1
Provides-Extra: dev
Requires-Dist: black; extra == "dev"
Requires-Dist: pyproj; extra == "dev"
Dynamic: license-file

# Shamash

> This aims to be a physically accurate worldbuilder. A lot of these calculations are rather educated guesses than well-founded scientific principals, but they all have their foundation in recent scientific discoveries and laws of nature.

Based on the great [Artifexian](https://www.artifexian.com/)'s Work "The Worldsmith" I wanted to continue his journey. Intruiged by building on the very idea of quantifying reality - but at the same time horrified by looking at a spreadsheet - the only logical next step was to build a python application. :)

> [!important] AI Disclaimer
> Yeah, I know, this sucks. Until version 2026.0.3 I wrote everything myself, except some exporting functionality. The architecture, the Design Patterns, I came up with that myself. But with time being the only thing that does not accumulate over time (weird, right?) I simply couldn't stand another project wasting away over the years, and me not having the energy to finish it. Claude assists me heavily in writing the API and UI.

So, before the whole thing became a webfrontend with an API I designed it to be a CLI-Application. I like to work from the terminal, but after having hundreds of parameters scrolling past your tired eyes this was just not feasable any more. So I turned the CLI into an API (which still can be used as CLI as well) and created a React-Frontend.

Worldbuilding is an incredibly complex process. This application will never aim at calculating EVERYTHING, you know. I know some of you (including me) have those thoughts and it is only natural to desire a feeling of absolute control over reality. Understanding, caluclating, quantifiying everything until its smallest detail. But look into yourself. The motivation will most likely be fear, or some derivation of it. You are afraid to not be able to hold the world you are building in the palm of your hand.

But the very thing you are seeking to do - describing a reality by natural laws - is something we humans perfected since 300.000 years by immitating our natural surroundings (impressions), using them to express our feelings, pain and pleasure (expressions). This enables you to describe your newly formed reality in a way far more compley than any table of figures can. **Put your world into writing, not into a table, so other humans can comprehend what you are creating.**

## The shamash repositories

shamash is split by audience:

- **shamash-core** (this repo) — the physics engine and the `shamash` CLI. Published on PyPI.
- **[shamash-server](https://codeberg.org/schobernoise/shamash-server)** — the HTTP API around the engine, dockerized.
- **[shamash-web](https://codeberg.org/schobernoise/shamash-web)** — the React frontend, dockerized, talking to a shamash-server.

## Getting started

```bash
pip install shamash-core
shamash -p
```

The distribution is `shamash-core`; the command and the import name are both `shamash`.

For development, a Makefile is provided:

```bash
make venv
uv pip install --python venv/bin/python -e ../naturedynamics   # the physics backend, as a sibling checkout
make install
make run
make test
```

Without any paramaters, shamash reads the seed world packaged at `src/shamash/data/default.yml`, which represents our very solar system. `make run` is configured to use the `-p` flag, which pretty-prints out all calculations.

### The CLI application

`shamash` is the terminal-first way to use this project — load a world, optionally change what gets read or written, and exit. A plain `shamash` prints nothing; it just loads the world and returns, which is what makes it safe to script. Every flag is optional and they compose:

```bash
shamash                    # load src/shamash/data/default.yml, print nothing
shamash -w my_world.yml    # load a different world file instead
shamash -p                 # also pretty-print the loaded (and computed) world to stdout
shamash -e out.yml         # also export the loaded world to out.yml
shamash -f json            # write JSON instead of YAML (also: csv)
shamash -i new_world.yml   # copy the input world to new_world.yml, to seed a new one, then exit
shamash -l                 # also log to the configured log file (stderr always gets the log otherwise)
```

`-p` and `-e` render through the exact same writer, so `shamash -p > world.yml` produces a file that reloads with `-w` byte-for-byte. That holds per format: `-f json -e world.json` reloads with `-w` too, and gives back the identical world. On a terminal, `-p` syntax-highlights the output with `rich`; piped or redirected, it falls back to the plain string, so the redirect stays clean.

`-f csv` is the flat one, and the exception: one row per scalar rather than a tree, `entity,type,property,source,value,unit,dimension`, with compound values exploded into rows of their own (`main_star_apparent_size[0]`, `atm_composition.o_2`). It is the same view shamash-web's matrix tab shows, and it is **one-way** — a world cannot be read back from it. Use it to pull a computed world into a spreadsheet or a plotting script:

```bash
shamash -f csv -p | awk -F, '$3 == "habitability"'
```

#### Verbs

Flags act on a world; **verbs change one**. `create` makes one entity and hangs it off an existing one:

```bash
shamash create Atmosphere earth -w my.yml        # asks for what it needs
shamash create Planet sol -w my.yml              # offers sol's free orbits, or makes one
shamash create Star --id alpha --set mass=1.0:MSol --set current_age=4.6:Gyr -w my.yml
```

Flags work on either side of the verb, so `shamash -w my.yml create Orbit sol` reads the same.

Required fields it isn't given, it asks for; `--set key=value` answers one up front. `--set` also reaches the *optional* fields it never asks about — a planet's core (`--set core_density=10900`), a star's wind — and refuses a name the type doesn't have, so a typo is a visible error rather than a value that quietly never arrives. Values are SI unless you give a unit — `--set mass=1.0:MSol` goes through the same conversion and the same checks a world file's `{value: 1.0, unit: MSol}` does, so a wrong dimension is refused rather than quietly scaled.

Choices with no safe default are asked too: a body that already has an atmosphere, a parent that already has orbits, a file about to be overwritten. `--answer name=value` settles those up front (`--answer strategy=add`), which is what makes the whole thing scriptable — including questions about an entity `create` makes along the way, like the orbit a new planet needs (`--answer semi_major_axis=1.52:ASol`). **With nothing to ask and no terminal to ask at, it refuses instead of hanging** — a piped run tells you exactly which answers it was missing and exits 1.

What can host what is not a list anywhere; each relationship declares what the child reads off its parent, and a type qualifies by providing it:

```
$ shamash create Atmosphere sol -w my.yml
Atmosphere cannot hang off 'sol': a Star has no surface_temperature,
which Atmosphere.parent_moon_or_planet needs. Types that do: Planet.
```

That sentence is assembled from the models every time, so it follows them — the day a Moon can compute a `surface_temperature`, moon atmospheres start working and this refusal stops being produced, with nothing to update.

`create` writes back to the file `-w` names, or to `-e` when given. It refuses without one of them (the default world lives inside the installed package), asks before overwriting unless `-y`, and refuses `-f csv`, which does not reload.

`crs` is the read-only one: it prints a coordinate reference system for a body's surface, so a world can be mapped in QGIS, GDAL or anything else built on PROJ:

```bash
shamash crs earth > earth.wkt                               # latitude/longitude
shamash crs earth --projection north-polar                  # or equirectangular, south-polar,
shamash crs earth_moon_1 --projection mollweide             #    sinusoidal, transverse-mercator
shamash crs earth --projection equirectangular --central-meridian 180
```

It writes WKT2 for a sphere of the body's radius, shaped like the IAU's own planetary CRSs (which PROJ ships as `IAU_2015:*`): planetocentric latitude, east-positive longitude, a reference meridian at 0. Anything with a radius qualifies — a Star, Planet or Moon; an Atmosphere or Orbit is refused, naming the types that do.

One catch that is PROJ's, not ours: PROJ decides which celestial body a CRS belongs to from its radius alone, and treats any sphere of roughly 6,330–6,423 km as Earth. A fictional world that close to Earth's size will transform to WGS 84 without complaint; anything further off is refused, as it should be.

### As a library

Everything the CLI does is reachable from Python. A world loads into a registry, every value on it is computed on access, and `Worldloader` turns it back into text:

```python
import shamash as sh

with sh.god.scope():
    loader = sh.services.Worldloader()
    loader.load(sh.services.BaseService.default_world_path())
    print(sh.god.summons("earth").habitability)
    print(loader.dumps(fmt="json"))
```

`god.scope()` gives the world a registry of its own, so two worlds can be computed at once without seeing each other — that is what shamash-server does per request.

`sh.utils.describe_entities()` is the per-type schema — every field and computed property with its unit, whether it is required, and its description — derived from the models rather than written down anywhere.

### Property documentation

Every field and computed property carries a `description`, and each has a long-form page with formulas and sources under `src/shamash/docs/properties/<Type>/<name>.md`. The pages ship inside the package, so a server always hands out the pages for exactly the engine version it runs; shamash-web renders them. `make test` fails if a description has no page or a page documents nothing.

## Releases

Development happens on `origin` (`dev.fs.or.at`). A release is an annotated
tag on `main`, and `codeberg` only ever sees tagged states:

```bash
git push origin main                          # every day
git tag -a v2026.1.1 -m "CLI verbs and output formats"
git push origin --tags                        # keep the markers on dev too
git push codeberg main v2026.1.1              # publish
```

Tags go to **both** remotes; commits go to `origin` until they are released.
Then the same commit goes to PyPI:

```bash
make publish-test   # TestPyPI, with $PYPI_TEST_TOKEN; check it installs
make publish        # PyPI, with $PYPI_TOKEN
```
That way the release history survives on the remote that has everything,
rather than only on the one it is published to.

So `git tag` is the list of releases, each heading below names one, and the
version in `pyproject.toml` is bumped in the commit the tag points at.
`__version__.py` reads it from there — see the note in `CLAUDE.md` about why
the checkout's `pyproject.toml` wins over `importlib.metadata`.

### 2026.3.0

- [x] A terrestrial service and the `crs` verb
  - `services/terrestrial/` is the home for anything on a body's surface.
    First in: WKT2 coordinate reference systems for any entity with a
    radius — geographic, equirectangular, polar stereographic, sinusoidal,
    Mollweide and transverse Mercator — mirroring the IAU 2015 definitions
    PROJ ships.
  - A sphere for now: a rotational ellipsoid needs a flattening model, and
    the simple homogeneous one gets Earth wrong (1/232 against 1/298).
  - Tested against PROJ itself through pyproj (a dev-only dependency):
    every CRS parses, and known points land where the closed-form
    projection says they must.

### 2026.2.0

- [x] Split the project into repositories
  - This repo becomes **shamash-core**: the engine and the CLI, published
    on PyPI as `shamash-core`. The import name and the command stay
    `shamash`.
  - The HTTP layer moved to **shamash-server** and the frontend to
    **shamash-web**, both dockerized; the marimo notebooks to a private
    experiments repo. `shamash-api` is gone from this package.
  - The property docs moved *into* the package
    (`src/shamash/docs/properties/`), next to the descriptions they expand
    on, so a page and its description change in one commit. The coverage
    test moved with them and now checks both directions, the Settings
    pages and the sidebar — it used to check only that entity descriptions
    had a page, and skipped itself in any install without the frontend.
  - `describe_entities`/`describe_config` moved from `api/schemas.py` to
    `sh.utils`: they read only the models.
  - MIT licensed, in all three public repos.
  - Dependencies cut to what the engine imports: `pyyaml`, `rich`,
    `naturedynamics`. `numpy`/`scipy`/`matplotlib` arrive through
    `naturedynamics`, which declares them itself.

### 2026.1.2

- [x] Bring back a test suite — `make test`, stdlib `unittest`, ~1 s
  - It loads the real seed world and asserts what comes out, rather than
    mocking collaborators: the physics is a pure function of `default.yml`,
    so running it *is* the test. Most of it is a transcription of numbers
    `CLAUDE.md` already presented as regression baselines — Sol at 1.000
    RSol / 5770.8 K / G2.9, Earth habitability 1.000, `#B3F49A`,
    magnetopause 9.52 R⊕ — which described a suite nobody had written down.
  - 70 tests over the physics anchors, the loader's round-trips and error
    taxonomy, `god.scope()` isolation under 8 threads, the HTTP contracts
    (since moved to shamash-server's suite), the CLI, and the `create`
    verb's parent rules.
  - Mutation-probed while being written, which is the only reason to trust
    it: feeding `Star.radius` kilograms fails 6 tests, dropping
    `FACTOR_FLOOR` fails the hostile-world test, removing the
    `Planet.moons` backref fails the tidal one.
  - `make test-packaging` is separate (~15 s): it builds a wheel and runs
    the CLI and the API from it with no source tree in sight.
- [x] Two bugs the suite found before it was finished
  - `create Moon sol` ran the orbit cascade *before* discovering a Star
    cannot host a moon, so it created an orbit and then refused — leaving
    it behind in a world the caller was told nothing had been added to.
    Legality is now decided first, so a refusal costs nothing.
  - `create` asked its questions before validating `--set` names and the
    `--id`, so a typo was reported only after you had answered three
    prompts. Both are checked up front now.
- [x] Corrected numbers the docs stated but the code did not produce
  - `magnetosphere.py` documented Earth's magnetopause at 9.6 planetary
    radii and the bow shock at 12.5, in five places including user-facing
    text mirrored into the docs site. The code computes 9.52 and 12.37.

### 2026.1.1

- [x] Output formats for the CLI — `-f yaml|json|csv`
  - Notation moved out of both the codec and the HTTP layer into
    `services/world/formats.py`, so there is one YAML dumper call and one
    JSON dumper call in the codebase and the CLI and API cannot drift.
    `-p` and `-e` are byte-identical per format; `shamash -f json -p` is
    byte-identical to `GET /v1/world/default`.
  - csv is the flat view — one row per scalar, the same shape shamash-web's
    matrix tab shows — and is one-way.
  - `Worldloader.loads` now parses a root-level `{` as JSON. PyYAML follows
    YAML 1.1, where `5e-05` without a dot is a *string*, so the app could not
    read back a file it had just written with `-f json -e`.
- [x] CLI verbs — `shamash create <Type> <parent_id>`
  - `cli.py` grows subparsers and a `VERBS` map; flags work on either side of
    the verb. `-w` names the file the verb modifies.
  - What can host what is declared per relationship as `parent_needs` — what
    the child reads off its parent — never as a table of types. The refusal
    message is assembled from the models, so it follows them: the day a Moon
    can compute a `surface_temperature`, moon atmospheres start working with
    nothing to update.
  - Services never prompt. They call `self.ask(questions)`; the CLI injects
    `InteractionService.ask` and everything else gets `refuse_to_ask`, which
    raises a 422 carrying the questions — a service reading stdin would block
    a uvicorn worker forever, and `world:create-orbit` runs the same code.
  - `--set` and `--answer` make a create fully scriptable; with nothing left
    to ask it needs no terminal at all.

### 2026.1.0

- [x] Docker, and a React web frontend — both now live in their own repos,
  [shamash-server](https://codeberg.org/schobernoise/shamash-server) and
  [shamash-web](https://codeberg.org/schobernoise/shamash-web), together with
  their design notes.
- [x] Atmospheric `sky_color` — the colour of the sky from the ground
  - Single-scattered starlight through the Rayleigh column, von Kries adapted
    to the local star. Earth reads `#788CC4`; a 90 atm column washes out to
    near-white, which is what a saturated column should do.

### 2026.0.4

- [x] Stateless HTTP API (FastAPI), so a web frontend can do what `-w` does —
  now [shamash-server](https://codeberg.org/schobernoise/shamash-server). What
  it needed from the engine stayed here:
  - [x] Phase 0: make the package survive outside its source checkout
    - [x] Resolve version/name/description without reading `pyproject.toml` at import
    - [x] Ship `data/default.yml` as declared package data
    - [x] Stop the exporter crashing on optional fields a world leaves out
    - [x] Validate a world file's units against the dimension the field declares
    - [x] Log normal conditions at DEBUG instead of ERROR
  - [x] Request-scoped `god` registry (`contextvars`)
  - [x] `Worldloader.loads`/`load_data`/`to_dict` seams, JSON and YAML in/out
  - [x] Console entry point `shamash`

### v2026.0.3

- [x] Refactor and Introduce Atmospheric model
  - Maybe even in naturedynamics, taking atmospheric models from omniastro
- [x] Introduce Plant Color and Biosphere Module
  - Based on Star and Atmosphere
- [x] Geomagnetic Dynamics/ Solarmagnetic Dynamics
- [x] Make Habitability not true or false but a probability scale, taking into account:
  - Class of the Star and Brightness
  - Calculation Method relative habitability to how far the planet is away

### v2026.0.2

Settled on name for project: shamash.

- [x] Rename Project
- [x] Refactor export/import handling
  - do not use pprint, but format in cli as well
  - use flag for cli dump
- [x] Unify Units handling, refactor bad claude code
  - Every function should have their units as metadata with them - decorators?
  - Distances in space are AU/Parsecs/Lightyears
- [x] Fix main_star_apparent_size=<error: division by zero>
- [x] Improve logging
  - [x] Add a flag to write to a log file
- [x] Star Class Calculation
- [x] Add Star Classes as Enums, maybe directly into naturedynamics
- [x] Fix Hillsphere Calculation and Inner/Outer Moon Zone Error
- [x] Also push naturedynamics when pushing worldcreator
  - As of 2026-09-01 naturedynamics is no longer a submodule; it lives at
    `~/Dev/projects/naturedynamics` and is installed into the venv.
  - Release Versioning and Cycle ?
- [x] Add Horizon Distance
- [x] Add proper error handling
- [x] Add a flag to init a new world yaml file
  - `shamash init new_world`
- [x] Add Flag to point to yaml world file
- [x] Make color calculation refined and add Hexcolor support

### v2026.0.1

Finally, first working version finished, shamash.

### Backlog

- [ ] Add Habitable Zone Shift Factor for planets, to shift habitable zone bonds,
  - regarding how many greenhouse gases exist
  - Axial Tilt
  - Orbital Period
- [ ] Make Apparent Brightness, etc a function that can be calculated at any given point of the orbit.
- [ ] Add LSol as Unit for Luminosity
- [ ] Change ASol to AU
- [ ] Decide what log level a _caller's_ mistake deserves
  - `AppError` logs at ERROR on construction. Over HTTP every 422 is the
    client's bad world, not a server incident, so a busy API fills the error
    log with things no operator can act on — but the same exception from the
    CLI, against a world file you just edited, is worth an ERROR. One class,
    two contexts; Phase 0 already established `log_level=` as the mechanism.
- [x] Containerise — done under 2026.1.0 above, once unblocked on
      2026-09-01 by the package no longer reading `pyproject.toml` at import,
      shipping its seed world as package data, and running (CLI and API both)
      from a wheel with no source tree.
- [x] `make venv` pins `--python 3.13` while the venv in use is 3.14.6
  - Pinned to 3.14, what everything is verified on. It also gained `--seed`:
    without pip in the venv, `make install` could not run on a fresh one.
    And its `uv pip install -r requirements.txt` now names `venv/bin/python`:
    uv looks for `.venv` or an activated env, so it failed unless `venv` was
    activated.
