#Engineering Guidelines

Best practices for maintaining high-quality, maintainable code in the writ-kb project, with a focus on AI-assisted development workflows.

If you are working in the /kbpy module follow the guidelines at /kbpy/docs/guidelines.

#Code Ownership

AI-generated code is your code the moment it enters the codebase. Before committing:

  • Read every line. If you cannot explain what it does, do not commit it.
  • Verify it follows the patterns already established in the project.
  • Run the type checker (npm run typecheck) and tests (npm test) before pushing.

#Readability

  • Keep functions short and single-purpose. If a function does two things, split it.
  • Use descriptive names: fetchNamespaces, writeToDomainState, not doStuff or process.
  • Do not add comments that restate what the code already says. Comments should explain why, not what.
  • Refrain from adding excessive inline comments or JSDoc to straightforward code.

#Type Safety

  • TypeScript strict mode is enabled. Do not weaken it.
  • Avoid any. If a type is genuinely unknown, use unknown and narrow it explicitly.
  • Define interfaces for data shapes rather than using inline object types repeatedly.
  • Place shared types in app/utils/types.ts.

#Testing

  • Every user-facing feature or data transformation should have at least one test.
  • Use the existing Vitest setup (npm test / npm run test:watch).
  • Test files live alongside the code they test: _index.test.tsx next to _index.tsx.
  • Prefer testing behaviour over implementation details.

#Error Handling

  • In Remix actions, use try/catch and return json({ error: message }, { status }) on failure. Never throw unhandled errors from actions.
  • Validate proxy responses before parsing. Check res.ok before calling res.json().
  • Surface errors to the user via the fetcher data pattern, not via thrown responses.

#Consistency

Follow the patterns already in the codebase:

  • Arrow functions for all declarations (const fn = () => {}), not function declarations.
  • Import ordering: Node built-ins, then third-party packages, then local imports (~/...).
  • Remix conventions: loader, action, meta, links exports in route files.
  • File naming: Route files follow Remix file-based routing (domains.$domainId.tsx). Utility files use lowercase with dots for separation (helpers.server.ts).
  • Tailwind CSS: Use the design tokens defined in tailwind.config.ts (font families, accent colours, semantic colours). Do not hardcode hex values in templates. Exception: SVG fill/stroke attributes require inline values — group these in a constant object (e.g. BRAND, STATUS_COLOURS) rather than scattering them.

#SQLite / Database

  • Enable WAL mode and foreign keys on every connection: db.pragma("journal_mode = WAL") and db.pragma("foreign_keys = ON").
  • Wrap batch writes in a transaction: db.transaction(() => { ... })(). Single-row inserts do not need an explicit transaction.
  • Use prepared statements (db.prepare().run(), .get(), .all()) rather than db.exec() for queries with parameters.
  • Reserve db.exec() for DDL (schema creation) only.
  • Cast database rows to typed interfaces rather than using any. Prefer as SomeType after .get() or .all() over leaving results untyped.
  • Schema initialisation belongs in an ensureSchema() function called once when the database is first opened. For databases opened read-only (e.g. materialised data produced by an external tool), skip schema creation and instead verify the expected tables exist before querying.

#kbpy (Python)

The kbpy package lives in the monorepo at kbpy/ and is invoked from the pipeline via uv run.

  • All new Python files must include the copyright notice: # Copyright 2026 Ignition Works Limited (09664387).
  • Run quality checks before pushing: ruff check, ruff format --check, pyright, and uv run pytest -x.
  • Follow kbpy's own CLAUDE.md for package-specific conventions.
  • SQLite output from kbpy uses the sqlite3 stdlib module (not better-sqlite3). Enable WAL mode and foreign keys. Use parameterised queries for inserts.

#Proxy Integration

  • Read the proxy URL from process.env.PROXY_URL with a fallback: const PROXY_URL = process.env.PROXY_URL ?? "http://localhost:3001".
  • Always POST to ${PROXY_URL}/query with Content-Type: application/json and body { data: QUERY, ns: false }.
  • Validate the response before parsing: check res.ok, then read res.json(), then parse the EDN string in result.result using parseEDNWithOptions.
  • Return user-facing errors with status context: Proxy responded with ${res.status}: ${await res.text()}.

#Architecture Layers

Separate concerns into three layers. Route files should be thin orchestrators, not holders of business logic or SQL.

  • Data access (app/services/*-db.server.ts): Owns all SQL queries for a given database. Opens the connection, runs prepared statements, returns typed rows. No Remix imports, no business logic.
  • Services (app/services/*.server.ts): Transforms raw data into the shapes consumed by routes. Contains business logic (aggregation, filtering, mapping). Imports from data access modules but has no SQL and no HTTP/Remix concerns.
  • Routes (app/routes/*.tsx): Thin loaders/actions that call service functions and return json(). No direct better-sqlite3 imports, no SQL strings, no data transformation beyond what Remix requires.

Use the .server.ts suffix on service files so Remix tree-shakes them from client bundles.

#Remix Loaders and Actions

  • Loaders and actions should delegate to the services layer. Keep them short — ideally a single function call plus json() / redirect().
  • Open the database at the start of a loader or action; close it before returning.
  • Route actions with a _action form field when a single route handles multiple operations. Switch on _action to dispatch.
  • Return json({ error: message }, { status }) on failure. Return json({ success: true, ...data }) or redirect() on success.
  • Cast formData.get() results to string explicitly. Trim string inputs before storing.

#Script Conventions

  • Scripts use ES modules with the .mjs extension.
  • Use import.meta.url with fileURLToPath for path resolution (no __dirname in ESM).
  • Top-level await is permitted in scripts.
  • Scripts log progress to stdout with console.log. Errors throw with throw new Error().
  • Shared logic between scripts and the app (e.g. EDN parsing) lives in a script-local module (scripts/edn.mjs) rather than importing from app/.

#Dependencies

  • Minimise new dependencies. Every new package is a maintenance burden.
  • Before adding a library, check if an existing utility already solves the problem (e.g. app/utils/edn.ts for EDN parsing).
  • Pin major versions. Review changelogs before upgrading.

#Security

  • Validate all data at system boundaries: user input, proxy responses, environment variables.
  • Never commit secrets, tokens, or credentials. Use environment variables via .env (which is gitignored).
  • Sanitise any data rendered in the UI to prevent XSS.

#Review Checklist for AI-Generated Code

Before merging any AI-generated code, verify:

  • You understand every change and can explain the reasoning.
  • No new any types introduced.
  • No unnecessary files, abstractions, or utilities created.
  • Follows existing naming conventions and project patterns.
  • Type checker passes (npm run typecheck).
  • Tests pass (npm test) and new behaviour is covered.
  • No secrets or credentials included.
  • No unnecessary comments or documentation added.
  • Changes are scoped to what was requested, nothing more.