thatcher 1.0.4 → 1.0.6

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 (62) hide show
  1. package/package.json +18 -1
  2. package/src/app/api/debug/[[...path]]/route.js +217 -0
  3. package/src/app/api/formula/[[...path]]/route.js +142 -0
  4. package/src/cli.js +0 -1
  5. package/src/config/spec-helpers.js +6 -0
  6. package/src/engine.server.js +2 -2
  7. package/src/index.js +21 -11
  8. package/src/lib/auth-middleware.js +4 -2
  9. package/src/lib/busybase-adapter.js +162 -0
  10. package/src/lib/config-field-helpers.js +1 -1
  11. package/src/lib/config-generator-engine.js +24 -0
  12. package/src/lib/crud-handlers.js +21 -1
  13. package/src/lib/database-core.js +4 -7
  14. package/src/lib/database-migrations.js +254 -0
  15. package/src/lib/date-utils.js +164 -0
  16. package/src/lib/debug-registry.js +165 -0
  17. package/src/lib/error-handler.js +78 -0
  18. package/src/lib/export-sink.js +226 -0
  19. package/src/lib/field-iterator.js +126 -0
  20. package/src/lib/hyperformula-service.js +304 -0
  21. package/src/lib/metrics-collector.js +196 -0
  22. package/src/lib/observability-bootstrap.js +121 -0
  23. package/src/lib/perf-profiler.js +238 -0
  24. package/src/lib/query-engine.js +47 -0
  25. package/src/lib/query-string-adapter.js +97 -2
  26. package/src/lib/request-tracing.js +216 -0
  27. package/src/lib/response-formatter.js +0 -2
  28. package/src/lib/route-resolver.js +10 -0
  29. package/src/lib/status-helpers.js +128 -0
  30. package/src/lib/tracing.js +287 -0
  31. package/src/lib/utils.js +81 -0
  32. package/src/lib/validate.js +124 -1
  33. package/src/lib/xstate-workflow-engine.js +478 -0
  34. package/src/plugins/index.js +27 -0
  35. package/src/server/server.js +23 -4
  36. package/src/services/permission.service.js +6 -2
  37. package/src/ui/advanced-search-renderer.js +3 -3
  38. package/src/ui/advanced-widgets.js +4 -4
  39. package/src/ui/common-handlers.js +4 -2
  40. package/src/ui/dashboard-renderer.js +4 -4
  41. package/src/ui/engagement-cards.js +10 -10
  42. package/src/ui/engagement-grid-renderer.js +11 -8
  43. package/src/ui/entity-renderer.js +4 -2
  44. package/src/ui/event-delegation.js +348 -8
  45. package/src/ui/file-dialogs.js +9 -9
  46. package/src/ui/format-helpers.js +37 -3
  47. package/src/ui/highlight-threading-renderer.js +9 -9
  48. package/src/ui/job-management-renderer.js +6 -6
  49. package/src/ui/monitoring-dashboard-client.js +17 -3
  50. package/src/ui/notifications-renderer.js +6 -7
  51. package/src/ui/page-handler.js +9 -0
  52. package/src/ui/render-helpers.js +8 -1
  53. package/src/ui/review-comparison-renderer.js +1 -1
  54. package/src/ui/review-detail-renderer.js +6 -6
  55. package/src/ui/review-mwr-renderer.js +13 -10
  56. package/src/ui/review-widgets.js +5 -5
  57. package/src/ui/rfi-detail-renderer.js +9 -9
  58. package/src/ui/settings-renderer-advanced.js +13 -13
  59. package/src/ui/settings-renderer-teams.js +10 -9
  60. package/src/ui/settings-renderer.js +25 -24
  61. package/src/ui/spacing-system.js +8 -4
  62. package/src/ui/widgets.js +2 -2
@@ -0,0 +1,304 @@
1
+ /**
2
+ * HyperFormula Compute Engine - Spreadsheet formula evaluation for Thatcher
3
+ * Provides calculated fields, business rules, and data transformations
4
+ */
5
+
6
+ import { HyperFormula } from 'hyperformula';
7
+ import { createLogger } from './logger.js';
8
+
9
+ const logger = createLogger('[HyperFormula]');
10
+
11
+ const _sheets = new Map();
12
+ const _evaluationLog = [];
13
+ const MAX_LOG_SIZE = 1000;
14
+
15
+ export class HyperFormulaService {
16
+ constructor(options = {}) {
17
+ this.options = {
18
+ licenseKey: options.licenseKey || 'gpl-v3',
19
+ maxRows: options.maxRows || 100000,
20
+ maxColumns: options.maxColumns || 1000,
21
+ useColumnIndex: options.useColumnIndex !== false,
22
+ useStats: options.useStats !== false,
23
+ ...options,
24
+ };
25
+ this._instance = null;
26
+ this._customFunctions = new Map();
27
+ this._sheetMeta = new Map();
28
+ }
29
+
30
+ async init() {
31
+ if (this._instance) return this;
32
+
33
+ this._instance = HyperFormula.buildEmpty({
34
+ licenseKey: this.options.licenseKey,
35
+ maxRows: this.options.maxRows,
36
+ maxColumns: this.options.maxColumns,
37
+ useColumnIndex: this.options.useColumnIndex,
38
+ useStats: this.options.useStats,
39
+ });
40
+
41
+ this._registerCustomFunctions();
42
+
43
+ if (globalThis.__debug__) {
44
+ globalThis.__debug__.expose('formula', {
45
+ sheets: () => this.listSheets(),
46
+ sheet: (name) => this.getSheetInfo(name),
47
+ instance: () => this._instance,
48
+ stats: () => this.getStats(),
49
+ log: () => [..._evaluationLog],
50
+ customFunctions: () => Array.from(this._customFunctions.keys()),
51
+ }, 'HyperFormula Service');
52
+ }
53
+
54
+ logger.info('HyperFormula initialized');
55
+ return this;
56
+ }
57
+
58
+ _registerCustomFunctions() {
59
+ const thatcherFunctions = [
60
+ { name: 'DB_LOOKUP', parameters: 4 },
61
+ { name: 'DB_COUNT', parameters: 4 },
62
+ { name: 'DB_SUM', parameters: 4 },
63
+ { name: 'DB_FILTER', parameters: 3 },
64
+ { name: 'WORKFLOW_STATE', parameters: 1 },
65
+ { name: 'USER_ROLE', parameters: 0 },
66
+ { name: 'NOW_TS', parameters: 0 },
67
+ { name: 'UUID', parameters: 0 },
68
+ ];
69
+
70
+ for (const fn of thatcherFunctions) {
71
+ this._customFunctions.set(fn.name, fn);
72
+ }
73
+ }
74
+
75
+ createSheet(name, data = []) {
76
+ if (!this._instance) throw new Error('HyperFormula not initialized');
77
+
78
+ this._instance.addSheet(name);
79
+ const sheetId = this._instance.getSheetId(name);
80
+
81
+ if (data.length > 0) {
82
+ this._instance.setSheetContent(sheetId, data);
83
+ }
84
+
85
+ this._sheetMeta.set(name, {
86
+ id: sheetId,
87
+ created: Date.now(),
88
+ rowCount: data.length,
89
+ colCount: data.length > 0 ? Math.max(...data.map(r => r.length)) : 0,
90
+ });
91
+
92
+ logger.info('Sheet created', { name, sheetId });
93
+ return { name, sheetId };
94
+ }
95
+
96
+ removeSheet(name) {
97
+ const meta = this._sheetMeta.get(name);
98
+ if (!meta) throw new Error(`Sheet "${name}" not found`);
99
+
100
+ this._instance.removeSheet(meta.id);
101
+ this._sheetMeta.delete(name);
102
+ logger.info('Sheet removed', { name });
103
+ }
104
+
105
+ renameSheet(oldName, newName) {
106
+ const meta = this._sheetMeta.get(oldName);
107
+ if (!meta) throw new Error(`Sheet "${oldName}" not found`);
108
+
109
+ this._instance.renameSheet(meta.id, newName);
110
+ this._sheetMeta.delete(oldName);
111
+ this._sheetMeta.set(newName, { ...meta, name: newName });
112
+ }
113
+
114
+ setCellContents(name, cellAddress, value) {
115
+ const meta = this._sheetMeta.get(name);
116
+ if (!meta) throw new Error(`Sheet "${name}" not found`);
117
+
118
+ const start = performance.now();
119
+ this._instance.setCellContents({ sheet: meta.id, ...cellAddress }, value);
120
+ const duration = performance.now() - start;
121
+
122
+ this._logEvaluation('setCellContents', { sheet: name, cell: cellAddress, value, duration });
123
+ }
124
+
125
+ getCellValue(name, cellAddress) {
126
+ const meta = this._sheetMeta.get(name);
127
+ if (!meta) throw new Error(`Sheet "${name}" not found`);
128
+
129
+ const start = performance.now();
130
+ const value = this._instance.getCellValue({ sheet: meta.id, ...cellAddress });
131
+ const duration = performance.now() - start;
132
+
133
+ this._logEvaluation('getCellValue', { sheet: name, cell: cellAddress, value, duration });
134
+ return value;
135
+ }
136
+
137
+ getSheetValues(name) {
138
+ const meta = this._sheetMeta.get(name);
139
+ if (!meta) throw new Error(`Sheet "${name}" not found`);
140
+
141
+ const start = performance.now();
142
+ const values = this._instance.getSheetValues(meta.id);
143
+ const duration = performance.now() - start;
144
+
145
+ this._logEvaluation('getSheetValues', { sheet: name, rowCount: values.length, duration });
146
+ return values;
147
+ }
148
+
149
+ getRangeValues(name, startRow, startCol, endRow, endCol) {
150
+ const meta = this._sheetMeta.get(name);
151
+ if (!meta) throw new Error(`Sheet "${name}" not found`);
152
+
153
+ const start = performance.now();
154
+ const values = this._instance.getRangeValue({
155
+ sheet: meta.id,
156
+ startRow,
157
+ startCol,
158
+ endRow,
159
+ endCol,
160
+ });
161
+ const duration = performance.now() - start;
162
+
163
+ this._logEvaluation('getRangeValues', { sheet: name, range: { startRow, startCol, endRow, endCol }, duration });
164
+ return values;
165
+ }
166
+
167
+ setSheetContent(name, data) {
168
+ const meta = this._sheetMeta.get(name);
169
+ if (!meta) throw new Error(`Sheet "${name}" not found`);
170
+
171
+ const start = performance.now();
172
+ this._instance.setSheetContent(meta.id, data);
173
+ const duration = performance.now() - start;
174
+
175
+ this._sheetMeta.set(name, {
176
+ ...meta,
177
+ rowCount: data.length,
178
+ colCount: data.length > 0 ? Math.max(...data.map(r => r.length)) : 0,
179
+ });
180
+
181
+ this._logEvaluation('setSheetContent', { sheet: name, rowCount: data.length, duration });
182
+ }
183
+
184
+ evaluateFormula(formula) {
185
+ const start = performance.now();
186
+
187
+ const tempSheet = this.createSheet(`_temp_${Date.now()}`);
188
+ try {
189
+ this.setCellContents(tempSheet.name, { row: 0, col: 0 }, [[formula]]);
190
+ const result = this.getCellValue(tempSheet.name, { row: 0, col: 0 });
191
+ const duration = performance.now() - start;
192
+
193
+ this._logEvaluation('evaluateFormula', { formula, result, duration });
194
+ return { result, duration };
195
+ } finally {
196
+ this.removeSheet(tempSheet.name);
197
+ }
198
+ }
199
+
200
+ validateFormula(formula) {
201
+ try {
202
+ const ast = HyperFormula.buildFromArray([[formula]], { licenseKey: 'gpl-v3' });
203
+ const errors = ast.getAllFormulas().filter(f => f?.error);
204
+ return { valid: errors.length === 0, errors };
205
+ } catch (e) {
206
+ return { valid: false, errors: [e.message] };
207
+ }
208
+ }
209
+
210
+ getCellFormula(name, cellAddress) {
211
+ const meta = this._sheetMeta.get(name);
212
+ if (!meta) throw new Error(`Sheet "${name}" not found`);
213
+
214
+ return this._instance.getCellFormula({ sheet: meta.id, ...cellAddress });
215
+ }
216
+
217
+ getDependencies(name, cellAddress) {
218
+ const meta = this._sheetMeta.get(name);
219
+ if (!meta) throw new Error(`Sheet "${name}" not found`);
220
+
221
+ return this._instance.dependencies.getDependencies({ sheet: meta.id, ...cellAddress });
222
+ }
223
+
224
+ undo() {
225
+ return this._instance.undo();
226
+ }
227
+
228
+ redo() {
229
+ return this._instance.redo();
230
+ }
231
+
232
+ listSheets() {
233
+ return Array.from(this._sheetMeta.entries()).map(([name, meta]) => ({
234
+ name,
235
+ ...meta,
236
+ }));
237
+ }
238
+
239
+ getSheetInfo(name) {
240
+ const meta = this._sheetMeta.get(name);
241
+ if (!meta) return null;
242
+
243
+ const dimensions = this._instance.getSheetDimensions(meta.id);
244
+ return {
245
+ ...meta,
246
+ dimensions,
247
+ filledRange: this._instance.getFilledRange(meta.id),
248
+ };
249
+ }
250
+
251
+ getStats() {
252
+ return {
253
+ sheetCount: this._sheetMeta.size,
254
+ customFunctions: this._customFunctions.size,
255
+ evaluationLogSize: _evaluationLog.length,
256
+ version: HyperFormula.version,
257
+ };
258
+ }
259
+
260
+ _logEvaluation(operation, details) {
261
+ _evaluationLog.push({
262
+ operation,
263
+ timestamp: Date.now(),
264
+ ...details,
265
+ });
266
+
267
+ while (_evaluationLog.length > MAX_LOG_SIZE) {
268
+ _evaluationLog.shift();
269
+ }
270
+ }
271
+
272
+ getInstance() {
273
+ return this._instance;
274
+ }
275
+
276
+ async close() {
277
+ if (this._instance) {
278
+ this._instance.destroy();
279
+ this._instance = null;
280
+ }
281
+ this._sheetMeta.clear();
282
+ _evaluationLog.length = 0;
283
+ }
284
+ }
285
+
286
+ let _hfService = null;
287
+
288
+ export async function createHyperFormulaService(options = {}) {
289
+ if (!_hfService) {
290
+ _hfService = new HyperFormulaService(options);
291
+ await _hfService.init();
292
+ }
293
+ return _hfService;
294
+ }
295
+
296
+ export function getHyperFormulaService() {
297
+ return _hfService;
298
+ }
299
+
300
+ export function getHyperFormulaInstance() {
301
+ return _hfService?._instance;
302
+ }
303
+
304
+ export default HyperFormulaService;
@@ -1,5 +1,12 @@
1
1
  /**
2
2
  * Metrics Collector - Request metrics and stats
3
+ *
4
+ * Thatcher keeps its original lightweight request-summary API
5
+ * (recordRequest/getMetrics/getSummary/resetMetrics) for backward compat.
6
+ * Merged in the richer per-channel collector API consumed by
7
+ * resource-monitor / db-monitor / request-tracker / metrics & health routes
8
+ * (recordError/recordDatabase/recordResource/recordCustom, getAllMetrics,
9
+ * clearMetrics, getStats, and the per-channel getters).
3
10
  */
4
11
 
5
12
  const metrics = {
@@ -45,6 +52,15 @@ export function recordRequest(endpoint, method, durationMs, statusCode) {
45
52
  if (statusCode >= 400) {
46
53
  metrics.errors.push(record);
47
54
  }
55
+
56
+ // Mirror into the per-channel request collector used by getAllMetrics().
57
+ const key = `${method}:${endpoint}`;
58
+ if (!channels.requests.has(key)) {
59
+ channels.requests.set(key, []);
60
+ }
61
+ const arr = channels.requests.get(key);
62
+ arr.push({ duration: durationMs, status: statusCode, ts: Date.now() });
63
+ if (arr.length > 1000) arr.shift();
48
64
  }
49
65
 
50
66
  /**
@@ -99,4 +115,184 @@ export function resetMetrics() {
99
115
  metrics.errors = [];
100
116
  _requestCount = 0;
101
117
  metrics.startTime = Date.now();
118
+ clearMetrics();
119
+ }
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // Per-channel collector API (ported from moonlanding feature set)
123
+ // Consumed by resource-monitor.js, db-monitor.js, request-tracker.js,
124
+ // api/metrics/route.js, api/health/route.js, monitoring-init.js.
125
+ // ---------------------------------------------------------------------------
126
+
127
+ const channels = {
128
+ requests: new Map(),
129
+ errors: new Map(),
130
+ database: new Map(),
131
+ resources: new Map(),
132
+ custom: new Map(),
133
+ };
134
+
135
+ const errorCounts = new Map();
136
+ const resourceSamples = [];
137
+
138
+ /**
139
+ * Record an error against an endpoint.
140
+ */
141
+ export function recordError(path, method, error, stack) {
142
+ const key = `${method}:${path}`;
143
+ if (!channels.errors.has(key)) {
144
+ channels.errors.set(key, []);
145
+ }
146
+ const arr = channels.errors.get(key);
147
+ arr.push({ error, stack, ts: Date.now() });
148
+ if (arr.length > 100) arr.shift();
149
+
150
+ const countKey = error || 'unknown';
151
+ errorCounts.set(countKey, (errorCounts.get(countKey) || 0) + 1);
152
+ }
153
+
154
+ /**
155
+ * Record a database operation timing.
156
+ */
157
+ export function recordDatabase(operation, duration, query) {
158
+ const key = operation;
159
+ if (!channels.database.has(key)) {
160
+ channels.database.set(key, []);
161
+ }
162
+ const arr = channels.database.get(key);
163
+ arr.push({ duration, query: query?.substring(0, 200), ts: Date.now() });
164
+ if (arr.length > 1000) arr.shift();
165
+ }
166
+
167
+ /**
168
+ * Record a resource sample (cpu/memory/disk).
169
+ */
170
+ export function recordResource(cpu, memory, disk) {
171
+ resourceSamples.push({ cpu, memory, disk, ts: Date.now() });
172
+ if (resourceSamples.length > 1000) resourceSamples.shift();
173
+ }
174
+
175
+ /**
176
+ * Record a custom metric value with optional tags.
177
+ */
178
+ export function recordCustom(name, value, tags = {}) {
179
+ const key = name;
180
+ if (!channels.custom.has(key)) {
181
+ channels.custom.set(key, []);
182
+ }
183
+ const arr = channels.custom.get(key);
184
+ arr.push({ value, tags, ts: Date.now() });
185
+ if (arr.length > 1000) arr.shift();
186
+ }
187
+
188
+ function getPercentile(values, percentile) {
189
+ if (values.length === 0) return 0;
190
+ const sorted = [...values].sort((a, b) => a - b);
191
+ const index = Math.ceil(sorted.length * percentile) - 1;
192
+ return sorted[Math.max(0, index)];
193
+ }
194
+
195
+ /**
196
+ * Compute count/min/max/avg/p50/p95/p99 stats over a data array.
197
+ */
198
+ export function getStats(dataArray, field = 'duration') {
199
+ if (!dataArray || dataArray.length === 0) {
200
+ return { count: 0, min: 0, max: 0, avg: 0, p50: 0, p95: 0, p99: 0 };
201
+ }
202
+
203
+ const values = dataArray.map(d => d[field] || 0).filter(v => typeof v === 'number');
204
+ if (values.length === 0) {
205
+ return { count: 0, min: 0, max: 0, avg: 0, p50: 0, p95: 0, p99: 0 };
206
+ }
207
+
208
+ const sum = values.reduce((a, b) => a + b, 0);
209
+ return {
210
+ count: values.length,
211
+ min: Math.min(...values),
212
+ max: Math.max(...values),
213
+ avg: sum / values.length,
214
+ p50: getPercentile(values, 0.50),
215
+ p95: getPercentile(values, 0.95),
216
+ p99: getPercentile(values, 0.99),
217
+ };
218
+ }
219
+
220
+ export function getRequestMetrics() {
221
+ const result = {};
222
+ for (const [key, data] of channels.requests.entries()) {
223
+ result[key] = getStats(data, 'duration');
224
+ }
225
+ return result;
226
+ }
227
+
228
+ export function getErrorMetrics() {
229
+ const result = {
230
+ byEndpoint: {},
231
+ byCause: Object.fromEntries(errorCounts),
232
+ };
233
+ for (const [key, data] of channels.errors.entries()) {
234
+ result.byEndpoint[key] = { count: data.length, recent: data.slice(-5) };
235
+ }
236
+ return result;
237
+ }
238
+
239
+ export function getDatabaseMetrics() {
240
+ const result = {};
241
+ for (const [key, data] of channels.database.entries()) {
242
+ result[key] = getStats(data, 'duration');
243
+ }
244
+ return result;
245
+ }
246
+
247
+ export function getResourceMetrics() {
248
+ if (resourceSamples.length === 0) return null;
249
+ const cpuVals = resourceSamples.map(s => s.cpu).filter(v => v != null);
250
+ const memVals = resourceSamples.map(s => s.memory).filter(v => v != null);
251
+ const diskVals = resourceSamples.map(s => s.disk).filter(v => v != null);
252
+
253
+ return {
254
+ cpu: cpuVals.length > 0 ? getStats(cpuVals.map(v => ({ duration: v }))) : null,
255
+ memory: memVals.length > 0 ? getStats(memVals.map(v => ({ duration: v }))) : null,
256
+ disk: diskVals.length > 0 ? getStats(diskVals.map(v => ({ duration: v }))) : null,
257
+ };
258
+ }
259
+
260
+ /**
261
+ * Aggregate snapshot across all channels.
262
+ */
263
+ export function getAllMetrics() {
264
+ return {
265
+ requests: getRequestMetrics(),
266
+ errors: getErrorMetrics(),
267
+ database: getDatabaseMetrics(),
268
+ resources: getResourceMetrics(),
269
+ custom: Object.fromEntries(
270
+ Array.from(channels.custom.entries()).map(([k, v]) => [k, getStats(v, 'value')])
271
+ ),
272
+ timestamp: Date.now(),
273
+ };
274
+ }
275
+
276
+ /**
277
+ * Clear the per-channel collector state.
278
+ */
279
+ export function clearMetrics() {
280
+ channels.requests.clear();
281
+ channels.errors.clear();
282
+ channels.database.clear();
283
+ channels.custom.clear();
284
+ resourceSamples.length = 0;
285
+ errorCounts.clear();
286
+ }
287
+
288
+ if (typeof globalThis !== 'undefined') {
289
+ globalThis.__metrics = {
290
+ recordRequest,
291
+ recordError,
292
+ recordDatabase,
293
+ recordResource,
294
+ recordCustom,
295
+ getAllMetrics,
296
+ clearMetrics,
297
+ };
102
298
  }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Observability Bootstrap - Initializes all observability subsystems on server startup
3
+ * Starts tracing, registers debug exposures, begins metric collection
4
+ */
5
+
6
+ import { tracer } from './tracing.js';
7
+ import { perfProfiler } from './perf-profiler.js';
8
+ import { debugRegistry } from './debug-registry.js';
9
+ import { createExportSink } from './export-sink.js';
10
+ import { withHookTracing } from './request-tracing.js';
11
+ import { hookEngine } from './hook-engine.js';
12
+ import { createLogger } from './logger.js';
13
+
14
+ const logger = createLogger('[ObservabilityBootstrap]');
15
+
16
+ export async function bootstrapObservability(options = {}) {
17
+ const results = {
18
+ tracing: false,
19
+ profiler: false,
20
+ registry: false,
21
+ export: false,
22
+ hookTracing: false,
23
+ errors: [],
24
+ };
25
+
26
+ try {
27
+ debugRegistry.expose('system', {
28
+ pid: () => process.pid,
29
+ uptime: () => process.uptime(),
30
+ memory: () => process.memoryUsage(),
31
+ cpuUsage: () => process.cpuUsage(),
32
+ version: () => process.version,
33
+ platform: () => process.platform,
34
+ }, 'System information');
35
+
36
+ results.registry = true;
37
+ logger.info('Debug registry initialized');
38
+ } catch (error) {
39
+ results.errors.push({ subsystem: 'registry', error: error.message });
40
+ logger.error('Registry bootstrap failed', { error: error.message });
41
+ }
42
+
43
+ try {
44
+ if (globalThis.__debug__) {
45
+ globalThis.__debug__.expose('tracing', {
46
+ activeSpans: () => tracer.getActiveSpans(),
47
+ recentTraces: (limit) => tracer.getRecentTraces(limit),
48
+ traceById: (id) => tracer.getTraceById(id),
49
+ stats: () => tracer.getStats(),
50
+ currentTraceId: () => tracer.getCurrentTraceId(),
51
+ }, 'Tracing Core');
52
+ }
53
+
54
+ results.tracing = true;
55
+ logger.info('Tracing initialized');
56
+ } catch (error) {
57
+ results.errors.push({ subsystem: 'tracing', error: error.message });
58
+ logger.error('Tracing bootstrap failed', { error: error.message });
59
+ }
60
+
61
+ try {
62
+ if (globalThis.__debug__) {
63
+ globalThis.__debug__.expose('perf', {
64
+ stats: (op) => op ? perfProfiler.getStats(op) : perfProfiler.getAllStats(),
65
+ slow: () => perfProfiler.getSlowOperations(),
66
+ regressions: () => perfProfiler.detectRegressions(),
67
+ alerts: () => perfProfiler.getAlerts(),
68
+ thresholds: () => Object.fromEntries(perfProfiler._thresholds),
69
+ }, 'Performance Profiler');
70
+ }
71
+
72
+ results.profiler = true;
73
+ logger.info('Performance profiler initialized');
74
+ } catch (error) {
75
+ results.errors.push({ subsystem: 'profiler', error: error.message });
76
+ logger.error('Profiler bootstrap failed', { error: error.message });
77
+ }
78
+
79
+ try {
80
+ const exportTarget = options.exportTarget || process.env.OBSERVABILITY_EXPORT_TARGET;
81
+ if (exportTarget) {
82
+ await createExportSink({
83
+ target: exportTarget,
84
+ url: options.exportUrl || process.env.OBSERVABILITY_EXPORT_URL,
85
+ batchSize: options.batchSize,
86
+ batchIntervalMs: options.batchIntervalMs,
87
+ });
88
+ results.export = true;
89
+ logger.info('Export sink initialized', { target: exportTarget });
90
+ }
91
+ } catch (error) {
92
+ results.errors.push({ subsystem: 'export', error: error.message });
93
+ logger.error('Export sink bootstrap failed', { error: error.message });
94
+ }
95
+
96
+ try {
97
+ withHookTracing(hookEngine);
98
+ results.hookTracing = true;
99
+ logger.info('Hook tracing initialized');
100
+ } catch (error) {
101
+ results.errors.push({ subsystem: 'hookTracing', error: error.message });
102
+ logger.error('Hook tracing bootstrap failed', { error: error.message });
103
+ }
104
+
105
+ return results;
106
+ }
107
+
108
+ export async function shutdownObservability() {
109
+ const { getExportSink } = await import('./export-sink.js');
110
+ const sink = getExportSink();
111
+ if (sink) {
112
+ await sink.close();
113
+ }
114
+
115
+ tracer.clear();
116
+ perfProfiler.clear();
117
+
118
+ logger.info('Observability shutdown complete');
119
+ }
120
+
121
+ export default bootstrapObservability;