Typed health classification for systems that measure things.
TypeScript port of the Python margin library. Zero dependencies.
import { Parser, createThresholds, expressionToString } from 'margin-ts';
const parser = new Parser(
{ throughput: 500, errorRate: 0.002 },
createThresholds(400, 150),
{ errorRate: createThresholds(0.01, 0.10, false) },
);
const expr = parser.parse({ throughput: 480, errorRate: 0.03 });
console.log(expressionToString(expr));
// [throughput:INTACT(-0.04σ)] [errorRate:DEGRADED(-14.00σ)]Throughput and error rate on the same scale. One is higher-is-better, the other is lower-is-better. Both classified correctly. Sigma-normalised so you can compare them.
npm install margin-tsA number comes in. Margin gives it:
- Health — INTACT / DEGRADED / ABLATED / RECOVERING / OOD
- Polarity — higher-is-better or lower-is-better, handled correctly everywhere
- Sigma — dimensionless deviation from baseline, always positive = healthier
- Confidence — CERTAIN / HIGH / MODERATE / LOW / INDETERMINATE
- Drift — trajectory: STABLE / DRIFTING / ACCELERATING / DECELERATING / REVERTING / OSCILLATING
- Anomaly — outlier: EXPECTED / UNUSUAL / ANOMALOUS / NOVEL
- Absence — typed nulls: NOT_MEASURED / BELOW_DETECTION / SENSOR_FAILED / REDACTED / ...
- Flow — ordered stages with typed live guidance (ADVANCE / HOLD / RETRY / ROLLBACK / ESCALATE)
- Issue — typed diagnostics the library surfaces about its own operation
import { classifyDrift, DriftState } from 'margin-ts';
const dc = classifyDrift(observations);
dc.state; // DriftState.DRIFTING
dc.direction; // DriftDirection.WORSENING
dc.rate; // slope per second (polarity-normalised)import { classifyAnomaly, AnomalyState } from 'margin-ts';
const ac = classifyAnomaly(currentValue, referenceValues, { component: 'cpu' });
ac.state; // AnomalyState.NOVEL
ac.zScore; // how many σ from historical mean
ac.isNovel; // true if outside historical rangeFor processes where each stage has distinct entry/quality/exit criteria — CI/CD pipelines, fermentation, clinical trials, incident response:
import { Flow, Stage, Guidance, Parser, createThresholds, flowResultToAtom } from 'margin-ts';
const flow = new Flow([
{
name: 'lag',
parser: new Parser({ biomass: 0.1 }, createThresholds(0.08, 0.02)),
advanceWhen: (expr) => expr.observations[0].value >= 0.15,
},
{ name: 'exponential', parser: /* ... */, advanceWhen: /* ... */ },
{ name: 'stationary', parser: /* ... */ },
], 'ferment');
const result = flow.observe({ biomass: 0.12 });
flowResultToAtom(result);
// 'flow:ferment:lag[1/3] → HOLD'
// When advanceWhen fires:
if (result.guidance === Guidance.ADVANCE) flow.advance();Predicate exceptions are caught as ERROR issues, guidance degrades to HOLD, the flow stays alive — safety-first semantics for production contexts.
The library surfaces its own operational state the same way it classifies observed state:
import { IssueLevel, IssueCode } from 'margin-ts';
monitor.update({ mystery: 1.0 }); // emits WARNING: BASELINE_MISSING
monitor.drift('unknown'); // emits WARNING: UNKNOWN_COMPONENT
// Query programmatically
monitor.issues.byCode(IssueCode.BASELINE_MISSING);
monitor.issues.atLeast(IssueLevel.WARNING);
monitor.diagnose(); // JSON-safe dashboard summary
// Bridge to any logger (pino, winston, console, webhook — your choice)
monitor.issues.attachListener((issue) => {
logger.warn(issue.detail, { code: issue.code, component: issue.component });
});Harvest-safe on hard fail: if StreamingMonitor.update() or Flow.observe() throws, a FATAL(INTERNAL_ERROR) issue is recorded before the exception propagates. monitor.issues survives crashes intact.
The TypeScript port mirrors the Python library's core types:
| Python | TypeScript |
|---|---|
Health.INTACT |
Health.INTACT |
Thresholds(intact=80, ablated=30) |
createThresholds(80, 30) |
classify(value, confidence, thresholds) |
classify(value, confidence, thresholds) |
Parser(baselines, thresholds) |
new Parser(baselines, thresholds) |
expr.to_string() |
expressionToString(expr) |
classify_drift(observations) |
classifyDrift(observations) |
classify_anomaly(value, ref) |
classifyAnomaly(value, ref) |
Flow(stages, label) |
new Flow(stages, label) |
result.to_atom() |
flowResultToAtom(result) |
buffer.attach_logger(logger) |
buffer.attachListener(cb) |
MIT — Copyright (c) 2026 Cope Labs LLC