# mock-exam-runner.mjs: CEH v13 Practice MCQ Runner

A single-file Node.js script (zero npm dependencies, Node ≥ 18) that
administers the CEH v13 practice sets under timed exam conditions and
produces an analytics report: score, verdict, per-domain breakdown,
time-per-question, and a wrong/skip list for review.

## Quick start

```powershell
# Run Set A interactively with the default 75-minute timer
node .\tools\mock-exam-runner.mjs --set a

# Quick sanity check (untimed, JSON, parse only)
node .\tools\mock-exam-runner.mjs --set a --no-timer --json

# Smoke-test the parser end-to-end (loads + parses Sets A+B+C, untimed)
node .\tools\mock-exam-runner.mjs --set all --no-timer --json | Select-Object -First 1
```

## CLI flags

| Flag                         | Default | Meaning                                                                 |
|------------------------------|---------|-------------------------------------------------------------------------|
| `--set a\|b\|c\|all`         | `a`     | Which practice set(s) to load.                                          |
| `--time <minutes>`           | scaled  | Total time budget in minutes. Default = 90 s × question count.          |
| `--shuffle`                  | off     | Randomise question order before the exam.                               |
| `--domain <tag>`             | none    | Restrict to questions whose domain tag contains `<tag>`.                |
| `--json`                     | off     | Output a structured JSON report instead of the human summary.           |
| `--no-timer`                 | off     | Run untimed (practice mode; useful with `--show-explanation`).          |
| `--show-explanation`         | off     | Print the `Why:` rationale after each answer (turn off for real exams). |
| `-h`, `--help`               | -       | Show usage help.                                                        |

## Common examples

```powershell
# Full 125-Q mock under real exam cadence (≈ 75 min, randomised)
node .\tools\mock-exam-runner.mjs --set all --shuffle

# Domain 3.C drill (web/SQL/wireless/cloud/crypto), untimed, with explanations
node .\tools\mock-exam-runner.mjs --set b --domain 3.C --no-timer --show-explanation

# Quick JSON sanity check (no human interaction)
node .\tools\mock-exam-runner.mjs --set all --no-timer --json

# Set C only (the hardest 25), 30 minutes hard cap
node .\tools\mock-exam-runner.mjs --set c --time 30
```

## In-exam controls

| Key       | Action                                                                                  |
|-----------|-----------------------------------------------------------------------------------------|
| `A`-`D`   | Submit answer for the current question.                                                 |
| `s`       | Skip the question (counted as unanswered).                                              |
| `q`       | Quit the run. Partial results are still printed.                                        |
| `Ctrl-C`  | Same as `q`: the runner saves whatever was answered so far before exiting.             |

The terminal shows one question at a time, a live `mm:ss` countdown in the
header line, and (when `--show-explanation` is on) the `Why:` rationale
after each answer.

## Output

### Human-readable (default)

```
═══════════════════════════════════════════════════════════════════
  CEH v13 Mock Exam - Final Report
═══════════════════════════════════════════════════════════════════
  Set:               A
  Total questions:   50
  Attempted:         50
  Correct:           44
  Skipped:           0
  Score:             88.0%
  Verdict:           EXAM READY
  Avg time/Q:        41.2 s
  Median time/Q:     38.5 s

  Per-domain breakdown:
  ──────────────────────────────────────────────────────────────────
    3.a.1 - nmap tcp scan types                 10/10 (100%)
    2.2 - ids/ips rule syntax and tuning         3/3  (100%)  ✓ strong
    ...
    3.b.5 - malware behaviour & mitre att&ck     1/4  ( 25%) ⚠ weak
  ──────────────────────────────────────────────────────────────────

  Questions to review (6):
    Q12  [3.A.5]  gave B, correct D
           The attacker wants to perform an idle/zombie scan against…
    Q24  [3.B.5]  skipped, correct A
           A ransomware family is observed performing ARP scanning…
```

### JSON (`--json`)

```json
{
  "set": "a",
  "totalQuestions": 50,
  "attempted": 50,
  "correct": 44,
  "skipped": 0,
  "pct": 88.0,
  "verdict": "EXAM READY",
  "perDomain": [
    { "domain": "2.2", "total": 3, "attempted": 3, "correct": 3, "skipped": 0, "pct": 100 },
    ...
  ],
  "weakDomains": ["3.b.5", "4.1"],
  "avgSecPerQ": 41.2,
  "medianSecPerQ": 38.5,
  "wrongOrSkip": [
    { "qnum": 12, "domain": "3.A.5", "given": "B", "correct": "D", "stem": "..." }
  ]
}
```

## Verdict thresholds

| Score      | Verdict       | Meaning                                                           |
|-----------:|---------------|-------------------------------------------------------------------|
| ≥ 85 %     | `EXAM READY`  | Comfortable pass margin: schedule the live exam.                 |
| 70 - 84 %  | `BORDERLINE`  | Close, but drill the weak domains before booking.                 |
| < 70 %     | `NOT READY`   | Re-read the relevant domain notes, then re-run the same set.      |

The thresholds are heuristic; the live exam uses EC-Council's own
scaled-scoring rubric, which is *not* a flat percentage.

## How parsing works

The runner expects each `### Q<n>.` block to look like:

```
### Q1. [stem text]
**Domain:** X.X - [topic]
A. [option A]
B. [option B]
C. [option C]
D. [option D]
**Answer: B**
**Why:** [explanation]
```

Multi-line stems (including fenced code blocks such as Q107's packet
capture) are kept verbatim. The runner concatenates continuation lines
into the option they belong to, so an option that wraps onto the next
markdown line still parses as one unit.

Domain tags are normalised to lowercase, with the trailing `- topic`
stripped, so `--domain 3.C` matches tags like `3.C - Web/SQL/Wireless…`
across all three sets.

## Troubleshooting

- **"No TTY detected"**: you ran the script piped to another command
  while still expecting an interactive prompt. Drop the pipe (or use
  `--json --no-timer` for a non-interactive smoke test).
- **Question count off**: the parser silently drops blocks that lack
  an `**Answer: X**` line or any of the A-D options. If you authored
  a new question, make sure those six pieces are all present.
- **Wrong domain matches**: the runner uses a `contains` match. Pass
  a more specific token (e.g. `3.C` rather than `3`) to disambiguate.

## Files

- `mock-exam-runner.mjs`: the runner
- `mock-exam-runner.README.md`: this file
- `..\practice\01-mcq-set-a.md`, `..\practice\02-mcq-set-b.md`,
  `..\practice\03-mcq-set-c.md`: the question banks
- `..\practice\99-answer-key.md`: human-readable consolidated key
  (the runner parses the inline `**Answer:**` line of each set file,
  so the answer-key file is optional for the runner itself).

## License / attribution

Built for the personal CEH v13 study kit. No external data is fetched;
the runner only reads the local `.md` set files.
