Architecture notes for contributors
The engineering rules, in particular:
- Keep protocol adapters independent of each other and keep UI state separate from execution state.
- Never block the renderer. Use bounded concurrency, stream data, and never hold unbounded payloads in memory.
- Never log secrets. Route every persisted artefact through the
Redactor. - Label AI-generated output (
source: 'ai-judge', AI-generated suggestion banners).
The map, and what keeps it
packages/core the engine, host-agnostic (Node): the same code runs in the CLI, the desktop app and a server later
model/ the types of everything saved (types.ts) and the schema version
storage/ workspaces on disk: collections, environments, tests, trash, git files, secrets, trust, watcher; the
workspace export (workspace-bundle, bundle-secrets: files capped, secrets replaced) and unique node ids
protocols/ http, graphql, grpc, websocket, socketio, mqtt, kafka, mcp: one client each, nothing shared between them
runner/ test files, suites, the run loop, flows (edits, blocks, data runs, env matrices, webhooks, timelines), results, baselines, CI config
eval/, load/ checks and evaluators, load tests
ai/ LLM providers, and the AI features: the task registry and runner (tasks/), the context builder (redaction,
privacy levels, budgets), structured output, verifiers, proposals (apply / undo) and the AI settings;
the analyses of a failed run (run-evidence, triage) and of a flaky test (flaky, flaky-run), the fix
items they share (fix-items) and the tp.* reference read from the sandbox (script-reference);
the usage ledger is storage/ai-usage.ts (see Design ▸ TestPion's own AI features)
import/ Postman, Insomnia (template tags), Bruno (.env, bru.* → tp.*), Hoppscotch, GraphQL SDL, OpenAPI, HAR,
cURL/fetch snippets, save-request (secrets to variables); foreign auth that never becomes inherit; notes
security/ the one secret detector (names and value shapes, with a confidence) for exports, git check and masks
scripts/ the tp.* sandbox (QuickJS in WebAssembly): scripts never reach Node
git/ git through the system git, changes by meaning, the merge driver
net/ the production guard (one rule for every host), private-network guards
mcp-server/ the workspace as MCP tools for agents: tool.ts (a tool), tool-contract.ts (paging, response_format, confirm, output schemas) and tool-docs.ts (when / when not / example), one module per subject, testpion-mcp.ts composes them
index.ts the public surface: everything the CLI and the app may use
packages/shared pure, browser-safe code for every host; output/ is the one registry of downloads: formats.ts (formats
and builders), filename.ts (safe names), csv-write.ts (the formula guard), surfaces.ts (every output that
can be saved, its formats and secrets policy; tests/unit/output-surfaces.test.ts keeps it complete)
packages/cli commands over core (commands/*.ts); every --json is one envelope (output.ts), exit codes 0 / 1 / 2 / 3
apps/desktop
backend/ the app's RPC handlers over core (handlers/*.ts, one file per subject); backend.ts wires them and owns long-running state;
production-guard.ts checks every sending or running call against production environments
electron/ the window, menu, updater, preload (the context bridge), --mcp-server and --merge-driver modes
src/ the renderer (React): talks to the backend only through api.ts (RPC); from core it takes types only;
components/output/ is the down arrow (OutputActions / OutputHeader): a new output describes itself as
an OutputSpec there and its id in surfaces.ts, and the backend's handlers/output.ts writes the file
e2e/ the UI regression suite (plans/*.cjs) and the profiler plan
docs/ the published site; planning/ is internalThree rules, kept by tests/unit/architecture.test.ts (it runs with npm test):
- Boundaries. Core imports no Electron, React or app code; the backend never imports the renderer; the renderer never imports Node, Electron, backend files or core at run time. The renderer is a web page that a cloud host will serve as it is.
- Big files do not grow.
backend.ts,testpion-mcp.ts,cli/commands/data.ts,workspace.tsandtypes.tshave a ceiling; a new MCP tool goes into the module of its subject (git-tools.ts,workspace-edit-tools.ts, …), a new RPC handler intohandlers/<subject>.ts, a new CLI command intocommands/<subject>.ts. Nothing else passes 1,200 lines. - One place per feature, every host. A feature is core logic + a CLI command with
--json+ an RPC handler + (when an agent would want it) an MCP tool, with the tool's name added to the pinned list intests/integration/workspace-run.test.ts.
Migrations
Workspace format changes need a new entry in MIGRATIONS (packages/core/src/storage/workspace.ts), a bump to SCHEMA_VERSION, and a test. Each migration upgrades exactly one version, and opening a newer format is refused rather than silently broken. The SQLite metadata schema has its own versioned migrations in metastore.ts.
