---
name: "roster_to_contact_list"
description: "Turn a roster in any form into a verified contact list: photos of sign-in sheets, screenshots, PDFs, and spreadsheets transcribed with band-crop discipline, E.164 phone formatting, vCard 4.0 output, and flag-dont-fix verification. Use when someone hands over a roster, sign-in sheet, class list, team sheet, or attendee list and wants clean contacts back as CSV and VCF."
---

# Roster to Contact List

## Purpose

Turn a messy roster into a contact list a person can trust and import. The
hard part is not typing. The hard part is not inventing. One wrong digit in a
phone number or one guessed letter in a name makes the whole list suspect,
so this skill is built around transcription discipline and verification:
transcribe in bands, flag anything uncertain instead of fixing it by feel,
then read the finished list back against the source before anything ships.

Team neutral by design. It works for a sports team, a class, a crew, an
event sign-in sheet, a club, or a work roster. No firm specific fields, no
assumed country, no assumed system at the far end.

## Workflow

1. **Intake and source triage.** Name the source type before touching it:
   photo of a paper sheet, screenshot, PDF with a text layer, scanned PDF,
   spreadsheet, or CSV. Keep the original file untouched and work on a copy.
   Record three facts up front: who the list belongs to, what country the
   phone numbers are from (ask if it is not written anywhere), and what the
   output will be used for (import to a phone, a CRM, a mailing tool).

2. **Extract the raw text by source type.**
   - Spreadsheet or CSV: read it directly. Map columns, never retype values.
   - PDF with a text layer: pull the text layer first. Scanned PDF: treat it
     like a photo and use step 3.
   - Photo or screenshot: straighten and crop out borders and background
     first. Higher resolution and even light help, but preprocessing can
     also erase thin characters like periods and decimal points, so compare
     one band with and without preprocessing before committing to it.

3. **Band-crop transcription (photos and scans).** Never transcribe a whole
   page in one pass. Divide the image into horizontal bands that follow the
   sheet: one band per ruled section, or three to five rows per band when
   there are no rules. Label bands A, B, C from the top. Work one band at a
   time, zoomed in until the handwriting fills the screen, and transcribe
   row by row, left to right. Give every row a source_row value like B3
   (band B, third row in the band). That label is how a later check finds
   the exact spot on the page in seconds.

4. **Transcribe literally.** Copy what is written, including odd spelling,
   nicknames, and punctuation in names. Do not expand initials, do not
   standardize names, do not fix a domain that looks misspelled. Where a
   character is genuinely ambiguous, write your best reading, add the
   marker `[?]` right after it, and note the alternative in the flag column.
   Common traps to mark, not decide: 0 vs O, 1 vs l vs I, 5 vs S, 8 vs B,
   rn read as m, cl read as d, a handwritten 7 crossed or not, and @ written
   so it reads like a or 2.

5. **Build the intermediate CSV.** One row per person, columns: name,
   phone, email, org, title, note, country_calling_code, source_row, flag.
   Headers, blank rows, and section titles on the sheet do not become
   contacts. Note each skipped line in the flag report so the row count in
   step 7 can be reconciled.

6. **Normalize phones to E.164, with no guessing.** E.164 (ITU-T) format is
   a plus sign, a country code of 1 to 3 digits, then the national number,
   digits only, 15 digits maximum in total, and no country code starts
   with 0. Rules the builder applies:
   - A number already starting with + keeps its digits.
   - A leading 00 converts to +.
   - A national number gets the country code from the row or the stated
     default, and a single leading trunk 0 is dropped (020 in London
     becomes +44 20...). The drop is recorded as a flag.
   - No known country: the number is flagged `phone_no_country_code_known`
     and left out of the vCard. Never default to +1 by habit.
   - An extension is stripped from the number and flagged, not dialed.
   Structural checks catch bad shape only. For real validity, run the
   numbers through Google libphonenumber (Python port: `phonenumbers`,
   parse with a region hint, then `is_valid_number`) when it is installed.

7. **Verify with a full read-back. This step is not optional.** Read the
   finished CSV against the source, band by band, in a different order
   than you transcribed (bottom band up works well, it stops autopilot).
   Check phone digits and email characters one at a time, out loud in your
   head, not as whole strings. Check names letter by letter wherever a
   `[?]` marker or a flag sits. Reconcile the counts: source rows equals
   contact rows plus skipped lines plus flagged unreadable rows, and every
   difference must have a written reason. A list that has not passed this
   step gets labeled UNVERIFIED in the handoff, plain as that.

8. **Build the outputs.** Run the bundled builder:
   `python3 scripts/contact_builder.py roster.csv --outdir out`
   Add `--default-calling-code 44` (or per-row values) when the country is
   confirmed. It writes contacts.csv, contacts.vcf, and flags.json.

9. **Hand off with the flag report first.** Lead with how many rows were
   flagged and why, then the counts from step 7, then the files. A person
   fixing three flagged rows by looking at the sheet is a good outcome.
   A list that hides its guesses is a bad one, however clean it looks.

## Output Contract

Every job delivers three files plus a short report:

- contacts.csv: name, phone_e164, email, org, title, note, source_row,
  flags. One row per person, in source order.
- contacts.vcf: one vCard 4.0 card per named person (RFC 6350: BEGIN:VCARD,
  VERSION:4.0 immediately after, FN, N split as family and given, TEL as a
  tel: URI only when the number passed the E.164 structural check, EMAIL,
  ORG, TITLE, NOTE carrying any flags). Lines over 75 octets are folded
  with CRLF plus one space, and text values escape backslash, comma,
  semicolon, and newline.
- flags.json: total rows, flagged row count, and every flagged row with its
  reasons.
- The report: source type, band count if transcribed, row reconciliation
  from step 7, flag summary, and whether the read-back happened.

Worked example. Input rows:

```
name,phone,email,org,title,note,country_calling_code,source_row,flag
Alex Rivera,(415) 555-2671,alex@example.com,Harbor Crew,Lead,,1,A1,
Sam Okafor,020 7183 8750,sam.okafor@example.co.uk,,,,44,A2,
Jo Lee,555 0142,jo@exampl,Studio,,number looks short,,A3,handwriting faint
```

Builder output: Rivera becomes +14155552671 with a clean card. Okafor
becomes +442071838750 with a `trunk_zero_dropped` flag. Lee is flagged
`phone_no_country_code_known` and `email_fails_basic_syntax`, keeps no TEL
or EMAIL line in the vCard, and appears in flags.json for a human to check
against band A. Nothing was silently corrected.

## Operating Rules

- Flag, do not fix. An ambiguous character, a suspect domain like gmial,
  a short number, a name that might be Jon or Jan: all get flags and stay
  as written. The owner of the list decides, with the sheet in front of
  them.
- Never invent a country code, a digit, an area code, or an email domain.
  A missing value is a flag. A guessed value is an error you shipped.
- Row counts must reconcile before delivery. An unreconciled count means
  a row was dropped or doubled, and that gets found before the user does.
- Duplicates are reported, not merged. Same phone or email on two rows
  gets a `possible_duplicate_of_row_N` flag on the later row. Two people
  can share a phone, and one person can sign in twice. The report says
  which it might be, the user decides.
- Personal data discipline: a roster is other people's names, phones, and
  emails. Keep the files local, do not paste the list into random online
  OCR or converter sites, and do not include the list in messages to
  anyone but the person who supplied it.
- Handwriting with no legible reading at all: enter the row with whatever
  fields are readable, mark the rest `[?]` in the flag column, and count
  it in the reconciliation as flagged unreadable. Never drop it silently.
- If the source is too degraded to transcribe honestly (blur, glare,
  cropped columns), say so and ask for a better copy instead of producing
  a confident list from a bad image.
- Sources behind the format claims: ITU-T E.164 via the Twilio glossary
  summary, RFC 6350 for vCard 4.0 (folding, escaping, FN and N, TEL TYPE
  values), and Google libphonenumber documentation for parse and validate
  behavior. Full URLs are in references/sources.md.

## References

- references/sources.md for the verified source list.
- references/verification-checklist.md for the read-back checklist in a
  form you can copy into a job folder.
