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
636 changes: 636 additions & 0 deletions CLAUDE-index.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ Threlte 8 + three.js, peerjs mesh networking (no server beyond PeerJS signaling)
node/flow editor (@xyflow/svelte 0.1.x) driving scene behavior, and a module SDK for
loadable play content. Everything a user does must be visible to connected peers.

**This file is ~7,000 lines; you are shown only its start.** `CLAUDE-index.md` lists every section, entry and
sub-entry with its line: grep the index for a word, then read this file from that line. After editing this file
(or on a merge conflict in the index), run `node scripts/claude-index.cjs`.

## Architecture map

- `src/stores/` — `appStore` (UI panels + `openSceneSection(label)`/`inspectorScrollTo`
Expand Down
16 changes: 16 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,22 @@ released version instead of drifting (it carried a stale 1.8.0 before 1.11.0).
```
(plus every changed module file; the same three lines with `scenes`/`format-2` and
`packs`/`format-1`). A modules merge alone no longer reaches production's gallery.
- **Record the release's known reds** (the desk battery's baseline, pipeline-1): right after the tag, run the
battery once ON THE TAG — two halves, each on a pc slot where the e2e-slot setup has them — and save it:
```sh
bash scripts/ci/battery-snapshot.sh vX.Y.Z <dir> # git archive + .env + certs + the desk tool (NOT archive alone)
cd <dir> && BATTERY_COMMIT=vX.Y.Z node scripts/ci/battery-desk.cjs run --half A --out test-results/bl --name blA # and --half B
node scripts/ci/battery-desk.cjs baseline save vX.Y.Z <dir>
```
The next integrator runs the same halves on its union and calls `battery-desk.cjs diff --baseline vX.Y.Z <dir>`:
a red whose every twice-red check is red on the tag too is PRE-EXISTING (no A/B run), a red that went green on
the end-of-half retry is FLAKY (logged; `battery-desk.cjs flakes` prints the rates), and only NEW reds need a look.
No pc slot for the tag run? Save the releasing union's OWN last battery as the tag's baseline instead (it is the
released code, run the way the next integrator will run it): `battery-desk.cjs import <u1A.txt> --logs <dir>
[--retry <rerun.txt>]` per half, then `baseline save vX.Y.Z <dirs>`. Measured on 1.43 (51-int) against 50-int's 1.42
battery imported that way: 50 reds -> 33 pre-existing, 11 NEW after the retry.
A new `tests/e2e/*.test.cjs` goes into `tests/e2e/battery.json` (suites, or `exclude` with a reason) —
`tests/unit/batteryDesk.test.js` fails on a file in neither.
- The peers warn (never block) on version mismatches, and `.tpscene`/`.tpmodule`
files confirm before loading a NEWER format int — older files always load
silently. Bump `SESSION_FORMAT`/`MODULE_FORMAT` only when the shape actually
Expand Down
496 changes: 496 additions & 0 deletions scripts/ci/battery-desk.cjs

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions scripts/ci/battery-snapshot.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# battery-snapshot.sh <tag> <dir> — a tree of a release tag to run the desk battery's baseline on.
#
# `git archive <tag>` alone is NOT enough, and the difference is silent: MEASURED (pipeline-1, 2026-10-24) a v1.42.0
# archive without the gitignored `.env` booted on the public PeerJS cloud instead of the self-hosted signaling box,
# and every two-peer suite failed at "connect: could not fill the peer id" on the PC slot while the pages would not
# boot at all on the Deck -- 67 of 228 suites red for a reason no integrator's union shares. So the snapshot takes
# this checkout's `.env` and `certs/localhost.*` too (the dev server's TLS), and the desk tool from THIS tree
# (a tag older than it has no scripts/ci/battery-desk.cjs). node_modules: a pc slot installs it by lockfile; for a
# local run, `npm ci` in <dir> (or hard-link a checkout's whose package-lock.json is identical).
set -euo pipefail
tag="${1:?usage: battery-snapshot.sh <tag> <dir>}"; dir="${2:?usage: battery-snapshot.sh <tag> <dir>}"
here="$(cd "$(dirname "$0")/../.." && pwd)"
[ ! -e "$dir" ] || [ -z "$(ls -A "$dir")" ] || { echo "battery-snapshot: $dir is not empty" >&2; exit 2; }
mkdir -p "$dir"
git -C "$here" archive "$tag" | tar -x -C "$dir"
for f in .env certs/localhost.crt certs/localhost.key; do
if [ -f "$here/$f" ]; then mkdir -p "$dir/$(dirname "$f")"; cp "$here/$f" "$dir/$f"; else echo "battery-snapshot: no $f in $here (the run may differ from a lane's)" >&2; fi
done
mkdir -p "$dir/scripts/ci" "$dir/tests/e2e"
cp "$here/scripts/ci/battery-desk.cjs" "$here/scripts/ci/battery.cjs" "$dir/scripts/ci/"
cp "$here/tests/e2e/battery.json" "$dir/tests/e2e/"
if git -C "$here" diff --quiet "$tag" HEAD -- package-lock.json; then lock=same; else lock=differs; fi
echo "$tag $(git -C "$here" rev-parse --short "$tag^{commit}") (package-lock vs this tree: $lock)" > "$dir/.snap"
echo "battery-snapshot: $dir = $tag + .env + certs + the desk tool; run it with BATTERY_COMMIT=$tag"
14 changes: 12 additions & 2 deletions scripts/ci/battery.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const BATTERY = path.join(ROOT, 'tests', 'e2e', 'battery.json');
/**
* @typedef {{ comment?: string, refs: Record<string, {repo: string, ref: string}>, shards: {target_minutes: number, max: number, min: number},
* pc?: {games: string[], levels: string[]}, skip: Record<string, string>, optional: Record<string, string>, setup: Record<string, string>,
* suites: Record<string, number> }} Battery
* suites: Record<string, number>, modules?: Record<string, number>, desk?: Record<string, string[]>, exclude?: Record<string, string[]> }} Battery
* @typedef {{ name: string, status: 'pass'|'flaky'|'fail'|'timeout'|'gone', required: boolean, rc: number|null, secs: number,
* retrySecs?: number, pass: number, fail: number, last: string, shard: number }} SuiteResult
*/
Expand Down Expand Up @@ -300,6 +300,10 @@ function write(b) {
`\t"setup": ${obj(b.setup, '\t')}`,
`\t"suites": ${obj(b.suites, '\t')}`
];
// the desk battery's keys (battery-desk.cjs): written only when present, so a file without them stays byte-identical
if (b.modules) body.push(`\t"modules": ${obj(b.modules, '\t')}`);
if (b.desk) body.push(`\t"desk": ${obj(b.desk, '\t')}`);
if (b.exclude) body.push(`\t"exclude": ${obj(Object.fromEntries(Object.entries(b.exclude).map(([k, v]) => [k, [...v].sort()])), '\t')}`);
fs.writeFileSync(BATTERY, `{\n${body.join(',\n')}\n}\n`);
}

Expand All @@ -309,8 +313,14 @@ function refresh(argv) {
const lists = opts(argv, '--list');
if (lists.length) {
// the integrator's battery lists ARE the battery: names not in them leave, new ones arrive at 120 s
const names = new Set(lists.flatMap((f) => fs.readFileSync(f, 'utf8').split(/\s+/).filter((n) => n && !n.startsWith('MOD:'))));
const all = lists.flatMap((f) => fs.readFileSync(f, 'utf8').split(/\s+/).filter(Boolean));
const names = new Set(all.filter((n) => !n.startsWith('MOD:')));
b.suites = Object.fromEntries([...names].map((n) => [n, b.suites[n] ?? 120]));
// the modules repo's flights (MOD:x) are the desk battery's, never a runner's
const mods = all.filter((n) => n.startsWith('MOD:')).map((n) => n.slice(4));
if (mods.length) b.modules = Object.fromEntries(mods.map((m) => [m, b.modules?.[m] ?? 600]));
// a suite that joined the battery is no longer excluded
if (b.exclude) for (const k of Object.keys(b.exclude)) b.exclude[k] = b.exclude[k].filter((/** @type {string} */ n) => !names.has(n));
}
/** @type {Record<string, number[]>} */
const desk = {};
Expand Down
70 changes: 70 additions & 0 deletions scripts/claude-index.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env node
// CLAUDE-index.md — A TABLE OF CONTENTS FOR CLAUDE.md, BY LINE.
//
// CLAUDE.md is ~7,000 lines (500 KB) of architecture map and gotchas, and an agent is shown only its
// first screen. Nobody reads it whole, so what it knows is found by luck or by an expensive grep across
// the whole file. The index is one line per entry — every `## ` section, every top-level `- ` bullet and
// every bold sub-entry (` · **…**`, ` **…**`) — with the line it starts on, so a reader greps the
// index for a word, then reads CLAUDE.md at that offset.
//
// node scripts/claude-index.cjs # write CLAUDE-index.md
// node scripts/claude-index.cjs --check # exit 1 when CLAUDE-index.md is not what this would write
//
// Generated, never edited: a merge conflict in it is resolved by running this again.

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

const ROOT = path.resolve(__dirname, '..');
const SRC = path.join(ROOT, 'CLAUDE.md');
const OUT = path.join(ROOT, 'CLAUDE-index.md');

/** the entry's name: its bold lead if it has one, else its first words @param {string} text */
function title(text) {
const bold = /^\*\*(.+?)\*\*/.exec(text);
const t = bold ? bold[1] : text;
const flat = t.replace(/\s+/g, ' ').trim();
return flat.length > 110 ? flat.slice(0, 107).replace(/\s+\S*$/, '') + ' …' : flat;
}

/** @param {string} md @returns {string} */
function build(md) {
const lines = md.split('\n');
const out = [
'# CLAUDE-index.md — where things are in CLAUDE.md',
'',
'Generated by `node scripts/claude-index.cjs` (never edit; on a merge conflict, run it again).',
'Grep this file for a word, then read CLAUDE.md from that line (`Read CLAUDE.md offset=<line> limit=60`).',
`${lines.length} lines indexed.`,
''
];
lines.forEach((l, i) => {
const n = String(i + 1).padStart(5);
let m;
if ((m = /^(#{1,3}) (.+)/.exec(l))) out.push(`${m[1].length === 1 ? '' : '\n'}${n} ${m[1]} ${m[2].trim()}`);
else if ((m = /^- (.+)/.exec(l))) out.push(`${n} - ${title(m[1])}`);
else if ((m = /^ {2}(?:· )?(\*\*.+)/.exec(l))) out.push(`${n} · ${title(m[1])}`);
});
return out.join('\n') + '\n';
}

if (require.main === module) {
const want = build(fs.readFileSync(SRC, 'utf8'));
if (process.argv.includes('--check')) {
const have = fs.existsSync(OUT) ? fs.readFileSync(OUT, 'utf8') : '';
if (have !== want) {
console.error('CLAUDE-index.md is stale: run node scripts/claude-index.cjs');
process.exit(1);
}
console.log('CLAUDE-index.md is current');
} else {
fs.writeFileSync(OUT, want);
console.log(`CLAUDE-index.md: ${want.split('\n').length} lines for ${mdLines()} lines of CLAUDE.md`);
}
}

function mdLines() {
return fs.readFileSync(SRC, 'utf8').split('\n').length;
}

module.exports = { build, title };
Loading
Loading