thatcher 1.0.6 → 1.0.33

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 +29 -88
  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 +14 -5
  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,7 +1,6 @@
1
- /**
2
- * Error Handling - Centralized error types and utilities
3
- */
1
+ import { createLogger } from './logger.js';
4
2
 
3
+ // AppError stays a real class (base error type; `instanceof AppError` works).
5
4
  export class AppError extends Error {
6
5
  constructor(message, code = 'APP_ERROR', status = 500, details = null) {
7
6
  super(message);
@@ -9,98 +8,46 @@ export class AppError extends Error {
9
8
  this.code = code;
10
9
  this.status = status;
11
10
  this.details = details;
12
- if (Error.captureStackTrace) Error.captureStackTrace(this, AppError);
11
+ if (Error.captureStackTrace) Error.captureStackTrace(this, this.constructor);
13
12
  }
14
13
  }
15
14
 
16
- export class UnauthorizedError extends AppError {
17
- constructor(message = 'Authentication required') {
18
- super(message, 'UNAUTHORIZED', 401);
19
- this.name = 'UnauthorizedError';
20
- }
15
+ // The specific error types are exported as FACTORY FUNCTIONS, not classes, so
16
+ // both `throw UnauthorizedError(x)` (the style moonlanding and ~17 callsites in
17
+ // this repo use) and a plain call work. A derived `class X extends AppError`
18
+ // cannot be invoked without `new` at all (the engine throws before the body, so
19
+ // a new.target guard does not help) — a factory is the portable fix. Each
20
+ // returns an AppError instance, so `instanceof AppError` still holds in catch.
21
+ export function UnauthorizedError(message = 'Authentication required') {
22
+ const e = new AppError(message, 'UNAUTHORIZED', 401); e.name = 'UnauthorizedError'; return e;
21
23
  }
22
-
23
- export class PermissionError extends AppError {
24
- constructor(message = 'Permission denied') {
25
- super(message, 'PERMISSION_DENIED', 403);
26
- this.name = 'PermissionError';
27
- }
24
+ export function PermissionError(message = 'Permission denied') {
25
+ const e = new AppError(message, 'PERMISSION_DENIED', 403); e.name = 'PermissionError'; return e;
28
26
  }
29
-
30
- export class NotFoundError extends AppError {
31
- constructor(entity = 'Resource', id = null) {
32
- const msg = id ? `${entity} with id ${id} not found` : `${entity} not found`;
33
- super(msg, 'NOT_FOUND', 404);
34
- this.name = 'NotFoundError';
35
- this.entity = entity;
36
- this.id = id;
37
- }
27
+ export function NotFoundError(entity = 'Resource', id = null) {
28
+ const msg = id ? `${entity} with id ${id} not found` : `${entity} not found`;
29
+ const e = new AppError(msg, 'NOT_FOUND', 404); e.name = 'NotFoundError'; e.entity = entity; e.id = id; return e;
38
30
  }
39
-
40
- export class ValidationError extends AppError {
41
- constructor(message = 'Validation failed', errors = {}) {
42
- super(message, 'VALIDATION_ERROR', 422);
43
- this.name = 'ValidationError';
44
- this.errors = errors;
45
- }
31
+ export function ValidationError(message = 'Validation failed', errors = {}) {
32
+ const e = new AppError(message, 'VALIDATION_ERROR', 422); e.name = 'ValidationError'; e.errors = errors; return e;
46
33
  }
47
-
48
- export class DatabaseError extends AppError {
49
- constructor(operation = 'Database operation', originalError = null) {
50
- super(`Database ${operation} failed: ${originalError?.message || 'unknown error'}`, 'DATABASE_ERROR', 500);
51
- this.name = 'DatabaseError';
52
- this.originalError = originalError;
53
- }
34
+ export function DatabaseError(operation = 'Database operation', originalError = null) {
35
+ const e = new AppError(`Database ${operation} failed: ${originalError?.message || 'unknown error'}`, 'DATABASE_ERROR', 500);
36
+ e.name = 'DatabaseError'; e.originalError = originalError; return e;
54
37
  }
55
-
56
- export class ConflictError extends AppError {
57
- constructor(message = 'Resource already exists') {
58
- super(message, 'CONFLICT', 409);
59
- this.name = 'ConflictError';
60
- }
38
+ export function ConflictError(message = 'Resource already exists') {
39
+ const e = new AppError(message, 'CONFLICT', 409); e.name = 'ConflictError'; return e;
61
40
  }
62
-
63
- export class BadRequestError extends AppError {
64
- constructor(message = 'Invalid request') {
65
- super(message, 'BAD_REQUEST', 400);
66
- this.name = 'BadRequestError';
67
- }
41
+ export function BadRequestError(message = 'Invalid request') {
42
+ const e = new AppError(message, 'BAD_REQUEST', 400); e.name = 'BadRequestError'; return e;
68
43
  }
69
44
 
70
- /**
71
- * Create an error logger
72
- * @param {string} context
73
- * @returns {object}
74
- */
75
45
  export function createErrorLogger(context = '') {
76
- return {
77
- error: (msg, meta = {}) => {
78
- console.error(`[${context}] ${msg}`, meta);
79
- },
80
- warn: (msg, meta = {}) => {
81
- console.warn(`[${context}] ${msg}`, meta);
82
- },
83
- info: (_msg, _meta = {}) => {},
84
- debug: (msg, meta = {}) => {
85
- if (process.env.DEBUG) {
86
- console.debug(`[${context}] Debug:`, msg, meta);
87
- }
88
- },
89
- };
46
+ const log = createLogger(`[${context}]`);
47
+ return { error: log.error, warn: log.warn, info: (_msg, _meta = {}) => {}, debug: log.debug };
90
48
  }
91
49
 
92
- /**
93
- * Normalize an arbitrary thrown value into one of thatcher's AppError-class
94
- * instances. AppError instances (and anything already shaped like one — a
95
- * numeric `status` + a `code`) pass through untouched; common low-level
96
- * errors are mapped to the appropriate thatcher error class.
97
- *
98
- * (Ported from moonlanding, adapted to thatcher's class-based hierarchy and
99
- * its `status`/`code`/`details` shape. Kept import-free so the module stays
100
- * loadable under the plain-node test runner.)
101
- * @param {unknown} error
102
- * @returns {AppError}
103
- */
50
+ // Ported from moonlanding; kept import-free so this module loads under the plain-node test runner.
104
51
  export function normalizeError(error) {
105
52
  if (error instanceof AppError) {
106
53
  return error;
@@ -135,13 +82,7 @@ export function normalizeError(error) {
135
82
  });
136
83
  }
137
84
 
138
- /**
139
- * Build a JSON-serializable error response body from any thrown value.
140
- * (Ported from moonlanding, adapted to thatcher's error shape.)
141
- * @param {unknown} error
142
- * @param {boolean} [includeStack=false]
143
- * @returns {object}
144
- */
85
+ // Ported from moonlanding.
145
86
  export function formatErrorResponse(error, includeStack = false) {
146
87
  const normalized = normalizeError(error);
147
88
  const response = {
@@ -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', {