Decisions Code and tooling - axolotl-logic/project-buddy GitHub Wiki

Decisions: Code and tooling

Part of Decisions. Each decision has a name, which issues and code comments use to refer to it.

practices-cover-all-code

The coding practices cover all code in the Project Buddies software ecosystem, not only the project-buddy site (Axlog, 2026-10-15). That includes the site in apps/web/, the issue pipeline in apps/issue-pipeline/, the software factory's agents and prompts, and every script and tool beside them. Each decision on this page applies to all of that code unless it names the site's files or frameworks. The general rules in CONTRIBUTING.md (Design, Types, Lint, and test-first, flake-free tests) apply to every app. Its Product, UI, Data and Prompts sections describe the site. Code that doesn't meet the practices yet is tracked as an issue, as the issue pipeline is in #162, and isn't exempt from them.

aposd-and-strict-typing

Ousterhout's A Philosophy of Software Design, and the strictest available TypeScript and Biome settings (Axlog, 2026-10-10), for all code (practices-cover-all-code). Both are written out in CONTRIBUTING.md, and reviews check every change against them.

biome-for-lint-and-format

Biome lints and formats instead of ESLint and Prettier, because eslint-config-next pulled in [email protected] (GHSA-vfj7-8cjw-p6xm), which had no patched release.

biome-infos-fail

Every Biome group is set to error, so info diagnostics also fail npm run check. Axlog ruled (2026-10-10) that no rule is turned off, the UI may be redesigned to satisfy the rules, and tests get no looser rules. Rules for other frameworks stay off.

biome-config-authoritative

The Biome config is authoritative (Axlog, 2026-10-10): the root biome.jsonc, which holds the policy, and apps/web/biome.jsonc, which extends it with the site's scoped overrides. Where a description of the lint setup differs from it, the config wins, and reviewers don't report the difference as drift. The Astro, Svelte, React Native, Turborepo, Solid, Qwik and Vue domains stay off. A new override or biome-ignore still follows best-practice-over-lint.

biome-drizzle-meta-exclusion

drizzle/meta/ stays excluded from Biome, because drizzle-kit rewrites it on every generate (Axlog, 2026-10-10).

best-practice-over-lint

Axlog, 2026-10-10: "Go with the stricter option, especially if it causes a rewrite that improves the general design. Don't hack around the failing rules, design a better system. Default exports are okay, as it is the prevailing setup. Best practices overrule having a lint rule."

  • noDefaultExport has a scoped override for Next's special files and the tool configs.
  • Next's Metadata API exports (metadata, generateMetadata, viewport) are allowed through useComponentExportOnlyModules' allowExportNames (orchestrator's reading).
  • noNodejsModules, noProcessEnv and noConsole are confined to the smallest platform boundaries, which alone get scoped overrides: src/env.ts, error-log.ts, CLI entry points, and the email outbox (src/email/outbox.ts, e2e/outbox.ts). Anthropic's snake_case usage keys get a useNamingConvention override on src/quota/anthropic-usage.ts, its test and spend-meter-test-kit.ts only. In the issue pipeline (apps/issue-pipeline/biome.jsonc, #166), the boundary is the CLI entry points (install.mjs, build.ts, the ops CLI's entry src/ops.ts, #187, and the report CLI's entry src/report.ts, #302) and the ops CLI's modules that run git, gh and commands or keep files on disk (pipeline-root, process, config, comment-only, locks, checkouts, wiki, issue-context, and the ops ops/sweep and ops/claim, #183), and the orchestrator's entry src/orchestrate.ts with its modules that spawn jobs, check pids or keep files (orchestrator/args, orchestrator/snapshot, orchestrator/ledger and orchestrator/log, #234, and orchestrator/escalation-consult, which keeps the escalation agent's reply schema and session folder and gives its pi the orchestrator's environment less the gh tokens, #282); its pure modules get no exception. GitHub's snake_case comment times (created_at, updated_at) get the same useNamingConvention override on src/comments.ts and on test/fake-gh.mjs, the fake gh that answers in that wire format (#219), only. The tests and their kits (test/**) and the installer (install.mjs) also get the pipeline's Node-API override, because they need node:test, node:fs, child_process and process (Axlog, 2026-10-15, #219).
  • as const stays on the <SECTION>_COPY objects in src/ui/copy/, as every copy module already does; everywhere else annotates the type (orchestrator, 2026-10-12). Rejected: annotating every copy section.
  • No lint dodges: no computed keys, bracket reads or process.getBuiltinModule to slip past a rule. A Biome bug with no supported fix goes back to Axlog.

biome-css-class-rules

Biome's noUndeclaredClasses and noUnusedClasses follow imports, and pages escape both because nothing imports them. Roles are plain classes in layered CSS files (page-roles.css, chat.css). Axlog approved (2026-10-11) a scoped override of both rules on the root layout, the site frame it imports (header, footer, theme picker) and the global CSS files, so the layout can use Tailwind utilities.

use-cases-drive-tests

use-cases/ (one file per area, conventions in its README.md) drives the tests. A case is tested by a Vitest component or browser test (src/**/*.test.tsx; browser tests are *.browser.test.tsx, run by npm test in headless Chromium and WebKit) titled by its slug, unless it needs a real server, database or navigation to be effective, in which case it stays a Playwright e2e test. use-cases/use-cases.test.ts checks that every case has a test or group titled exactly its slug, in an e2e file or a src test, or is pending in e2e/pending-cases.ts, never both; that every slug title names a case; that every e2e test outside e2e/visual/ sits under a slug title; and that every link resolves. In src files only the titles of Vitest's test, it and describe are checked: a slug title there must name a case, other src tests keep plain titles, and other text may name any slug. Rejected: keeping every use case in Playwright, because it was too slow (#131). Version 1 is not done until every case has a passing test. Tests use the mock model against a non-production database; only smoke tests use real Claude. A test that needs the browser's time zone is *.zoned.browser.test.tsx, which the plain browser project skips; ZONES in vitest.config.mts gives each zone its own Vitest project, browser-<zone>, whose Chromium and WebKit instances each set the zone and a locale through Playwright's context options (#140). A browser test that measures a layout calls installThemeFonts() once and then awaits loadFonts() before measuring, both from src/themes/theme-fonts-test-kit.ts, since the fallback fonts lay text out differently (#140). The screen sizes and MINIMUM_TARGET_PX live once, in src/viewports-test-kit.ts, for both the browser tests and Playwright.

tests-pin-promised-behaviour

Tests pin promised behaviour, not internals (#230, #255): what the wiki and the README promise, the ops replies' shapes ({ ok, op, posted, ... }, status, code, label), stop codes and their agent:stopped-<control> labels, caps and limits (lease hours, the no-checks grace, the CI wait's deadline, the comment length clip), GitHub-visible effects, and past bugs named by issue number. A GitHub-visible effect is checked through gh's writes only (the calls ghIsRead doesn't count as reads: comments posted or edited on which issue or PR, labels added or removed, claims closed, issues and sub-issues opened), so a test passes whatever reads come between them; in the issue pipeline's tests writes() (test/fake-gh.mjs, test/ops-cli-test-kit.mjs) and test/gh-effects.mjs name them. A comment's body is checked for the facts the wiki promises it holds, such as a stop comment's control, label, what it checks, reason and how to resume, not copied whole. A test that asserts internals is rewritten to assert the promise, or removed when it protects nothing promised. This complements use-cases-drive-tests, which says which behaviour a test covers. Rejected: copied constants, prompts, comment or log text the wiki doesn't promise word for word; call-order, gh-argv or launch-key assertions and object key order that nothing promises; and whole-trace snapshots, which the issue pipeline's tests no longer use (#259): its workflow-stages-* scenarios assert the summary, the ops requests, the state saved at release and the sessions that ran, through test/stage-outcome-test-kit.mjs.

e2e-test-database-speed

The test database's password is hashed with one SCRAM iteration, in compose.test-db.yml only, because the local Neon proxy authenticates every HTTP query (about 8 ms per query instead of 76) (orchestrator, 2026-10-12). The visual projects run in parallel, and the chat screenshots are built from stored rows. Rejected: switching the tests to the websocket Pool, which production doesn't use; and dropping the second full run at merges, which is the flake gate.

Local runs use 80% of the cores as workers, 8 on a 10-core laptop: a full npm run test:e2e took 69–70 s instead of 80–81 s with the default of half the cores, and 73 s with 10 (issue #20, 2026-10-13). CI keeps half the cores, unmeasured there. Rejected, as neither moved the full run's wall time: halving MOCK_SLOW_DELAY_MS to 4 s (70–72 s; the slow test isn't on the critical path, and 4 s is only twice the slowest second-tab exchange seen, 1.9 s), and polling toPass() every 100 ms (71 s; the summed test time stayed at about 472 s). Locator assertions such as toHaveText poll on a fixed schedule Playwright 1.64 doesn't let a config change.

flakes-are-failures

A flake is a failure: fix its cause or the measurement, and never rerun until it passes (Axlog). A passing repeat adds no evidence, and a diagnosis gets two hours before it goes back to Axlog.

biome-overload-false-positive

Biome's noDeprecatedImports checks imports, not calls, so it reports drizzle's primaryKey because one of its overloads (the variadic one) is deprecated, even where code calls the documented primaryKey({ columns }). Biome closed biomejs/biome#7636 as a trade-off of the rule's design, so no release will fix it (Axlog, 2026-10-14). src/db/tables/monthly-spend.ts calls primaryKey({ columns }) and suppresses the rule on that one import with a biome-ignore that gives this reason and links the issue; src/db/monthly-spend-table.test.ts checks it is the only biome-ignore in src/. The rule stays on. Rejected: drizzle's undocumented PrimaryKeyBuilder class, turning the rule off, and waiting for a Biome release.

gitguardian-false-positive

GitGuardian's NODE_CRYPTO_TOKEN alert on src/auth/session-token.test.ts is a false positive: a known-answer token signed with the in-repo test secret (Axlog). Don't rotate it, rewrite history, or derive the fixture at test time.

monorepo-layout

The repo is a monorepo of npm workspaces, with mise monorepo tasks across the apps (Axlog, 2026-10-14). The site lives in apps/web/ and the issue pipeline in apps/issue-pipeline/ (#160). The root holds only the shared tooling and the top-level docs: one package.json that declares the apps/* workspaces and holds Biome and the overrides, the one package-lock.json, mise.toml (the monorepo root, pinning node), the Biome policy in biome.jsonc, AGENTS.md, CONTRIBUTING.md and the README. Each app has its own mise.toml with its tasks, run from anywhere as mise //apps/web:check, or mise //...:check for every app.

Rejected: Turborepo, because its cache would replay the flake gate's second e2e run, its hashes miss the test database and .env.test, fresh pipeline worktrees never hit the cache, and two packages gain nothing from its task graph. Nx and moon, which are built for many projects and languages and have the same caching concern. Bazel, Buck2 and Pants, which need BUILD files per target and a build engineer to maintain them.

sandbox-ready-jobs

Scripts come first: each issue-pipeline stage job declares its boundary, its role (reader, writer or merger), its checkout (since #250 its own per-job checkout, work/<slug>/jobs/issue-N-<stage>, cloned from a bare mirror and removed on exit), the names of the environment its model sessions get and its network, so it is ready to run in a sandbox, and the sandbox comes later (orchestrator, #231, #249). src/job/roles.ts holds one table from stage to role and one rule for sessions: a session runs as writer when its agent is issue-pipeline-writer, and as reader otherwise; no session runs as merger. Agents get no gh token variables, and a writer or merger gets only the token its jobRoles.<role>.tokenEnv names, as GH_TOKEN. The network lists are recorded, not enforced. job.mjs --print-boundary prints the boundary (see Issue-Pipeline).

Rejected: a role table keyed by launch names (StageName | "conflicts" | "ci-fix"), because those two are launches inside sync and checks, not stages --stage accepts, so nothing could look them up; and stage-only session roles, which would give the writer's token to the reader sessions inside fix and checks.

⚠️ **GitHub.com Fallback** ⚠️