Skip to content

[codex] Extract light device session engine#459

Open
elkimek wants to merge 1 commit into
mainfrom
codex/extract-light-device-session-engine
Open

[codex] Extract light device session engine#459
elkimek wants to merge 1 commit into
mainfrom
codex/extract-light-device-session-engine

Conversation

@elkimek
Copy link
Copy Markdown
Owner

@elkimek elkimek commented May 30, 2026

Summary

  • Extract shared light-device session dose math into js/light-device-session-engine.js
  • Route log, stop, and update session recomputation through the same engine
  • Keep UI and persistence in light-devices.js, while centralizing mode resolution, body fraction, distance scaling, spectrum dose, and SAD lux fallback logic
  • Add the new module to the service worker app shell and bump APP_VERSION to 1.8.287

Validation

  • node --check js/light-device-session-engine.js
  • node --check js/light-devices.js
  • node --check tests/test-light-devices.js
  • node tests/test-light-devices.js
  • node tests/test-correctness-phase2.js
  • node tests/test-light-device-ai-analysis.js
  • node tests/test-light-tools.js
  • git diff --check
  • ./run-tests.sh

@vercel
Copy link
Copy Markdown

vercel Bot commented May 30, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
get-based Ready Ready Preview, Comment May 30, 2026 5:27pm

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 30, 2026

Greptile Summary

This PR extracts the repeated dose-math logic (mode resolution, body-area fraction, distance scaling, spectrum synthesis, and SAD-lux fallback) from light-devices.js into a new js/light-device-session-engine.js module, then routes all three call sites — logDeviceSession, stopDeviceSession, and updateDeviceSession — through the shared engine.

  • New engine module exports five pure/near-pure functions (resolveDeviceMode, bodyFractionForDeviceSession, deviceDistanceFactor, computeDeviceSessionDoses) plus the two constant maps (DEVICE_BODY_AREA_FRACTIONS, DEVICE_TYPE_CHANNELS) previously inlined in light-devices.js; runtime globals (window.synthesizeDeviceSpectrum, etc.) are accessed through a _runtimeDeps shim that also accepts an injectable deps map for testing.
  • light-devices.js replaces three near-identical ~40-line dose-computation blocks with single computeDeviceSessionDoses calls, and the old inlined TYPE_CHANNELS / AREA_FRACTIONS literals are removed in favour of the exported constants.
  • Tests in test-light-devices.js add explicit coverage for every exported engine function, including the coupling-validation fallback, the distance-factor cap, and the spectrum vs. lux-only code paths.

Confidence Score: 4/5

Safe to merge — the refactoring faithfully preserves existing dose math across all three session lifecycle functions, and the new engine module is covered by targeted unit tests.

The dose formulas, fallback logic, and mode-resolution paths are all identical to what was inline before. The one notable oddity is a now-redundant mode pre-normalization block in updateDeviceSession (lines 358–360) that mutates sess.mode before calling the engine, which then resolves the same value again and overwrites it — harmless but unnecessary dead logic.

js/light-devices.js — the redundant pre-normalization block at lines 358–360 in updateDeviceSession is worth cleaning up in a follow-on commit.

Important Files Changed

Filename Overview
js/light-device-session-engine.js New module cleanly extracts mode resolution, body-fraction, distance-factor, and dose computation; exports are well-typed and unit-tested.
js/light-devices.js All three dose-computation sites (log, stop, update) correctly delegate to the engine; a redundant mode pre-normalization block in updateDeviceSession is a dead no-op (P2 style).
tests/test-light-devices.js Adds thorough unit tests for all five exported engine functions including the spectrum path, lux-only SAD fallback, coupling validation, and distance-factor capping.
service-worker.js New engine file added to APP_SHELL cache list; no functional issues with placement in the conceptually grouped (non-alphabetical) list.
tests/test-correctness-phase2.js Correctly adds the new module to the PWA app-shell asset manifest check.
version.js Version bump from 1.8.286 to 1.8.287 as expected for a new module addition.

Sequence Diagram

sequenceDiagram
    participant Caller as Caller (UI / timer)
    participant LD as light-devices.js
    participant Engine as light-device-session-engine.js
    participant Win as window.* (synthesize / compute)

    Note over Caller,Win: logDeviceSession
    Caller->>LD: "logDeviceSession({deviceId, durationMin, ...})"
    LD->>Engine: "computeDeviceSessionDoses({device, durationMin, ...})"
    Engine->>Engine: resolveDeviceMode(device, mode)
    Engine->>Engine: bodyFractionForDeviceSession(...)
    Engine->>Engine: deviceDistanceFactor(device, distanceCm)
    Engine->>Win: synthesizeDeviceSpectrum(effectiveDevice)
    Win-->>Engine: baseSpec
    Engine->>Win: "computeChannelDoses({spectrum, ...})"
    Win-->>Engine: doses
    Engine-->>LD: "{doses, mode, bodyExposureFraction, ...}"
    LD-->>Caller: session record

    Note over Caller,Win: stopDeviceSession
    Caller->>LD: stopDeviceSession(id)
    LD->>Engine: "computeDeviceSessionDoses({device, durationMin, ...})"
    Engine->>Win: synthesizeDeviceSpectrum / computeChannelDoses
    Win-->>Engine: doses
    Engine-->>LD: "{doses}"
    LD-->>Caller: finalized session

    Note over Caller,Win: updateDeviceSession (patch triggers recompute)
    Caller->>LD: updateDeviceSession(id, patch)
    LD->>Engine: resolveDeviceMode(device, patch.mode)
    Engine-->>LD: resolvedMode
    LD->>Engine: "computeDeviceSessionDoses({device, ...sess params})"
    Engine->>Win: synthesizeDeviceSpectrum / computeChannelDoses
    Win-->>Engine: doses
    Engine-->>LD: "{doses, mode}"
    LD-->>Caller: updated session
Loading

Reviews (1): Last reviewed commit: "Extract light device session engine" | Re-trigger Greptile

Comment thread js/light-devices.js
Comment on lines 358 to 360
if ((sess.mode === undefined || sess.mode === null) && Array.isArray(device.modes) && device.modes.length > 0) {
const defaultMode = device.modes.find(m => m.default) || device.modes[0];
sess.mode = defaultMode.id;
}
const seconds = sess.durationMin * 60;
const fracByKey = Object.fromEntries((BODY_REGIONS || []).map(r => [r.key, r.fraction]));
const AREA_FRACTIONS = { face: 0.04, arms: 0.10, torso: 0.13, legs: 0.30, 'whole-body': 0.92, targeted: 0.05 };
let area;
if (Array.isArray(sess.bodyAreas) && sess.bodyAreas.length > 0) {
area = sess.bodyAreas.reduce((s, k) => s + (fracByKey[k] || 0), 0) || 0.05;
} else {
area = AREA_FRACTIONS[sess.bodyArea] ?? 0.10;
}
const baseRangeCm = device.recommendedDistanceCm || 15;
const distFactor = Math.min((baseRangeCm / Math.max(sess.distanceCm || 15, 5)) ** 2, 3.0);
const eyeMode = sess.eyesProtected ? 'closed-eyes' : 'direct';
const synthesizeDeviceSpectrum = window.synthesizeDeviceSpectrum;
const computeChannelDoses = window.computeChannelDoses;
const hasPeaks = Array.isArray(device.peakWavelengths) && device.peakWavelengths.length > 0;
const hasIrradiance = (device.mwPerCm2At15cm || 0) > 0;
let doses = {};
if (synthesizeDeviceSpectrum && computeChannelDoses && hasPeaks && hasIrradiance) {
const effectiveDevice = window.effectiveDeviceForMode
? window.effectiveDeviceForMode(device, sess.mode)
: device;
const baseSpec = synthesizeDeviceSpectrum(effectiveDevice);
const spectrum = {
wavelengths: baseSpec.wavelengths,
irradiance: baseSpec.irradiance.map(v => v * distFactor),
};
doses = computeChannelDoses({
spectrum, durationMin: sess.durationMin, bodyExposureFraction: area,
eyeExposure: { mode: eyeMode, durationSec: seconds },
});
} else {
const lux = device.lux || 0;
if (!sess.eyesProtected && lux > 0) doses.circadian = lux * distFactor * seconds / 100;
sess.mode = resolveDeviceMode(device, sess.mode);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Redundant pre-normalization before engine call

The if ((sess.mode === undefined || sess.mode === null)...) block mutates sess.mode to defaultMode.id, but the very next line passes mode: sess.mode to computeDeviceSessionDoses, which calls resolveDeviceMode(device, mode) internally and returns the same defaultMode.id as resolvedMode. sess.mode = resolvedMode then overwrites it again with the identical value. The pre-normalization block is a no-op and could be removed — relying on the engine's returned resolvedMode alone is sufficient (and already handles the legacy null-mode case).

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant