2026-08-19 01:43:50 +03:30
|
|
|
/** Convert an absolute instant into weekday + minute-of-day in an IANA time zone. */
|
|
|
|
|
|
|
|
|
|
const JS_WEEKDAY: Record<string, number> = {
|
|
|
|
|
Sun: 0,
|
|
|
|
|
Mon: 1,
|
|
|
|
|
Tue: 2,
|
|
|
|
|
Wed: 3,
|
|
|
|
|
Thu: 4,
|
|
|
|
|
Fri: 5,
|
|
|
|
|
Sat: 6,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export function isValidIanaTimeZone(timeZone: string): boolean {
|
|
|
|
|
if (!timeZone || timeZone.length > 64) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
Intl.DateTimeFormat('en-US', { timeZone }).format(new Date(0));
|
|
|
|
|
return true;
|
|
|
|
|
} catch {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function zonedWeekdayAndMinutes(
|
|
|
|
|
date: Date,
|
|
|
|
|
timeZone: string,
|
|
|
|
|
): { jsWeekday: number; minuteOfDay: number } {
|
|
|
|
|
const parts = new Intl.DateTimeFormat('en-US', {
|
|
|
|
|
timeZone,
|
|
|
|
|
weekday: 'short',
|
|
|
|
|
hour: '2-digit',
|
|
|
|
|
minute: '2-digit',
|
|
|
|
|
hourCycle: 'h23',
|
|
|
|
|
}).formatToParts(date);
|
|
|
|
|
|
|
|
|
|
const weekdayToken = parts.find((p) => p.type === 'weekday')?.value ?? 'Sun';
|
|
|
|
|
let hour = Number(parts.find((p) => p.type === 'hour')?.value ?? '0');
|
|
|
|
|
const minute = Number(parts.find((p) => p.type === 'minute')?.value ?? '0');
|
|
|
|
|
if (hour === 24) {
|
|
|
|
|
hour = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
jsWeekday: JS_WEEKDAY[weekdayToken] ?? 0,
|
|
|
|
|
minuteOfDay: hour * 60 + minute,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Weekday of a YYYY-MM-DD civil date (Gregorian; same worldwide). */
|
|
|
|
|
export function civilDateJsWeekday(isoDate: string): number {
|
|
|
|
|
const [y, m, d] = isoDate.split('-').map(Number);
|
|
|
|
|
const utcNoon = new Date(Date.UTC(y, m - 1, d, 12, 0, 0, 0));
|
|
|
|
|
return utcNoon.getUTCDay();
|
|
|
|
|
}
|
2026-08-20 17:17:15 +03:30
|
|
|
|
|
|
|
|
/**
|
docs: cut the comments that were not earning their place
I wrote 731 comment lines on this branch against 4,530 lines of code — 14%,
where the rest of the repo runs at 1.8%. CLAUDE.md asks for code that reads
like its surroundings, and this did not.
Removed by genre rather than by taste:
- restating the code, e.g. "JS getUTCDay() numbering: Sunday = 0" above the
map that literally shows it, and a docblock on startOfWeek explaining that
it returns the start of the week;
- narrating history — "this used to rebuild the whole map", "left the bar
recording forever" — which the commit message and git blame already carry;
- saying the same thing in several places: the "cannot record is not a
denied microphone" reason appeared three times in one file, and the
"aborting stops a per-minute metered call" reason across three files. Each
now lives once, where the behaviour it explains lives;
- defending decisions nobody would question, like why toLatinDigits is its
own module;
- over-explaining defensive branches, three separate comments to distinguish
null from missing-kind from unrecognised-kind.
What stays is what the code cannot say: the patient-right convention in
toFdi, whose failure mode is a valid code for the wrong tooth; the
"this"-vs-"next" week anchoring; StrictMode re-arming mountedRef; Safari
accepting no mimeType hint; and the invariants whose violation already cost
a bug — the body parser's middleware ordering and the dispatch panel's
auto-fill rules.
Comments only. The diff contains no non-comment line.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:46:56 +08:00
|
|
|
* Today's civil date (`YYYY-MM-DD`) in an IANA zone, so the server derives "today" from a
|
|
|
|
|
* client-supplied *zone* rather than trusting a client-supplied date.
|
2026-08-20 17:17:15 +03:30
|
|
|
*/
|
|
|
|
|
export function civilDateInZone(date: Date, timeZone: string): string {
|
docs: cut the comments that were not earning their place
I wrote 731 comment lines on this branch against 4,530 lines of code — 14%,
where the rest of the repo runs at 1.8%. CLAUDE.md asks for code that reads
like its surroundings, and this did not.
Removed by genre rather than by taste:
- restating the code, e.g. "JS getUTCDay() numbering: Sunday = 0" above the
map that literally shows it, and a docblock on startOfWeek explaining that
it returns the start of the week;
- narrating history — "this used to rebuild the whole map", "left the bar
recording forever" — which the commit message and git blame already carry;
- saying the same thing in several places: the "cannot record is not a
denied microphone" reason appeared three times in one file, and the
"aborting stops a per-minute metered call" reason across three files. Each
now lives once, where the behaviour it explains lives;
- defending decisions nobody would question, like why toLatinDigits is its
own module;
- over-explaining defensive branches, three separate comments to distinguish
null from missing-kind from unrecognised-kind.
What stays is what the code cannot say: the patient-right convention in
toFdi, whose failure mode is a valid code for the wrong tooth; the
"this"-vs-"next" week anchoring; StrictMode re-arming mountedRef; Safari
accepting no mimeType hint; and the invariants whose violation already cost
a bug — the body parser's middleware ordering and the dispatch panel's
auto-fill rules.
Comments only. The diff contains no non-comment line.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 23:46:56 +08:00
|
|
|
// Intl throws RangeError on an unknown zone and this takes a client-supplied string;
|
|
|
|
|
// callers validate first, this is the backstop.
|
fix(backend): correct "next weekday" and harden resolvers against model output
Four defects found by review of the preceding commits.
"next <weekday>" was occurrence-anchored ("this" plus seven) rather than week-
anchored. Said on a Thursday, "Thursday next week" resolved to +14 instead of
+7: next week runs Sat 10-18 to Fri 10-24, so its Thursday is 10-23, not 10-30.
A lab case a week late. "next" now counts from the start of the following
Saturday-start week, which also lets "this" and "next" correctly coincide —
said on a Thursday, "the coming Saturday" and "Saturday next week" are the same
day. "this" stays occurrence-anchored so it can never resolve into the past.
The other three all come from the same root cause: exported functions that are
reachable from untrusted model output must degrade, not throw or drop.
- a non-object `due` (the model emitting a bare string) was treated as "no
deadline spoken" and silently discarded; only null/undefined mean absent now,
anything else is flagged so the clinician sees something was heard and lost
- isJalaliLeapYear / jalaliDaysInMonth threw for years outside the conversion
table, contradicting the module's own "degrade to null" contract; they now
return false / 0, which also makes isValidJalaliDate's day check naturally
false
- civilDateInZone passed a client-supplied zone straight to Intl, which raises
RangeError before any fallback; it now validates and backstops to UTC, so a
bad zone costs at most a day rather than a 500
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 17:25:44 +03:30
|
|
|
const zone = isValidIanaTimeZone(timeZone) ? timeZone : 'UTC';
|
2026-08-20 17:17:15 +03:30
|
|
|
const parts = new Intl.DateTimeFormat('en-CA', {
|
fix(backend): correct "next weekday" and harden resolvers against model output
Four defects found by review of the preceding commits.
"next <weekday>" was occurrence-anchored ("this" plus seven) rather than week-
anchored. Said on a Thursday, "Thursday next week" resolved to +14 instead of
+7: next week runs Sat 10-18 to Fri 10-24, so its Thursday is 10-23, not 10-30.
A lab case a week late. "next" now counts from the start of the following
Saturday-start week, which also lets "this" and "next" correctly coincide —
said on a Thursday, "the coming Saturday" and "Saturday next week" are the same
day. "this" stays occurrence-anchored so it can never resolve into the past.
The other three all come from the same root cause: exported functions that are
reachable from untrusted model output must degrade, not throw or drop.
- a non-object `due` (the model emitting a bare string) was treated as "no
deadline spoken" and silently discarded; only null/undefined mean absent now,
anything else is flagged so the clinician sees something was heard and lost
- isJalaliLeapYear / jalaliDaysInMonth threw for years outside the conversion
table, contradicting the module's own "degrade to null" contract; they now
return false / 0, which also makes isValidJalaliDate's day check naturally
false
- civilDateInZone passed a client-supplied zone straight to Intl, which raises
RangeError before any fallback; it now validates and backstops to UTC, so a
bad zone costs at most a day rather than a 500
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-20 17:25:44 +03:30
|
|
|
timeZone: zone,
|
2026-08-20 17:17:15 +03:30
|
|
|
year: 'numeric',
|
|
|
|
|
month: '2-digit',
|
|
|
|
|
day: '2-digit',
|
|
|
|
|
}).formatToParts(date);
|
|
|
|
|
|
|
|
|
|
const year = parts.find((p) => p.type === 'year')?.value ?? '1970';
|
|
|
|
|
const month = parts.find((p) => p.type === 'month')?.value ?? '01';
|
|
|
|
|
const day = parts.find((p) => p.type === 'day')?.value ?? '01';
|
|
|
|
|
return `${year}-${month}-${day}`;
|
|
|
|
|
}
|