Handbook
Outlook People harvest — dual architecture (LCDL)
Consumer mirror: forge-cockpit-web org-people-dual-architecture.md.
Updated
LCDL owns Playwright read atoms on the OWA People host page (outlook.office.com/people/); Cockpit owns SQLCipher, canonical disk, ingest queue, caps, and contact linking; Edge owns operator session on an existing People tab.
Sole org-directory surface: The legacy Org Explorer (Delve iframe) crawl has been removed. Outlook People — and specifically its Organization tab — is now the only OWA org-structure source. harvest_source=outlook_people.
No Delve iframe / no EML: Host-page DOM + directory suggest API only.
Stable state boundary
LCDL does not write files/org/, maintain org_ingest_queue, stamp profile_check, or resolve pending_mail:* to canonical GUIDs. Cockpit consumer performs mail→graph_id lookup and canonical folder selection after OrgHarvestWire.from_task_result.
Suggestion disambiguation (identity = graph_id)
People search suggestion selection is centralized in the pure helper choose_person_suggestion(bodies, *, display_name, mail, graph_id) (consumed by OutlookPeopleReader._choose_suggestion). Resolution order is (1) exact graph_id, (2) exact mail, (3) a single loose display-name match. When a name query matches multiple distinct identities and neither graph_id nor mail pins one, the helper returns ok=False with reason suggest_ambiguous_no_graph_id_match — it never guesses. focus_person_via_search surfaces that reason as its error, so the Cockpit consumer keeps its identity guarantee (no contamination) without LCDL knowing canonical-disk policy. No safe match at all returns suggest_no_match.
Guided ingest prefetch: guided_focus_read_and_dismiss may open LPC from captured suggest API bodies before Enter (search_path: prefetch); Cockpit validators skip the one-Enter contract for that path only.
Org-chart edges via the Organization tab. LCDL's home_snapshot wire carries empty edges[]. The richer path is the guided org item: LCDL parses the LPC Organization tab (manager chain, direct reports, works-with) into name-bearing fields on the profile card; the Cockpit consumer (persist_org_structure_from_card) resolves those names to graph_id and writes org_edge rows. LCDL still emits names only — it never resolves identity or writes the graph. With Org Explorer removed, the Organization tab is the sole source for org structure including net-new discovery; People resolves edges by display-name match on the consumer side.
Governed tasks (v1)
task_id |
Purpose |
|---|---|
outlook_probe_people_health |
/people/ URL, search combobox, left tree ready |
outlook_people_home_snapshot |
Visible contact rows (+ scroll); operator row from tree mail |
outlook_people_focus_search |
Directory search → suggest API → option click |
outlook_people_focus_tree |
Click [role=row] or [role=treeitem] by name/mail |
outlook_people_snapshot_person |
Tree/search focus + profile panel parse |
outlook_people_read_profile_panel |
Parse flyout / detail panel text |
outlook_people_dismiss_profile_panel |
Escape dismiss |
Input: { "session_ref": "…", "operator_graph_id": "…", "operator_mail": "…" } on a CdpOwaPageShim for a People tab.
Connector: src/forge_lcdl/playwright/connectors/outlook_people.py (OutlookPeopleReader).
End-to-end (LCDL slice)
session_ref → PlaywrightPageSession (People host tab)
→ probe_health (search + tree)
→ harvest_home_snapshot ([role=row] parse, optional scroll)
→ wire JSON: people[], edges[] (empty v1), focused_graph_id, upn
→ optional: focus_search / focus_tree → snapshot_person → read_profile_panel
Consumer calls via cockpit_server.lcdl_outlook_bridge.run_task — LCDL does not import Cockpit.
Responsibility matrix
| Concern | forge-lcdl | Cockpit consumer | Edge |
|---|---|---|---|
| Governed read tasks | tasks/outlook_read_v1.py runners |
lcdl_outlook_bridge.run_task |
— |
| Host-page selectors / evaluate | outlook_people.py |
Must not add page.locator in people_harvest.py |
People UI |
| CDP tab pick | pick_outlook_people_cdp_page_target |
_attach_outlook_people_tab |
Existing People tab |
| Directory suggest capture | Network listener on POST …/search/api/v1/suggestions?domain=People |
— | Signed-in session |
| Row / panel parse | parse_people_row_text, reuses parse_profile_card_text from owa_lpc_reader |
— | — |
pending_mail:{email} wire ids |
resolve_wire_graph_id |
_resolve_graph_ids_from_db |
— |
| Wire shape | people[], focused_graph_id |
OrgHarvestWire.from_task_result |
— |
| SQLCipher, caps, source tag | — | people_harvest.py, ingest.py |
— |
| Org structural edges (home wire) | — (empty edges[]) |
Organization tab parse for net-new discovery | — |
Org structural edges (guided org item) |
Organization tab parse → manager / DR / works-with names on the card | persist_org_structure_from_card resolves names → org_edge (section_reports_to, section_manager_peers, works_with); surfaces edges_linked / org_edges_name_only |
People Organization sub-view |
| Read-only guarantee | READONLY_POLICY.md, read-only clicks |
_ORG_CDP_LOCK |
Operator obeys policy |
What LCDL must not do
| Forbidden in LCDL | Belongs in consumer |
|---|---|
org_people.sqlite, org_graph.sqlite |
Cockpit SQLCipher |
files/org/ paths, harvest_manifest.json |
Cockpit org/ingest.py |
COCKPIT_ORG_PEOPLE_MAX_ROWS cap |
people_harvest.py |
Mail → canonical graph_id merge |
people_harvest._resolve_graph_ids_from_db |
| Depth / guided scope policy | Cockpit people_harvest.py + depth_policy.py |
Data sources (priority)
- Directory suggest API —
POST /search/api/v1/suggestions?domain=People(DisplayName,EmailAddresses,JobTitle,Department,OfficeLocation) - Contact list rows —
[role=row]innerText (name, mail, phone) - Left tree — operator mail from
[role=tree"] [role=treeitem"] - Profile panel — flyout text via shared owa_lpc_reader parsers (
parse_contact_panel_fields,parse_profile_card_text)
Not in v1: Delve GraphQL, personaphoto?aadObjectId=, org hierarchy edges in the home-snapshot wire (the guided org item still yields hierarchy via the Organization tab → consumer org_edge).
Wire shape (summary)
{
"ok": true,
"focused_graph_id": "5af741ae-…",
"upn": "lzvyahin@softserveinc.com",
"people": [{
"graph_id": "pending_mail:user@example.com",
"display_name": "User Name",
"mail": "user@example.com",
"job_title": "…"
}],
"edges": []
}
When Azure AD GUID is known (operator env or prior harvest), graph_id is a lowercase GUID.
People task surface
| LCDL capability | People task |
|---|---|
| Hierarchy snapshot | outlook_people_home_snapshot (flat list; structure from Organization tab) |
| Search focus | outlook_people_focus_search (directory suggest) |
| Row click | outlook_people_focus_tree |
| Profile panel | outlook_people_read_profile_panel |
| Health | outlook_probe_people_health |
Outlook People is the only org-directory surface; the former Org Explorer task atoms were removed.
AutomationFlow examples (forge-cdp-manager)
Documented JSON v1 flows (orchestration loops stay in Cockpit people_ingest.py):
| File | Atoms |
|---|---|
flows/examples/outlook/people-health-probe.lmeta |
outlook_probe_people_health |
flows/examples/outlook/people-guided-one.lmeta |
probe → outlook_people_guided_ingest_one |
flows/examples/outlook/people-home-snapshot.lmeta |
probe → outlook_people_home_snapshot |
Canonical operator entry: Cockpit org-people-ingest.md.