Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions .claude/skills/builder-piece/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
name: builder-piece
description: The steps for one System Builder piece in CITY_NET, from branch to the report with a compare link. Use when starting, finishing or pushing any piece of builder work, or any small PR into feature/system-builder.
---

# One builder piece

Builder work goes in small pieces, one branch and one PR each, into `feature/system-builder`.
The person who owns the repo merges; never push to `main` or `dev`.

## 1. Start

- The previous piece must be merged first. Then branch from the updated origin:
`git fetch origin && git checkout -b sb/<piece> origin/feature/system-builder`.
- A new page, window or mechanic gets a mockup first (`docs/mockups/`, listed in its README), and is
built only once it's approved.

## 2. Build

- Rules go in pure modules (backend `systemBuilder/`, frontend `src/sheets/`), and components only
draw them.
- Use theme variables only, and portal floating UI into `themeRoot()` (see CLAUDE.md).
- A new or changed hint goes in `frontend/src/sheets/builderHelp.ts`.

## 3. Test

- Add tests for what was built: server tests for rules and routes, component tests for pages.
- Run the tests of every file that imports a changed module (grep for its name), plus
`npx tsc -b` and `npx eslint <files>` in `frontend/`. Check exit codes.
- If a message changed, grep the tests for its old words.
- Run the **mutation check** (the `mutation-check` skill) on the new logic. Every break should be
caught; for each one that survives, add or tighten a test, or remove the code it shows isn't
needed.
- **Browser journeys:** a new page, tab or panel joins `e2e/tests/builder-clicks.spec.ts`. A new
thing the GM makes gets its own short journey. Run the new journey and its neighbours locally,
never the whole suite (CI does that).

## 4. Upload paths

Check that every way in and out of a system carries what the piece adds, and add a round-trip test
(download, then import back unchanged):
- the CHOICES importer (`backend/systemBuilder/choicesImport.js`) and its DOWNLOAD EXAMPLE and
DOWNLOAD THIS LIST (`frontend/src/sheets/choicesImport.ts`);
- the sheet importer and its downloadable PDF form;
- the system file and its .zip, exported and installed (`backend/systemBuilder/citysys.js`).

Say in the report which paths were checked, even when none needed a change.

## 5. Docs, in the same PR

- `CHANGELOG.md`, under `[Unreleased]`: `### Added`, `### Changed`, `### Fixed`, or
`### Under the hood` for work players can't see. Write the entry to a file, then
`node scripts/changelog-add.js "### Added" entry.md`.
- `README.md` Project Structure: a line for every new file, and an updated line for a file that
changed what it does.
- `e2e/README.md`: a row for every new journey. `e2e/COVERAGE.md`: a row for every option added or
changed.
- At the end of a phase, also update the builder's tour, HELP topics and hints, and extend
`e2e/tests/builder-journey.spec.ts`.

## 6. Push and report

- Commit only the files of the piece (`git add <files>`, never `git add -A`). No AI attribution.
- Push the branch, then report to the user:
- the compare link: `https://github.com/over2take/CITY_NET/compare/feature/system-builder...sb/<piece>`;
- what changed, in plain words;
- what was checked, with real numbers;
- which upload paths were checked;
- then **TO TRY** (steps, and whether the backend needs a restart) or **NOTHING TO TRY**.
- Wait for "merged" before the next piece.
67 changes: 67 additions & 0 deletions .claude/skills/mutation-check/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
name: mutation-check
description: Check that new tests really guard new code in CITY_NET by breaking the code on purpose and confirming the tests fail. Use after writing tests for a builder piece or a fix, before pushing, or when asked to mutation-check.
---

# Mutation check

A test suite that passes proves little until it has failed for the right reason. Break the new
code on purpose, one small change at a time, and confirm a test fails each time. The runner is
`scripts/mutation-check.js`.

## 1. Pick the breaks

Pick 5 to 30 for a piece, aimed at what the piece added or changed. Each one is a single exact
text swap:
- a condition flipped or loosened: `>=` to `>`, `a && b` to `a`, `kind === 'x'` to `true`;
- a constant changed: a limit, a default, a sample value;
- a branch or a line removed (replaced with `''`), a guard dropped;
- a returned field left out, a message's words changed;
- on the frontend: an `aria-*` attribute removed, a condition that shows a control set to `true`.

Leave out breaks that change nothing a person could notice. A surviving break like that only shows
the code could be simpler.

## 2. Write the plan

Write it to the scratchpad, not the repo:

```json
{
"tests": {
"server": { "cwd": "backend", "cmd": "npx vitest run --maxWorkers=2 --bail=1 __tests__/system_builder_x.test.js" },
"front": { "cwd": "frontend", "cmd": "npx vitest run --maxWorkers=2 --bail=1 src/sheets/__tests__/x.test.ts" }
},
"files": [
{ "file": "backend/systemBuilder/x.js", "tests": "server", "mutants": [["if (a && b)", "if (a)"]] }
]
}
```

- Give each file the narrowest tests that cover it. `--bail=1` stops at the first failure, so a
caught break doesn't wait out every other test.
- Each "from" must appear exactly once in its file. Write line breaks as `\n`; the runner handles
CRLF.

## 3. Run it

```bash
node scripts/mutation-check.js <plan.json>
```

The runner checks that the tests pass before anything is broken, saves every file first, and puts
each one back after each break. It prints `N of M caught` and exits 1 if anything survived, was
missing, or didn't come back as it was.

**If a run is stopped or killed:** run `node scripts/mutation-check.js --restore`, then
`git diff` the files, before anything else, and before committing. Never chain a commit after a
mutation run except with `&&`.

## 4. Act on what survived

- **A missing test:** add or tighten one so it fails on that break, then run the plan again.
- **Code that isn't needed:** if no test could catch the break because the code makes no
difference, remove the code.
- **A break that can't be caught by a test:** say why in the report, and don't leave it unexplained.

Report the result as **N of M caught**, and name any test you added because of a survivor.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ backend/nodemon.json
.idea/
.DS_Store
Thumbs.db
.claude/
# Claude Code: local settings stay private; the repo's skills are shared.
.claude/*
!.claude/skills/
.agents/

# Tunnel Configs and Certificates
Expand Down
74 changes: 74 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# CITY_NET: notes for AI coding agents

For any AI coding tool working on this repo (Claude Code reads it through `CLAUDE.md`), and for the
people using one.

A shared city map for tabletop games, with tokens, character sheets, shops, a bank and dice, run
by a GM for their players. Since Phase 1 a **System Builder** lets a GM make any game's rules:
words, parts, stats and formulas, rolls, choices, rests, conditions.

The builder's plan and its mockups live in `docs/`, which is gitignored and exists only on the
main developer's machine. If you don't have it, don't go looking for it: ask the owner, and work
from the code, `CHANGELOG.md` and the README.

- `backend/`: Node + Express + Socket.IO, SQLite. The builder's rules are `backend/systemBuilder/`.
- `frontend/`: React + TypeScript + Vite. Pure logic sits in `frontend/src/sheets/`, and the
components only draw it.
- `e2e/`: Playwright browser journeys against the real backend and frontend (`e2e/README.md`).

## Commands

| What | Where | Command |
|---|---|---|
| Backend tests | `backend/` | `npx vitest run [files]`. Vitest, never `npx jest`. |
| Frontend tests | `frontend/` | `npx vitest run [files] --maxWorkers=2` |
| Frontend typecheck | `frontend/` | `npx tsc -b`. Plain `tsc --noEmit` checks nothing here. |
| Frontend lint | `frontend/` | `npx eslint <files>` |
| Browser journeys | `e2e/` | `npx playwright test tests/<file> --workers=1 --retries=0` |
| Mutation check | root | `node scripts/mutation-check.js <plan.json>` (see `.claude/skills/mutation-check/SKILL.md`) |
| Changelog entry | root | `node scripts/changelog-add.js "### Added" <entry.md>` |

Check a test run by its exit code, not its summary line: Vitest can print every test green and
still exit 1 on an error outside a test. The person you're working for is usually on the same
machine, so go easy on it: `--maxWorkers=2`, one headless browser, a small viewport, and stop
servers when done. Their dev server and live database (`backend/city.db`) are theirs: never use them
for tests. The journeys start their own servers on ports 5055 and 5174.

## House rules

- **Git:** never push to `main` or `dev`. Builder work goes in small pieces, one branch each, PR'd
into `feature/system-builder`. Wait for a piece to be merged before starting the next one,
with no stacked branches. No AI co-author trailers or "generated with" footers.
- **Every piece** follows `.claude/skills/builder-piece/SKILL.md`: tests, a mutation check, the
upload paths, the docs, then a compare link and what to try.
- **US spelling** everywhere (color, armor), as the code's own names are.
- **Themes:** all 7 must work. Only theme variables, never a hard-coded color: `--green`, `--cyan`,
`--black`, `--dark-green`, `--danger`, `--warning`, `--grid-section`, `--grid-cell`,
`--panel-bg`. Portal floating UI into `themeRoot()` (`frontend/src/utils/themeRoot.ts`), never
`document.body`, or it falls outside the theme.
- **Windows:** use `DraggableWindow`, named like a program (`SHOP.EXE · VIC'S ARMS`), and
`TerminalWindow` for a window with several parts. Prefer folders in an existing window over new
windows. Keep rules in tested pure modules.
- **Line endings:** the source is CRLF. A script that edits files reads them as LF and writes CRLF
back. On Windows, don't pass quoted code to `node -e` from PowerShell: write a script file.
- **Lint:** react-refresh allows no hooks or other exports beside an exported component, so hooks go
in `frontend/src/hooks/`.
- **New libraries:** ask first, and add each to the README's Tech stack table in the same PR.
- **Frontend tests and backend code:** a frontend test may load a backend module only if that
module needs no third-party package (CI's frontend job has none).
`frontend/src/__tests__/crossBoundaryImports.test.ts` checks it.
- **Changing a message:** grep the tests for the old words before pushing. Tests pin messages word
for word.

## Browser journeys

- **On CI they run in four parts at once**, split test by test, each part on a fresh database. So
every test stands alone: never lean on what an earlier test made.
- CI is several times slower than a desktop, so wait for what you expect (`expect`, `expect.poll`),
never for a fixed time. A builder draft saves itself 5 s after the last edit: wait for
`save-status` to say `· SAVED` before reading it through the API.
- Look up a status within its panel, never across the whole page: each formula box has a
`role="status"` words line.
- A new builder page, tab or panel joins `tests/builder-clicks.spec.ts` in the same PR. A new thing
the GM makes gets its own short journey. Each option a GM can set is proven at the table once,
tracked in `e2e/COVERAGE.md`.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,13 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

### Under the hood

- **Notes and skills for AI coding tools in the repo.** `AGENTS.md` holds the project's commands and
house rules for any AI coding tool that reads it (Cursor, Copilot, Codex and others), and `CLAUDE.md`
loads it for Claude Code, so there is one copy. `.claude/skills/` holds two shared skills, `builder-piece` (the steps for each builder piece)
and `mutation-check` (breaking new code on purpose to see the tests catch it). Two helpers moved
into `scripts/`: `mutation-check.js`, which runs the tests first, puts every file back and can
undo a killed run with `--restore`, and `changelog-add.js`. The rest of `.claude/` (local
settings) stays out of the repo.
- **The browser journeys run in four parts at once on CI.** Each pull request's journeys were run
one after another on one machine, about 27 minutes. They now run on four machines at once, each with
its own servers and database and a share of the tests, about 12 minutes. The check is still called
Expand Down
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
The project's notes for AI coding agents are in AGENTS.md, shared with other tools. The skills it
names are in .claude/skills/.

@AGENTS.md
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1294,6 +1294,14 @@ CITY_NET/
├── Dockerfile.backend
├── Dockerfile.frontend
├── .github/workflows/ # CI Tests on PRs and main; Release to Docker Hub on green main; Dev Build to Docker Hub on dispatch or a push to dev. Includes Updater on Real Docker (docker-test/updater-journey.mjs), and Nginx Proxy Behaviour, which runs the real nginx against the repository config and a stub upstream — a 40MB body through, the caller's address forwarded, the socket upgrading — because every other test mounts a router directly and never sees the proxy. And Browser Journeys, the e2e/ Playwright tests in a real browser
├── .claude/skills/ # Skills for Claude Code, shared with the repo (the rest of .claude/ is local and ignored)
│ ├── builder-piece/SKILL.md # The steps for one builder piece: branch, tests, mutation check, upload paths, docs, then a compare link and what to try
│ └── mutation-check/SKILL.md # Breaking new code on purpose to see the tests catch it: picking the breaks, writing the plan, acting on what survives
├── AGENTS.md # Notes for any AI coding tool (Cursor, Copilot, Codex, Claude Code...): what the project is, its commands, and the house rules (git, spelling, themes, windows, line endings, CI journeys); docs/ is only on the main developer's machine
├── CLAUDE.md # Claude Code's way in: it loads AGENTS.md (@AGENTS.md), so there is one copy of the rules
├── scripts/ # Helpers for working on the repo, run from its root
│ ├── mutation-check.js # Runs a mutation plan: tests pass first, each break run against its tests and put back, a backup so --restore undoes a killed run, exit 1 on a survivor
│ └── changelog-add.js # Adds an entry from a file under a heading of CHANGELOG.md's [Unreleased], keeping its line endings
├── docker-compose.yml # Image tags read ${IMAGE_TAG:-latest}, so the release channel is a setting rather than an edit
├── nginx.conf # Proxies /api and the socket to the backend. Forwards X-Forwarded-For, without which every request reaches the app from this container's address and anything counting per caller counts the whole table as one; and allows a body at least as large as the biggest upload the app accepts, or a large map is refused by the proxy before the app ever sees it. A backend test asserts the second
└── .env.example
Expand Down
31 changes: 31 additions & 0 deletions scripts/changelog-add.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/usr/bin/env node
// Adds an entry to CHANGELOG.md under a heading of its [Unreleased] section: the first heading of that
// name before the first released version. The entry is a file (written with an editor, so it has no
// stray BOM or shell quoting), its own lines kept; the changelog keeps its line endings.
//
// node scripts/changelog-add.js "### Added" entry.md

const fs = require('fs');
const path = require('path');

const [heading, entryFile] = process.argv.slice(2);
if (!heading || !entryFile) {
console.error('Usage: node scripts/changelog-add.js "### Added" <entry-file>');
process.exit(2);
}
const F = path.resolve(__dirname, '..', 'CHANGELOG.md');
let t = fs.readFileSync(F, 'utf8');
const crlf = t.includes('\r\n');
if (crlf) t = t.replace(/\r\n/g, '\n');
const entry = fs.readFileSync(entryFile, 'utf8').replace(/^\uFEFF/, '').replace(/\r\n/g, '\n').replace(/\n*$/, '\n');
const start = t.indexOf('## [Unreleased]');
const end = t.indexOf('\n## [', start + 1);
const head = `${heading}\n\n`;
const at = t.indexOf(head, start);
if (start < 0 || at < 0 || (end > 0 && at > end)) {
console.error(`No "${heading}" under [Unreleased] in CHANGELOG.md`);
process.exit(1);
}
t = t.slice(0, at + head.length) + entry + t.slice(at + head.length);
fs.writeFileSync(F, crlf ? t.replace(/\n/g, '\r\n') : t);
console.log(`Added under ${heading}.`);
Loading
Loading