refactor: remove world smuggling — type styles, fragments, topologySubjectIds

registry.ts: Add optional styles and fragments table types to the
  solver's GeometryWorld. Previously these were attached via
  (world as any).styles / .fragments, bypassing the type system.

extraction.ts:
  - remapTopologyIds: accept topologySubjectIds as explicit parameter
    instead of reading (world as any)._topologySubjectIds and deleting
    it post-remap. Eliminates 3 smuggling accesses (read, set, delete)
  - All worldAny.styles / worldAny.fragments / worldAny.strings /
    worldAny.transforms / worldAny.matrices assignments now use
    typed world.xxx access. worldAny variable removed entirely.
  - attachMeasuredChWidths: use world.styles directly.
  - (result.world as any).env mutation replaced with typed
    result.world.env assignment.
  - (sizeWorld as any).styles replaced with typed access.

predicates.ts: Replace all 3 (world as any).styles inline type-assert
  reads (getSubjectFontSizePx, getRootFontSizePx, getSubjectChWidthPx)
  with direct world.styles?.xxx access. No runtime behavior change.

Zero (world as any) casts remain in extraction.ts or predicates.ts.
658 tests pass.
This commit is contained in:
John Dvorak
2026-05-22 14:38:37 -07:00
parent 45b5575e53
commit e78ffe3419
3 changed files with 59 additions and 52 deletions
+22 -36
View File
@@ -342,12 +342,7 @@ export function attachMeasuredChWidths(
selectorToIds: Map<string, number[]>, selectorToIds: Map<string, number[]>,
chWidthsBySelector: Map<string, number[]>, chWidthsBySelector: Map<string, number[]>,
): void { ): void {
const worldAny = world as any const styles = world.styles
const styles = worldAny.styles as {
subjectId?: number[]
fontSize?: number[]
chWidth?: number[]
} | undefined
if (!styles) return if (!styles) return
const bySubject = new Map<number, number>() const bySubject = new Map<number, number>()
for (const [key, ids] of selectorToIds) { for (const [key, ids] of selectorToIds) {
@@ -543,7 +538,6 @@ export async function extractWorldFastGeometry(
}) as FastExtractedPayload }) as FastExtractedPayload
const world = buildGeometryWorld(extracted.elements) as GeometryWorld const world = buildGeometryWorld(extracted.elements) as GeometryWorld
const worldAny = world as any
if (requiredFacts?.fragments) { if (requiredFacts?.fragments) {
const fragmentId: number[] = [] const fragmentId: number[] = []
@@ -581,20 +575,15 @@ export async function extractWorldFastGeometry(
} }
} }
worldAny.fragments = { world.fragments = {
fragmentId,
subjectId, subjectId,
boxId: fragmentId,
fragmentKind, fragmentKind,
boxLeft, firstTextRunId: [],
boxTop, textRunCount: [],
boxRight,
boxBottom,
lineIndex,
flowIndex,
parentFragmentId,
} }
worldAny.subjects.firstFragmentId = firstFragmentIds world.subjects.firstFragmentId = firstFragmentIds
worldAny.subjects.fragmentCount = fragmentCounts world.subjects.fragmentCount = fragmentCounts
} }
if (requiredFacts?.geometry) { if (requiredFacts?.geometry) {
@@ -619,7 +608,7 @@ export async function extractWorldFastGeometry(
originY.push(t.originY) originY.push(t.originY)
} }
worldAny.transforms = { world.transforms = {
transformId, transformId,
subjectId: transformSubjectId, subjectId: transformSubjectId,
matrixStart, matrixStart,
@@ -627,11 +616,11 @@ export async function extractWorldFastGeometry(
originX, originX,
originY, originY,
} }
worldAny.matrices = { values: matrices } world.matrices = { values: matrices }
} }
if (requiredFacts?.styles) { if (requiredFacts?.styles) {
const strings = worldAny.strings?.values ?? [] const strings = world.strings?.values ?? []
const stringToId = new Map<string, number>() const stringToId = new Map<string, number>()
for (let i = 0; i < strings.length; i++) stringToId.set(strings[i], i) for (let i = 0; i < strings.length; i++) stringToId.set(strings[i], i)
const intern = (value: string): number => { const intern = (value: string): number => {
@@ -696,8 +685,8 @@ export async function extractWorldFastGeometry(
chWidth.push(s.chWidth) chWidth.push(s.chWidth)
} }
worldAny.strings = { values: strings } world.strings = { values: strings }
worldAny.styles = { world.styles = {
subjectId: styleSubjectId, subjectId: styleSubjectId,
display, display,
position, position,
@@ -757,7 +746,7 @@ export async function extractWorldFastGeometry(
// CDP Extraction // CDP Extraction
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function remapTopologyIds(world: GeometryWorld): void { function remapTopologyIds(world: GeometryWorld, topologySubjectIds?: number[]): void {
const nSubjects = world.subjects.ids.length const nSubjects = world.subjects.ids.length
if (nSubjects === 0) return if (nSubjects === 0) return
@@ -766,7 +755,7 @@ function remapTopologyIds(world: GeometryWorld): void {
backendToSolver.set(world.subjects.domNodeId[i], world.subjects.ids[i]) backendToSolver.set(world.subjects.domNodeId[i], world.subjects.ids[i])
} }
const rawSubjectIds = (world as any)._topologySubjectIds as number[] | undefined const rawSubjectIds = topologySubjectIds
function reorderAndRemap(rawValues: ArrayLike<number>, targetLength: number): number[] { function reorderAndRemap(rawValues: ArrayLike<number>, targetLength: number): number[] {
if (!rawSubjectIds || rawSubjectIds.length === 0) { if (!rawSubjectIds || rawSubjectIds.length === 0) {
@@ -810,8 +799,6 @@ function remapTopologyIds(world: GeometryWorld): void {
if (world.scroll) { if (world.scroll) {
world.scroll.containerId = remapSimple(world.scroll.containerId) world.scroll.containerId = remapSimple(world.scroll.containerId)
} }
delete (world as any)._topologySubjectIds
} }
export async function extractWorldCdp( export async function extractWorldCdp(
@@ -901,7 +888,8 @@ export async function extractWorldCdp(
const snapshot = cdpResponse.snapshots[0] const snapshot = cdpResponse.snapshots[0]
const canonical = adaptSnapshotToCanonical(snapshot) const canonical = adaptSnapshotToCanonical(snapshot)
const world = adaptCanonicalWorldToSolver(canonical as any) as GeometryWorld const world = adaptCanonicalWorldToSolver(canonical as any) as GeometryWorld
;(world as any).styles = { const topologySubjectIds = (canonical as any).topology?.subjectIds as number[] | undefined
world.styles = {
subjectId: Array.from(canonical.styles.subjectId), subjectId: Array.from(canonical.styles.subjectId),
lineHeight: Array.from(canonical.styles.lineHeight), lineHeight: Array.from(canonical.styles.lineHeight),
fontFamilyStringId: Array.from(canonical.styles.fontFamilyStringId), fontFamilyStringId: Array.from(canonical.styles.fontFamilyStringId),
@@ -909,9 +897,7 @@ export async function extractWorldCdp(
fontWeight: Array.from(canonical.styles.fontWeight), fontWeight: Array.from(canonical.styles.fontWeight),
} }
;(world as any)._topologySubjectIds = (canonical as any).topology?.subjectIds remapTopologyIds(world, topologySubjectIds)
remapTopologyIds(world)
const selectorToIds = new Map<string, number[]>() const selectorToIds = new Map<string, number[]>()
for (const [selectorKey, nodeIds] of selectorToNodeIds) { for (const [selectorKey, nodeIds] of selectorToNodeIds) {
@@ -1010,10 +996,10 @@ export async function extractWorld(
? await extractWorldFastGeometry(playwrightPage, filteredSelectors, requiredFacts) ? await extractWorldFastGeometry(playwrightPage, filteredSelectors, requiredFacts)
: await extractWorldCdp(playwrightPage, filteredSelectors, requiredFacts) : await extractWorldCdp(playwrightPage, filteredSelectors, requiredFacts)
;(result.world as any).env = { result.world.env = {
...(result.world as any).env, ...result.world.env,
viewportWidth: env.viewportWidth, viewportWidth: env.viewportWidth as number,
viewportHeight: env.viewportHeight, viewportHeight: env.viewportHeight as number,
} }
if (requiredFacts?.styles) { if (requiredFacts?.styles) {
@@ -1959,7 +1945,7 @@ export function buildCompatibilityReport(ui: ImhotepUi): CompatibilityReport {
viewportWidth: 1280, viewportWidth: 1280,
viewportHeight: 720, viewportHeight: 720,
} }
;(sizeWorld as any).styles = { sizeWorld.styles = {
subjectId: [1], subjectId: [1],
fontSize: [16], fontSize: [16],
chWidth: [9], chWidth: [9],
+10 -16
View File
@@ -314,8 +314,7 @@ function parseLengthOption(raw: unknown): { value: number; unit: string } | null
} }
function getSubjectFontSizePx(world: GeometryWorld, subjectId: number): number { function getSubjectFontSizePx(world: GeometryWorld, subjectId: number): number {
const styles = (world as any).styles as { fontSize?: ArrayLike<number> } | undefined; const fontSize = world.styles?.fontSize;
const fontSize = styles?.fontSize;
if (!fontSize) return 16; if (!fontSize) return 16;
const ids = world.subjects?.ids; const ids = world.subjects?.ids;
if (!ids) return 16; if (!ids) return 16;
@@ -326,28 +325,23 @@ function getSubjectFontSizePx(world: GeometryWorld, subjectId: number): number {
} }
function getRootFontSizePx(world: GeometryWorld): number { function getRootFontSizePx(world: GeometryWorld): number {
const styles = (world as any).styles as { fontSize?: ArrayLike<number> } | undefined; const fontSize = world.styles?.fontSize;
const fontSize = styles?.fontSize;
if (!fontSize || fontSize.length === 0) return 16; if (!fontSize || fontSize.length === 0) return 16;
const fs = Number(fontSize[0] ?? 16); const fs = Number(fontSize[0] ?? 16);
return Number.isFinite(fs) && fs > 0 ? fs : 16; return Number.isFinite(fs) && fs > 0 ? fs : 16;
} }
function getSubjectChWidthPx(world: GeometryWorld, subjectId: number): number { function getSubjectChWidthPx(world: GeometryWorld, subjectId: number): number {
const styles = (world as any).styles as { const subjectIdArr = world.styles?.subjectId;
subjectId?: ArrayLike<number> const chWidthArr = world.styles?.chWidth;
chWidth?: ArrayLike<number> if (!chWidthArr || chWidthArr.length === 0) {
} | undefined;
const chWidth = styles?.chWidth;
if (!chWidth || chWidth.length === 0) {
return getSubjectFontSizePx(world, subjectId) * 0.5; return getSubjectFontSizePx(world, subjectId) * 0.5;
} }
const styleSubjectIds = styles?.subjectId; if (subjectIdArr && subjectIdArr.length > 0) {
if (styleSubjectIds && styleSubjectIds.length > 0) { for (let i = 0; i < subjectIdArr.length; i++) {
for (let i = 0; i < styleSubjectIds.length; i++) { if (Number(subjectIdArr[i]) !== subjectId) continue;
if (Number(styleSubjectIds[i]) !== subjectId) continue; const w = Number(chWidthArr[i]);
const w = Number(chWidth[i]);
if (Number.isFinite(w) && w > 0) return w; if (Number.isFinite(w) && w > 0) return w;
break; break;
} }
@@ -357,7 +351,7 @@ function getSubjectChWidthPx(world: GeometryWorld, subjectId: number): number {
if (ids) { if (ids) {
const idx = ids.indexOf(subjectId); const idx = ids.indexOf(subjectId);
if (idx >= 0) { if (idx >= 0) {
const w = Number(chWidth[idx]); const w = Number(chWidthArr[idx]);
if (Number.isFinite(w) && w > 0) return w; if (Number.isFinite(w) && w > 0) return w;
} }
} }
+27
View File
@@ -139,6 +139,33 @@ export interface GeometryWorld {
visibleArea: number[]; visibleArea: number[];
clippedArea: number[]; clippedArea: number[];
}; };
/** Computed style data attached by extraction (optional, populated when requiredFacts.styles=true). */
styles?: {
subjectId: number[];
display?: number[];
position?: number[];
zIndexKind?: number[];
zIndexValue?: number[];
overflowX?: number[];
overflowY?: number[];
opacity?: number[];
visibility?: number[];
containFlags?: number[];
pointerEvents?: number[];
lineHeight?: number[];
fontFamilyStringId?: number[];
fontSize?: number[];
fontWeight?: number[];
chWidth?: number[];
};
/** Fragment data attached by extraction (optional, populated when requiredFacts.fragments=true). */
fragments?: {
subjectId: number[];
boxId: number[];
fragmentKind: number[];
firstTextRunId: number[];
textRunCount: number[];
};
} }
// --- Clause descriptor ------------------------------------------------------- // --- Clause descriptor -------------------------------------------------------