@uptimizr/db 1.0.1 → 2.0.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.
Files changed (88) hide show
  1. package/AGENTS.md +103 -1
  2. package/README.md +100 -3
  3. package/dist/cli/createProject.js +23 -6
  4. package/dist/cli/createProject.js.map +1 -1
  5. package/dist/cli/seed.js +12 -4
  6. package/dist/cli/seed.js.map +1 -1
  7. package/dist/duckdb/audit.d.ts +13 -0
  8. package/dist/duckdb/audit.d.ts.map +1 -0
  9. package/dist/duckdb/audit.js +67 -0
  10. package/dist/duckdb/audit.js.map +1 -0
  11. package/dist/duckdb/client.d.ts.map +1 -1
  12. package/dist/duckdb/client.js +4 -0
  13. package/dist/duckdb/client.js.map +1 -1
  14. package/dist/duckdb/migrations.d.ts.map +1 -1
  15. package/dist/duckdb/migrations.js +97 -0
  16. package/dist/duckdb/migrations.js.map +1 -1
  17. package/dist/duckdb/projects.d.ts +9 -5
  18. package/dist/duckdb/projects.d.ts.map +1 -1
  19. package/dist/duckdb/projects.js +54 -24
  20. package/dist/duckdb/projects.js.map +1 -1
  21. package/dist/duckdb/queries.d.ts +7 -0
  22. package/dist/duckdb/queries.d.ts.map +1 -1
  23. package/dist/duckdb/queries.js +10 -1
  24. package/dist/duckdb/queries.js.map +1 -1
  25. package/dist/duckdb/sceneRegions.d.ts +24 -0
  26. package/dist/duckdb/sceneRegions.d.ts.map +1 -0
  27. package/dist/duckdb/sceneRegions.js +72 -0
  28. package/dist/duckdb/sceneRegions.js.map +1 -0
  29. package/dist/index.d.ts +8 -2
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +18 -1
  32. package/dist/index.js.map +1 -1
  33. package/dist/metadata.d.ts +195 -9
  34. package/dist/metadata.d.ts.map +1 -1
  35. package/dist/metadata.js +192 -0
  36. package/dist/metadata.js.map +1 -1
  37. package/dist/parity/compare.d.ts +22 -0
  38. package/dist/parity/compare.d.ts.map +1 -1
  39. package/dist/parity/compare.js +31 -0
  40. package/dist/parity/compare.js.map +1 -1
  41. package/dist/query/aggregations.d.ts +8 -0
  42. package/dist/query/aggregations.d.ts.map +1 -1
  43. package/dist/query/aggregations.js +78 -0
  44. package/dist/query/aggregations.js.map +1 -1
  45. package/dist/query/coerce.d.ts +94 -0
  46. package/dist/query/coerce.d.ts.map +1 -0
  47. package/dist/query/coerce.js +191 -0
  48. package/dist/query/coerce.js.map +1 -0
  49. package/dist/query/summary/cluster.d.ts +78 -0
  50. package/dist/query/summary/cluster.d.ts.map +1 -0
  51. package/dist/query/summary/cluster.js +200 -0
  52. package/dist/query/summary/cluster.js.map +1 -0
  53. package/dist/query/summary/columns.d.ts +66 -0
  54. package/dist/query/summary/columns.d.ts.map +1 -0
  55. package/dist/query/summary/columns.js +98 -0
  56. package/dist/query/summary/columns.js.map +1 -0
  57. package/dist/query/summary/format.d.ts +31 -0
  58. package/dist/query/summary/format.d.ts.map +1 -0
  59. package/dist/query/summary/format.js +119 -0
  60. package/dist/query/summary/format.js.map +1 -0
  61. package/dist/query/summary/index.d.ts +19 -0
  62. package/dist/query/summary/index.d.ts.map +1 -0
  63. package/dist/query/summary/index.js +16 -0
  64. package/dist/query/summary/index.js.map +1 -0
  65. package/dist/query/summary/reading.d.ts +26 -0
  66. package/dist/query/summary/reading.d.ts.map +1 -0
  67. package/dist/query/summary/reading.js +169 -0
  68. package/dist/query/summary/reading.js.map +1 -0
  69. package/dist/query/summary/schema.d.ts +217 -0
  70. package/dist/query/summary/schema.d.ts.map +1 -0
  71. package/dist/query/summary/schema.js +167 -0
  72. package/dist/query/summary/schema.js.map +1 -0
  73. package/dist/query/summary/stats.d.ts +33 -0
  74. package/dist/query/summary/stats.d.ts.map +1 -0
  75. package/dist/query/summary/stats.js +73 -0
  76. package/dist/query/summary/stats.js.map +1 -0
  77. package/dist/query/summary/summarize.d.ts +57 -0
  78. package/dist/query/summary/summarize.d.ts.map +1 -0
  79. package/dist/query/summary/summarize.js +424 -0
  80. package/dist/query/summary/summarize.js.map +1 -0
  81. package/dist/query/summary/types.d.ts +223 -0
  82. package/dist/query/summary/types.d.ts.map +1 -0
  83. package/dist/query/summary/types.js +8 -0
  84. package/dist/query/summary/types.js.map +1 -0
  85. package/dist/query/types.d.ts +13 -0
  86. package/dist/query/types.d.ts.map +1 -1
  87. package/llms.txt +7 -3
  88. package/package.json +10 -4
@@ -0,0 +1,78 @@
1
+ /**
2
+ * **Deterministic greedy cluster merge** for binned and voxelised metrics
3
+ * (design sketch §B.2).
4
+ *
5
+ * A 500-bin heatmap or a voxel cloud is the wrong shape for a language model:
6
+ * every cell is individually meaningless and collectively unaffordable. This
7
+ * module collapses the grid into a handful of *hotspots* — connected runs of
8
+ * adjacent occupied cells whose weight clears a density threshold — each
9
+ * reporting where it is, how big it is and how much of the total it holds.
10
+ *
11
+ * ## Why not k-means
12
+ *
13
+ * A summary has to be reproducible: the same rows must produce the same words,
14
+ * whatever order the store returned them in and however many times it is asked.
15
+ * k-means needs seeds, so it is neither. What is implemented here is a plain
16
+ * connected-components merge over the grid:
17
+ *
18
+ * 1. duplicate coordinates are summed, so a metric with extra dimensions (a
19
+ * per-mesh flow link, say) collapses onto its grid first;
20
+ * 2. cells with weight at or above `densityThreshold` qualify — by default the
21
+ * mean weight per occupied cell, which keeps the busy cells and drops the
22
+ * long thin tail that would otherwise fuse every hotspot into one blob;
23
+ * 3. qualifying cells are merged with their neighbours — the 8-neighbourhood
24
+ * for a 2D grid, the 26-neighbourhood for a 3D one (every cell whose indices
25
+ * differ by at most one on each axis);
26
+ * 4. clusters are ranked by summed weight, and the rest is reported in bulk.
27
+ *
28
+ * Connected components do not depend on iteration order, and every accumulation
29
+ * (centroid, weight) is done over the component's cells in **sorted coordinate
30
+ * order**, so even floating-point summation is bit-identical when the input is
31
+ * shuffled. `__tests__/summary.test.ts` asserts exactly that.
32
+ *
33
+ * Pure and browser-safe: no I/O, no `node:` import, no store reference.
34
+ */
35
+ import type { SpatialCluster } from "./types.js";
36
+ /** One occupied grid cell: its integer indices on each axis, and its weight. */
37
+ export interface GridCell {
38
+ coords: readonly number[];
39
+ weight: number;
40
+ }
41
+ /** Knobs for {@link clusterCells}. */
42
+ export interface ClusterOptions {
43
+ /**
44
+ * Minimum weight a cell needs to take part in the merge. Defaults to the mean
45
+ * weight per occupied cell. Pass `0` to cluster every occupied cell.
46
+ */
47
+ densityThreshold?: number;
48
+ /** Maximum clusters to report individually; the remainder goes to `rest`. */
49
+ maxClusters?: number;
50
+ }
51
+ /** What {@link clusterCells} found. */
52
+ export interface ClusterResult {
53
+ clusters: SpatialCluster[];
54
+ /** Distinct occupied cells, after duplicate coordinates were summed. */
55
+ occupiedCells: number;
56
+ /** Summed weight of every occupied cell (clustered or not). */
57
+ totalWeight: number;
58
+ /** The threshold that was actually applied. */
59
+ densityThreshold: number;
60
+ /** Clusters, cells and weight not reported individually. */
61
+ rest: {
62
+ clusters: number;
63
+ cells: number;
64
+ weight: number;
65
+ };
66
+ }
67
+ /**
68
+ * Merge adjacent occupied cells into ranked hotspots.
69
+ *
70
+ * Cells with a non-finite or non-positive weight are ignored (an empty bin is
71
+ * not a hotspot), as are cells whose coordinate arity does not match the first
72
+ * cell's — a malformed row must not silently shift the grid.
73
+ *
74
+ * Returns clusters ordered by weight descending, ties broken by their lowest
75
+ * corner then by cell count, so the output is a pure function of the cell *set*.
76
+ */
77
+ export declare function clusterCells(cells: readonly GridCell[], options?: ClusterOptions): ClusterResult;
78
+ //# sourceMappingURL=cluster.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cluster.d.ts","sourceRoot":"","sources":["../../../src/query/summary/cluster.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,gFAAgF;AAChF,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,sCAAsC;AACtC,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,uCAAuC;AACvC,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B,wEAAwE;IACxE,aAAa,EAAE,MAAM,CAAC;IACtB,+DAA+D;IAC/D,WAAW,EAAE,MAAM,CAAC;IACpB,+CAA+C;IAC/C,gBAAgB,EAAE,MAAM,CAAC;IACzB,4DAA4D;IAC5D,IAAI,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAC3D;AAwCD;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,SAAS,QAAQ,EAAE,EAC1B,OAAO,GAAE,cAAmB,GAC3B,aAAa,CAiHf"}
@@ -0,0 +1,200 @@
1
+ /**
2
+ * **Deterministic greedy cluster merge** for binned and voxelised metrics
3
+ * (design sketch §B.2).
4
+ *
5
+ * A 500-bin heatmap or a voxel cloud is the wrong shape for a language model:
6
+ * every cell is individually meaningless and collectively unaffordable. This
7
+ * module collapses the grid into a handful of *hotspots* — connected runs of
8
+ * adjacent occupied cells whose weight clears a density threshold — each
9
+ * reporting where it is, how big it is and how much of the total it holds.
10
+ *
11
+ * ## Why not k-means
12
+ *
13
+ * A summary has to be reproducible: the same rows must produce the same words,
14
+ * whatever order the store returned them in and however many times it is asked.
15
+ * k-means needs seeds, so it is neither. What is implemented here is a plain
16
+ * connected-components merge over the grid:
17
+ *
18
+ * 1. duplicate coordinates are summed, so a metric with extra dimensions (a
19
+ * per-mesh flow link, say) collapses onto its grid first;
20
+ * 2. cells with weight at or above `densityThreshold` qualify — by default the
21
+ * mean weight per occupied cell, which keeps the busy cells and drops the
22
+ * long thin tail that would otherwise fuse every hotspot into one blob;
23
+ * 3. qualifying cells are merged with their neighbours — the 8-neighbourhood
24
+ * for a 2D grid, the 26-neighbourhood for a 3D one (every cell whose indices
25
+ * differ by at most one on each axis);
26
+ * 4. clusters are ranked by summed weight, and the rest is reported in bulk.
27
+ *
28
+ * Connected components do not depend on iteration order, and every accumulation
29
+ * (centroid, weight) is done over the component's cells in **sorted coordinate
30
+ * order**, so even floating-point summation is bit-identical when the input is
31
+ * shuffled. `__tests__/summary.test.ts` asserts exactly that.
32
+ *
33
+ * Pure and browser-safe: no I/O, no `node:` import, no store reference.
34
+ */
35
+ /** Compare two coordinate tuples lexicographically (ascending). */
36
+ function compareCoords(a, b) {
37
+ for (let i = 0; i < a.length; i++) {
38
+ const left = a[i] ?? 0;
39
+ const right = b[i] ?? 0;
40
+ if (left !== right)
41
+ return left - right;
42
+ }
43
+ return 0;
44
+ }
45
+ /**
46
+ * Every neighbour offset for a grid of `axes` dimensions: all combinations of
47
+ * `-1 | 0 | +1` per axis, minus the all-zero one. Eight offsets in 2D, twenty-six
48
+ * in 3D — the neighbourhoods the sketch specifies, derived rather than typed out.
49
+ */
50
+ function neighbourOffsets(axes) {
51
+ let offsets = [[]];
52
+ for (let axis = 0; axis < axes; axis++) {
53
+ const next = [];
54
+ for (const prefix of offsets) {
55
+ for (const delta of [-1, 0, 1])
56
+ next.push([...prefix, delta]);
57
+ }
58
+ offsets = next;
59
+ }
60
+ return offsets.filter((offset) => offset.some((delta) => delta !== 0));
61
+ }
62
+ /** Cached offsets — the only two shapes that occur are 2D and 3D. */
63
+ const OFFSETS = new Map();
64
+ function offsetsFor(axes) {
65
+ let cached = OFFSETS.get(axes);
66
+ if (cached == null) {
67
+ cached = neighbourOffsets(axes);
68
+ OFFSETS.set(axes, cached);
69
+ }
70
+ return cached;
71
+ }
72
+ /**
73
+ * Merge adjacent occupied cells into ranked hotspots.
74
+ *
75
+ * Cells with a non-finite or non-positive weight are ignored (an empty bin is
76
+ * not a hotspot), as are cells whose coordinate arity does not match the first
77
+ * cell's — a malformed row must not silently shift the grid.
78
+ *
79
+ * Returns clusters ordered by weight descending, ties broken by their lowest
80
+ * corner then by cell count, so the output is a pure function of the cell *set*.
81
+ */
82
+ export function clusterCells(cells, options = {}) {
83
+ const axes = cells[0]?.coords.length ?? 0;
84
+ const empty = {
85
+ clusters: [],
86
+ occupiedCells: 0,
87
+ totalWeight: 0,
88
+ densityThreshold: options.densityThreshold ?? 0,
89
+ rest: { clusters: 0, cells: 0, weight: 0 },
90
+ };
91
+ if (axes === 0)
92
+ return empty;
93
+ // 1. Collapse duplicate coordinates onto the grid.
94
+ const grid = new Map();
95
+ let totalWeight = 0;
96
+ for (const cell of cells) {
97
+ if (cell.coords.length !== axes)
98
+ continue;
99
+ if (!Number.isFinite(cell.weight) || cell.weight <= 0)
100
+ continue;
101
+ if (!cell.coords.every((coordinate) => Number.isFinite(coordinate)))
102
+ continue;
103
+ const coords = cell.coords.map((coordinate) => Math.trunc(coordinate));
104
+ const key = coords.join(",");
105
+ const existing = grid.get(key);
106
+ if (existing)
107
+ existing.weight += cell.weight;
108
+ else
109
+ grid.set(key, { coords, weight: cell.weight });
110
+ totalWeight += cell.weight;
111
+ }
112
+ if (grid.size === 0)
113
+ return empty;
114
+ // 2. Qualify cells against the density threshold (mean weight by default).
115
+ const densityThreshold = options.densityThreshold ?? totalWeight / grid.size;
116
+ const qualifying = new Map();
117
+ for (const [key, cell] of grid) {
118
+ if (cell.weight >= densityThreshold)
119
+ qualifying.set(key, cell);
120
+ }
121
+ // 3. Connected components over the qualifying cells (8- or 26-neighbourhood).
122
+ const offsets = offsetsFor(axes);
123
+ const seen = new Set();
124
+ const components = [];
125
+ // Seed order only affects which component is discovered first, never their
126
+ // membership — but iterate the grid in sorted order anyway so a debugger shows
127
+ // the same thing twice.
128
+ const seeds = [...qualifying.keys()].sort();
129
+ for (const seed of seeds) {
130
+ if (seen.has(seed))
131
+ continue;
132
+ const component = [];
133
+ const stack = [seed];
134
+ seen.add(seed);
135
+ while (stack.length > 0) {
136
+ const key = stack.pop();
137
+ const cell = qualifying.get(key);
138
+ if (cell == null)
139
+ continue;
140
+ component.push(cell);
141
+ for (const offset of offsets) {
142
+ const neighbourKey = cell.coords
143
+ .map((value, axis) => value + (offset[axis] ?? 0))
144
+ .join(",");
145
+ if (seen.has(neighbourKey) || !qualifying.has(neighbourKey))
146
+ continue;
147
+ seen.add(neighbourKey);
148
+ stack.push(neighbourKey);
149
+ }
150
+ }
151
+ components.push(component);
152
+ }
153
+ // 4. Describe each component. Accumulating in sorted coordinate order makes
154
+ // the floating-point sums independent of the input's row order.
155
+ const described = components.map((component) => {
156
+ const ordered = [...component].sort((a, b) => compareCoords(a.coords, b.coords));
157
+ const min = [...ordered[0].coords];
158
+ const max = [...min];
159
+ const weighted = new Array(axes).fill(0);
160
+ let weight = 0;
161
+ for (const cell of ordered) {
162
+ weight += cell.weight;
163
+ for (let axis = 0; axis < axes; axis++) {
164
+ const value = cell.coords[axis];
165
+ if (value < min[axis])
166
+ min[axis] = value;
167
+ if (value > max[axis])
168
+ max[axis] = value;
169
+ weighted[axis] = weighted[axis] + value * cell.weight;
170
+ }
171
+ }
172
+ return {
173
+ centroid: weighted.map((sum) => (weight > 0 ? sum / weight : 0)),
174
+ extent: { min, max },
175
+ cells: ordered.length,
176
+ weight,
177
+ share: totalWeight > 0 ? weight / totalWeight : null,
178
+ };
179
+ });
180
+ described.sort((a, b) => b.weight - a.weight || compareCoords(a.extent.min, b.extent.min) || b.cells - a.cells);
181
+ const cap = Math.max(0, options.maxClusters ?? described.length);
182
+ const clusters = described.slice(0, cap);
183
+ const dropped = described.slice(cap);
184
+ const clusteredCells = described.reduce((sum, cluster) => sum + cluster.cells, 0);
185
+ const reportedWeight = clusters.reduce((sum, cluster) => sum + cluster.weight, 0);
186
+ return {
187
+ clusters,
188
+ occupiedCells: grid.size,
189
+ totalWeight,
190
+ densityThreshold,
191
+ rest: {
192
+ clusters: dropped.length,
193
+ // Everything not listed individually: the dropped clusters' cells plus
194
+ // every occupied cell that never cleared the threshold.
195
+ cells: grid.size - clusteredCells + dropped.reduce((sum, cluster) => sum + cluster.cells, 0),
196
+ weight: totalWeight - reportedWeight,
197
+ },
198
+ };
199
+ }
200
+ //# sourceMappingURL=cluster.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cluster.js","sourceRoot":"","sources":["../../../src/query/summary/cluster.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAkCH,mEAAmE;AACnE,SAAS,aAAa,CAAC,CAAoB,EAAE,CAAoB;IAC/D,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAClC,MAAM,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACvB,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACxB,IAAI,IAAI,KAAK,KAAK;YAAE,OAAO,IAAI,GAAG,KAAK,CAAC;IAC1C,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED;;;;GAIG;AACH,SAAS,gBAAgB,CAAC,IAAY;IACpC,IAAI,OAAO,GAAe,CAAC,EAAE,CAAC,CAAC;IAC/B,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC;QACvC,MAAM,IAAI,GAAe,EAAE,CAAC;QAC5B,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC7B,KAAK,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;gBAAE,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC;QAChE,CAAC;QACD,OAAO,GAAG,IAAI,CAAC;IACjB,CAAC;IACD,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC;AACzE,CAAC;AAED,qEAAqE;AACrE,MAAM,OAAO,GAAG,IAAI,GAAG,EAAsB,CAAC;AAC9C,SAAS,UAAU,CAAC,IAAY;IAC9B,IAAI,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC/B,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC;QACnB,MAAM,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;QAChC,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC5B,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,YAAY,CAC1B,KAA0B,EAC1B,UAA0B,EAAE;IAE5B,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC;IAC1C,MAAM,KAAK,GAAkB;QAC3B,QAAQ,EAAE,EAAE;QACZ,aAAa,EAAE,CAAC;QAChB,WAAW,EAAE,CAAC;QACd,gBAAgB,EAAE,OAAO,CAAC,gBAAgB,IAAI,CAAC;QAC/C,IAAI,EAAE,EAAE,QAAQ,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE;KAC3C,CAAC;IACF,IAAI,IAAI,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAE7B,mDAAmD;IACnD,MAAM,IAAI,GAAG,IAAI,GAAG,EAAgD,CAAC;IACrE,IAAI,WAAW,GAAG,CAAC,CAAC;IACpB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,IAAI;YAAE,SAAS;QAC1C,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM,IAAI,CAAC;YAAE,SAAS;QAChE,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;YAAE,SAAS;QAC9E,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,UAAU,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC;QACvE,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC7B,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,QAAQ;YAAE,QAAQ,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC;;YACxC,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;QACpD,WAAW,IAAI,IAAI,CAAC,MAAM,CAAC;IAC7B,CAAC;IACD,IAAI,IAAI,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAElC,2EAA2E;IAC3E,MAAM,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,IAAI,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC;IAC7E,MAAM,UAAU,GAAG,IAAI,GAAG,EAAgD,CAAC;IAC3E,KAAK,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC;QAC/B,IAAI,IAAI,CAAC,MAAM,IAAI,gBAAgB;YAAE,UAAU,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IACjE,CAAC;IAED,8EAA8E;IAC9E,MAAM,OAAO,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;IACjC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,UAAU,GAA6C,EAAE,CAAC;IAChE,2EAA2E;IAC3E,+EAA+E;IAC/E,wBAAwB;IACxB,MAAM,KAAK,GAAG,CAAC,GAAG,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAC5C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,SAAS;QAC7B,MAAM,SAAS,GAA2C,EAAE,CAAC;QAC7D,MAAM,KAAK,GAAG,CAAC,IAAI,CAAC,CAAC;QACrB,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACf,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxB,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,EAAY,CAAC;YAClC,MAAM,IAAI,GAAG,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACjC,IAAI,IAAI,IAAI,IAAI;gBAAE,SAAS;YAC3B,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACrB,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;gBAC7B,MAAM,YAAY,GAAG,IAAI,CAAC,MAAM;qBAC7B,GAAG,CAAC,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,KAAK,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;qBACjD,IAAI,CAAC,GAAG,CAAC,CAAC;gBACb,IAAI,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,YAAY,CAAC;oBAAE,SAAS;gBACtE,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;gBACvB,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;YAC3B,CAAC;QACH,CAAC;QACD,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC7B,CAAC;IAED,4EAA4E;IAC5E,mEAAmE;IACnE,MAAM,SAAS,GAAqB,UAAU,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE;QAC/D,MAAM,OAAO,GAAG,CAAC,GAAG,SAAS,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;QACjF,MAAM,GAAG,GAAG,CAAC,GAAI,OAAO,CAAC,CAAC,CAA0B,CAAC,MAAM,CAAC,CAAC;QAC7D,MAAM,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC;QACrB,MAAM,QAAQ,GAAG,IAAI,KAAK,CAAS,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACjD,IAAI,MAAM,GAAG,CAAC,CAAC;QACf,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;YAC3B,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC;YACtB,KAAK,IAAI,IAAI,GAAG,CAAC,EAAE,IAAI,GAAG,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC;gBACvC,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,CAAW,CAAC;gBAC1C,IAAI,KAAK,GAAI,GAAG,CAAC,IAAI,CAAY;oBAAE,GAAG,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;gBACrD,IAAI,KAAK,GAAI,GAAG,CAAC,IAAI,CAAY;oBAAE,GAAG,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC;gBACrD,QAAQ,CAAC,IAAI,CAAC,GAAI,QAAQ,CAAC,IAAI,CAAY,GAAG,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC;YACpE,CAAC;QACH,CAAC;QACD,OAAO;YACL,QAAQ,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YAChE,MAAM,EAAE,EAAE,GAAG,EAAE,GAAG,EAAE;YACpB,KAAK,EAAE,OAAO,CAAC,MAAM;YACrB,MAAM;YACN,KAAK,EAAE,WAAW,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,IAAI;SACrD,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,SAAS,CAAC,IAAI,CACZ,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,IAAI,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAChG,CAAC;IAEF,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,CAAC,WAAW,IAAI,SAAS,CAAC,MAAM,CAAC,CAAC;IACjE,MAAM,QAAQ,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IACzC,MAAM,OAAO,GAAG,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACrC,MAAM,cAAc,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,CAAC,GAAG,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IAClF,MAAM,cAAc,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,CAAC,GAAG,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;IAElF,OAAO;QACL,QAAQ;QACR,aAAa,EAAE,IAAI,CAAC,IAAI;QACxB,WAAW;QACX,gBAAgB;QAChB,IAAI,EAAE;YACJ,QAAQ,EAAE,OAAO,CAAC,MAAM;YACxB,uEAAuE;YACvE,wDAAwD;YACxD,KAAK,EAAE,IAAI,CAAC,IAAI,GAAG,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,CAAC,GAAG,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC;YAC5F,MAAM,EAAE,WAAW,GAAG,cAAc;SACrC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Registry column lookups shared by the summariser.
3
+ *
4
+ * Everything the summary does — what to rank by, what to call a row, which
5
+ * columns index a grid, how big the sample is — is read out of the metric's
6
+ * `columns` semantics rather than hard-coded per metric. That is the whole point
7
+ * of the registry: a new aggregation gets a usable summary for free the moment
8
+ * it declares what its columns mean.
9
+ */
10
+ import type { ColumnSemantics, MetricDefinition } from "@uptimizr/metrics";
11
+ import type { SampleSize } from "./types.js";
12
+ /** A column name paired with its declared semantics. */
13
+ export type Column = readonly [name: string, semantics: ColumnSemantics];
14
+ /** Every declared column, in declaration order. */
15
+ export declare function columnsOf(metric: MetricDefinition): Column[];
16
+ /** The column a metric is ranked by (`measure: true`), if it declares one. */
17
+ export declare function measureColumn(metric: MetricDefinition): Column | undefined;
18
+ /** The column that names a row (`label: true`), if it declares one. */
19
+ export declare function labelColumn(metric: MetricDefinition): Column | undefined;
20
+ /**
21
+ * The ordered column of a `bucket`-grain metric (`axis: true`), falling back to
22
+ * the label column so a metric that has not declared an axis still summarises.
23
+ */
24
+ export declare function axisColumn(metric: MetricDefinition): Column | undefined;
25
+ /** The first column declared with a given unit, in declaration order. */
26
+ export declare function firstColumnWithUnit(metric: MetricDefinition, unit: string): Column | undefined;
27
+ /**
28
+ * The grid-index columns of a binned or voxelised metric: the first `axes`
29
+ * columns declared with `unit: "index"`, in declaration order.
30
+ *
31
+ * Declaration order is the rule, and it is what makes the choice explainable:
32
+ * `position_heatmap` indexes on `gx`/`gz` and skips its world-space `avg_y`,
33
+ * `flow_links` clusters on the view-direction grid it leads with, and
34
+ * `click_rays` clusters on the click-time standpoint voxel (`cam_v*`) it
35
+ * declares first. Returns `undefined` when the metric declares too few.
36
+ */
37
+ export declare function gridColumns(metric: MetricDefinition, axes: number): Column[] | undefined;
38
+ /**
39
+ * The column a spatial cluster weighs its cells by: the first `count` column if
40
+ * the metric declares one, else the measure. `perf_heatmap` measures `avg_fps`
41
+ * but counts `samples` — clustering by an average would weigh a one-sample
42
+ * voxel the same as a thousand-sample one.
43
+ */
44
+ export declare function weightColumn(metric: MetricDefinition): Column | undefined;
45
+ /** A row's value for a column, when it is a finite number; `null` otherwise. */
46
+ export declare function numberAt(row: Readonly<Record<string, unknown>>, column: string): number | null;
47
+ /**
48
+ * Sum a column across rows. `null` when the column is absent from every row or
49
+ * every value is null — a metric that reports nothing must not report `0`.
50
+ */
51
+ export declare function sumColumn(rows: readonly Readonly<Record<string, unknown>>[], column: string | undefined): number | null;
52
+ /**
53
+ * How much data is behind a result, derived entirely from column units.
54
+ *
55
+ * - `sessions` — the number of rows when one row *is* a session
56
+ * (`grain: "session"`), otherwise the sum of the first column declared
57
+ * `unit: "sessions"`. When rows can share a session (a per-mesh breakdown,
58
+ * say) that sum is an upper bound, not a distinct count; the caveats say so.
59
+ * - `events` — the sum of the first column declared `unit: "count"`. Declaration
60
+ * order matters: registry rows lead with the denominator (`total_clicks`
61
+ * before `dead_clicks`, `samples` before the percentiles computed from them).
62
+ *
63
+ * `null` means the metric declares nothing that could answer the question.
64
+ */
65
+ export declare function sampleSizeOf(metric: MetricDefinition, rows: readonly Readonly<Record<string, unknown>>[]): SampleSize;
66
+ //# sourceMappingURL=columns.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"columns.d.ts","sourceRoot":"","sources":["../../../src/query/summary/columns.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC3E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,wDAAwD;AACxD,MAAM,MAAM,MAAM,GAAG,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,eAAe,CAAC,CAAC;AAEzE,mDAAmD;AACnD,wBAAgB,SAAS,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,EAAE,CAE5D;AAED,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,GAAG,SAAS,CAE1E;AAED,uEAAuE;AACvE,wBAAgB,WAAW,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,GAAG,SAAS,CAExE;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,GAAG,SAAS,CAEvE;AAED,yEAAyE;AACzE,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAE9F;AAED;;;;;;;;;GASG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAGxF;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,GAAG,SAAS,CAEzE;AAED,gFAAgF;AAChF,wBAAgB,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAG9F;AAED;;;GAGG;AACH,wBAAgB,SAAS,CACvB,IAAI,EAAE,SAAS,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAAE,EAClD,MAAM,EAAE,MAAM,GAAG,SAAS,GACzB,MAAM,GAAG,IAAI,CAWf;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,gBAAgB,EACxB,IAAI,EAAE,SAAS,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EAAE,GACjD,UAAU,CAMZ"}
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Registry column lookups shared by the summariser.
3
+ *
4
+ * Everything the summary does — what to rank by, what to call a row, which
5
+ * columns index a grid, how big the sample is — is read out of the metric's
6
+ * `columns` semantics rather than hard-coded per metric. That is the whole point
7
+ * of the registry: a new aggregation gets a usable summary for free the moment
8
+ * it declares what its columns mean.
9
+ */
10
+ /** Every declared column, in declaration order. */
11
+ export function columnsOf(metric) {
12
+ return Object.entries(metric.columns);
13
+ }
14
+ /** The column a metric is ranked by (`measure: true`), if it declares one. */
15
+ export function measureColumn(metric) {
16
+ return columnsOf(metric).find(([, semantics]) => semantics.measure);
17
+ }
18
+ /** The column that names a row (`label: true`), if it declares one. */
19
+ export function labelColumn(metric) {
20
+ return columnsOf(metric).find(([, semantics]) => semantics.label);
21
+ }
22
+ /**
23
+ * The ordered column of a `bucket`-grain metric (`axis: true`), falling back to
24
+ * the label column so a metric that has not declared an axis still summarises.
25
+ */
26
+ export function axisColumn(metric) {
27
+ return columnsOf(metric).find(([, semantics]) => semantics.axis) ?? labelColumn(metric);
28
+ }
29
+ /** The first column declared with a given unit, in declaration order. */
30
+ export function firstColumnWithUnit(metric, unit) {
31
+ return columnsOf(metric).find(([, semantics]) => semantics.unit === unit);
32
+ }
33
+ /**
34
+ * The grid-index columns of a binned or voxelised metric: the first `axes`
35
+ * columns declared with `unit: "index"`, in declaration order.
36
+ *
37
+ * Declaration order is the rule, and it is what makes the choice explainable:
38
+ * `position_heatmap` indexes on `gx`/`gz` and skips its world-space `avg_y`,
39
+ * `flow_links` clusters on the view-direction grid it leads with, and
40
+ * `click_rays` clusters on the click-time standpoint voxel (`cam_v*`) it
41
+ * declares first. Returns `undefined` when the metric declares too few.
42
+ */
43
+ export function gridColumns(metric, axes) {
44
+ const indexed = columnsOf(metric).filter(([, semantics]) => semantics.unit === "index");
45
+ return indexed.length >= axes ? indexed.slice(0, axes) : undefined;
46
+ }
47
+ /**
48
+ * The column a spatial cluster weighs its cells by: the first `count` column if
49
+ * the metric declares one, else the measure. `perf_heatmap` measures `avg_fps`
50
+ * but counts `samples` — clustering by an average would weigh a one-sample
51
+ * voxel the same as a thousand-sample one.
52
+ */
53
+ export function weightColumn(metric) {
54
+ return firstColumnWithUnit(metric, "count") ?? measureColumn(metric);
55
+ }
56
+ /** A row's value for a column, when it is a finite number; `null` otherwise. */
57
+ export function numberAt(row, column) {
58
+ const value = row[column];
59
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
60
+ }
61
+ /**
62
+ * Sum a column across rows. `null` when the column is absent from every row or
63
+ * every value is null — a metric that reports nothing must not report `0`.
64
+ */
65
+ export function sumColumn(rows, column) {
66
+ if (column == null)
67
+ return null;
68
+ let total = 0;
69
+ let seen = false;
70
+ for (const row of rows) {
71
+ const value = numberAt(row, column);
72
+ if (value == null)
73
+ continue;
74
+ seen = true;
75
+ total += value;
76
+ }
77
+ return seen ? total : null;
78
+ }
79
+ /**
80
+ * How much data is behind a result, derived entirely from column units.
81
+ *
82
+ * - `sessions` — the number of rows when one row *is* a session
83
+ * (`grain: "session"`), otherwise the sum of the first column declared
84
+ * `unit: "sessions"`. When rows can share a session (a per-mesh breakdown,
85
+ * say) that sum is an upper bound, not a distinct count; the caveats say so.
86
+ * - `events` — the sum of the first column declared `unit: "count"`. Declaration
87
+ * order matters: registry rows lead with the denominator (`total_clicks`
88
+ * before `dead_clicks`, `samples` before the percentiles computed from them).
89
+ *
90
+ * `null` means the metric declares nothing that could answer the question.
91
+ */
92
+ export function sampleSizeOf(metric, rows) {
93
+ const sessions = metric.grain === "session"
94
+ ? rows.length
95
+ : sumColumn(rows, firstColumnWithUnit(metric, "sessions")?.[0]);
96
+ return { sessions, events: sumColumn(rows, firstColumnWithUnit(metric, "count")?.[0]) };
97
+ }
98
+ //# sourceMappingURL=columns.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"columns.js","sourceRoot":"","sources":["../../../src/query/summary/columns.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Deterministic, locale-independent number and unit rendering for the `reading`
3
+ * sentence (design sketch §B.1).
4
+ *
5
+ * `toLocaleString` is deliberately avoided: a summary is read by an agent and
6
+ * compared in tests, so "1,234" must not become "1.234" because the collector
7
+ * happens to run in a different locale. Every helper here also has to be
8
+ * **total** — a `reading` may never contain `undefined` or `NaN`, so a missing
9
+ * or non-finite value renders as `n/a` rather than leaking through.
10
+ */
11
+ import type { ColumnUnit } from "@uptimizr/metrics";
12
+ /** Whether values in a column with this unit may be added together. */
13
+ export declare function isAdditiveUnit(unit: ColumnUnit | null | undefined): boolean;
14
+ /**
15
+ * Render a number for prose: thousands-grouped, with at most two decimals and
16
+ * only when they carry information. Non-finite and missing values become `n/a`.
17
+ */
18
+ export declare function formatNumber(value: number | null | undefined): string;
19
+ /** Render a 0..1 share as a percentage with one decimal; `n/a` when unknown. */
20
+ export declare function formatShare(share: number | null | undefined): string;
21
+ /** The suffix that turns a bare number into a quantity, e.g. `450` → `450 ms`. */
22
+ export declare function unitSuffix(unit: ColumnUnit | null | undefined): string;
23
+ /** A number rendered with its unit, e.g. `2,210` or `450 ms`. */
24
+ export declare function formatQuantity(value: number | null | undefined, unit: ColumnUnit | null | undefined): string;
25
+ /** The plural noun for one row of a metric, used in prose. */
26
+ export declare function grainNoun(grain: string, count: number): string;
27
+ /** Turn a column name into readable prose (`avg_js_heap_bytes` → `avg js heap bytes`). */
28
+ export declare function humanizeColumn(column: string): string;
29
+ /** Render a cell value for prose; objects and nullish values become `n/a`. */
30
+ export declare function formatLabel(value: unknown): string;
31
+ //# sourceMappingURL=format.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../../src/query/summary/format.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAkBpD,uEAAuE;AACvE,wBAAgB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAE3E;AAOD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CASrE;AAED,gFAAgF;AAChF,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAGpE;AAED,kFAAkF;AAClF,wBAAgB,UAAU,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,CAsBtE;AAED,iEAAiE;AACjE,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAChC,IAAI,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,GAClC,MAAM,CAGR;AAED,8DAA8D;AAC9D,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAiB9D;AAED,0FAA0F;AAC1F,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAErD;AAED,8EAA8E;AAC9E,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAMlD"}
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Deterministic, locale-independent number and unit rendering for the `reading`
3
+ * sentence (design sketch §B.1).
4
+ *
5
+ * `toLocaleString` is deliberately avoided: a summary is read by an agent and
6
+ * compared in tests, so "1,234" must not become "1.234" because the collector
7
+ * happens to run in a different locale. Every helper here also has to be
8
+ * **total** — a `reading` may never contain `undefined` or `NaN`, so a missing
9
+ * or non-finite value renders as `n/a` rather than leaking through.
10
+ */
11
+ /**
12
+ * Units whose values may be summed across rows. Shares, `total` and the cluster
13
+ * weights are only reported for these: adding FPS, a ratio or a bin index
14
+ * together produces a number that means nothing, and a `share` derived from it
15
+ * would be actively misleading.
16
+ */
17
+ const ADDITIVE_UNITS = new Set([
18
+ "count",
19
+ "sessions",
20
+ "ms",
21
+ "s",
22
+ "bytes",
23
+ "world-units",
24
+ "radians",
25
+ ]);
26
+ /** Whether values in a column with this unit may be added together. */
27
+ export function isAdditiveUnit(unit) {
28
+ return unit != null && ADDITIVE_UNITS.has(unit);
29
+ }
30
+ /** Insert thousands separators into the integer part of a rendered number. */
31
+ function group(digits) {
32
+ return digits.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
33
+ }
34
+ /**
35
+ * Render a number for prose: thousands-grouped, with at most two decimals and
36
+ * only when they carry information. Non-finite and missing values become `n/a`.
37
+ */
38
+ export function formatNumber(value) {
39
+ if (value == null || !Number.isFinite(value))
40
+ return "n/a";
41
+ const rounded = Math.abs(value) >= 1000 ? Math.round(value) : Math.round(value * 100) / 100;
42
+ const sign = rounded < 0 ? "-" : "";
43
+ const text = Math.abs(rounded).toFixed(Number.isInteger(rounded) ? 0 : 2);
44
+ const [whole = "0", fraction] = text.split(".");
45
+ // Trim a trailing zero decimal ("1.50" → "1.5"), but never the whole part.
46
+ const trimmed = fraction == null ? "" : `.${fraction.replace(/0$/, "")}`;
47
+ return `${sign}${group(whole)}${trimmed === "." ? "" : trimmed}`;
48
+ }
49
+ /** Render a 0..1 share as a percentage with one decimal; `n/a` when unknown. */
50
+ export function formatShare(share) {
51
+ if (share == null || !Number.isFinite(share))
52
+ return "n/a";
53
+ return `${formatNumber(Math.round(share * 1000) / 10)}%`;
54
+ }
55
+ /** The suffix that turns a bare number into a quantity, e.g. `450` → `450 ms`. */
56
+ export function unitSuffix(unit) {
57
+ switch (unit) {
58
+ case "sessions":
59
+ return " sessions";
60
+ case "ms":
61
+ return " ms";
62
+ case "s":
63
+ return " s";
64
+ case "fps":
65
+ return " FPS";
66
+ case "percent":
67
+ return "%";
68
+ case "world-units":
69
+ return " units";
70
+ case "radians":
71
+ return " rad";
72
+ case "bytes":
73
+ return " bytes";
74
+ default:
75
+ // count / ratio / index / epoch-ms / id / label / timestamp read better bare.
76
+ return "";
77
+ }
78
+ }
79
+ /** A number rendered with its unit, e.g. `2,210` or `450 ms`. */
80
+ export function formatQuantity(value, unit) {
81
+ const rendered = formatNumber(value);
82
+ return rendered === "n/a" ? rendered : `${rendered}${unitSuffix(unit)}`;
83
+ }
84
+ /** The plural noun for one row of a metric, used in prose. */
85
+ export function grainNoun(grain, count) {
86
+ const singular = grain === "mesh"
87
+ ? "mesh"
88
+ : grain === "scene"
89
+ ? "scene"
90
+ : grain === "session"
91
+ ? "session"
92
+ : grain === "bucket"
93
+ ? "bucket"
94
+ : grain === "bin"
95
+ ? "bin"
96
+ : grain === "voxel"
97
+ ? "voxel"
98
+ : "row";
99
+ if (count === 1)
100
+ return singular;
101
+ return singular === "mesh" ? "meshes" : `${singular}s`;
102
+ }
103
+ /** Turn a column name into readable prose (`avg_js_heap_bytes` → `avg js heap bytes`). */
104
+ export function humanizeColumn(column) {
105
+ return column.replace(/_/g, " ");
106
+ }
107
+ /** Render a cell value for prose; objects and nullish values become `n/a`. */
108
+ export function formatLabel(value) {
109
+ if (value == null)
110
+ return "(unattributed)";
111
+ if (typeof value === "number")
112
+ return formatNumber(value);
113
+ if (typeof value === "string")
114
+ return value.length === 0 ? "(unattributed)" : value;
115
+ if (typeof value === "boolean")
116
+ return value ? "true" : "false";
117
+ return "n/a";
118
+ }
119
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.js","sourceRoot":"","sources":["../../../src/query/summary/format.ts"],"names":[],"mappings":"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"}