LMG / NCAM Census Translation Engine v3.5.0

Turns any carrier census spreadsheet into a ready-to-upload LaunchMyGroup file — plus an NCAM quoting sheet — right inside Excel. Your census data never leaves your workbook.

Latest version: v3.5.0 (build 2026-10-09). To see which version you have, run the script and look at the SUMMARY tab — if it shows an older number, jump to Keep it up to date below (takes about 20 seconds).

① Put the script into Excel (one time, about a minute)

  1. Click the blue Copy script button below. The whole script is now on your clipboard.
  2. Open Excel — desktop app or excel on the web — signed in with your Humana account. Any workbook is fine.
  3. Click the Automate tab in the ribbon at the top (to the right of View). If you don't see it, open the workbook in Excel on the web instead.
  4. Click New Script. A panel opens on the right with a small sample script in it.
  5. Click anywhere inside that code, press Ctrl+A (select all), then Ctrl+V (paste). The sample is replaced with this engine.
  6. At the top of the panel, click the name Script 1, rename it to LMG NCAM Census Engine, then click Save script.

You only do this once. The script is saved to your Office account, so it shows up in the Automate tab of every workbook you open from now on.

View the script code (you don't need to read it — the Copy button grabs all of it)

Quote-only script (NCAM Quote Engine)

Same engine, run separately for quotes: one line per employee, dependents removed (they only show up as the Dental/Vision tier), no plan names, basic life or voluntary life. Output tab: QUOTE. Install it the same way and name it NCAM Quote Engine.

View the quote script code

② Run it on a census

  1. Open the workbook that contains the census you want to convert.
  2. Click the sheet tab that holds the raw census data, so it's the active sheet.
  3. Go to Automate → find LMG NCAM Census Engine in the script gallery → click it → click Run.
  4. When it finishes you land on the LMG tab — that's your upload file (always sheet #1; NCAM quoting is #2). Review the VALIDATION ISSUES tab before uploading.

Safe to re-run as many times as you like: the first run saves your original data to a RAW DATA tab and never touches it again — later runs reprocess from that untouched copy.

③ Keep it up to date (about 20 seconds)

The script doesn't update itself — when a new version ships, you paste the new copy over the old one:

  1. On this page, click Copy script (step ① above).
  2. In Excel: Automate → click LMG NCAM Census Engine → Edit (opens the code panel).
  3. Click inside the code, Ctrl+A, Ctrl+V, then Save script. Done.

Want to know when a new version is out? Sign up for update emails below — no need to keep checking this page.

Try it on a sample first

Three deliberately messy test censuses — run the script on one of these before pointing it at a real file, so you know what to expect.

Flat sample — triggers most cleanup rules (split address, spelled-out states, hourly pay, a retiree, a bad row, a flagged duplicate).
⬇ SAMPLE-census.xlsx

Stacked sample — one person spread across several product rows, with dependents stored in columns; the tool collapses each person to one clean row.
⬇ SAMPLE-stacked-census.xlsx

Product-coded sample — the hard case: dental and vision share one generic "Coverage Tier" column, told apart by a "Plan Type" column; the tool routes each tier to the right product.
⬇ SAMPLE-product-coded.xlsx

Found a problem? Want update emails?

🐛 Report a problem

Something mapped wrong, flagged wrong, or errored? Describe what happened — the census layout, what you expected, what you got. Don't paste real employee data (names/SSNs) — column headers are fine.

📧 Get update emails

Leave your name and email and pick what you want to hear about. No spam — just news about this tool.

For power users

Ask Jev about unmapped headers

Paste the SOURCE HEADER values the HEADER MAP tab left unmapped or only suggested, one per line. Jev (a typed classifier, not a chatbot) answers with the LMG field it thinks each one is and how sure it is. Nothing is applied for you: tick the rows you agree with and send them to the mappings form below. ≥ 90% — auto 70–89% — suggest < 70% — ask

Contribute learned mappings

As you confirm columns on the HEADER MAP tab, copy that tab's SHARE column (each line is Source Header | LMG Field) and paste it below. Good mappings get folded into the built-in dictionary so future versions recognize them automatically — for everyone.

Docs & downloads

Read the User Guide inline
# LMG / NCAM Census Translation Engine: User Guide (v3.1.0)

A one-click Excel tool that converts a carrier census into the LaunchMyGroup
(LMG) upload format and a derived NCAM quoting sheet, with full validation and
an audit trail. It runs **inside Excel** — no install, no Python, no website.

---

## 1. What it does

Point it at a messy census file and press **Run**. In a few seconds your
workbook is rebuilt into eight tabs:

| Tab | What it is |
|-----|------------|
| **LMG** | Your clean enrollment file, ready to upload. Always tab #1. |
| **NCAM** | Employees-only sheet in quoting layout. |
| **SUMMARY** | Counts by relationship and by plan tier (EE/ES/EC/FAM/W). |
| **VALIDATION ISSUES** | Every problem found, rated ERROR / WARNING / INFO, with the source row on RAW DATA and the row on the LMG tab. |
| **PROCESSING LOG** | A record of every transformation it performed. |
| **RAW DATA** | An untouched copy of your original census. Never modified. |
| **Other Columns** | Every column kept out of the LMG upload, with the reason — blank LMG fields, unrecognized source headers, and duplicate columns. The data still lives on RAW DATA. |
| **HEADER MAP** | Columns the tool could not match exactly, with a suggested field. Type the right LMG field and re-run; your choices are remembered (section 5b). |

The **LMG** and **NCAM** tabs are colored green; the rest are navy. On the
**LMG** tab, any cell with a validation issue is highlighted by severity
(red = error, amber = warning, blue = info), so problems jump out at a glance.

It standardizes headers, merges split address fields, converts state names to
2-letter codes, cleans phones to 10 digits, formats dates to MM/DD/YYYY,
normalizes gender to M/F, converts coverage to EE/ES/EC/FAM/W, abbreviates long
job titles, annualizes hourly wages, and more.

**It also reshapes the two "nightmare" layouts before cleaning:** if a person is
split across several rows (one per line of coverage — Dental, Vision, Life…), it
**collapses them into one row**; if dependents are stored sideways as columns
(`Dependent 1 Name`, `Dependent 2 …`), it **explodes them into their own
Spouse/Child rows**. It only reshapes when it's confident, and flags anything it's
unsure about. Try `SAMPLE-stacked-census.xlsx` to see it. A census where dental
and vision share one generic `Coverage Tier` column, told apart by a `Plan Type`
column, is routed product by product (`SAMPLE-product-coded.xlsx`).

**Family roll-up:** dependents belong to the employee row above them. Their
voluntary life and dependent basic life move up to the employee row, and the
employee's dental/vision tier is raised (never lowered) when a spouse or child
is enrolled in that product. Mismatches are flagged, not fixed.

---

## 1b. Rolling this out to your team

**The Automate tab (Office Scripts) is enabled on the Humana network** —
confirmed — so this is the right primary tool for the team. Everyone uses their
Humana M365 account in Excel Online or desktop Excel.

**Share one copy so everyone runs the same logic.** Two mechanisms, with an
important difference:

- **Share into a workbook** (script's **⋯** menu → **Share**): attaches the
  script to *that workbook only* — anyone opening that workbook can run it, but
  it doesn't follow them to other files. Fine for a shared template; not a
  team-wide rollout.
- **Save the script to a shared SharePoint/Teams library** (script's **⋯**
  menu → **Save As** → pick a SharePoint site your team can access): everyone
  adds that location once in **Automate → All Scripts** and runs the *same
  file* in any workbook. When the engine updates, you paste the new version
  into that one script and **the whole team is updated — no re-pasting for
  anyone else.** This is the recommended way to manage versions for a team.

If neither is available on your tenant, everyone maintains their own copy and
updates it from the hub page (see section 2c — it takes ~20 seconds).

**Fallback (off-network / non-Humana users):** the offline HTML cleaner
(`CensaScrub/lmg_census_cleaner_offline.html`) and the web Cleaner run in any
browser and keep data on the machine. **Their output does not match this script
yet:** they have not been brought up to `CENSUS-RULES.md` v2.1 (tier handling,
waivers, salary, family roll-up and more differ). Use this script when you can,
and review a Cleaner file carefully before uploading it.

## 2. One-time setup (≈2 minutes)

You need **Excel for the web** or **Excel on desktop with a Microsoft 365
work account** (the **Automate** tab must be visible on the ribbon).

1. Open the census workbook in Excel.
2. Click the **Automate** tab on the ribbon → **New Script**.
3. Delete the sample code in the editor.
4. Open `LMG-NCAM-Census-Engine.ts`, copy **all** of it, and paste it in.
5. Rename the script to `LMG NCAM Census Engine` and click **Save script**.

That script now lives in your account. For every future census you only do
section 3.

---

## 2b. Confirming you're on the latest version (for testing)

The script carries a version number. When it runs, that version is printed on the
**SUMMARY** tab (`Engine version`) and as the first line of the **PROCESSING LOG**
tab. The hub page (lmg-ncam-engine.netlify.app) shows the latest version at the
top. **If the version in your output doesn't match the hub, re-copy the script.**
This is how we stay lined up while testing.

## 2c. Updating to a new version (≈20 seconds)

The script doesn't update itself. When the hub shows a newer version:

1. On the hub page, click **Copy script**.
2. In Excel: **Automate** tab → click **LMG NCAM Census Engine** → **Edit**.
3. Click inside the code, **Ctrl+A** (select all), **Ctrl+V** (paste),
   then **Save script**.

You can also sign up on the hub page to get an email when a new version ships.

## 3. Running it on a census (every time)

1. Open the census workbook.
2. Make sure the tab with the raw census data is the **active** tab
   (just click it). The engine reads from the active tab, or from a tab named
   `RAW DATA` if one already exists.
3. **Automate** tab → find `LMG NCAM Census Engine` → **Run**.
4. When it finishes, you'll land on the **LMG** tab.

**Always review the VALIDATION ISSUES tab before you upload.** See section 5.

It is safe to run more than once. The first run snapshots your original to
`RAW DATA`; every later run reprocesses from that untouched snapshot, so you
can fix the source and re-run without losing anything.

---

## 4. What gets cleaned (the rules)

- **Headers** — parenthetical text removed (`RELATIONSHIP (EE, SP, CH)` →
  `RELATIONSHIP`); common variants mapped to LMG field names (`SSN`,
  `Soc Sec No`, `Tax ID` → the SSN column).
- **Relationship** → `Employee`, `Spouse`, `Domestic Partner`, `Child`.
  Unknown values (e.g. `Retiree`) are flagged, not guessed. If the census has
  **no Relationship column at all**, every row is treated as an Employee (one
  INFO note says so).
- **Names** — Last/First UPPERCASE, Middle Initial trimmed to one letter,
  Relationship in Proper Case. Apostrophes and hyphens are kept (`O'BRIEN`,
  `SMITH-JONES`); other punctuation becomes a space.
- **Gender** — `Male`→`M`, `Female`→`F`. Anything else is flagged.
- **Dates** (DOB, hire, prior-carrier) → `MM/DD/YYYY`. Handles slashes,
  dashes, ISO, 2-digit years, and Excel date serials. A 2-digit year is read as
  20YY unless that is after this year, then 19YY (`05/05/28` → 1928). Impossible
  dates (`02/30/1980`) and unparseable dates are flagged. A date of birth in the
  future is an ERROR; anyone over 100 and an employee under 14 are WARNINGs; a
  hire date in the future, over 100 years ago, or before age 14 is a WARNING.
- **SSN** — digits only, must be 9. Employees missing an SSN are flagged.
- **Address** — `Address 1` + `Address 2` + `Address 3` merged into one
  `Member Street Address`, UPPERCASE.
- **State** — full names → USPS (`Texas`→`TX`). Invalid states flagged.
- **Zip** — first 5 digits; restores a dropped leading zero.
- **Phone** — digits only, 10 digits; strips a leading country `1`. Bad
  phones are flagged and left blank rather than uploaded wrong.
- **Salary** — `$50,000` → `50000`. `$25.50/hr` with 40 hours →
  `25.50 × 40 × 52 = 53040`. A salary under $1,000 is flagged as a possible
  hourly rate.
- **Hours** — blank employee defaults to `40`.
- **Occupation** — UPPERCASE, common abbreviations (`SENIOR`→`SR`,
  `ADMINISTRATOR`→`ADMIN`, …), max 25 characters. A title that is still too
  long is cut at the last whole word that fits and flagged as a WARNING that
  shows the shortened title.
- **Disabled?** — blank defaults to `No`.
- **Coverage tier** — normalized to `EE / ES / EC / FAM / W`. A tier the tool
  does not recognize (e.g. `EE+1`) is kept exactly as written and flagged as a
  WARNING; fix it before uploading.
  - On **LMG**, a waived dental/vision tier becomes
    `Waive with other coverage`; a blank employee tier (when the group
    carries that product) is also forced to `Waive with other coverage`.
    That product's Plan column is left blank (the original stays on RAW DATA).
  - On **NCAM**, those convert back to `W` (because NCAM defaults blanks to EE).
- **Basic Life, Dependent Basic Life, STD, LTD**: read `EE` or `Waive`.
  `Waive`, `Declined`, `None`, `N`, `No`, `0` and `False` all mean `Waive`;
  `Enroll`, `Yes`, `Y`, `EE` mean `EE`.
- **Primary Beneficiary** — if an employee is enrolled in Basic Life or has an
  Employee Voluntary Life amount but no beneficiary, it's set to `ON FILE`.
  Employees who waived life get no `ON FILE`.
- **Voluntary Life** — employee blank/waive amounts become `0`.

---

## 5. Reading the VALIDATION ISSUES tab

| Severity | Meaning | Do before upload? |
|----------|---------|-------------------|
| **ERROR** | Missing/invalid required data (no SSN, bad DOB, no gender). | **Yes — fix.** |
| **WARNING** | Likely wrong but not blocking (odd salary, unknown relationship, **possible duplicate**). | Review. |
| **INFO** | FYI only (e.g. spousal vol-life on a dependent row). | Optional. |

Each issue lists the **source row** (the row on RAW DATA; when several stacked
rows were collapsed into one person you see all of them, e.g. `2, 3, 4`), the
**LMG row**, member name, column, the offending value, the problem, and a
recommended action. The SUMMARY tab's Errors / Warnings / Info counts match this
tab exactly. Fix the issues on your source tab and re-run,
or fix them directly on the LMG tab before uploading.

## 5b. Mapping columns the tool didn't recognize (HEADER MAP tab)

When the tool isn't sure where a column belongs, it doesn't guess silently — it
puts it on the **HEADER MAP** tab. To include such a column:

1. Open the **HEADER MAP** tab. Each row is a column the tool couldn't match
   exactly, with a **SUGGESTED** field, a **CONFIDENCE** %, and a blank
   **MAP TO LMG FIELD** cell.
2. Click the **MAP TO LMG FIELD** cell and pick the LMG field it should become
   from the dropdown (e.g. `Dental Cov Level`). Every available field is also
   listed in column **H**, **LMG FIELDS (Map To options)**.
3. **Re-run** the script. The column is now mapped and included.

The tool reads your **MAP TO** entries first (they override everything), and they
**persist** — the next file from the same carrier maps cleanly without redoing it.
A very close match (≥85%) is applied automatically and marked "verify" so you can
check it; clear its **MAP TO** cell to undo.

### Duplicates are flagged, not deleted
If two rows share the same last name + first name + DOB + gender, the engine
flags them as a WARNING. It does **not** delete a row — you decide which to
remove. This is deliberate: an upload file should never silently drop an
enrollee.

---

## 6. Uploading to LaunchMyGroup

The **LMG** tab is your upload file and is always worksheet #1 (LMG uploads the
first sheet only). Either upload the workbook as-is, or right-click the **LMG**
tab → **Move or Copy** → *(new book)* to export just that sheet.

---

## 7. Known limits in v3.1.0 (by design)

- **Families are linked by row order.** A dependent belongs to the nearest
  Employee row above it; there is no linking by subscriber SSN yet.
- **Duplicates are flagged, not merged.**
- **Relationship codes `S`, `C`, `D`** are not recognized; they are flagged and
  the row is treated as a non-employee. Spell them out (`SP`, `CH`, `Spouse`).
- **Short or long zip codes** are padded or cut to 5 digits without a flag.
- **NCAM** uses the spec's minimum quoting columns plus Salary/Occupation/STD/
  LTD when disability coverage is present.
- **The web Cleaner's output differs** from this script until it is rebuilt on
  the same rules (see section 1b).

The fastest way to improve the tool is to run real census files through it and
report the edge cases on the hub page.

---

## 8. If something looks wrong

1. Check **PROCESSING LOG** — it lists what the engine did.
2. Check **RAW DATA** — confirm the original value.
3. Note the case, the column, the source value, and the wrong output, and bring
   that example back to your AI assistant with the reference blurb
   (`AI-REFERENCE-BLURB.md`). Real examples improve the tool far faster than
   guessing at carrier formats up front.
Read the Rulebook (canonical spec) inline
# Census Translation Rules — Canonical Spec v2.4

**This document is the single source of truth for every LMG/NCAM census tool.**
The Office Script, the CensaScrub Python tools, and the offline HTML cleaner
should each implement *these* rules so they produce identical output and don't
drift apart. When a rule changes, change it **here first**, bump the version,
then update each implementation.

| Implementation | Repo / file | Runtime | Audience |
|----------------|-------------|---------|----------|
| LMG/NCAM Office Script | `lmg-ncam-engine/LMG-NCAM-Census-Engine.ts` | Excel Automate tab (M365) | Excel + LMG uploaders, zero-install |
| Python CLI/GUI | `CensaScrub/censascrub_v001.py`, `censascrub_gui.py` | Windows desktop | Local power users |
| Web / offline HTML cleaner | `CensaScrub/site/index.html` (lmgcensus.netlify.app, SSPWest login) + `CensaScrub/lmg_census_cleaner_offline.html` (same engine, no account) | Any browser | **Team upload site; offline file is the universal fallback** |
| ~~Web app~~ (archived 2026-06-29) | `_archive/census-data-refiner/` — dead, never deployed; not a sync target | — | — |

Column source of truth: `CensaScrub/LMG_Column_Dictionary.xlsx` (LMG platform
field names + alias variants).

---

## 1. Workbook / output structure

Seven outputs, in this order. **LMG must be worksheet #1** (LaunchMyGroup uploads
the first sheet only).

1. `LMG` — full enrollment upload (employees, spouses, domestic partners, children). Tab #1, colored `#5C9A1B`.
2. `NCAM` — employees-only quoting sheet derived from LMG. Tab #2, colored `#5C9A1B`.
3. `SUMMARY` — counts by relationship and by plan tier.
4. `VALIDATION ISSUES` — ERROR / WARNING / INFO with source row, LMG row, member, column, value, issue, action. The source row is the row on `RAW DATA` (when the pre-pass collapsed stacked rows, every source row is listed, e.g. `2, 3, 4`; an exploded dependent shows its employee's source row); the LMG row is the row on the `LMG` tab.
5. `PROCESSING LOG` — audit trail of every transformation.
6. `RAW DATA` — untouched copy of the original census. Never modified.
7. `Other Columns` — every column **not** in the LMG upload, each with a reason: entirely-blank LMG fields (removed because blank columns break the upload), unrecognized source headers, and duplicate/unused source columns. Two columns: COLUMN, WHY IT IS NOT IN THE LMG UPLOAD. Tab colored Plum `#AF0061`. All other secondary tabs are colored `#003057`.

**Blank-column removal:** after processing, any LMG output column with no data in
any row is dropped from the `LMG` sheet and its name listed on `Other Columns`.
**Exception — REQUIRED columns are never dropped** (Relationship, Member Last
Name, Member First Name, Gender, Date of Birth, State); if a required column is
entirely blank it is kept and flagged ERROR. NCAM and SUMMARY read from the full
(unfiltered) records, so they are unaffected.

**Validation highlighting:** each issue's offending cell is highlighted on the
`LMG` tab by severity — ERROR `#FFC7CE`, WARNING `#FFEB9C`, INFO `#BDD7EE`
(most-severe color wins per cell; duplicate flags highlight the whole row). The
SEVERITY column on `VALIDATION ISSUES` is shaded the same colors.

A tool may read its source from `RAW DATA` if present (idempotent re-runs),
else from the active/first data sheet.

## 2. LMG output columns (canonical order)

**One dictionary for every tool** (owner decision 2026-09-10 — the web cleaner's
names, from the LMG column dictionary). 41 columns, in this order:

MEMBER INFORMATION — Relationship, Member Last Name, First Name, Middle Initial,
Gender, Date of Birth, Member Street Address, City, State, Zip Code, Phone Number,
Social Security Number, Status Field, Date of Hire, Email Address.
(`Status Field` was `Employee Status` from v2.0 to v2.3; owner decision 2026-10-09.)

ADDITIONAL DETAILS — Hours Worked Per Week, Salary, Occupation, Disabled?,
Billing Division.

BENEFITS INFORMATION — Dental Cov Level, Dental Plan Elected, DHMO Provider Name,
Dental Prior Current Carrier Name, Dental Prior Carrier Effective Date,
Dental Prior Carrier Term Date, Dental Prior Carrier Ortho included,
Vision Cov Level, Vision Plan Election,
Basic Life Cov Level, Life/AD&D Class, Primary Life Beneficiary,
Dependent Basic Life,
Employee Voluntary Life Flat Amount, Spouse Voluntary Life Flat Amount,
Dependent Voluntary Life Flat Amount,
Short-Term Disability Cov Level, STD Class, Long-Term Disability Cov Level, LTD Class,
Working Location.

Notes: COV/COVERAGE → **Cov Level** (tier `EE` / `ES` / `EC` / `FAM` / waived; Dental/Vision waived prints `Waive with other coverage`, §5);
Plan/Select/Elect → Plan Elected / Plan Election. **Basic Life Cov Level,
Dependent Basic Life, Short-Term / Long-Term Disability Cov Level read `EE` /
`Waive`** (owner decision 2026-09-10 — was Enroll/Waive). Waiver words for these
fields: Waive / Waived / Decline / None / N/A / Opt Out **and `N`, `No`, `0`,
`False`** (v2.1); Enroll / Elect / Yes / Y / E / EE / Active → `EE`. SSN is kept on the
employee only. `Member Last Name` becomes `Last Name` on the NCAM tab. Voluntary
Life columns are omitted when blank/0/Waive for everyone (§6). Superseded names
(`Dental Coverage Type`, `STD`, `Employee Status`, `Dental Coverage Tier`, `STD
Election`, …) stay in every tool's alias table so old files still map.

## 3. Header normalization

- Remove parenthetical text: `RELATIONSHIP (EE, SP, CH)` → `RELATIONSHIP`.
- Unbalanced parens: if a stray `)` remains after stripping (e.g. `Status
  (Active, COBRA, Disabled ) If COBRA, provide Loss of Event Date and Event
  Type)`), everything from the first `(` onward is instruction text — keep only
  the text before the first `(` (→ `STATUS`).
- Uppercase, strip punctuation to spaces, collapse whitespace.
- A bare `Coverage Tier` / `Effective Date` / `Term Date` header is **not** an
  alias (v2.1): a generic tier belongs to §11 Stage 0 product routing, and a plain
  effective/term date is usually the coverage date, not the prior carrier's.
  Without a product column they are only suggested on HEADER MAP.
- Map normalized header to the LMG field. Priority: **user-confirmed HEADER MAP
  entry > exact alias > high-confidence fuzzy match (≥85% word+character
  similarity, auto-applied with a verify flag) > suggestion (50–85%, listed but
  not applied) > unrecognized**. Exact matches never get flagged.
- **HEADER MAP tab:** every non-exact column is listed with a suggested field +
  confidence and an editable `Map To` cell. Fill `Map To` with an LMG field and
  re-run to include the column. Entries persist across runs (the tool learns).
- **No column leaves the LMG upload silently.** Any source column the tool does
  not recognize — or recognizes but can't use because an earlier column already
  filled that LMG field (e.g. both `Home Phone` and `Cell`) — is **flagged
  WARNING on VALIDATION ISSUES** and **listed on Other Columns**, while its raw
  values are always preserved on `RAW DATA`. The user decides; the tool never
  drops a column without surfacing it.

## 4. Field rules

| Field | Rule |
|-------|------|
| Relationship | → `Employee` / `Spouse` / `Domestic Partner` / `Child`. Unknown (e.g. `Retiree`) = flag, keep original. **No Relationship column at all** (common on quoting censuses) → every row is `Employee`, one INFO flag (v2.1). |
| Last / First name | UPPERCASE. Missing = ERROR. |
| Middle Initial | First character only, UPPERCASE. |
| Gender | `Male`→`M`, `Female`→`F`. Else flag (blank/unknown = ERROR for enrollees). |
| Date of Birth | → `MM/DD/YYYY`. Accept slashes/dashes/ISO/2-digit-year/Excel serial. Two-digit year → `20YY` unless that is after the current year, else `19YY`. The day is checked against the month (`02/30` is invalid). Unparseable or missing = ERROR. **Plausibility (v2.1):** future date = ERROR; over 100 = WARNING; an employee under 14 = WARNING. |
| Date of Hire | Employees only (blanked on dependents). → `MM/DD/YYYY`. Future hire date, hire over 100 years ago, or hired before age 14 = WARNING. |
| Prior carrier eff/term dates | → `MM/DD/YYYY`. |
| SSN | **Kept on the EMPLOYEE only** — removed from spouse / domestic partner / child. Digits only. If fewer than 9 digits, **pad leading zeros to 9** (INFO spot-check). More than 9 digits = ERROR. Employee missing SSN = ERROR. |
| Member Street Address | Merge Address 1/2/3 (and Addr/Street variants) with single spaces, UPPERCASE. |
| City | UPPERCASE. |
| State | Output header is **`State`** (not "Member State"). Full name → USPS 2-letter (`Texas`→`TX`). Invalid = flag; employee missing state = ERROR. **Required — never dropped.** |
| Zip Code | First 5 digits; restore dropped leading zero. |
| Phone Number | Digits only, 10 digits; strip leading country `1`. Invalid = flag + leave blank. **Multiple phone columns coalesce per row by priority: personal/home/generic → cell/mobile → work/office.** |
| Email Address | **Multiple email columns coalesce per row by priority: personal/home/generic → work/office.** |
| Salary | Strip `$` and commas. Hourly → `rate × Hours Worked Per Week × 52`, using the member's **actual** weekly hours (e.g. 20 → `rate × 20 × 52`), default 40 only when hours blank/invalid (INFO flag). **Hourly vs annual is decided by a Compensation Type / Salary Type column when present** (Hourly/Annual), else by detecting "/hr" in the salary cell. Salary < $1,000 = flag as possible hourly. Non-numeric = flag. |
| Hours Worked Per Week | Use the value as given. Blank/invalid employee → `40` (fallback only). |
| Occupation | UPPERCASE; apply abbreviation table; max 25 chars: if still longer, truncate at the last whole word that fits (hard cut only for a single word over 25) + WARNING showing the shortened title. (v2.1: the vowel-drop was removed; it made titles unreadable.) |
| Disabled? | Blank → `No`. Y/N variants → `Yes`/`No`. |
| Combined columns | A single `Name` / `City, State ZIP` / full-address column is split into the separate fields per §11c **before** the field rules above apply. |

### Abbreviation table (occupation)
SENIOR→SR, JUNIOR→JR, INFORMATION TECHNOLOGY→IT, SYSTEM(S)→SYS,
ADMINISTRATOR/ADMINISTRATION→ADMIN, SUPPORT→SUP, TECHNICIAN→TECH,
REPRESENTATIVE→REP, BUSINESS→BIZ, OPERATIONS→OPS, MANAGER→MGR, MANAGEMENT→MGMT,
ASSISTANT→ASST, DIRECTOR→DIR, ENGINEER→ENGR, SUPERVISOR→SUPV, DEVELOPER→DEV,
COORDINATOR→COORD, SPECIALIST→SPEC, GENERAL→GEN.

### Coverage vs Plan columns
- A column containing **COV / COVERAGE** (e.g. `Dental Coverage`, `Vision Cov
  Level`) is the **Cov Level** (coverage tier) — the election EE/ES/EC/FAM/Waive.
- A column containing **Plan / Select / Elect** (e.g. `Dental Plan`, `Vision
  Select`, `Dental Election`) is the **Plan Elected** — which plan to enroll on.
- A Dental/Vision **Plan** column that holds only **one distinct value** is left
  off the upload (the system auto-selects the single plan); it's noted on Other
  Columns.

## 5. Coverage normalization

Normalize any tier value to one of: `EE`, `ES`, `EC`, `FAM`, `Waive`.

- Employee only / Self → `EE`.
- Employee + Spouse **or** Domestic Partner → `ES`.
- Employee + Child(ren) → `EC`.
- Employee + Spouse/DP + Child(ren) → `FAM`.
- Waive / Waived / Decline / None / Opt Out → `Waive`.
- Unrecognized → flag, keep original. On LMG the original value is output as-is
  (e.g. `EE+1`), never turned into `Waive`, and the family roll-up does not
  overwrite it.

### LMG-specific
- Dental/Vision tier waived → output text `Waive with other coverage` (owner
  decision 2026-09-27, reverting 2026-09-10 for Dental/Vision only). Every other
  coverage column (Basic Life, Dependent Basic Life, STD, LTD) stays `Waive` (§2).
- Blank **employee** dental/vision tier, **when the group carries that product**,
  → `Waive with other coverage` (LMG would otherwise treat blank as EE).
- Dental/Vision waived → that product's **Plan** column (`Dental Plan Elected` /
  `Vision Plan Election`) is left **blank** on LMG; a non-blank source plan is
  flagged INFO and stays on RAW DATA.

### NCAM-specific (reverse)
- `Waive` (any value starting with WAIV) → `W`.
- Blank employee dental/vision → `W` (NCAM defaults blanks to EE).

## 6. Life rules

- Primary Beneficiary: if an employee is **actually enrolled in life** (Basic Life
  elected and not waived, and/or a real Employee Voluntary Life amount) and the
  beneficiary is blank → `ON FILE`. Employee rows only. A waiver (`Waive`, `No`,
  `N`, …) never gets `ON FILE`; spouse/child vol life alone does not trigger it (v2.1).
- Employee & Spousal Voluntary Life: blank or waive → `0`. Employee rows only.
- **Dependent Voluntary Life: blank, `0`, or waive → `Waive`** (not `0`).
- A Voluntary Life column (Employee / Spousal / Dependent) is **dropped from the
  LMG output entirely if it is blank or `0` for every row** — it only appears when
  there are real elections. (Dropped columns are listed on Other Columns.)
- Spousal Voluntary Life found on a dependent row → flag INFO (roll-up to
  employee is a future version; handle manually for now).

## 7. NCAM derivation

From LMG, employees only. `Member Last Name` → `Last Name`.
Columns: Last Name, Gender, Date of Birth, Dental Coverage Type,
Vision Coverage Type. **If STD or LTD coverage exists**, also include:
Salary, Occupation, STD, STD Class, LTD, LTD Class (values as on LMG: EE/Waive,
class number). Counts must reconcile to LMG. Implemented by the Office Script,
the web/offline cleaner (`CensaEngine.buildNcam`) and the Python tool.

## 7b. Quote mode (NCAM Quote Engine)

A second script, generated from the same source with `QUOTE_MODE = true`, for
quotes rather than LMG uploads. It runs the same pre-pass (§11, §11b, §11c),
normalization and validation, then writes a **QUOTE** tab (#1) instead of LMG,
NCAM and Other Columns:

- **One line per employee.** Spouse / domestic partner / child rows are purged;
  they appear only as the derived Dental / Vision tier (EE / ES / EC / FAM / W).
- Columns, in order, each left off when blank for every employee (Last Name and
  Gender always kept; Date of Birth kept when there is no Age either): Last Name,
  First Name, Gender, Date of Birth, Age, Dental Coverage Type, Vision Coverage
  Type, Salary, Occupation, Date of Hire, Short-Term Disability Cov Level, STD
  Class, Long-Term Disability Cov Level, LTD Class, City, State, Zip Code.
- Not on the quote: plan names, Basic Life, Dependent Basic Life, voluntary life,
  SSN, address, phone, email (NCAM assumes every listed employee for
  non-contributory lines).
- An **Age** column is read (alias `Age`, `Current Age`, …); with Age present a
  missing DOB is not an ERROR in quote mode.
- VALIDATION ISSUES lists only issues on the quote's columns, Relationship and
  whole-row (duplicate) flags; the number left out is written to PROCESSING LOG.
- Running either script removes the other's output tabs, so a workbook never
  mixes LMG and QUOTE results.

## 8. Duplicates

Key = Last Name + First Name + Date of Birth + Gender. Duplicates are
**flagged** as WARNING in VALIDATION ISSUES — **never silently merged or
deleted**. An upload file must never silently drop an enrollee. The user
reviews and removes. (Family-unit-aware merge is a planned future version.)

## 8b. Special characters & extra validation flags

- **Special characters** anywhere in the census are replaced with a space; `#` →
  ` UNIT `. Exempt: date fields, Email Address, numeric fields (SSN, Phone,
  Zip, Salary, Hours, Voluntary Life) and Dental/Vision Cov Level (so an
  unrecognized tier stays verbatim for review).
- **Names and plan/class values keep apostrophes and hyphens** (v2.1): Member Last
  Name, First Name, Middle Initial, Dental Plan Elected, Vision Plan Election,
  Life/AD&D Class, STD Class and LTD Class. `O'Brien` → `O'BRIEN` (not `O BRIEN`),
  `Smith-Jones` → `SMITH-JONES`, `PPO-1500` stays `PPO-1500`. Curly apostrophes
  become straight ones; other punctuation in these fields still becomes a space.
- **Age 65+**: any member (employee or dependent) 65 or older is flagged.
- **Implausible dates** (v2.1): see §4 Date of Birth / Date of Hire.
- **Voluntary life not in even thousands** (e.g. 62,500) is flagged.
- **Family consistency** (flag only, never auto-fixed): a dependent listed but not
  covered by any tier → flag; a tier that implies a dependent not listed (ES w/o
  spouse, EC w/o child, FAM missing either) → flag (row-listing censuses only).

## 9. Validation severities

- **ERROR** — missing/invalid required data (no SSN, bad/missing DOB, missing gender). Fix before upload.
- **WARNING** — likely wrong but not blocking (odd salary, unknown relationship/tier, possible duplicate, over-long occupation, bad phone).
- **INFO** — FYI (e.g. spousal vol-life on a dependent row).

## 10. Guiding principle for variable carrier formats

Carriers, payroll vendors, and TPAs each format census files differently; for the
most part their columns match the alias logic above, but it varies. The tool
**auto-handles the normal patterns and FLAGS anything out of the norm for the end
user to decide — it never guesses silently.** When a structural detection isn't
confident, it does not transform; it flags the rows and passes them through. This
extends the no-silent-loss rule from values to structure.

## 11. Pivot pre-pass (IMPLEMENTED)

Runs BEFORE normalization, reshaping stacked/sideways layouts into one clean
enrollee row per person, then hands off to the existing pipeline. Transforms only
when confident; flags anything out of the norm.

- **Stage 0 — product-coded routing.** When a row has a generic value column
  (Coverage Tier / Coverage Level / Coverage Type, Plan, Election, Class) plus a
  product column (Plan Type / Product = Dental / Vision / Basic Life / STD / LTD),
  the generic value is moved into the product-specific LMG field for that row's
  product *before* the collapse: TIER→Dental/Vision Coverage Tier, PLAN→Dental/
  Vision Plan Elected, ELECTION→Basic Life/STD/LTD Election, CLASS→Basic Life/STD/
  LTD Class. Unknown products are left alone (flagged). Output stays
  ancillary-focused — Medical/Medicare generic values are not routed/output.
- **Stage 1 — collapse stacked product rows.** Gated on a Product column
  (Product / Coverage / Coverage Type / Plan Type / Benefit / Line of Coverage).
  Groups rows by person (SSN + last + first + DOB), merges each person's non-blank
  cells into one row (so the Dental row's tier, the Vision row's tier, etc. land on
  one line). Conflicting non-blank values for the same person/column are flagged
  WARNING, not silently overwritten. The existing duplicate check is the post-pivot
  fail-safe.
- **Stage 2 — explode dependent columns.** Detects `Dependent N <field>` groups
  (Name/SSN/DOB/Gender/Relationship, split or combined name; index N to any depth).
  Each populated dependent becomes its own row under the employee, inheriting the
  employee's address/city/state/zip/location to tie the family. Missing dependent
  relationship → defaulted to Child + INFO flag. A comma-less combined name is split
  "First Last" with an INFO flag to verify. (The dependent combined-name split
  shares the same name parser as §11c — identical suffix and middle-initial rules.)
- **Vertical census (v2.4).** The product column is found by header, or, when the
  header is unknown, by its values (80%+ of filled cells name a product, 2+
  different products). A column whose values are tiers (Employee Only, Family,
  Waived...) is the shared tier column whatever its header (Election, Option...).
  Routing: tier → Dental / Vision Cov Level, and on Basic Life / STD / LTD rows →
  that coverage's Cov Level (a tier there means enrolled = `EE`; a waiver = `Waive`);
  class → Life/AD&D / STD / LTD Class; a shared amount column on Voluntary Life
  rows → Employee Voluntary Life (the §11b roll-up moves a spouse's/child's).
  Per-product cost/premium columns are not used. Values with no output column
  (voluntary life tier, basic life amount, medical, unknown products) are counted
  in one INFO per product and stay on RAW DATA.
- The product column and the shared columns are listed on HEADER MAP as **used by
  the vertical-census pivot** (and on Other Columns with the reason). They are never
  shown as unrecognized or asked for a mapping: they cover several coverages, so
  there is no one field to map them to.

## 11b. Family roll-up (IMPLEMENTED)

After the rows are built, families are grouped by **adjacency** — an Employee row
plus the Spouse/Domestic Partner/Child rows beneath it, until the next Employee.
Onto the employee row the engine rolls: spouse's voluntary life → Spousal
Voluntary Life; child's → Dependent Voluntary Life; dependent basic life →
Dependent Basic Life (those are blanked on the dependent rows). The employee's
Dental/Vision Coverage Type is **derived per product from who is enrolled in that
product** (a dependent who waived that product does NOT count) — spouse+child →
FAM, spouse → ES, child → EC. Upgrade-only: never lowers a stated tier.
Demographics stay on the dependent rows. Every roll-up is flagged INFO to verify.

**Only employee-line data is uploaded.** After the roll-up, spouse / domestic
partner / child rows have all election, employment, AND address/contact fields
blanked (coverage types, plans, life, disability, beneficiary, salary, occupation,
hire, location, street/city/state/zip, phone, email). Dependents keep only
Relationship, name, Middle Initial, gender, DOB, and Disabled?.

## 11c. Combined-column split — "data extrapolation" (IMPLEMENTED)

Some carriers consolidate several fields into ONE column. This split runs as the
**first stage of the pre-pass** (before §11 Stage 0 product routing, collapse,
and dependent explode, so person-keying sees the split First/Last), detects
combined columns by header + value shape, and extrapolates them into the
separate canonical fields. Per §10: transform only when confident; otherwise
flag and pass through untouched.

### Shapes

| # | Shape | Example values | Split into |
|---|-------|----------------|------------|
| 1 | Combined name | `Smith, John A` / `Jane Doe` | Member Last Name, First Name, Middle Initial |
| 2 | City/State/Zip | `Austin, TX 78701` / `Round Rock TX 78664-1234` | City, State, Zip Code |
| 3 | Full address | `123 Main St, Apt 4, Austin, TX 78701` | Member Street Address, City, State, Zip Code |

### Detection (header candidates + value-shape confidence)

Confidence = fraction of **non-blank** cells in the column that parse (full
scan). Thresholds: **60%** for a named combined header (`SPLIT_CONFIRM`),
**90%** for a value-shape-only trigger on a plainly-named column
(`SPLIT_STRONG`), **70%** for full address, which is always value-gated
(`SPLIT_ADDR`). Columns with fewer than 5 non-blank values require **100%**
parse for value-shape-only triggers. Between **30%** (`SPLIT_HINT`) and the
threshold → WARNING flag, **no transform** (the column passes through as
today). Below 30% → nothing.

- **Shape 1 — combined name.** Header candidates: `NAME`, `FULL NAME`,
  `EMPLOYEE NAME`, `EMP NAME`, `MEMBER NAME`, `ENROLLEE NAME`,
  `SUBSCRIBER NAME`, `PARTICIPANT NAME`, and other `* NAME` headers except
  FIRST/LAST/MIDDLE/CARRIER/PLAN/PROVIDER/BENEFICIARY/PCP/OFFICE/FACILITY
  names. **Guard:** if separate First AND Last Name columns exist, the combined
  column is left alone (INFO note) — the explicit columns win.
- **Shape 2 — City/State/Zip.** Header candidates: `CITY STATE ZIP`,
  `CITY ST ZIP`, `CSZ`, `CITY STATE`, `CITY AND STATE` (zip optional when the
  header lacks ZIP). Also triggers on a plain `CITY`/`TOWN` header when ≥90% of
  values are full `City, ST 12345` shapes (zip + valid state required).
- **Shape 3 — full address.** Candidate columns: `ADDRESS`, `FULL ADDRESS`,
  `HOME ADDRESS`, `MAILING ADDRESS` and Address-column variants. Header alone
  is never sufficient (an `Address` column is normally street-only): requires
  ≥70% of values to parse as street + city + valid state + zip with a
  comma-delimited city. **Guard:** skipped entirely when separate City AND
  State AND Zip columns exist; only the first qualifying column splits (other
  address columns keep merging into the street per §4).

### Parsing rules

- **Name:** a trailing suffix (JR/SR/II/III/IV/V, with or without periods) is
  detached and re-appended to the last name. Comma present → `Last, First
  [Middle]` (reliable). No comma → guessed: 2 tokens = `First Last`; 3 tokens
  with a 1-character middle = `First MI Last`; 3+ tokens otherwise = first word
  is First Name, the rest is Last Name (protects `Mary Van Buren`); 1 token =
  Last Name only + WARNING.
- **City/State/Zip and full address parse right-to-left:** trailing zip
  (`12345` or `12345-6789`), then a trailing valid USPS 2-letter code or full
  state name (longest match first — `DISTRICT OF COLUMBIA`), remainder = city
  (full address: last comma segment = city, the rest = street). Values are
  stored raw; the normal §4 field rules (UPPERCASE, USPS conversion, zip-5)
  apply downstream.

### No silent loss

- The consumed combined column is removed from mapping but its raw values stay
  on `RAW DATA` (Office Script) / the source file (siblings), and every
  executed split is logged and flagged INFO with a parsed-row count.
- In a confidently-split column, a row that fails to parse keeps its **whole
  original value** in the primary target field (City for shape 2, Member
  Street Address for shape 3, Member Last Name for a one-word name) + WARNING.
- Comma-less name rows get ONE column-level INFO with a count ("split as
  'First [MI] Last' — verify"), not a flag per row.
- Low-confidence columns (30%–threshold) are flagged WARNING and passed
  through unchanged for the user to decide.

## 12. Roadmap (not yet implemented)

- **v2+** — duplicate merge (currently flagged), per-product dependent enrollment
  for tier derivation, family linking by subscriber-SSN / sequence-number (today:
  adjacency), beneficiary refinements.
- **v3** — richer NCAM column selection + reconciliation guarantees.
- **v4** — expanded SUMMARY (per-plan, per-tier, enrolled/waived/eligible).

---

### Changelog
- **v2.4** (2026-10-09, Office Script v3.5.0): vertical census routing for
  Basic Life / STD / LTD tiers, value-based product and tier column detection,
  pivot columns labeled on HEADER MAP (§11); LMG field `Employee Status` renamed
  `Status Field` in every tool (§2).
- **v2.3** (2026-10-09, Office Script v3.4.0): §7b quote mode — NCAM Quote
  Engine script, QUOTE tab, one line per employee, `Age` alias.
- **v2.2** (2026-09-27, Office Script v3.3.0): Dental/Vision waived tier on LMG
  is `Waive with other coverage` again (other coverages stay `Waive`), and the
  waived product's Plan column is blank on LMG (§5). NCAM still reads `W`.
- **v2.1** (2026-09-19, Office Script v3.1.0): fixes from the 2026-09-19 audit:
  no Relationship column → every row Employee + INFO (§4); bare `Coverage Tier`,
  `Effective Date`, `Term Date` removed from the alias table and the duplicate
  `Basic Life Class` alias removed (it maps to Life/AD&D Class) (§3); unrecognized
  tier kept verbatim on LMG, never `Waive` (§5); `N` / `No` / `0` / `False` are
  waivers for the Enroll/Waive fields and `ON FILE` only for employees enrolled in
  life (§2, §6); names and plan/class values keep `'` and `-` (§8b); DOB/DOH
  plausibility, current-year two-digit pivot and calendar check (§4); occupation
  word-boundary truncation (§4); VALIDATION ISSUES shows the RAW DATA source row
  plus the LMG row, and the SUMMARY issue tally counts every issue (§1). The web
  Cleaner and Python tools have **not** been updated to v2.1 yet; their output can
  differ from the Office Script until they are.
- **v2.0** (2026-09-10) — **one column dictionary + one vocabulary for all tools**
  (owner decision): §2 is now the web cleaner's 41-column LMG list (was the 40-name
  Office-Script list; Python had a third set); elections read `EE`/`Waive` (was
  Enroll/Waive); waived tier text is `Waive` (was `Waive with other coverage`);
  Python tiers are codes (was `Employee + Spouse` …); Python/cleaner tab #1 is `LMG`
  (was CENSUS); `Life/AD&D Class` added to the Office Script. Office Script v3.0.0,
  cleaner Build 3. Old names remain as aliases everywhere. If LMG rejects an upload
  on a header or value, that is the first thing to revisit.
- **2026-09-10** (implementation note, spec unchanged) — §7 NCAM derivation is now
  implemented by the web cleaner (lmgcensus.netlify.app, `CensaScrub/site/index.html`
  + the offline HTML) and the Python CLI/GUI, not only the Office Script. Known
  drift still open: the cleaner and Python tools keep their own LMG column
  dictionaries and tier vocabularies (e.g. `Dental Cov Level` / `Dental Coverage Tier`
  vs the §2 canonical `Dental Coverage Type`; CENSUS tab vs LMG tab; `Waive` vs
  `Waive with other coverage`) — reconciling to §2 is an owner decision, not done silently.
- **v1.1** (2026-07-06) — added §11c combined-column split ("data
  extrapolation"): combined Name, City/State/Zip, and full-address columns are
  split into separate canonical fields; §4 combined-columns row; dependent
  name split now shares the §11c name parser.
- **v1.0** (2026-06-29) — initial canonical spec, extracted from the LMG/NCAM
  Office Script and the Copilot requirements transcript.
Read the AI Reference Blurb inline
# Future-AI Reference Blurb

Paste this into any AI chat (Claude, Copilot, ChatGPT) when you need help
adjusting the tool. It gives the assistant the full context in one shot.

---

We are maintaining the **LMG / NCAM Census Translation Engine** (v3.1.0), an Excel
**Office Script** (TypeScript) for Humana ancillary census processing. The rules
it implements are in `CENSUS-RULES.md` (v2.1), the canonical spec. It
converts a carrier census into two outputs:

1. **LMG** — LaunchMyGroup enrollment upload format.
2. **NCAM** — employees-only quoting format derived from LMG.

**Delivery:** a single Office Script pasted into Excel → Automate tab → Run. It
operates on the open workbook. No backend, no install.

**Output workbook tabs:** `LMG` (always worksheet #1 — LMG uploads the first
sheet only), `NCAM`, `SUMMARY`, `VALIDATION ISSUES`, `PROCESSING LOG`,
`RAW DATA` (untouched original), `Other Columns` (every column not in the upload,
with the reason), `HEADER MAP` (user-confirmed column mappings, remembered across
runs).

**Source of truth for columns:** the LMG platform field list and alias variants
live in `LMG_Column_Dictionary.xlsx` (CensaScrub project). Output columns follow
that dictionary's order and names.

**Key processing rules:**
- Headers: strip parenthetical text; map common variants to LMG field names.
- Relationship → Employee / Spouse / Domestic Partner / Child (unknown = flag).
  No Relationship column at all → every row is Employee + one INFO flag.
- Names: Last/First UPPERCASE; Middle Initial = 1 char; Relationship Proper Case.
  Names and plan/class values keep apostrophes and hyphens (O'BRIEN); other
  special characters become a space.
- Gender → M / F (else flag).
- Dates (DOB, hire, prior-carrier) → MM/DD/YYYY; handles slashes/dashes/ISO/
  2-digit years/Excel serials. 2-digit year → 20YY unless after the current year,
  else 19YY; day checked against month. DOB in the future = ERROR; over 100 or an
  employee under 14 = WARNING; hire date in the future / before age 14 = WARNING.
- SSN: 9 digits, digits only; employee missing SSN = error.
- Address 1/2/3 merged → MEMBER STREET ADDRESS (UPPERCASE).
- State names → USPS 2-letter; invalid = flag.
- Zip → first 5 digits; restore dropped leading zero.
- Phone → 10 digits (strip leading country 1); invalid = flag + blanked.
- Salary: strip $ and commas; `$X/hr` annualized as rate × hours × 52
  (hours default 40); salary < $1,000 flagged as possible hourly.
- Occupation: UPPERCASE, abbreviation table, max 25 chars (truncate at a word
  boundary + WARNING).
- Disabled? blank → No.
- Coverage tier → EE / ES / EC / FAM / W. Unrecognized (e.g. EE+1) → kept
  verbatim + WARNING, never turned into Waive.
  - LMG: waived dental/vision → `Waive with other coverage` and that product's Plan column blank; blank employee
    tier (if the group carries that product) → `Waive with other coverage`. Elections (Basic Life / Dep Basic Life / STD / LTD) read `EE` / `Waive`; N / No / 0 / False are waivers. Column names = CENSUS-RULES §2 shared dictionary.
  - NCAM: convert those back to `W` (NCAM defaults blanks to EE).
- Primary Beneficiary: employee enrolled in Basic Life or with a real Employee
  Voluntary Life amount, blank bene → `ON FILE` (never for a waiver).
- Voluntary Life: employee/spouse blank/waive → 0; dependent blank/0/waive → Waive.
  All-0 columns are dropped.

**Pre-pass (reshaping, runs before normalization):**
- Stage S splits combined Name, City/State/Zip and full-address columns.
- Stage 0 routes a generic Coverage Tier / Plan / Class column into the
  product-specific fields using a Plan Type / Product column.
- Stage 1 collapses stacked product rows (same person across many rows, one per
  coverage line) into one row — gated on a Product column; groups by SSN+name+DOB;
  merges non-blank cells; flags value conflicts.
- Stage 2 explodes `Dependent N <field>` columns into Spouse/Child rows under the
  employee, inheriting address/location; flags defaulted relationships and guessed
  name splits.
- Reshapes only when confident; flags anything out of the norm. Governing rule:
  auto-handle the normal patterns, flag the rest, never guess silently.

**Family roll-up (after the per-row pass):** dependents join the Employee row
above them; dependent vol life and dependent basic life move up to the employee;
dental/vision tiers are raised (never lowered) for enrolled dependents; family
mismatches are flagged, not fixed.

**Validation:** ERROR (missing/invalid required), WARNING (likely wrong),
INFO (FYI). Columns: SEVERITY, SOURCE ROW (RAW DATA), LMG ROW, MEMBER NAME,
COLUMN, VALUE, ISSUE, RECOMMENDED ACTION. The SUMMARY tally counts every issue.

**Deliberate deviation from earlier spec:** duplicates (same last+first+DOB+
gender) are **flagged, not silently merged** — an upload file must never drop an
enrollee silently. The user reviews and removes.

**NCAM columns:** Last Name, Gender, Date of Birth, Dental Coverage Type,
Vision Coverage Type; plus Salary, Occupation, Short-Term Disability Cov Level,
STD Class, Long-Term Disability Cov Level, LTD Class **only if** STD or LTD
coverage is present.

**Other tools:** the web Cleaner (lmgcensus) and the Python CensaScrub tool
claim the same spec but do **not** produce identical output yet; treat this
script as the reference until they are rebuilt on the same rules.

**Planned next:** family linking by subscriber SSN (today: row order), duplicate
merge (today: flagged), relationship codes S/C/D, a shared rules core with the
web Cleaner, golden-file tests.

**When I bring you a problem,** I'll paste a real example: the case, the column,
the source value, and the wrong output. Help me adjust the matching script
(`LMG-NCAM-Census-Engine.ts`) — give me the exact edited code block, not
pseudocode.

Duplicates are flagged, not deleted — an upload file should never silently drop an enrollee. Family roll-up (deps → employee line) and product-coded coverage routing are built in.