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, notdoStufforprocess. - 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, useunknownand 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.tsxnext 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.okbefore callingres.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 = () => {}), notfunctiondeclarations. - Import ordering: Node built-ins, then third-party packages, then local imports (
~/...). - Remix conventions:
loader,action,meta,linksexports 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: SVGfill/strokeattributes 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")anddb.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 thandb.exec()for queries with parameters. - Reserve
db.exec()for DDL (schema creation) only. - Cast database rows to typed interfaces rather than using
any. Preferas SomeTypeafter.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, anduv run pytest -x. - Follow kbpy's own
CLAUDE.mdfor package-specific conventions. - SQLite output from kbpy uses the
sqlite3stdlib module (notbetter-sqlite3). Enable WAL mode and foreign keys. Use parameterised queries for inserts.
Proxy Integration
- Read the proxy URL from
process.env.PROXY_URLwith a fallback:const PROXY_URL = process.env.PROXY_URL ?? "http://localhost:3001". - Always POST to
${PROXY_URL}/querywithContent-Type: application/jsonand body{ data: QUERY, ns: false }. - Validate the response before parsing: check
res.ok, then readres.json(), then parse the EDN string inresult.resultusingparseEDNWithOptions. - 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 returnjson(). No directbetter-sqlite3imports, 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
_actionform field when a single route handles multiple operations. Switch on_actionto dispatch. - Return
json({ error: message }, { status })on failure. Returnjson({ success: true, ...data })orredirect()on success. - Cast
formData.get()results tostringexplicitly. Trim string inputs before storing.
Script Conventions
- Scripts use ES modules with the
.mjsextension. - Use
import.meta.urlwithfileURLToPathfor path resolution (no__dirnamein ESM). - Top-level
awaitis permitted in scripts. - Scripts log progress to stdout with
console.log. Errors throw withthrow 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 fromapp/.
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.tsfor 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
anytypes 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.
WRIT Docs