Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
0cf730c
Three npm workspaces, and the single lockfile that comes with them
thalida Aug 23, 2026
a9d1063
Everything the product is made of moves under src/, in three npm proj…
thalida Aug 24, 2026
4093038
src/ becomes packages/, and each package carries its own manifest
thalida Aug 26, 2026
f2f8eb2
Prettier moves into each package; the repo root stops being an npm pr…
thalida Aug 26, 2026
8d3ece3
Two npm packages, not three: the client folds into the city
thalida Aug 26, 2026
a4c24b1
Per-city GPU resources: two cities on one page now share nothing
thalida Aug 26, 2026
4b6f997
One package talks HTTP: the api layer moves into @codecity/city
thalida Aug 26, 2026
c1c55ac
The wire and geometry types move into the package; the types barrel i…
thalida Aug 27, 2026
8db62bf
Note picker.ts as a temporary home, with why it cannot move yet
thalida Aug 27, 2026
5a6cd14
gen_openapi.py moves into the api package; scripts/ leaves the repo root
thalida Aug 27, 2026
83fd28a
Each package owns its compose services and its ignores; the root only…
thalida Aug 27, 2026
efcf5d7
The leaf utilities move; step 3 is done
thalida Aug 27, 2026
af6b40c
The settings schema moves into the package; the app keeps the values
thalida Aug 27, 2026
72305fa
The renderer moves into @codecity/city
thalida Aug 27, 2026
407f05d
Each city resolves its own settings
thalida Aug 27, 2026
d7ee574
One camera per city, and CameraMode is deleted
thalida Aug 27, 2026
e4f9b7e
The city reports; the app decides what that means
thalida Aug 27, 2026
322ca37
A city fetches its own repo
thalida Aug 27, 2026
be12989
Each city scrubs its own history
thalida Aug 28, 2026
ab1c419
The scene handle leaves the package, and the alias with it
thalida Aug 28, 2026
ec30597
Settings become a dispatch table, not a dependency graph
thalida Aug 28, 2026
0a502d7
The city publishes; it does not propagate
thalida Aug 28, 2026
984e05e
Clear the lint the last commit left behind
thalida Aug 28, 2026
2607b39
The package has no reactive runtime
thalida Aug 28, 2026
9d102fb
Ignore the vite cache a root-run vitest leaves behind
thalida Aug 28, 2026
8837583
The city's tests move into the city
thalida Aug 28, 2026
028eb72
The docs describe the layout that exists
thalida Aug 28, 2026
81eb198
Two bugs the dev server catches and the gate does not
thalida Aug 28, 2026
f98ed38
A city hands out its timeline, not a snapshot of it
thalida Aug 28, 2026
481dcab
One bad frame is not the end of rendering
thalida Aug 28, 2026
7c19c6a
The alias is cut, in both directions
thalida Aug 28, 2026
1bccb94
A city says what is still coming, and the overlay believes it
thalida Aug 28, 2026
d8b8f0c
Generate the app's city stores from the city's own list
thalida Aug 28, 2026
2d921a0
fields/ becomes values/, and the city's stores move in beside the rest
thalida Aug 28, 2026
461965a
A city re-packs itself when a setting says it must
thalida Aug 28, 2026
d3b49c1
A city says what it is doing, once
thalida Aug 28, 2026
692fc89
Clear what the status change left behind
thalida Aug 28, 2026
5c44e95
Commands take a city, and a source is the city's to describe
thalida Aug 28, 2026
7fea953
One notification, one snapshot, and a layer of your own
thalida Aug 28, 2026
0922d82
Two hosts for the same package, and the test that keeps them honest
thalida Aug 28, 2026
baf7997
A city keeps itself current
thalida Aug 28, 2026
08b3e5c
The component moves into the package, as a component
thalida Aug 29, 2026
eb8d77c
Timeline is a way of showing a city, not a feature of one app
thalida Aug 29, 2026
5a9538e
A layout, a wire contract that is not a page, and hooks
thalida Aug 29, 2026
07ab379
A city is a class, and it is its own type
thalida Aug 29, 2026
9372d16
A city is a value you pass down, not a slot you read up
thalida Aug 29, 2026
cc3197e
The app stops mirroring the city, and the URL becomes a prop pair
thalida Aug 29, 2026
ff5be11
Loading becomes a prop, and the URL stops being a pipeline
thalida Aug 30, 2026
47287de
The mirrors go: thirteen signals that were copies of the city
thalida Aug 30, 2026
8a2a40e
SCENE_HANDLE is gone
thalida Aug 30, 2026
a920954
Chrome state becomes per city, and the component gets a slot
thalida Aug 30, 2026
faccf4f
City logic moves to the city package, and the last mirrors go
thalida Aug 30, 2026
f45b21e
state/stores is dissolved; state moves to the feature that owns it
thalida Aug 30, 2026
6122f10
The layer folders keep only what more than one feature uses
thalida Aug 30, 2026
82e1deb
Server data becomes server state, with TanStack Query behind it
thalida Aug 30, 2026
e7dab52
One import per module, one URL hook, and an api/ folder
thalida Aug 30, 2026
ba33c5b
The router loses a circular import and a folder
thalida Aug 30, 2026
7b72c96
Trim the location header to the cap
thalida Aug 30, 2026
6c28e68
The app becomes vertical features
thalida Aug 30, 2026
5fe555d
Tests follow their code into the package, and #128 is fixed again
thalida Aug 30, 2026
dab2fc9
Split the source tests by what they are about now
thalida Aug 30, 2026
61149a6
Chrome tests read the city and its chrome through the providers
thalida Aug 31, 2026
03b1380
Server reads are seeded in tests, and vitest sees preact/compat
thalida Aug 31, 2026
c70c6f5
The panes take plain state, and the fake city emits what a real one does
thalida Aug 31, 2026
5322f6b
The panes' tests re-render instead of poking a signal
thalida Aug 31, 2026
634afe5
The last chrome tests read the city and the query cache
thalida Aug 31, 2026
88f359e
Integration tests wire the reactions the way CityStage does
thalida Aug 31, 2026
0088fba
The suites are green: 1013 app tests, 1210 package tests
thalida Aug 31, 2026
1d090bf
Lint the tests to the same bar as the source
thalida Aug 31, 2026
c54270c
One overlay that knows which load it is describing
thalida Sep 2, 2026
e16896b
The city says it is reading a history; the app stops inferring it
thalida Sep 2, 2026
4813286
An audit for the gaps tests cannot see
thalida Sep 2, 2026
e3dbe05
Work the audit's findings: the app keeps no city logic, the routes ar…
thalida Sep 2, 2026
aa539a6
The tldraw pattern: batteries included, every piece replaceable
thalida Sep 2, 2026
ebfcdb4
The audit reaches zero, and two of its findings were real
thalida Sep 3, 2026
9e8c605
A cancelled load is an outcome, and the city now says so
thalida Sep 3, 2026
6ad4151
Entering Timeline is over when the READ is, not when its pack is
thalida Sep 3, 2026
bb4a14c
The city says a read is in flight; the app stops inferring it
thalida Sep 3, 2026
b913418
Trim the comments the moved files brought with them to the cap
thalida Sep 3, 2026
f352af0
Restore the generated types to the generator's own output
thalida Sep 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 2 additions & 2 deletions .codecityignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
api/tests/fixtures/large-repo
api/tests/fixtures/sample-repo
packages/api/api/tests/fixtures/large-repo
packages/api/api/tests/fixtures/sample-repo
41 changes: 15 additions & 26 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,34 +1,29 @@
# Version control + CI scaffolding
# At the build-context root, because that is what `docker build .` sends.
# Globs, not package paths: a new package inherits them.

.git
.github

# Python local
.venv
.mypy_cache
.pytest_cache
.ruff_cache
api/__pycache__
# Python
**/.venv
**/__pycache__
**/*.pyc
**/.mypy_cache
**/.pytest_cache
**/.ruff_cache

# Node
**/node_modules
**/dist
**/.vite

# Node local
node_modules
app/node_modules
app/dist
app/.vite

# Codecity local artifacts
# Local-run scratch and tool artifacts
.local
.codecity
.codecityignore

# Tool artifacts
.claude
.superpowers
docs

# Stale static dir (legacy)
api/static
TODO.md

# OS / editors
**/.DS_Store
Expand All @@ -38,9 +33,3 @@ api/static
*~
.idea
.vscode

# Repo-level scaffolding not needed at build time
TODO.md
# NOTE: README.md and LICENSE are NOT ignored — pyproject.toml references
# them (`readme = "README.md"`, `license = { file = "LICENSE" }`), and
# hatchling validates their existence during the wheel build.
39 changes: 27 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ jobs:
# pyproject.toml's pytest addopts already include
# `--cov=api --cov-report=term --cov-fail-under=80`. The extra
# `--cov-report=xml:/srv/api/coverage.xml` lands the XML report
# inside the mounted `./api` dir so it's visible on the host for
# artifact upload (the compose service only mounts ./api, uv.lock,
# inside the mounted `./packages/api/api` dir so it's visible on the host for
# artifact upload (the compose service only mounts ./packages/api/api, uv.lock,
# and pyproject.toml — not the repo root).
run: |
docker compose -f docker-compose.test.yml run --rm pytest \
Expand All @@ -61,7 +61,7 @@ jobs:

# Stdlib-only, so it runs on the runner rather than paying for a container.
- name: Python comment cap
run: python3 bin/check-comments.py api bin scripts
run: python3 bin/check-comments.py packages/api/api packages/api/scripts bin

# Reads PYRIGHT_VERSION from the repo-root .env, which compose loads
# automatically — no `env:` mirror needed here, unlike NPM_VERSION
Expand All @@ -72,36 +72,51 @@ jobs:
- name: Run vitest with coverage
# Override the default compose command (which runs `npm test`) to
# run `npm run coverage` instead. Same apt-get / npm bootstrap as
# the compose service. Output lands in app/coverage/ on the host
# via the ./app:/app bind mount.
# the compose service. Output lands in packages/app/coverage/ on the host
# via the ./packages/app:/app bind mount.
run: |
docker compose -f docker-compose.test.yml run --rm vitest \
sh -c "apt-get update && apt-get install -y --no-install-recommends libexpat1 fontconfig fonts-dejavu-core && npm install -g npm@$NPM_VERSION && (cd /city && npm ci) && npm ci && npm run coverage"

- name: Run the city's vitest with coverage
run: |
docker compose -f docker-compose.test.yml run --rm city-vitest \
sh -c "apt-get update && apt-get install -y --no-install-recommends libexpat1 fontconfig fonts-dejavu-core && npm install -g npm@$NPM_VERSION && npm ci && npm run coverage"

- name: Upload pytest coverage report
if: always()
uses: actions/upload-artifact@v7
with:
name: pytest-coverage
path: api/coverage.xml
path: packages/api/api/coverage.xml
if-no-files-found: warn

- name: Upload vitest coverage report
if: always()
uses: actions/upload-artifact@v7
with:
name: vitest-coverage
path: app/coverage/
path: packages/app/coverage/
if-no-files-found: warn

- name: Upload city vitest coverage report
if: always()
uses: actions/upload-artifact@v7
with:
name: city-vitest-coverage
path: packages/city/coverage/
if-no-files-found: warn

- name: Lint + typecheck
run: |
docker compose -f docker-compose.test.yml run --rm vitest \
sh -c "apt-get update && apt-get install -y --no-install-recommends libexpat1 fontconfig fonts-dejavu-core && npm install -g npm@$NPM_VERSION && npm ci && npm run lint && npm run typecheck"
sh -c "apt-get update && apt-get install -y --no-install-recommends libexpat1 fontconfig fonts-dejavu-core && npm install -g npm@$NPM_VERSION && (cd /city && npm ci) && npm ci && npm run lint && npm run typecheck && npm run format:check"

# Its own service: the app-scoped vitest service can't see the root config.
- name: Format check (prettier)
run: docker compose -f docker-compose.test.yml run --rm prettier
# Prettier lives inside each package, so there is no repo-wide format
# check — README, AGENTS.md, compose and these workflows belong to no
# package and are hand-formatted.
- name: city/ typecheck + format check
run: docker compose -f docker-compose.test.yml run --rm packages

# Only the schema is written out here; the container owns everything node.
# npm on the runner cannot work: compose mounts anonymous volumes over
Expand All @@ -110,7 +125,7 @@ jobs:
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
mkdir -p .local
"$HOME/.local/bin/uv" run python scripts/gen_openapi.py > .local/openapi.generated.json
"$HOME/.local/bin/uv" run --project packages/api python packages/api/scripts/gen_openapi.py > .local/openapi.generated.json
docker compose -f docker-compose.test.yml run --rm gentypes

- name: Trivy scan
Expand Down
48 changes: 5 additions & 43 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Output
.codecity/
# Repo-level only. Anything a single package generates is ignored by that
# package's own .gitignore, so lifting one out takes its ignores with it.

# Your own settings and credentials, seeded from .env.local.example.
# .env itself is tracked: it holds what everyone shares.
Expand All @@ -9,58 +9,20 @@
# always safe to delete.
.local/

# Tool artifacts
.superpowers/

# Generated test fixtures
# run `bash api/tests/fixtures/setup.sh` to regenerate
api/tests/fixtures/sample-repo/
api/tests/fixtures/.sample-repo-ready
api/tests/fixtures/.sample-repo-setup.lock
# run `bash api/tests/fixtures/large-repo-setup.sh` to regenerate
api/tests/fixtures/large-repo/
# The root is not an npm project, but running a package's vitest from here
# leaves vite's cache behind. Nothing installs into it.
node_modules/

# OS
.DS_Store
Thumbs.db

# Dependencies
node_modules/

# Vite caches
.vite/
app/.vite/

# vitest coverage report output
app/coverage/

# pytest-cov output
# .coverage = SQLite data file (default)
# coverage.xml = Cobertura XML (CI artifact, written into api/ via
# --cov-report=xml:/srv/api/coverage.xml in ci.yml)
.coverage
coverage.xml
api/coverage.xml

# Python
__pycache__/
*.pyc
.venv/
dist/
*.egg-info/
.pytest_cache/
.mypy_cache/
.ruff_cache/

# Editors
*.swp
*.swo
*~
.idea/
.vscode/

# Visual regression diff output (generated by verify-references.ts)
app/tests/visual/references-diff/

# Claude Code session scratch (per-dev handoff notes, local settings)
.claude/
27 changes: 0 additions & 27 deletions .prettierignore

This file was deleted.

10 changes: 0 additions & 10 deletions .prettierrc.json

This file was deleted.

69 changes: 57 additions & 12 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,20 +110,65 @@ leaving those containers pointing at a gone network (`just dev` then fails with

## Layout

- `app/` — Preact + TypeScript frontend. Two routes, one view each: `/` is the
landing (pick a project) and `/city?src=…` is a world. The URL is the source of
truth for both — `router/` owns it, and `?src`, `?mode`, `?commit` and `?sel`
all survive Back and Forward.
- `src/city/` — the 3D city, a signals-driven mini-app. Layout runs in a
worker under `src/city/layout/` (snapshot-tested — keep output identical).
- `src/state/` — seven stores under `stores/`, each named for the question it
answers. `settings/` is its own subsystem: schema, drafts, reactions and
indicators over the fields they operate on.
Everything the product is made of lives under `packages/`. `bin/` and
`.github/` are how it gets built and shipped; the split is what stops the two
kinds from interleaving alphabetically at the repo root.

Each package is independent: its own manifest, its own lockfile, its own
installed dependencies. Lifting one into a repo of its own is a copy, not a
untangling.

- `packages/app/` (`codecity`) and `packages/city/` (`@codecity/city`) — two
separate npm projects, each with its own `package.json`, `package-lock.json`,
`node_modules` and prettier.
There is no npm project at the repo root, so nothing formats `README.md`,
`AGENTS.md`, the compose files or the workflows: those belong to no package
and are hand-formatted on purpose.
- `packages/city/` (`@codecity/city`) — the 3D renderer and the client every
backend call goes through. `createCity(canvas)` is the whole entry point: hand
it a canvas and an api base, and it fetches, builds, and reports what it is
doing. It depends on `three`, `three-mesh-bvh` and `rbush`, and on nothing
else — no Preact, no signals, no reactive runtime. Everything is per instance,
so two cities on one page share no settings, selection, timeline or GPU
resources; the landing's wallpaper and the `/city` scene are two such cities.
- Values in, events out. The consumer owns settings values and pushes them
with `updateSettings`; the city reports with `on(kind, listener)`. Layout
runs in a worker under `src/layout/` (snapshot-tested — keep output
identical).
- `src/index.ts` is the public surface. `tests/index.ts` is a second one,
`@codecity/city/testing`: the wire fixtures and stubs a consumer needs to
test against a city. The renderer stubs sit behind
`@codecity/city/testing/three` — a `vi.mock('three')` factory that awaits
the main barrel deadlocks, because the barrel reaches source that imports
three.
- Every import inside this package is RELATIVE, and there is no path alias to
add one back. An alias in published source resolves only if the consumer
maps the same prefix; ours did, which hid the fact that nobody else could.
The app reaches this package through `@codecity/city` and nothing else — a
test may reach past that surface, by explicit path so it says so, but no
file under `src/` may.
- `packages/app/` — Preact + TypeScript frontend. Two routes, one view each:
`/` is the landing (pick a project) and `/city?src=…` is a world. The URL is
the source of truth for both — `router/` owns it, and `?src`, `?mode`,
`?commit` and `?sel` all survive Back and Forward.
- This is where signals live. `state/stores/city.ts` is the seam: it holds the
handle, mirrors the city's hover and selection onto app signals, and
attaches the app's half of each event family (`attachCityChrome`,
`attachBuildProgress`, `attachScanProgress`).
- `src/state/` — stores under `stores/`, each named for the question it
answers. `settings/` is its own subsystem: the city declares the fields and
what each one costs, the app owns their values, persistence and signals.
- `src/components/` — grouped by what a component is, not where it appears. A
component used by exactly one thing lives beside that thing instead.
- `api/` — FastAPI backend that walks the repo and serves the manifest. Layered,
and imports only ever point down: `routers/` → `scan/` → `git/` and `cache/` →
`models/`, `core/`, `utils/`.
- `tests/integration/` is the seam under test: the app driving a real city.
Everything testing the city itself lives in `packages/city/tests/`.
- `packages/api/` — the Python project: `pyproject.toml`, `uv.lock`, and its own
README and LICENSE (hatchling refuses paths outside the project directory).
The importable package is `packages/api/api/`, because Python resolves
`import api` by finding a directory named `api` and the manifest has to sit
above the directory it names. FastAPI backend that walks the repo and serves
the manifest. Layered, and imports only ever point down: `routers/` →
`scan/` → `git/` and `cache/` → `models/`, `core/`, `utils/`.
- `routers/` — the whole HTTP surface, one module per route family. `sse.py`
is the streaming plumbing the two SSE routes share, not a route.
- `git/`, `scan/` and `cache/` each curate a barrel in `__init__`.
Expand Down
Loading
Loading