bulltrackers-module 1.0.211 → 1.0.212

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,284 +1,324 @@
1
- /**
2
- * @fileoverview
3
- * Dynamic Manifest Builder (v3 - Refactored)
4
- *
5
- * This script builds the computation manifest dynamically by introspecting all
6
- * calculation classes from the new product-line structure (core, gem, gauss, etc.).
7
- * It validates metadata, resolves dependencies, and performs a topological sort
8
- * to determine execution passes.
9
- *
10
- * UPDATED LOGIC:
11
- * - Core Computations: Metadata 'category' determines the output path/grouping.
12
- * - Product Lines: Folder name STRICTLY determines the category (metadata ignored).
13
- */
14
-
15
- /* --------------------------------------------------
16
- * Pretty Console Helpers
17
- * -------------------------------------------------- */
18
- const log = {
19
- info: (msg) => console.log('ℹ︎ ' + msg),
20
- step: (msg) => console.log('› ' + msg),
21
- warn: (msg) => console.warn('⚠︎ ' + msg),
22
- success: (msg) => console.log('✔︎ ' + msg),
23
- error: (msg) => console.error('' + msg),
24
- fatal: (msg) => { console.error('✖ FATAL ✖ ' + msg); console.error('✖ FATAL ✖ Manifest build FAILED.'); },
25
- divider: (label) => { const line = ''.padEnd(60, '─'); console.log(`\n${line}\n${label}\n${line}\n`); },
26
- };
27
-
28
- /* --------------------------------------------------
29
- * Helper Utilities
30
- * -------------------------------------------------- */
31
-
32
- const normalizeName = (name) => { if (typeof name !== 'string') return name; return name.trim().replace(/,$/, '').replace(/_/g, '-').toLowerCase(); };
33
-
34
- /**
35
- * Finds the closest string matches for a typo.
36
- */
37
- function suggestClosest(name, candidates, n = 3) {
38
- const levenshtein = (a = '', b = '') => {
39
- const m = a.length, n = b.length;
40
- if (!m) return n; if (!n) return m;
41
- const dp = Array.from({ length: m + 1 }, (_, i) => Array(n + 1).fill(i));
42
- for (let j = 0; j <= n; j++) dp[0][j] = j;
43
- for (let i = 1; i <= m; i++)
44
- for (let j = 1; j <= n; j++)
45
- dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : Math.min(dp[i - 1][j - 1], dp[i][j - 1], dp[i - 1][j]) + 1;
46
- return dp[m][n];
47
- };
48
- const scores = candidates.map(c => [c, levenshtein(name, c)]);
49
- scores.sort((a, b) => a[1] - b[1]);
50
- return scores.slice(0, n).map(s => s[0]);
51
- }
52
-
53
- /**
54
- * Checks for a cycle using Depth First Search.
55
- */
56
- function findCycles(manifestMap, adjacencyList) {
57
- const visited = new Set(), stack = new Set(), cycles = [];
58
- const dfs = (node, path) => {
59
- if (stack.has(node)) { const idx = path.indexOf(node); cycles.push([...path.slice(idx), node]); return; }
60
- if (visited.has(node)) return;
61
- visited.add(node); stack.add(node);
62
- for (const nb of adjacencyList.get(node) || []) dfs(nb, [...path, nb]);
63
- stack.delete(node); };
64
- for (const name of manifestMap.keys()) dfs(name, [name]);
65
- return cycles;
66
- }
67
-
68
- /**
69
- * Recursively traces all dependencies for a given set of "endpoint" calcs.
70
- */
71
- function getDependencySet(endpoints, adjacencyList) {
72
- const required = new Set(endpoints);
73
- const queue = [...endpoints];
74
- while (queue.length > 0) { const calcName = queue.shift(); const dependencies = adjacencyList.get(calcName);
75
- if (dependencies) { for (const dep of dependencies) { if (!required.has(dep)) { required.add(dep); queue.push(dep); } } } }
76
- return required;
77
- }
78
-
79
- /* --------------------------------------------------
80
- * Core Manifest Builder
81
- * -------------------------------------------------- */
82
-
83
- /**
84
- * Builds the full computation manifest.
85
- * @param {string[]} productLinesToRun - Array of product line categories (folder names) to build for.
86
- * @param {object} calculations - The injected calculations object from the main application.
87
- * @returns {object[]} The final sorted manifest array.
88
- */
89
- function buildManifest(productLinesToRun = [], calculations) {
90
- log.divider('Building Dynamic Manifest');
91
- log.info(`Target Product Lines: [${productLinesToRun.join(', ')}]`);
92
- const manifestMap = new Map();
93
- const adjacency = new Map();
94
- const reverseAdjacency = new Map();
95
- const inDegree = new Map();
96
- let hasFatalError = false;
97
-
98
- /* ---------------- 1. Load All Calculations ---------------- */
99
- log.step('Loading and validating all calculation classes…');
100
- const allCalculationClasses = new Map();
101
-
102
- /**
103
- * Processes a single calculation class, validates it, and adds to maps.
104
- * @param {Class} Class - The calculation class structure.
105
- * @param {string} name - The filename/key.
106
- * @param {string} folderName - The folder this file came from (e.g. 'core', 'gem').
107
- */
108
- function processCalc(Class, name, folderName) {
109
- if (!Class || typeof Class !== 'function') return;
110
- const normalizedName = normalizeName(name);
111
- allCalculationClasses.set(normalizedName, Class);
112
-
113
- // --- RULE 1: Check for static getMetadata() ---
114
- if (typeof Class.getMetadata !== 'function') { log.fatal(`Calculation "${normalizedName}" is missing the static getMetadata() method. Build FAILED.`); hasFatalError = true; return; }
115
- // --- RULE 2: Check for static getDependencies() ---
116
- if (typeof Class.getDependencies !== 'function') { log.fatal(`Calculation "${normalizedName}" is missing the static getDependencies() method. Build FAILED.`); hasFatalError = true;return; }
117
- // --- RULE 3: Check for static getSchema() ---
118
- if (typeof Class.getSchema !== 'function') {log.warn(`Calculation "${normalizedName}" is missing the static getSchema() method. (Recommended)`); }
119
-
120
- const metadata = Class.getMetadata();
121
- const dependencies = Class.getDependencies().map(normalizeName);
122
-
123
- // --- RULE 4: Check for isHistorical mismatch ---
124
- if (metadata.isHistorical === true && !Class.toString().includes('yesterday')) {
125
- log.warn(`Calculation "${normalizedName}" is marked 'isHistorical: true' but does not seem to reference 'yesterday' data.`);
126
- }
127
-
128
- // --- RULE 5: Category Enforcement Logic ---
129
- let finalCategory = folderName; // Default to folder name
130
-
131
- if (folderName === 'core') {
132
- // For CORE: We respect metadata.category if it exists.
133
- // This allows core computations to write to specific output paths (e.g. 'market_stats').
134
- if (metadata.category) {
135
- finalCategory = metadata.category;
136
- }
137
- } else {
138
- // For PRODUCT LINES: We IGNORE metadata.category and enforce the folder name.
139
- // This ensures product lines always group correctly regardless of typos in metadata.
140
- finalCategory = folderName;
141
- }
142
-
143
- const manifestEntry = {
144
- name: normalizedName,
145
- class: Class,
146
- category: finalCategory, // The logic category / output path
147
- sourcePackage: folderName, // The physical source folder (used for "Core Always Run" checks)
148
- type: metadata.type,
149
- isHistorical: metadata.isHistorical,
150
- rootDataDependencies: metadata.rootDataDependencies || [],
151
- userType: metadata.userType,
152
- dependencies: dependencies,
153
- pass: 0,
154
- };
155
- manifestMap.set(normalizedName, manifestEntry);
156
- adjacency.set(normalizedName, dependencies);
157
- inDegree.set(normalizedName, dependencies.length);
158
- dependencies.forEach(dep => { if (!reverseAdjacency.has(dep)) reverseAdjacency.set(dep, []); reverseAdjacency.get(dep).push(normalizedName); });
159
- }
160
-
161
- if (!calculations || typeof calculations !== 'object') {
162
- log.fatal('Calculations object was not provided or is invalid.');
163
- throw new Error('Manifest build failed: Invalid calculations object.');
164
- }
165
-
166
- // Iterate over folders (folderName becomes the default category)
167
- for (const folderName in calculations) {
168
- if (folderName === 'legacy') { log.info('Skipping "legacy" calculations package.'); continue; }
169
- const group = calculations[folderName];
170
- for (const key in group) {
171
- const entry = group[key];
172
- if (typeof entry === 'function') { processCalc(entry, key, folderName); }
173
- }
174
- }
175
-
176
- if (hasFatalError) { throw new Error('Manifest build failed due to missing static methods in calculations.'); }
177
- log.success(`Loaded and validated ${manifestMap.size} total calculations.`);
178
-
179
- /* ---------------- 2. Validate Dependency Links ---------------- */
180
- log.divider('Validating Dependency Links');
181
- const allNames = new Set(manifestMap.keys());
182
- let invalidLinks = false;
183
- for (const [name, entry] of manifestMap) {
184
- for (const dep of entry.dependencies) {
185
- if (!allNames.has(dep)) {
186
- invalidLinks = true;
187
- const guesses = suggestClosest(dep, Array.from(allNames));
188
- log.error(`${name} depends on unknown calculation "${dep}"`);
189
- if (guesses.length) log.info(`Did you mean: ${guesses.join(', ')} ?`);
190
- }
191
- if (dep === name) {
192
- invalidLinks = true;
193
- log.error(`${name} has a circular dependency on *itself*!`);
194
- }
195
- }
196
- }
197
- if (invalidLinks) { throw new Error('Manifest validation failed. Fix missing or self-referencing dependencies.'); }
198
- log.success('All dependency links are valid.');
199
-
200
- /* ---------------- 3. Filter for Product Lines ---------------- */
201
- log.divider('Filtering by Product Line');
202
-
203
- // 1. Find all "endpoint" calculations in the target product lines.
204
- // This checks the assigned category (which now matches the folder for all non-core lines).
205
- const productLineEndpoints = [];
206
- for (const [name, entry] of manifestMap.entries()) {
207
- if (productLinesToRun.includes(entry.category)) {
208
- productLineEndpoints.push(name);
209
- }
210
- }
211
-
212
- // 2. Add 'core' calculations as they are always included.
213
- // We check 'sourcePackage' here so that even if a core calculation has a custom category
214
- // (like 'market_stats'), it is still recognized as Core and included.
215
- for (const [name, entry] of manifestMap.entries()) {
216
- if (entry.sourcePackage === 'core') {
217
- productLineEndpoints.push(name);
218
- }
219
- }
220
-
221
- // 3. Trace all dependencies upwards from these endpoints.
222
- const requiredCalcs = getDependencySet(productLineEndpoints, adjacency);
223
- log.info(`Identified ${productLineEndpoints.length} endpoint/core calculations.`);
224
- log.info(`Traced dependencies: ${requiredCalcs.size} total calculations are required.`);
225
-
226
- // 4. Create the final, filtered maps for sorting.
227
- const filteredManifestMap = new Map();
228
- const filteredInDegree = new Map();
229
- const filteredReverseAdjacency = new Map();
230
- for (const name of requiredCalcs) { filteredManifestMap.set(name, manifestMap.get(name)); filteredInDegree.set(name, inDegree.get(name));
231
- const consumers = (reverseAdjacency.get(name) || []).filter(consumer => requiredCalcs.has(consumer)); filteredReverseAdjacency.set(name, consumers); }
232
- log.success(`Filtered manifest to ${filteredManifestMap.size} calculations.`);
233
-
234
- /* ---------------- 4. Topological Sort (Kahn's Algorithm) ---------------- */
235
- log.divider('Topological Sorting (Kahn\'s Algorithm)');
236
- const sortedManifest = [];
237
- const queue = [];
238
- let maxPass = 0;
239
-
240
- for (const [name, degree] of filteredInDegree) { if (degree === 0) { queue.push(name); filteredManifestMap.get(name).pass = 1; maxPass = 1; } }
241
- queue.sort();
242
- while (queue.length) {
243
- const currentName = queue.shift();
244
- const currentEntry = filteredManifestMap.get(currentName);
245
- sortedManifest.push(currentEntry);
246
-
247
- for (const neighborName of (filteredReverseAdjacency.get(currentName) || [])) { const newDegree = filteredInDegree.get(neighborName) - 1; filteredInDegree.set(neighborName, newDegree);
248
- const neighborEntry = filteredManifestMap.get(neighborName);
249
- if (neighborEntry.pass <= currentEntry.pass) { neighborEntry.pass = currentEntry.pass + 1; if (neighborEntry.pass > maxPass) maxPass = neighborEntry.pass; }
250
- if (newDegree === 0) { queue.push(neighborName); } }
251
- queue.sort(); }
252
-
253
- if (sortedManifest.length !== filteredManifestMap.size) {
254
- log.divider('Circular Dependency Detected');
255
- const cycles = findCycles(filteredManifestMap, adjacency);
256
- for (const c of cycles) log.error('Cycle: ' + c.join(' → '));
257
- throw new Error('Circular dependency detected. Manifest build failed.'); }
258
-
259
- /* ---------------- 5. Summary ---------------- */
260
- log.divider('Manifest Summary');
261
- log.success(`Total calculations in build: ${sortedManifest.length}`);
262
- log.success(`Total passes required: ${maxPass}\n`);
263
- const grouped = [...sortedManifest.reduce((m, e) => { if (!m.has(e.pass)) m.set(e.pass, []); m.get(e.pass).push(e); return m; }, new Map())].sort((a, b) => a[0] - b[0]);
264
- for (const [pass, list] of grouped) { console.log(`Pass ${pass} (${list.length} calcs):`); console.log(' ' + list.map(l => l.name).join(', ')); }
265
- log.divider('Build Complete');
266
- return sortedManifest;
267
- }
268
-
269
- /**
270
- * Main entry point for building and exporting the manifest.
271
- * @param {string[]} productLinesToRun - Array of product line categories (folder names) to build for.
272
- * @param {object} calculations - The injected calculations object.
273
- */
274
- function build(productLinesToRun, calculations) {
275
- try {
276
- const manifest = buildManifest(productLinesToRun, calculations);
277
- return manifest; // Returns the array
278
- } catch (error) {
279
- log.error(error.message);
280
- return null;
281
- }
282
- }
283
-
1
+ /**
2
+ * @fileoverview
3
+ * Dynamic Manifest Builder (v5 - Smart Hashing with Safe Mode)
4
+ *
5
+ * This script builds the computation manifest and generates a "Smart Hash"
6
+ * for each calculation.
7
+ *
8
+ * KEY FEATURE:
9
+ * It performs static analysis on the calculation code to detect which
10
+ * specific math layers it utilizes. It then combines the calculation's own
11
+ * code hash with the current hashes of those specific layers.
12
+ * * SAFE MODE:
13
+ * If a calculation uses no detectable layer keywords, it defaults to
14
+ * depending on ALL layers to prevent staleness.
15
+ */
16
+
17
+ const { generateCodeHash } = require('../utils/utils');
18
+
19
+ // 1. Import Layers directly to generate their "State Hashes"
20
+ // We import them individually to hash them as distinct domains.
21
+ const MathematicsLayer = require('../layers/mathematics');
22
+ const ExtractorsLayer = require('../layers/extractors');
23
+ const ProfilingLayer = require('../layers/profiling');
24
+ const ValidatorsLayer = require('../layers/validators');
25
+
26
+ /* --------------------------------------------------
27
+ * 1. Layer Hash Generation
28
+ * -------------------------------------------------- */
29
+
30
+ /**
31
+ * Generates a single hash representing the entire state of a layer module.
32
+ * Combines all exported classes/functions/objects into one deterministic string.
33
+ */
34
+ function generateLayerHash(layerExports, layerName) {
35
+ const keys = Object.keys(layerExports).sort(); // Sort for determinism
36
+ let combinedSource = `LAYER:${layerName}`;
37
+
38
+ for (const key of keys) {
39
+ const item = layerExports[key];
40
+ if (typeof item === 'function') {
41
+ combinedSource += item.toString();
42
+ } else if (typeof item === 'object' && item !== null) {
43
+ combinedSource += JSON.stringify(item);
44
+ } else {
45
+ combinedSource += String(item);
46
+ }
47
+ }
48
+ return generateCodeHash(combinedSource);
49
+ }
50
+
51
+ // Pre-compute layer hashes at startup
52
+ const LAYER_HASHES = {
53
+ 'mathematics': generateLayerHash(MathematicsLayer, 'mathematics'),
54
+ 'extractors': generateLayerHash(ExtractorsLayer, 'extractors'),
55
+ 'profiling': generateLayerHash(ProfilingLayer, 'profiling'),
56
+ 'validators': generateLayerHash(ValidatorsLayer, 'validators')
57
+ };
58
+
59
+ // Map code patterns to Layer dependencies
60
+ // If a calculation's code contains these strings, it depends on that layer.
61
+ const LAYER_TRIGGERS = {
62
+ 'mathematics': [
63
+ 'math.compute', 'MathPrimitives',
64
+ 'math.signals', 'SignalPrimitives',
65
+ 'math.aggregate', 'Aggregators',
66
+ 'math.timeseries', 'TimeSeries',
67
+ 'math.distribution', 'DistributionAnalytics',
68
+ 'math.financial', 'FinancialEngineering'
69
+ ],
70
+ 'extractors': [
71
+ 'math.extract', 'DataExtractor',
72
+ 'math.history', 'HistoryExtractor',
73
+ 'math.prices', 'priceExtractor',
74
+ 'math.insights', 'InsightsExtractor',
75
+ 'math.tradeSeries', 'TradeSeriesBuilder'
76
+ ],
77
+ 'profiling': [
78
+ 'math.profiling', 'SCHEMAS',
79
+ 'math.classifier', 'UserClassifier',
80
+ 'math.psychometrics', 'Psychometrics',
81
+ 'math.bias', 'CognitiveBiases',
82
+ 'math.skill', 'SkillAttribution'
83
+ ],
84
+ 'validators': [
85
+ 'math.validate', 'Validators'
86
+ ]
87
+ };
88
+
89
+ /* --------------------------------------------------
90
+ * Pretty Console Helpers
91
+ * -------------------------------------------------- */
92
+ const log = {
93
+ info: (msg) => console.log('ℹ︎ ' + msg),
94
+ step: (msg) => console.log('› ' + msg),
95
+ warn: (msg) => console.warn('⚠︎ ' + msg),
96
+ success: (msg) => console.log('✔︎ ' + msg),
97
+ error: (msg) => console.error('✖ ' + msg),
98
+ fatal: (msg) => { console.error('✖ FATAL ' + msg); console.error('✖ FATAL ✖ Manifest build FAILED.'); },
99
+ divider: (label) => { const line = ''.padEnd(60, '─'); console.log(`\n${line}\n${label}\n${line}\n`); },
100
+ };
101
+
102
+ /* --------------------------------------------------
103
+ * Helper Utilities
104
+ * -------------------------------------------------- */
105
+
106
+ const normalizeName = (name) => { if (typeof name !== 'string') return name; return name.trim().replace(/,$/, '').replace(/_/g, '-').toLowerCase(); };
107
+
108
+ function suggestClosest(name, candidates, n = 3) {
109
+ const levenshtein = (a = '', b = '') => {
110
+ const m = a.length, n = b.length;
111
+ if (!m) return n; if (!n) return m;
112
+ const dp = Array.from({ length: m + 1 }, (_, i) => Array(n + 1).fill(i));
113
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
114
+ for (let i = 1; i <= m; i++)
115
+ for (let j = 1; j <= n; j++)
116
+ dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : Math.min(dp[i - 1][j - 1], dp[i][j - 1], dp[i - 1][j]) + 1;
117
+ return dp[m][n];
118
+ };
119
+ const scores = candidates.map(c => [c, levenshtein(name, c)]);
120
+ scores.sort((a, b) => a[1] - b[1]);
121
+ return scores.slice(0, n).map(s => s[0]);
122
+ }
123
+
124
+ function findCycles(manifestMap, adjacencyList) {
125
+ const visited = new Set(), stack = new Set(), cycles = [];
126
+ const dfs = (node, path) => {
127
+ if (stack.has(node)) { const idx = path.indexOf(node); cycles.push([...path.slice(idx), node]); return; }
128
+ if (visited.has(node)) return;
129
+ visited.add(node); stack.add(node);
130
+ for (const nb of adjacencyList.get(node) || []) dfs(nb, [...path, nb]);
131
+ stack.delete(node); };
132
+ for (const name of manifestMap.keys()) dfs(name, [name]);
133
+ return cycles;
134
+ }
135
+
136
+ function getDependencySet(endpoints, adjacencyList) {
137
+ const required = new Set(endpoints);
138
+ const queue = [...endpoints];
139
+ while (queue.length > 0) { const calcName = queue.shift(); const dependencies = adjacencyList.get(calcName);
140
+ if (dependencies) { for (const dep of dependencies) { if (!required.has(dep)) { required.add(dep); queue.push(dep); } } } }
141
+ return required;
142
+ }
143
+
144
+ /* --------------------------------------------------
145
+ * Core Manifest Builder
146
+ * -------------------------------------------------- */
147
+
148
+ function buildManifest(productLinesToRun = [], calculations) {
149
+ log.divider('Building Dynamic Manifest (Smart Hashing)');
150
+ log.info(`Target Product Lines: [${productLinesToRun.join(', ')}]`);
151
+ log.info(`Layer Hashes Initialized: ${Object.keys(LAYER_HASHES).join(', ')}`);
152
+
153
+ const manifestMap = new Map();
154
+ const adjacency = new Map();
155
+ const reverseAdjacency = new Map();
156
+ const inDegree = new Map();
157
+ let hasFatalError = false;
158
+
159
+ /* ---------------- 1. Load All Calculations ---------------- */
160
+ log.step('Loading and validating all calculation classes…');
161
+ const allCalculationClasses = new Map();
162
+
163
+ function processCalc(Class, name, folderName) {
164
+ if (!Class || typeof Class !== 'function') return;
165
+ const normalizedName = normalizeName(name);
166
+ allCalculationClasses.set(normalizedName, Class);
167
+
168
+ if (typeof Class.getMetadata !== 'function') { log.fatal(`Calculation "${normalizedName}" is missing static getMetadata().`); hasFatalError = true; return; }
169
+ if (typeof Class.getDependencies !== 'function') { log.fatal(`Calculation "${normalizedName}" is missing static getDependencies().`); hasFatalError = true;return; }
170
+
171
+ const metadata = Class.getMetadata();
172
+ const dependencies = Class.getDependencies().map(normalizeName);
173
+
174
+ if (metadata.isHistorical === true && !Class.toString().includes('yesterday')) {
175
+ log.warn(`Calculation "${normalizedName}" marked 'isHistorical' but no 'yesterday' reference found.`);
176
+ }
177
+
178
+ let finalCategory = folderName;
179
+ if (folderName === 'core') {
180
+ if (metadata.category) finalCategory = metadata.category;
181
+ } else {
182
+ finalCategory = folderName;
183
+ }
184
+
185
+ // --- SMART HASH GENERATION ---
186
+ const classCode = Class.toString();
187
+ let compositeHashString = generateCodeHash(classCode); // Start with own code
188
+ const usedLayers = [];
189
+
190
+ // 1. Check for specific layer usage
191
+ for (const [layerName, triggers] of Object.entries(LAYER_TRIGGERS)) {
192
+ // If code contains any trigger for this layer
193
+ if (triggers.some(trigger => classCode.includes(trigger))) {
194
+ compositeHashString += LAYER_HASHES[layerName]; // Append Layer Hash
195
+ usedLayers.push(layerName);
196
+ }
197
+ }
198
+
199
+ // 2. SAFE MODE FALLBACK
200
+ // If we found NO specific triggers (e.g. legacy code or unique structure),
201
+ // assume it might use anything. Depend on ALL layers to be safe.
202
+ let isSafeMode = false;
203
+ if (usedLayers.length === 0) {
204
+ isSafeMode = true;
205
+ Object.values(LAYER_HASHES).forEach(h => compositeHashString += h);
206
+ }
207
+
208
+ // Generate final composite hash
209
+ const finalHash = generateCodeHash(compositeHashString);
210
+
211
+ if (isSafeMode) {
212
+ log.warn(`[Hash] ${normalizedName}: No specific dependencies detected. Enforcing SAFE MODE (Linked to ALL Layers).`);
213
+ }
214
+
215
+ const manifestEntry = {
216
+ name: normalizedName,
217
+ class: Class,
218
+ category: finalCategory,
219
+ sourcePackage: folderName,
220
+ type: metadata.type,
221
+ isHistorical: metadata.isHistorical,
222
+ rootDataDependencies: metadata.rootDataDependencies || [],
223
+ userType: metadata.userType,
224
+ dependencies: dependencies,
225
+ pass: 0,
226
+ hash: finalHash, // The Smart Hash
227
+ debugUsedLayers: isSafeMode ? ['ALL (Safe Mode)'] : usedLayers
228
+ };
229
+
230
+ manifestMap.set(normalizedName, manifestEntry);
231
+ adjacency.set(normalizedName, dependencies);
232
+ inDegree.set(normalizedName, dependencies.length);
233
+ dependencies.forEach(dep => { if (!reverseAdjacency.has(dep)) reverseAdjacency.set(dep, []); reverseAdjacency.get(dep).push(normalizedName); });
234
+ }
235
+
236
+ if (!calculations || typeof calculations !== 'object') {
237
+ throw new Error('Manifest build failed: Invalid calculations object.');
238
+ }
239
+
240
+ // Iterate over folders (folderName becomes the default category)
241
+ for (const folderName in calculations) {
242
+ if (folderName === 'legacy') continue;
243
+ const group = calculations[folderName];
244
+ for (const key in group) {
245
+ const entry = group[key];
246
+ if (typeof entry === 'function') { processCalc(entry, key, folderName); }
247
+ }
248
+ }
249
+
250
+ if (hasFatalError) { throw new Error('Manifest build failed due to missing static methods.'); }
251
+ log.success(`Loaded ${manifestMap.size} calculations.`);
252
+
253
+ /* ---------------- 2. Validate Dependency Links ---------------- */
254
+ const allNames = new Set(manifestMap.keys());
255
+ let invalidLinks = false;
256
+ for (const [name, entry] of manifestMap) {
257
+ for (const dep of entry.dependencies) {
258
+ if (!allNames.has(dep)) {
259
+ invalidLinks = true;
260
+ const guesses = suggestClosest(dep, Array.from(allNames));
261
+ log.error(`${name} depends on unknown calculation "${dep}". Did you mean: ${guesses.join(', ')}?`);
262
+ }
263
+ if (dep === name) {
264
+ invalidLinks = true;
265
+ log.error(`${name} has a circular dependency on *itself*!`);
266
+ }
267
+ }
268
+ }
269
+ if (invalidLinks) { throw new Error('Manifest validation failed.'); }
270
+
271
+ /* ---------------- 3. Filter for Product Lines ---------------- */
272
+ const productLineEndpoints = [];
273
+ for (const [name, entry] of manifestMap.entries()) {
274
+ if (productLinesToRun.includes(entry.category)) { productLineEndpoints.push(name); }
275
+ }
276
+ // Always include core
277
+ for (const [name, entry] of manifestMap.entries()) {
278
+ if (entry.sourcePackage === 'core') { productLineEndpoints.push(name); }
279
+ }
280
+
281
+ const requiredCalcs = getDependencySet(productLineEndpoints, adjacency);
282
+ log.info(`Filtered down to ${requiredCalcs.size} active calculations.`);
283
+
284
+ const filteredManifestMap = new Map();
285
+ const filteredInDegree = new Map();
286
+ const filteredReverseAdjacency = new Map();
287
+ for (const name of requiredCalcs) { filteredManifestMap.set(name, manifestMap.get(name)); filteredInDegree.set(name, inDegree.get(name));
288
+ const consumers = (reverseAdjacency.get(name) || []).filter(consumer => requiredCalcs.has(consumer)); filteredReverseAdjacency.set(name, consumers); }
289
+
290
+ /* ---------------- 4. Topological Sort ---------------- */
291
+ const sortedManifest = [];
292
+ const queue = [];
293
+ let maxPass = 0;
294
+
295
+ for (const [name, degree] of filteredInDegree) { if (degree === 0) { queue.push(name); filteredManifestMap.get(name).pass = 1; maxPass = 1; } }
296
+ queue.sort();
297
+ while (queue.length) {
298
+ const currentName = queue.shift();
299
+ const currentEntry = filteredManifestMap.get(currentName);
300
+ sortedManifest.push(currentEntry);
301
+
302
+ for (const neighborName of (filteredReverseAdjacency.get(currentName) || [])) { const newDegree = filteredInDegree.get(neighborName) - 1; filteredInDegree.set(neighborName, newDegree);
303
+ const neighborEntry = filteredManifestMap.get(neighborName);
304
+ if (neighborEntry.pass <= currentEntry.pass) { neighborEntry.pass = currentEntry.pass + 1; if (neighborEntry.pass > maxPass) maxPass = neighborEntry.pass; }
305
+ if (newDegree === 0) { queue.push(neighborName); } }
306
+ queue.sort(); }
307
+
308
+ if (sortedManifest.length !== filteredManifestMap.size) {
309
+ throw new Error('Circular dependency detected. Manifest build failed.'); }
310
+
311
+ log.success(`Total passes required: ${maxPass}`);
312
+ return sortedManifest;
313
+ }
314
+
315
+ function build(productLinesToRun, calculations) {
316
+ try {
317
+ return buildManifest(productLinesToRun, calculations);
318
+ } catch (error) {
319
+ log.error(error.message);
320
+ return null;
321
+ }
322
+ }
323
+
284
324
  module.exports = { build };