This repo intentionally uses a minimal “runtime TypeScript lane” plus boundary validation and small unit tests to prevent regressions in the app’s most fragile, high-impact paths.
This document is the standard we will follow (and enforce) for any future work in this area.
- Reliability at boundaries: Validate/normalize at process boundaries (IPC, disk JSON, device input, sandbox/module execution).
- Zero regression: Valid inputs must behave the same; invalid inputs must fail safely and predictably.
- No bloat: No new dependencies for validation or testing. No “framework creep”.
- Real safety: TypeScript must catch mistakes; avoid “fake TS” (no
any, andunknownonly at true boundaries). - Centralization: Put checks in one place so the rest of the code can assume stable contracts.
- No
anyin runtime TS lane. unknownis allowed only at true boundaries, and must be narrowed immediately.- Examples of “true boundaries”: JSON parsing, IPC payloads, sandbox messages, device callbacks.
- Rule: keep
unknownat the function signature ((value: unknown)) and convert to safe, constrained types in the first few lines using guards.
- No “validation for validation’s sake.”
- Only add runtime validation/tests when the area is truly critical (rubric below).
- No mystery diffs.
- If you can’t explain a changed line, revert it.
- No silent behavior changes.
- For valid data, normalizers should be no-ops (or provably equivalent).
Treat an area as critical only if it matches at least 2 of the following:
- Single point of failure: one change can break core behavior across the app.
- Cross-process contract: data crosses boundaries (renderer ↔ main, main ↔ sandbox, disk ↔ app, device ↔ app).
- Failure is silent or hard to debug: “nothing happens”, subtle timing bugs, intermittent breakage.
- High fan-out: many call sites depend on the shape (e.g., shared JSON state, sandbox contracts).
- User-authored inputs: workspace modules, user-edited JSON, OSC addresses, etc.
- Production risk: likely to break in real environments (filesystem weirdness, corrupted JSON, unexpected IPC payloads).
Examples of critical areas in this repo:
- Input ingestion payloads (
input-event) - JSON bridge read/write (
userData.json,appState.json,recordingData.json) - Sandbox request/result contracts (
sandbox:request,sandbox:toMain) - Workspace module read/list results (
bridge:workspace:*)
Examples of not critical:
- Pure UI state inside a single component
- Cosmetic settings that don’t affect runtime execution
- One-off debug helpers
Do
- Validate/normalize once when data crosses a boundary.
- Make downstream code assume stable types/shapes.
Don’t
- Sprinkle checks across business logic.
- Duplicate validators in multiple layers.
In this repo, the canonical boundary points include:
- Main process IPC handlers in
src/index.js - JSON bridge sanitization in
src/shared/validation/jsonBridgeValidation.ts - Sandbox request/result handling in
src/index.jsand runtime validators insrc/shared/validation/* - Input emission in
src/main/InputManager.ts
- The runtime TS lane compiles selected
.tsfiles insrc/into executable JS underdist/runtime/**. - It is built via:
npm run build:runtime
- It is required by:
npm start(so runtime JS exists before Electron boots)npm run test:unit(tests run against compiled runtime output)
Rule: If you add a new runtime TS file that must execute in Electron/Node, it must be included in tsconfig.runtime.json.
- Define the boundary:
- Where does the data enter/leave? (IPC handler name, JSON filename, device callback, sandbox message type)
- Define the contract:
- What is the minimal stable shape needed downstream?
- Write a normalizer/sanitizer:
- Accept
unknownat boundaries (preferred) or a constrained “json-ish” union. - Return stable outputs.
- Preserve unknown fields unless unsafe.
- Be a no-op on valid inputs whenever possible.
- Accept
- Wire it at the boundary:
- Central place (e.g.,
src/index.jsIPC handler,sanitizeJsonForBridge).
- Central place (e.g.,
- Add minimal unit tests:
- 1–2 “valid input preserved” tests
- 1–2 “invalid input contained” tests
- Prove no regression:
- Run
npm run test:unit.
- Run
Use the established style:
- Prefer boundary signatures like:
normalizeX(value: unknown): X | null
- Internally, narrow into constrained shapes (examples):
type Jsonish = string | number | boolean | null | undefined | object;
- Use safe guards:
isPlainObject(...)Array.isArray(...)asNonEmptyString(...)Number.isFinite(...)
- Avoid over-modeling:
- Only type what you need to enforce contract stability.
Rule: Normalizers must be deterministic and side-effect free.
- Use Node’s built-in runner:
node --test - Tests live in
test/*.test.js - Tests should import the compiled runtime output from
dist/runtime/**when validating runtime TS islands. - Keep tests short and high-signal:
- Avoid big fixtures and broad integration setups unless the boundary truly demands it.
When to go beyond basic unit tests (only for truly critical paths):
- Add a tiny integration test if wiring is easy to break (e.g., an IPC handler must call a sanitizer).
- Add property-style tests only if the failure mode is intermittent and the input space is large.
- Keep it dependency-free (generate randomized inputs with
Math.random()and assert invariants).
- Keep it dependency-free (generate randomized inputs with
Run:
npm run test:unit
Pick the failure strategy that matches the boundary:
- Renderer-facing IPC/bridge reads:
- Prefer returning a safe default (or
null) rather than throwing. - Validation failures should be surfaced as structured
{ ok:false, error:"..." }results where possible.
- Prefer returning a safe default (or
- Sandbox request boundaries:
- Prefer
{ ok:false, error:"INVALID_..." }rather than throwing. - Never send malformed payloads into the sandbox.
- Prefer
- Developer diagnostics:
- Avoid silent
catch {}for validation failures. - Log only at boundaries, and include context (which boundary, which contract), keeping logs concise.
- Avoid silent
type Jsonish = string | number | boolean | null | undefined | object;
function isPlainObject(value: Jsonish): value is Record<string, Jsonish> {
return (
Boolean(value) &&
typeof value === "object" &&
!Array.isArray(value) &&
Object.prototype.toString.call(value) === "[object Object]"
);
}
function asNonEmptyString(value: Jsonish): string | null {
if (typeof value !== "string") return null;
const s = value.trim();
return s ? s : null;
}
export function normalizeCriticalPayload(value: Jsonish): Jsonish {
const v = value as unknown as Jsonish;
if (!isPlainObject(v)) return null;
const id = asNonEmptyString(v.id);
if (!id) return null;
return v;
}const test = require("node:test");
const assert = require("node:assert/strict");
const path = require("node:path");
const { normalizeCriticalPayload } = require(path.join(
__dirname,
"..",
"dist",
"runtime",
"shared",
"validation",
"yourNewValidator.js"
));
test("normalizer preserves valid payload", () => {
const payload = { id: "x" };
assert.deepEqual(normalizeCriticalPayload(payload), payload);
});
test("normalizer rejects invalid payload", () => {
assert.equal(normalizeCriticalPayload({}), null);
});- Prefer 1 normalizer at 1 boundary over many validators scattered in the app.
- Prefer 3–6 tests total for a new critical area; more is usually diminishing returns.
- Don’t normalize deep trees unless needed:
- Stabilize the minimal shape that prevents crashes or silent failure.
- Avoid creating “a schema system”:
- If a validator grows beyond ~200 lines, consider splitting by sub-contract (still no dependencies) or re-check whether the area is truly critical.
Default posture: no new dependencies for validation/testing.
Exception policy: consider a validation library only if ALL are true:
- The contract is complex and changing frequently, and
- Hand-rolled validators are demonstrably causing bugs or slowing iteration, and
- The dependency is small, stable, and widely used, and
- You can justify it with a concrete maintenance win.
This repo currently follows these standards in practice via:
- Runtime TS compilation (
tsconfig.runtime.json→dist/runtime/**) - Boundary validators/sanitizers under
src/shared/validation/** - Main-process boundary wiring in
src/index.js - Minimal unit tests under
test/**usingnode --test