Architecture principles

A constitution, not a task list.

Tepna is a browser-native, 100%-local physiological analysis instrument — a fleet of single-signal analyzers over a shared kernel. These are the rules that keep it scalable: adopt them forward, migrate old code opportunistically, never in one risky sweep.

Read order: CLAUDE.md wins on any conflict — especially the Clock Contract and the two gates.
00 / FRAME THE DECISIONS

Three advantages every decision must protect.

A change that improves "code cleanliness" but weakens any of these is the wrong change. This is why the answer to "the system is getting serious — move to a backend, microservices, Python?" is no: that trades away advantages #1 and #2 to solve a problem we don't have. The real scaling limit is drift, and the fix for drift is tighter contracts inside the architecture we already have — not more infrastructure.

ADVANTAGE 01

Instant reproducibility

A bundled Foo.html is a frozen, hash-verifiable instrument. Open it in 2026 or 2036, online or on a plane — it computes the same numbers.

ADVANTAGE 02

Portability

One file. No install, no server, no network. Runs straight from file://.

ADVANTAGE 03

Auditability

Provenance stamps + the test suite mean every number traces to a reproducible build and a known contract.

01 / THE THREE LAYERS

One job each.

The single most important rule. Every file belongs to exactly one layer, and each layer has exactly one responsibility. The boundaries — not the folder names — are what matter.

CORE

Shared, stable, frozen-ish truth

The constitution every node obeys. Nothing device-specific ever lives here — no "oxygen" knowledge, no "glucose" knowledge. CORE is stable by default; new capability is added as new surface (new field, new method), never by mutating what exists.

metric-registry.jsDexKernel · kernel-constantscrossnight-envelope.jsClock ContractGanglior export schema
TEST · "Identical regardless of which signal the node measures?" → CORE
DSP

One node's signal logic, and nothing else

Pure domain math: signal transforms, feature extraction, metric computation. Allowed to be complex — but only about its one signal. Runs headless in Node; the test suite essentially does.

Rule: no UI, no rendering, no DOM, no state, no localStorage. Takes parsed input, returns metrics. parseTimestamp is duplicated into each DSP by design (Clock Contract) — the one sanctioned duplication.
oxydex-dsp.jshrvdex-dsp.jsecgdex-dsp.jscpapdex-dsp.js
TEST · "Computes a number from a signal?" → DSP  ·  "Decides how it looks?" → not DSP
UI

Rendering and input only

Visualization, file input, calling DSP, wiring controls. A render file must never recompute a metric, re-threshold a value, or re-derive physiology — it asks DSP for the number and the registry for how to label and badge it.

*-render.js*-app.jsFoo.src.html
TEST · "Delete this line — does a NUMBER change, or only its APPEARANCE?" Appearance → UI · Number → misplaced

Why this matters concretely. Without the boundary, a desaturation rule could live in OxyDex's render code and get subtly re-implemented in CPAPDex's — two truths. With it, that rule can only live in DSP (or as a shared CORE definition), so there's exactly one place it can drift from — and the test suite watches that one place.

02 / DEPENDENCY DIRECTION

Acyclic, downhill.

Dependencies point downhill only: UI → DSP → CORE. CORE never imports a node. DSP never imports UI. No cycles, ever. And nodes never import each other — a -Dex is standalone by design (a user may own only one device). Cross-node interaction happens exclusively through the Ganglior export contract, consumed by the Integrator.

CORE — depends on nothingkernel · metric-registry · envelope · clock contract · ganglior export schema
uses       uses
DSP — per nodepure signal math
Integrator fusionconsumes node exports, never internals
calls
UI — per nodedepends on its DSP + CORE · never on another node
Nodes never import each other. The Integrator depends on the export schema (a CORE-level contract), not on any node's DSP.
03 / CONTRACTS ARE THE PRODUCT

Durability comes from enforced contracts, not tidy code.

The system's real product is three contracts. The test suite is the mechanism that makes them real — the shared assertions in tests/dex-tests.js are the public contract. When behavior and assertion disagree, the assertion is right until a human deliberately changes it.

Contract 01

The metric contract

Every metric has one definition — id, label, unit, goodDirection, depth, evidence, cite — in its node's registry. Render and export read it; never redefine it.

Contract 02

The export contract

ganglior.node-export: schema.name, recording.startEpochMs = floating t0Ms, ganglior_events[] with wall-clock t + absolute tMs. Consumers tolerate legacy t-only exports.

Contract 03

The CORE API contract

Kernel scoring, envelope definitions, registry methods. These do not silently evolve — they are versioned, deliberate surface.

The golden rule of changing a contract

Keep back-compat. Add new params last and optional; expose new data via a new field or method. Do not edit a shared assertion to match changed behavior — either preserve the old shape, or update tests/dex-tests.js deliberately, knowing Node CI uses the same file.

04 / HONESTY IS ARCHITECTURAL

Never present a guess as a measurement.

Tepna's credibility rests on this, and it is encoded, not just cultural. The evidence ladder is attached to every metric — a raw sensor reading and a population projection must never look alike. Trust is encoded in disc shape, never hue.

measured

Read straight off the device.

validated

Checked against a standard.

emerging

Published, device-dependent.

experimental

Plausible, not confirmed.

heuristic

Rule-of-thumb direction.

null ≠ now()

A missing value is null, never fabricated. A missing timestamp is null, never now() — absence must stay visible.

conf ≠ quality

An event's conf (severity/likelihood) and sqi (signal quality) are separate axes. Fusion attenuates via effConf = conf × (sqi ?? 1) — never collapse them.

capability before consensus

When sources disagree, you don't count devices. Filter to sensors that can observe the signal, check internal plausibility, then look for the obligate physiological consequence.

05 / PER-SIGNAL AUTHORITY

There is no single "best device."

Authority is assigned per signal, and a gold-standard source yields to a clean backup when its own quality gate trips. Chest ECG owns HRV — but is useless when its battery dies; the wrist PPG is the fallback for heart rate, but never for HRV magnitude under motion. New nodes declare what they are authoritative for, not that they are authoritative.

06 / STAY LOCAL & REPRODUCIBLE

Don't break the thing that makes us special.

100% local. No network, no CDN, ever. Fonts are system stacks; assets are inlined at bundle time, not fetched. Edit the inputs (*-dsp.js / *-render.js / *.src.html) and re-bundle to Foo.html — never edit the bundle. The bundle's manifestHash — a deterministic projection of its inlined JS/CSS — is the instrument's executed-code fingerprint, and the sole code identity the gates check. (buildHash is retired as a provenance signal — stamped into exports as inert legacy metadata, but no gate reads it.)

GATE · behavior

Dex-Test-Suite.html

All-green after any DSP / app / bundle change. The guardian of physiological truth — the assertions are the contract.

GATE · provenance

verify-provenance.html

No red mismatch after any re-bundle. A JS/CSS-only change doesn't shift buildHash; a .src.html change does — both fine as long as no fixture flips unexpectedly.

07 / THE LAYER-SEPARATION PASS

Forward-first, one node at a time.

The one larger refactor implied by the three layers — done safely. Not during a rollout or a node build (moving code while other work edits it guarantees conflicts). Adopt forward first: new nodes are born layer-clean. Migrate old nodes opportunistically, one per pass, gating after each — never six at once.

Success test for a migrated node: you can run its DSP headless in Node with no DOM, and its render file contains no number that isn't sourced from DSP or the registry.

TL;DR FOR A CODER IN A HURRY

Seven rules.

  1. Three layers, one job each. CORE (shared truth, no device logic) · DSP (one signal's math, no UI) · UI (rendering, no signal logic).
  2. Dependencies point downhill (UI→DSP→CORE); nodes never import each other — only via the export contract.
  3. Contracts are the product. Back-compat always; the test suite is the guardian; never edit an assertion to hide a break.
  4. Be honest. Evidence ladder on every metric; null not fabricated; conf ≠ quality; capability before consensus.
  5. Per-signal authority, quality-gated. No global "best device."
  6. Stay local & reproducible. Edit inputs, re-bundle, pass both gates.
  7. The layer-separation refactor is forward-first, one node at a time, never mid-rollout.
T © 2026 Michal Planicka ·Tepna v1.0.0 ·Apache-2.0 ·◈ Asheville, NC ·not a medical device