Spaces: the inside of the world, and a sun that is actually where it should be

Ten agents wrote this in parallel against CONTRACT.md, which exists because the
five design agents before them collided on fifteen blocking points — four files
specified twice with incompatible contents, three separate backends for one box,
and `Environment` exported twice meaning different things.

What landed: a Stage owning only the renderer and the loop, with the city and an
office as two scenes over it. They cannot share one — San Francisco is ~94 m per
scene unit with 3.6x vertical exaggeration and an office is 1 unit = 1 m — and
the city is paused rather than disposed on the way in, because rebuilding its
336,864-point heightfield costs about a second on the way back out.

Offices are data. `src/offices/lumbridge-hq.ts` is fifteen rooms and seventy-six
seats, and it is the file a self-hoster copies. Walls are a segment list with
1-D openings, so doors and windows are holes punched in a wall rather than
placed objects, and the pass that splits a wall around its openings hands the
walk-mode collider its segments for free.

The sun is real. `solar.ts` is a NOAA/Meeus implementation with no imports at
all — not even three.js — so time of day keeps working on a laptop in a field.
Verified against known values: 75.45 degrees at the June solstice in SF, 28.79
at December, sunset at 03:15Z. The first screenshot after wiring it was a black
rectangle, which turned out to be correct: it was midnight in San Francisco.

Presence binds to a seat id and never to a coordinate. The pack knows where
`eng-04` is; who is sitting in it is private data behind an API. Same shape as
the marker rule, one level in.

Two corrections to ARCHITECTURE.md are in here. Containment does not discharge
ODbL — publishing OSM-derived coordinates is Public Use of a Derivative Database
wherever the rows live, so the rule is about the geocoder (US Census, public
domain) and not the storage. And a person at a desk is not a Marker; markers are
geographic.

One contract gap surfaced only in a screenshot: two agents read `height` on a
viewpoint differently, so the establishing shot aimed at empty air fourteen
metres above the roof. It now means what the same field means for a city.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Karti Tripathi
2026-08-05 00:11:01 -07:00
parent 36471bbad7
commit d464459838
77 changed files with 14266 additions and 216 deletions
+42
View File
@@ -0,0 +1,42 @@
# The one Caddy snippet.
#
# Three server designs each brought their own, on three different ports, and all
# three wrote to this filename. This is the one that replaced them: one service,
# one port, one prefix. CONTRACT.md §5.
#
# Install it beside your Caddyfile and import it into whichever site serves the
# Tera browser build:
#
# import /etc/caddy/snippets/tera-api.snippet
#
# tera.lumbridgecorp.com {
# import tera_api
# root * /srv/tera/dist
# file_server
# }
#
# The API and the static build are deliberately the same origin. Nothing here
# needs CORS, which is why TERA_CORS_ORIGIN defaults to empty — set it only for
# a Vite dev server on another port.
(tera_api) {
handle /api/v1/* {
reverse_proxy 127.0.0.1:8431 {
# Fail fast rather than holding a browser connection open while the
# API is restarting. systemd brings it back in under two seconds.
transport http {
dial_timeout 2s
}
}
}
# The API stamps its own Cache-Control — `private, no-store` by default, and
# `public, max-age=…` only where a route opted in. Do not add a cache
# directive here: this file cannot tell which route answered, and the
# fail-closed policy is only fail-closed if nothing downstream overrides it.
header {
X-Content-Type-Options nosniff
Referrer-Policy strict-origin-when-cross-origin
}
}
+65
View File
@@ -0,0 +1,65 @@
# The one compose file.
#
# cd deploy && docker compose up
#
# That is the whole quickstart, and it has to work under `env -i` with no .env
# file, no keys and no account — which is what the CI job asserts by curling
# /api/v1/health. Every variable below therefore carries a `:-` default, so an
# empty environment resolves to an empty string, and the config layer reads an
# empty string as "not set" and falls back. Nothing here is required.
#
# They are listed anyway because being able to run
#
# TERA_WEATHER_SOURCE=nws TERA_WEATHER_CONTACT=you@example.com docker compose up
#
# without editing a file is most of what makes this a dev kit. The full list of
# knobs is in ../server/README.md.
name: tera
services:
api:
build:
context: ..
dockerfile: server/Dockerfile
# Loopback only. This is meant to sit behind Caddy on the same box; see
# Caddyfile.snippet. Change the left-hand side, not the right.
ports:
- "127.0.0.1:8431:8431"
environment:
TERA_HOST: "0.0.0.0"
TERA_LOG_LEVEL: "${TERA_LOG_LEVEL:-info}"
TERA_ORIGIN_LAT: "${TERA_ORIGIN_LAT:-}"
TERA_ORIGIN_LNG: "${TERA_ORIGIN_LNG:-}"
TERA_WEATHER_SOURCE: "${TERA_WEATHER_SOURCE:-}"
TERA_WEATHER_CONTACT: "${TERA_WEATHER_CONTACT:-}"
TERA_FLIGHTS_SOURCE: "${TERA_FLIGHTS_SOURCE:-}"
TERA_MARKERS_SOURCE: "${TERA_MARKERS_SOURCE:-}"
TERA_MARKERS_FILE: "${TERA_MARKERS_FILE:-}"
TERA_OFFICES_DIR: "${TERA_OFFICES_DIR:-}"
TERA_AUTH_MODE: "${TERA_AUTH_MODE:-}"
TERA_AUTH_ENTRY_URL: "${TERA_AUTH_ENTRY_URL:-}"
TERA_AUTH_REVALIDATE_URL: "${TERA_AUTH_REVALIDATE_URL:-}"
TERA_AUTH_JWT_SECRET: "${TERA_AUTH_JWT_SECRET:-}"
# Offices and marker snapshots are files. Mount them read-only where you
# keep them; the container writes nothing, ever.
#
# volumes:
# - ../public/offices:/data/offices:ro
healthcheck:
# No curl in the image and none wanted. Node is already here.
test:
- CMD
- node
- -e
- "fetch('http://127.0.0.1:8431/api/v1/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
interval: 15s
timeout: 5s
retries: 5
start_period: 5s
restart: unless-stopped
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
+61
View File
@@ -0,0 +1,61 @@
# The one systemd unit.
#
# sudo cp deploy/tera-api.service /etc/systemd/system/
# sudo systemctl enable --now tera-api
#
# Note the `-` on EnvironmentFile: the file is optional, so this unit starts and
# serves /api/v1/health on a box where /etc/tera/tera.env was never created. That
# is the same promise the compose file and the tests make, expressed in the one
# place an operator is most likely to discover it the hard way. CONTRACT.md §5.1.
#
# Assumes the repo is checked out at /srv/tera with `npm ci --omit=dev` already
# run at the root. There is no build step — Node runs the TypeScript sources
# directly — so a deploy is a git pull and a restart.
[Unit]
Description=Tera API — city data for the Tera map view
Documentation=https://github.com/lumbridge-public/tera
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=tera
Group=tera
WorkingDirectory=/srv/tera
EnvironmentFile=-/etc/tera/tera.env
ExecStart=/usr/bin/node server/src/index.ts
Restart=on-failure
RestartSec=2s
# Requires Node >= 22.18, where type stripping runs without a flag.
Environment=NODE_ENV=production
# Hardening. This process reads two directories, opens outbound HTTPS to at most
# one weather feed, and listens on loopback. It has no business doing anything
# else, and saying so here is cheaper than trusting that it never will.
NoNewPrivileges=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectSystem=strict
ProtectHome=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictAddressFamilies=AF_INET AF_INET6
RestrictNamespaces=yes
LockPersonality=yes
MemoryDenyWriteExecute=no
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
# Nothing is written at runtime. The marker snapshot is written by the sync
# oneshot, which is a different unit with a different user and the only holder of
# a Workie credential — add its directory here only if you run both as `tera`.
ReadOnlyPaths=/srv/tera
MemoryMax=512M
TasksMax=64
[Install]
WantedBy=multi-user.target