bulltrackers-module 1.0.306 → 1.0.307

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,277 +1,151 @@
1
1
  /**
2
- * @fileoverview Main Orchestrator. Coordinates the topological execution.
3
- * UPDATED: Includes Content-Based Dependency Short-Circuiting.
4
- * UPDATED: Includes 'Audit Upgrade' check.
5
- * UPDATED: Detailed Dependency Reporting for Impossible Chains.
2
+ * FILENAME: computation-system/WorkflowOrchestrator.js
3
+ * UPDATED: Historical Continuity is now a first-class "Temporal Dependency".
4
+ * Includes Content-Based Short-Circuiting for both Upstream and Historical dependencies.
6
5
  */
6
+
7
7
  const { normalizeName, DEFINITIVE_EARLIEST_DATES } = require('./utils/utils');
8
8
  const { checkRootDataAvailability, checkRootDependencies } = require('./data/AvailabilityChecker');
9
9
  const { fetchExistingResults } = require('./data/DependencyFetcher');
10
10
  const { fetchComputationStatus, updateComputationStatus } = require('./persistence/StatusRepository');
11
11
  const { StandardExecutor } = require('./executors/StandardExecutor');
12
12
  const { MetaExecutor } = require('./executors/MetaExecutor');
13
- const { generateProcessId, PROCESS_TYPES } = require('./logger/logger');
14
13
 
15
14
  const STATUS_IMPOSSIBLE_PREFIX = 'IMPOSSIBLE';
16
15
 
17
- function groupByPass(manifest) { return manifest.reduce((acc, calc) => { (acc[calc.pass] = acc[calc.pass] || []).push(calc); return acc; }, {}); }
18
-
19
16
  /**
20
- * Analyzes whether calculations should run, be skipped, or are blocked.
21
- * [NEW] Implements ResultHash short-circuit logic.
17
+ * [NEW] Core Short-Circuit Logic.
18
+ * Checks if a dependency (either a different node or "yesterday's self") is satisfied.
22
19
  */
23
- function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus, manifestMap, prevDailyStatus = null) {
24
- const report = { runnable: [], blocked: [], impossible: [], failedDependency: [], reRuns: [], skipped: [] };
25
- const simulationStatus = { ...dailyStatus };
26
- const isTargetToday = (dateStr === new Date().toISOString().slice(0, 10));
27
-
28
- // Helper: Validates if a dependency is satisfied, either by Code Match OR Content Match
29
- const isDepSatisfied = (depName, currentStatusMap, manifestMap, dependentStoredStatus) => {
30
- const norm = normalizeName(depName);
31
- const storedDep = currentStatusMap[norm];
32
- const depManifest = manifestMap.get(norm);
33
-
34
- // 1. Basic Existence Checks
35
- if (!storedDep) return false;
36
- if (typeof storedDep.hash === 'string' && storedDep.hash.startsWith(STATUS_IMPOSSIBLE_PREFIX)) return false;
37
- if (!depManifest) return false;
38
-
39
- // 2. Code Hash Check (The Standard Check)
40
- if (storedDep.hash === depManifest.hash) return true;
20
+ function isDependencyReady(depName, isHistoricalSelf, currentStatusMap, prevStatusMap, manifestMap, storedStatus) {
21
+ const norm = normalizeName(depName);
22
+ const targetStatus = isHistoricalSelf ? (prevStatusMap ? prevStatusMap[norm] : null) : currentStatusMap[norm];
23
+ const depManifest = manifestMap.get(norm);
24
+
25
+ if (!targetStatus) return { ready: false, reason: 'Missing' };
26
+ if (String(targetStatus.hash).startsWith(STATUS_IMPOSSIBLE_PREFIX)) return { ready: false, reason: 'Impossible Upstream' };
27
+
28
+ // 1. Code Hash Match (Strict)
29
+ if (targetStatus.hash === depManifest.hash) return { ready: true };
30
+
31
+ // 2. Content Hash Match (Short-Circuit)
32
+ // If our code didn't change, check if the output of the dependency is what we expect.
33
+ const lastSeenResultHash = storedStatus?.dependencyResultHashes?.[depName];
34
+ if (lastSeenResultHash && targetStatus.resultHash === lastSeenResultHash) {
35
+ return { ready: true, shortCircuited: true };
36
+ }
41
37
 
42
- // 3. [NEW] Content-Based Short-Circuit Check
43
- // If Code Hash mismatch, check if the *Result Hash* is identical to what we used last time.
44
- // dependentStoredStatus = The status of the calculation (B) that depends on this (A).
45
- // dependentStoredStatus.dependencyResultHashes[depName] = The ResultHash of A when B last ran.
46
- // storedDep.resultHash = The current ResultHash of A.
47
- if (dependentStoredStatus &&
48
- dependentStoredStatus.dependencyResultHashes &&
49
- dependentStoredStatus.dependencyResultHashes[depName] &&
50
- storedDep.resultHash &&
51
- storedDep.resultHash === dependentStoredStatus.dependencyResultHashes[depName]) {
52
- return true; // Short-circuit: The output didn't change, so we are safe.
53
- }
38
+ return { ready: false, reason: 'Hash Mismatch' };
39
+ }
54
40
 
55
- return false;
56
- };
41
+ function analyzeDateExecution(dateStr, calcsInPass, rootDataStatus, dailyStatus, manifestMap, prevDailyStatus = null) {
42
+ const report = { runnable: [], blocked: [], impossible: [], failedDependency: [], reRuns: [], skipped: [] };
43
+ const simulationStatus = { ...dailyStatus };
57
44
 
58
45
  for (const calc of calcsInPass) {
59
- const cName = normalizeName(calc.name);
60
- const stored = simulationStatus[cName];
61
- const storedHash = stored ? stored.hash : null;
62
- const storedCategory = stored ? stored.category : null;
63
- const currentHash = calc.hash;
64
-
65
- // Collect current result hashes of dependencies for the next run
66
- const currentDependencyResultHashes = {};
67
- if (calc.dependencies) {
68
- calc.dependencies.forEach(d => {
69
- const normD = normalizeName(d);
70
- if (simulationStatus[normD] && simulationStatus[normD].resultHash) {
71
- currentDependencyResultHashes[d] = simulationStatus[normD].resultHash;
72
- }
73
- });
74
- }
75
-
76
- const markImpossible = (reason, type = 'GENERIC') => {
77
- report.impossible.push({ name: cName, reason });
78
- const statusHash = `${STATUS_IMPOSSIBLE_PREFIX}:${type}`;
79
- simulationStatus[cName] = { hash: statusHash, category: calc.category };
80
- };
81
-
82
- const markRunnable = (isReRun = false, reRunDetails = null) => {
83
- const payload = {
84
- name: cName,
85
- ...reRunDetails,
86
- dependencyResultHashes: currentDependencyResultHashes // Pass forward
87
- };
88
- if (isReRun) report.reRuns.push(payload);
89
- else report.runnable.push(payload);
90
- // Simulate success so dependents can pass their check
91
- simulationStatus[cName] = {
92
- hash: currentHash,
93
- resultHash: 'SIMULATED',
94
- category: calc.category,
95
- composition: calc.composition
96
- };
97
- };
46
+ const cName = normalizeName(calc.name);
47
+ const stored = simulationStatus[cName];
48
+ const currentHash = calc.hash;
98
49
 
99
- let migrationOldCategory = null;
100
- if (storedCategory && storedCategory !== calc.category) { migrationOldCategory = storedCategory; }
101
-
102
- // 1. Check Root Data
50
+ // 1. Root Data Check
103
51
  const rootCheck = checkRootDependencies(calc, rootDataStatus);
104
-
105
52
  if (!rootCheck.canRun) {
106
- const missingStr = rootCheck.missing.join(', ');
107
- if (!isTargetToday) {
108
- markImpossible(`Missing Root Data: ${missingStr} (Historical)`, 'NO_DATA');
53
+ if (dateStr !== new Date().toISOString().slice(0, 10)) {
54
+ report.impossible.push({ name: cName, reason: `Missing Root: ${rootCheck.missing.join(', ')}` });
55
+ simulationStatus[cName] = { hash: `${STATUS_IMPOSSIBLE_PREFIX}:NO_DATA` };
109
56
  } else {
110
- report.blocked.push({ name: cName, reason: `Missing Root Data: ${missingStr} (Waiting)` });
57
+ report.blocked.push({ name: cName, reason: `Waiting for Root Data` });
111
58
  }
112
59
  continue;
113
60
  }
114
61
 
115
- // 2. Check Dependencies
116
- let dependencyIsImpossible = false;
117
- let impossibleDepCause = null;
62
+ // 2. Dependency & Temporal Check
118
63
  const missingDeps = [];
64
+ let isBlockedByHistory = false;
65
+
66
+ // A. Standard Upstream Dependencies
119
67
  if (calc.dependencies) {
120
68
  for (const dep of calc.dependencies) {
121
- const normDep = normalizeName(dep);
122
- const depStored = simulationStatus[normDep];
123
- if (depStored && typeof depStored.hash === 'string' && depStored.hash.startsWith(STATUS_IMPOSSIBLE_PREFIX)) {
124
- dependencyIsImpossible = true;
125
- impossibleDepCause = dep;
126
- break;
127
- }
128
- // Pass 'stored' (this calc's status) to check short-circuiting
129
- if (!isDepSatisfied(dep, simulationStatus, manifestMap, stored)) { missingDeps.push(dep); }
69
+ const check = isDependencyReady(dep, false, simulationStatus, null, manifestMap, stored);
70
+ if (!check.ready) missingDeps.push(dep);
130
71
  }
131
72
  }
132
73
 
133
- if (dependencyIsImpossible) {
134
- markImpossible(`Dependency is Impossible (${impossibleDepCause})`, 'UPSTREAM');
135
- continue;
136
- }
137
- if (missingDeps.length > 0) { report.failedDependency.push({ name: cName, missing: missingDeps }); continue; }
138
-
139
- // 3. Check Historical Continuity
140
- if (calc.isHistorical && prevDailyStatus) {
74
+ // B. [UPGRADED] Temporal Dependency (Yesterday's Self)
75
+ if (calc.isHistorical) {
141
76
  const yesterday = new Date(dateStr + 'T00:00:00Z');
142
77
  yesterday.setUTCDate(yesterday.getUTCDate() - 1);
78
+
143
79
  if (yesterday >= DEFINITIVE_EARLIEST_DATES.absoluteEarliest) {
144
- const prevStored = prevDailyStatus[cName];
145
- if (!prevStored || prevStored.hash !== currentHash) {
146
- report.blocked.push({ name: cName, reason: `Waiting for historical continuity (Yesterday ${!prevStored ? 'Missing' : 'Hash Mismatch'})` });
147
- continue;
148
- }
80
+ const check = isDependencyReady(calc.name, true, null, prevDailyStatus, manifestMap, stored);
81
+ if (!check.ready) isBlockedByHistory = true;
149
82
  }
150
83
  }
151
-
152
- // 4. Check Hash / Composition (The Audit Gate)
153
- if (!storedHash) {
154
- markRunnable(false, { reason: "New Calculation" });
155
- }
156
- else if (storedHash !== currentHash) {
157
- // [NEW] Check if Dependencies caused this, and if their content is actually same
158
- // Note: If we are here, it means code changed.
159
- // Short-circuiting logic was handled in 'isDepSatisfied' for upstream checks.
160
- // But if *my* code changed, I must re-run unless I implement output-caching which is dangerous.
161
- // So we strictly re-run if code changes.
162
-
163
- let changeReason = "Hash Mismatch (Unknown)";
164
- const oldComp = stored.composition;
165
- const newComp = calc.composition;
166
84
 
167
- if (oldComp && newComp) {
168
- if (oldComp.code !== newComp.code) {
169
- changeReason = "Code Changed";
170
- }
171
- else if (JSON.stringify(oldComp.layers) !== JSON.stringify(newComp.layers)) {
172
- const changedLayers = [];
173
- for(const lKey in newComp.layers) {
174
- if (newComp.layers[lKey] !== oldComp.layers[lKey]) changedLayers.push(lKey);
175
- }
176
- changeReason = `Layer Update: [${changedLayers.join(', ')}]`;
177
- }
178
- else if (JSON.stringify(oldComp.deps) !== JSON.stringify(newComp.deps)) {
179
- // Dependency Hash Mismatch.
180
- // This is where we COULD have short-circuited if we weren't enforcing code-hash strictness here.
181
- // But typically if code hash mismatches, we re-run.
182
- // The "Short-Circuit" benefit is mainly that *dependents* of this calculation
183
- // won't need to re-run if *this* calculation produces the same output.
184
- const changedDeps = [];
185
- for(const dKey in newComp.deps) {
186
- if (newComp.deps[dKey] !== oldComp.deps[dKey]) changedDeps.push(dKey);
187
- }
188
- changeReason = `Upstream Change: [${changedDeps.join(', ')}]`;
189
- }
190
- else {
191
- changeReason = "Logic/Epoch Change";
192
- }
85
+ if (missingDeps.length > 0) {
86
+ const isImpossible = missingDeps.some(d => simulationStatus[normalizeName(d)]?.hash?.startsWith(STATUS_IMPOSSIBLE_PREFIX));
87
+ if (isImpossible) {
88
+ report.impossible.push({ name: cName, reason: 'Upstream Impossible' });
89
+ simulationStatus[cName] = { hash: `${STATUS_IMPOSSIBLE_PREFIX}:UPSTREAM` };
193
90
  } else {
194
- changeReason = "Hash Mismatch (No prior composition)";
91
+ report.failedDependency.push({ name: cName, missing: missingDeps });
195
92
  }
93
+ continue;
94
+ }
95
+
96
+ if (isBlockedByHistory) {
97
+ report.blocked.push({ name: cName, reason: 'Waiting for Yesterday' });
98
+ continue;
99
+ }
196
100
 
197
- markRunnable(true, {
198
- name: cName,
199
- oldHash: storedHash,
200
- newHash: currentHash,
201
- previousCategory: migrationOldCategory,
202
- reason: changeReason
203
- });
204
- }
205
- else if (migrationOldCategory) {
206
- markRunnable(true, { name: cName, reason: 'Category Migration', previousCategory: migrationOldCategory, newCategory: calc.category });
207
- }
208
- else if (!stored.composition) {
209
- markRunnable(true, {
210
- name: cName,
211
- oldHash: storedHash,
212
- newHash: currentHash,
213
- reason: 'Audit Upgrade (Populating Composition Metadata)'
101
+ // 3. Runnable / Skip Logic
102
+ const currentDependencyResultHashes = {};
103
+ if (calc.dependencies) {
104
+ calc.dependencies.forEach(d => {
105
+ const resHash = simulationStatus[normalizeName(d)]?.resultHash;
106
+ if (resHash) currentDependencyResultHashes[d] = resHash;
214
107
  });
215
108
  }
216
- else {
217
- report.skipped.push({ name: cName, reason: "Up To Date" });
218
- simulationStatus[cName] = { hash: currentHash, category: calc.category, composition: calc.composition };
109
+
110
+ const taskPayload = { name: cName, dependencyResultHashes: currentDependencyResultHashes };
111
+
112
+ if (!stored?.hash) {
113
+ report.runnable.push({ ...taskPayload, reason: "New Calculation" });
114
+ simulationStatus[cName] = { hash: currentHash, resultHash: 'SIMULATED' };
115
+ } else if (stored.hash !== currentHash) {
116
+ report.reRuns.push({ ...taskPayload, oldHash: stored.hash, newHash: currentHash, reason: "Hash Mismatch" });
117
+ simulationStatus[cName] = { hash: currentHash, resultHash: 'SIMULATED' };
118
+ } else {
119
+ report.skipped.push({ name: cName, reason: "Up To Date" });
219
120
  }
220
121
  }
221
122
  return report;
222
123
  }
223
124
 
224
- /**
225
- * DIRECT EXECUTION PIPELINE (For Workers)
226
- * [UPDATED] Accepts dependencyResultHashes
227
- */
228
125
  async function executeDispatchTask(dateStr, pass, targetComputation, config, dependencies, computationManifest, previousCategory = null, dependencyResultHashes = {}) {
229
126
  const { logger } = dependencies;
230
- const pid = generateProcessId(PROCESS_TYPES.EXECUTOR, targetComputation, dateStr);
231
-
232
127
  const manifestMap = new Map(computationManifest.map(c => [normalizeName(c.name), c]));
233
128
  const calcManifest = manifestMap.get(normalizeName(targetComputation));
234
129
 
235
- if (!calcManifest) { throw new Error(`Calculation '${targetComputation}' not found in manifest.`); }
236
-
237
- // [NEW] Attach the dependency result hashes to the manifest so ResultCommitter can save them
130
+ if (!calcManifest) throw new Error(`Calc '${targetComputation}' not found.`);
238
131
  calcManifest.dependencyResultHashes = dependencyResultHashes;
239
132
 
240
- if (previousCategory) {
241
- calcManifest.previousCategory = previousCategory;
242
- logger.log('INFO', `[Executor] Migration detected for ${calcManifest.name}. Old data will be cleaned from: ${previousCategory}`);
243
- }
244
-
245
133
  const rootData = await checkRootDataAvailability(dateStr, config, dependencies, DEFINITIVE_EARLIEST_DATES);
246
- if (!rootData) {
247
- logger.log('ERROR', `[Executor] FATAL: Root data check failed for ${targetComputation} on ${dateStr}.`);
248
- return;
249
- }
250
-
251
134
  const calcsToRun = [calcManifest];
252
- const existingResults = await fetchExistingResults(dateStr, calcsToRun, computationManifest, config, dependencies, false);
253
135
 
136
+ const existingResults = await fetchExistingResults(dateStr, calcsToRun, computationManifest, config, dependencies, false);
254
137
  let previousResults = {};
255
138
  if (calcManifest.isHistorical) {
256
- const prevDate = new Date(dateStr + 'T00:00:00Z');
257
- prevDate.setUTCDate(prevDate.getUTCDate() - 1);
258
- const prevDateStr = prevDate.toISOString().slice(0, 10);
259
- previousResults = await fetchExistingResults(prevDateStr, calcsToRun, computationManifest, config, dependencies, true);
139
+ const prev = new Date(dateStr + 'T00:00:00Z'); prev.setUTCDate(prev.getUTCDate() - 1);
140
+ previousResults = await fetchExistingResults(prev.toISOString().slice(0, 10), calcsToRun, computationManifest, config, dependencies, true);
260
141
  }
261
142
 
262
- logger.log('INFO', `[Executor] Running ${calcManifest.name} for ${dateStr}`, { processId: pid });
263
- let resultUpdates = {};
143
+ const execDate = new Date(dateStr + 'T00:00:00Z');
144
+ const updates = (calcManifest.type === 'standard')
145
+ ? await StandardExecutor.run(execDate, calcsToRun, `Pass ${pass}`, config, dependencies, rootData, existingResults, previousResults)
146
+ : await MetaExecutor.run(execDate, calcsToRun, `Pass ${pass}`, config, dependencies, existingResults, previousResults, rootData);
264
147
 
265
- try {
266
- if (calcManifest.type === 'standard') { resultUpdates = await StandardExecutor.run(new Date(dateStr + 'T00:00:00Z'), [calcManifest], `Pass ${pass}`, config, dependencies, rootData, existingResults, previousResults);
267
- } else if (calcManifest.type === 'meta') { resultUpdates = await MetaExecutor.run (new Date(dateStr + 'T00:00:00Z'), [calcManifest], `Pass ${pass}`, config, dependencies, existingResults, previousResults, rootData);
268
- }
269
- logger.log('INFO', `[Executor] Success: ${calcManifest.name} for ${dateStr}`);
270
- return { date: dateStr, updates: resultUpdates };
271
- } catch (err) {
272
- logger.log('ERROR', `[Executor] Failed ${calcManifest.name}: ${err.message}`, { processId: pid, stack: err.stack });
273
- throw err;
274
- }
148
+ return { date: dateStr, updates };
275
149
  }
276
150
 
277
- module.exports = { executeDispatchTask, groupByPass, analyzeDateExecution };
151
+ module.exports = { executeDispatchTask, analyzeDateExecution };