Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ node_modules/
.DS_Store

# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
Expand Down
143 changes: 8 additions & 135 deletions app/api/astrology-mathbrain/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ import { randomUUID } from 'crypto';
export const maxDuration = 26; // seconds
export const dynamic = 'force-dynamic'; // Disable caching for this route

// Reuse the legacy math brain implementation directly
const mathBrainFunction = require('../../../lib/server/astrology-mathbrain.js');
import { mathBrainService } from '@/src/math_brain/service';

// NEW: Import the v2 Math Brain orchestrator
const { runMathBrain } = require('../../../src/math_brain/main.js');
Expand Down Expand Up @@ -88,57 +87,12 @@ function buildFailureResponse(status: number, payload: any, rawBody: string | nu
}

export async function GET(request: NextRequest) {
// Convert Next.js request to Netlify event format
const url = new URL(request.url);

// Convert headers
const headers: Record<string, string> = {};
request.headers.forEach((value, key) => {
headers[key] = value;
return NextResponse.json({
success: true,
message: 'Astrology MathBrain v2 service is active.',
service: 'astrology-mathbrain',
version: 'v2'
});

const event = {
httpMethod: 'GET',
queryStringParameters: Object.fromEntries(url.searchParams),
headers,
body: null,
path: url.pathname,
pathParameters: null,
requestContext: {},
resource: '',
stageVariables: null,
isBase64Encoded: false
};

const context = {
callbackWaitsForEmptyEventLoop: false,
functionName: 'astrology-mathbrain',
functionVersion: '$LATEST',
invokedFunctionArn: '',
memoryLimitInMB: '1024',
awsRequestId: randomUUID(),
logGroupName: '',
logStreamName: '',
getRemainingTimeInMillis: () => 30000
};

try {
const result = await mathBrainFunction.handler(event, context);

return new NextResponse(result.body, {
status: result.statusCode,
headers: new Headers(result.headers || {})
});
} catch (error: any) {
logger.error('Astrology MathBrain API error', {
error: error instanceof Error ? error.message : String(error)
});
return NextResponse.json({
success: false,
error: 'Internal server error',
code: 'ASTROLOGY_API_ERROR'
}, { status: 500 });
}
}

export async function POST(request: NextRequest) {
Expand All @@ -149,93 +103,12 @@ export async function POST(request: NextRequest) {
return NextResponse.json({ success: false, error: 'Invalid JSON body' }, { status: 400 });
}

const body = JSON.stringify(rawPayload);

const windowConfig = rawPayload?.window || null;
const windowStep = typeof windowConfig?.step === 'string' ? windowConfig.step.toLowerCase() : null;

if (windowStep === 'daily' && windowConfig?.start && windowConfig?.end) {
const startDate = parseIsoDate(windowConfig.start);
const endDate = parseIsoDate(windowConfig.end);
if (!startDate || !endDate) {
logger.warn('Invalid daily window dates received', { start: windowConfig.start, end: windowConfig.end });
return NextResponse.json({
success: false,
error: 'Invalid transit window dates. Please use ISO format (YYYY-MM-DD).',
code: 'INVALID_TRANSIT_WINDOW'
}, { status: 400 });
}

if (endDate.getTime() < startDate.getTime()) {
logger.warn('Daily window end precedes start', { start: windowConfig.start, end: windowConfig.end });
return NextResponse.json({
success: false,
error: 'Transit window end date must be on or after the start date.',
code: 'INVALID_TRANSIT_WINDOW_ORDER'
}, { status: 400 });
}

const totalDays = countInclusiveDays(startDate, endDate);
if (totalDays > MAX_DAILY_TRANSIT_WINDOW_DAYS) {
logger.warn('Daily window exceeds maximum allowed span', { totalDays, start: windowConfig.start, end: windowConfig.end });
return NextResponse.json({
success: false,
error: `Daily symbolic weather windows are limited to ${MAX_DAILY_TRANSIT_WINDOW_DAYS} days. Consider weekly sampling or shorten the range.`,
code: 'TRANSIT_WINDOW_TOO_LARGE',
limit: MAX_DAILY_TRANSIT_WINDOW_DAYS
}, { status: 400 });
}
}

// Prepare event for legacy handler
const url = new URL(request.url);
const headers: Record<string, string> = {};
request.headers.forEach((value, key) => { headers[key] = value; });

const event = {
httpMethod: 'POST',
headers,
body,
queryStringParameters: Object.fromEntries(url.searchParams),
path: url.pathname, pathParameters: null, requestContext: {}, resource: '', stageVariables: null, isBase64Encoded: false
};

const context = {
callbackWaitsForEmptyEventLoop: false, functionName: 'astrology-mathbrain', functionVersion: '$LATEST', invokedFunctionArn: '',
memoryLimitInMB: '1024', awsRequestId: randomUUID(), logGroupName: '', logStreamName: '', getRemainingTimeInMillis: () => 30000
};

// Execute the unified pipeline
logger.info('Routing to unified Math Brain v2 pipeline');

try {
// Get raw chart data by calling the legacy handler
const legacyResult = await mathBrainFunction.handler(event, context);
const legacyStatus = legacyResult?.statusCode ?? 500;
let legacyBody: any = null;
let rawBody: string | null = null;

if (legacyResult?.body) {
rawBody = legacyResult.body;
try {
legacyBody = JSON.parse(legacyResult.body);
} catch {
legacyBody = { error: legacyResult.body };
}
}

if (!legacyResult || legacyStatus >= 400) {
logger.error('Failed to fetch raw chart data from legacy handler', {
statusCode: legacyStatus,
errorCode: legacyBody?.code,
errorMessage: legacyBody?.error,
errorDetails: legacyBody
});

return buildFailureResponse(legacyStatus, legacyBody, rawBody);
}

const chartData = legacyBody;
// Get raw chart data by calling the new Math Brain Service
const chartData = await mathBrainService.fetch(rawPayload);

// Prepare the config for the v2 formatter/aggregator
const relationshipContextRaw =
Expand Down
148 changes: 148 additions & 0 deletions src/math_brain/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// src/math_brain/service.ts

/**
* @module mathBrainService
*
* This service is responsible for orchestrating the fetching and processing of
* astrological chart data. It acts as a replacement for the monolithic
* `lib/server/astrology-mathbrain.js` file, providing a modern, testable,
* and maintainable interface for the Math Brain v2 system.
*/

import {
normalizeSubjectData,
validateSubject,
buildHeaders,
fetchNatalChartComplete,
getTransits,
calculateSeismograph,
computeComposite,
} from './utils';


interface ChartDataPayload {
[key: string]: any;
}

interface ChartDataResult {
success: boolean;
provenance: any;
person_a: any;
person_b?: any;
synastry?: any;
composite?: any;
woven_map?: any;
relationship_context?: any;
transitsByDate?: any;
[key: string]: any; // Allow other properties to match monolith's output
}

class MathBrainService {
/**
* Fetches and processes all necessary astrological data for a given request payload.
* This is the primary entry point for the service.
*
* @param {ChartDataPayload} rawPayload - The raw request body from the API route.
* @returns {Promise<ChartDataResult>} The processed chart data, ready for the v2 orchestrator.
*/
public async fetch(rawPayload: ChartDataPayload): Promise<ChartDataResult> {
const personA = normalizeSubjectData(rawPayload.personA || {});
const validation = validateSubject(personA);
if (!validation.isValid) {
throw new Error(`Primary subject validation failed: ${validation.message}`);
}

const headers = buildHeaders();
const pass = {};
[
'active_points', 'active_aspects', 'houses_system_identifier',
'sidereal_mode', 'perspective_type', 'wheel_only', 'wheel_format',
'theme', 'language'
].forEach((key) => {
if (rawPayload[key] !== undefined) pass[key] = rawPayload[key];
});

const personANatal = await fetchNatalChartComplete(
personA,
headers,
pass,
'person_a',
rawPayload.mode || 'standard'
);

const result: ChartDataResult = {
success: true,
provenance: {},
person_a: {
details: personANatal.details,
chart: personANatal.chart,
aspects: personANatal.aspects,
assets: personANatal.assets || [],
},
};

const win = rawPayload.window || null;
const start = win?.start;
const end = win?.end;
const step = win?.step || 'daily';
const haveRange = Boolean(start && end);

if (haveRange) {
const { transitsByDate, retroFlagsByDate, provenanceByDate } = await getTransits(
personA,
{ startDate: start, endDate: end, step: step },
headers,
pass
);
const seismographData = calculateSeismograph(transitsByDate, retroFlagsByDate, {
orbsProfile: rawPayload.orbs_profile || 'wm-tight-2025-11-v5'
});
result.person_a.chart.transitsByDate = seismographData.daily;
result.person_a.chart.provenanceByDate = provenanceByDate;
result.person_a.derived = {
seismograph_summary: seismographData.summary,
};
}
Comment on lines +90 to +105
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing handling for Person B transits in relational modes. When isSynastry or isComposite is true and transits are requested, the service should also compute transits for Person B, not just Person A. The original monolith computed transits for both subjects in synastry/composite transit modes.

Copilot uses AI. Check for mistakes.

const hasPersonB = rawPayload.personB && Object.keys(rawPayload.personB).length > 0;
const mode = rawPayload.mode || '';
const isSynastry = mode.includes('SYNASTRY');
const isComposite = mode.includes('COMPOSITE');
const isRelational = isSynastry || isComposite || hasPersonB;

if (isRelational) {
if (!hasPersonB) {
throw new Error('Relational report requested but personB is missing.');
}
const personB = normalizeSubjectData(rawPayload.personB);
const validationB = validateSubject(personB);
if (!validationB.isValid) {
throw new Error(`Secondary subject validation failed: ${validationB.message}`);
}
const personBNatal = await fetchNatalChartComplete(
personB,
headers,
pass,
'person_b',
mode
);
result.person_b = {
details: personBNatal.details,
chart: personBNatal.chart,
aspects: personBNatal.aspects,
assets: personBNatal.assets || [],
};
if (isComposite) {
const composite = await computeComposite(personA, personB, pass, headers);
result.composite = {
aspects: composite.aspects,
data: composite.raw,
};
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing composite transits calculation. When isComposite is true and a transit window is provided, the service should compute transits to the composite chart, not just return the composite natal aspects. The original handler had logic to calculate composite transits with seismograph data.

Suggested change
};
};
// If a transit window is provided, compute composite transits and seismograph
if (rawPayload.transitWindow) {
// getTransits expects a chart and a window; composite.raw is the chart data
const compositeTransits = await getTransits(
composite.raw,
rawPayload.transitWindow,
headers,
pass
);
// calculateSeismograph expects a natal chart and a set of transits
const compositeSeismograph = calculateSeismograph(
composite.raw,
compositeTransits
);
result.composite.transits = compositeTransits;
result.composite.seismograph = compositeSeismograph;
}

Copilot uses AI. Check for mistakes.
}
Comment on lines +113 to +141
Copy link

Copilot AI Nov 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing synastry aspects calculation in the service. The original monolith handler computed synastry aspects when isSynastry is true, but this implementation only fetches natal charts for both subjects without computing the cross-chart aspects. This will result in incomplete data for synastry reports.

Copilot uses AI. Check for mistakes.
}

return result;
}
}

export const mathBrainService = new MathBrainService();
Loading
Loading