@blamejs/core 0.9.46 → 0.9.49

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/lib/metrics.js CHANGED
@@ -853,18 +853,43 @@ function snapshotRead(p) {
853
853
  * Format a snapshot object for human or machine consumption.
854
854
  *
855
855
  * format: "text" — operator-readable lines, one field per row (default)
856
- * format: "prometheus" — Prometheus 0.0.4 text format, gauge metrics
857
- * named with a configurable prefix; only top-level
858
- * numeric fields under `snap.fields` are emitted
856
+ * format: "prometheus" — Prometheus 0.0.4 text format
857
+ *
858
+ * ## Type detection (`prometheus` format only)
859
+ *
860
+ * Per Prometheus naming convention + OpenMetrics 1.0.0 §6.2, counter
861
+ * metric families MUST carry the `_total` suffix; every other numeric
862
+ * field renders as a gauge. The renderer auto-detects by suffix:
863
+ *
864
+ * - field name ends in `_total` → `# TYPE <name> counter`
865
+ * - everything else → `# TYPE <name> gauge`
866
+ *
867
+ * Operators with metrics that don't fit the convention (e.g. a counter
868
+ * named `bytes_sent` without the `_total` suffix, or a gauge that
869
+ * happens to end in `_total`) opt the right type via `opts.fieldTypes`:
870
+ *
871
+ * render(snap, { format: "prometheus", fieldTypes: {
872
+ * bytes_sent: "counter", // override default gauge
873
+ * ratio_total: "gauge", // override default counter
874
+ * }});
875
+ *
876
+ * Pre-v0.9.47 every field rendered as gauge regardless of name, which
877
+ * broke `rate()` queries against counter-shaped series. Operators
878
+ * scraping a long-running deployment will see `rate(*_total[5m])`
879
+ * queries start returning the right answer once the new types reach
880
+ * the scrape target.
859
881
  *
860
882
  * @opts
861
- * format: "text" | "prometheus", // default: "text"
862
- * prefix: string, // prometheus-only; default: "blamejs"
883
+ * format: "text" | "prometheus", // default: "text"
884
+ * prefix: string, // prometheus-only; default: "blamejs"
885
+ * fieldTypes: Object, // prometheus-only; per-field type override
886
+ * // map. Values: "counter" | "gauge".
863
887
  *
864
888
  * @example
865
889
  * var snap = b.metrics.snapshot.read("/run/blamejs/metrics.json");
866
890
  * process.stdout.write(b.metrics.snapshot.render(snap));
867
- * // or for Prometheus scraping:
891
+ * // or for Prometheus scraping (auto-detects http_requests_total
892
+ * // as a counter via the _total suffix):
868
893
  * res.setHeader("Content-Type", "text/plain; version=0.0.4");
869
894
  * res.end(b.metrics.snapshot.render(snap, { format: "prometheus", prefix: "myapp" }));
870
895
  */
@@ -899,6 +924,11 @@ function snapshotRender(snap, opts) {
899
924
  throw new MetricsError("metrics-snapshot/bad-prefix",
900
925
  "metrics.snapshot.render: prometheus prefix must match [a-zA-Z_][a-zA-Z0-9_]*, got '" + prefix + "'");
901
926
  }
927
+ var fieldTypes = opts.fieldTypes || {};
928
+ if (typeof fieldTypes !== "object" || fieldTypes === null || Array.isArray(fieldTypes)) {
929
+ throw new MetricsError("metrics-snapshot/bad-field-types",
930
+ "metrics.snapshot.render: opts.fieldTypes must be an object mapping field-name → 'counter' | 'gauge'");
931
+ }
902
932
  var out = [];
903
933
  // allow:bare-canonicalize-walk — sort is for stable Prometheus
904
934
  // exposition output ordering, not canonicalize-for-hashing
@@ -909,7 +939,20 @@ function snapshotRender(snap, opts) {
909
939
  if (typeof v2 !== "number" || !isFinite(v2)) continue; // only numeric scalars
910
940
  if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(k2)) continue; // skip prom-incompatible names
911
941
  var metric = prefix + "_" + k2;
912
- out.push("# TYPE " + metric + " gauge");
942
+ var declared = fieldTypes[k2];
943
+ var fieldType;
944
+ if (declared !== undefined) {
945
+ if (declared !== "counter" && declared !== "gauge") {
946
+ throw new MetricsError("metrics-snapshot/bad-field-type",
947
+ "metrics.snapshot.render: opts.fieldTypes." + k2 + " must be 'counter' or 'gauge', got '" + declared + "'");
948
+ }
949
+ fieldType = declared;
950
+ } else {
951
+ // Prometheus naming convention + OpenMetrics 1.0.0 §6.2:
952
+ // counter family names carry the _total suffix.
953
+ fieldType = /_total$/.test(k2) ? "counter" : "gauge";
954
+ }
955
+ out.push("# TYPE " + metric + " " + fieldType);
913
956
  out.push(metric + " " + v2);
914
957
  }
915
958
  return out.join("\n") + "\n";
@@ -86,14 +86,41 @@ function _safeAuditEmit(action, outcome, metadata) {
86
86
  }
87
87
 
88
88
  // ---- semver-shaped comparison (tag_name like "v0.7.30" or "0.7.30") ----
89
- // Strips a leading "v" / "V" then parses dot-separated numeric components.
90
- // Non-numeric components are compared lexicographically (handles release
91
- // suffixes like "1.0.0-rc.1" by falling back to string comparison after
92
- // the matching numeric prefix). Returns -1 / 0 / +1.
93
89
  function _normalizeTag(tag) {
94
90
  if (typeof tag !== "string") return "";
95
91
  return tag.replace(/^v/i, "").trim();
96
92
  }
93
+
94
+ /**
95
+ * @primitive b.selfUpdate.compareTags
96
+ * @signature b.selfUpdate.compareTags(a, b)
97
+ * @since 0.9.47
98
+ * @status stable
99
+ *
100
+ * Compare two release tags / version strings. Returns `-1` if `a < b`,
101
+ * `+1` if `a > b`, `0` if equal. Strips a leading `v` / `V`, then walks
102
+ * dot-separated components: numeric pairs compared numerically; any
103
+ * non-numeric component (release suffixes like `1.0.0-rc.1`) falls back
104
+ * to lexicographic compare on that component. Missing components on
105
+ * either side are treated as `"0"`.
106
+ *
107
+ * Shape follows SemVer 2.0.0 §11 precedence rules for the numeric prefix.
108
+ * Deviations from the full SemVer §11 spec — pre-release identifiers
109
+ * (`-rc.1` < release) are compared lexicographically rather than the
110
+ * SemVer-mandated "alphanumeric identifiers compared as numbers if all
111
+ * numeric" rule. For most version-shaped strings the result is identical;
112
+ * exotic pre-release shapes (`1.0.0-alpha.10` vs `1.0.0-alpha.9`) sort
113
+ * lexicographically here (`10` < `9` as strings) rather than numerically.
114
+ * Operators with strict SemVer §11 needs should use a dedicated SemVer
115
+ * parser; this primitive targets the common framework-update polling
116
+ * shape (`v0.9.46` vs `v0.9.47`) where pre-release tags are rare.
117
+ *
118
+ * @example
119
+ * b.selfUpdate.compareTags("v0.9.46", "v0.9.47"); // → -1
120
+ * b.selfUpdate.compareTags("v0.9.47", "0.9.47"); // → 0 (leading "v" stripped)
121
+ * b.selfUpdate.compareTags("1.10.0", "1.9.0"); // → +1 (numeric, not lex)
122
+ * b.selfUpdate.compareTags("v0.7.30", "v0.7.30"); // → 0
123
+ */
97
124
  function _compareTags(a, b) {
98
125
  var na = _normalizeTag(a);
99
126
  var nb2 = _normalizeTag(b);
@@ -648,6 +675,10 @@ module.exports = {
648
675
  SelfUpdateError: SelfUpdateError,
649
676
  ALLOWED_HASH_ALGS: ALLOWED_HASH_ALGS,
650
677
  DEFAULT_HASH_ALG: DEFAULT_HASH_ALG,
678
+ // Public surface — same impl as the internal `_compareTags`;
679
+ // downstream consumers replacing one-off compareVersions helpers
680
+ // call this.
681
+ compareTags: _compareTags,
651
682
  // Internal — exposed for the layer-0 test suite only.
652
683
  _compareTags: _compareTags,
653
684
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.9.46",
3
+ "version": "0.9.49",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.6",
5
- "serialNumber": "urn:uuid:c4e7762c-d190-47e8-9996-ee555a2414f8",
5
+ "serialNumber": "urn:uuid:21323cdb-5bfe-4b88-a63f-906795a497e6",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-15T23:01:46.269Z",
8
+ "timestamp": "2026-05-16T03:34:31.380Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/core@0.9.46",
22
+ "bom-ref": "@blamejs/core@0.9.49",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.9.46",
25
+ "version": "0.9.49",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "The Node framework that owns its stack.",
29
- "purl": "pkg:npm/%40blamejs/core@0.9.46",
29
+ "purl": "pkg:npm/%40blamejs/core@0.9.49",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/core@0.9.46",
57
+ "ref": "@blamejs/core@0.9.49",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]