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>
146 lines
4.2 KiB
TypeScript
146 lines
4.2 KiB
TypeScript
/**
|
||
* FDI tooth geometry — permanent dentition only.
|
||
*
|
||
* Mirrors `frontend/src/components/treatment/fdiToothMeta.ts` and the adjacency rules in
|
||
* `toothSelectionGroups.ts`. Adjacency is defined by position in the arch order, so the
|
||
* midline pairs (11–21, 41–31) are neighbours, exactly as the chart treats them.
|
||
*/
|
||
|
||
import { toLatinDigits } from './digits';
|
||
|
||
export type Arch = 'upper' | 'lower';
|
||
|
||
/** Which side of the *patient*, not of the screen. Quadrant 1 is the patient's upper right. */
|
||
export type PatientSide = 'patient_right' | 'patient_left';
|
||
|
||
/**
|
||
* Upper arch in chart order: patient's RIGHT (18) → midline → patient's LEFT (28) — the drawn
|
||
* layout, which mirrors the patient's own sides. Never read a position off this array by
|
||
* index; use `toFdi()`, which owns the side convention.
|
||
*/
|
||
export const FDI_UPPER_ARCH_ORDER = [
|
||
'18',
|
||
'17',
|
||
'16',
|
||
'15',
|
||
'14',
|
||
'13',
|
||
'12',
|
||
'11',
|
||
'21',
|
||
'22',
|
||
'23',
|
||
'24',
|
||
'25',
|
||
'26',
|
||
'27',
|
||
'28',
|
||
] as const;
|
||
|
||
/** Lower arch, same chart ordering: patient's RIGHT (48) → midline → patient's LEFT (38). */
|
||
export const FDI_LOWER_ARCH_ORDER = [
|
||
'48',
|
||
'47',
|
||
'46',
|
||
'45',
|
||
'44',
|
||
'43',
|
||
'42',
|
||
'41',
|
||
'31',
|
||
'32',
|
||
'33',
|
||
'34',
|
||
'35',
|
||
'36',
|
||
'37',
|
||
'38',
|
||
] as const;
|
||
|
||
export const FDI_TOOTH_IDS: ReadonlySet<string> = new Set<string>([
|
||
...FDI_UPPER_ARCH_ORDER,
|
||
...FDI_LOWER_ARCH_ORDER,
|
||
]);
|
||
|
||
export function isFdiTooth(value: unknown): value is string {
|
||
return typeof value === 'string' && FDI_TOOTH_IDS.has(value);
|
||
}
|
||
|
||
/**
|
||
* Clean up a tooth code the model echoed back. It is reading Persian speech, so it can hand
|
||
* back "۲۶" or "2 6" from digit-by-digit dictation; neither matches literally, and the
|
||
* near-miss does not fail loudly — the tooth just turns into "not understood".
|
||
*/
|
||
export function normalizeFdiCode(value: unknown): string {
|
||
if (typeof value !== 'string') return '';
|
||
return toLatinDigits(value).replace(/\s+/g, '');
|
||
}
|
||
|
||
function archOrder(tooth: string): readonly string[] | null {
|
||
if ((FDI_UPPER_ARCH_ORDER as readonly string[]).includes(tooth))
|
||
return FDI_UPPER_ARCH_ORDER;
|
||
if ((FDI_LOWER_ARCH_ORDER as readonly string[]).includes(tooth))
|
||
return FDI_LOWER_ARCH_ORDER;
|
||
return null;
|
||
}
|
||
|
||
export function sameArch(a: string, b: string): boolean {
|
||
const archA = archOrder(a);
|
||
const archB = archOrder(b);
|
||
return Boolean(archA && archB && archA === archB);
|
||
}
|
||
|
||
export function areArchNeighbors(a: string, b: string): boolean {
|
||
const arch = archOrder(a);
|
||
if (!arch || !sameArch(a, b)) return false;
|
||
return Math.abs(arch.indexOf(a) - arch.indexOf(b)) === 1;
|
||
}
|
||
|
||
/** Inclusive span between two teeth of the same arch, in arch order. Null if not comparable. */
|
||
export function teethBetweenInclusive(a: string, b: string): string[] | null {
|
||
const arch = archOrder(a);
|
||
if (!arch || !sameArch(a, b)) return null;
|
||
const i = arch.indexOf(a);
|
||
const j = arch.indexOf(b);
|
||
if (i < 0 || j < 0) return null;
|
||
const [from, to] = i <= j ? [i, j] : [j, i];
|
||
return [...arch.slice(from, to + 1)];
|
||
}
|
||
|
||
/**
|
||
* Arch + patient side + position (1 = central incisor … 8 = third molar) → FDI code.
|
||
*
|
||
* The single place the patient-right convention lives. Getting it backwards mirrors every
|
||
* quadrant into a valid-looking code for the wrong tooth, which no schema check can catch.
|
||
*/
|
||
export function toFdi(
|
||
arch: Arch,
|
||
side: PatientSide,
|
||
position: number,
|
||
): string | null {
|
||
if (!Number.isInteger(position) || position < 1 || position > 8) return null;
|
||
let quadrant: number;
|
||
if (arch === 'upper') {
|
||
quadrant = side === 'patient_right' ? 1 : 2;
|
||
} else {
|
||
quadrant = side === 'patient_left' ? 3 : 4;
|
||
}
|
||
const code = `${quadrant}${position}`;
|
||
return FDI_TOOTH_IDS.has(code) ? code : null;
|
||
}
|
||
|
||
/**
|
||
* Along the arch, not lexically — a bridge reads 16-15-14, and 11 sits beside 21 across the
|
||
* midline. Teeth from another arch sort to the end, stably.
|
||
*/
|
||
export function sortInArchOrder(teeth: readonly string[]): string[] {
|
||
if (teeth.length === 0) return [];
|
||
const arch = teeth.map((t) => archOrder(t)).find((a) => a !== null) ?? null;
|
||
if (!arch) return [...teeth];
|
||
const indexOf = (tooth: string) => {
|
||
const i = arch.indexOf(tooth);
|
||
return i === -1 ? Number.MAX_SAFE_INTEGER : i;
|
||
};
|
||
return [...teeth].sort((a, b) => indexOf(a) - indexOf(b));
|
||
}
|