@loadmap/core 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +15 -2
- package/dist/calibrate-CRZgyOq5.d.ts +52 -0
- package/dist/calibrate.d.ts +2 -0
- package/dist/calibrate.js +41 -0
- package/dist/capacity-0zHaaRMk.js +30 -0
- package/dist/format.d.ts +28 -0
- package/dist/format.js +85 -0
- package/dist/index.d.ts +535 -0
- package/dist/index.js +887 -0
- package/package.json +40 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jaime Rojas
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @loadmap/core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The model behind [loadmap](https://jaim3r0jas.github.io/loadmap/), in plain TypeScript with no dependencies: params,
|
|
4
|
+
flows, components whose capacities say where they came from, limits as ranges, the verdict, scenarios, growth,
|
|
5
|
+
`compare()`, `headroom()` and `check()`. It runs in Node, so a test can hold a model to its headroom:
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { assertHolds } from '@loadmap/core'
|
|
9
|
+
import { shop } from './shop'
|
|
10
|
+
|
|
11
|
+
it('keeps its headroom', () => assertHolds(shop)) // throws, listing every failed expectation
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`@loadmap/core/calibrate` fits `cost = a + b × rate` from measurements (`fitLinear`); `@loadmap/core/format` has the
|
|
15
|
+
number formats the verdict uses. Most pages install [`@loadmap/react`](https://www.npmjs.com/package/@loadmap/react),
|
|
16
|
+
which re-exports all of this. MIT.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
type Provenance = 'measured' | 'estimated';
|
|
2
|
+
/** A capacity always says where it came from: the verdict and the assumptions list show it. */
|
|
3
|
+
interface Capacity {
|
|
4
|
+
value: number;
|
|
5
|
+
unit: string;
|
|
6
|
+
provenance: Provenance;
|
|
7
|
+
source?: string;
|
|
8
|
+
/** How sure the value is, as a share: 0.3 is ±30 %. The limits become ranges between the two bounds. */
|
|
9
|
+
within?: number;
|
|
10
|
+
}
|
|
11
|
+
interface CapacityOptions {
|
|
12
|
+
source?: string;
|
|
13
|
+
/** ±, as a share of the value: 0.3 is ±30 %. At least 0, below 1. */
|
|
14
|
+
within?: number;
|
|
15
|
+
}
|
|
16
|
+
/** A capacity read off a dashboard or a load test. */
|
|
17
|
+
declare function measured(value: number, unit: string, o?: string | CapacityOptions): Capacity;
|
|
18
|
+
/** A capacity from a vendor's figure, a benchmark elsewhere or a guess: the verdict says so. */
|
|
19
|
+
declare function estimated(value: number, unit: string, o?: string | CapacityOptions): Capacity;
|
|
20
|
+
/** The capacity's value at its low or high bound; the value itself without a ±. */
|
|
21
|
+
declare function boundOf(c: Capacity, bound: Bound | undefined): number;
|
|
22
|
+
/** Every capacity at its low bound (the pessimistic edge) or its high bound. */
|
|
23
|
+
type Bound = 'low' | 'high';
|
|
24
|
+
/** Where something is reached with every capacity at its low bound (`low`) and at its high bound (`high`). */
|
|
25
|
+
interface CrossingRange {
|
|
26
|
+
low: number;
|
|
27
|
+
high: number;
|
|
28
|
+
}
|
|
29
|
+
/** cost = a + b × x, fitted from a measured series. Callable: `fit(x)` is the cost at x. */
|
|
30
|
+
interface LinearFit {
|
|
31
|
+
(x: number): number;
|
|
32
|
+
a: number;
|
|
33
|
+
b: number;
|
|
34
|
+
/** Coefficient of determination of the fit, 0 to 1. */
|
|
35
|
+
r2: number;
|
|
36
|
+
n: number;
|
|
37
|
+
x: string;
|
|
38
|
+
y: string;
|
|
39
|
+
source?: string;
|
|
40
|
+
provenance: 'measured';
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Ordinary least squares over `points[x]` → `points[y]`. Needs at least two distinct x values.
|
|
44
|
+
* `through: 'origin'` fixes a at 0 (a cost with no idle part).
|
|
45
|
+
*/
|
|
46
|
+
declare function fitLinear<T extends Record<string, unknown>>(points: readonly T[], o: {
|
|
47
|
+
x: keyof T & string;
|
|
48
|
+
y: keyof T & string;
|
|
49
|
+
source?: string;
|
|
50
|
+
through?: 'origin';
|
|
51
|
+
}): LinearFit;
|
|
52
|
+
export { CapacityOptions as a, boundOf as c, Capacity as i, estimated as l, fitLinear as n, CrossingRange as o, Bound as r, Provenance as s, LinearFit as t, measured as u };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { n as estimated, r as measured } from "./capacity-0zHaaRMk.js";
|
|
2
|
+
// src/calibrate.ts
|
|
3
|
+
/**
|
|
4
|
+
* Ordinary least squares over `points[x]` → `points[y]`. Needs at least two distinct x values.
|
|
5
|
+
* `through: 'origin'` fixes a at 0 (a cost with no idle part).
|
|
6
|
+
*/
|
|
7
|
+
function fitLinear(points, o) {
|
|
8
|
+
const xs = points.map((pt) => Number(pt[o.x]));
|
|
9
|
+
const ys = points.map((pt) => Number(pt[o.y]));
|
|
10
|
+
const n = xs.length;
|
|
11
|
+
if (xs.some((v) => !Number.isFinite(v)) || ys.some((v) => !Number.isFinite(v))) throw new Error(`loadmap: fitLinear(${o.x} → ${o.y}) got a non-numeric point`);
|
|
12
|
+
if (n < 2 || new Set(xs).size < 2) throw new Error(`loadmap: fitLinear(${o.x} → ${o.y}) needs two distinct x values`);
|
|
13
|
+
let a;
|
|
14
|
+
let b;
|
|
15
|
+
if (o.through === "origin") {
|
|
16
|
+
a = 0;
|
|
17
|
+
b = sum(xs.map((x, i) => x * ys[i])) / sum(xs.map((x) => x * x));
|
|
18
|
+
} else {
|
|
19
|
+
const mx = sum(xs) / n;
|
|
20
|
+
const my = sum(ys) / n;
|
|
21
|
+
b = sum(xs.map((x, i) => (x - mx) * (ys[i] - my))) / sum(xs.map((x) => (x - mx) ** 2));
|
|
22
|
+
a = my - b * mx;
|
|
23
|
+
}
|
|
24
|
+
const my = sum(ys) / n;
|
|
25
|
+
const ssTot = sum(ys.map((y) => (y - my) ** 2));
|
|
26
|
+
const ssRes = sum(xs.map((x, i) => (ys[i] - (a + b * x)) ** 2));
|
|
27
|
+
const r2 = ssTot === 0 ? 1 : Math.max(0, 1 - ssRes / ssTot);
|
|
28
|
+
const fit = ((x) => a + b * x);
|
|
29
|
+
return Object.assign(fit, {
|
|
30
|
+
a,
|
|
31
|
+
b,
|
|
32
|
+
r2,
|
|
33
|
+
n,
|
|
34
|
+
x: o.x,
|
|
35
|
+
y: o.y,
|
|
36
|
+
source: o.source,
|
|
37
|
+
provenance: "measured"
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
const sum = (v) => v.reduce((s, x) => s + x, 0);
|
|
41
|
+
export { estimated, fitLinear, measured };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// src/capacity.ts
|
|
2
|
+
/** A capacity read off a dashboard or a load test. */
|
|
3
|
+
function measured(value, unit, o) {
|
|
4
|
+
return capacity(value, unit, "measured", o);
|
|
5
|
+
}
|
|
6
|
+
/** A capacity from a vendor's figure, a benchmark elsewhere or a guess: the verdict says so. */
|
|
7
|
+
function estimated(value, unit, o) {
|
|
8
|
+
return capacity(value, unit, "estimated", o);
|
|
9
|
+
}
|
|
10
|
+
function capacity(value, unit, provenance, o) {
|
|
11
|
+
const { source, within } = typeof o === "string" ? {
|
|
12
|
+
source: o,
|
|
13
|
+
within: void 0
|
|
14
|
+
} : o ?? {};
|
|
15
|
+
if (within != null && !(within >= 0 && within < 1)) throw new Error(`loadmap: within is a share from 0 to below 1 (0.3 is ±30 %), not ${within}`);
|
|
16
|
+
const c = {
|
|
17
|
+
value,
|
|
18
|
+
unit,
|
|
19
|
+
provenance,
|
|
20
|
+
source
|
|
21
|
+
};
|
|
22
|
+
if (within) c.within = within;
|
|
23
|
+
return c;
|
|
24
|
+
}
|
|
25
|
+
/** The capacity's value at its low or high bound; the value itself without a ±. */
|
|
26
|
+
function boundOf(c, bound) {
|
|
27
|
+
if (!bound || !c.within) return c.value;
|
|
28
|
+
return c.value * (bound === "low" ? 1 - c.within : 1 + c.within);
|
|
29
|
+
}
|
|
30
|
+
export { estimated as n, measured as r, boundOf as t };
|
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** A number for a reader: 1,234 / 12.3 / 0.4. */
|
|
2
|
+
export declare function fmt(n: number): string;
|
|
3
|
+
/** A compact number: 980 / 1.2k / 34k / 1.5M. */
|
|
4
|
+
export declare function compact(n: number): string;
|
|
5
|
+
/** A load multiple, rounded down so it never promises more than the model: 2.6× / 53×. */
|
|
6
|
+
export declare function multiple(u: number | null, searchMax?: number): string;
|
|
7
|
+
/**
|
|
8
|
+
* Two load multiples: "7.4×" when both edges read the same, else "between 7.4× and 10×" or, short, "7.4×–10×". An
|
|
9
|
+
* edge that never comes reads "7.4× or more" ("≥ 7.4×").
|
|
10
|
+
*/
|
|
11
|
+
export declare function multiples(r: {
|
|
12
|
+
low: number;
|
|
13
|
+
high: number;
|
|
14
|
+
}, searchMax?: number, short?: boolean): string;
|
|
15
|
+
/** When two edges are reached: "(around March 2029)", "(between March 2029 and May 2030)" or "(… or later)". */
|
|
16
|
+
export declare function months(low: Date | null, high: Date | null): string;
|
|
17
|
+
/** A percentage of a 0–1 share: 50 %. */
|
|
18
|
+
export declare function pct(share: number): string;
|
|
19
|
+
/** A duration in seconds for a meter: 0 s / 42 s / 3 min / > 1 h / stalled. */
|
|
20
|
+
export declare function secs(s: number): string;
|
|
21
|
+
/** The 1-2-5 step at or above x: 1, 2, 5, 10, 20, 50, … */
|
|
22
|
+
export declare function niceStep(x: number): number;
|
|
23
|
+
/** "a", "a and b", "a, b and c". */
|
|
24
|
+
export declare function joinAnd(items: readonly string[]): string;
|
|
25
|
+
/** A month in UTC, "March 2028" or short "Mar 2028". Fixed names, whatever the locale. */
|
|
26
|
+
export declare function monthOf(d: Date, short?: boolean): string;
|
|
27
|
+
/** A date as a reader plans with it, by month: "around March 2028". */
|
|
28
|
+
export declare function around(d: Date): string;
|
package/dist/format.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// src/format.ts
|
|
2
|
+
/** A number for a reader: 1,234 / 12.3 / 0.4. */
|
|
3
|
+
function fmt(n) {
|
|
4
|
+
if (!Number.isFinite(n)) return n > 0 ? "∞" : "—";
|
|
5
|
+
if (Math.abs(n) >= 1e3) return Math.round(n).toLocaleString("en-US");
|
|
6
|
+
return (Math.round(n * 10) / 10).toString();
|
|
7
|
+
}
|
|
8
|
+
/** A compact number: 980 / 1.2k / 34k / 1.5M. */
|
|
9
|
+
function compact(n) {
|
|
10
|
+
if (!Number.isFinite(n)) return n > 0 ? "∞" : "—";
|
|
11
|
+
const a = Math.abs(n);
|
|
12
|
+
if (a >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
13
|
+
if (a >= 1e4) return `${Math.round(n / 1e3)}k`;
|
|
14
|
+
if (a >= 1e3) return `${(n / 1e3).toFixed(1)}k`;
|
|
15
|
+
return Math.round(n).toString();
|
|
16
|
+
}
|
|
17
|
+
/** A load multiple, rounded down so it never promises more than the model: 2.6× / 53×. */
|
|
18
|
+
function multiple(u, searchMax = 2e3) {
|
|
19
|
+
if (u == null) return "—";
|
|
20
|
+
if (!Number.isFinite(u)) return `beyond ${fmt(searchMax)}×`;
|
|
21
|
+
return u >= 10 ? `${Math.floor(u)}×` : `${Math.floor(u * 10) / 10}×`;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Two load multiples: "7.4×" when both edges read the same, else "between 7.4× and 10×" or, short, "7.4×–10×". An
|
|
25
|
+
* edge that never comes reads "7.4× or more" ("≥ 7.4×").
|
|
26
|
+
*/
|
|
27
|
+
function multiples(r, searchMax = 2e3, short = false) {
|
|
28
|
+
const [lo, hi] = [multiple(r.low, searchMax), multiple(r.high, searchMax)];
|
|
29
|
+
if (lo === hi) return lo;
|
|
30
|
+
if (!Number.isFinite(r.high)) return short ? `≥ ${lo}` : `${lo} or more`;
|
|
31
|
+
return short ? `${lo}–${hi}` : `between ${lo} and ${hi}`;
|
|
32
|
+
}
|
|
33
|
+
/** When two edges are reached: "(around March 2029)", "(between March 2029 and May 2030)" or "(… or later)". */
|
|
34
|
+
function months(low, high) {
|
|
35
|
+
if (!low) return "";
|
|
36
|
+
if (!high) return ` (${around(low)} or later)`;
|
|
37
|
+
return monthOf(low) === monthOf(high) ? ` (${around(low)})` : ` (between ${monthOf(low)} and ${monthOf(high)})`;
|
|
38
|
+
}
|
|
39
|
+
/** A percentage of a 0–1 share: 50 %. */
|
|
40
|
+
function pct(share) {
|
|
41
|
+
return `${Math.round(share * 100)} %`;
|
|
42
|
+
}
|
|
43
|
+
/** A duration in seconds for a meter: 0 s / 42 s / 3 min / > 1 h / stalled. */
|
|
44
|
+
function secs(s) {
|
|
45
|
+
if (!Number.isFinite(s)) return "stalled";
|
|
46
|
+
if (s < 1) return "0 s";
|
|
47
|
+
if (s < 60) return `${Math.round(s)} s`;
|
|
48
|
+
if (s < 3600) return `${Math.round(s / 60)} min`;
|
|
49
|
+
return "> 1 h";
|
|
50
|
+
}
|
|
51
|
+
/** The 1-2-5 step at or above x: 1, 2, 5, 10, 20, 50, … */
|
|
52
|
+
function niceStep(x) {
|
|
53
|
+
const p = 10 ** Math.floor(Math.log10(Math.max(x, 1)));
|
|
54
|
+
const f = x / p;
|
|
55
|
+
return (f <= 1 ? 1 : f <= 2 ? 2 : f <= 5 ? 5 : 10) * p;
|
|
56
|
+
}
|
|
57
|
+
/** "a", "a and b", "a, b and c". */
|
|
58
|
+
function joinAnd(items) {
|
|
59
|
+
if (items.length <= 1) return items[0] ?? "";
|
|
60
|
+
return `${items.slice(0, -1).join(", ")} and ${items[items.length - 1]}`;
|
|
61
|
+
}
|
|
62
|
+
const MONTHS = [
|
|
63
|
+
"January",
|
|
64
|
+
"February",
|
|
65
|
+
"March",
|
|
66
|
+
"April",
|
|
67
|
+
"May",
|
|
68
|
+
"June",
|
|
69
|
+
"July",
|
|
70
|
+
"August",
|
|
71
|
+
"September",
|
|
72
|
+
"October",
|
|
73
|
+
"November",
|
|
74
|
+
"December"
|
|
75
|
+
];
|
|
76
|
+
/** A month in UTC, "March 2028" or short "Mar 2028". Fixed names, whatever the locale. */
|
|
77
|
+
function monthOf(d, short = false) {
|
|
78
|
+
const name = MONTHS[d.getUTCMonth()];
|
|
79
|
+
return `${short ? name.slice(0, 3) : name} ${d.getUTCFullYear()}`;
|
|
80
|
+
}
|
|
81
|
+
/** A date as a reader plans with it, by month: "around March 2028". */
|
|
82
|
+
function around(d) {
|
|
83
|
+
return `around ${monthOf(d)}`;
|
|
84
|
+
}
|
|
85
|
+
export { around, compact, fmt, joinAnd, monthOf, months, multiple, multiples, niceStep, pct, secs };
|