thatcher 1.0.7 → 1.0.34

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 (174) hide show
  1. package/README.md +4 -16
  2. package/package.json +12 -18
  3. package/src/adapters/google-auth.js +4 -43
  4. package/src/adapters/google-drive.js +12 -67
  5. package/src/app/api/audit/dashboard/route.js +14 -11
  6. package/src/app/api/audit/logs/route.js +2 -2
  7. package/src/app/api/audit/permissions/[id]/route.js +6 -3
  8. package/src/app/api/audit/permissions/route.js +10 -7
  9. package/src/app/api/audit/permissions/stats/route.js +8 -5
  10. package/src/app/api/audit/route.js +4 -4
  11. package/src/app/api/audit/stats/route.js +1 -1
  12. package/src/app/api/auth/google/callback/route.js +5 -2
  13. package/src/app/api/auth/google/route.js +4 -1
  14. package/src/app/api/auth/login/route.js +5 -2
  15. package/src/app/api/auth/mwr-bridge/route.js +11 -21
  16. package/src/app/api/auth/password-reset/route.js +15 -14
  17. package/src/app/api/cron/trigger/route.js +4 -1
  18. package/src/app/api/debug/[[...path]]/route.js +6 -5
  19. package/src/app/api/debug/sqlite/route.js +7 -12
  20. package/src/app/api/debug/sync/route.js +5 -5
  21. package/src/app/api/domains/route.js +4 -1
  22. package/src/app/api/email/allocate/batch/route.js +19 -29
  23. package/src/app/api/email/allocate/route.js +16 -17
  24. package/src/app/api/email/receive/route.js +31 -42
  25. package/src/app/api/email/send/route.js +28 -16
  26. package/src/app/api/email/unallocated/route.js +10 -13
  27. package/src/app/api/files/[id]/route.js +4 -1
  28. package/src/app/api/formula/[[...path]]/route.js +0 -4
  29. package/src/app/api/health/route.js +10 -9
  30. package/src/app/api/metrics/route.js +1 -1
  31. package/src/cli.js +4 -4
  32. package/src/config/config-loader.js +1 -21
  33. package/src/config/constants.js +0 -35
  34. package/src/config/env.js +0 -30
  35. package/src/config/spec-helpers.js +6 -95
  36. package/src/engine.js +23 -0
  37. package/src/engine.server.js +8 -78
  38. package/src/index.js +42 -67
  39. package/src/lib/alert-manager.js +7 -3
  40. package/src/lib/api-helpers.js +4 -3
  41. package/src/lib/api.js +10 -10
  42. package/src/lib/audit-logger-enhanced.js +77 -83
  43. package/src/lib/auth-middleware.js +1 -32
  44. package/src/lib/auth-route-helpers.js +5 -0
  45. package/src/lib/busybase-adapter.js +0 -5
  46. package/src/lib/busybase-audit-reads.js +142 -0
  47. package/src/lib/busybase-audit.js +41 -0
  48. package/src/lib/busybase-lucia-adapter.js +72 -0
  49. package/src/lib/busybase-store.js +338 -0
  50. package/src/lib/config-field-helpers.js +0 -17
  51. package/src/lib/config-generator-engine.js +19 -36
  52. package/src/lib/config-helpers.js +0 -21
  53. package/src/lib/crud-action-helpers.js +2 -2
  54. package/src/lib/crud-factory.js +1 -16
  55. package/src/lib/crud-handlers.js +17 -52
  56. package/src/lib/csrf-protection.js +5 -2
  57. package/src/lib/date-utils.js +3 -127
  58. package/src/lib/debug-registry.js +0 -5
  59. package/src/lib/domain-loader.js +4 -1
  60. package/src/lib/email-sender.js +25 -23
  61. package/src/lib/error-handler.js +5 -41
  62. package/src/lib/error-recovery.js +10 -7
  63. package/src/lib/error-resilience.js +7 -5
  64. package/src/lib/events-engine.js +9 -6
  65. package/src/lib/export-sink.js +0 -5
  66. package/src/lib/field-iterator.js +7 -76
  67. package/src/lib/field-registry.js +0 -22
  68. package/src/lib/generic-crud-handler.js +0 -16
  69. package/src/lib/hook-engine.js +5 -53
  70. package/src/lib/hot-reload/checkpoint.js +5 -2
  71. package/src/lib/hot-reload/promise-container.js +7 -3
  72. package/src/lib/hot-reload/route-wrapper.js +4 -1
  73. package/src/lib/hot-reload/supervisor.js +6 -3
  74. package/src/lib/hyperformula-service.js +0 -5
  75. package/src/lib/id-helpers.js +10 -0
  76. package/src/lib/index.js +4 -7
  77. package/src/lib/log-aggregator.js +0 -1
  78. package/src/lib/logger.js +0 -15
  79. package/src/lib/metrics-collector.js +2 -46
  80. package/src/lib/next-compat.js +15 -1
  81. package/src/lib/observability-bootstrap.js +0 -5
  82. package/src/lib/perf-monitor.js +4 -1
  83. package/src/lib/perf-profiler.js +0 -5
  84. package/src/lib/query-string-adapter.js +5 -47
  85. package/src/lib/realtime-server.js +4 -26
  86. package/src/lib/request-tracing.js +0 -5
  87. package/src/lib/resource-monitor.js +4 -1
  88. package/src/lib/response-formatter.js +0 -38
  89. package/src/lib/route-helpers.js +3 -1
  90. package/src/lib/safe-json.js +5 -1
  91. package/src/lib/server-bootstrap.js +10 -9
  92. package/src/lib/stage-pipeline.js +2 -2
  93. package/src/lib/state-transport-client.js +4 -1
  94. package/src/lib/static-server.js +2 -2
  95. package/src/lib/status-helpers.js +2 -51
  96. package/src/lib/tracing.js +0 -5
  97. package/src/lib/utils.js +6 -67
  98. package/src/lib/validate.js +5 -88
  99. package/src/lib/validators.js +0 -38
  100. package/src/lib/with-audit-logging.js +1 -1
  101. package/src/lib/workflow-engine.js +10 -77
  102. package/src/lib/xstate-workflow-engine.js +3 -9
  103. package/src/plugins/index.js +5 -3
  104. package/src/server/server.js +26 -27
  105. package/src/services/collaborator-role.service.js +21 -77
  106. package/src/services/email-sender.js +7 -34
  107. package/src/services/notification-engine.js +12 -33
  108. package/src/services/permission.service.js +10 -73
  109. package/src/ui/advanced-search-renderer.js +3 -2
  110. package/src/ui/advanced-widgets.js +1 -1
  111. package/src/ui/auth-pages.js +10 -9
  112. package/src/ui/auth-styles.js +1 -1
  113. package/src/ui/client.js +1 -1
  114. package/src/ui/component-engine.js +1 -1
  115. package/src/ui/dashboard-renderer.js +5 -5
  116. package/src/ui/dialog-engine.js +6 -9
  117. package/src/ui/dialog-factory.js +2 -2
  118. package/src/ui/engagement-cards.js +7 -7
  119. package/src/ui/engagement-dialogs.js +2 -3
  120. package/src/ui/engagement-grid-renderer.js +20 -19
  121. package/src/ui/entity-renderer.js +56 -42
  122. package/src/ui/file-dialogs.js +25 -9
  123. package/src/ui/flexup-report-renderer.js +21 -18
  124. package/src/ui/format-helpers.js +27 -28
  125. package/src/ui/highlight-threading-renderer.js +9 -9
  126. package/src/ui/job-management-renderer.js +18 -16
  127. package/src/ui/layout.js +51 -25
  128. package/src/ui/ml-console-renderer.js +15 -16
  129. package/src/ui/monitoring-dashboard-client.js +9 -8
  130. package/src/ui/notifications-renderer.js +11 -10
  131. package/src/ui/page-handler-admin.js +32 -23
  132. package/src/ui/page-handler-helpers.js +27 -23
  133. package/src/ui/page-handler-reviews.js +71 -35
  134. package/src/ui/page-handler-rfi.js +28 -14
  135. package/src/ui/page-handler.js +65 -48
  136. package/src/ui/picker-dialogs.js +26 -19
  137. package/src/ui/render-helpers.js +84 -46
  138. package/src/ui/review-comparison-renderer.js +8 -7
  139. package/src/ui/review-detail-panels.js +8 -9
  140. package/src/ui/review-detail-renderer.js +3 -3
  141. package/src/ui/review-detail-script.js +13 -12
  142. package/src/ui/review-mwr-renderer.js +6 -5
  143. package/src/ui/review-renderer.js +7 -6
  144. package/src/ui/review-widgets.js +3 -3
  145. package/src/ui/rfi-detail-renderer.js +37 -20
  146. package/src/ui/rfi-report-renderer.js +14 -9
  147. package/src/ui/settings-renderer-advanced.js +21 -18
  148. package/src/ui/settings-renderer-advanced2.js +20 -19
  149. package/src/ui/settings-renderer-teams.js +4 -4
  150. package/src/ui/settings-renderer.js +4 -4
  151. package/src/ui/spacing-system.js +5 -2
  152. package/src/ui/standalone-login.js +2 -2
  153. package/src/ui/styles.css +2 -1
  154. package/src/ui/styles2.css +75 -5
  155. package/src/ui/test-page.js +4 -4
  156. package/src/ui/widgets.js +20 -15
  157. package/src/lib/audit-logger.js +0 -193
  158. package/src/lib/database-core.js +0 -255
  159. package/src/lib/database-migrations.js +0 -350
  160. package/src/lib/db-backup.js +0 -97
  161. package/src/lib/error-boundary.js +0 -134
  162. package/src/lib/errors.js +0 -69
  163. package/src/lib/health-monitor.js +0 -134
  164. package/src/lib/index-optimizer.js +0 -96
  165. package/src/lib/query-engine-write.js +0 -221
  166. package/src/lib/query-engine.js +0 -446
  167. package/src/ui/collaboration-dialogs.js +0 -31
  168. package/src/ui/global-tags.js +0 -144
  169. package/src/ui/idle-logout.js +0 -156
  170. package/src/ui/letter-dialogs.js +0 -37
  171. package/src/ui/password-reset-page.js +0 -146
  172. package/src/ui/perf-renderer.js +0 -71
  173. package/src/ui/rfi-renderer.js +0 -194
  174. package/src/ui/validation-ui.js +0 -147
@@ -1,5 +1,8 @@
1
- import { retryWithBackoff, withCircuitBreaker, checkpoint, restoreCheckpoint, logRecovery, normalizeError } from '@/lib/error-handler';
2
- import { AppError } from '@/lib/errors';
1
+ import { createLogger } from './logger.js';
2
+ import { retryWithBackoff, withCircuitBreaker, checkpoint, restoreCheckpoint, logRecovery } from '@/lib/error-resilience';
3
+
4
+ const log = createLogger('[Recovery]');
5
+ import { normalizeError, AppError } from '@/lib/error-handler';
3
6
  import { HTTP } from '@/config/constants';
4
7
 
5
8
  const recoveryState = { supervisors: new Map(), lastHealthCheck: null };
@@ -37,7 +40,7 @@ export async function supervise(name, fn, options = {}) {
37
40
  );
38
41
  supervisor.state = 'failed';
39
42
  supervisor.lastError = error;
40
- console.error(`[SUPERVISOR] ${name} failed permanently after ${supervisor.restarts.length} restarts`);
43
+ log.error(`${name} failed permanently after ${supervisor.restarts.length} restarts`);
41
44
  throw error;
42
45
  }
43
46
 
@@ -58,7 +61,7 @@ export async function supervise(name, fn, options = {}) {
58
61
 
59
62
  if (supervisor.restarts.length < supervisor.maxRestarts) {
60
63
  const delay = Math.min(1000 * Math.pow(2, supervisor.restarts.length - 1), 30000);
61
- console.warn(`[SUPERVISOR] ${name} restarting in ${delay}ms (attempt ${supervisor.restarts.length})`);
64
+ log.warn(`${name} restarting in ${delay}ms (attempt ${supervisor.restarts.length})`);
62
65
  await new Promise(resolve => setTimeout(resolve, delay));
63
66
  return supervise(name, fn, options);
64
67
  }
@@ -79,7 +82,7 @@ export async function degradedMode(fn, fallback, options = {}) {
79
82
  ]);
80
83
  return { mode: 'normal', result };
81
84
  } catch (error) {
82
- console.warn('[DEGRADED] Falling back to degraded mode', { error: error.message });
85
+ log.warn('falling back to degraded mode', { error: error.message });
83
86
  const result = await fallback();
84
87
  return { mode: 'degraded', result };
85
88
  }
@@ -115,7 +118,7 @@ export function isolateFailure(fn, defaultValue = null) {
115
118
  try {
116
119
  return await fn(...args);
117
120
  } catch (error) {
118
- console.error('[ISOLATE] Failure contained', {
121
+ log.error('failure contained', {
119
122
  function: fn.name,
120
123
  error: String(error.message || error)
121
124
  });
@@ -163,7 +166,7 @@ export async function withRecovery(fn, options = {}) {
163
166
  return result;
164
167
  } catch (error) {
165
168
  if (fallback) {
166
- console.warn('[RECOVERY] Using fallback after error', { error: error.message });
169
+ log.warn('using fallback after error', { error: error.message });
167
170
  return fallback();
168
171
  }
169
172
  throw error;
@@ -1,6 +1,8 @@
1
+ import { createLogger } from './logger.js';
1
2
  import { HTTP } from '@/config/constants';
2
- import { AppError } from '@/lib/errors';
3
- import { normalizeError } from '@/lib/error-handler';
3
+
4
+ const log = createLogger('[Resilience]');
5
+ import { AppError, normalizeError } from '@/lib/error-handler';
4
6
 
5
7
  const errorState = { errors: [], circuitBreakers: new Map(), checkpoints: new Map() };
6
8
 
@@ -13,12 +15,12 @@ export async function retryWithBackoff(fn, options = {}) {
13
15
  } catch (error) {
14
16
  if (attempt === maxAttempts) {
15
17
  const normalized = normalizeError(error);
16
- console.error(`[RETRY] Final attempt failed:`, { ...context, attempt, error: normalized.toJSON() });
18
+ log.error('final attempt failed:', { ...context, attempt, error: normalized.toJSON() });
17
19
  throw normalized;
18
20
  }
19
21
 
20
22
  const waitTime = delay * Math.pow(backoff, attempt - 1);
21
- console.warn(`[RETRY] Attempt ${attempt} failed, retrying in ${waitTime}ms`, context);
23
+ log.warn(`attempt ${attempt} failed, retrying in ${waitTime}ms`, context);
22
24
  await new Promise(resolve => setTimeout(resolve, waitTime));
23
25
  }
24
26
  }
@@ -66,7 +68,7 @@ export async function withCircuitBreaker(name, fn, options = {}) {
66
68
  if (breaker.failures >= breaker.threshold) {
67
69
  breaker.state = 'open';
68
70
  breaker.nextAttempt = Date.now() + breaker.resetTimeout;
69
- console.error(`[CIRCUIT] ${name} opened after ${breaker.failures} failures`);
71
+ log.error(`circuit ${name} opened after ${breaker.failures} failures`);
70
72
  }
71
73
 
72
74
  throw error;
@@ -1,4 +1,7 @@
1
+ import { createLogger } from './logger.js';
1
2
  import { hookEngine } from '@/lib/hook-engine.js';
3
+
4
+ const log = createLogger('[EventsEngine]');
2
5
  import { list, get, update, create, remove } from '@/engine.js';
3
6
  import { queueEmail } from '@/engine/notification-engine';
4
7
  import { safeJsonParse } from '@/lib/safe-json.js';
@@ -31,7 +34,7 @@ const activateWorkflowsForStage = async (engagementId, stage, user) => {
31
34
  hookEngine.execute(`feature:activated:${feature}`, { engagementId, stage, feature, user });
32
35
  }
33
36
  } catch (error) {
34
- console.error(`[ACTIVATION] Error activating workflows for stage ${stage}:`, error.message);
37
+ log.error(`error activating workflows for stage ${stage}:`, { message: error.message });
35
38
  }
36
39
  };
37
40
 
@@ -47,7 +50,7 @@ export const registerEntityHandlers = () => {
47
50
  create('activity_log', { entity_type: 'engagement', entity_id: context.id, action: 'stage_transition',
48
51
  message: `Stage transitioned: ${prev.stage} -> ${data.stage}`,
49
52
  details: JSON.stringify({ from: prev.stage, to: data.stage, user_role: user?.role }), user_email: user?.email }, user);
50
- } catch (e) { console.error('[events-engine] Failed to log stage change:', e.message); }
53
+ } catch (e) { log.error('failed to log stage change:', { message: e.message }); }
51
54
  return context;
52
55
  }, { priority: 100, name: 'workflow-stage-validator' });
53
56
 
@@ -70,7 +73,7 @@ export const registerEntityHandlers = () => {
70
73
 
71
74
  hookEngine.on('engagement:afterUpdate', async (engagement, changes, prev, user) => {
72
75
  if (changes.stage && changes.stage !== prev.stage) {
73
- logActivity('engagement', engagement.id, 'stage_change', `Stage: ${prev.stage} → ${changes.stage}`, user, { from: prev.stage, to: changes.stage });
76
+ logActivity('engagement', engagement.id, 'stage_change', `Stage: ${prev.stage} -> ${changes.stage}`, user, { from: prev.stage, to: changes.stage });
74
77
  const stageActions = {
75
78
  commencement: () => queueEmail('engagement_commencement', { engagement, recipients: 'client_users' }),
76
79
  finalization: async () => {
@@ -85,7 +88,7 @@ export const registerEntityHandlers = () => {
85
88
  await stageActions[changes.stage]?.();
86
89
  }
87
90
  if (changes.status && changes.status !== prev.status) {
88
- logActivity('engagement', engagement.id, 'status_change', `Status: ${prev.status} → ${changes.status}`, user);
91
+ logActivity('engagement', engagement.id, 'status_change', `Status: ${prev.status} -> ${changes.status}`, user);
89
92
  }
90
93
  if (changes.commencement_date && changes.commencement_date !== prev.commencement_date) {
91
94
  await queueEmail('engagement_date_change', { engagement, field: 'commencement_date', recipients: 'team_members' });
@@ -118,7 +121,7 @@ export const registerEntityHandlers = () => {
118
121
  if (changes.client_status && changes.client_status !== prev.client_status) {
119
122
  await queueEmail('rfi_status_change', { rfi, from: prev.client_status, to: changes.client_status, recipients: 'assigned_users' });
120
123
  }
121
- logActivity('rfi', rfi.id, 'status_change', `Status: ${prev.status} → ${changes.status}`, user);
124
+ logActivity('rfi', rfi.id, 'status_change', `Status: ${prev.status} -> ${changes.status}`, user);
122
125
  }
123
126
  if (changes.deadline && changes.deadline !== prev.deadline) {
124
127
  await queueEmail('rfi_deadline_change', { rfi, newDeadline: changes.deadline, recipients: 'assigned_users' });
@@ -160,7 +163,7 @@ export const registerEntityHandlers = () => {
160
163
  hookEngine.on('review:afterUpdate', async (review, changes, prev, user) => {
161
164
  if (changes.status && changes.status !== prev.status) {
162
165
  await queueEmail('review_status_change', { review, from: prev.status, to: changes.status, recipients: 'team_members' });
163
- logActivity('review', review.id, 'status_change', `Status: ${prev.status} → ${changes.status}`, user);
166
+ logActivity('review', review.id, 'status_change', `Status: ${prev.status} -> ${changes.status}`, user);
164
167
  }
165
168
  if (changes.collaborators && changes.collaborators !== prev.collaborators) {
166
169
  const added = safeJsonParse(changes.collaborators, []).filter(c => !safeJsonParse(prev.collaborators, []).find(p => p.id === c.id));
@@ -1,8 +1,3 @@
1
- /**
2
- * Observability Export Sink - Streams traces and metrics to external systems
3
- * Supports file, HTTP (OTLP), and stdout targets with batching and retry
4
- */
5
-
6
1
  import fs from 'fs';
7
2
  import path from 'path';
8
3
  import { createLogger } from './logger.js';
@@ -1,13 +1,9 @@
1
- /**
2
- * Field Iterator - Iterate over all fields (including nested/computed) in a spec
3
- */
4
-
5
- /**
6
- * Iterate over all fields in an entity spec
7
- * @param {object} spec - Entity specification
8
- * @param {Function} callback - (key, fieldDef) => void
9
- * @param {object} options - { includeComputed, includeHidden }
10
- */
1
+ /* ------------------------------------------------------------------ *
2
+ * Predicate-based field query helpers (ported from moonlanding).
3
+ * Additive: these do not change thatcher's existing forEachField /
4
+ * getEditableFields / getFieldNames / getFieldsByType signatures.
5
+ * ------------------------------------------------------------------ */
6
+
11
7
  export function forEachField(spec, callback, options = {}) {
12
8
  const { includeComputed = false, includeHidden = false } = options;
13
9
 
@@ -18,50 +14,21 @@ export function forEachField(spec, callback, options = {}) {
18
14
  }
19
15
  }
20
16
 
21
- /**
22
- * Get all field names
23
- * @param {object} spec
24
- * @returns {string[]}
25
- */
26
17
  export function getFieldNames(spec) {
27
18
  return Object.keys(spec.fields || {});
28
19
  }
29
20
 
30
- /**
31
- * Get only editable fields
32
- * @param {object} spec
33
- * @returns {string[]}
34
- */
35
21
  export function getEditableFields(spec) {
36
22
  return Object.entries(spec.fields || {})
37
23
  .filter(([key, field]) => !field.readonly && !field.auto && !field.auto_generate)
38
24
  .map(([key]) => key);
39
25
  }
40
26
 
41
- /**
42
- * Get field by type
43
- * @param {object} spec
44
- * @param {string} type
45
- * @returns {Array<{key: string, field: object}>}
46
- */
47
27
  export function getFieldsByType(spec, type) {
48
28
  return Object.entries(spec.fields || {})
49
29
  .filter(([key, field]) => field.type === type);
50
30
  }
51
31
 
52
- /* ------------------------------------------------------------------ *
53
- * Predicate-based field query helpers (ported from moonlanding).
54
- * Additive: these do not change thatcher's existing forEachField /
55
- * getEditableFields / getFieldNames / getFieldsByType signatures.
56
- * ------------------------------------------------------------------ */
57
-
58
- /**
59
- * Query the fields of a spec with one or more predicates.
60
- * @param {object} spec
61
- * @param {Function|Function[]} predicates - (field, key) => boolean
62
- * @param {object} [options] - { keysOnly }
63
- * @returns {Array<string|object>} keys when keysOnly, else { key, ...field }
64
- */
65
32
  export function fieldQuery(spec, predicates, options = {}) {
66
33
  const preds = Array.isArray(predicates) ? predicates : [predicates];
67
34
  const results = [];
@@ -73,9 +40,6 @@ export function fieldQuery(spec, predicates, options = {}) {
73
40
  return results;
74
41
  }
75
42
 
76
- /**
77
- * Reusable field predicates for fieldQuery.
78
- */
79
43
  export const is = {
80
44
  notId: f => f.type !== 'id',
81
45
  notHidden: f => !f.hidden,
@@ -90,85 +54,52 @@ export const is = {
90
54
  hasProperty: prop => f => f[prop] !== undefined,
91
55
  };
92
56
 
93
- /**
94
- * Editable fields as full { key, ...field } objects (form rendering).
95
- * Distinct from thatcher's getEditableFields (which returns names only).
96
- */
57
+ // Returns { key, ...field } objects, unlike getEditableFields() which returns names only.
97
58
  export function getFormFields(spec) {
98
59
  if (spec.system_entity) return [];
99
60
  return fieldQuery(spec, is.editable);
100
61
  }
101
62
 
102
- /**
103
- * Listable fields as full { key, ...field } objects.
104
- */
105
63
  export function getListFields(spec) {
106
64
  return fieldQuery(spec, is.listable);
107
65
  }
108
66
 
109
- /**
110
- * Displayable fields as full { key, ...field } objects.
111
- */
112
67
  export function getDisplayFields(spec) {
113
68
  return fieldQuery(spec, is.displayable);
114
69
  }
115
70
 
116
- /**
117
- * Required field names.
118
- */
119
71
  export function getRequiredFields(spec) {
120
72
  return fieldQuery(spec, is.required, { keysOnly: true });
121
73
  }
122
74
 
123
- /**
124
- * Searchable field names.
125
- */
126
75
  export function getSearchFields(spec) {
127
76
  return fieldQuery(spec, is.searchable, { keysOnly: true });
128
77
  }
129
78
 
130
- /**
131
- * Filterable fields, resolved from list.filters config.
132
- */
133
79
  export function getFilterableFields(spec) {
134
80
  return (spec.list?.filters || [])
135
81
  .map(filterKey => spec.fields?.[filterKey])
136
82
  .filter(Boolean);
137
83
  }
138
84
 
139
- /**
140
- * Reference (ref) fields as full { key, ...field } objects.
141
- */
142
85
  export function getRefFields(spec) {
143
86
  return fieldQuery(spec, is.ref);
144
87
  }
145
88
 
146
- /**
147
- * Get a single field definition by key.
148
- */
149
89
  export function getField(spec, fieldKey) {
150
90
  return spec.fields?.[fieldKey];
151
91
  }
152
92
 
153
- /**
154
- * Get a single field's type by key.
155
- */
156
93
  export function getFieldType(spec, fieldKey) {
157
94
  return spec.fields?.[fieldKey]?.type;
158
95
  }
159
96
 
160
- /**
161
- * Iterate editable fields on create. callback = (key, field) => void
162
- */
163
97
  export function iterateCreateFields(spec, callback) {
164
98
  for (const { key, ...field } of fieldQuery(spec, is.editable)) {
165
99
  callback(key, field);
166
100
  }
167
101
  }
168
102
 
169
- /**
170
- * Iterate editable fields on update. callback = (key, field) => void
171
- */
172
103
  export function iterateUpdateFields(spec, callback) {
173
104
  for (const { key, ...field } of fieldQuery(spec, is.editable)) {
174
105
  callback(key, field);
@@ -1,13 +1,3 @@
1
- /**
2
- * Field Registry - Type coercion and field utilities
3
- */
4
-
5
- /**
6
- * Coerce value to field type
7
- * @param {any} value
8
- * @param {string} type
9
- * @returns {any}
10
- */
11
1
  export function coerceFieldValue(value, type) {
12
2
  if (value === null || value === undefined) return value;
13
3
 
@@ -31,22 +21,10 @@ export function coerceFieldValue(value, type) {
31
21
  }
32
22
  }
33
23
 
34
- /**
35
- * Serialize field value for database
36
- * @param {any} value
37
- * @param {string} type
38
- * @returns {any}
39
- */
40
24
  export function serializeField(value, type) {
41
25
  return coerceFieldValue(value, type);
42
26
  }
43
27
 
44
- /**
45
- * Deserialize field value from database
46
- * @param {any} value
47
- * @param {string} type
48
- * @returns {any}
49
- */
50
28
  export function deserializeField(value, type) {
51
29
  if (value === null) return null;
52
30
 
@@ -1,26 +1,10 @@
1
- /**
2
- * Generic CRUD Handler - Catch-all API handler for any entity
3
- * This provides out-of-the-box CRUD for all entities defined in config
4
- * Users can override by creating their own route files
5
- */
6
-
7
1
  import { createCrudHandlers } from '../lib/crud-factory.js';
8
2
  import { getConfigEngineSync } from '../lib/config-generator-engine.js';
9
3
 
10
- /**
11
- * Get handler for an entity
12
- * @param {string} entityName
13
- * @returns {object} {GET, POST, PUT, DELETE}
14
- */
15
4
  export function getEntityHandlers(entityName) {
16
5
  return createCrudHandlers(entityName);
17
6
  }
18
7
 
19
- /**
20
- * Check if entity exists and is accessible
21
- * @param {string} entityName
22
- * @returns {boolean}
23
- */
24
8
  export function hasEntity(entityName) {
25
9
  try {
26
10
  const engine = getConfigEngineSync();
@@ -1,19 +1,12 @@
1
- /**
2
- * Hook Engine - Event system for plugins and lifecycle hooks
3
- * Fires events like 'create:entity:after', 'update:entity:before', etc.
4
- */
1
+ import { createLogger } from './logger.js';
2
+
3
+ const log = createLogger('[HookEngine]');
5
4
 
6
5
  export class HookEngine {
7
6
  constructor() {
8
7
  this.hooks = new Map();
9
8
  }
10
9
 
11
- /**
12
- * Register a hook handler
13
- * @param {string} name - Event name
14
- * @param {Function} callback - Handler function
15
- * @param {object} options - { priority, once }
16
- */
17
10
  register(name, callback, options = {}) {
18
11
  const { priority = 0, once = false } = options;
19
12
  if (!this.hooks.has(name)) this.hooks.set(name, []);
@@ -23,16 +16,10 @@ export class HookEngine {
23
16
  return this;
24
17
  }
25
18
 
26
- /**
27
- * Alias for register
28
- */
29
19
  on(name, callback, options = {}) {
30
20
  return this.register(name, callback, options);
31
21
  }
32
22
 
33
- /**
34
- * Unregister a hook handler
35
- */
36
23
  off(name, callback) {
37
24
  const list = this.hooks.get(name);
38
25
  if (!list) return this;
@@ -42,13 +29,6 @@ export class HookEngine {
42
29
  return this;
43
30
  }
44
31
 
45
- /**
46
- * Execute all handlers for an event (fire-and-forget style)
47
- * @param {string} name
48
- * @param {object} data
49
- * @param {object} options - { fallthrough: true to continue on error }
50
- * @returns {Promise<{success: boolean, data, errors}>}
51
- */
52
32
  async execute(name, data = {}, options = {}) {
53
33
  const { fallthrough = true } = options;
54
34
  const hooks = this.hooks.get(name);
@@ -60,7 +40,7 @@ export class HookEngine {
60
40
  await hook.callback(data);
61
41
  if (hook.once) this.off(name, hook.callback);
62
42
  } catch (error) {
63
- console.error(`[HookEngine] Hook "${name}" error:`, error.message);
43
+ log.error(`hook "${name}" error:`, { message: error.message });
64
44
  errors.push(error);
65
45
  if (!fallthrough) throw error;
66
46
  }
@@ -69,12 +49,6 @@ export class HookEngine {
69
49
  return { success: errors.length === 0, data, errors };
70
50
  }
71
51
 
72
- /**
73
- * Pipe data through transformation hooks
74
- * @param {string} name
75
- * @param {any} data
76
- * @returns {Promise<any>}
77
- */
78
52
  async pipe(name, data = {}) {
79
53
  const hooks = this.hooks.get(name);
80
54
  if (!hooks || hooks.length === 0) return data;
@@ -86,25 +60,16 @@ export class HookEngine {
86
60
  if (result !== undefined) current = result;
87
61
  if (hook.once) this.off(name, hook.callback);
88
62
  } catch (error) {
89
- console.error(`[HookEngine] Pipe hook "${name}" error:`, error.message);
63
+ log.error(`pipe hook "${name}" error:`, { message: error.message });
90
64
  }
91
65
  }
92
66
  return current;
93
67
  }
94
68
 
95
- /**
96
- * Get all listeners for an event
97
- * @param {string} name
98
- * @returns {Array}
99
- */
100
69
  listeners(name) {
101
70
  return this.hooks.has(name) ? Array.from(this.hooks.get(name)) : [];
102
71
  }
103
72
 
104
- /**
105
- * Get statistics
106
- * @returns {object} Map of event -> count
107
- */
108
73
  stats() {
109
74
  const result = {};
110
75
  for (const [name, list] of this.hooks.entries()) {
@@ -113,34 +78,21 @@ export class HookEngine {
113
78
  return result;
114
79
  }
115
80
 
116
- /**
117
- * Clear all hooks
118
- */
119
81
  clear() {
120
82
  this.hooks.clear();
121
83
  }
122
84
  }
123
85
 
124
- // Global singleton
125
86
  export const hookEngine = new HookEngine();
126
87
 
127
- /**
128
- * Execute hook (convenience function)
129
- */
130
88
  export async function executeHook(name, data = {}, options = {}) {
131
89
  return hookEngine.execute(name, data, options);
132
90
  }
133
91
 
134
- /**
135
- * Pipe through hooks
136
- */
137
92
  export async function pipeHook(name, data = {}) {
138
93
  return hookEngine.pipe(name, data);
139
94
  }
140
95
 
141
- /**
142
- * Register hook debug exposure
143
- */
144
96
  function registerHookDebug() {
145
97
  if (globalThis.__debug__) {
146
98
  globalThis.__debug__.expose('hooks', {
@@ -1,6 +1,9 @@
1
+ import { createLogger } from '../logger.js';
1
2
  import fs from 'fs';
2
3
  import path from 'path';
3
4
 
5
+ const log = createLogger('[Checkpoint]');
6
+
4
7
  export class CheckpointManager {
5
8
  constructor(options = {}) {
6
9
  this.checkpointDir = options.checkpointDir || path.join(process.cwd(), 'data', 'checkpoints');
@@ -23,7 +26,7 @@ export class CheckpointManager {
23
26
  await this._pruneOldCheckpoints(key);
24
27
  return true;
25
28
  } catch (err) {
26
- console.error(`[Checkpoint] Failed to save ${key}:`, err);
29
+ log.error(`failed to save ${key}:`, { message: err?.message || String(err) });
27
30
  return false;
28
31
  }
29
32
  }
@@ -42,7 +45,7 @@ export class CheckpointManager {
42
45
  this.checkpoints.set(key, checkpoint);
43
46
  return checkpoint.state;
44
47
  } catch (err) {
45
- console.error(`[Checkpoint] Failed to load ${key}:`, err);
48
+ log.error(`failed to load ${key}:`, { message: err?.message || String(err) });
46
49
  return null;
47
50
  }
48
51
  }
@@ -1,5 +1,9 @@
1
1
  import { EventEmitter } from 'events';
2
2
 
3
+ import { createLogger } from '../logger.js';
4
+
5
+ const log = createLogger('[PromiseContainer]');
6
+
3
7
  export class PromiseContainer extends EventEmitter {
4
8
  constructor() {
5
9
  super();
@@ -40,7 +44,7 @@ export class PromiseContainer extends EventEmitter {
40
44
  try {
41
45
  await Promise.race([drainPromise, timeoutPromise]);
42
46
  } catch (err) {
43
- console.warn(`Promise drain timeout: ${this.activePromises.size} promises still active`);
47
+ log.warn(`promise drain timeout: ${this.activePromises.size} promises still active`);
44
48
  }
45
49
  }
46
50
 
@@ -55,11 +59,11 @@ export class PromiseContainer extends EventEmitter {
55
59
  export const globalContainer = new PromiseContainer();
56
60
 
57
61
  globalContainer.setGlobalHandler((err, context) => {
58
- console.error(`[PromiseContainer] Unhandled rejection in ${context}:`, err);
62
+ log.error(`unhandled rejection in ${context}:`, { message: err?.message || String(err) });
59
63
  });
60
64
 
61
65
  process.on('unhandledRejection', (reason, promise) => {
62
- console.error('[Process] Unhandled Promise Rejection:', reason);
66
+ log.error('unhandled promise rejection:', { message: reason?.message || String(reason) });
63
67
  globalContainer.emit('processRejection', { reason, promise });
64
68
  });
65
69
 
@@ -1,4 +1,7 @@
1
+ import { createLogger } from '../logger.js';
1
2
  import { safeError } from './safe-error.js';
3
+
4
+ const log = createLogger('[RouteWrapper]');
2
5
  import { contain } from './promise-container.js';
3
6
 
4
7
  export function wrapRouteHandler(handler, options = {}) {
@@ -13,7 +16,7 @@ export function wrapRouteHandler(handler, options = {}) {
13
16
  return result;
14
17
  } catch (err) {
15
18
  if (logErrors) {
16
- console.error(`[RouteError:${context}] ${req.method} ${req.url}:`, err);
19
+ log.error(`${context} ${req.method} ${req.url}:`, { message: err?.message || String(err) });
17
20
  }
18
21
  if (sendErrorResponse && !res.headersSent) {
19
22
  const safe = safeError(err);
@@ -1,5 +1,8 @@
1
+ import { createLogger } from '../logger.js';
1
2
  import { EventEmitter } from 'events';
2
3
 
4
+ const log = createLogger('[Supervisor]');
5
+
3
6
  export class Supervisor extends EventEmitter {
4
7
  constructor(name, workerFn, options = {}) {
5
8
  super();
@@ -22,7 +25,7 @@ export class Supervisor extends EventEmitter {
22
25
 
23
26
  if (this.listenerCount('error') === 0) {
24
27
  this.on('error', (data) => {
25
- console.error(`[Supervisor:${this.name}] Worker error:`, data.error?.message || data.error);
28
+ log.error(`[${this.name}] worker error:`, { message: data.error?.message || String(data.error) });
26
29
  });
27
30
  }
28
31
  }
@@ -73,7 +76,7 @@ export class Supervisor extends EventEmitter {
73
76
  try {
74
77
  await this.options.onError(err);
75
78
  } catch (handlerErr) {
76
- console.error(`[Supervisor:${this.name}] Error handler failed:`, handlerErr);
79
+ log.error(`[${this.name}] error handler failed:`, { message: handlerErr?.message || String(handlerErr) });
77
80
  }
78
81
  }
79
82
 
@@ -84,7 +87,7 @@ export class Supervisor extends EventEmitter {
84
87
  this.restarts.push(now);
85
88
 
86
89
  if (this.restarts.length > this.options.maxRestarts) {
87
- console.error(`[Supervisor:${this.name}] Max restarts exceeded. Giving up.`);
90
+ log.error(`[${this.name}] max restarts exceeded, giving up`);
88
91
  this.emit('giveup', this.name);
89
92
  this.running = false;
90
93
  return;
@@ -1,8 +1,3 @@
1
- /**
2
- * HyperFormula Compute Engine - Spreadsheet formula evaluation for Thatcher
3
- * Provides calculated fields, business rules, and data transformations
4
- */
5
-
6
1
  import { HyperFormula } from 'hyperformula';
7
2
  import { createLogger } from './logger.js';
8
3
 
@@ -0,0 +1,10 @@
1
+ // Extracted from database-core.js so modules that need genId()/now() (e.g. the
2
+ // busybase store) don't have to import database-core, which pulls in better-sqlite3.
3
+
4
+ export function genId() {
5
+ return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
6
+ }
7
+
8
+ export function now() {
9
+ return Math.floor(Date.now() / 1000);
10
+ }