margin-ts 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ /**
3
+ * margin — Typed health classification for systems that measure things.
4
+ *
5
+ * TypeScript port of the Python margin library.
6
+ * Zero dependencies. Pure TypeScript.
7
+ *
8
+ * Copyright (c) 2026 Cope Labs LLC. MIT License.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.classifyAnomaly = exports.ANOMALY_SEVERITY = exports.AnomalyState = exports.classifyDrift = exports.DriftDirection = exports.DriftState = exports.Parser = exports.expressionToDict = exports.expressionToString = exports.degraded = exports.healthOf = exports.createExpression = exports.correctionIsActive = exports.observationFromDict = exports.observationToDict = exports.observationToAtom = exports.observationSigma = exports.Op = exports.classify = exports.isAblated = exports.isIntact = exports.createThresholds = exports.SEVERITY = exports.Health = exports.minConfidence = exports.confidenceLt = exports.confidenceGte = exports.confidenceRank = exports.Confidence = void 0;
12
+ var confidence_js_1 = require("./confidence.js");
13
+ Object.defineProperty(exports, "Confidence", { enumerable: true, get: function () { return confidence_js_1.Confidence; } });
14
+ Object.defineProperty(exports, "confidenceRank", { enumerable: true, get: function () { return confidence_js_1.confidenceRank; } });
15
+ Object.defineProperty(exports, "confidenceGte", { enumerable: true, get: function () { return confidence_js_1.confidenceGte; } });
16
+ Object.defineProperty(exports, "confidenceLt", { enumerable: true, get: function () { return confidence_js_1.confidenceLt; } });
17
+ Object.defineProperty(exports, "minConfidence", { enumerable: true, get: function () { return confidence_js_1.minConfidence; } });
18
+ var health_js_1 = require("./health.js");
19
+ Object.defineProperty(exports, "Health", { enumerable: true, get: function () { return health_js_1.Health; } });
20
+ Object.defineProperty(exports, "SEVERITY", { enumerable: true, get: function () { return health_js_1.SEVERITY; } });
21
+ Object.defineProperty(exports, "createThresholds", { enumerable: true, get: function () { return health_js_1.createThresholds; } });
22
+ Object.defineProperty(exports, "isIntact", { enumerable: true, get: function () { return health_js_1.isIntact; } });
23
+ Object.defineProperty(exports, "isAblated", { enumerable: true, get: function () { return health_js_1.isAblated; } });
24
+ Object.defineProperty(exports, "classify", { enumerable: true, get: function () { return health_js_1.classify; } });
25
+ var observation_js_1 = require("./observation.js");
26
+ Object.defineProperty(exports, "Op", { enumerable: true, get: function () { return observation_js_1.Op; } });
27
+ Object.defineProperty(exports, "observationSigma", { enumerable: true, get: function () { return observation_js_1.observationSigma; } });
28
+ Object.defineProperty(exports, "observationToAtom", { enumerable: true, get: function () { return observation_js_1.observationToAtom; } });
29
+ Object.defineProperty(exports, "observationToDict", { enumerable: true, get: function () { return observation_js_1.observationToDict; } });
30
+ Object.defineProperty(exports, "observationFromDict", { enumerable: true, get: function () { return observation_js_1.observationFromDict; } });
31
+ Object.defineProperty(exports, "correctionIsActive", { enumerable: true, get: function () { return observation_js_1.correctionIsActive; } });
32
+ Object.defineProperty(exports, "createExpression", { enumerable: true, get: function () { return observation_js_1.createExpression; } });
33
+ Object.defineProperty(exports, "healthOf", { enumerable: true, get: function () { return observation_js_1.healthOf; } });
34
+ Object.defineProperty(exports, "degraded", { enumerable: true, get: function () { return observation_js_1.degraded; } });
35
+ Object.defineProperty(exports, "expressionToString", { enumerable: true, get: function () { return observation_js_1.expressionToString; } });
36
+ Object.defineProperty(exports, "expressionToDict", { enumerable: true, get: function () { return observation_js_1.expressionToDict; } });
37
+ Object.defineProperty(exports, "Parser", { enumerable: true, get: function () { return observation_js_1.Parser; } });
38
+ var drift_js_1 = require("./drift.js");
39
+ Object.defineProperty(exports, "DriftState", { enumerable: true, get: function () { return drift_js_1.DriftState; } });
40
+ Object.defineProperty(exports, "DriftDirection", { enumerable: true, get: function () { return drift_js_1.DriftDirection; } });
41
+ Object.defineProperty(exports, "classifyDrift", { enumerable: true, get: function () { return drift_js_1.classifyDrift; } });
42
+ var anomaly_js_1 = require("./anomaly.js");
43
+ Object.defineProperty(exports, "AnomalyState", { enumerable: true, get: function () { return anomaly_js_1.AnomalyState; } });
44
+ Object.defineProperty(exports, "ANOMALY_SEVERITY", { enumerable: true, get: function () { return anomaly_js_1.ANOMALY_SEVERITY; } });
45
+ Object.defineProperty(exports, "classifyAnomaly", { enumerable: true, get: function () { return anomaly_js_1.classifyAnomaly; } });
46
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;;AAEH,iDAAyG;AAAhG,2GAAA,UAAU,OAAA;AAAE,+GAAA,cAAc,OAAA;AAAE,8GAAA,aAAa,OAAA;AAAE,6GAAA,YAAY,OAAA;AAAE,8GAAA,aAAa,OAAA;AAE/E,yCAGqB;AAFnB,mGAAA,MAAM,OAAA;AAAE,qGAAA,QAAQ,OAAA;AAAc,6GAAA,gBAAgB,OAAA;AAC9C,qGAAA,QAAQ,OAAA;AAAE,sGAAA,SAAS,OAAA;AAAE,qGAAA,QAAQ,OAAA;AAG/B,mDAM0B;AALxB,oGAAA,EAAE,OAAA;AACF,kHAAA,gBAAgB,OAAA;AAAE,mHAAA,iBAAiB,OAAA;AAAE,mHAAA,iBAAiB,OAAA;AAAE,qHAAA,mBAAmB,OAAA;AAC3E,oHAAA,kBAAkB,OAAA;AAClB,kHAAA,gBAAgB,OAAA;AAAE,0GAAA,QAAQ,OAAA;AAAE,0GAAA,QAAQ,OAAA;AAAE,oHAAA,kBAAkB,OAAA;AAAE,kHAAA,gBAAgB,OAAA;AAC1E,wGAAA,MAAM,OAAA;AAGR,uCAGoB;AAFlB,sGAAA,UAAU,OAAA;AAAE,0GAAA,cAAc,OAAA;AAC1B,yGAAA,aAAa,OAAA;AAGf,2CAGsB;AAFpB,0GAAA,YAAY,OAAA;AAAE,8GAAA,gBAAgB,OAAA;AAC9B,6GAAA,eAAe,OAAA"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Observations, corrections, expressions, and parser.
3
+ */
4
+ import { Confidence } from './confidence.js';
5
+ import { Health, Thresholds } from './health.js';
6
+ export interface Observation {
7
+ name: string;
8
+ health: Health;
9
+ value: number;
10
+ baseline: number;
11
+ confidence: Confidence;
12
+ higherIsBetter: boolean;
13
+ provenance: string[];
14
+ measuredAt?: Date;
15
+ }
16
+ export declare function observationSigma(obs: Observation): number;
17
+ export declare function observationToAtom(obs: Observation): string;
18
+ export declare function observationToDict(obs: Observation): Record<string, unknown>;
19
+ export declare function observationFromDict(d: Record<string, unknown>): Observation;
20
+ export declare enum Op {
21
+ RESTORE = "RESTORE",
22
+ SUPPRESS = "SUPPRESS",
23
+ AMPLIFY = "AMPLIFY",
24
+ NOOP = "NOOP"
25
+ }
26
+ export interface Correction {
27
+ target: string;
28
+ op: Op;
29
+ alpha: number;
30
+ magnitude: number;
31
+ triggeredBy: string[];
32
+ provenance: string[];
33
+ }
34
+ export declare function correctionIsActive(c: Correction): boolean;
35
+ export interface Expression {
36
+ observations: Observation[];
37
+ corrections: Correction[];
38
+ confidence: Confidence;
39
+ label: string;
40
+ step?: number;
41
+ }
42
+ export declare function createExpression(observations?: Observation[], corrections?: Correction[], label?: string, step?: number): Expression;
43
+ export declare function healthOf(expr: Expression, name: string): Health | undefined;
44
+ export declare function degraded(expr: Expression): Observation[];
45
+ export declare function expressionToString(expr: Expression): string;
46
+ export declare function expressionToDict(expr: Expression): Record<string, unknown>;
47
+ export declare class Parser {
48
+ baselines: Record<string, number>;
49
+ thresholds: Thresholds;
50
+ componentThresholds: Record<string, Thresholds>;
51
+ constructor(baselines: Record<string, number>, thresholds: Thresholds, componentThresholds?: Record<string, Thresholds>);
52
+ thresholdsFor(name: string): Thresholds;
53
+ parse(values: Record<string, number>, options?: {
54
+ label?: string;
55
+ step?: number;
56
+ confidences?: Record<string, Confidence>;
57
+ }): Expression;
58
+ }
59
+ //# sourceMappingURL=observation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observation.d.ts","sourceRoot":"","sources":["../src/observation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+ /**
3
+ * Observations, corrections, expressions, and parser.
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.Parser = exports.Op = void 0;
7
+ exports.observationSigma = observationSigma;
8
+ exports.observationToAtom = observationToAtom;
9
+ exports.observationToDict = observationToDict;
10
+ exports.observationFromDict = observationFromDict;
11
+ exports.correctionIsActive = correctionIsActive;
12
+ exports.createExpression = createExpression;
13
+ exports.healthOf = healthOf;
14
+ exports.degraded = degraded;
15
+ exports.expressionToString = expressionToString;
16
+ exports.expressionToDict = expressionToDict;
17
+ const confidence_js_1 = require("./confidence.js");
18
+ const health_js_1 = require("./health.js");
19
+ function observationSigma(obs) {
20
+ if (obs.baseline === 0)
21
+ return 0;
22
+ const raw = (obs.value - obs.baseline) / Math.abs(obs.baseline);
23
+ return obs.higherIsBetter ? raw : -raw;
24
+ }
25
+ function observationToAtom(obs) {
26
+ if (obs.health === health_js_1.Health.OOD)
27
+ return `${obs.name}:${obs.health}`;
28
+ const sigma = observationSigma(obs);
29
+ const sign = sigma >= 0 ? '+' : '';
30
+ return `${obs.name}:${obs.health}(${sign}${sigma.toFixed(2)}σ)`;
31
+ }
32
+ function observationToDict(obs) {
33
+ const d = {
34
+ name: obs.name,
35
+ health: obs.health,
36
+ value: obs.value,
37
+ baseline: obs.baseline,
38
+ sigma: observationSigma(obs),
39
+ confidence: obs.confidence,
40
+ higherIsBetter: obs.higherIsBetter,
41
+ provenance: obs.provenance,
42
+ };
43
+ if (obs.measuredAt)
44
+ d.measuredAt = obs.measuredAt.toISOString();
45
+ return d;
46
+ }
47
+ function observationFromDict(d) {
48
+ return {
49
+ name: d.name,
50
+ health: d.health,
51
+ value: d.value,
52
+ baseline: d.baseline,
53
+ confidence: d.confidence,
54
+ higherIsBetter: d.higherIsBetter ?? true,
55
+ provenance: d.provenance ?? [],
56
+ measuredAt: d.measuredAt ? new Date(d.measuredAt) : undefined,
57
+ };
58
+ }
59
+ // -----------------------------------------------------------------------
60
+ // Correction
61
+ // -----------------------------------------------------------------------
62
+ var Op;
63
+ (function (Op) {
64
+ Op["RESTORE"] = "RESTORE";
65
+ Op["SUPPRESS"] = "SUPPRESS";
66
+ Op["AMPLIFY"] = "AMPLIFY";
67
+ Op["NOOP"] = "NOOP";
68
+ })(Op || (exports.Op = Op = {}));
69
+ function correctionIsActive(c) {
70
+ return c.op !== Op.NOOP && c.alpha > 0;
71
+ }
72
+ function createExpression(observations = [], corrections = [], label = '', step) {
73
+ const confidence = observations.length > 0
74
+ ? (0, confidence_js_1.minConfidence)(...observations.map(o => o.confidence))
75
+ : confidence_js_1.Confidence.INDETERMINATE;
76
+ return { observations, corrections, confidence, label, step };
77
+ }
78
+ function healthOf(expr, name) {
79
+ return expr.observations.find(o => o.name === name)?.health;
80
+ }
81
+ function degraded(expr) {
82
+ return expr.observations.filter(o => o.health === health_js_1.Health.DEGRADED || o.health === health_js_1.Health.ABLATED || o.health === health_js_1.Health.RECOVERING);
83
+ }
84
+ function expressionToString(expr) {
85
+ if (expr.observations.length === 0)
86
+ return '[∅]';
87
+ const parts = expr.observations.map(o => {
88
+ const atom = observationToAtom(o);
89
+ const corr = expr.corrections.find(c => c.target === o.name);
90
+ if (corr && correctionIsActive(corr)) {
91
+ return `[${atom} → ${corr.op}(α=${corr.alpha.toFixed(2)})]`;
92
+ }
93
+ return `[${atom}]`;
94
+ });
95
+ return parts.join(' ');
96
+ }
97
+ function expressionToDict(expr) {
98
+ return {
99
+ confidence: expr.confidence,
100
+ label: expr.label,
101
+ step: expr.step,
102
+ observations: expr.observations.map(observationToDict),
103
+ corrections: expr.corrections.map(c => ({
104
+ target: c.target, op: c.op, alpha: c.alpha,
105
+ magnitude: c.magnitude, triggeredBy: c.triggeredBy,
106
+ })),
107
+ };
108
+ }
109
+ // -----------------------------------------------------------------------
110
+ // Parser
111
+ // -----------------------------------------------------------------------
112
+ class Parser {
113
+ baselines;
114
+ thresholds;
115
+ componentThresholds;
116
+ constructor(baselines, thresholds, componentThresholds = {}) {
117
+ this.baselines = baselines;
118
+ this.thresholds = thresholds;
119
+ this.componentThresholds = componentThresholds;
120
+ }
121
+ thresholdsFor(name) {
122
+ return this.componentThresholds[name] ?? this.thresholds;
123
+ }
124
+ parse(values, options = {}) {
125
+ const confidences = options.confidences ?? {};
126
+ const observations = [];
127
+ for (const [name, val] of Object.entries(values)) {
128
+ const baseline = this.baselines[name] ?? val;
129
+ const conf = confidences[name] ?? confidence_js_1.Confidence.MODERATE;
130
+ const t = this.thresholdsFor(name);
131
+ const h = (0, health_js_1.classify)(val, conf, t);
132
+ observations.push({
133
+ name,
134
+ health: h,
135
+ value: val,
136
+ baseline,
137
+ confidence: conf,
138
+ higherIsBetter: t.higherIsBetter !== false,
139
+ provenance: [],
140
+ measuredAt: undefined,
141
+ });
142
+ }
143
+ return createExpression(observations, [], options.label ?? '', options.step);
144
+ }
145
+ }
146
+ exports.Parser = Parser;
147
+ //# sourceMappingURL=observation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observation.js","sourceRoot":"","sources":["../src/observation.ts"],"names":[],"mappings":";AAAA;;GAEG;;;AAoBH,4CAIC;AAED,8CAKC;AAED,8CAaC;AAED,kDAWC;AAsBD,gDAEC;AAcD,4CAUC;AAED,4BAEC;AAED,4BAIC;AAED,gDAWC;AAED,4CAWC;AA7ID,mDAA4D;AAC5D,2CAAiG;AAiBjG,SAAgB,gBAAgB,CAAC,GAAgB;IAC/C,IAAI,GAAG,CAAC,QAAQ,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IACjC,MAAM,GAAG,GAAG,CAAC,GAAG,CAAC,KAAK,GAAG,GAAG,CAAC,QAAQ,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IAChE,OAAO,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;AACzC,CAAC;AAED,SAAgB,iBAAiB,CAAC,GAAgB;IAChD,IAAI,GAAG,CAAC,MAAM,KAAK,kBAAM,CAAC,GAAG;QAAE,OAAO,GAAG,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,MAAM,EAAE,CAAC;IAClE,MAAM,KAAK,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAC;IACpC,MAAM,IAAI,GAAG,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;IACnC,OAAO,GAAG,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,MAAM,IAAI,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;AAClE,CAAC;AAED,SAAgB,iBAAiB,CAAC,GAAgB;IAChD,MAAM,CAAC,GAA4B;QACjC,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,KAAK,EAAE,gBAAgB,CAAC,GAAG,CAAC;QAC5B,UAAU,EAAE,GAAG,CAAC,UAAU;QAC1B,cAAc,EAAE,GAAG,CAAC,cAAc;QAClC,UAAU,EAAE,GAAG,CAAC,UAAU;KAC3B,CAAC;IACF,IAAI,GAAG,CAAC,UAAU;QAAE,CAAC,CAAC,UAAU,GAAG,GAAG,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC;IAChE,OAAO,CAAC,CAAC;AACX,CAAC;AAED,SAAgB,mBAAmB,CAAC,CAA0B;IAC5D,OAAO;QACL,IAAI,EAAE,CAAC,CAAC,IAAc;QACtB,MAAM,EAAE,CAAC,CAAC,MAAgB;QAC1B,KAAK,EAAE,CAAC,CAAC,KAAe;QACxB,QAAQ,EAAE,CAAC,CAAC,QAAkB;QAC9B,UAAU,EAAE,CAAC,CAAC,UAAwB;QACtC,cAAc,EAAG,CAAC,CAAC,cAA0B,IAAI,IAAI;QACrD,UAAU,EAAG,CAAC,CAAC,UAAuB,IAAI,EAAE;QAC5C,UAAU,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,UAAoB,CAAC,CAAC,CAAC,CAAC,SAAS;KACxE,CAAC;AACJ,CAAC;AAED,0EAA0E;AAC1E,aAAa;AACb,0EAA0E;AAE1E,IAAY,EAKX;AALD,WAAY,EAAE;IACZ,yBAAmB,CAAA;IACnB,2BAAqB,CAAA;IACrB,yBAAmB,CAAA;IACnB,mBAAa,CAAA;AACf,CAAC,EALW,EAAE,kBAAF,EAAE,QAKb;AAWD,SAAgB,kBAAkB,CAAC,CAAa;IAC9C,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,IAAI,IAAI,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC;AACzC,CAAC;AAcD,SAAgB,gBAAgB,CAC9B,eAA8B,EAAE,EAChC,cAA4B,EAAE,EAC9B,KAAK,GAAG,EAAE,EACV,IAAa;IAEb,MAAM,UAAU,GAAG,YAAY,CAAC,MAAM,GAAG,CAAC;QACxC,CAAC,CAAC,IAAA,6BAAa,EAAC,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;QACvD,CAAC,CAAC,0BAAU,CAAC,aAAa,CAAC;IAC7B,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AAChE,CAAC;AAED,SAAgB,QAAQ,CAAC,IAAgB,EAAE,IAAY;IACrD,OAAO,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,EAAE,MAAM,CAAC;AAC9D,CAAC;AAED,SAAgB,QAAQ,CAAC,IAAgB;IACvC,OAAO,IAAI,CAAC,YAAY,CAAC,MAAM,CAC7B,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,kBAAM,CAAC,QAAQ,IAAI,CAAC,CAAC,MAAM,KAAK,kBAAM,CAAC,OAAO,IAAI,CAAC,CAAC,MAAM,KAAK,kBAAM,CAAC,UAAU,CACnG,CAAC;AACJ,CAAC;AAED,SAAgB,kBAAkB,CAAC,IAAgB;IACjD,IAAI,IAAI,CAAC,YAAY,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACjD,MAAM,KAAK,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE;QACtC,MAAM,IAAI,GAAG,iBAAiB,CAAC,CAAC,CAAC,CAAC;QAClC,MAAM,IAAI,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC;QAC7D,IAAI,IAAI,IAAI,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC;YACrC,OAAO,IAAI,IAAI,MAAM,IAAI,CAAC,EAAE,MAAM,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;QAC9D,CAAC;QACD,OAAO,IAAI,IAAI,GAAG,CAAC;IACrB,CAAC,CAAC,CAAC;IACH,OAAO,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACzB,CAAC;AAED,SAAgB,gBAAgB,CAAC,IAAgB;IAC/C,OAAO;QACL,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,YAAY,EAAE,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,iBAAiB,CAAC;QACtD,WAAW,EAAE,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;YACtC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK;YAC1C,SAAS,EAAE,CAAC,CAAC,SAAS,EAAE,WAAW,EAAE,CAAC,CAAC,WAAW;SACnD,CAAC,CAAC;KACJ,CAAC;AACJ,CAAC;AAED,0EAA0E;AAC1E,SAAS;AACT,0EAA0E;AAE1E,MAAa,MAAM;IACjB,SAAS,CAAyB;IAClC,UAAU,CAAa;IACvB,mBAAmB,CAA6B;IAEhD,YACE,SAAiC,EACjC,UAAsB,EACtB,sBAAkD,EAAE;QAEpD,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,mBAAmB,GAAG,mBAAmB,CAAC;IACjD,CAAC;IAED,aAAa,CAAC,IAAY;QACxB,OAAO,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC;IAC3D,CAAC;IAED,KAAK,CACH,MAA8B,EAC9B,UAII,EAAE;QAEN,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,EAAE,CAAC;QAC9C,MAAM,YAAY,GAAkB,EAAE,CAAC;QAEvC,KAAK,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;YACjD,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC;YAC7C,MAAM,IAAI,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,0BAAU,CAAC,QAAQ,CAAC;YACtD,MAAM,CAAC,GAAG,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;YACnC,MAAM,CAAC,GAAG,IAAA,oBAAQ,EAAC,GAAG,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;YACjC,YAAY,CAAC,IAAI,CAAC;gBAChB,IAAI;gBACJ,MAAM,EAAE,CAAC;gBACT,KAAK,EAAE,GAAG;gBACV,QAAQ;gBACR,UAAU,EAAE,IAAI;gBAChB,cAAc,EAAE,CAAC,CAAC,cAAc,KAAK,KAAK;gBAC1C,UAAU,EAAE,EAAE;gBACd,UAAU,EAAE,SAAS;aACtB,CAAC,CAAC;QACL,CAAC;QAED,OAAO,gBAAgB,CAAC,YAAY,EAAE,EAAE,EAAE,OAAO,CAAC,KAAK,IAAI,EAAE,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/E,CAAC;CACF;AAjDD,wBAiDC"}
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "margin-ts",
3
+ "version": "0.6.0",
4
+ "description": "Typed health classification for systems that measure things. Zero dependencies.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "test": "vitest run",
10
+ "test:watch": "vitest"
11
+ },
12
+ "keywords": [
13
+ "health", "monitoring", "typed", "classification",
14
+ "threshold", "polarity", "drift", "anomaly",
15
+ "observability", "alerting"
16
+ ],
17
+ "author": "Cope Labs LLC",
18
+ "license": "MIT",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/sethc5/margin-ts"
22
+ },
23
+ "files": ["dist", "src"],
24
+ "devDependencies": {
25
+ "@types/node": "^25.5.0",
26
+ "typescript": "^6.0.2",
27
+ "vitest": "^4.1.2"
28
+ }
29
+ }
package/src/anomaly.ts ADDED
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Anomaly detection: typed states for statistical outliers.
3
+ *
4
+ * Health says "is it good?", Drift says "is it changing?",
5
+ * Anomaly says "is it normal?"
6
+ */
7
+
8
+ import { Confidence } from './confidence.js';
9
+
10
+ export enum AnomalyState {
11
+ EXPECTED = 'EXPECTED',
12
+ UNUSUAL = 'UNUSUAL',
13
+ ANOMALOUS = 'ANOMALOUS',
14
+ NOVEL = 'NOVEL',
15
+ }
16
+
17
+ export const ANOMALY_SEVERITY: Record<AnomalyState, number> = {
18
+ [AnomalyState.EXPECTED]: 0,
19
+ [AnomalyState.UNUSUAL]: 1,
20
+ [AnomalyState.ANOMALOUS]: 2,
21
+ [AnomalyState.NOVEL]: 3,
22
+ };
23
+
24
+ export interface AnomalyClassification {
25
+ component: string;
26
+ state: AnomalyState;
27
+ zScore: number;
28
+ historicalMean: number;
29
+ historicalStd: number;
30
+ historicalMin: number;
31
+ historicalMax: number;
32
+ isNovel: boolean;
33
+ confidence: Confidence;
34
+ nReference: number;
35
+ }
36
+
37
+ function mean(xs: number[]): number {
38
+ return xs.reduce((a, b) => a + b, 0) / xs.length;
39
+ }
40
+
41
+ function std(xs: number[], m: number): number {
42
+ if (xs.length < 2) return 0;
43
+ const variance = xs.reduce((a, x) => a + (x - m) ** 2, 0) / (xs.length - 1);
44
+ return Math.sqrt(variance);
45
+ }
46
+
47
+ function confidenceFromN(n: number): Confidence {
48
+ if (n >= 30) return Confidence.HIGH;
49
+ if (n >= 10) return Confidence.MODERATE;
50
+ if (n >= 3) return Confidence.LOW;
51
+ return Confidence.INDETERMINATE;
52
+ }
53
+
54
+ export function classifyAnomaly(
55
+ value: number,
56
+ reference: number[],
57
+ options: {
58
+ component?: string;
59
+ unusualThreshold?: number;
60
+ anomalousThreshold?: number;
61
+ novelMargin?: number;
62
+ minReference?: number;
63
+ } = {},
64
+ ): AnomalyClassification | null {
65
+ const component = options.component ?? '';
66
+ const unusualThreshold = options.unusualThreshold ?? 2.0;
67
+ const anomalousThreshold = options.anomalousThreshold ?? 3.0;
68
+ const novelMargin = options.novelMargin ?? 0.1;
69
+ const minReference = options.minReference ?? 3;
70
+
71
+ if (reference.length < minReference) return null;
72
+
73
+ const refMean = mean(reference);
74
+ const refStd = std(reference, refMean);
75
+ const refMin = Math.min(...reference);
76
+ const refMax = Math.max(...reference);
77
+ const n = reference.length;
78
+
79
+ // Z-score
80
+ let z: number;
81
+ if (refStd > 0) {
82
+ z = (value - refMean) / refStd;
83
+ } else {
84
+ z = value === refMean ? 0 : (value > refMean ? Infinity : -Infinity);
85
+ }
86
+
87
+ // Novelty check
88
+ const refRange = refMax - refMin;
89
+ const margin = refRange > 0 ? Math.max(refRange * novelMargin, refStd) : refStd * 2;
90
+ const isNovel = value < refMin - margin || value > refMax + margin;
91
+
92
+ // Classification
93
+ let state: AnomalyState;
94
+ if (isNovel) state = AnomalyState.NOVEL;
95
+ else if (Math.abs(z) >= anomalousThreshold) state = AnomalyState.ANOMALOUS;
96
+ else if (Math.abs(z) >= unusualThreshold) state = AnomalyState.UNUSUAL;
97
+ else state = AnomalyState.EXPECTED;
98
+
99
+ return {
100
+ component,
101
+ state,
102
+ zScore: z,
103
+ historicalMean: refMean,
104
+ historicalStd: refStd,
105
+ historicalMin: refMin,
106
+ historicalMax: refMax,
107
+ isNovel,
108
+ confidence: confidenceFromN(n),
109
+ nReference: n,
110
+ };
111
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Confidence tiers for uncertain comparisons.
3
+ * Replaces binary booleans with graded confidence levels.
4
+ */
5
+
6
+ export enum Confidence {
7
+ CERTAIN = 'certain',
8
+ HIGH = 'high',
9
+ MODERATE = 'moderate',
10
+ LOW = 'low',
11
+ INDETERMINATE = 'indeterminate',
12
+ }
13
+
14
+ const RANK: Record<Confidence, number> = {
15
+ [Confidence.CERTAIN]: 4,
16
+ [Confidence.HIGH]: 3,
17
+ [Confidence.MODERATE]: 2,
18
+ [Confidence.LOW]: 1,
19
+ [Confidence.INDETERMINATE]: 0,
20
+ };
21
+
22
+ export function confidenceRank(c: Confidence): number {
23
+ return RANK[c];
24
+ }
25
+
26
+ export function confidenceGte(a: Confidence, b: Confidence): boolean {
27
+ return RANK[a] >= RANK[b];
28
+ }
29
+
30
+ export function confidenceLt(a: Confidence, b: Confidence): boolean {
31
+ return RANK[a] < RANK[b];
32
+ }
33
+
34
+ export function minConfidence(...confs: Confidence[]): Confidence {
35
+ if (confs.length === 0) return Confidence.INDETERMINATE;
36
+ return confs.reduce((a, b) => (RANK[a] <= RANK[b] ? a : b));
37
+ }
package/src/drift.ts ADDED
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Drift classification: typed states for value trajectories.
3
+ *
4
+ * Health tells you WHERE a value is. Drift tells you WHERE IT'S HEADED.
5
+ */
6
+
7
+ import { Confidence } from './confidence.js';
8
+ import { Observation } from './observation.js';
9
+
10
+ export enum DriftState {
11
+ STABLE = 'STABLE',
12
+ DRIFTING = 'DRIFTING',
13
+ ACCELERATING = 'ACCELERATING',
14
+ DECELERATING = 'DECELERATING',
15
+ REVERTING = 'REVERTING',
16
+ OSCILLATING = 'OSCILLATING',
17
+ }
18
+
19
+ export enum DriftDirection {
20
+ IMPROVING = 'IMPROVING',
21
+ WORSENING = 'WORSENING',
22
+ NEUTRAL = 'NEUTRAL',
23
+ }
24
+
25
+ export interface DriftClassification {
26
+ component: string;
27
+ state: DriftState;
28
+ direction: DriftDirection;
29
+ rate: number;
30
+ acceleration: number;
31
+ rSquared: number;
32
+ confidence: Confidence;
33
+ nSamples: number;
34
+ windowSeconds: number;
35
+ }
36
+
37
+ // -----------------------------------------------------------------------
38
+ // Linear regression
39
+ // -----------------------------------------------------------------------
40
+
41
+ function linreg(xs: number[], ys: number[]): { slope: number; intercept: number; rSq: number; se: number } {
42
+ const n = xs.length;
43
+ const mx = xs.reduce((a, b) => a + b, 0) / n;
44
+ const my = ys.reduce((a, b) => a + b, 0) / n;
45
+ let ssxx = 0, ssyy = 0, ssxy = 0;
46
+ for (let i = 0; i < n; i++) {
47
+ ssxx += (xs[i] - mx) ** 2;
48
+ ssyy += (ys[i] - my) ** 2;
49
+ ssxy += (xs[i] - mx) * (ys[i] - my);
50
+ }
51
+ if (ssxx === 0) return { slope: 0, intercept: my, rSq: 0, se: 0 };
52
+ const slope = ssxy / ssxx;
53
+ const intercept = my - slope * mx;
54
+ let ssRes = 0;
55
+ for (let i = 0; i < n; i++) ssRes += (ys[i] - (intercept + slope * xs[i])) ** 2;
56
+ const rSq = ssyy > 0 ? Math.max(1 - ssRes / ssyy, 0) : 0;
57
+ const se = ssxx > 0 ? Math.sqrt(ssRes / Math.max(n - 2, 1) / ssxx) : 0;
58
+ return { slope, intercept, rSq, se };
59
+ }
60
+
61
+ // -----------------------------------------------------------------------
62
+ // Oscillation detection
63
+ // -----------------------------------------------------------------------
64
+
65
+ function isOscillating(ys: number[], residuals: number[]): boolean {
66
+ if (ys.length < 5) return false;
67
+ const maxRes = Math.max(...residuals.map(Math.abs));
68
+ const tol = 0.01 * maxRes;
69
+ let crossings = 0;
70
+ for (let i = 1; i < residuals.length; i++) {
71
+ if (residuals[i - 1] * residuals[i] < 0 &&
72
+ Math.abs(residuals[i - 1]) > tol && Math.abs(residuals[i]) > tol) {
73
+ crossings++;
74
+ }
75
+ }
76
+ const amplitude = Math.max(...ys) - Math.min(...ys);
77
+ const meanAbs = ys.reduce((a, b) => a + Math.abs(b), 0) / ys.length;
78
+ const relAmp = amplitude / Math.max(meanAbs, 1e-10);
79
+ return crossings >= 2 && relAmp > 0.02;
80
+ }
81
+
82
+ // -----------------------------------------------------------------------
83
+ // Main classification
84
+ // -----------------------------------------------------------------------
85
+
86
+ export function classifyDrift(
87
+ observations: Observation[],
88
+ options: { minSamples?: number; noiseThreshold?: number } = {},
89
+ ): DriftClassification | null {
90
+ const minSamples = options.minSamples ?? 3;
91
+ const noiseThreshold = options.noiseThreshold ?? 1.5;
92
+
93
+ const timed = observations
94
+ .filter(o => o.measuredAt != null)
95
+ .sort((a, b) => a.measuredAt!.getTime() - b.measuredAt!.getTime());
96
+
97
+ if (timed.length < minSamples) return null;
98
+
99
+ const name = timed[0].name;
100
+ const higherIsBetter = timed[0].higherIsBetter;
101
+ const baseline = timed[0].baseline;
102
+ const t0 = timed[0].measuredAt!.getTime();
103
+ const xs = timed.map(o => (o.measuredAt!.getTime() - t0) / 1000);
104
+ const ys = timed.map(o => o.value);
105
+ const n = xs.length;
106
+ const window = xs[n - 1] - xs[0];
107
+
108
+ if (window === 0) return null;
109
+
110
+ const { slope, intercept, rSq, se } = linreg(xs, ys);
111
+ const residuals = ys.map((y, i) => y - (intercept + slope * xs[i]));
112
+ const normSlope = higherIsBetter ? slope : -slope;
113
+
114
+ // Slope significance
115
+ const slopeSignificant = se === 0 ? Math.abs(slope) > 0 : Math.abs(slope) > noiseThreshold * se;
116
+
117
+ let state: DriftState;
118
+ let direction: DriftDirection;
119
+
120
+ if (!slopeSignificant) {
121
+ if (isOscillating(ys, residuals)) {
122
+ state = DriftState.OSCILLATING;
123
+ direction = DriftDirection.NEUTRAL;
124
+ } else {
125
+ state = DriftState.STABLE;
126
+ direction = DriftDirection.NEUTRAL;
127
+ }
128
+ } else {
129
+ direction = normSlope > 0 ? DriftDirection.IMPROVING : DriftDirection.WORSENING;
130
+
131
+ const firstVal = ys[0];
132
+ const currentVal = ys[n - 1];
133
+ const wasUnhealthy = higherIsBetter ? firstVal < baseline : firstVal > baseline;
134
+ const nowCloser = Math.abs(currentVal - baseline) < Math.abs(firstVal - baseline);
135
+
136
+ if (wasUnhealthy && nowCloser) {
137
+ state = DriftState.REVERTING;
138
+ } else {
139
+ state = DriftState.DRIFTING;
140
+ }
141
+ }
142
+
143
+ // Confidence
144
+ let confidence: Confidence;
145
+ if (n >= 10 && rSq > 0.8) confidence = Confidence.HIGH;
146
+ else if (n >= 5 && rSq > 0.5) confidence = Confidence.MODERATE;
147
+ else confidence = Confidence.LOW;
148
+
149
+ return {
150
+ component: name,
151
+ state,
152
+ direction,
153
+ rate: normSlope,
154
+ acceleration: 0,
155
+ rSquared: rSq,
156
+ confidence,
157
+ nSamples: n,
158
+ windowSeconds: window,
159
+ };
160
+ }
package/src/health.ts ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Health classification: typed states for any monitored component.
3
+ *
4
+ * Maps a scalar measurement against thresholds into a typed health predicate.
5
+ * Supports both polarities: higher_is_better and lower_is_better.
6
+ */
7
+
8
+ import { Confidence } from './confidence.js';
9
+
10
+ export enum Health {
11
+ INTACT = 'INTACT',
12
+ DEGRADED = 'DEGRADED',
13
+ ABLATED = 'ABLATED',
14
+ RECOVERING = 'RECOVERING',
15
+ OOD = 'OOD',
16
+ }
17
+
18
+ export const SEVERITY: Record<Health, number> = {
19
+ [Health.INTACT]: 0,
20
+ [Health.RECOVERING]: 1,
21
+ [Health.DEGRADED]: 2,
22
+ [Health.ABLATED]: 3,
23
+ [Health.OOD]: 4,
24
+ };
25
+
26
+ export interface Thresholds {
27
+ intact: number;
28
+ ablated: number;
29
+ higherIsBetter?: boolean;
30
+ activeMin?: number;
31
+ }
32
+
33
+ export function createThresholds(
34
+ intact: number,
35
+ ablated: number,
36
+ higherIsBetter = true,
37
+ activeMin = 0.05,
38
+ ): Thresholds {
39
+ if (higherIsBetter && ablated > intact) {
40
+ throw new Error(`higherIsBetter but ablated (${ablated}) > intact (${intact})`);
41
+ }
42
+ if (!higherIsBetter && ablated < intact) {
43
+ throw new Error(`lowerIsBetter but ablated (${ablated}) < intact (${intact})`);
44
+ }
45
+ return { intact, ablated, higherIsBetter, activeMin };
46
+ }
47
+
48
+ export function isIntact(value: number, t: Thresholds): boolean {
49
+ return t.higherIsBetter !== false ? value >= t.intact : value <= t.intact;
50
+ }
51
+
52
+ export function isAblated(value: number, t: Thresholds): boolean {
53
+ return t.higherIsBetter !== false ? value < t.ablated : value > t.ablated;
54
+ }
55
+
56
+ export function classify(
57
+ value: number,
58
+ confidence: Confidence,
59
+ thresholds: Thresholds,
60
+ correcting = false,
61
+ ): Health {
62
+ if (confidence === Confidence.INDETERMINATE) return Health.OOD;
63
+ if (isIntact(value, thresholds)) return Health.INTACT;
64
+ if (isAblated(value, thresholds)) return correcting ? Health.RECOVERING : Health.ABLATED;
65
+ return correcting ? Health.RECOVERING : Health.DEGRADED;
66
+ }