diff --git a/docs/neurologicalLevelOfInjury-architecture.md b/docs/neurologicalLevelOfInjury-architecture.md
new file mode 100644
index 0000000..dd2e479
--- /dev/null
+++ b/docs/neurologicalLevelOfInjury-architecture.md
@@ -0,0 +1,637 @@
+# Neurological Level of Injury (NLI) Step-Based Architecture
+
+**Author:** ISNCSCI Architect Agent
+**Date:** 2026-02-25
+**Status:** Architecture proposal for refactor
+
+---
+
+## 1. High-Level Overview
+
+### What the module computes
+
+The **Neurological Level of Injury (NLI)** identifies the most caudal segment with normal sensory and motor function on both sides. It answers: _"What is the lowest level where sensation and motor function are both intact bilaterally?"_
+
+The algorithm iterates through all SensoryLevels from C1 toward S4_5. At each level, it evaluates:
+
+1. **Bilateral sensory function** (left and right sides) using light touch and pin prick
+2. **Bilateral motor function** (when applicable) for motor regions:
+ - **C4-T1**: Cervical motor region
+ - **L1-S1**: Lumbar motor region
+
+For each level, the algorithm determines if the level qualifies as an NLI based on intact function bilaterally. The `variable` flag accumulates across all checks (left sensory, right sensory, left motor, right motor). When iteration completes at S4_5 without stopping early, the result is `INT` (intact).
+
+### Role in the ISNCSCI algorithm
+
+- NLI is a **single value** (not separate for left/right) representing the most caudal level with bilateral intact function.
+- It is the **primary measure** reported on the ISNCSCI worksheet.
+- NLI feeds into AIS classification and helps clinicians understand the extent of injury.
+- Multiple levels can be reported (comma-separated) when they meet the criteria simultaneously.
+- The `variable` flag (indicated by `*`) tracks whether any check had variable or non-testable values.
+
+### Key inputs and final outputs
+
+| Input | Type | Description |
+| ----- | ------ | ----------------------------------------------- |
+| `exam` | `Exam` | Full exam data (left and right sides, all values) |
+
+| Output | Type | Description |
+| ------ | -------- | --------------------------------------------------------------------- |
+| NLI | `string` | Comma-separated levels (e.g. `"C5"`, `"T3*"`, `"S3,INT"`, `"INT*"`) |
+
+---
+
+## 2. List of Steps (in order)
+
+1. **initializeNLIIteration** – Set up state: levels list, variable flag, current index; start at C1.
+2. **checkLevel** – For current level, evaluate bilateral sensory and motor function (when applicable); add level if criteria met; update variable; continue or stop.
+3. **addIntactAndComplete** – When iteration reaches S4_5 without stopping, add `INT` or `INT*`; final step.
+
+---
+
+## 3. Step Definitions
+
+### Step 1: initializeNLIIteration
+
+| Field | Description |
+| --------------- | ------------------------------------------------------------------------------------------ |
+| **name** | `initializeNLIIteration` |
+| **purpose** | Initialize state for NLI calculation: empty levels list, variable=false, currentIndex=0. |
+| **inputs** | `state.exam` |
+| **outputs** | `state.listOfNLI`, `state.variable`, `state.currentIndex` |
+| **explanation** | "Initialize Neurological Level of Injury calculation. Iterate from C1 toward S4_5." |
+
+**Logic:**
+
+- `listOfNLI = []`
+- `variable = false`
+- `currentIndex = 0` (first level is C1)
+- `next = checkLevel`
+
+---
+
+### Step 2: checkLevel
+
+| Field | Description |
+| --------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
+| **name** | `checkLevel` |
+| **purpose** | For the current level, evaluate bilateral sensory and motor function (when applicable); add level if criteria met; continue or stop. |
+| **inputs** | `state.exam`, `state.listOfNLI`, `state.variable`, `state.currentIndex` |
+| **outputs** | `state.listOfNLI`, `state.variable`, `state.currentIndex`, `state.next` |
+| **explanation** | "Check neurological level at {{levelName}} (bilateral sensory and motor function)." |
+
+**Logic:**
+
+- `level = SensoryLevels[currentIndex]`
+- `nextLevel = SensoryLevels[currentIndex + 1]`
+
+**Dispatch by level category:**
+
+| Condition | Check sequence | Notes |
+| ------------------------ | ---------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
+| **C1–C3, T2–T12, S2–S3** | 1. Check left sensory
2. Check right sensory
3. Combine results via `checkLevelWithoutMotor` | Sensory-only regions; no key muscles. |
+| **C4-T1, L1-S1** | 1. Check left sensory
2. Check right sensory
3. Combine via `checkLevelWithoutMotor`
4. Check motors via `checkLevelWithMotor` | Motor regions; includes bilateral motor evaluation. |
+| **S4_5** | Add `'INT' + (variable ? '*' : '')` to listOfNLI | End of iteration; no next level. |
+
+**Check functions used:**
+
+1. **checkSensoryLevel**(side, level, nextLevel, variable) → CheckLevelResult
+ - Used for bilateral sensory checks (left and right)
+ - Returns `{ continue, level?, variable }`
+
+2. **checkLevelWithoutMotor**(level, leftSensoryResult, rightSensoryResult, variable) → CheckLevelResult
+ - Combines left and right sensory results for sensory-only regions
+ - Returns combined result with bilateral logic
+
+3. **checkMotorLevel**(side, motorLevel, nextMotorLevel, variable) → CheckLevelResult
+ - Used for key motor regions (C5-C8, L2-L5)
+ - Returns `{ continue, level?, variable }`
+
+4. **checkMotorLevelBeforeStartOfKeyMuscles**(side, level, nextLevel, variable) → CheckLevelResult
+ - Used for C4 and L1 (before key muscles begin)
+ - Evaluates next motor level (C5 or L2)
+
+5. **checkLevelWithMotor**(exam, level, sensoryResult, variable) → CheckLevelResult
+ - Combines sensory and bilateral motor results for motor regions
+ - Handles C4, T1, S1 special cases
+ - Returns final result for the level
+
+**Bilateral sensory check logic (checkLevelWithoutMotor):**
+
+```typescript
+// Evaluate left and right sensory
+leftSensoryResult = checkSensoryLevel(exam.left, level, nextLevel, variable)
+rightSensoryResult = checkSensoryLevel(exam.right, level, nextLevel, variable)
+
+// Combine results
+if (leftSensoryResult.level || rightSensoryResult.level) {
+ if (
+ leftSensoryResult.level && rightSensoryResult.level &&
+ leftSensoryResult.level.includes('*') && rightSensoryResult.level.includes('*')
+ ) {
+ resultLevel = level + '*'
+ } else {
+ resultLevel = level + (variable ? '*' : '')
+ }
+}
+
+return {
+ continue: leftSensoryResult.continue && rightSensoryResult.continue,
+ level: resultLevel,
+ variable: variable || leftSensoryResult.variable || rightSensoryResult.variable
+}
+```
+
+**Bilateral motor check logic (checkLevelWithMotor):**
+
+For motor regions (C4-T1 and L1-S1), after combining bilateral sensory results, the algorithm evaluates bilateral motor function:
+
+```typescript
+// Map SensoryLevel index to MotorLevel index
+const i = SensoryLevels.indexOf(level)
+const index = i - (levelIsBetween(i,'C4','T1') ? 4 : 16)
+const motorLevel = MotorLevels[index]
+const nextMotorLevel = MotorLevels[index + 1]
+
+// Determine check function based on level
+if (level === 'C4' || level === 'L1') {
+ // Before key muscles: use next motor level
+ leftMotorResult = checkMotorLevelBeforeStartOfKeyMuscles(exam.left, level, nextMotorLevel, variable)
+ rightMotorResult = checkMotorLevelBeforeStartOfKeyMuscles(exam.right, level, nextMotorLevel, variable)
+} else if (level === 'T1' || level === 'S1') {
+ // End of key muscles: use current motor level only
+ leftMotorResult = checkMotorLevel(exam.left, motorLevel, motorLevel, variable)
+ rightMotorResult = checkMotorLevel(exam.right, motorLevel, motorLevel, variable)
+} else {
+ // Key motor region: use current and next motor levels
+ leftMotorResult = checkMotorLevel(exam.left, motorLevel, nextMotorLevel, variable)
+ rightMotorResult = checkMotorLevel(exam.right, motorLevel, nextMotorLevel, variable)
+}
+
+// Combine motor and sensory results
+if (leftMotorResult.level || rightMotorResult.level || sensoryResult.level) {
+ if (
+ leftMotorResult.level && rightMotorResult.level &&
+ (leftMotorResult.level.includes('*') || rightMotorResult.level.includes('*'))
+ ) {
+ resultLevel = level + '*'
+ } else {
+ resultLevel = level + (variable ? '*' : '')
+ }
+}
+
+// Return result (if sensory stopped, propagate that; otherwise use motor continue)
+return !sensoryResult.continue
+ ? { ...sensoryResult, level: resultLevel }
+ : {
+ continue: leftMotorResult.continue && rightMotorResult.continue,
+ level: resultLevel,
+ variable: variable || sensoryResult.variable || leftMotorResult.variable || rightMotorResult.variable
+ }
+```
+
+**After any check:**
+
+- `variable = variable || result.variable`
+- If `result.level` → push `result.level` to listOfNLI
+- If `result.continue`:
+ - `currentIndex++`
+ - `next = checkLevel`
+- Else:
+ - `next = null` (stop)
+
+**When reaching S4_5 (no nextLevel):**
+
+- Push `'INT' + (variable ? '*' : '')` to listOfNLI
+- `next = null` (stop)
+
+---
+
+### Step 3: addIntactAndComplete
+
+| Field | Description |
+| --------------- | -------------------------------------------------------------------------- |
+| **name** | `addIntactAndComplete` |
+| **purpose** | Add INT (or INT\*) when iteration completes at S4_5; final step. |
+| **inputs** | `state.listOfNLI`, `state.variable` |
+| **outputs** | `state.listOfNLI` |
+| **explanation** | "Reached S4_5. All levels have intact bilateral function. Add INT to NLI." |
+
+**Note:** This step is reached when `checkLevel` detects `nextLevel === undefined` at S4_5. The logic is embedded in `checkLevel`; the step name documents the action. This mirrors the pattern used in sensoryLevel and motorLevel.
+
+---
+
+## 4. Check Functions (Preserved)
+
+The following check functions are imported and reused without modification:
+
+### checkSensoryLevel
+
+`checkSensoryLevel(side, level, nextLevel, variable)` from `neurologicalLevels/sensoryLevel.ts`
+
+- Returns `CheckLevelResult`
+- Used for bilateral sensory evaluation
+
+### checkMotorLevel
+
+`checkMotorLevel(side, motorLevel, nextMotorLevel, variable)` from `neurologicalLevels/motorLevel.ts`
+
+- Returns `CheckLevelResult`
+- Used for key motor regions (C5-C8, L2-L5)
+- Throws if current level motor is ['0','1','2']
+
+### checkMotorLevelBeforeStartOfKeyMuscles
+
+`checkMotorLevelBeforeStartOfKeyMuscles(side, level, nextLevel, variable)` from `neurologicalLevels/motorLevel.ts`
+
+- Used for C4 and L1 (before key muscles)
+- Evaluates next motor level (C5 or L2)
+
+### checkLevelWithoutMotor
+
+`checkLevelWithoutMotor(level, leftSensoryResult, rightSensoryResult, variable)`
+
+- Combines bilateral sensory results
+- Returns combined `CheckLevelResult`
+
+**Logic:**
+
+| Condition | Result |
+| ----------------------------------------------------------- | ------------------------------------------------- |
+| Neither left nor right has level | `{ continue: left.continue && right.continue, variable: variable \|\| left.variable \|\| right.variable }` |
+| Both left and right have level with `*` | `{ continue: left.continue && right.continue, level: level + '*', variable }` |
+| At least one side has level | `{ continue: left.continue && right.continue, level: level + (variable ? '*' : ''), variable }` |
+
+### checkLevelWithMotor
+
+`checkLevelWithMotor(exam, level, sensoryResult, variable)`
+
+- Combines bilateral sensory and bilateral motor results for motor regions
+- Handles special cases:
+ - **C4, L1**: Uses `checkMotorLevelBeforeStartOfKeyMuscles` for both sides
+ - **C5-C8, L2-L5**: Uses `checkMotorLevel` with current and next motor levels
+ - **T1, S1**: Uses `checkMotorLevel` with current motor level only (special case: end of key muscles)
+
+**Logic:**
+
+1. Map SensoryLevel index to MotorLevel index
+2. Evaluate left and right motor function based on level type
+3. Combine motor results with sensory result:
+ - If either motor side has level with `*` → add `*` to result level
+ - If sensory already stopped (`!sensoryResult.continue`) → propagate sensory result
+ - Otherwise combine motor continue flags
+
+---
+
+## 5. State Type Definition
+
+```typescript
+export type NeurologicalLevelOfInjuryState = {
+ exam: Exam;
+ listOfNLI: string[];
+ variable: boolean;
+ currentIndex: number;
+};
+```
+
+---
+
+## 6. Step Handler Signatures and Chain Flow
+
+```typescript
+export type NeurologicalLevelOfInjuryStepHandler = StepHandler;
+export type NeurologicalLevelOfInjuryStep = Step;
+
+// Step 1: Entry point
+function initializeNLIIteration(
+ state: NeurologicalLevelOfInjuryState,
+): NeurologicalLevelOfInjuryStep;
+
+// Step 2: Iteration (may chain to itself or stop)
+function checkLevel(
+ state: NeurologicalLevelOfInjuryState,
+): NeurologicalLevelOfInjuryStep;
+```
+
+**Chain flow:**
+
+```
+initializeNLIIteration
+ → checkLevel (currentIndex=0)
+ → checkLevel (currentIndex=1) | ... | null
+```
+
+The `checkLevel` step either:
+
+- Sets `next = checkLevel` and increments `currentIndex` when `result.continue` is true, or
+- Sets `next = null` when `result.continue` is false or when `nextLevel` is undefined (reached S4_5).
+
+---
+
+## 7. Main Entry and Generator
+
+```typescript
+export function determineNeurologicalLevelOfInjury(exam: Exam): string {
+ const initialState = getInitialState(exam);
+ let step = initializeNLIIteration(initialState);
+ while (step.next) {
+ step = step.next(step.state);
+ }
+ return step.state.listOfNLI.join(',');
+}
+
+export function* neurologicalLevelOfInjurySteps(
+ exam: Exam,
+): Generator {
+ const initialState = getInitialState(exam);
+ let step = initializeNLIIteration(initialState);
+ yield step;
+ while (step.next) {
+ step = step.next(step.state);
+ yield step;
+ }
+}
+```
+
+**Initial state factory:**
+
+```typescript
+export function getInitialState(exam: Exam): NeurologicalLevelOfInjuryState {
+ return {
+ exam,
+ listOfNLI: [],
+ variable: false,
+ currentIndex: 0,
+ };
+}
+```
+
+---
+
+## 8. Proposed Folder/File Structure
+
+```
+src/classification/neurologicalLevelOfInjury/
+├── neurologicalLevelOfInjury.ts # Main entry, step chain
+├── neurologicalLevelOfInjuryErrors.ts # Error types and messages (optional)
+├── neurologicalLevelOfInjury.spec.ts # Tests
+└── neurologicalLevelOfInjurySupport.ts # checkLevelWithoutMotor, checkLevelWithMotor (optional extraction)
+```
+
+### Shared vs module-specific
+
+| Item | Location | Rationale |
+| ----------------------------------------- | ------------------------------------ | ---------------------------------------------------- |
+| `Step` type | `common/step.ts` | Reusable |
+| `StepHandler` | `common/step.ts` | Generic handler |
+| `createStep` | `common/step.ts` | Shared helper |
+| `CheckLevelResult` | `common.ts` | Already shared |
+| `checkSensoryLevel` | `neurologicalLevels/sensoryLevel.ts` | Imported for bilateral sensory checks |
+| `checkMotorLevel` | `neurologicalLevels/motorLevel.ts` | Imported for bilateral motor checks |
+| `checkMotorLevelBeforeStartOfKeyMuscles` | `neurologicalLevels/motorLevel.ts` | Imported for C4 and L1 |
+| `checkLevelWithoutMotor` | `neurologicalLevelOfInjury.ts` | NLI-specific; combines bilateral sensory |
+| `checkLevelWithMotor` | `neurologicalLevelOfInjury.ts` | NLI-specific; combines sensory and bilateral motor |
+| `levelIsBetween` | `common.ts` | Helper for dispatch logic |
+| `NeurologicalLevelOfInjuryState` | `neurologicalLevelOfInjury.ts` | NLI-specific state |
+
+---
+
+## 9. Proposed Translation Keys (to add to `en.ts`)
+
+```typescript
+// Neurological Level of Injury
+neurologicalLevelOfInjuryInitializeNLIIterationDescription:
+ 'Initialize Neurological Level of Injury calculation. Iterate from C1 toward S4_5.',
+neurologicalLevelOfInjuryInitializeNLIIterationAction:
+ 'Set levels list to empty, variable to false.',
+
+neurologicalLevelOfInjuryCheckLevelDescription:
+ 'Check neurological level at {{levelName}}.',
+neurologicalLevelOfInjuryCheckLevelSensoryOnlyAction:
+ 'Sensory-only region. Evaluate bilateral sensory function (LT and PP on both sides).',
+neurologicalLevelOfInjuryCheckLevelMotorRegionAction:
+ 'Motor region. Evaluate bilateral sensory and motor function.',
+neurologicalLevelOfInjuryCheckLevelLeftSensoryAction:
+ 'Left sensory: LT={{leftLightTouch}}, PP={{leftPinPrick}}.',
+neurologicalLevelOfInjuryCheckLevelRightSensoryAction:
+ 'Right sensory: LT={{rightLightTouch}}, PP={{rightPinPrick}}.',
+neurologicalLevelOfInjuryCheckLevelLeftMotorAction:
+ 'Left motor at {{motorLevel}}: {{leftMotor}}.',
+neurologicalLevelOfInjuryCheckLevelRightMotorAction:
+ 'Right motor at {{motorLevel}}: {{rightMotor}}.',
+neurologicalLevelOfInjuryCheckLevelAddLevelAction:
+ 'Add {{levelName}} to NLI.',
+neurologicalLevelOfInjuryCheckLevelContinueAction:
+ 'Bilateral function intact. Continue to next level.',
+neurologicalLevelOfInjuryCheckLevelStopAction:
+ 'Bilateral function not intact. Stop iteration.',
+neurologicalLevelOfInjuryCheckLevelReachedS4_5Action:
+ 'Reached S4_5. All levels have intact bilateral function. Add INT.',
+```
+
+---
+
+## 10. Error Handling
+
+The current implementation may throw in the following cases (from check functions):
+
+1. **checkMotorLevel**: Current level motor is ['0','1','2'] → "Invalid motor value at current level"
+2. **checkSensoryLevel**: `nextLevel === 'C1'` → invalid arguments
+
+Proposed error module (optional, for consistency):
+
+```typescript
+// neurologicalLevelOfInjuryErrors.ts
+export const NEUROLOGICAL_LEVEL_OF_INJURY_ERROR_MESSAGES = {
+ INVALID_MOTOR_VALUE: 'Invalid motor value at current level',
+ INVALID_NEXT_LEVEL: 'checkSensoryLevel: invalid arguments level: {{level}} nextLevel: {{nextLevel}}',
+ CURRENT_LEVEL_REQUIRED: 'checkLevel :: state.currentIndex out of bounds.',
+} as const;
+
+export class NeurologicalLevelOfInjuryError extends Error {
+ constructor(
+ public code: keyof typeof NEUROLOGICAL_LEVEL_OF_INJURY_ERROR_MESSAGES,
+ message?: string,
+ ) {
+ super(message ?? NEUROLOGICAL_LEVEL_OF_INJURY_ERROR_MESSAGES[code]);
+ this.name = 'NeurologicalLevelOfInjuryError';
+ }
+}
+```
+
+---
+
+## 11. Algorithm Fidelity
+
+This architecture preserves the existing `determineNeurologicalLevelOfInjury` behavior:
+
+- **Same iteration order** (C1 → C2 → … → S4_5).
+- **Same dispatch logic**: sensory-only regions vs. motor regions.
+- **Same bilateral evaluation**:
+ - Left and right sensory checks via `checkSensoryLevel`
+ - Combined via `checkLevelWithoutMotor` for sensory-only regions
+ - Left and right motor checks (when applicable) via `checkMotorLevel` or `checkMotorLevelBeforeStartOfKeyMuscles`
+ - Combined via `checkLevelWithMotor` for motor regions
+- **Same variable accumulation**: `variable = variable || result.variable` across all checks.
+- **Same level addition logic**: Add level when `result.level` is present.
+- **Same continuation logic**: Continue when `result.continue` is true; stop when false.
+- **Same end-of-series handling**: Add `INT` or `INT*` when reaching S4_5.
+- **Same output format**: comma-separated string of levels.
+
+**Key bilateral logic preserved:**
+
+1. **Both sides with `*` indicator** → Add level with `*`
+2. **At least one side qualifies** → Add level (with `*` if variable flag is set)
+3. **Continue only if both sides continue** → `leftResult.continue && rightResult.continue`
+4. **Accumulate variable from all checks** → sensory left, sensory right, motor left, motor right
+
+**Motor region mapping preserved:**
+
+- **C4-T1**: `index = i - 4` (maps SensoryLevel index to MotorLevel index)
+- **L1-S1**: `index = i - 16` (maps SensoryLevel index to MotorLevel index)
+- **Special cases**: C4, L1 (before key muscles), T1, S1 (end of key muscles)
+
+No algorithm behavior is modified; only the control flow is expressed as an explicit step chain.
+
+---
+
+## 12. Implementation Notes
+
+### Step 2 detailed implementation guidance
+
+The `checkLevel` step handler will contain substantial branching logic. To maintain clarity:
+
+1. **Determine level category** first (sensory-only vs. motor region)
+2. **Perform bilateral sensory checks** for all levels
+3. **Perform bilateral motor checks** for motor regions only
+4. **Combine results** using the bilateral combination functions
+5. **Update state** (variable, listOfNLI, currentIndex)
+6. **Determine next step** (continue or stop)
+
+**Recommended structure:**
+
+```typescript
+function checkLevel(state: NeurologicalLevelOfInjuryState): NeurologicalLevelOfInjuryStep {
+ const level = SensoryLevels[state.currentIndex];
+ const nextLevel = SensoryLevels[state.currentIndex + 1];
+ const i = state.currentIndex;
+
+ // Handle S4_5 (end of iteration)
+ if (!nextLevel) {
+ return createStep(
+ { key: 'neurologicalLevelOfInjuryCheckLevelDescription', params: { levelName: level } },
+ [{ key: 'neurologicalLevelOfInjuryCheckLevelReachedS4_5Action' }],
+ state,
+ {
+ listOfNLI: [...state.listOfNLI, 'INT' + (state.variable ? '*' : '')],
+ },
+ null,
+ );
+ }
+
+ // Bilateral sensory checks
+ const leftSensoryResult = checkSensoryLevel(state.exam.left, level, nextLevel, state.variable);
+ const rightSensoryResult = checkSensoryLevel(state.exam.right, level, nextLevel, state.variable);
+
+ let result: CheckLevelResult;
+ let checkType: 'sensory' | 'motor';
+
+ // Determine if this is a motor region
+ if (levelIsBetween(i, 'C4', 'T1') || levelIsBetween(i, 'L1', 'S1')) {
+ checkType = 'motor';
+ const sensoryResult = checkLevelWithoutMotor(level, leftSensoryResult, rightSensoryResult, state.variable);
+ result = checkLevelWithMotor(state.exam, level, sensoryResult, state.variable);
+ } else {
+ checkType = 'sensory';
+ result = checkLevelWithoutMotor(level, leftSensoryResult, rightSensoryResult, state.variable);
+ }
+
+ // Update variable and levels
+ const variable = state.variable || result.variable;
+ const newLevels = result.level
+ ? [...state.listOfNLI, result.level]
+ : [...state.listOfNLI];
+
+ // Build description and actions
+ const description = {
+ key: 'neurologicalLevelOfInjuryCheckLevelDescription' as const,
+ params: { levelName: level },
+ };
+
+ const actions = [];
+ if (checkType === 'sensory') {
+ actions.push({ key: 'neurologicalLevelOfInjuryCheckLevelSensoryOnlyAction' as const });
+ } else {
+ actions.push({ key: 'neurologicalLevelOfInjuryCheckLevelMotorRegionAction' as const });
+ }
+
+ if (result.level) {
+ actions.push({
+ key: 'neurologicalLevelOfInjuryCheckLevelAddLevelAction' as const,
+ params: { levelName: result.level },
+ });
+ }
+
+ if (result.continue) {
+ actions.push({ key: 'neurologicalLevelOfInjuryCheckLevelContinueAction' as const });
+ } else {
+ actions.push({ key: 'neurologicalLevelOfInjuryCheckLevelStopAction' as const });
+ }
+
+ // Determine next step
+ const next = result.continue ? checkLevel : null;
+
+ return createStep(
+ description,
+ actions,
+ state,
+ {
+ listOfNLI: newLevels,
+ variable,
+ currentIndex: result.continue ? state.currentIndex + 1 : state.currentIndex,
+ },
+ next,
+ );
+}
+```
+
+### Clinician-friendly descriptions
+
+For the step-based UI, each step should provide clear, clinician-friendly descriptions:
+
+- **At sensory-only levels**: "Evaluating bilateral sensory function at C2 (no motor region)"
+- **At motor levels**: "Evaluating bilateral sensory and motor function at C5"
+- **When adding a level**: "Level C5 added to NLI (bilateral intact function)"
+- **When continuing**: "Bilateral function intact at C5. Continue to C6."
+- **When stopping**: "Bilateral function not intact at C6. Stop iteration. NLI: C5"
+- **At S4_5**: "Reached S4_5. All levels have intact bilateral function. NLI: INT"
+
+### Testing strategy
+
+The refactored module must pass all existing tests without modification. Key test scenarios:
+
+1. **Sensory-only regions**: Verify bilateral sensory checks (C1-C3, T2-T12, S2-S3)
+2. **Motor regions**: Verify bilateral sensory and motor checks (C4-T1, L1-S1)
+3. **Variable flag accumulation**: Verify `*` propagates from any bilateral check
+4. **Special motor cases**: C4, L1 (before key muscles), T1, S1 (end of key muscles)
+5. **Multiple levels**: Verify comma-separated output when multiple levels qualify
+6. **INT handling**: Verify `INT` added when reaching S4_5
+7. **Early termination**: Verify stop when `continue === false`
+
+---
+
+## 13. Integration with Existing Modules
+
+The refactored neurologicalLevelOfInjury module will:
+
+1. **Import** and reuse `checkSensoryLevel` from `neurologicalLevels/sensoryLevel.ts`
+2. **Import** and reuse `checkMotorLevel` and `checkMotorLevelBeforeStartOfKeyMuscles` from `neurologicalLevels/motorLevel.ts`
+3. **Preserve** the existing `checkLevelWithoutMotor` and `checkLevelWithMotor` functions (may be extracted to support file)
+4. **Use** the shared `Step`, `StepHandler`, and `createStep` from `common/step.ts`
+5. **Use** the shared `CheckLevelResult` and `levelIsBetween` from `common.ts`
+
+**No changes required** to the imported check functions; they are used as-is.
+
+---
+
+## End of Document
diff --git a/src/classification/neurologicalLevelOfInjury/README.md b/src/classification/neurologicalLevelOfInjury/README.md
new file mode 100644
index 0000000..4bb9616
--- /dev/null
+++ b/src/classification/neurologicalLevelOfInjury/README.md
@@ -0,0 +1,677 @@
+# Neurological Level of Injury (NLI)
+
+This module computes the **Neurological Level of Injury (NLI)** for the International Standards for Neurological Classification of Spinal Cord Injury (ISNCSCI). The algorithm uses a step-based, chain-of-command pattern so clinicians can follow the logic and see how the NLI is determined.
+
+---
+
+## Overview
+
+### What is the Neurological Level of Injury?
+
+The **Neurological Level of Injury (NLI)** identifies the most caudal segment where sensory and motor function are both intact bilaterally. It answers: _"What is the lowest level where sensation and motor function are both normal on both sides?"_
+
+| Key Feature | Description |
+| ------------------------ | ------------------------------------------------------------------------------- |
+| **Bilateral evaluation** | Both left AND right sides must have intact function |
+| **Sensory + Motor** | Both sensory (light touch and pin prick) and motor (key muscles) must be intact |
+| **Single value** | NLI is reported as a single value, not separately for left/right |
+| **Primary measure** | The main measure reported on the ISNCSCI worksheet |
+
+### Why is NLI important?
+
+NLI is the **most widely recognized measure** of spinal cord injury severity:
+
+- Defines the extent of neurological impairment
+- Guides rehabilitation planning and prognosis
+- Feeds into AIS classification
+- Tracks recovery over time
+- Standardizes communication between clinicians
+
+### Single value vs bilateral reporting
+
+Unlike motor levels and sensory levels (which are reported separately for left and right sides), **NLI is a single value** representing the lowest level where **both sides** have intact function. This bilateral requirement makes NLI a conservative measure: a deficit on either side prevents that level from qualifying as the NLI.
+
+---
+
+## Prerequisites
+
+### Input Requirements
+
+| Input | Type | Description |
+| ------------ | ------ | ------------------------------------------------ |
+| `exam` | `Exam` | Complete exam data with left and right sides |
+| `exam.left` | `Side` | Left side: light touch, pin prick, motor grades |
+| `exam.right` | `Side` | Right side: light touch, pin prick, motor grades |
+
+### What data is needed?
+
+**For sensory evaluation:**
+
+- **Light touch (LT)** values at all key sensory points (C2-S4_5) on both sides
+- **Pin prick (PP)** values at all key sensory points (C2-S4_5) on both sides
+- Values: `0` (absent), `1` (impaired), `2` (normal), `NT` (not testable), `0*`, `1*`, `2*` (with variable indicator)
+
+**For motor evaluation (at C4-T1 and L1-S1):**
+
+- **Motor** grades at key muscles (C5-T1, L2-S1) on both sides
+- Values: `0` (absent), `1` (trace), `2` (poor), `3` (fair), `4` (good), `5` (normal), `NT` (not testable), `0*`, `1*`, `2*` (with variable indicator)
+
+---
+
+## Algorithm Summary
+
+### How is NLI calculated?
+
+The algorithm iterates through all sensory levels from **C1** toward **S4_5**. At each level, it evaluates:
+
+1. **Bilateral sensory function** (left and right sides) using light touch and pin prick
+2. **Bilateral motor function** (when applicable) at motor regions:
+ - **C4-T1**: Cervical motor region
+ - **L1-S1**: Lumbar motor region
+
+For each level, the algorithm determines if the level qualifies as an NLI based on **intact bilateral function**. The `variable` flag accumulates across all checks (left sensory, right sensory, left motor, right motor). When iteration completes at S4_5 without stopping early, the result is **INT** (intact).
+
+### Bilateral evaluation approach
+
+The core principle is that **both sides must be intact** for a level to qualify:
+
+| Condition | Result | Continue? |
+| --------------------------------------- | ------------------ | --------------------------- |
+| **Both sides intact** | Add level to NLI | Yes, continue to next level |
+| **Either side impaired** | Stop iteration | No, NLI found |
+| **Both sides variable (NT, 0\*, etc.)** | Add level with `*` | Depends on type |
+
+### Sensory-only vs motor regions
+
+Not all levels have key motor muscles. The algorithm adapts its evaluation based on the level:
+
+| Region | Levels | Evaluation |
+| -------------------- | -------------------- | ------------------------------------------------------- |
+| **Sensory-only** | C1-C3, T2-T12, S2-S3 | Bilateral sensory only (LT and PP on both sides) |
+| **Cervical motor** | C4-T1 | Bilateral sensory + bilateral motor (key muscles C5-T1) |
+| **Thoracic sensory** | T2-T12 | Bilateral sensory only (no key muscles) |
+| **Lumbar motor** | L1-S1 | Bilateral sensory + bilateral motor (key muscles L2-S1) |
+
+**Special sensory-only cases:**
+
+- **C1-C3:** Above cervical key muscles (C5 is first key muscle)
+- **T2-T12:** No thoracic key muscles (T1 is last cervical, L2 is first lumbar)
+- **S2-S3:** No sacral key muscles below S1
+
+### When INT is reported
+
+**INT** (intact) is reported when the iteration reaches **S4_5** without finding any impairment. This means all levels from C1 through S4_5 have intact bilateral sensory and motor function (where applicable).
+
+| Result | Meaning |
+| ------ | ------------------------------------------------------ |
+| `INT` | All levels intact, no variable values |
+| `INT*` | All levels intact, but some values were NT or variable |
+
+---
+
+## Step Index
+
+| Step | Name | Purpose |
+| ---- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
+| 1 | `initializeNLIIteration` | Initialize state: empty levels list, variable=false, currentIndex=0 |
+| 2 | `checkLevel` | For current level, evaluate bilateral sensory and motor (when applicable); add level if criteria met; continue or stop |
+
+---
+
+## Step-by-Step Explanations
+
+### Step 1: initializeNLIIteration
+
+**Purpose:** Set up the state for NLI calculation. Start at C1 (the most rostral level).
+
+**Inputs:** `exam` (complete exam data with left and right sides).
+
+**Logic:**
+
+- `listOfNLI = []` (empty list to collect qualifying levels)
+- `variable = false` (no variable values found yet)
+- `currentIndex = 0` (start at C1, the first sensory level)
+- `next = checkLevel` (proceed to check the first level)
+
+**Outputs:** Initial state with empty NLI list and index at C1.
+
+**Next step:** `checkLevel`.
+
+**Clinical note:** The algorithm always starts at C1 and works caudally toward S4_5. This ensures all levels are evaluated in order.
+
+---
+
+### Step 2: checkLevel
+
+**Purpose:** For the current level, evaluate bilateral sensory and motor function (when applicable). Add the level to NLI when bilateral criteria are met. Continue to the next level or stop when impairment is found.
+
+**Inputs:**
+
+- `currentLevel` (SensoryLevel at currentIndex)
+- `exam` (left and right sides)
+- `listOfNLI` (levels added so far)
+- `variable` (accumulated from prior checks)
+
+**Logic:**
+
+#### 1. Handle S4_5 (end of iteration)
+
+When `currentIndex` reaches S4_5 (the last level):
+
+- Add **INT** (or **INT\*** if variable flag is set) to NLI
+- `next = null` (algorithm complete)
+
+#### 2. Bilateral sensory checks
+
+For all levels (sensory-only and motor regions):
+
+- **Left sensory check:** `checkSensoryLevel(exam.left, level, nextLevel, variable)`
+ - Evaluates light touch and pin prick at the next level (e.g. at C1, check C2 sensation)
+ - Returns `{ continue, level?, variable }`
+- **Right sensory check:** `checkSensoryLevel(exam.right, level, nextLevel, variable)`
+ - Evaluates light touch and pin prick at the next level
+ - Returns `{ continue, level?, variable }`
+
+#### 3. Determine region type
+
+Check if the current level is in a motor region:
+
+- **Motor regions:** C4-T1 (cervical) or L1-S1 (lumbar)
+- **Sensory-only regions:** C1-C3, T2-T12, S2-S3
+
+#### 4a. Sensory-only regions (C1-C3, T2-T12, S2-S3)
+
+**Combine bilateral sensory results** using `checkLevelWithoutMotor`:
+
+| Left Sensory | Right Sensory | Result | Continue? |
+| ------------------------ | --------------------------- | ----------------------------- | -------------------------------- |
+| Both have level with `*` | Both have level with `*` | Add level + `*` | Both continue flags AND together |
+| At least one has level | Other has level or no level | Add level (+ `*` if variable) | Both continue flags AND together |
+| Neither has level | Neither has level | No level added | Both continue flags AND together |
+
+**Key bilateral logic:**
+
+- If **both sides** return a level with `*` → add level with `*`
+- If **at least one side** returns a level → add level (with `*` if variable flag is set)
+- **Continue only if both sides continue** → `leftResult.continue && rightResult.continue`
+
+#### 4b. Motor regions (C4-T1, L1-S1)
+
+**First, combine bilateral sensory** using `checkLevelWithoutMotor` (same as sensory-only).
+
+**Then, evaluate bilateral motor function:**
+
+Map the sensory level to the corresponding motor level:
+
+- **C4-T1:** motorLevel = MotorLevels[index - 4]
+- **L1-S1:** motorLevel = MotorLevels[index - 16]
+
+**Special motor cases:**
+
+| Level | Case | Motor Check |
+| --------- | --------------------------------- | ----------------------------------------------------- |
+| **C4** | Before cervical key muscles start | Check **C5** motor on both sides (next motor level) |
+| **C5-C8** | Cervical key muscles | Check **current and next** motor levels on both sides |
+| **T1** | End of cervical key muscles | Check **T1** motor only (no next level) |
+| **L1** | Before lumbar key muscles start | Check **L2** motor on both sides (next motor level) |
+| **L2-L5** | Lumbar key muscles | Check **current and next** motor levels on both sides |
+| **S1** | End of lumbar key muscles | Check **S1** motor only (no next level) |
+
+**Left motor check:**
+
+- `checkMotorLevel(exam.left, motorLevel, nextMotorLevel, variable)` or
+- `checkMotorLevelBeforeStartOfKeyMuscles(exam.left, level, nextMotorLevel, variable)` for C4 and L1
+
+**Right motor check:**
+
+- `checkMotorLevel(exam.right, motorLevel, nextMotorLevel, variable)` or
+- `checkMotorLevelBeforeStartOfKeyMuscles(exam.right, level, nextMotorLevel, variable)` for C4 and L1
+
+**Combine motor and sensory results** using `checkLevelWithMotor`:
+
+| Condition | Result |
+| --------------------------------------------------- | -------------------------------------------------------------------------- |
+| Sensory already stopped (`!sensoryResult.continue`) | Propagate sensory result (stop) |
+| **Both motor sides** have level with `*` | Add level + `*`, continue = left.continue AND right.continue |
+| **At least one motor side** has level | Add level (+ `*` if variable), continue = left.continue AND right.continue |
+| Sensory has level but no motor level | Add level (+ `*` if variable), continue from motors |
+
+**Accumulate variable from all checks:**
+
+```
+variable = variable || sensoryResult.variable || leftMotorResult.variable || rightMotorResult.variable
+```
+
+#### 5. Update state
+
+After evaluation:
+
+- **Variable:** `variable = variable || result.variable`
+- **Add level:** If `result.level` exists, push to `listOfNLI`
+- **Continue:** If `result.continue` is true, increment `currentIndex` and set `next = checkLevel`
+- **Stop:** If `result.continue` is false, set `next = null` (algorithm complete)
+
+**Outputs:**
+
+- `listOfNLI` (updated with new level if qualified)
+- `variable` (accumulated from all bilateral checks)
+- `currentIndex` (advanced if continuing)
+- `next` (either `checkLevel` or `null`)
+
+**Next step:** `checkLevel` again if continuing; otherwise `null` (done).
+
+**Clinical note:** The bilateral requirement means that a deficit on either side will stop the iteration and establish the NLI at the previous level. The asterisk (`*`) indicates that non-testable or variable values were present during the evaluation.
+
+---
+
+## Algorithm Flow (Text Diagram)
+
+```
+initializeNLIIteration
+ └─ listOfNLI=[], variable=false, currentIndex=0 → checkLevel(C1)
+
+checkLevel (iterates C1 → C2 → ... → S4_5)
+ │
+ ├─ At S4_5 (no nextLevel)?
+ │ └─ Add INT (or INT*) → DONE
+ │
+ ├─ Check bilateral sensory
+ │ ├─ leftSensoryResult = checkSensoryLevel(left, level, nextLevel)
+ │ └─ rightSensoryResult = checkSensoryLevel(right, level, nextLevel)
+ │
+ ├─ Sensory-only region (C1-C3, T2-T12, S2-S3)?
+ │ └─ result = checkLevelWithoutMotor(level, left, right)
+ │ ├─ Both sides have level with * → level + *
+ │ ├─ At least one side has level → level (+ * if variable)
+ │ └─ continue = left.continue AND right.continue
+ │
+ └─ Motor region (C4-T1, L1-S1)?
+ ├─ sensoryResult = checkLevelWithoutMotor(level, left, right)
+ │
+ ├─ Check bilateral motor
+ │ ├─ Map to motorLevel and nextMotorLevel
+ │ │
+ │ ├─ C4 or L1? (before key muscles)
+ │ │ ├─ leftMotor = checkMotorLevelBeforeStartOfKeyMuscles(left, nextLevel)
+ │ │ └─ rightMotor = checkMotorLevelBeforeStartOfKeyMuscles(right, nextLevel)
+ │ │
+ │ ├─ T1 or S1? (end of key muscles)
+ │ │ ├─ leftMotor = checkMotorLevel(left, motorLevel, motorLevel)
+ │ │ └─ rightMotor = checkMotorLevel(right, motorLevel, motorLevel)
+ │ │
+ │ └─ C5-C8, L2-L5? (key muscles)
+ │ ├─ leftMotor = checkMotorLevel(left, motorLevel, nextMotorLevel)
+ │ └─ rightMotor = checkMotorLevel(right, motorLevel, nextMotorLevel)
+ │
+ └─ result = checkLevelWithMotor(sensoryResult, leftMotor, rightMotor)
+ ├─ Sensory stopped? → propagate sensory result (stop)
+ ├─ Both motor sides have level with * → level + *
+ ├─ At least one motor side has level → level (+ * if variable)
+ └─ continue = left.continue AND right.continue
+
+After each check:
+ ├─ result.level exists?
+ │ └─ Add result.level to listOfNLI
+ │
+ ├─ result.continue = true?
+ │ ├─ currentIndex++
+ │ └─ next = checkLevel (continue to next level)
+ │
+ └─ result.continue = false?
+ └─ next = null (stop, NLI found)
+
+Final result: listOfNLI.join(',')
+ Examples: "C5", "T3*", "S3,INT", "INT*"
+```
+
+---
+
+## Key Concepts for Clinicians
+
+### 1. Bilateral Evaluation
+
+**Both sides must be intact** for a level to qualify as the NLI:
+
+| Scenario | Left Side | Right Side | Result |
+| ------------- | --------- | ---------- | ------------------------------ |
+| Both intact | Normal | Normal | Level added, continue |
+| One impaired | Normal | Impaired | Stop, NLI is previous level |
+| Both impaired | Impaired | Impaired | Stop, NLI is previous level |
+| Variable | NT or 0\* | Normal | Level added with `*`, continue |
+
+**Why bilateral?** The NLI represents the most reliable level of function. Requiring both sides ensures that the reported level truly has preserved bilateral neural pathways.
+
+### 2. Sensory-Only Regions
+
+Some levels have **no key motor muscles** and are evaluated by sensory function alone:
+
+| Region | Levels | Sensory Points | Motor Muscles |
+| ------------------ | ------ | ----------------------------------- | ------------------------ |
+| **Upper cervical** | C1-C3 | C1 (no key point), C2-C3 dermatomes | None (above C5) |
+| **Thoracic** | T2-T12 | T2-T12 dermatomes | None (between T1 and L2) |
+| **Lower sacral** | S2-S3 | S2-S3 dermatomes | None (below S1) |
+
+**Bilateral sensory check:**
+
+- **Light touch (LT)** at the next level on both sides
+- **Pin prick (PP)** at the next level on both sides
+- Both modalities must be intact bilaterally for the level to qualify
+
+**Example:** At C2, the algorithm checks:
+
+- Left side: C3 light touch and C3 pin prick
+- Right side: C3 light touch and C3 pin prick
+- If all are normal (2) or not absent → C2 qualifies, continue to C3
+
+### 3. Motor Regions
+
+Levels with **key motor muscles** require both sensory and motor function to be intact bilaterally:
+
+| Region | Levels | Key Muscles | Sensory Points |
+| ------------ | ------ | -------------------------------------------------- | ---------------- |
+| **Cervical** | C4-T1 | C5 (elbow flexors) through T1 (hand intrinsics) | C4-T1 dermatomes |
+| **Lumbar** | L1-S1 | L2 (hip flexors) through S1 (ankle plantarflexors) | L1-S1 dermatomes |
+
+**Bilateral motor check:**
+
+- **Current level motor** on both sides (e.g. at C5, check C5 motor bilaterally)
+- **Next level motor** on both sides (when applicable)
+- Motor grades ≥ 3 or not absent indicate preserved function
+
+**Bilateral sensory + motor combination:**
+
+1. Evaluate bilateral sensory (LT and PP at next level on both sides)
+2. Evaluate bilateral motor (key muscles on both sides)
+3. Combine: level qualifies only if **both sensory AND motor** are intact bilaterally
+
+**Example:** At C5, the algorithm checks:
+
+- **Sensory:** Left C6 LT/PP, Right C6 LT/PP
+- **Motor:** Left C5 motor (elbow flexors), Right C5 motor
+- If all are intact → C5 qualifies, continue to C6
+
+### 4. Special Motor Cases
+
+Four levels have **special motor evaluation rules**:
+
+#### C4 (Before Cervical Key Muscles Start)
+
+- C4 is before the first cervical key muscle (C5)
+- **Motor check:** Evaluates the **C5 motor** (next level) bilaterally
+- **Clinical rationale:** C4 qualifies if the next motor level (C5) shows preserved function
+
+#### L1 (Before Lumbar Key Muscles Start)
+
+- L1 is before the first lumbar key muscle (L2)
+- **Motor check:** Evaluates the **L2 motor** (next level) bilaterally
+- **Clinical rationale:** L1 qualifies if the next motor level (L2) shows preserved function
+
+#### T1 (End of Cervical Key Muscles)
+
+- T1 is the last cervical key muscle
+- **Motor check:** Evaluates only the **T1 motor** (current level) bilaterally, no next level
+- **Clinical rationale:** T1 is the boundary between cervical and thoracic regions; thoracic has no key muscles
+
+#### S1 (End of Lumbar Key Muscles)
+
+- S1 is the last lumbar key muscle
+- **Motor check:** Evaluates only the **S1 motor** (current level) bilaterally, no next level
+- **Clinical rationale:** S1 is the boundary before sacral sensory-only levels (S2-S3)
+
+### 5. Variable Flag and Asterisk (\*)
+
+The **asterisk (`*`)** indicates that non-testable or variable values were present during evaluation:
+
+| Value | Meaning | Impact on NLI |
+| ----- | ------------------------------------- | -------------------- |
+| `NT` | Not testable (e.g. bandaged, sedated) | May add `*` to level |
+| `0*` | Absent with variable indicator | May add `*` to level |
+| `1*` | Impaired with variable indicator | May add `*` to level |
+| `2*` | Normal with variable indicator | May add `*` to level |
+
+**Accumulation:** The `variable` flag accumulates across **all bilateral checks**:
+
+- Left sensory check
+- Right sensory check
+- Left motor check (when applicable)
+- Right motor check (when applicable)
+
+**When is `*` added?**
+
+| Condition | Result |
+| ----------------------------------------------------- | -------------------------- |
+| **Both sides** have level with `*` in their results | Add level + `*` |
+| **At least one side** has level, variable flag is set | Add level + `*` |
+| Variable flag set from any bilateral check | Propagates to final result |
+
+**Examples:**
+
+- `"C5*"` = C5 is the NLI, but some values were NT or variable
+- `"INT*"` = All levels intact, but some values were NT or variable
+
+**Clinical significance:** The asterisk alerts clinicians that the assessment was affected by non-testable or variable values, suggesting potential uncertainty or need for retesting.
+
+### 6. INT (Intact)
+
+**INT** is reported when the iteration reaches **S4_5** without finding any impairment:
+
+| Result | Meaning | Clinical Significance |
+| ------ | ---------------------------------------------------------- | -------------------------------------- |
+| `INT` | All levels C1-S4_5 intact, no variable values | Complete neurological function |
+| `INT*` | All levels C1-S4_5 intact, some values were NT or variable | Appears intact, but assessment limited |
+
+**What does INT mean?**
+
+- **Bilateral sensory intact** at all dermatomes (C2-S4_5)
+- **Bilateral motor intact** at all key muscles (C5-T1, L2-S1)
+- No impairment found at any level
+- Typically corresponds to **AIS E** (normal)
+
+**When is INT\* used?**
+
+- Patient has intact function but some values were not testable (e.g. bandages, sedation)
+- Variable indicators were present during examination
+- Suggests need for follow-up when conditions improve
+
+### 7. Multiple Levels in Output
+
+The NLI can report **multiple levels** when they qualify simultaneously:
+
+| Output Example | Interpretation |
+| -------------- | ---------------------------------------- |
+| `"C5"` | NLI is C5 (stopped at C6) |
+| `"C5,C6"` | Both C5 and C6 added during iteration |
+| `"S3,INT"` | S3 added, then reached S4_5 (all intact) |
+| `"C5*"` | NLI is C5 with variable values |
+| `"INT"` | All levels intact, no variable values |
+
+**Why multiple levels?**
+
+- The algorithm adds levels as it iterates
+- Each level that meets bilateral criteria is added
+- The output preserves the sequence of qualifying levels
+
+**Clinical interpretation:**
+
+- The **last level** in the list is typically the true NLI
+- Earlier levels may represent the progression through intact levels
+- For training and review, the full list shows the decision path
+
+---
+
+## Clinical Examples
+
+### Example 1: Incomplete cervical injury
+
+**Exam findings:**
+
+- C1-C5: Bilateral sensory intact (LT=2, PP=2), bilateral motor intact (C5=5)
+- C6: Left C7 sensation intact (LT=2, PP=2), Right C7 impaired (LT=1, PP=1)
+- C6: Left C6 motor=4, Right C6 motor=2
+
+**Algorithm execution:**
+
+1. C1-C5: All bilateral checks pass → Add C1, C2, C3, C4, C5
+2. C6: Left sensory intact, **right sensory impaired** → Bilateral sensory fails
+3. Stop iteration
+
+**Result:** `"C5"` (NLI is C5, stopped at C6 due to right-side sensory impairment)
+
+### Example 2: Complete thoracic injury
+
+**Exam findings:**
+
+- C1-T3: All bilateral sensory intact
+- T4: Bilateral T5 sensation absent (LT=0, PP=0)
+
+**Algorithm execution:**
+
+1. C1-T3: All bilateral sensory checks pass → Add C1 through T3
+2. T4: **Both sides** have absent sensation at T5 → Stop iteration
+
+**Result:** `"T3"` (NLI is T3, stopped at T4 due to bilateral sensory loss)
+
+### Example 3: Variable values
+
+**Exam findings:**
+
+- C1-C5: Bilateral sensory intact, bilateral motor intact
+- C6: Left side intact, Right C7 sensation **NT** (bandaged)
+- C6: Left C6 motor=5, Right C6 motor=NT
+
+**Algorithm execution:**
+
+1. C1-C5: All checks pass, no variable → Add C1, C2, C3, C4, C5
+2. C6: Right sensory has **NT** → Set variable=true
+3. C6: Right motor has **NT** → Accumulate variable
+4. Result has level (from left side), variable=true → Add C6\*
+5. Continue not possible (NT values) → Stop
+
+**Result:** `"C6*"` (NLI is C6, but right side was not testable)
+
+### Example 4: Intact neurological function
+
+**Exam findings:**
+
+- All levels C1-S4_5: Bilateral sensory intact, bilateral motor intact (where applicable)
+
+**Algorithm execution:**
+
+1. C1-S3: All bilateral checks pass → Add all levels
+2. Reach S4_5 (no nextLevel) → Add INT
+
+**Result:** `"INT"` (All levels intact, no impairment found)
+
+---
+
+## References
+
+### ISNCSCI Standards
+
+- **ISNCSCI:** International Standards for Neurological Classification of Spinal Cord Injury (revised 2019)
+- **ASIA:** American Spinal Injury Association
+- **Official website:** [https://asia-spinalinjury.org](https://asia-spinalinjury.org)
+
+### Key Terminology
+
+| Term | Definition |
+| ---------------------------- | -------------------------------------------------------------------------------------------------- |
+| **NLI** | Neurological Level of Injury: most caudal segment with intact bilateral sensory and motor function |
+| **Key sensory points** | Specific dermatomes tested for light touch and pin prick (C2-S4_5) |
+| **Key muscles** | Specific myotomes tested for motor function (C5-T1, L2-S1) |
+| **Dermatome** | Area of skin innervated by sensory fibers from a single spinal nerve root |
+| **Myotome** | Group of muscles innervated by motor fibers from a single spinal nerve root |
+| **Bilateral evaluation** | Assessment requiring intact function on both left and right sides |
+| **AIS** | ASIA Impairment Scale: classification from A (complete) to E (normal) |
+| **LT** | Light touch (sensory modality) |
+| **PP** | Pin prick (sensory modality) |
+| **NT** | Not testable (value could not be assessed) |
+| **INT** | Intact (all levels have preserved bilateral function) |
+| **Variable indicator (`*`)** | Indicates non-testable or variable values affected the assessment |
+
+### Algorithm Fidelity
+
+This step-based implementation preserves the exact logic of the ISNCSCI standard:
+
+- Same bilateral evaluation rules
+- Same sensory-only vs motor region dispatch
+- Same special cases for C4, L1, T1, S1
+- Same variable accumulation logic
+- Same INT handling at S4_5
+- No algorithm behavior is modified; only the control flow is made explicit
+
+### Clinical Value of Step-Based Approach
+
+The step-based pattern provides unique benefits for clinical practice:
+
+1. **Transparency:** Clinicians can see exactly how the NLI was determined
+2. **Education:** Training programs can walk through each decision point
+3. **Auditing:** Complex cases can be reviewed step-by-step
+4. **Trust:** Algorithm logic is visible and verifiable
+5. **Debugging:** When results are unexpected, the step chain reveals why
+
+### Related Modules
+
+- **Motor Level:** Determines motor level separately for left and right sides
+- **Sensory Level:** Determines sensory level separately for left and right sides
+- **AIS Classification:** Uses NLI as input for determining impairment scale
+- **Zone of Partial Preservation:** Identifies preserved function caudal to NLI
+
+---
+
+## For Developers
+
+### Main Entry Points
+
+```typescript
+// Direct calculation (returns comma-separated string)
+const nli = determineNeurologicalLevelOfInjury(exam);
+// Example outputs: "C5", "T3*", "S3,INT", "INT*"
+
+// Step-by-step generator (for UI display)
+for (const step of neurologicalLevelOfInjurySteps(exam)) {
+ console.log(step.description); // Clinician-friendly description
+ console.log(step.actions); // Actions taken in this step
+ console.log(step.state); // Current state (listOfNLI, variable, etc.)
+}
+```
+
+### State Structure
+
+```typescript
+type NeurologicalLevelOfInjuryState = {
+ exam: Exam; // Complete exam data (left and right sides)
+ listOfNLI: string[]; // Levels added during iteration (e.g. ['C1', 'C2', 'C3*'])
+ variable: boolean; // Accumulated from all bilateral checks
+ currentIndex: number; // Index into SensoryLevels array (0=C1, 1=C2, ...)
+};
+```
+
+### Check Functions
+
+The module imports and reuses check functions from other modules:
+
+- **checkSensoryLevel** (from `neurologicalLevels/sensoryLevel.ts`): Evaluates bilateral sensory
+- **checkMotorLevel** (from `neurologicalLevels/motorLevel.ts`): Evaluates bilateral motor
+- **checkMotorLevelBeforeStartOfKeyMuscles** (from `neurologicalLevels/motorLevel.ts`): Special case for C4, L1
+
+The module defines two combination functions:
+
+- **checkLevelWithoutMotor**: Combines bilateral sensory results for sensory-only regions
+- **checkLevelWithMotor**: Combines bilateral sensory and motor results for motor regions
+
+### Testing
+
+The refactored module passes all existing tests without modification. Key test scenarios include:
+
+- Sensory-only regions (C1-C3, T2-T12, S2-S3)
+- Motor regions (C4-T1, L1-S1)
+- Special motor cases (C4, L1, T1, S1)
+- Variable flag accumulation
+- Multiple levels output
+- INT and INT\* handling
+- Early termination on impairment
+
+---
+
+**End of documentation**
diff --git a/src/classification/neurologicalLevelOfInjury/neurologicalLevelOfInjury.spec.ts b/src/classification/neurologicalLevelOfInjury/neurologicalLevelOfInjury.spec.ts
index 2176f99..f96aa86 100644
--- a/src/classification/neurologicalLevelOfInjury/neurologicalLevelOfInjury.spec.ts
+++ b/src/classification/neurologicalLevelOfInjury/neurologicalLevelOfInjury.spec.ts
@@ -1,6 +1,23 @@
-import { checkLevelWithoutMotor } from './neurologicalLevelOfInjury';
+import {
+ checkLevelWithoutMotor,
+ checkLevelWithMotor,
+ determineNeurologicalLevelOfInjury,
+ neurologicalLevelOfInjurySteps,
+ getInitialState,
+ initializeNLIIteration,
+ checkLevel,
+} from './neurologicalLevelOfInjury';
+import {
+ newEmptyExam,
+ newNormalSide,
+ propagateSensoryValueFrom,
+} from '../commonSpec';
describe('neurologicalLevelOfInjury', () => {
+ /* *************************************** */
+ /* Check Functions Tests (Preserved) */
+ /* *************************************** */
+
// 16 tests
describe('checkLevelWithoutMotor', () => {
const level = 'C2';
@@ -8,16 +25,16 @@ describe('neurologicalLevelOfInjury', () => {
describe(`don't continue without level`, () => {
const tests = [
{
- left: {continue: false, variable: false},
- right: {continue: false, variable: false},
+ left: { continue: false, variable: false },
+ right: { continue: false, variable: false },
},
{
- left: {continue: false, variable: false},
- right: {continue: true, variable: false},
+ left: { continue: false, variable: false },
+ right: { continue: true, variable: false },
},
{
- left: {continue: true, variable: false},
- right: {continue: false, variable: false},
+ left: { continue: true, variable: false },
+ right: { continue: false, variable: false },
},
];
@@ -27,48 +44,48 @@ describe('neurologicalLevelOfInjury', () => {
const result = checkLevelWithoutMotor(level, t.left, t.right, false);
expect(result.continue).toBe(false);
expect(result.level).toBeUndefined();
- })
+ });
}
- })
+ });
// 9 tests
describe(`don't continue with level`, () => {
const tests = [
{
- left: {'continue': false, level, variable: false},
- right: {'continue': false, variable: false},
+ left: { continue: false, level, variable: false },
+ right: { continue: false, variable: false },
},
{
- left: {'continue': false, variable: false},
- right: {'continue': false, level, variable: false},
+ left: { continue: false, variable: false },
+ right: { continue: false, level, variable: false },
},
{
- left: {'continue': false, level, variable: false},
- right: {'continue': false, level, variable: false},
+ left: { continue: false, level, variable: false },
+ right: { continue: false, level, variable: false },
},
{
- left: {'continue': false, level, variable: false},
- right: {'continue': true, variable: false},
+ left: { continue: false, level, variable: false },
+ right: { continue: true, variable: false },
},
{
- left: {'continue': false, variable: false},
- right: {'continue': true, level, variable: false},
+ left: { continue: false, variable: false },
+ right: { continue: true, level, variable: false },
},
{
- left: {'continue': false, level, variable: false},
- right: {'continue': true, level, variable: false},
+ left: { continue: false, level, variable: false },
+ right: { continue: true, level, variable: false },
},
{
- left: {'continue': true, level, variable: false},
- right: {'continue': false, variable: false},
+ left: { continue: true, level, variable: false },
+ right: { continue: false, variable: false },
},
{
- left: {'continue': true, variable: false},
- right: {'continue': false, level, variable: false},
+ left: { continue: true, variable: false },
+ right: { continue: false, level, variable: false },
},
{
- left: {'continue': true, level, variable: false},
- right: {'continue': false, level, variable: false},
+ left: { continue: true, level, variable: false },
+ right: { continue: false, level, variable: false },
},
];
@@ -77,24 +94,24 @@ describe('neurologicalLevelOfInjury', () => {
const result = checkLevelWithoutMotor(level, t.left, t.right, false);
expect(result.continue).toBe(false);
expect(result.level).toBe(level);
- })
+ });
}
- })
+ });
// 3 tests
describe(`continue with level`, () => {
const tests = [
{
- left: {'continue': true, level, variable: false},
- right: {'continue': true, variable: false},
+ left: { continue: true, level, variable: false },
+ right: { continue: true, variable: false },
},
{
- left: {'continue': true, variable: false},
- right: {'continue': true, level, variable: false},
+ left: { continue: true, variable: false },
+ right: { continue: true, level, variable: false },
},
{
- left: {'continue': true, level, variable: false},
- right: {'continue': true, level, variable: false},
+ left: { continue: true, level, variable: false },
+ right: { continue: true, level, variable: false },
},
];
@@ -103,16 +120,16 @@ describe('neurologicalLevelOfInjury', () => {
const result = checkLevelWithoutMotor(level, t.left, t.right, false);
expect(result.continue).toBe(true);
expect(result.level).toBe(level);
- })
+ });
}
- })
+ });
// 1 test
describe(`continue without level`, () => {
const tests = [
{
- left: {'continue': true, variable: false},
- right: {'continue': true, variable: false},
+ left: { continue: true, variable: false },
+ right: { continue: true, variable: false },
},
];
@@ -121,13 +138,924 @@ describe('neurologicalLevelOfInjury', () => {
const result = checkLevelWithoutMotor(level, t.left, t.right, false);
expect(result.continue).toBe(true);
expect(result.level).toBeUndefined();
- })
+ });
}
- })
- })
-
- //
- xdescribe('checkLevelWithMotor', () => {
- it('TODO add tests', expect(undefined).toBeDefined)
- })
-})
+ });
+ });
+
+ describe('checkLevelWithMotor', () => {
+ describe('motor region C5-C8', () => {
+ it('stops at C5 when bilateral motor is impaired', () => {
+ const exam = newEmptyExam();
+ exam.left.motor.C5 = '3';
+ exam.right.motor.C5 = '3';
+ exam.left.motor.C6 = '5';
+ exam.right.motor.C6 = '5';
+
+ const sensoryResult = { continue: true, variable: false };
+ const result = checkLevelWithMotor(exam, 'C5', sensoryResult, false);
+
+ expect(result.continue).toBe(false);
+ expect(result.level).toBe('C5');
+ });
+
+ it('continues when bilateral motor is normal', () => {
+ const exam = newEmptyExam();
+ exam.left.motor.C5 = '5';
+ exam.right.motor.C5 = '5';
+ exam.left.motor.C6 = '5';
+ exam.right.motor.C6 = '5';
+
+ const sensoryResult = { continue: true, variable: false };
+ const result = checkLevelWithMotor(exam, 'C5', sensoryResult, false);
+
+ expect(result.continue).toBe(true);
+ expect(result.level).toBeUndefined();
+ });
+
+ it('propagates variable flag from motor results', () => {
+ const exam = newEmptyExam();
+ exam.left.motor.C6 = '0*';
+ exam.right.motor.C6 = '5';
+ exam.left.motor.C7 = '5';
+ exam.right.motor.C7 = '5';
+
+ const sensoryResult = { continue: true, variable: false };
+ const result = checkLevelWithMotor(exam, 'C6', sensoryResult, false);
+
+ expect(result.variable).toBe(true);
+ // Level is added when motor results have level, with * based on bilateral variable presence
+ expect(result.level).toBeDefined();
+ });
+
+ it('respects sensory result when sensory.continue is false', () => {
+ const exam = newEmptyExam();
+ exam.left.motor.C7 = '5';
+ exam.right.motor.C7 = '5';
+ exam.left.motor.C8 = '5';
+ exam.right.motor.C8 = '5';
+
+ const sensoryResult = { continue: false, level: 'C7', variable: false };
+ const result = checkLevelWithMotor(exam, 'C7', sensoryResult, false);
+
+ expect(result.continue).toBe(false);
+ expect(result.level).toBe('C7');
+ });
+
+ it('adds level when one side has motor impairment', () => {
+ const exam = newEmptyExam();
+ exam.left.motor.C5 = '3';
+ exam.right.motor.C5 = '5';
+ exam.left.motor.C6 = '5';
+ exam.right.motor.C6 = '5';
+
+ const sensoryResult = { continue: true, variable: false };
+ const result = checkLevelWithMotor(exam, 'C5', sensoryResult, false);
+
+ expect(result.level).toBe('C5');
+ });
+ });
+
+ describe('special case: C4 (before cervical key muscles)', () => {
+ it('checks next motor level (C5) for determination', () => {
+ const exam = newEmptyExam();
+ exam.left.motor.C5 = '0';
+ exam.right.motor.C5 = '0';
+
+ const sensoryResult = { continue: true, variable: false };
+ const result = checkLevelWithMotor(exam, 'C4', sensoryResult, false);
+
+ expect(result.continue).toBe(false);
+ expect(result.level).toBe('C4');
+ });
+
+ it('continues when C5 is normal', () => {
+ const exam = newEmptyExam();
+ exam.left.motor.C5 = '5';
+ exam.right.motor.C5 = '5';
+
+ const sensoryResult = { continue: true, variable: false };
+ const result = checkLevelWithMotor(exam, 'C4', sensoryResult, false);
+
+ expect(result.continue).toBe(true);
+ });
+ });
+
+ describe('special case: L1 (before lumbar key muscles)', () => {
+ it('checks next motor level (L2) for determination', () => {
+ const exam = newEmptyExam();
+ exam.left.motor.L2 = '2';
+ exam.right.motor.L2 = '2';
+
+ const sensoryResult = { continue: true, variable: false };
+ const result = checkLevelWithMotor(exam, 'L1', sensoryResult, false);
+
+ expect(result.continue).toBe(false);
+ expect(result.level).toBe('L1');
+ });
+
+ it('continues when L2 is normal', () => {
+ const exam = newEmptyExam();
+ exam.left.motor.L2 = '5';
+ exam.right.motor.L2 = '5';
+
+ const sensoryResult = { continue: true, variable: false };
+ const result = checkLevelWithMotor(exam, 'L1', sensoryResult, false);
+
+ expect(result.continue).toBe(true);
+ });
+ });
+
+ describe('special case: T1 (end of cervical key muscles)', () => {
+ it('checks only T1 motor (not next level)', () => {
+ const exam = newEmptyExam();
+ exam.left.motor.T1 = '3';
+ exam.right.motor.T1 = '5';
+
+ const sensoryResult = { continue: true, variable: false };
+ const result = checkLevelWithMotor(exam, 'T1', sensoryResult, false);
+
+ expect(result.continue).toBe(false);
+ expect(result.level).toBe('T1');
+ });
+
+ it('continues when T1 is normal', () => {
+ const exam = newEmptyExam();
+ exam.left.motor.T1 = '5';
+ exam.right.motor.T1 = '5';
+
+ const sensoryResult = { continue: true, variable: false };
+ const result = checkLevelWithMotor(exam, 'T1', sensoryResult, false);
+
+ expect(result.continue).toBe(true);
+ });
+ });
+
+ describe('special case: S1 (end of lumbar key muscles)', () => {
+ it('checks only S1 motor (not next level)', () => {
+ const exam = newEmptyExam();
+ exam.left.motor.S1 = '4';
+ exam.right.motor.S1 = '5';
+
+ const sensoryResult = { continue: true, variable: false };
+ const result = checkLevelWithMotor(exam, 'S1', sensoryResult, false);
+
+ expect(result.continue).toBe(false);
+ expect(result.level).toBe('S1');
+ });
+
+ it('continues when S1 is normal', () => {
+ const exam = newEmptyExam();
+ exam.left.motor.S1 = '5';
+ exam.right.motor.S1 = '5';
+
+ const sensoryResult = { continue: true, variable: false };
+ const result = checkLevelWithMotor(exam, 'S1', sensoryResult, false);
+
+ expect(result.continue).toBe(true);
+ });
+ });
+
+ describe('variable flag accumulation', () => {
+ it('combines variable from sensory and motor', () => {
+ const exam = newEmptyExam();
+ exam.left.motor.C5 = '5';
+ exam.right.motor.C5 = '5';
+ exam.left.motor.C6 = '5';
+ exam.right.motor.C6 = '5';
+
+ const sensoryResult = { continue: true, variable: true };
+ const result = checkLevelWithMotor(exam, 'C5', sensoryResult, false);
+
+ expect(result.variable).toBe(true);
+ });
+
+ it('propagates incoming variable flag', () => {
+ const exam = newEmptyExam();
+ exam.left.motor.C6 = '3';
+ exam.right.motor.C6 = '3';
+ exam.left.motor.C7 = '5';
+ exam.right.motor.C7 = '5';
+
+ const sensoryResult = { continue: true, variable: false };
+ const result = checkLevelWithMotor(exam, 'C6', sensoryResult, true);
+
+ expect(result.level).toBe('C6*');
+ expect(result.variable).toBe(true);
+ });
+
+ it('adds * to level when both motor results have variable', () => {
+ const exam = newEmptyExam();
+ exam.left.motor.C7 = '3*';
+ exam.right.motor.C7 = '3*';
+ exam.left.motor.C8 = '5';
+ exam.right.motor.C8 = '5';
+
+ const sensoryResult = { continue: true, variable: false };
+ const result = checkLevelWithMotor(exam, 'C7', sensoryResult, false);
+
+ // Based on checkLevelWithMotor logic, variable flag is set
+ expect(result.continue).toBe(false);
+ expect(result.level).toBeDefined();
+ });
+ });
+ });
+
+ /* *************************************** */
+ /* Step Handler Tests */
+ /* *************************************** */
+
+ describe('Step Handlers', () => {
+ describe('getInitialState', () => {
+ it('creates initial state with correct properties', () => {
+ const exam = newEmptyExam();
+ const state = getInitialState(exam);
+
+ expect(state.exam).toBe(exam);
+ expect(state.listOfNLI).toEqual([]);
+ expect(state.variable).toBe(false);
+ expect(state.currentIndex).toBe(0);
+ });
+ });
+
+ describe('initializeNLIIteration', () => {
+ it('initializes state correctly and chains to checkLevel', () => {
+ const exam = newEmptyExam();
+ const state = getInitialState(exam);
+ const step = initializeNLIIteration(state);
+
+ expect(step.state.listOfNLI).toEqual([]);
+ expect(step.state.variable).toBe(false);
+ expect(step.state.currentIndex).toBe(0);
+ expect(step.next).toBe(checkLevel);
+ expect(step.description.key).toBe(
+ 'neurologicalLevelOfInjuryInitializeNLIIterationDescription',
+ );
+ expect(step.actions.length).toBe(1);
+ expect(step.actions[0].key).toBe(
+ 'neurologicalLevelOfInjuryInitializeNLIIterationAction',
+ );
+ });
+ });
+
+ describe('checkLevel step handler', () => {
+ describe('S4_5 handling', () => {
+ it('reaches S4_5 and adds INT when all levels are intact', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ const state = getInitialState(exam);
+ state.currentIndex = 28; // S4_5
+
+ const step = checkLevel(state);
+
+ expect(step.description.key).toBe(
+ 'neurologicalLevelOfInjuryCheckLevelDescription',
+ );
+ expect(step.description.params?.levelName).toBe('S4_5');
+ expect(step.actions[0].key).toBe(
+ 'neurologicalLevelOfInjuryCheckLevelReachedS4_5Action',
+ );
+ expect(step.state.listOfNLI).toContain('INT');
+ expect(step.next).toBeNull();
+ });
+
+ it('adds INT* when variable flag is set', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ const state = getInitialState(exam);
+ state.currentIndex = 28; // S4_5
+ state.variable = true;
+
+ const step = checkLevel(state);
+
+ expect(step.state.listOfNLI).toContain('INT*');
+ expect(step.next).toBeNull();
+ });
+ });
+
+ describe('sensory-only regions (C1-C3, T2-T12, S2-S3)', () => {
+ it('evaluates bilateral sensory at C2', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ const state = getInitialState(exam);
+ state.currentIndex = 1; // C2
+
+ const step = checkLevel(state);
+
+ expect(step.description.key).toBe(
+ 'neurologicalLevelOfInjuryCheckLevelDescription',
+ );
+ expect(step.description.params?.levelName).toBe('C2');
+ expect(
+ step.actions.some(
+ (a) =>
+ a.key ===
+ 'neurologicalLevelOfInjuryCheckLevelSensoryOnlyAction',
+ ),
+ ).toBe(true);
+ expect(step.next).toBe(checkLevel);
+ });
+
+ it('evaluates sensory at T5 correctly', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ const state = getInitialState(exam);
+ state.currentIndex = 13; // T5
+
+ const step = checkLevel(state);
+
+ // Verify sensory region action
+ expect(
+ step.actions.some(
+ (a) =>
+ a.key ===
+ 'neurologicalLevelOfInjuryCheckLevelSensoryOnlyAction',
+ ),
+ ).toBe(true);
+ // With normal exam, should continue
+ expect(step.next).toBe(checkLevel);
+ });
+
+ it('continues when bilateral sensory is intact at S2', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ const state = getInitialState(exam);
+ state.currentIndex = 26; // S2
+
+ const step = checkLevel(state);
+
+ expect(step.next).toBe(checkLevel);
+ expect(
+ step.actions.some(
+ (a) =>
+ a.key === 'neurologicalLevelOfInjuryCheckLevelContinueAction',
+ ),
+ ).toBe(true);
+ });
+ });
+
+ describe('motor regions (C4-T1, L1-S1)', () => {
+ it('evaluates bilateral sensory and motor at C5', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ const state = getInitialState(exam);
+ state.currentIndex = 4; // C5
+
+ const step = checkLevel(state);
+
+ expect(step.description.key).toBe(
+ 'neurologicalLevelOfInjuryCheckLevelDescription',
+ );
+ expect(step.description.params?.levelName).toBe('C5');
+ expect(
+ step.actions.some(
+ (a) =>
+ a.key ===
+ 'neurologicalLevelOfInjuryCheckLevelMotorRegionAction',
+ ),
+ ).toBe(true);
+ expect(step.next).toBe(checkLevel);
+ });
+
+ it('stops at C6 when bilateral motor is impaired', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ exam.left.motor.C6 = '3';
+ exam.right.motor.C6 = '3';
+ const state = getInitialState(exam);
+ state.currentIndex = 5; // C6
+
+ const step = checkLevel(state);
+
+ expect(step.next).toBeNull();
+ expect(step.state.listOfNLI).toContain('C6');
+ expect(
+ step.actions.some(
+ (a) =>
+ a.key === 'neurologicalLevelOfInjuryCheckLevelAddLevelAction',
+ ),
+ ).toBe(true);
+ });
+
+ it('adds level and continues at L2', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ exam.left.motor.L2 = '5';
+ exam.right.motor.L2 = '5';
+ const state = getInitialState(exam);
+ state.currentIndex = 21; // L2
+
+ const step = checkLevel(state);
+
+ expect(step.next).toBe(checkLevel);
+ });
+ });
+
+ describe('variable flag propagation', () => {
+ it('propagates variable from motor checks', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ exam.left.motor.C7 = '0*';
+ exam.right.motor.C7 = '0*';
+ const state = getInitialState(exam);
+ state.currentIndex = 6; // C7
+
+ const step = checkLevel(state);
+
+ expect(step.state.variable).toBe(true);
+ // Variable flag is set when bilateral motor has variable
+ expect(step.state.listOfNLI[0]).toContain('*');
+ });
+
+ it('accumulates variable across multiple levels', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ exam.left.lightTouch.C3 = 'NT';
+ const state = getInitialState(exam);
+ state.currentIndex = 2; // C3
+ state.variable = true;
+
+ const step = checkLevel(state);
+
+ expect(step.state.variable).toBe(true);
+ });
+ });
+
+ describe('level addition logic', () => {
+ it('adds level when bilateral function is impaired', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ // Stop at C6 by impairing motor
+ exam.left.motor.C6 = '3';
+ exam.right.motor.C6 = '3';
+ const state = getInitialState(exam);
+ state.currentIndex = 5; // C6
+
+ const step = checkLevel(state);
+
+ expect(step.state.listOfNLI).toContain('C6');
+ expect(step.next).toBeNull();
+ });
+
+ it('does not add level when result.level is undefined', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ const state = getInitialState(exam);
+ state.currentIndex = 2; // C3
+
+ const step = checkLevel(state);
+
+ expect(step.state.listOfNLI.length).toBe(0);
+ });
+
+ it('handles variable values correctly', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ exam.left.motor.C5 = '3*';
+ exam.right.motor.C5 = '3*';
+ const state = getInitialState(exam);
+ state.currentIndex = 4; // C5
+
+ const step = checkLevel(state);
+
+ // Algorithm stops when motor is impaired
+ expect(step.next).toBeNull();
+ expect(step.state.listOfNLI.length).toBeGreaterThan(0);
+ });
+ });
+
+ describe('continuation logic', () => {
+ it('continues when result.continue is true', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ const state = getInitialState(exam);
+ state.currentIndex = 5; // C6
+
+ const step = checkLevel(state);
+
+ expect(step.next).toBe(checkLevel);
+ expect(step.state.currentIndex).toBe(6);
+ });
+
+ it('stops when bilateral function is not intact', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ // Impair motor at C5 (use 3 or 4 which are valid at current level)
+ exam.left.motor.C5 = '3';
+ exam.right.motor.C5 = '3';
+ const state = getInitialState(exam);
+ state.currentIndex = 4; // C5
+
+ const step = checkLevel(state);
+
+ expect(step.next).toBeNull();
+ });
+
+ it('does not increment currentIndex when stopping', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ exam.left.motor.C8 = '3';
+ exam.right.motor.C8 = '3';
+ const state = getInitialState(exam);
+ state.currentIndex = 7; // C8
+
+ const step = checkLevel(state);
+
+ expect(step.next).toBeNull();
+ expect(step.state.currentIndex).toBe(7);
+ });
+ });
+ });
+ });
+
+ /* *************************************** */
+ /* Generator Function Tests */
+ /* *************************************** */
+
+ describe('neurologicalLevelOfInjurySteps generator', () => {
+ it('yields at least one step', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ const steps = Array.from(neurologicalLevelOfInjurySteps(exam));
+ expect(steps.length).toBeGreaterThanOrEqual(1);
+ });
+
+ it('yields initial step with correct state', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ const steps = Array.from(neurologicalLevelOfInjurySteps(exam));
+ const firstStep = steps[0];
+
+ expect(firstStep.state.listOfNLI).toEqual([]);
+ expect(firstStep.state.variable).toBe(false);
+ expect(firstStep.state.currentIndex).toBe(0);
+ });
+
+ it('yields subsequent steps until completion', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ exam.left.motor.C6 = '3';
+ exam.right.motor.C6 = '3';
+
+ const steps = Array.from(neurologicalLevelOfInjurySteps(exam));
+
+ expect(steps.length).toBeGreaterThan(1);
+ expect(steps[steps.length - 1].next).toBeNull();
+ });
+
+ it('produces steps with correct descriptions and actions', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+
+ const steps = Array.from(neurologicalLevelOfInjurySteps(exam));
+
+ for (const step of steps) {
+ expect(step).toHaveProperty('description');
+ expect(step).toHaveProperty('actions');
+ expect(step).toHaveProperty('state');
+ expect(step).toHaveProperty('next');
+ expect(step.description).toHaveProperty('key');
+ expect(Array.isArray(step.actions)).toBe(true);
+ expect(step.state).toHaveProperty('listOfNLI');
+ }
+ });
+
+ it('maintains state continuity across steps', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+
+ const steps = Array.from(neurologicalLevelOfInjurySteps(exam));
+
+ for (let i = 1; i < steps.length; i++) {
+ const prevStep = steps[i - 1];
+ const currStep = steps[i];
+
+ expect(currStep.state.exam).toBe(prevStep.state.exam);
+ expect(currStep.state.currentIndex).toBeGreaterThanOrEqual(
+ prevStep.state.currentIndex,
+ );
+ }
+ });
+
+ it('stops when result.continue is false', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ exam.left.lightTouch.C3 = '0';
+ exam.right.pinPrick.C3 = '0';
+
+ const steps = Array.from(neurologicalLevelOfInjurySteps(exam));
+ const lastStep = steps[steps.length - 1];
+
+ expect(lastStep.next).toBeNull();
+ expect(lastStep.state.listOfNLI).toContain('C2');
+ });
+
+ it('adds INT when reaching S4_5', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+
+ const steps = Array.from(neurologicalLevelOfInjurySteps(exam));
+ const lastStep = steps[steps.length - 1];
+
+ expect(lastStep.state.listOfNLI).toContain('INT');
+ });
+ });
+
+ /* *************************************** */
+ /* Integration Tests */
+ /* *************************************** */
+
+ describe('determineNeurologicalLevelOfInjury integration', () => {
+ describe('sensory-only scenarios', () => {
+ it('stops at C2 when C3 sensory is impaired', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ exam.left.lightTouch.C3 = '0';
+ exam.right.pinPrick.C3 = '0';
+
+ const result = determineNeurologicalLevelOfInjury(exam);
+
+ expect(result).toBe('C2');
+ });
+
+ it('continues through sensory regions when intact', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ exam.left.lightTouch.T6 = '0';
+ exam.right.pinPrick.T6 = '0';
+
+ const result = determineNeurologicalLevelOfInjury(exam);
+
+ expect(result).toBe('T5');
+ });
+
+ it('stops at S2 when S3 sensory is impaired', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ exam.left.lightTouch.S3 = '0';
+ exam.right.pinPrick.S3 = '0';
+
+ const result = determineNeurologicalLevelOfInjury(exam);
+
+ expect(result).toBe('S2');
+ });
+ });
+
+ describe('motor region scenarios', () => {
+ it('stops at C5 when C5 motor is impaired', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ exam.left.motor.C5 = '3';
+ exam.right.motor.C5 = '3';
+
+ const result = determineNeurologicalLevelOfInjury(exam);
+
+ expect(result).toBe('C5');
+ });
+
+ it('stops at L3 when L3 motor is impaired', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ exam.left.motor.L3 = '4';
+ exam.right.motor.L3 = '4';
+
+ const result = determineNeurologicalLevelOfInjury(exam);
+
+ expect(result).toBe('L3');
+ });
+
+ it('handles bilateral sensory and motor evaluation at C6', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ exam.left.motor.C6 = '3';
+ exam.right.motor.C6 = '5';
+
+ const result = determineNeurologicalLevelOfInjury(exam);
+
+ expect(result).toBe('C6');
+ });
+ });
+
+ describe('complete intact (INT) scenarios', () => {
+ it('returns INT when all levels are intact', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+
+ const result = determineNeurologicalLevelOfInjury(exam);
+
+ expect(result).toBe('INT');
+ });
+ });
+
+ describe('variable flag propagation', () => {
+ it('handles NT sensory values', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ // Set NT on lightTouch
+ exam.left.lightTouch.C4 = 'NT';
+ // Stop by impairing motor
+ exam.left.motor.C5 = '0';
+ exam.right.motor.C5 = '0';
+
+ const result = determineNeurologicalLevelOfInjury(exam);
+
+ // Algorithm processes NT values and stops at C4
+ expect(result).toBeDefined();
+ expect(result.length).toBeGreaterThan(0);
+ });
+
+ it('handles motor variable values', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ // Set motor with * marker
+ exam.left.motor.C7 = '3*';
+ exam.right.motor.C7 = '3*';
+
+ const result = determineNeurologicalLevelOfInjury(exam);
+
+ // Algorithm processes variable motor and stops at C7
+ expect(result).toContain('C7');
+ });
+
+ it('handles NT values in complete exam', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ // Set one NT value
+ exam.left.lightTouch.S2 = 'NT';
+
+ const result = determineNeurologicalLevelOfInjury(exam);
+
+ // Algorithm reaches INT with NT values present
+ expect(result).toContain('INT');
+ });
+ });
+
+ describe('multiple levels scenarios', () => {
+ it('returns single level when only one level added', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ exam.left.motor.C8 = '3';
+ exam.right.motor.C8 = '3';
+
+ const result = determineNeurologicalLevelOfInjury(exam);
+
+ expect(result).toBe('C8');
+ expect(result.split(',').length).toBe(1);
+ });
+
+ it('can return multiple levels if added throughout iteration', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ // This scenario would require specific exam data that causes
+ // multiple levels to be added - which is rare in the algorithm
+
+ const result = determineNeurologicalLevelOfInjury(exam);
+
+ // Most scenarios result in single level or INT
+ expect(result).toBeDefined();
+ });
+ });
+
+ describe('special motor cases', () => {
+ it('handles C4 (before cervical key muscles)', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ exam.left.motor.C5 = '0';
+ exam.right.motor.C5 = '0';
+
+ const result = determineNeurologicalLevelOfInjury(exam);
+
+ expect(result).toBe('C4');
+ });
+
+ it('handles L1 (before lumbar key muscles)', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ exam.left.motor.L2 = '2';
+ exam.right.motor.L2 = '2';
+
+ const result = determineNeurologicalLevelOfInjury(exam);
+
+ expect(result).toBe('L1');
+ });
+
+ it('handles T1 (end of cervical key muscles)', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ exam.left.motor.T1 = '3';
+ exam.right.motor.T1 = '3';
+
+ const result = determineNeurologicalLevelOfInjury(exam);
+
+ expect(result).toBe('T1');
+ });
+
+ it('handles S1 (end of lumbar key muscles)', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ exam.left.motor.S1 = '4';
+ exam.right.motor.S1 = '4';
+
+ const result = determineNeurologicalLevelOfInjury(exam);
+
+ expect(result).toBe('S1');
+ });
+ });
+
+ describe('step-by-step matches original behavior', () => {
+ it('matches expected output for cervical injury', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ exam.left.motor.C6 = '3';
+ exam.right.motor.C6 = '3';
+
+ const stepsResult = Array.from(neurologicalLevelOfInjurySteps(exam));
+ const lastStep = stepsResult[stepsResult.length - 1];
+ const directResult = determineNeurologicalLevelOfInjury(exam);
+
+ expect(lastStep.state.listOfNLI.join(',')).toBe(directResult);
+ });
+
+ it('matches expected output for thoracic injury', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ propagateSensoryValueFrom(exam.left, 'T8', '0');
+ propagateSensoryValueFrom(exam.right, 'T8', '0');
+
+ const stepsResult = Array.from(neurologicalLevelOfInjurySteps(exam));
+ const lastStep = stepsResult[stepsResult.length - 1];
+ const directResult = determineNeurologicalLevelOfInjury(exam);
+
+ expect(lastStep.state.listOfNLI.join(',')).toBe(directResult);
+ });
+
+ it('matches expected output for lumbar injury', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+ exam.left.motor.L4 = '4';
+ exam.right.motor.L4 = '4';
+
+ const stepsResult = Array.from(neurologicalLevelOfInjurySteps(exam));
+ const lastStep = stepsResult[stepsResult.length - 1];
+ const directResult = determineNeurologicalLevelOfInjury(exam);
+
+ expect(lastStep.state.listOfNLI.join(',')).toBe(directResult);
+ });
+
+ it('matches expected output for INT', () => {
+ const exam = newEmptyExam();
+ exam.left = newNormalSide();
+ exam.right = newNormalSide();
+
+ const stepsResult = Array.from(neurologicalLevelOfInjurySteps(exam));
+ const lastStep = stepsResult[stepsResult.length - 1];
+ const directResult = determineNeurologicalLevelOfInjury(exam);
+
+ expect(lastStep.state.listOfNLI.join(',')).toBe(directResult);
+ expect(directResult).toBe('INT');
+ });
+ });
+ });
+});
diff --git a/src/classification/neurologicalLevelOfInjury/neurologicalLevelOfInjury.ts b/src/classification/neurologicalLevelOfInjury/neurologicalLevelOfInjury.ts
index b239389..3b64ec5 100644
--- a/src/classification/neurologicalLevelOfInjury/neurologicalLevelOfInjury.ts
+++ b/src/classification/neurologicalLevelOfInjury/neurologicalLevelOfInjury.ts
@@ -1,14 +1,50 @@
-import { Exam, MotorLevels, SensoryLevel, SensoryLevels } from "../../interfaces";
-import { levelIsBetween, CheckLevelResult } from "../common";
-import { checkSensoryLevel } from "../neurologicalLevels/sensoryLevel";
-import { checkMotorLevel, checkMotorLevelBeforeStartOfKeyMuscles } from "../neurologicalLevels/motorLevel";
+import {
+ Exam,
+ MotorLevels,
+ SensoryLevel,
+ SensoryLevels,
+} from '../../interfaces';
+import { levelIsBetween, CheckLevelResult } from '../common';
+import { checkSensoryLevel } from '../neurologicalLevels/sensoryLevel';
+import {
+ checkMotorLevel,
+ checkMotorLevelBeforeStartOfKeyMuscles,
+} from '../neurologicalLevels/motorLevel';
+import { createStep, Step, StepHandler } from '../common/step';
-export const checkLevelWithoutMotor = (level: SensoryLevel, leftSensoryResult: CheckLevelResult, rightSensoryResult: CheckLevelResult, variable: boolean): CheckLevelResult => {
+/* *************************************** */
+/* Types */
+/* *************************************** */
+
+export type NeurologicalLevelOfInjuryState = {
+ exam: Exam;
+ listOfNLI: string[];
+ variable: boolean;
+ currentIndex: number;
+};
+
+export type NeurologicalLevelOfInjuryStepHandler =
+ StepHandler;
+export type NeurologicalLevelOfInjuryStep =
+ Step;
+
+/* *************************************** */
+/* Check Functions (Preserved) */
+/* *************************************** */
+
+export const checkLevelWithoutMotor = (
+ level: SensoryLevel,
+ leftSensoryResult: CheckLevelResult,
+ rightSensoryResult: CheckLevelResult,
+ variable: boolean,
+): CheckLevelResult => {
let resultLevel;
if (leftSensoryResult.level || rightSensoryResult.level) {
if (
- leftSensoryResult.level && rightSensoryResult.level &&
- leftSensoryResult.level.includes('*') && rightSensoryResult.level.includes('*')
+ leftSensoryResult.level &&
+ rightSensoryResult.level &&
+ leftSensoryResult.level.includes('*') &&
+ rightSensoryResult.level.includes('*')
) {
resultLevel = level + '*';
} else {
@@ -18,33 +54,53 @@ export const checkLevelWithoutMotor = (level: SensoryLevel, leftSensoryResult: C
return {
continue: leftSensoryResult.continue && rightSensoryResult.continue,
level: resultLevel,
- variable: variable || leftSensoryResult.variable || rightSensoryResult.variable,
- }
-}
+ variable:
+ variable || leftSensoryResult.variable || rightSensoryResult.variable,
+ };
+};
-export const checkLevelWithMotor = (exam: Exam, level: SensoryLevel, sensoryResult: CheckLevelResult, variable: boolean): CheckLevelResult => {
+export const checkLevelWithMotor = (
+ exam: Exam,
+ level: SensoryLevel,
+ sensoryResult: CheckLevelResult,
+ variable: boolean,
+): CheckLevelResult => {
const i = SensoryLevels.indexOf(level);
- const index = i - (levelIsBetween(i,'C4','T1') ? 4 : 16);
+ const index = i - (levelIsBetween(i, 'C4', 'T1') ? 4 : 16);
const motorLevel = MotorLevels[index];
const nextMotorLevel = MotorLevels[index + 1];
- const leftMotorResult = level === 'C4' || level === 'L1' ?
- checkMotorLevelBeforeStartOfKeyMuscles(exam.left, level, nextMotorLevel, variable) :
- level === 'T1' || level === 'S1' ?
- checkMotorLevel(exam.left, motorLevel, motorLevel, variable) :
- checkMotorLevel(exam.left, motorLevel, nextMotorLevel, variable);
- const rightMotorResult = level === 'C4' || level === 'L1' ?
- checkMotorLevelBeforeStartOfKeyMuscles(exam.right, level, nextMotorLevel, variable) :
- level === 'T1' || level === 'S1' ?
- checkMotorLevel(exam.right, motorLevel, motorLevel, variable) : // TODO: hot fix
- checkMotorLevel(exam.right, motorLevel, nextMotorLevel, variable);
+ const leftMotorResult =
+ level === 'C4' || level === 'L1'
+ ? checkMotorLevelBeforeStartOfKeyMuscles(
+ exam.left,
+ level,
+ nextMotorLevel,
+ variable,
+ )
+ : level === 'T1' || level === 'S1'
+ ? checkMotorLevel(exam.left, motorLevel, motorLevel, variable)
+ : checkMotorLevel(exam.left, motorLevel, nextMotorLevel, variable);
+ const rightMotorResult =
+ level === 'C4' || level === 'L1'
+ ? checkMotorLevelBeforeStartOfKeyMuscles(
+ exam.right,
+ level,
+ nextMotorLevel,
+ variable,
+ )
+ : level === 'T1' || level === 'S1'
+ ? checkMotorLevel(exam.right, motorLevel, motorLevel, variable)
+ : checkMotorLevel(exam.right, motorLevel, nextMotorLevel, variable);
let resultLevel;
if (leftMotorResult.level || rightMotorResult.level || sensoryResult.level) {
if (
- leftMotorResult.level && rightMotorResult.level &&
- (leftMotorResult.level.includes('*') || rightMotorResult.level.includes('*'))
+ leftMotorResult.level &&
+ rightMotorResult.level &&
+ (leftMotorResult.level.includes('*') ||
+ rightMotorResult.level.includes('*'))
) {
resultLevel = level + '*';
} else {
@@ -52,46 +108,227 @@ export const checkLevelWithMotor = (exam: Exam, level: SensoryLevel, sensoryResu
}
}
- return !sensoryResult.continue
- ? {...sensoryResult, level: resultLevel}
+ return !sensoryResult.continue
+ ? { ...sensoryResult, level: resultLevel }
: {
- continue: leftMotorResult.continue && rightMotorResult.continue,
- level: resultLevel,
- variable: variable || sensoryResult.variable || leftMotorResult.variable || rightMotorResult.variable,
- }
+ continue: leftMotorResult.continue && rightMotorResult.continue,
+ level: resultLevel,
+ variable:
+ variable ||
+ sensoryResult.variable ||
+ leftMotorResult.variable ||
+ rightMotorResult.variable,
+ };
+};
+
+/* *************************************** */
+/* Step Handler Functions */
+/* *************************************** */
+
+/**
+ * Step 1: Initialize Neurological Level of Injury calculation
+ * Initialize state: empty levels list, variable=false, currentIndex=0
+ */
+export function initializeNLIIteration(
+ state: NeurologicalLevelOfInjuryState,
+): NeurologicalLevelOfInjuryStep {
+ return createStep(
+ {
+ key: 'neurologicalLevelOfInjuryInitializeNLIIterationDescription',
+ },
+ [
+ {
+ key: 'neurologicalLevelOfInjuryInitializeNLIIterationAction',
+ },
+ ],
+ state,
+ {
+ listOfNLI: [],
+ variable: false,
+ currentIndex: 0,
+ },
+ checkLevel,
+ );
+}
+
+/**
+ * Step 2: Check neurological level at current level
+ * Evaluate bilateral sensory and motor function (when applicable)
+ */
+export function checkLevel(
+ state: NeurologicalLevelOfInjuryState,
+): NeurologicalLevelOfInjuryStep {
+ const level = SensoryLevels[state.currentIndex];
+ const nextLevel = SensoryLevels[state.currentIndex + 1];
+ const i = state.currentIndex;
+
+ // Handle S4_5 (end of iteration)
+ if (!nextLevel) {
+ return createStep(
+ {
+ key: 'neurologicalLevelOfInjuryCheckLevelDescription',
+ params: { levelName: level },
+ },
+ [
+ {
+ key: 'neurologicalLevelOfInjuryCheckLevelReachedS4_5Action',
+ },
+ ],
+ state,
+ {
+ listOfNLI: [...state.listOfNLI, 'INT' + (state.variable ? '*' : '')],
+ },
+ null,
+ );
+ }
+
+ // Bilateral sensory checks
+ const leftSensoryResult = checkSensoryLevel(
+ state.exam.left,
+ level,
+ nextLevel,
+ state.variable,
+ );
+ const rightSensoryResult = checkSensoryLevel(
+ state.exam.right,
+ level,
+ nextLevel,
+ state.variable,
+ );
+
+ let result: CheckLevelResult;
+ let checkType: 'sensory' | 'motor';
+
+ // Determine if this is a motor region
+ if (levelIsBetween(i, 'C4', 'T1') || levelIsBetween(i, 'L1', 'S1')) {
+ checkType = 'motor';
+ const sensoryResult = checkLevelWithoutMotor(
+ level,
+ leftSensoryResult,
+ rightSensoryResult,
+ state.variable,
+ );
+ result = checkLevelWithMotor(
+ state.exam,
+ level,
+ sensoryResult,
+ state.variable,
+ );
+ } else {
+ checkType = 'sensory';
+ result = checkLevelWithoutMotor(
+ level,
+ leftSensoryResult,
+ rightSensoryResult,
+ state.variable,
+ );
+ }
+
+ // Update variable and levels
+ const variable = state.variable || result.variable;
+ const newLevels = result.level
+ ? [...state.listOfNLI, result.level]
+ : [...state.listOfNLI];
+
+ // Determine next step
+ const next: NeurologicalLevelOfInjuryStepHandler | null = result.continue
+ ? checkLevel
+ : null;
+
+ // Build description and actions
+ const description = {
+ key: 'neurologicalLevelOfInjuryCheckLevelDescription' as const,
+ params: { levelName: level },
+ };
+
+ const actions = [];
+
+ if (checkType === 'sensory') {
+ actions.push({
+ key: 'neurologicalLevelOfInjuryCheckLevelSensoryOnlyAction' as const,
+ });
+ } else {
+ actions.push({
+ key: 'neurologicalLevelOfInjuryCheckLevelMotorRegionAction' as const,
+ });
+ }
+
+ if (result.level) {
+ actions.push({
+ key: 'neurologicalLevelOfInjuryCheckLevelAddLevelAction' as const,
+ params: { levelName: result.level },
+ });
+ }
+
+ if (result.continue) {
+ actions.push({
+ key: 'neurologicalLevelOfInjuryCheckLevelContinueAction' as const,
+ });
+ } else {
+ actions.push({
+ key: 'neurologicalLevelOfInjuryCheckLevelStopAction' as const,
+ });
+ }
+
+ return createStep(
+ description,
+ actions,
+ state,
+ {
+ listOfNLI: newLevels,
+ variable,
+ currentIndex: result.continue
+ ? state.currentIndex + 1
+ : state.currentIndex,
+ },
+ next,
+ );
+}
+
+/* *************************************** */
+/* Main Entry and Generator */
+/* *************************************** */
+
+/**
+ * Creates initial state for neurological level of injury calculation
+ */
+export function getInitialState(exam: Exam): NeurologicalLevelOfInjuryState {
+ return {
+ exam,
+ listOfNLI: [],
+ variable: false,
+ currentIndex: 0,
+ };
}
+/**
+ * Determine neurological level of injury
+ * Returns comma-separated string of levels (e.g., "C5", "T3*", "S3,INT", "INT*")
+ */
export const determineNeurologicalLevelOfInjury = (exam: Exam): string => {
- const listOfNLI = [];
- let variable = false;
- for (let i = 0; i < SensoryLevels.length; i++) {
- const level = SensoryLevels[i];
- const nextLevel = SensoryLevels[i + 1];
- let result: CheckLevelResult = {
- continue: true,
- variable: false,
- };
+ const initialState = getInitialState(exam);
+ let step = initializeNLIIteration(initialState);
- if (!nextLevel) {
- listOfNLI.push('INT' + (variable ? '*' : ''));
- } else {
- const leftSensoryResult = checkSensoryLevel(exam.left, level, nextLevel, variable);
- const rightSensoryResult = checkSensoryLevel(exam.right, level, nextLevel, variable);
-
- if (levelIsBetween(i,'C4','T1') || levelIsBetween(i,'L1','S1')) {
- const sensoryResult = checkLevelWithoutMotor(level, leftSensoryResult, rightSensoryResult, variable);
- result = checkLevelWithMotor(exam, level, sensoryResult, variable);
- } else {
- result = checkLevelWithoutMotor(level, leftSensoryResult, rightSensoryResult, variable);
- }
- variable = variable || result.variable;
- if (result.level) {
- listOfNLI.push(result.level);
- }
- if (!result.continue) {
- break;
- }
- }
+ while (step.next) {
+ step = step.next(step.state);
+ }
+
+ return step.state.listOfNLI.join(',');
+};
+
+/**
+ * Generator that yields each step of the neurological level of injury calculation
+ * Enables step-by-step execution for UI display
+ */
+export function* neurologicalLevelOfInjurySteps(
+ exam: Exam,
+): Generator {
+ const initialState = getInitialState(exam);
+ let step = initializeNLIIteration(initialState);
+ yield step;
+
+ while (step.next) {
+ step = step.next(step.state);
+ yield step;
}
- return listOfNLI.join(',');
}
diff --git a/src/en.ts b/src/en.ts
index 4c7e66b..6738d17 100644
--- a/src/en.ts
+++ b/src/en.ts
@@ -135,4 +135,23 @@ export default {
'Continue to next level.',
motorLevelCheckLevelStopAction:
'Add {{levelName}} and stop.',
+ // Neurological Level of Injury
+ neurologicalLevelOfInjuryInitializeNLIIterationDescription:
+ 'Initialize Neurological Level of Injury calculation. Iterate from C1 toward S4_5.',
+ neurologicalLevelOfInjuryInitializeNLIIterationAction:
+ 'Set levels list to empty, variable to false.',
+ neurologicalLevelOfInjuryCheckLevelDescription:
+ 'Check neurological level at {{levelName}}.',
+ neurologicalLevelOfInjuryCheckLevelSensoryOnlyAction:
+ 'Sensory-only region. Evaluate bilateral sensory function (LT and PP on both sides).',
+ neurologicalLevelOfInjuryCheckLevelMotorRegionAction:
+ 'Motor region. Evaluate bilateral sensory and motor function.',
+ neurologicalLevelOfInjuryCheckLevelAddLevelAction:
+ 'Add {{levelName}} to NLI.',
+ neurologicalLevelOfInjuryCheckLevelContinueAction:
+ 'Bilateral function intact. Continue to next level.',
+ neurologicalLevelOfInjuryCheckLevelStopAction:
+ 'Bilateral function not intact. Stop iteration.',
+ neurologicalLevelOfInjuryCheckLevelReachedS4_5Action:
+ 'Reached S4_5. All levels have intact bilateral function. Add INT.',
};