Files
dyolink/backend/src/common/fdi.ts
Amin Mousavi dc10d8dbe3 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-23 23:14:15 +03:30

146 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 (1121, 4131) 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));
}