@chemx/starter-kit 26.9.9-786 → 26.9.11-481

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 (45) hide show
  1. package/cli/audit/ast-visitors.js +243 -0
  2. package/cli/audit/discussion-store.js +39 -0
  3. package/cli/audit/history.d.ts +69 -0
  4. package/cli/audit/history.js +397 -0
  5. package/cli/audit/metrics.js +179 -0
  6. package/cli/audit/prompts.js +174 -0
  7. package/cli/audit/reporter-ascii.d.ts +4 -0
  8. package/cli/audit/reporter-ascii.js +99 -0
  9. package/cli/audit/reporter-grades.js +275 -0
  10. package/cli/audit/reporter-markdown.js +137 -0
  11. package/cli/audit/reporter-utils.js +125 -0
  12. package/cli/audit/reporter.js +497 -0
  13. package/cli/audit/rules-helpers.js +109 -0
  14. package/cli/audit/rules-registry.js +97 -0
  15. package/cli/audit/rules.js +123 -0
  16. package/cli/audit/social-constants.js +4 -0
  17. package/cli/audit/social-gh.js +126 -0
  18. package/cli/audit/social-git.js +214 -0
  19. package/cli/audit/social-http.js +222 -0
  20. package/cli/audit/social-publisher.js +193 -0
  21. package/cli/audit/social.d.ts +69 -0
  22. package/cli/audit/social.js +303 -0
  23. package/cli/audit/types.d.ts +140 -0
  24. package/cli/audit.d.ts +3 -24
  25. package/cli/audit.js +207 -210
  26. package/cli/help.d.ts +1 -0
  27. package/cli/help.js +136 -0
  28. package/cli/index.js +128 -566
  29. package/cli/installer-templates.js +91 -0
  30. package/cli/installer.d.ts +21 -0
  31. package/cli/installer.js +104 -0
  32. package/cli/license.js +320 -0
  33. package/cli/navigator-actions.js +274 -0
  34. package/cli/navigator-banner.js +98 -0
  35. package/cli/navigator-conversion.js +100 -0
  36. package/cli/navigator-grades.js +87 -0
  37. package/cli/navigator-menu.js +127 -0
  38. package/cli/navigator-paged.js +86 -0
  39. package/cli/navigator-share.js +129 -0
  40. package/cli/navigator.d.ts +43 -0
  41. package/cli/navigator.js +126 -0
  42. package/cli/scaffold.js +148 -0
  43. package/cli/terminal.js +126 -0
  44. package/docs/CHANGELOG.md +101 -0
  45. package/package.json +11 -10
@@ -0,0 +1,397 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { groupViolationsBySeverity } from './reporter.js';
4
+
5
+ const RESET = '\x1b[0m';
6
+ const BOLD = '\x1b[1m';
7
+ const DIM = '\x1b[2m';
8
+ const CYAN = '\x1b[36m';
9
+ const GREEN = '\x1b[32m';
10
+ const YELLOW = '\x1b[33m';
11
+ const RED = '\x1b[31m';
12
+
13
+ export const ensureChemxDir = (cwd = process.cwd()) => {
14
+ const dir = path.resolve(cwd, '.chemx');
15
+ if (!fs.existsSync(dir)) {
16
+ fs.mkdirSync(dir, { recursive: true });
17
+ }
18
+
19
+ // Ensure .chemx is gitignored if git repo exists
20
+ const gitDir = path.resolve(cwd, '.git');
21
+ if (fs.existsSync(gitDir)) {
22
+ const gitignorePath = path.resolve(cwd, '.gitignore');
23
+ try {
24
+ let content = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf-8') : '';
25
+ if (!content.includes('.chemx')) {
26
+ const trailingNewline = content.endsWith('\n') || content.length === 0 ? '' : '\n';
27
+ fs.appendFileSync(gitignorePath, `${trailingNewline}# Chemical X local telemetry & audit history\n.chemx/\n`, 'utf-8');
28
+ }
29
+ } catch {}
30
+ }
31
+
32
+ return dir;
33
+ };
34
+
35
+ export const createSnapshotFromReport = (report) => {
36
+ const { metrics, health, hotspots = [], violations = [], pillars = {}, contextAnalysis = {} } = report;
37
+ const { critical, high, medium, low } = groupViolationsBySeverity(violations);
38
+
39
+ const isExtremeMonolith = (h) => h.lineCount >= 2000;
40
+ const isSevereMonolith = (h) => h.lineCount >= 1000 && h.lineCount < 2000;
41
+ const isWarningMonolith = (h) => h.lineCount >= 500 && h.lineCount < 1000;
42
+
43
+ const extremeMonoliths = hotspots.filter(isExtremeMonolith).length;
44
+ const severeMonoliths = hotspots.filter(isSevereMonolith).length;
45
+ const warningMonoliths = hotspots.filter(isWarningMonolith).length;
46
+ const totalMonoliths = extremeMonoliths + severeMonoliths + warningMonoliths;
47
+
48
+ const pillarSummaries = {};
49
+ for (const [key, data] of Object.entries(pillars)) {
50
+ pillarSummaries[key] = {
51
+ status: data.status,
52
+ violations: data.violations,
53
+ critical: data.critical
54
+ };
55
+ }
56
+
57
+ return {
58
+ id: `audit-${Date.now()}`,
59
+ timestamp: new Date().toISOString(),
60
+ health: {
61
+ score: health.score,
62
+ grade: health.grade,
63
+ label: health.label
64
+ },
65
+ metrics: {
66
+ scannedFiles: metrics.scannedFiles,
67
+ totalLoc: metrics.totalLoc,
68
+ avgLoc: metrics.avgLoc,
69
+ moleculeCount: metrics.moleculeCount,
70
+ moleculeCompliantCount: metrics.moleculeCompliantCount,
71
+ moleculeCompliantPct: metrics.moleculeCompliantPct,
72
+ hookCount: metrics.hookCount
73
+ },
74
+ violations: {
75
+ total: violations.length,
76
+ critical: critical.length,
77
+ high: high.length,
78
+ medium: medium.length,
79
+ low: low.length
80
+ },
81
+ monoliths: {
82
+ total: totalMonoliths,
83
+ extreme: extremeMonoliths,
84
+ severe: severeMonoliths,
85
+ warning: warningMonoliths
86
+ },
87
+ tokens: {
88
+ estimatedTokens: contextAnalysis.estimatedTokens || 0,
89
+ estimatedExcessTokens: contextAnalysis.estimatedExcessTokens || 0,
90
+ potentialSavingsPct: contextAnalysis.potentialSavingsPct || 0,
91
+ riskLevel: contextAnalysis.riskLevel || 'LOW',
92
+ pricingModel: contextAnalysis.pricingModel || 'Frontier Blended ($3.00/1M)',
93
+ costPerMillion: contextAnalysis.costPerMillion !== undefined ? contextAnalysis.costPerMillion : 3.0,
94
+ excessCostPerPass: contextAnalysis.excessCostPerPass !== undefined ? contextAnalysis.excessCostPerPass : 0,
95
+ weeklyWastePerDev: contextAnalysis.weeklyWastePerDev !== undefined ? contextAnalysis.weeklyWastePerDev : 0,
96
+ monthlyWastePerDev: contextAnalysis.monthlyWastePerDev !== undefined ? contextAnalysis.monthlyWastePerDev : 0
97
+ },
98
+ pillars: pillarSummaries
99
+ };
100
+ };
101
+
102
+ export const getAuditHistory = (cwd = process.cwd()) => {
103
+ const historyPath = path.resolve(cwd, '.chemx', 'history.json');
104
+ if (!fs.existsSync(historyPath)) return [];
105
+ try {
106
+ const raw = fs.readFileSync(historyPath, 'utf-8');
107
+ const parsed = JSON.parse(raw);
108
+ return Array.isArray(parsed) ? parsed : [];
109
+ } catch {
110
+ return [];
111
+ }
112
+ };
113
+
114
+ export const getAuditBaseline = (cwd = process.cwd()) => {
115
+ const baselinePath = path.resolve(cwd, '.chemx', 'baseline.json');
116
+ if (fs.existsSync(baselinePath)) {
117
+ try {
118
+ const raw = fs.readFileSync(baselinePath, 'utf-8');
119
+ return JSON.parse(raw);
120
+ } catch {}
121
+ }
122
+ const history = getAuditHistory(cwd);
123
+ return history.length > 0 ? history[0] : null;
124
+ };
125
+
126
+ export const setAuditBaseline = (snapshot, cwd = process.cwd()) => {
127
+ ensureChemxDir(cwd);
128
+ const baselinePath = path.resolve(cwd, '.chemx', 'baseline.json');
129
+ fs.writeFileSync(baselinePath, JSON.stringify(snapshot, null, 2), 'utf-8');
130
+ return snapshot;
131
+ };
132
+
133
+ export const saveAuditSnapshot = (report, cwd = process.cwd()) => {
134
+ ensureChemxDir(cwd);
135
+ const snapshot = createSnapshotFromReport(report);
136
+ const history = getAuditHistory(cwd);
137
+
138
+ // Prevent duplicate snapshots if called multiple times within 5 seconds with same metrics
139
+ if (history.length > 0) {
140
+ const last = history[history.length - 1];
141
+ const timeDiff = Math.abs(Date.now() - new Date(last.timestamp).getTime());
142
+ const isSameMetrics =
143
+ last.health?.score === snapshot.health?.score &&
144
+ last.metrics?.totalLoc === snapshot.metrics?.totalLoc &&
145
+ last.violations?.total === snapshot.violations?.total;
146
+
147
+ if (timeDiff < 5000 && isSameMetrics) {
148
+ return {
149
+ snapshot: last,
150
+ history,
151
+ baseline: getAuditBaseline(cwd),
152
+ isNewBaseline: false,
153
+ totalAudits: history.length
154
+ };
155
+ }
156
+ }
157
+
158
+ history.push(snapshot);
159
+
160
+ // Keep last 50 snapshots
161
+ const trimmed = history.slice(-50);
162
+ const historyPath = path.resolve(cwd, '.chemx', 'history.json');
163
+ fs.writeFileSync(historyPath, JSON.stringify(trimmed, null, 2), 'utf-8');
164
+
165
+ // Auto-establish first run as baseline if none exists
166
+ const baselinePath = path.resolve(cwd, '.chemx', 'baseline.json');
167
+ let isNewBaseline = false;
168
+ if (!fs.existsSync(baselinePath)) {
169
+ fs.writeFileSync(baselinePath, JSON.stringify(snapshot, null, 2), 'utf-8');
170
+ isNewBaseline = true;
171
+ }
172
+
173
+ const baseline = getAuditBaseline(cwd);
174
+ return {
175
+ snapshot,
176
+ history: trimmed,
177
+ baseline,
178
+ isNewBaseline,
179
+ totalAudits: trimmed.length
180
+ };
181
+ };
182
+
183
+ export const calculateTransformationDelta = (beforeSnapshot, afterSnapshot) => {
184
+ const scoreBefore = beforeSnapshot.health.score;
185
+ const scoreAfter = afterSnapshot.health.score;
186
+ const scoreDelta = scoreAfter - scoreBefore;
187
+
188
+ const critBefore = beforeSnapshot.violations.critical;
189
+ const critAfter = afterSnapshot.violations.critical;
190
+ const critDelta = critAfter - critBefore;
191
+
192
+ const totalBefore = beforeSnapshot.violations.total;
193
+ const totalAfter = afterSnapshot.violations.total;
194
+ const totalDelta = totalAfter - totalBefore;
195
+
196
+ const monolithBefore = beforeSnapshot.monoliths.total;
197
+ const monolithAfter = afterSnapshot.monoliths.total;
198
+ const monolithDelta = monolithAfter - monolithBefore;
199
+
200
+ const tokensBefore = beforeSnapshot.tokens.estimatedExcessTokens;
201
+ const tokensAfter = afterSnapshot.tokens.estimatedExcessTokens;
202
+ const tokensDelta = tokensAfter - tokensBefore;
203
+
204
+ const costPerMillion = afterSnapshot.tokens?.costPerMillion || beforeSnapshot.tokens?.costPerMillion || 3.0;
205
+
206
+ const resolveCostPass = (tokensObj) => {
207
+ if (tokensObj?.excessCostPerPass !== undefined) return tokensObj.excessCostPerPass;
208
+ const excess = tokensObj?.estimatedExcessTokens || 0;
209
+ return Number(((excess / 1000000) * costPerMillion).toFixed(3));
210
+ };
211
+
212
+ const resolveMonthlyTax = (tokensObj) => {
213
+ if (tokensObj?.monthlyWastePerDev !== undefined) return tokensObj.monthlyWastePerDev;
214
+ const costPass = resolveCostPass(tokensObj);
215
+ return Number((costPass * 20 * 5 * 4).toFixed(2));
216
+ };
217
+
218
+ const costPassBefore = resolveCostPass(beforeSnapshot.tokens);
219
+ const costPassAfter = resolveCostPass(afterSnapshot.tokens);
220
+ const costPassDelta = Number((costPassAfter - costPassBefore).toFixed(3));
221
+
222
+ const monthlyTaxBefore = resolveMonthlyTax(beforeSnapshot.tokens);
223
+ const monthlyTaxAfter = resolveMonthlyTax(afterSnapshot.tokens);
224
+ const monthlyTaxDelta = Number((monthlyTaxAfter - monthlyTaxBefore).toFixed(2));
225
+
226
+ const pillarDeltas = {};
227
+ const allPillars = new Set([
228
+ ...Object.keys(beforeSnapshot.pillars || {}),
229
+ ...Object.keys(afterSnapshot.pillars || {})
230
+ ]);
231
+
232
+ for (const pillar of allPillars) {
233
+ const bPillar = beforeSnapshot.pillars?.[pillar] || { status: 'UNKNOWN', violations: 0 };
234
+ const aPillar = afterSnapshot.pillars?.[pillar] || { status: 'UNKNOWN', violations: 0 };
235
+ pillarDeltas[pillar] = {
236
+ beforeStatus: bPillar.status,
237
+ afterStatus: aPillar.status,
238
+ beforeViolations: bPillar.violations,
239
+ afterViolations: aPillar.violations,
240
+ improved: aPillar.violations < bPillar.violations || (bPillar.status !== 'PASSED' && aPillar.status === 'PASSED')
241
+ };
242
+ }
243
+
244
+ return {
245
+ scoreDelta,
246
+ critDelta,
247
+ totalDelta,
248
+ monolithDelta,
249
+ tokensDelta,
250
+ costPassBefore,
251
+ costPassAfter,
252
+ costPassDelta,
253
+ monthlyTaxBefore,
254
+ monthlyTaxAfter,
255
+ monthlyTaxDelta,
256
+ pillarDeltas,
257
+ isImproved: scoreDelta > 0 || critDelta < 0 || totalDelta < 0
258
+ };
259
+ };
260
+
261
+ export const formatTransformationTerminal = (beforeSnapshot, afterSnapshot) => {
262
+ const delta = calculateTransformationDelta(beforeSnapshot, afterSnapshot);
263
+ const lines = [];
264
+
265
+ const formatDeltaNumber = (val, invertPositiveGood = false) => {
266
+ if (val === 0) return `${DIM}0 (No change)${RESET}`;
267
+ const isGood = invertPositiveGood ? val < 0 : val > 0;
268
+ const sign = val > 0 ? `+${val}` : `${val}`;
269
+ const color = isGood ? GREEN : RED;
270
+ return `${color}${BOLD}${sign}${RESET}`;
271
+ };
272
+
273
+ const formatDeltaCurrency = (val, perUnit = '') => {
274
+ if (val === 0) return `${DIM}0 (No change)${RESET}`;
275
+ const isGood = val < 0;
276
+ const sign = val > 0 ? `+$${val.toFixed(2)}` : `-$${Math.abs(val).toFixed(2)}`;
277
+ const color = isGood ? GREEN : RED;
278
+ return `${color}${BOLD}${sign}${perUnit}${RESET}`;
279
+ };
280
+
281
+ lines.push('');
282
+ lines.push(`${CYAN}======================================================================${RESET}`);
283
+ lines.push(`${BOLD}${CYAN} ARCHITECTURAL TRANSFORMATION : BEFORE & AFTER PROGRESSION${RESET}`);
284
+ lines.push(`${DIM} Comparing baseline snapshot vs. latest refactored audit${RESET}`);
285
+ lines.push(`${CYAN}======================================================================${RESET}`);
286
+ lines.push('');
287
+
288
+ const dateBefore = new Date(beforeSnapshot.timestamp).toLocaleDateString();
289
+ const dateAfter = new Date(afterSnapshot.timestamp).toLocaleDateString();
290
+
291
+ lines.push(` ${BOLD}Baseline Audit:${RESET} ${DIM}${dateBefore} [${beforeSnapshot.id}]${RESET}`);
292
+ lines.push(` ${BOLD}Latest Audit:${RESET} ${GREEN}${dateAfter} [${afterSnapshot.id}]${RESET}`);
293
+ lines.push('');
294
+
295
+ lines.push(`${BOLD} METRIC COMPARISON TABLE${RESET}`);
296
+ lines.push(` ----------------------------------------------------------------------------`);
297
+ lines.push(` ${'Metric'.padEnd(40)} ${'Before'.padEnd(16)} ${'After'.padEnd(16)} Delta`);
298
+ lines.push(` ----------------------------------------------------------------------------`);
299
+
300
+ const scoreBeforeStr = `${beforeSnapshot.health.score} (${beforeSnapshot.health.grade})`;
301
+ const scoreAfterStr = `${afterSnapshot.health.score} (${afterSnapshot.health.grade})`;
302
+ lines.push(` ${'Molecular Health (MHI)'.padEnd(40)} ${scoreBeforeStr.padEnd(16)} ${scoreAfterStr.padEnd(16)} ${formatDeltaNumber(delta.scoreDelta)}`);
303
+
304
+ const critBeforeStr = `${beforeSnapshot.violations.critical}`;
305
+ const critAfterStr = `${afterSnapshot.violations.critical}`;
306
+ lines.push(` ${'Critical Hazards'.padEnd(40)} ${critBeforeStr.padEnd(16)} ${critAfterStr.padEnd(16)} ${formatDeltaNumber(delta.critDelta, true)}`);
307
+
308
+ const totalBeforeStr = `${beforeSnapshot.violations.total}`;
309
+ const totalAfterStr = `${afterSnapshot.violations.total}`;
310
+ lines.push(` ${'Total Violations'.padEnd(40)} ${totalBeforeStr.padEnd(16)} ${totalAfterStr.padEnd(16)} ${formatDeltaNumber(delta.totalDelta, true)}`);
311
+
312
+ const monoBeforeStr = `${beforeSnapshot.monoliths.total}`;
313
+ const monoAfterStr = `${afterSnapshot.monoliths.total}`;
314
+ lines.push(` ${'Monolith Files (>500 lines of code)'.padEnd(40)} ${monoBeforeStr.padEnd(16)} ${monoAfterStr.padEnd(16)} ${formatDeltaNumber(delta.monolithDelta, true)}`);
315
+
316
+ const excessBeforeStr = `${beforeSnapshot.tokens.estimatedExcessTokens.toLocaleString()} tok`;
317
+ const excessAfterStr = `${afterSnapshot.tokens.estimatedExcessTokens.toLocaleString()} tok`;
318
+ lines.push(` ${'Excess Token Burn'.padEnd(40)} ${excessBeforeStr.padEnd(16)} ${excessAfterStr.padEnd(16)} ${formatDeltaNumber(delta.tokensDelta, true)}`);
319
+
320
+ const taxBeforeStr = `$${delta.monthlyTaxBefore.toFixed(2)}/mo`;
321
+ const taxAfterStr = `$${delta.monthlyTaxAfter.toFixed(2)}/mo`;
322
+ lines.push(` ${'Dev Context Tax (Monthly)'.padEnd(40)} ${taxBeforeStr.padEnd(16)} ${taxAfterStr.padEnd(16)} ${formatDeltaCurrency(delta.monthlyTaxDelta, '/mo')}`);
323
+
324
+ lines.push(` ----------------------------------------------------------------------------`);
325
+ lines.push('');
326
+
327
+ const resolvePillarDeltaArrow = (pDelta) => {
328
+ if (pDelta.improved) {
329
+ return `${GREEN}▲ RESOLVED${RESET}`;
330
+ }
331
+ if (pDelta.beforeStatus === pDelta.afterStatus) {
332
+ return `${DIM}━ UNCHANGED${RESET}`;
333
+ }
334
+ return `${RED}▼ DEGRADED${RESET}`;
335
+ };
336
+
337
+ lines.push(`${BOLD} 7-PILLAR PROGRESSION${RESET}`);
338
+ lines.push(` ----------------------------------------------------------------------------`);
339
+ for (const [pillar, pDelta] of Object.entries(delta.pillarDeltas)) {
340
+ const arrow = resolvePillarDeltaArrow(pDelta);
341
+ lines.push(` ${pillar.padEnd(40)} ${pDelta.beforeStatus.padEnd(8)} -> ${pDelta.afterStatus.padEnd(8)} ${arrow}`);
342
+ }
343
+ lines.push(` ----------------------------------------------------------------------------`);
344
+ lines.push('');
345
+
346
+ if (delta.isImproved) {
347
+ lines.push(` ${GREEN}${BOLD}✔ CODEBASE SIGNIFICANTLY IMPROVED!${RESET}`);
348
+ lines.push(` Post your transformation story to GitHub Discussions using the Share menu.`);
349
+ } else {
350
+ lines.push(` ${YELLOW}ℹ Codebase metrics are stable or currently under refactoring.${RESET}`);
351
+ }
352
+ lines.push('');
353
+
354
+ return lines.join('\n');
355
+ };
356
+
357
+ export const formatHistoryTimelineTerminal = (history) => {
358
+ const lines = [];
359
+ lines.push('');
360
+ lines.push(`${CYAN}======================================================================${RESET}`);
361
+ lines.push(`${BOLD}${CYAN} LOCAL AUDIT HISTORY TIMELINE (${history.length} RUNS RECORDED)${RESET}`);
362
+ lines.push(`${DIM} Persistent snapshots stored in .chemx/history.json${RESET}`);
363
+ lines.push(`${CYAN}======================================================================${RESET}`);
364
+ lines.push('');
365
+
366
+ if (history.length === 0) {
367
+ lines.push(' No historical audits recorded yet.');
368
+ lines.push('');
369
+ return lines.join('\n');
370
+ }
371
+
372
+ lines.push(` ${'#'.padEnd(4)} ${'Date/Time'.padEnd(22)} ${'Score'.padEnd(12)} ${'Grade'.padEnd(10)} ${'Monoliths'.padEnd(12)} Hazards`);
373
+ lines.push(` ----------------------------------------------------------------------------`);
374
+
375
+ const resolveScoreGradeColor = (score) => {
376
+ if (score >= 90) return GREEN;
377
+ if (score >= 70) return YELLOW;
378
+ return RED;
379
+ };
380
+
381
+ const renderTimelineRow = (snap, idx) => {
382
+ const num = `${idx + 1}`.padEnd(4);
383
+ const dateStr = new Date(snap.timestamp).toLocaleString().slice(0, 20).padEnd(22);
384
+ const scoreStr = `${snap.health.score}/100`.padEnd(12);
385
+ const gradeColor = resolveScoreGradeColor(snap.health.score);
386
+ const gradeStr = `${gradeColor}${snap.health.grade.padEnd(10)}${RESET}`;
387
+ const monoStr = `${snap.monoliths.total} files`.padEnd(12);
388
+ const hazStr = `${snap.violations.total} (Crit: ${snap.violations.critical})`;
389
+ lines.push(` ${num} ${dateStr} ${scoreStr} ${gradeStr} ${monoStr} ${hazStr}`);
390
+ };
391
+
392
+ history.forEach(renderTimelineRow);
393
+
394
+ lines.push(` ----------------------------------------------------------------------------`);
395
+ lines.push('');
396
+ return lines.join('\n');
397
+ };
@@ -0,0 +1,179 @@
1
+ import { PILLARS } from './rules.js';
2
+
3
+ export const calculateMolecularHealthScore = (violations, totalFiles) => {
4
+ if (totalFiles === 0) {
5
+ return { score: 100, grade: 'A+', label: 'Crystalline Molecular' };
6
+ }
7
+
8
+ let penalty = 0;
9
+ for (const v of violations) {
10
+ if (v.severity === 'CRITICAL') penalty += 8;
11
+ else if (v.severity === 'HIGH') penalty += 4;
12
+ else if (v.severity === 'MEDIUM') penalty += 2;
13
+ else if (v.severity === 'LOW') penalty += 1;
14
+ }
15
+
16
+ // Normalize penalty against codebase size
17
+ const scale = Math.max(1, Math.log10(totalFiles + 1));
18
+ const adjustedPenalty = penalty / scale;
19
+ const score = Math.max(0, Math.min(100, Math.round(100 - adjustedPenalty)));
20
+
21
+ let grade = 'F';
22
+ let label = 'Severe Context Rot';
23
+
24
+ if (score >= 95) {
25
+ grade = 'A+';
26
+ label = 'Crystalline Molecular';
27
+ } else if (score >= 90) {
28
+ grade = 'A';
29
+ label = 'Highly Modular';
30
+ } else if (score >= 80) {
31
+ grade = 'B';
32
+ label = 'Moderately Modular';
33
+ } else if (score >= 70) {
34
+ grade = 'C';
35
+ label = 'Monolithic Drift';
36
+ } else if (score >= 60) {
37
+ grade = 'D';
38
+ label = 'High Context Hazard';
39
+ }
40
+
41
+ return { score, grade, label };
42
+ };
43
+
44
+ export const calculatePillarBreakdown = (violations) => {
45
+ const breakdown = {};
46
+ for (const [key, name] of Object.entries(PILLARS)) {
47
+ breakdown[name] = {
48
+ pillarKey: key,
49
+ violations: 0,
50
+ critical: 0,
51
+ high: 0,
52
+ medium: 0,
53
+ low: 0,
54
+ status: 'PASSED'
55
+ };
56
+ }
57
+
58
+ for (const v of violations) {
59
+ const pillarName = v.pillar || PILLARS.PILLAR_1;
60
+ if (!breakdown[pillarName]) {
61
+ breakdown[pillarName] = {
62
+ pillarKey: 'UNKNOWN',
63
+ violations: 0,
64
+ critical: 0,
65
+ high: 0,
66
+ medium: 0,
67
+ low: 0,
68
+ status: 'PASSED'
69
+ };
70
+ }
71
+ const item = breakdown[pillarName];
72
+ item.violations += 1;
73
+ if (v.severity === 'CRITICAL') item.critical += 1;
74
+ else if (v.severity === 'HIGH') item.high += 1;
75
+ else if (v.severity === 'MEDIUM') item.medium += 1;
76
+ else if (v.severity === 'LOW') item.low += 1;
77
+ }
78
+
79
+ for (const item of Object.values(breakdown)) {
80
+ if (item.critical > 0) {
81
+ item.status = 'FAILED';
82
+ } else if (item.high > 0 || item.medium > 1) {
83
+ item.status = 'WARN';
84
+ } else {
85
+ item.status = 'PASSED';
86
+ }
87
+ }
88
+
89
+ return breakdown;
90
+ };
91
+
92
+ export const MODEL_PRICING_RATES = {
93
+ blended: { name: 'Frontier Blended ($3.00/1M)', costPerMillion: 3.0 },
94
+ claude: { name: 'Claude 3.5 Sonnet ($3.00/1M)', costPerMillion: 3.0 },
95
+ gpt4o: { name: 'GPT-4o ($2.50/1M)', costPerMillion: 2.5 }
96
+ };
97
+
98
+ export const calculateTokenBurnAnalytics = (fileStats, options = {}) => {
99
+ let totalRawChars = 0;
100
+ let excessChars = 0;
101
+
102
+ for (const f of fileStats) {
103
+ totalRawChars += f.charCount;
104
+ // Budget: 500 lines max for files (~18,000 chars), 100 lines for molecules (~3,600 chars)
105
+ const maxChars = f.isMolecule ? 3600 : 18000;
106
+ if (f.charCount > maxChars) {
107
+ excessChars += f.charCount - maxChars;
108
+ }
109
+ }
110
+
111
+ const estimatedTokens = Math.round(totalRawChars / 3.8);
112
+ const estimatedExcessTokens = Math.round(excessChars / 3.8);
113
+ const potentialSavingsPct = totalRawChars > 0
114
+ ? Math.min(85, Math.round((excessChars / totalRawChars) * 100))
115
+ : 0;
116
+
117
+ const resolveRiskLevel = (pct) => {
118
+ if (pct > 40) return 'HIGH';
119
+ if (pct > 15) return 'MODERATE';
120
+ return 'LOW';
121
+ };
122
+
123
+ const selectedModel = options?.model?.toLowerCase() || 'blended';
124
+ const modelConfig = MODEL_PRICING_RATES[selectedModel] || MODEL_PRICING_RATES.blended;
125
+ const costPerMillion = options?.costPerMillion ? Number(options.costPerMillion) : modelConfig.costPerMillion;
126
+
127
+ const excessCostPerPass = Number(((estimatedExcessTokens / 1000000) * costPerMillion).toFixed(3));
128
+ const weeklyWastePerDev = Number((excessCostPerPass * 20 * 5).toFixed(2));
129
+ const monthlyWastePerDev = Number((weeklyWastePerDev * 4).toFixed(2));
130
+
131
+ return {
132
+ estimatedTokens,
133
+ estimatedExcessTokens,
134
+ potentialSavingsPct,
135
+ riskLevel: resolveRiskLevel(potentialSavingsPct),
136
+ pricingModel: modelConfig.name,
137
+ costPerMillion,
138
+ excessCostPerPass,
139
+ weeklyWastePerDev,
140
+ monthlyWastePerDev
141
+ };
142
+ };
143
+
144
+ const resolveMonolithTierName = (lineCount) => {
145
+ if (lineCount >= 2000) return 'CRITICAL';
146
+ if (lineCount >= 1000) return 'SEVERE';
147
+ if (lineCount > 500) return 'WARNING';
148
+ return null;
149
+ };
150
+
151
+ export const calculateHotspots = (violations, fileStats, limit = 5) => {
152
+ const violationCountsByFile = {};
153
+ for (const v of violations) {
154
+ violationCountsByFile[v.filePath] = (violationCountsByFile[v.filePath] || 0) + 1;
155
+ }
156
+
157
+ const fileMap = {};
158
+ for (const f of fileStats) {
159
+ fileMap[f.relativePath] = f;
160
+ }
161
+
162
+ const scoredFiles = Object.entries(violationCountsByFile).map(([filePath, count]) => {
163
+ const stats = fileMap[filePath] || { lineCount: 0 };
164
+ const monolithTier = resolveMonolithTierName(stats.lineCount);
165
+
166
+ return {
167
+ filePath,
168
+ violationCount: count,
169
+ lineCount: stats.lineCount,
170
+ isMonolith: stats.lineCount > 500,
171
+ monolithTier
172
+ };
173
+ });
174
+
175
+ scoredFiles.sort((a, b) => b.violationCount - a.violationCount || b.lineCount - a.lineCount);
176
+ return scoredFiles.slice(0, limit);
177
+ };
178
+
179
+ export const calculateQuantumHealthScore = calculateMolecularHealthScore;