A service that ingests meeting records from two upstream systems (a CRM and a Calendar), reconciles the records that refer to the same real-world meeting, and serves a unified meeting list through a REST API and a simple web frontend — showing, for every piece of data, which source it came from and where the sources disagree.
Requires Python 3.11+ and nothing else (no Node, no database).
./run.shThat single command creates a virtualenv on first run, installs dependencies,
and serves the app at http://localhost:8000. The reconciliation pipeline
runs at startup from the files in data/.
To run the tests:
.venv/bin/python -m pytest tests/- 24 unified meetings built from 20 CRM records + 22 calendar records: 17 matched across both sources, 3 CRM-only, 4 calendar-only, and 1 intra-source duplicate collapsed.
- Source badges on every meeting (CRM / Calendar / Both) and a per-field "from CRM / from Calendar / from Both" tag inside each meeting's details.
- 5 conflicts surfaced, none resolved silently: a meeting the CRM says is in-person but the calendar says is on Zoom; three start-time disagreements; and a meeting cancelled in the CRM that the calendar still shows as confirmed. Expanding a conflicted field shows both source values side by side.
- A data-quality report (bottom panel, or
GET /api/report): every anomaly found in the input (malformed date, missing time,[at]-mangled email, non-email attendee, empty attendee list), the duplicate that was collapsed, and every cross-source match with its confidence score.
| Endpoint | Description |
|---|---|
GET /api/meetings |
Unified list. Filters: ?source=crm|calendar|both, ?conflicts_only=true, ?q=<text> |
GET /api/meetings/{id} |
One meeting with full per-field provenance and both raw source payloads |
GET /api/report |
Data-quality anomalies, dedup decisions, match decisions with scores |
GET /api/health |
Liveness check |
The pipeline runs in three stages at startup (see docs/PLAN.md for the full
design and docs/SPEC.md for every documented decision):
ingest → normalize → reconcile → in-memory store → REST API → frontend
- Ingest treats the input as untrusted: malformed values are repaired when the repair is unambiguous, skipped with a logged anomaly when not, and never crash the service.
- Normalize brings both schemas to a common shape: ISO dates (repairing
03-15/2025), timezone-normalized datetimes (naive timestamps assumed US Eastern; UTC-suffixed ones converted), cleaned attendee emails, and a name↔email mapping ("Sarah Chen"↔sarah.chen@firma.com). - Reconcile does the real work, in three passes:
- Intra-source dedup — two calendar entries for the same Pinnacle meeting collapse into one before cross-source matching (recurring-series instances are exempt: a weekly sync is many real meetings, not duplicates).
- Matching — records don't share an ID, so candidate pairs (same day)
are scored on weighted signals: participants (40%), start-time proximity
(25%), title similarity (20%), company↔email-domain (15%). Pairs above a
0.45 threshold match greedily, best score first. Every accepted match and
its score is visible in
/api/report. - Merge — matched pairs merge field by field. Each field carries its value, its source, and — when sources materially disagree — a conflict object holding both values. Nulls on one side are enriched from the other. Nothing is dropped.
The full decision register — every ambiguity found in the data and how it's
handled — is in docs/SPEC.md §5. The ones that shape the design:
- Surface conflicts, don't resolve them. The system cannot know whether a meeting is really on Zoom or in the NYC office; the sales team can. Every material disagreement is preserved with both values and highlighted in the UI. An automatic resolver — however clever — would hide exactly the information a user needs most.
- Display precedence: calendar wins operational facts (time, location,
attendees); CRM wins business context (client, owner, status, notes). The
calendar is what people actually attend; the CRM is where relationship
lifecycle lives. Precedence only picks the displayed value — the losing
value stays in the conflict object. A recency rule was considered and
rejected: the sources expose
created_atbut noupdated_at. - A 5-minute tolerance for start-time conflicts. Client meetings are operationally time-sensitive: a 30-minute disagreement between sources means someone shows up at the wrong time, so it must not pass silently. Verified against the data that the tight threshold adds only true conflicts, no noise.
- "Conflict" means incompatible claims about the same fact — not any textual difference. "Conference Room B" vs "HQ - Conference Room B" is the same place at different specificity; "Zoom" vs "NYC Office" cannot both be true. Flagging cosmetic differences would bury the 5 real conflicts under noise.
- Cancelled-in-CRM wins over confirmed-in-calendar, shown with a conflict flag: an explicit cancellation with a reschedule note is stronger evidence than a calendar entry nobody cleaned up.
- No database, no frontend build toolchain, read-only API. The dataset is two static files read once at startup; the brief asks for a unified view. Each of these would be justified by a requirement this exercise doesn't have (incremental ingestion → database; write-back → a bidirectional sync design).
Documented in docs/SPEC.md §8, found while stress-testing the design:
- Matching blocks candidate pairs by date, so a meeting rescheduled to a different day in only one source would appear as two single-source records instead of one meeting with a date conflict. Mitigations (not needed for this dataset): widen the blocking window to ±1 day, or add a participant-based blocking pass.
- Display precedence can promote a stale value when the other source is
fresher; without an
updated_atfield no automatic rule can do better, so the conflict badge is the deliberate mitigation.
- Participant-based secondary blocking (closes the rescheduling gap above).
- Persistence + incremental ingestion if the sources became live APIs.
- Pagination and caching once the dataset outgrows a single response.
21 behavioral tests run the full pipeline against the real data files — one test per documented decision in the SPEC's ambiguity register (timezone normalization, dedup, enrichment, conflict preservation, malformed-data handling…), plus synthetic robustness cases (missing IDs, non-string fields, missing data files) added after an independent review pass.
This project was built in close collaboration with AI (Claude Code), as
encouraged by the brief. The process and the division of labor — including
where AI proposals were rejected or corrected by human review — are documented
in docs/AI_COLLABORATION.md. The planning
artifacts it produced are docs/SPEC.md,
docs/PLAN.md and docs/TASKS.md.
~3 hours of focused time across two days. Most of it went into analysis and
decision-making rather than typing: understanding the data's edge cases,
deciding how each one should be handled (the ambiguity register in
docs/SPEC.md §5), reviewing each implementation delivery, and verifying the
final result. The implementation itself was largely executed by AI under that
direction — see docs/AI_COLLABORATION.md for the exact division of labor and
where human review changed the outcome.