bulltrackers-module 1.0.324 → 1.0.325

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,7 @@
1
1
  /**
2
2
  * FILENAME: computation-system/helpers/computation_dispatcher.js
3
- * PURPOSE: Sequential Cursor-Based Dispatcher with Ledger Awareness, SimHash Stability, and Session Caching.
4
- * UPDATED: Fixed Ledger Blindness, Cursor Shifting, and Live Analysis Disconnect.
3
+ * PURPOSE: Sequential Cursor-Based Dispatcher.
4
+ * BEHAVIOR: Dispatch -> Wait ETA -> Next Date.
5
5
  */
6
6
 
7
7
  const { getExpectedDateStrings, getEarliestDataDates, normalizeName, DEFINITIVE_EARLIEST_DATES } = require('../utils/utils.js');
@@ -34,7 +34,7 @@ async function filterActiveTasks(db, date, pass, tasks) {
34
34
  data.completedAt &&
35
35
  (Date.now() - new Date(data.completedAt).getTime() < 60 * 1000);
36
36
 
37
- if (isActive || isJustFinished) return null;
37
+ if (isActive || isJustFinished) return null; // Filter out
38
38
  }
39
39
  return t;
40
40
  });
@@ -50,18 +50,13 @@ async function attemptSimHashResolution(dependencies, date, tasks, dailyStatus,
50
50
  const { db, logger } = dependencies;
51
51
  const resolvedTasks = [];
52
52
  const remainingTasks = [];
53
-
54
- // Cache for SimHashes to avoid redundant DB lookups in loop
55
53
  const simHashCache = new Map();
56
54
 
57
55
  for (const task of tasks) {
58
- // Only apply to Re-Runs (Hash Mismatches), not fresh runs (Missing Data)
59
56
  const currentStatus = dailyStatus ? dailyStatus[task.name] : null;
60
57
  const manifestItem = manifestMap.get(normalizeName(task.name));
61
58
 
62
59
  if (currentStatus && currentStatus.simHash && manifestItem) {
63
-
64
- // 1. Get the SimHash for the NEW code (from Registry)
65
60
  let newSimHash = simHashCache.get(manifestItem.hash);
66
61
  if (!newSimHash) {
67
62
  const simDoc = await db.collection('system_simhash_registry').doc(manifestItem.hash).get();
@@ -71,13 +66,12 @@ async function attemptSimHashResolution(dependencies, date, tasks, dailyStatus,
71
66
  }
72
67
  }
73
68
 
74
- // 2. Compare
75
69
  if (newSimHash && newSimHash === currentStatus.simHash) {
76
70
  resolvedTasks.push({
77
71
  name: task.name,
78
72
  hash: manifestItem.hash,
79
73
  simHash: newSimHash,
80
- prevStatus: currentStatus // Pass previous status to preserve other fields
74
+ prevStatus: currentStatus
81
75
  });
82
76
  continue;
83
77
  }
@@ -85,30 +79,24 @@ async function attemptSimHashResolution(dependencies, date, tasks, dailyStatus,
85
79
  remainingTasks.push(task);
86
80
  }
87
81
 
88
- // 3. Apply Updates for Stable Tasks
89
82
  if (resolvedTasks.length > 0) {
90
83
  const updatePayload = {};
91
-
92
84
  resolvedTasks.forEach(t => {
93
- // [FIXED] Construct full nested object to avoid dot-notation issues with .set()
94
- // We merge existing data (like resultHash) so we don't lose the valid calculation output
95
85
  updatePayload[t.name] = {
96
- ...(t.prevStatus || {}), // Keep existing resultHash, output, etc.
97
- hash: t.hash, // Update to new code hash
98
- simHash: t.simHash, // Confirmed stable simHash
86
+ ...(t.prevStatus || {}),
87
+ hash: t.hash,
88
+ simHash: t.simHash,
99
89
  reason: 'SimHash Stable (Auto-Resolved)',
100
90
  lastUpdated: new Date().toISOString()
101
91
  };
102
92
  });
103
-
104
- // Use set with merge: true. Now that keys are "clean" (no dots),
105
- // objects will merge correctly into the document structure.
106
93
  await db.collection('computation_status').doc(date).set(updatePayload, { merge: true });
107
94
  logger.log('INFO', `[SimHash] ⏩ Fast-forwarded ${resolvedTasks.length} tasks for ${date} (Logic Unchanged).`);
108
95
  }
109
96
 
110
97
  return remainingTasks;
111
98
  }
99
+
112
100
  // =============================================================================
113
101
  // HELPER: Stable Session Management (Solves Cursor Shifting)
114
102
  // =============================================================================
@@ -117,36 +105,22 @@ async function getStableDateSession(config, dependencies, passToRun, dateLimitSt
117
105
  const sessionId = `pass_${passToRun}_${dateLimitStr.replace(/-/g, '')}`;
118
106
  const sessionRef = db.collection('dispatcher_sessions').doc(sessionId);
119
107
 
120
- // 1. Try to Load Session
121
108
  if (!forceRebuild) {
122
109
  const sessionSnap = await sessionRef.get();
123
110
  if (sessionSnap.exists) {
124
111
  const data = sessionSnap.data();
125
- const age = Date.now() - new Date(data.createdAt).getTime();
126
- if (age < SESSION_CACHE_DURATION_MS) {
127
- logger.log('INFO', `[Session] 📂 Loaded stable session for Pass ${passToRun} (${data.dates.length} dates).`);
112
+ if ((Date.now() - new Date(data.createdAt).getTime()) < SESSION_CACHE_DURATION_MS) {
113
+ // logger.log('INFO', `[Session] 📂 Loaded stable session for Pass ${passToRun}.`);
128
114
  return data.dates;
129
115
  }
130
116
  }
131
117
  }
132
118
 
133
- // 2. Rebuild Session (Expensive Scan)
134
119
  logger.log('INFO', `[Session] 🔄 Rebuilding dispatch session for Pass ${passToRun}...`);
135
120
  const earliestDates = await getEarliestDataDates(config, dependencies);
136
121
  const allDates = getExpectedDateStrings(earliestDates.absoluteEarliest, new Date(dateLimitStr + 'T00:00:00Z'));
137
122
 
138
- // We only want dates that *might* be dirty.
139
- // Optimization: We add ALL dates to the list. The dispatcher checks them individually.
140
- // Why? Because if we pre-filter here, we repeat the work of the dispatcher.
141
- // Better: Store the plain list of dates sorted descending (newest first usually better for backfills, ascending for standard).
142
- // Let's stick to Ascending (oldest first) as standard.
143
-
144
- await sessionRef.set({
145
- dates: allDates,
146
- createdAt: new Date().toISOString(),
147
- configHash: dateLimitStr // Simple versioning
148
- });
149
-
123
+ await sessionRef.set({ dates: allDates, createdAt: new Date().toISOString(), configHash: dateLimitStr });
150
124
  return allDates;
151
125
  }
152
126
 
@@ -157,9 +131,9 @@ async function dispatchComputationPass(config, dependencies, computationManifest
157
131
  const { logger, db } = dependencies;
158
132
  const pubsubUtils = new PubSubUtils(dependencies);
159
133
 
160
- const passToRun = String(reqBody.pass || config.COMPUTATION_PASS_TO_RUN || "1");
134
+ const passToRun = String(reqBody.pass || "1");
161
135
  const targetCursorN = parseInt(reqBody.cursorIndex || 1);
162
- const dateLimitStr = reqBody.date || config.date || "2025-01-01";
136
+ const dateLimitStr = reqBody.date || "2025-01-01";
163
137
  const forceRebuild = reqBody.forceRebuild === true;
164
138
 
165
139
  const manifestMap = new Map(computationManifest.map(c => [normalizeName(c.name), c]));
@@ -167,38 +141,26 @@ async function dispatchComputationPass(config, dependencies, computationManifest
167
141
  const calcsInThisPass = passes[passToRun] || [];
168
142
  const manifestWeightMap = new Map(computationManifest.map(c => [normalizeName(c.name), c.weight || 1.0]));
169
143
 
170
- if (!calcsInThisPass.length) {
171
- return { status: 'MOVE_TO_NEXT_PASS', dispatched: 0 };
172
- }
144
+ if (!calcsInThisPass.length) return { status: 'MOVE_TO_NEXT_PASS', dispatched: 0 };
173
145
 
174
- // 1. Get Stable Date List (Solves Shifting Cursor)
146
+ // 1. Get Stable Date List
175
147
  const sessionDates = await getStableDateSession(config, dependencies, passToRun, dateLimitStr, forceRebuild);
176
-
177
- if (!sessionDates || sessionDates.length === 0) {
178
- return { status: 'MOVE_TO_NEXT_PASS', dispatched: 0 };
179
- }
148
+ if (!sessionDates || sessionDates.length === 0) return { status: 'MOVE_TO_NEXT_PASS', dispatched: 0 };
180
149
 
181
- // 2. Select Date based on Cursor
150
+ // 2. Select Date
182
151
  let selectedDate = null;
183
152
  let selectedTasks = [];
184
- let isReroute = false;
185
- let isSweep = false;
186
153
 
187
- // Check bounds
188
154
  if (targetCursorN <= sessionDates.length) {
189
- // Normal Operation
190
155
  selectedDate = sessionDates[targetCursorN - 1];
191
156
  } else {
192
- // End of list
193
157
  return { status: 'MOVE_TO_NEXT_PASS', dispatched: 0 };
194
158
  }
195
159
 
196
- // 3. Analyze SPECIFIC Date (Live Analysis)
197
- // We only fetch status for the ONE date we are looking at + context
160
+ // 3. Analyze SPECIFIC Date
198
161
  if (selectedDate) {
199
- // A. Fetch Context
200
- const needsHistory = calcsInThisPass.some(c => c.isHistorical);
201
162
  const earliestDates = await getEarliestDataDates(config, dependencies);
163
+ const needsHistory = calcsInThisPass.some(c => c.isHistorical);
202
164
 
203
165
  let prevDailyStatusPromise = Promise.resolve(null);
204
166
  if (needsHistory) {
@@ -219,51 +181,42 @@ async function dispatchComputationPass(config, dependencies, computationManifest
219
181
  const report = analyzeDateExecution(selectedDate, calcsInThisPass, availability.status, dailyStatus, manifestMap, prevDailyStatus);
220
182
  let rawTasks = [...report.runnable, ...report.reRuns];
221
183
 
222
- // B. Apply SimHash Resolution (Problem #1)
223
184
  if (rawTasks.length > 0) {
224
185
  rawTasks = await attemptSimHashResolution(dependencies, selectedDate, rawTasks, dailyStatus, manifestMap);
225
- }
226
-
227
- // C. Apply Ledger Filter (Problem #2)
228
- if (rawTasks.length > 0) {
186
+
187
+ // Ledger Filter: Removes tasks that are already running
229
188
  selectedTasks = await filterActiveTasks(db, selectedDate, passToRun, rawTasks);
230
189
  }
231
190
 
232
- // D. Check for High-Mem Reroutes (OOM handling)
191
+ // OOM / High-Mem Reroute Check
233
192
  if (selectedTasks.length > 0) {
234
193
  const reroutes = await getHighMemReroutes(db, selectedDate, passToRun, selectedTasks);
235
194
  if (reroutes.length > 0) {
236
195
  selectedTasks = reroutes;
237
- isReroute = true;
238
196
  }
239
197
  }
240
- } else {
241
- logger.log('WARN', `[Dispatcher] Date ${selectedDate} skipped (Data Unavailable).`);
242
198
  }
243
199
  }
244
200
 
245
201
  // 4. Dispatch Logic
246
202
  if (selectedTasks.length === 0) {
247
- // Nothing to do for this date.
248
- // CRITICAL: We return dispatched: 0, but n_cursor_ignored: FALSE.
249
- // This tells workflow to increment cursor and check the next date in the Stable Session.
203
+ // Return 0 dispatched, FALSE cursor ignored -> Move to NEXT date immediately.
250
204
  return {
251
205
  status: 'CONTINUE_PASS',
252
206
  dateProcessed: selectedDate,
253
207
  dispatched: 0,
254
- n_cursor_ignored: false, // Proceed to next date
208
+ n_cursor_ignored: false,
255
209
  etaSeconds: 0,
256
210
  remainingDates: sessionDates.length - targetCursorN
257
211
  };
258
212
  }
259
213
 
260
- // 5. Publish Tasks
214
+ // 5. Send Tasks
261
215
  const totalweight = selectedTasks.reduce((sum, t) => sum + (manifestWeightMap.get(normalizeName(t.name)) || 1.0), 0);
262
216
  const currentDispatchId = crypto.randomUUID();
263
217
  const etaSeconds = Math.max(20, Math.ceil(totalweight * BASE_SECONDS_PER_WEIGHT_UNIT));
264
218
 
265
219
  const taskDetails = selectedTasks.map(t => `${t.name} (${t.reason})`);
266
-
267
220
  logger.log('INFO', `[Dispatcher] ✅ Dispatching ${selectedTasks.length} tasks for ${selectedDate}.`, {
268
221
  date: selectedDate,
269
222
  pass: passToRun,
@@ -305,13 +258,14 @@ async function dispatchComputationPass(config, dependencies, computationManifest
305
258
  }
306
259
  await Promise.all(pubPromises);
307
260
 
308
- // CRITICAL: We dispatched work. We want to check THIS date again next time
309
- // to ensure tasks completed. So we IGNORE cursor increment.
261
+ // CRITICAL: We dispatched work.
262
+ // We return n_cursor_ignored: FALSE.
263
+ // This tells the workflow to Wait ETA -> Increment Cursor -> Move to Next Date.
310
264
  return {
311
265
  status: 'CONTINUE_PASS',
312
266
  dateProcessed: selectedDate,
313
267
  dispatched: selectedTasks.length,
314
- n_cursor_ignored: true, // Hold cursor until this date is clean
268
+ n_cursor_ignored: false, // FORCE NEXT DATE
315
269
  etaSeconds: etaSeconds,
316
270
  remainingDates: sessionDates.length - targetCursorN
317
271
  };
@@ -1,5 +1,5 @@
1
1
  # Cloud Workflows: Precision Cursor-Based Orchestrator
2
- # UPDATED: Added satiation detection to break early on 0 remaining dates.
2
+ # SIMPLE MODE: Dispatch -> Wait ETA -> Next Date
3
3
 
4
4
  main:
5
5
  params: [input]
@@ -20,7 +20,6 @@ main:
20
20
  assign:
21
21
  - n_cursor: 1
22
22
  - pass_complete: false
23
- - consecutive_empty_dispatches: 0
24
23
 
25
24
  - sequential_date_loop:
26
25
  switch:
@@ -39,26 +38,25 @@ main:
39
38
 
40
39
  - evaluate_dispatch:
41
40
  switch:
41
+ # 1. End of Session (Dispatcher reached end of date list)
42
42
  - condition: '${dispatch_res.body.status == "MOVE_TO_NEXT_PASS"}'
43
43
  assign:
44
44
  - pass_complete: true
45
45
 
46
- # NEW: Explicit Satiation Check
47
- - condition: '${dispatch_res.body.status == "CONTINUE_PASS" and dispatch_res.body.remainingDates == 0}'
46
+ # 2. Satiation Check (Specific to date/logic)
47
+ - condition: '${dispatch_res.body.status == "CONTINUE_PASS" and dispatch_res.body.remainingDates == 0 and dispatch_res.body.dispatched == 0}'
48
48
  steps:
49
49
  - log_satiation:
50
50
  call: sys.log
51
51
  args:
52
- text: '${"Pass " + pass_id + " - ✅ Pass satiated (0 remaining dates). Moving to next pass."}'
52
+ text: '${"Pass " + pass_id + " - ✅ Pass satiated (0 remaining). Next pass."}'
53
53
  - mark_complete:
54
54
  assign:
55
55
  - pass_complete: true
56
56
 
57
+ # 3. Work Dispatched: Wait ETA -> Move Next (Ignored flag is FALSE)
57
58
  - condition: '${dispatch_res.body.dispatched > 0}'
58
59
  steps:
59
- - reset_retry_counter:
60
- assign:
61
- - consecutive_empty_dispatches: 0
62
60
  - wait_for_completion:
63
61
  call: sys.sleep
64
62
  args:
@@ -69,26 +67,18 @@ main:
69
67
  - next_loop_work:
70
68
  next: sequential_date_loop
71
69
 
70
+ # 4. No Work (Clean or Busy): Move Next Immediately
72
71
  - condition: '${dispatch_res.body.dispatched == 0}'
73
72
  steps:
74
- - increment_retry:
73
+ - wait_short:
74
+ call: sys.sleep
75
+ args:
76
+ seconds: 2 # Tiny debounce
77
+ - update_cursor_retry:
75
78
  assign:
76
- - consecutive_empty_dispatches: '${consecutive_empty_dispatches + 1}'
77
- - check_break_condition:
78
- switch:
79
- - condition: '${consecutive_empty_dispatches >= 3}'
80
- assign:
81
- - pass_complete: true
82
- - condition: '${true}'
83
- steps:
84
- - wait_short:
85
- call: sys.sleep
86
- args:
87
- seconds: 5
88
- - update_cursor_retry:
89
- assign:
90
- - n_cursor: '${if(dispatch_res.body.n_cursor_ignored, n_cursor, n_cursor + 1)}'
91
- - next_loop_retry:
92
- next: sequential_date_loop
79
+ # Dispatcher sends n_cursor_ignored=false, so we increment.
80
+ - n_cursor: '${if(dispatch_res.body.n_cursor_ignored, n_cursor, n_cursor + 1)}'
81
+ - next_loop_retry:
82
+ next: sequential_date_loop
93
83
  - finish:
94
84
  return: "Pipeline Execution Satiated and Complete"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bulltrackers-module",
3
- "version": "1.0.324",
3
+ "version": "1.0.325",
4
4
  "description": "Helper Functions for Bulltrackers.",
5
5
  "main": "index.js",
6
6
  "files": [