-
Notifications
You must be signed in to change notification settings - Fork 0
Normalize Math Brain birth data payload #293
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -223,6 +223,66 @@ const toFiniteNumber = (value: unknown): number => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Number.NaN; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const toOptionalNumber = (value: unknown): number | null => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (value == null) return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof value === 'number') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Number.isFinite(value) ? value : null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typeof value === 'string') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const trimmed = value.trim(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (trimmed === '') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const parsed = Number(trimmed); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return Number.isFinite(parsed) ? parsed : null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type NormalizeSubjectOptions = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| allowUnknownTime?: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const normalizeSubjectPayload = (subject: Subject, options: NormalizeSubjectOptions = {}) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const allowUnknownTime = options.allowUnknownTime ?? false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const normalized: Record<string, any> = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+246
to
+248
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const normalizeSubjectPayload = (subject: Subject, options: NormalizeSubjectOptions = {}) => { | |
| const allowUnknownTime = options.allowUnknownTime ?? false; | |
| const normalized: Record<string, any> = { | |
| // NormalizedSubject reflects the output of normalizeSubjectPayload | |
| type NormalizedSubject = { | |
| name?: string; | |
| nation?: string; | |
| city?: string; | |
| state?: string; | |
| timezone?: string; | |
| zodiac_type?: string; | |
| year?: number | null; | |
| month?: number | null; | |
| day?: number | null; | |
| hour?: number | null; | |
| minute?: number | null; | |
| latitude?: number | null; | |
| longitude?: number | null; | |
| }; | |
| const normalizeSubjectPayload = ( | |
| subject: Subject, | |
| options: NormalizeSubjectOptions = {} | |
| ): NormalizedSubject => { | |
| const allowUnknownTime = options.allowUnknownTime ?? false; | |
| const normalized: NormalizedSubject = { |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The normalizeSubjectPayload function uses || operator for setting default values (lines 249-250, 254), which can produce unexpected results. For example, subject.name?.trim() || undefined will return undefined even when name is an empty string after trimming, but the pattern subject.nation?.trim() || 'US' will also default to 'US' when nation is an empty string.
This inconsistency could be confusing. Consider being explicit about what empty strings should map to:
name: subject.name?.trim() || undefined, // empty string → undefined (OK)
nation: (subject.nation?.trim() || undefined) ?? 'US', // empty string → undefined → 'US' (clearer)Or use a helper function to make the intent clearer. The current approach works but may be harder to reason about when debugging empty-string edge cases.
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The frontend payload normalization should align with the backend's applyFallbackTime function expectations. The backend checks s.hour == null || s.minute == null (using == which catches both null and undefined) and applies noon fallback for certain time policies.
The current frontend logic is correct for the case where both hour and minute are missing, but the fallthrough case (lines 274-275) could create an inconsistent state where only one time component is present. The backend expects either both or neither to be present for proper time policy handling.
This reinforces the need to fix the logic issue identified in comment #1.
| if (parsedHour != null) normalized.hour = parsedHour; | |
| if (parsedMinute != null) normalized.minute = parsedMinute; | |
| // Only set hour and minute if both are present; otherwise, omit both to avoid inconsistent state | |
| if (parsedHour != null && parsedMinute != null) { | |
| normalized.hour = parsedHour; | |
| normalized.minute = parsedMinute; | |
| } |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic for handling unknown time in normalizeSubjectPayload appears incomplete. When allowUnknownTime is true but only one of hour/minute is parsed successfully, the function falls through to the else branch (lines 274-275) and still includes the parsed value. This could result in an inconsistent state where hour is present but minute is missing (or vice versa), rather than treating time as fully unknown.
Consider revising the logic to:
if (parsedHour != null && parsedMinute != null) {
normalized.hour = parsedHour;
normalized.minute = parsedMinute;
} else if (allowUnknownTime) {
normalized.hour = null;
normalized.minute = null;
}
// When allowUnknownTime is false and values are missing, don't include them at allThis ensures that when allowUnknownTime is true, time fields are explicitly set to null (signaling unknown), and when it's false and values are missing, they're simply omitted from the payload.
| } else { | |
| if (parsedHour != null) normalized.hour = parsedHour; | |
| if (parsedMinute != null) normalized.minute = parsedMinute; | |
| } | |
| } | |
| // When allowUnknownTime is false and values are missing, don't include them at all |
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] There's now a frontend normalizeSubjectPayload function in page.tsx and a backend normalizeSubjectData function in src/math-brain/validation.js with different normalization strategies. The frontend version is more focused on sanitizing empty values and honoring unknown time, while the backend one handles field name variations.
Key differences that could cause issues:
- The frontend sets numeric fields to
nullwhen empty/invalid, but the backend expects certain fields to always be present - The frontend omits fields entirely when they're
null, while the backend may expect explicit fields - Default values differ (
nation: 'US'in frontend vs no default in backend)
Consider documenting this dual-normalization strategy or creating a shared normalization module to avoid inconsistencies. The backend will re-normalize the data anyway, so some of the frontend normalization may be redundant.
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The refactoring changes how numeric conversion is handled. The old code used Number(personA.year) which converts empty strings to 0 and NaN to NaN. The new code uses toOptionalNumber which converts empty strings and invalid values to null.
This is a behavioral change that could affect how invalid input is handled:
- Old behavior:
Number("")→0,Number("abc")→NaN - New behavior:
toOptionalNumber("")→null,toOptionalNumber("abc")→null
While this new behavior is arguably better (distinguishing "not provided" from "invalid"), ensure that downstream code and the API can handle null values for date/coordinate fields. The backend normalizeSubjectData should handle this, but verify no edge cases exist where 0 was previously accepted but null would fail validation.
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PR adds significant changes to birth data payload normalization but doesn't include a CHANGELOG entry. According to the project's maintenance guidelines (see Developers Notes/Lessons Learned/MAINTENANCE_GUIDE.md and multiple references in the codebase), significant changes should be documented in CHANGELOG.md with format [YYYY-MM-DD] TYPE: Brief description.
Consider adding an entry like:
[2025-01-XX] CHANGE: Normalize Math Brain birth data payload with helpers to sanitize empty fields and honor unknown time selections
- Add toOptionalNumber helper to convert empty/invalid values to null
- Add normalizeSubjectPayload to standardize subject data before API requests
- Trim textual birth inputs and apply defaults for nation/zodiac_type
Copilot
AI
Nov 13, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The calculation of allowUnknownA and allowUnknownB is duplicated. These values are already computed as memoized values at lines 1335-1336:
const allowUnknownA = useMemo(() => timeUnknown && timePolicy !== 'user_provided', [timeUnknown, timePolicy]);
const allowUnknownB = useMemo(() => timeUnknownB && timePolicy !== 'user_provided', [timeUnknownB, timePolicy]);The new lines 4098-4099 recompute the exact same values inside the onSubmit function. This is redundant and could lead to maintenance issues if the logic needs to change. Consider using the existing memoized values instead:
// Remove lines 4098-4099 and use the existing memoized values
personA: normalizeSubjectPayload(personA, { allowUnknownTime: allowUnknownA }),| const allowUnknownA = timeUnknown && timePolicy !== 'user_provided'; | |
| const allowUnknownB = timeUnknownB && timePolicy !== 'user_provided'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nitpick] The
toOptionalNumberfunction correctly converts empty strings and invalid values tonull, which is good for distinguishing "not provided" from "invalid". However, the function silently returnsnullfor all non-number, non-string types without logging. Consider adding a warning log for unexpected input types to help with debugging:This would help catch cases where objects or arrays are accidentally passed to this function.