Replace DB-backed instrument upserts with app.yaml-driven config loading, matching, and translator application in the ingestion workflow. Also add serial-port connector support, startup validation tooling, and migration tracking updates to keep runtime behavior and docs aligned.
15 KiB
15 KiB
AGENTS
Mission & Audience
- This document lives at the root so every agentic helper knows how to make, run, and reason about the middleware.
- Refer back to
docs/workstation_plan.mdfor the architectural story, expected flows, and the canonical payload contract before touching new features. - Preserve the operational stability that the SQLite queue + delivery worker already provides; avoid accidental schema drift or config leaks.
- Tailor every change to the Node 20+ CommonJS ecosystem and the SQLite-backed persistence layer this repo already embraces.
Command Reference
Install & Bootstrapping
npm installpopulatesnode_modules(no lockfile generation beyond the committedpackage-lock.json).npm startis the go-to run command; it migrates the database, primes the instrument cache, spins up connectors, and starts the delivery worker plus health/metrics services.npm run migraterunsmiddleware/src/storage/migrate.json demand; use it before seeding schema migrations in new environments or CI jobs.
Maintenance & Database Care
npm run maintenance -- backupcopiesmiddleware/data/workstation.sqlitetoworkstation.sqlite.bak-<timestamp>; this file should stay in place and not be committed or removed.npm run maintenance -- vacuumruns SQLite'sVACUUMviamiddleware/src/scripts/maintenance.jsand logs success/failure to stdout/stderr.npm run maintenance -- prune --days=<n>deletesdelivery_logentries older than<n>days; default is 30 if--daysis omitted.
Testing & Single-Test Command
npm testexecutesnode middleware/test/parsers.test.jsand serves as the allowable smoke check until a richer test harness exists.- To rerun the single parser suite manually, target
node middleware/test/parsers.test.jsdirectly; it logs success viaconsole.logand exits non-zero on failure.
Environment & Secrets
- Node 20+ is assumed because the code uses optional chaining,
String.raw, and other modern primitives; keep the same runtime for development and CI. - All ports, DB paths, and CLQMS credentials are sourced from
middleware/config/app.yaml(loaded bymiddleware/config/default.js) as the single runtime config file. - Treat
CLQMS_TOKEN, database files, and other secrets as environment-provided values; never embed them in checked-in files. middleware/data/workstation.sqliteis the runtime database. Don’t delete or reinitialize it from the repository tree unless part of an explicit migration/backup operation.
Observability Endpoints
/healthreturns connector statuses plus pending/retrying/dead-letter counts frommiddleware/src/routes/health.js./health/readypings the SQLite queue; any failure there should log an error and respond with503per the existing route logic./metricsexposes Prometheus-style gauges/counters that read straight fromqueue/sqliteQueue; keep the plaintext format exactly as defined so Prometheus scrapers don't break.- Health and metrics routers are mounted on
middleware/src/index.jsat ports declared in the config, so any addition should remain consistent with Express middleware ordering.
Delivery Runbook & Retry Behavior
- Backoff:
30s -> 2m -> 10m -> 30m -> 2h -> 6h, max 10 attempts as defined inconfig.retries.schedule. The worker tapsbuildNextAttemptindeliveryWorker.jsto honor this array. - Retry transient failures (timeouts, DNS/connection, HTTP 5xx); skip HTTP 400/422 or validation errors and ship those payloads immediately to
dead_letterwith the response body. - After max attempts move the canonical payload to
dead_letterwith the final error message so postmortem tooling can surface the failure. queue.recordDeliveryAttemptaccompanies every outbound delivery, so keep latency, status, and response code logging aligned with this helper.- Duplicate detection relies on
utils/hash.dedupeKey; keepresultssorted and hashed consistently so deduplication stays stable. deliveryWorkermarkslocked_at/locked_byusingqueue.claimPendingand always releases them viaqueue.markOutboxStatusto avoid worker starvation.
Instrument Configuration Cache
- Instrument configuration is cached in
instrumentConfig/service.js; reloads happen on init and viasetInterval, so mutate the cache throughservice.upsertrather than touchingstoredirectly. service.reloadparses JSON in theconfigcolumn, logs parsing failures withlogger.warn, and only keeps rows that successfully parse.- Service helpers expose
list,get, andbyConnectorso connectors can fetch the subset they care about without iterating raw rows. - Store interactions use
middleware/src/storage/instrumentConfigStore.js, which leveragesDatabaseClientand parameterizedON CONFLICTupserts; follow that pattern when extending tables. instrumentService.initmust run before connectors start soprocessMessagecan enforce instrument-enabled checks and connector matching.- Always drop payloads with no enabled config or connector mismatch and mark the raw row as
droppedso operators can trace why a message was ignored.
Metrics & Logging Enhancements
metrics.jsbuilds human-readable Prometheus strings viaformatMetric; keep the helper intact when adding new metrics so type/help annotations stay formatted correctly.- Metrics route reports pending, retrying, dead letters, delivery attempts, last success timestamp, and average latency; add new stats only when there is a clear operational need.
- Use
queuehelpers (pendingCount,retryingCount,deadLetterCount,getLastSuccessTimestamp,getAverageLatency,getDeliveryAttempts) rather than running fresh queries in routes. - Always set the response content type to
text/plain; version=0.0.4; charset=utf-8before returning metrics so Prometheus scrapers accept the payload. - Health logs should cite both connectors and queue metrics so failure contexts are actionable and correlate with the operational dashboards referenced in
docs/workstation_plan.md. - Mask sensitive fields and avoid dumping raw payloads in logs; connectors and parsers add context objects to errors rather than full payload dumps.
Maintenance Checklist
middleware/src/scripts/maintenance.jssupports the commandsbackup,vacuum, andprune --days=<n>(default 30); call these from CI or ops scripts when the backlog grows.backupcopies the SQLite file before running migrations or schema updates so you can roll back quickly.vacuumrecalculates and rebuilds the DB; wrap it in maintenance windows because it briefly locks the database.prunedeletes old rows fromdelivery_log; use the same threshold asdocs/workstation_plan.md(default 30 days) unless stakeholders approve a different retention.maintenancelogging usesconsole.log/console.errorbecause the script runs outside the Express app; keep those calls simple and exit with non-zero codes on failure to alert CI.- Document every manual maintenance action in the repository README or a runbook so second-tier operators know what happened.
Data & Schema Source of Truth
- All schema statements live in
middleware/db/migrations/00*_*.sql; the bootstrapper iterates over these files alphabetically viafs.readdirSyncanddb.exec, so keep new migrations in that folder and add them with increasing numeric prefixes. - Table definitions include:
inbox_raw,outbox_result,delivery_log,instrument_config, anddead_letter. An additional migration addslocked_atandlocked_bytooutbox_result. middleware/src/storage/migrate.jsis idempotent; it applies every.sqlin the migrations folder unconditionally. Avoid writing irreversible SQL (DROP, ALTER without fallback) unless you also add compensating migrations.DatabaseClientinmiddleware/src/storage/db.jswraps sqlite3 callbacks in promises; reuse itsrun,get, andallhelpers to keep SQL parameterization consistent and to centralizebusyTimeoutconfiguration.
Code Style Guidelines
Modules, Imports, and Exports
- Prefer CommonJS
const ... = require(...)at the top of each module; grouping localrequires by directory depth (config, utils, domain) keeps files predictable. - Export objects/functions via
module.exports = { ... }ormodule.exports = <function>depending on whether multiple helpers are exported. - When a file exposes a factory (connectors, queue), return named methods (
start,stop,onMessage,health) to keep the bootstrapper happy.
Formatting & Layout
- Use two spaces for indentation and include semicolons at the end of statements; this matches existing files such as
middleware/src/utils/logger.jsandindex.js. - Keep line length reasonable (~100 characters) and break wrapped strings with template literals (see metric formatters) rather than concatenating with
+. - Prefer single quotes for strings unless interpolation or escaping makes backticks clearer.
- Keep helper functions (splitters, builders) at the top of parser modules, followed by the main exported parse function.
Naming Conventions
- Stick to camelCase for functions, methods, and variables (
processMessage,buildNextAttempt,messageHandler). - Use descriptive object properties that mirror domain terms (
instrument_id,result_time,connector,status). - Constants for configuration or retry schedules stay uppercase/lowercase as seen in
config.retries.schedule; keep them grouped insideconfig/default.js.
Async Flow & Error Handling
- Embrace
async/awaiteverywhere; existing code rarely uses raw promises (except for wrappers likenew Promise((resolve) => ...)). - Wrap I/O boundaries in
try/catchblocks and log failures with structured data vialogger.error({ err: err.message }, '...')so Pino hooks can parse them. - When rethrowing an error, ensure the calling context knows whether the failure is fatal (e.g.,
processMessagerethrows after queue logging). - For connectors, propagate errors through
onErrorhooks so the bootstrapper can log them consistently.
Logging & Diagnostics
- Always prefer
middleware/src/utils/logger.jsinstead ofconsole.log/console.errorinside core services; the exception is low-level scripts likemaintenance.jsand migration runners. - Use structured objects for context (
{ err: err.message, connector: connector.name() }), especially around delivery failures and config reloads. - Log positive states (start listening, health server ready) along with port numbers so the runtime state can be traced during deployment.
Validation & Canonical Payloads
- Use
zodfor inbound schema checks; validators already live inmiddleware/src/routes/instrumentConfig.jsandmiddleware/src/normalizers/index.js. - Always normalize parser output via
normalize(parsed)before queue insertion to guaranteeinstrument_id,sample_id,result_time, andresultsconform to expectations. - If
normalizethrows, let the caller log the failure and drop the payload silently after markinginbox_rawasfailedto avoid partial writes.
Database & Queue Best Practices
- Use
DatabaseClientfor all SQL interactions; it centralizesbusyTimeoutand promise conversion and prevents sqlite3 callback spaghetti. - Parameterize every statement with
?placeholders (seequeue/sqliteQueue.jsandinstrumentConfigStore.js) to avoid SQL injection hazards. - Always mark
inbox_rawrows asprocessed,failed, ordroppedafter parsing to keep operators aware of what happened. - When marking
outbox_resultstatuses, clearlocked_at/locked_byand updateattempts/next_attempt_atin one statement so watchers can rely on atomic semantics.
Connectors & Pipeline Contracts
- Each connector must provide
name,type,start,stop,health,onMessage, andonErrorper the current implementation; keep this contract if you add new protocols. - Keep connector internals event-driven: emit
messageHandler(payload)and handle.catch(errorHandler)to ensure downstream failures get logged. - For TCP connectors, track connections in
Sets sostop()can destroy them before closing the server. - Do not assume payload framing beyond what the current parser needs; let the parser module handle splitting text and trimming.
Worker & Delivery Guidelines
- The delivery worker polls the queue (
config.worker.batchSize) and records every attempt viaqueue.recordDeliveryAttempt; add retries in the same pattern if you introduce new failure-handling logic. - Respect the retry schedule defined in
config.retries.schedule;buildNextAttemptusesMath.minto cap indexes, so new delays should append toconfig.retries.scheduleonly. - Duplicate detection relies on
utils/hash.dedupeKey; keepresultssorted and hashed consistently so deduplication stays stable. - On HTTP 400/422 responses or too many retries, move payloads to
dead_letterand log the reason to keep operators informed.
Testing & Coverage Expectations
- Parser tests live in
middleware/test/parsers.test.js; they rely onnode:assertand deliberately simple sample payloads to avoid external dependencies. - Add new tests by mimicking that file’s style—plain
assert.strictEqualchecks, no test framework dependencies, andconsole.logsuccess acknowledgment. - If you enhance the test surface, keep it runnable via
npm testso agents and CI scripts can still rely on a single command line.
Documentation & Storytelling
- Keep
docs/workstation_plan.mdin sync with architectural changes; it surfaces connector flows, phases, retry policies, and maintenance checklists that agents rely on. - When adding routes/features, document the endpoint, request payload, and expected responses in either
docs/or inline comments near the route.
Cursor & Copilot Rules
- No
.cursor/rules/or.cursorrulesdirectories are present in this repo; therefore there are no Cursor-specific constraints to copy here. .github/copilot-instructions.mdis absent as well, so there are no Copilot instructions to enforce or repeat.
Final Notes for Agents
- Keep changes isolated to their area of responsibility; the middleware is intentionally minimal, so avoid introducing new bundlers/languages.
- Before opening PRs, rerun
npm run migrateandnpm testto verify schema/app coherence. - Use environment variable overrides from
middleware/config/default.jswhen running in staging/production so the same config file can stay committed.
Additional Notes
- Never revert existing changes you did not make unless explicitly requested, since those changes were made by the user.
- If there are unrelated changes in the working tree, leave them untouched and focus on the files that matter for the ticket.
- Avoid destructive git commands (
git reset --hard,git checkout --) unless the user explicitly requests them. - If documentation updates were part of your change, add them to
docs/workstation_plan.mdor explain why the doc already covers the behavior. - When a connector or parser handles a new instrument, double-check
instrument_configrows to ensure the connector name matches the incoming protocol. - The
queuekeepsstatus,attempts,next_attempt_at, andlocked_*in sync; always update all relevant columns in a single SQL call to avoid race conditions. - Keep the SQL schema in sync with
middleware/db/migrations; add new migrations rather than editing existing ones when altering tables.