Three commands and one rule.
npm install
npm run build:ui && npm run typecheck
npm testThe rule: never edit public/index.html. It is generated. The frontend lives in src/ui/ as small source files — shell/ (the document's head and tail), styles/, markup/ (one file per view and modal) and app/ (the behaviour) — and scripts/build-ui.js stitches them, in src/ui/manifest.json order, into that one page. Edit the source, run npm run build:ui, and commit both; node scripts/build-ui.js --check says whether the page matches its sources without writing it. tests/buildUi.test.ts fails the suite if the page drifts from its sources, and a new source file must be added to the manifest or it never ships. src/ui/README.md explains the layout.
npm run devruns the server with reload;npm run build && node scripts/dev-isolated.jsruns it against a throwaway data folder, so the scheduler cannot start scanning your real folders (it starts the compileddist/server.js, hence the build).- One test file at a time:
npx tsx --test tests/<name>.test.ts. The whole suite takes minutes;npm testruns it on every OS in CI, so usepath.joinandos.tmpdir()in tests rather than POSIX literals, and never touch a real home folder — synthetic fixtures only (tests/realHomeUntouched.test.tsfails a test that removes a path built from it). - Every change to behaviour comes with a test that failed before the change. A test that never went red is not evidence.
- A test never touches the owner's app data or the machine's Trash. App data: every test file that loads
src/importstests/fixtures/dataDir.tsat its top and callsisolatedDataDir()there, and makes its fixtures withfileTempDir();tests/testDataIsolation.test.tsholds every file to it, and names the few that keep a data folder of their own, each with its reason. The machine: every door to it has a stand-in, and under a test runner the real door refuses — it throwsRealMachineRefusalbefore any call to the operating system, says so on stderr, and fails the test file with exit code 1 even if the throw was caught (src/services/realMachineGuard.ts). The doors and their stand-ins: moving a file to the Trash (setTrashStepForTests,src/services/cleaner.ts— every delete, cart commit, offload, encode, restore and Autopilot run goes through it), emptying the Trash and listing it (TREEMAP_TRASH_DIR, a folder the test owns — a server whose page boots asks for the Trash's size), deleting Time Machine's local snapshots (setSnapshotPurgeStepForTests), a cloud provider's trash (TM_<PROVIDER>_API, a stand-in server) andgit gc --prune=now(setGitGcStepForTests). "Under a test runner" meansTREEMAP_FORBID_REAL_TRASH, whichnpm testsets for the whole run,tests/fixtures/dataDir.tssets as it loads (so a plainnpx tsx tests/x.test.tsis guarded too) andnestedRunEnv()puts into every nested run, orNODE_TEST_CONTEXT, which--testsets in each test file's process. A child a test starts carries them when its environment is built from...process.envornestedRunEnv(); a hand-builtenvdrops both, andtests/childEnvCarriesGuard.test.tsfails on one. Outside a test runner every door goes ahead, as the app needs it to;tests/realMachineGuard.test.tsshows so for each, in a child that disarms the machine itself. Until 30 Sep 2026 none of this was enforced, and tests put files into the owner's real Trash on every run (tests/realMachineGuard.test.tssays which). - What a person reads is written in English on every machine, because the interface is English and
formatBytesalways prints an English decimal point. Sizes are in decimal units (1 KB = 1,000 bytes, 1 GB = 1,000,000,000 bytes, as Finder counts them):src/utils/formatBytes.tson the server and the page's copy insrc/ui/app/000-prelude.js, whichtests/polishServerNumbers.test.tsholds to the same output. A size a person types goes through the page'sKB,MB,GBandTBbeside it, andtests/decimalUnitsUi.test.tsfails on a unit list whose option is not its decimal number of bytes, and on a 1024-based size in the page's script spelled as 1024 itself (v /= 1024,n % 1024), as a shift or power of two by 10, 20, 30 or 40 bits (1 << 20,2 ** 30), or as an integer literal that is a whole number of MiB (104857600) — not on smaller powers of two such as 4096 or on hex literals, so write a size with the page'sMBorGB(5 * GB) rather than counting on the test to catch every spelling of 1,024; a real binary quantity (a buffer, a block size) keeps its binary value and is named in KiB or MiB. A count goes throughformatCount(src/utils/formatCount.tson the server, the page's own insrc/ui/app/000-prelude.js) and comes out "1,234" everywhere. A date namesUI_LOCALE, and a clock also passes the machine's hour cycle (HOUR_CYCLEon the page,machineHourCycle()on the server), so it reads "Sep 6, 2026" but keeps the machine's 12- or 24-hour clock and its time zone. Never a locale-lesstoLocaleString(),toLocaleDateString([])orIntl.DateTimeFormat(undefined, …): those follow the machine's locale, and once printed "1.234 shapes" beside "1.2 GB" and Arabic-Indic digits inside English sentences.tests/localeIndependence.test.tsbans the locale-less forms in the app, comments included (describe the call without its leading dot), and CI runs the whole suite under a Portuguese locale, so a test that hard-codes locale output, or a stub that follows the machine, goes red there. - Deletes go to the Trash, never a hard delete; nothing outside a scanned folder is ever touched. Those two rules are the product.
The app runs without it, but the scan engine is Rust (native/treemap-core). With Rust from https://rustup.rs: npm run build:native builds the module (on Windows also tm-mft-helper.exe) into native/prebuilt/<platform>-<arch>/; npm run test:native runs cargo test --workspace. rust-toolchain.toml pins the Rust version, and rustup installs it the first time cargo runs there. Before pushing a Rust change, run what CI runs in that folder: cargo fmt --all -- --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace. Without the module, or without gdu (npm run fetch:gdu:dev), the tests that need them skip on your machine and fail on CI, which builds and fetches both on every leg. Bump nativeVersion in package.json and the workspace version in native/treemap-core/Cargo.toml together whenever native/index.d.ts changes. Every dist build needs Rust too: it builds the module first and stops, with the reason, if it cannot. native/README.md has the rest.
Everything the user reads is calm, plain English in sentence case: "folder", not "directory"; "Trash", not "unlink"; a reason, never an error code.
- A bug: the bug report template asks for what a TreeMap report needs.
- A security problem: privately, as SECURITY.md says — not a public issue.
- Conduct: CODE_OF_CONDUCT.md.