bulltrackers-module 1.0.317 → 1.0.319

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.
@@ -1,7 +1,9 @@
1
1
  /**
2
- * FILENAME: computation-system/tools/BuildReporter.js
3
- * UPDATED: Replaced hardcoded 90-day window with a dynamic window based on system data availability.
2
+ * FILENAME: bulltrackers-module/functions/computation-system/tools/BuildReporter.js
3
+ * UPGRADED: Offloads heavy logic to a dedicated Cloud Function via Pub/Sub.
4
+ * FEATURES: Patch versioning, data-drift detection (window changes), and checkpointed writes.
4
5
  */
6
+
5
7
  const { analyzeDateExecution } = require('../WorkflowOrchestrator');
6
8
  const { fetchComputationStatus, updateComputationStatus } = require('../persistence/StatusRepository');
7
9
  const { normalizeName, getExpectedDateStrings, DEFINITIVE_EARLIEST_DATES } = require('../utils/utils');
@@ -11,12 +13,48 @@ const SYSTEM_EPOCH = require('../system_epoch');
11
13
  const pLimit = require('p-limit');
12
14
  const path = require('path');
13
15
  const crypto = require('crypto');
14
- const fs = require('fs');
16
+
17
+ // Load package info for versioning
15
18
  const packageJson = require(path.join(__dirname, '..', '..', '..', 'package.json'));
16
19
  const packageVersion = packageJson.version;
17
20
 
21
+ const BUILD_RECORDS_COLLECTION = 'computation_build_records';
22
+ const BUILD_METADATA_DOC = 'system_build_metadata';
18
23
  const SIMHASH_REGISTRY_COLLECTION = 'system_simhash_registry';
19
24
 
25
+ /**
26
+ * Publishes a message to trigger the dedicated Build Reporter Cloud Function.
27
+ * Replaces the old ensureBuildReport that ran locally on module load.
28
+ */
29
+ async function requestBuildReport(config, dependencies) {
30
+ const { pubsubUtils, logger } = dependencies;
31
+ try {
32
+ await pubsubUtils.publish(config.buildReporterTopic, {
33
+ requestedAt: new Date().toISOString(),
34
+ packageVersion: packageVersion
35
+ });
36
+ logger.log('INFO', `[BuildReporter] 🛰️ Trigger message sent to ${config.buildReporterTopic}`);
37
+ return { success: true };
38
+ } catch (e) {
39
+ logger.log('ERROR', `[BuildReporter] Failed to publish trigger: ${e.message}`);
40
+ throw e;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Cloud Function Entry Point for the Build Reporter.
46
+ */
47
+ async function handleBuildReportTrigger(message, context, config, dependencies, manifest) {
48
+ const { logger } = dependencies;
49
+ logger.log('INFO', `[BuildReporter] 📥 Trigger received. Starting build analysis...`);
50
+ try {
51
+ return await generateBuildReport(config, dependencies, manifest);
52
+ } catch (e) {
53
+ logger.log('ERROR', `[BuildReporter] Fatal error in execution: ${e.message}`);
54
+ throw e;
55
+ }
56
+ }
57
+
20
58
  /**
21
59
  * Replaces expensive file walking with System Epoch + Manifest Hash.
22
60
  */
@@ -27,6 +65,20 @@ function getSystemFingerprint(manifest) {
27
65
  .digest('hex');
28
66
  }
29
67
 
68
+ /**
69
+ * Increments the patch number for the current package version in Firestore.
70
+ */
71
+ async function getNextBuildId(db, version) {
72
+ const metaRef = db.collection(BUILD_RECORDS_COLLECTION).doc(BUILD_METADATA_DOC);
73
+ return await db.runTransaction(async (t) => {
74
+ const doc = await t.get(metaRef);
75
+ const data = doc.exists ? doc.data() : {};
76
+ const currentPatch = (data[version] || 0) + 1;
77
+ t.set(metaRef, { [version]: currentPatch }, { merge: true });
78
+ return `v${version}_p${currentPatch}`;
79
+ });
80
+ }
81
+
30
82
  function isDateBeforeAvailability(dateStr, calcManifest) {
31
83
  const targetDate = new Date(dateStr + 'T00:00:00Z');
32
84
  const deps = calcManifest.rootDataDependencies || [];
@@ -115,72 +167,44 @@ async function verifyBehavioralStability(candidates, manifestMap, dailyStatus, l
115
167
  }
116
168
 
117
169
  /**
118
- * UPDATED: Now calculates daysBack dynamically based on absoluteEarliest data date.
170
+ * The main reporter logic. Handles drift detection, minor versioning,
171
+ * and checkpointed batch writes to Firestore.
119
172
  */
120
- async function ensureBuildReport(config, dependencies, manifest) {
173
+ async function generateBuildReport(config, dependencies, manifest) {
121
174
  const { db, logger } = dependencies;
122
- const now = new Date();
123
- const buildId = `v${packageVersion}_${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,'0')}-${String(now.getDate()).padStart(2,'0')}_${String(now.getHours()).padStart(2,'0')}`;
124
- const lockRef = db.collection('computation_build_records').doc(`init_lock_v${packageVersion}`);
125
-
126
- try {
127
- const shouldRun = await db.runTransaction(async (t) => {
128
- const doc = await t.get(lockRef);
129
- if (doc.exists) { return false; }
130
- t.set(lockRef, { status: 'IN_PROGRESS', startedAt: new Date(), buildId: buildId });
131
- return true;
132
- });
133
-
134
- if (!shouldRun) { logger.log('INFO', `[BuildReporter] 🔒 Report for v${packageVersion} locked. Skipping.`); return; }
135
-
136
- const currentSystemHash = getSystemFingerprint(manifest);
137
- const latestBuildDoc = await db.collection('computation_build_records').doc('latest').get();
138
-
139
- if (latestBuildDoc.exists) {
140
- const latestData = latestBuildDoc.data();
141
- if (latestData.systemFingerprint === currentSystemHash) {
142
- logger.log('INFO', `[BuildReporter] ⚡ System Fingerprint (${currentSystemHash.substring(0,8)}) matches latest build. Skipping Report.`);
143
- await db.collection('computation_build_records').doc(buildId).set({
144
- buildId,
145
- packageVersion,
146
- systemFingerprint: currentSystemHash,
147
- status: 'SKIPPED_IDENTICAL',
148
- referenceBuild: latestData.buildId,
149
- generatedAt: new Date().toISOString()
150
- });
151
- lockRef.update({ status: 'SKIPPED', completedAt: new Date() }).catch(() => {});
152
- return;
153
- }
154
- }
155
-
156
- // [DYNAMIC WINDOW CALCULATION]
157
- const { absoluteEarliest } = DEFINITIVE_EARLIEST_DATES;
158
- let dynamicDaysBack = 90; // Fallback default
159
- if (absoluteEarliest) {
160
- const today = new Date();
161
- const diffTime = Math.abs(today - absoluteEarliest);
162
- dynamicDaysBack = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 2; // Days difference + 2 day buffer
163
- }
164
-
165
- logger.log('INFO', `[BuildReporter] 🚀 Change Detected. Running Pre-flight Report for v${packageVersion} (Scope: ${dynamicDaysBack} days)...`);
166
- await generateBuildReport(config, dependencies, manifest, dynamicDaysBack, buildId, currentSystemHash);
167
- lockRef.update({ status: 'COMPLETED', completedAt: new Date() }).catch(() => {});
168
-
169
- } catch (e) {
170
- logger.log('ERROR', `[BuildReporter] Auto-run check failed: ${e.message}`);
175
+
176
+ const currentFingerprint = getSystemFingerprint(manifest);
177
+ const latestBuildDoc = await db.collection(BUILD_RECORDS_COLLECTION).doc('latest').get();
178
+ const latest = latestBuildDoc.exists ? latestBuildDoc.data() : null;
179
+
180
+ // [DATA DRIFT DETECTION]
181
+ // Force a run if the definitive earliest data date has changed (e.g. new history backfilled)
182
+ const currentEarliestStr = DEFINITIVE_EARLIEST_DATES.absoluteEarliest?.toISOString() || 'NONE';
183
+ const lastEarliestStr = latest?.windowEarliest || 'NONE';
184
+ const windowChanged = currentEarliestStr !== lastEarliestStr;
185
+
186
+ // If fingerprints match AND the window is the same, we can truly skip.
187
+ if (latest && latest.systemFingerprint === currentFingerprint && !windowChanged) {
188
+ logger.log('INFO', `[BuildReporter] ⚡ System fingerprint and window stable. Skipping report.`);
189
+ return { success: true, status: 'SKIPPED_IDENTICAL' };
171
190
  }
172
- }
173
191
 
174
- async function generateBuildReport(config, dependencies, manifest, daysBack = 90, customBuildId = null, systemFingerprint = null) {
175
- const { db, logger } = dependencies;
176
- const buildId = customBuildId || `manual_${Date.now()}`;
177
- const finalFingerprint = systemFingerprint || getSystemFingerprint(manifest);
192
+ // Increment patch version (e.g., v1.0.0_p1, v1.0.0_p2)
193
+ const buildId = await getNextBuildId(db, packageVersion);
194
+ logger.log('INFO', `[BuildReporter] 🚀 Change Detected. Generating Build ${buildId}. Reason: ${windowChanged ? 'Data Window Drift' : 'Code Change'}`);
178
195
 
179
- logger.log('INFO', `[BuildReporter] Generating Build Report: ${buildId} (Scope: ${daysBack} days, Fingerprint: ${finalFingerprint.substring(0,8)})...`);
196
+ const today = new Date();
197
+ const { absoluteEarliest } = DEFINITIVE_EARLIEST_DATES;
198
+
199
+ // Dynamic Window calculation
200
+ let dynamicDaysBack = 90;
201
+ if (absoluteEarliest) {
202
+ const diffTime = Math.abs(today - absoluteEarliest);
203
+ dynamicDaysBack = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 2;
204
+ }
180
205
 
181
- const today = new Date();
182
206
  const startDate = new Date();
183
- startDate.setDate(today.getDate() - daysBack);
207
+ startDate.setDate(today.getDate() - dynamicDaysBack);
184
208
 
185
209
  const datesToCheck = getExpectedDateStrings(startDate, today);
186
210
  const manifestMap = new Map(manifest.map(c => [normalizeName(c.name), c]));
@@ -201,131 +225,131 @@ async function generateBuildReport(config, dependencies, manifest, daysBack = 90
201
225
  const reportHeader = {
202
226
  buildId,
203
227
  packageVersion,
204
- systemFingerprint: finalFingerprint,
228
+ systemFingerprint: currentFingerprint,
229
+ windowEarliest: currentEarliestStr,
205
230
  generatedAt: new Date().toISOString(),
231
+ status: 'IN_PROGRESS',
206
232
  summary: {},
207
233
  _sharded: true
208
234
  };
209
235
 
236
+ // Initialize the build record
237
+ await db.collection(BUILD_RECORDS_COLLECTION).doc(buildId).set(reportHeader);
238
+
210
239
  let totalRun = 0, totalReRun = 0, totalStable = 0;
211
- const limit = pLimit(5);
240
+ const limit = pLimit(10); // Concurrency for fetching statuses
212
241
 
213
- const processingPromises = datesToCheck.map(dateStr => limit(async () => {
214
- try {
215
- const fetchPromises = [
216
- fetchComputationStatus(dateStr, config, dependencies),
217
- checkRootDataAvailability(dateStr, config, dependencies, DEFINITIVE_EARLIEST_DATES)
218
- ];
219
-
220
- let prevDateStr = null;
221
- if (manifest.some(c => c.isHistorical)) {
222
- const prevDate = new Date(dateStr + 'T00:00:00Z');
223
- prevDate.setUTCDate(prevDate.getUTCDate() - 1);
224
- prevDateStr = prevDate.toISOString().slice(0, 10);
225
- if (prevDate >= DEFINITIVE_EARLIEST_DATES.absoluteEarliest) {
226
- fetchPromises.push(fetchComputationStatus(prevDateStr, config, dependencies));
242
+ // Process dates in chunks of 5 for checkpointed writing
243
+ for (let i = 0; i < datesToCheck.length; i += 5) {
244
+ const dateBatch = datesToCheck.slice(i, i + 5);
245
+
246
+ const results = await Promise.all(dateBatch.map(dateStr => limit(async () => {
247
+ try {
248
+ const fetchPromises = [
249
+ fetchComputationStatus(dateStr, config, dependencies),
250
+ checkRootDataAvailability(dateStr, config, dependencies, DEFINITIVE_EARLIEST_DATES)
251
+ ];
252
+
253
+ let prevDateStr = null;
254
+ if (manifest.some(c => c.isHistorical)) {
255
+ const prevDate = new Date(dateStr + 'T00:00:00Z');
256
+ prevDate.setUTCDate(prevDate.getUTCDate() - 1);
257
+ prevDateStr = prevDate.toISOString().slice(0, 10);
258
+ if (prevDate >= DEFINITIVE_EARLIEST_DATES.absoluteEarliest) {
259
+ fetchPromises.push(fetchComputationStatus(prevDateStr, config, dependencies));
260
+ }
227
261
  }
228
- }
229
262
 
230
- const results = await Promise.all(fetchPromises);
231
- const dailyStatus = results[0];
232
- const availability = results[1];
233
- const prevDailyStatus = (prevDateStr && results[2]) ? results[2] : (prevDateStr ? {} : null);
234
- const rootDataStatus = availability ? availability.status : { hasPortfolio: false, hasHistory: false };
235
-
236
- const analysis = analyzeDateExecution(dateStr, manifest, rootDataStatus, dailyStatus, manifestMap, prevDailyStatus);
237
-
238
- const dateSummary = {
239
- run: [], rerun: [], stable: [], blocked: [], impossible: [], uptodate: [],
240
- meta: { totalIncluded: 0, totalExpected: 0, match: false }
241
- };
242
-
243
- const expectedCount = manifest.filter(c => !isDateBeforeAvailability(dateStr, c)).length;
244
- dateSummary.meta.totalExpected = expectedCount;
245
-
246
- const pushIfValid = (targetArray, item, extraReason = null) => {
247
- const calcManifest = manifestMap.get(item.name);
248
- if (calcManifest && isDateBeforeAvailability(dateStr, calcManifest)) return;
249
-
250
- const entry = {
251
- name: item.name,
252
- reason: item.reason || extraReason,
253
- pass: calcManifest ? calcManifest.pass : '?'
263
+ const [dailyStatus, availability, prevRes] = await Promise.all(fetchPromises);
264
+ const prevDailyStatus = (prevDateStr && prevRes) ? prevRes : (prevDateStr ? {} : null);
265
+ const rootDataStatus = availability ? availability.status : { hasPortfolio: false, hasHistory: false };
266
+
267
+ const analysis = analyzeDateExecution(dateStr, manifest, rootDataStatus, dailyStatus, manifestMap, prevDailyStatus);
268
+
269
+ const dateSummary = {
270
+ run: [], rerun: [], stable: [], blocked: [], impossible: [], uptodate: [],
271
+ meta: { totalIncluded: 0, totalExpected: 0, match: false }
254
272
  };
255
- if (targetArray === dateSummary.rerun) {
256
- entry.impact = calculateBlastRadius(item.name, reverseGraph);
257
- }
258
- targetArray.push(entry);
259
- };
260
-
261
- analysis.runnable.forEach(item => pushIfValid(dateSummary.run, item, "New Calculation"));
262
-
263
- if (analysis.reRuns.length > 0) {
264
- const { trueReRuns, stableUpdates } = await verifyBehavioralStability(analysis.reRuns, manifestMap, dailyStatus, logger, simHashCache, db);
265
-
266
- trueReRuns.forEach(item => pushIfValid(dateSummary.rerun, item, "Logic Changed"));
267
- stableUpdates.forEach(item => pushIfValid(dateSummary.stable, item, "Cosmetic Change"));
268
-
269
- if (stableUpdates.length > 0) {
270
- const updatesPayload = {};
271
- for (const stable of stableUpdates) {
272
- const m = manifestMap.get(stable.name);
273
- const stored = dailyStatus[stable.name];
274
- if (m && stored) {
275
- updatesPayload[stable.name] = {
276
- hash: m.hash,
277
- simHash: stable.simHash,
278
- resultHash: stored.resultHash,
279
- dependencyResultHashes: stored.dependencyResultHashes || {},
280
- category: m.category,
281
- composition: m.composition,
282
- lastUpdated: new Date()
283
- };
273
+
274
+ const expectedCount = manifest.filter(c => !isDateBeforeAvailability(dateStr, c)).length;
275
+ dateSummary.meta.totalExpected = expectedCount;
276
+
277
+ const pushIfValid = (targetArray, item, extraReason = null) => {
278
+ const calcManifest = manifestMap.get(item.name);
279
+ if (calcManifest && isDateBeforeAvailability(dateStr, calcManifest)) return;
280
+ const entry = { name: item.name, reason: item.reason || extraReason, pass: calcManifest?.pass || '?' };
281
+ if (targetArray === dateSummary.rerun) entry.impact = calculateBlastRadius(item.name, reverseGraph);
282
+ targetArray.push(entry);
283
+ };
284
+
285
+ analysis.runnable.forEach(item => pushIfValid(dateSummary.run, item, "New Calculation"));
286
+
287
+ if (analysis.reRuns.length > 0) {
288
+ const { trueReRuns, stableUpdates } = await verifyBehavioralStability(analysis.reRuns, manifestMap, dailyStatus, logger, simHashCache, db);
289
+ trueReRuns.forEach(item => pushIfValid(dateSummary.rerun, item, "Logic Changed"));
290
+ stableUpdates.forEach(item => pushIfValid(dateSummary.stable, item, "Logic Stable"));
291
+
292
+ if (stableUpdates.length > 0) {
293
+ const updatesPayload = {};
294
+ for (const stable of stableUpdates) {
295
+ const m = manifestMap.get(stable.name);
296
+ const stored = dailyStatus[stable.name];
297
+ if (m && stored) {
298
+ updatesPayload[stable.name] = {
299
+ hash: m.hash, simHash: stable.simHash, resultHash: stored.resultHash,
300
+ dependencyResultHashes: stored.dependencyResultHashes || {},
301
+ category: m.category, composition: m.composition, lastUpdated: new Date()
302
+ };
303
+ }
304
+ }
305
+ if (Object.keys(updatesPayload).length > 0) {
306
+ await updateComputationStatus(dateStr, updatesPayload, config, dependencies);
284
307
  }
285
- }
286
- if (Object.keys(updatesPayload).length > 0) {
287
- await updateComputationStatus(dateStr, updatesPayload, config, dependencies);
288
- logger.log('INFO', `[BuildReporter] 🩹 Fixed ${Object.keys(updatesPayload).length} stable items for ${dateStr}. They will NOT re-run.`);
289
308
  }
290
309
  }
291
- }
292
310
 
293
- analysis.blocked.forEach (item => pushIfValid(dateSummary.blocked, item));
294
- analysis.failedDependency.forEach (item => pushIfValid(dateSummary.blocked, item, "Dependency Missing"));
295
- analysis.impossible.forEach (item => pushIfValid(dateSummary.impossible, item));
296
- analysis.skipped.forEach (item => pushIfValid(dateSummary.uptodate, item, "Up To Date"));
311
+ analysis.blocked.forEach(item => pushIfValid(dateSummary.blocked, item));
312
+ analysis.failedDependency.forEach(item => pushIfValid(dateSummary.blocked, item, "Dependency Missing"));
313
+ analysis.impossible.forEach(item => pushIfValid(dateSummary.impossible, item));
314
+ analysis.skipped.forEach(item => pushIfValid(dateSummary.uptodate, item, "Up To Date"));
297
315
 
298
- const includedCount = dateSummary.run.length + dateSummary.rerun.length + dateSummary.stable.length +
299
- dateSummary.blocked.length + dateSummary.impossible.length + dateSummary.uptodate.length;
300
- dateSummary.meta.totalIncluded = includedCount;
301
- dateSummary.meta.match = (includedCount === expectedCount);
316
+ const includedCount = dateSummary.run.length + dateSummary.rerun.length + dateSummary.stable.length +
317
+ dateSummary.blocked.length + dateSummary.impossible.length + dateSummary.uptodate.length;
318
+ dateSummary.meta.totalIncluded = includedCount;
319
+ dateSummary.meta.match = (includedCount === expectedCount);
302
320
 
303
- await db.collection('computation_build_records')
304
- .doc(buildId)
305
- .collection('details')
306
- .doc(dateStr)
307
- .set(dateSummary);
321
+ // Write detailed date record
322
+ await db.collection(BUILD_RECORDS_COLLECTION).doc(buildId).collection('details').doc(dateStr).set(dateSummary);
308
323
 
309
- return { stats: { run: dateSummary.run.length, rerun: dateSummary.rerun.length, stable: dateSummary.stable.length } };
310
-
311
- } catch (err) {
312
- logger.log('ERROR', `[BuildReporter] Error analyzing date ${dateStr}: ${err.message}`);
313
- return null;
314
- }
315
- }));
316
-
317
- const results = await Promise.all(processingPromises);
324
+ return { run: dateSummary.run.length, rerun: dateSummary.rerun.length, stable: dateSummary.stable.length };
325
+ } catch (err) {
326
+ logger.log('ERROR', `[BuildReporter] Analysis failed for ${dateStr}: ${err.message}`);
327
+ return { run: 0, rerun: 0, stable: 0 };
328
+ }
329
+ })));
318
330
 
319
- results.forEach(res => { if (res) { totalRun += res.stats.run; totalReRun += res.stats.rerun; totalStable += res.stats.stable; } });
331
+ // Accumulate stats and write a progress checkpoint
332
+ results.forEach(res => { totalRun += res.run; totalReRun += res.rerun; totalStable += res.stable; });
333
+ await db.collection(BUILD_RECORDS_COLLECTION).doc(buildId).update({
334
+ checkpoint: `Processed ${i + dateBatch.length}/${datesToCheck.length} dates`
335
+ });
336
+ }
320
337
 
321
- reportHeader.summary = { totalReRuns: totalReRun, totalNew: totalRun, totalStable: totalStable, scanRange: `${datesToCheck[0]} to ${datesToCheck[datesToCheck.length-1]}` };
338
+ reportHeader.status = 'COMPLETED';
339
+ reportHeader.summary = {
340
+ totalReRuns: totalReRun,
341
+ totalNew: totalRun,
342
+ totalStable: totalStable,
343
+ scanRange: `${datesToCheck[0]} to ${datesToCheck[datesToCheck.length-1]}`
344
+ };
322
345
 
323
- await db.collection('computation_build_records').doc(buildId).set(reportHeader);
324
- await db.collection('computation_build_records').doc('latest').set({ ...reportHeader, note: "Latest build report pointer." });
346
+ // Finalize build record and update the 'latest' pointer
347
+ await db.collection(BUILD_RECORDS_COLLECTION).doc(buildId).set(reportHeader);
348
+ await db.collection(BUILD_RECORDS_COLLECTION).doc('latest').set({ ...reportHeader, note: "Latest completed build report." });
325
349
 
326
- logger.log('SUCCESS', `[BuildReporter] Report ${buildId} saved. Re-runs: ${totalReRun}, Stable (Fixed): ${totalStable}, New: ${totalRun}.`);
350
+ logger.log('SUCCESS', `[BuildReporter] Build ${buildId} completed. Re-runs: ${totalReRun}, Stable: ${totalStable}, New: ${totalRun}.`);
327
351
 
328
- return { success: true, reportId: buildId, summary: reportHeader.summary };
352
+ return { success: true, buildId, summary: reportHeader.summary };
329
353
  }
330
354
 
331
- module.exports = { ensureBuildReport, generateBuildReport };
355
+ module.exports = { requestBuildReport, handleBuildReportTrigger, generateBuildReport };
@@ -1,14 +1,8 @@
1
1
  /**
2
2
  * @fileoverview Core Pub/Sub utility functions.
3
- * REFACTORED: Hybrid module supporting both Stateless functions and Stateful Class.
4
- * Fixes "PubSubUtils is not a constructor" error.
3
+ * UPDATED: Added single-message publish method to the PubSubUtils class.
5
4
  */
6
5
 
7
- /**
8
- * Stateless Function: Publishes tasks in batches.
9
- * @param {object} dependencies - { pubsub, logger }
10
- * @param {object} config - { topicName, tasks, taskType, maxPubsubBatchSize }
11
- */
12
6
  async function batchPublishTasks(dependencies, config) {
13
7
  const { pubsub, logger } = dependencies;
14
8
  const { topicName, tasks, taskType, maxPubsubBatchSize = 500 } = config;
@@ -33,7 +27,6 @@ async function batchPublishTasks(dependencies, config) {
33
27
 
34
28
  await Promise.all(batchPromises);
35
29
  messagesPublished += batchTasks.length;
36
- logger.log('TRACE', `[Core Utils] Published batch ${Math.ceil((i + 1) / maxPubsubBatchSize)} for ${taskType} (${batchTasks.length} messages)`);
37
30
  }
38
31
  logger.log('SUCCESS', `[Core Utils] Finished publishing ${messagesPublished} ${taskType} tasks to ${topicName}.`);
39
32
  } catch (error) {
@@ -42,39 +35,35 @@ async function batchPublishTasks(dependencies, config) {
42
35
  }
43
36
  }
44
37
 
45
- /**
46
- * Stateful Class Wrapper
47
- * Allows usage like: const utils = new PubSubUtils(deps); utils.batchPublishTasks(...)
48
- */
49
38
  class PubSubUtils {
50
39
  constructor(dependencies) {
51
40
  this.dependencies = dependencies;
52
41
  }
53
42
 
54
43
  /**
55
- * Hybrid method: Supports both (config) and (dependencies, config) signatures.
44
+ * [NEW] Publishes a single JSON message to a topic.
56
45
  */
57
- async batchPublishTasks(arg1, arg2) {
58
- // If called as (dependencies, config), use passed dependencies (Stateless/Legacy style)
59
- if (arg2) {
60
- return batchPublishTasks(arg1, arg2);
46
+ async publish(topicName, message) {
47
+ const { pubsub, logger } = this.dependencies;
48
+ const topic = pubsub.topic(topicName);
49
+ const dataBuffer = Buffer.from(JSON.stringify(message));
50
+
51
+ try {
52
+ await topic.publishMessage({ data: dataBuffer });
53
+ } catch (error) {
54
+ logger.log('ERROR', `[Core Utils] Failed to publish message to ${topicName}`, { error: error.message });
55
+ throw error;
61
56
  }
62
- // If called as (config), use this.dependencies (Stateful style)
57
+ }
58
+
59
+ async batchPublishTasks(arg1, arg2) {
60
+ if (arg2) return batchPublishTasks(arg1, arg2);
63
61
  return batchPublishTasks(this.dependencies, arg1);
64
62
  }
65
63
 
66
- /**
67
- * Helper for Computation System (Dispatcher)
68
- * Maps (topic, messages) -> batchPublishTasks
69
- */
70
64
  async publishMessageBatch(topicName, messages) {
71
- // Unpack {json: ...} structure if present
72
65
  const tasks = messages.map(m => m.json || m);
73
- const config = {
74
- topicName,
75
- tasks,
76
- taskType: 'computation-batch'
77
- };
66
+ const config = { topicName, tasks, taskType: 'computation-batch' };
78
67
  return batchPublishTasks(this.dependencies, config);
79
68
  }
80
69
  }
package/index.js CHANGED
@@ -29,8 +29,12 @@ const { handleUpdate } = require('./functions
29
29
  const { build: buildManifest } = require('./functions/computation-system/context/ManifestBuilder');
30
30
  const { dispatchComputationPass } = require('./functions/computation-system/helpers/computation_dispatcher');
31
31
  const { handleComputationTask } = require('./functions/computation-system/helpers/computation_worker');
32
- const { ensureBuildReport, generateBuildReport } = require('./functions/computation-system/tools/BuildReporter');
33
- // [NEW] Import Monitor
32
+ const {
33
+ requestBuildReport,
34
+ handleBuildReportTrigger,
35
+ generateBuildReport
36
+ } = require('./functions/computation-system/tools/BuildReporter');// [NEW] Import Monitor
37
+
34
38
  const { checkPassStatus } = require('./functions/computation-system/helpers/monitor');
35
39
 
36
40
  const dataLoader = require('./functions/computation-system/utils/data_loader');
@@ -92,9 +96,10 @@ const computationSystem = {
92
96
  dataLoader,
93
97
  computationUtils,
94
98
  buildManifest,
95
- ensureBuildReport,
99
+ // [UPDATED] Refactored Reporter Pipes
100
+ requestBuildReport,
101
+ handleBuildReportTrigger,
96
102
  generateBuildReport,
97
- // [NEW] Export Monitor Pipe
98
103
  checkPassStatus
99
104
  };
100
105
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.317",
3
+ "version": "1.0.319",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [