Handbook
Org Explorer harvest — dual architecture (LCDL)
Consumer mirror: forge-cockpit-web org-explorer-dual-architecture.md.
Updated
LCDL owns Playwright read atoms (Delve iframe isolated-world evaluate, read-only clicks); Cockpit owns SQLCipher, crawl state, depth policy, on-disk person folders, org chart UI, and contact linking; Edge owns operator session on an existing OWA Org Explorer tab (loki.delve.office.com/orgexplorer/).
No EML / Downloads / unified_message: Org harvest is people + edges JSON only.
Parallel surface: Outlook People — host-page /people/ atoms (outlook_people_*) write the same consumer store with harvest_source=outlook_people. See ORG-DIRECTORY-CONSUMER-BOUNDARY.md.
Stable state boundary
LCDL does not dedupe people by display_name, write files/org/, maintain org_ingest_queue, or stamp profile_check. The wire may list the same human under different graph_id values when OWA surfaces them in different contexts (focus card, manager strip, team column, works-with).
Cockpit consumer (disk_canonical.py, ingest_queue.py, profile_resolve.py, profile_checker.py) owns:
- canonical folder selection (
canonical_graph_id) - queueing incomplete / id-only detections
- offline compaction and checker stamps
LCDL task list and wire shape are unchanged — only Cockpit ingest semantics after OrgHarvestWire.from_task_result.
Governed tasks (v1)
task_id |
Purpose |
|---|---|
outlook_probe_org_explorer_health |
Iframe loaded, search box ready |
outlook_org_explorer_home_snapshot |
Focused person + visible hierarchy wire JSON |
outlook_org_explorer_open_profile_card |
View profile panel (email, LinkedIn if present) |
outlook_org_explorer_prepare_focus |
Profile-enrich: mail-first search, optional chart click, View profile |
outlook_org_explorer_host_dr_panel |
Host flyout structure when iframe focus fails |
outlook_org_explorer_focus_search |
Search picker → confirmed focus (display_name, optional graph_id, optional mail) |
outlook_org_explorer_click_person |
Card click when it changes focused person |
outlook_org_explorer_focus_url |
Navigate/focus via URL when supported |
Input: { "session_ref": "…" } where the session page is a CdpOwaPageShim on an Org Explorer host tab.
Evaluate scripts: src/forge_lcdl/playwright/scripts/outlook_org_explorer_readonly.py (and related org explorer modules).
End-to-end (LCDL slice)
session_ref resolved → PlaywrightPageSession (Org Explorer host tab)
→ evaluate_readonly(org_explorer_health) # probe
→ evaluate_readonly(home_snapshot) # GraphQL walk + DOM fallback
→ wire JSON: people[], edges[], focused_graph_id, tenant_id, upn
→ optional: click_person / focus_search / open_profile_card
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 (org explorer + people runners) |
lcdl_outlook_bridge.run_task |
— |
| Delve iframe selectors / evaluate JS | outlook_org_explorer_readonly.py |
Must not add page.locator in explorer_harvest.py |
Org Explorer UI |
| CDP tab pick | pick_org_explorer_cdp_page_target in edge_cdp_attach.py |
_attach_org_explorer_tab |
Existing tab only |
| Tab visibility (background crawl) | apply_cdp_focus_emulation via connect_owa_page_via_selective_cdp |
FORGE_LCDL_CDP_BRING_TO_FRONT=0 (default) |
Delve iframe expand/show-more needs visible tab |
| Isolated world iframe read | Page.createIsolatedWorld + Runtime.evaluate |
— | Signed-in Delve session |
| Photo bytes on wire | Delve blob: fetch in snapshot |
ingest_wire_photos → photo_120.jpg |
Avatars rendered |
| Wire shape | people[], edges[], focused_graph_id |
OrgHarvestWire.from_task_result |
— |
| SQLCipher, crawl caps | — | explorer_harvest.py, ingest.py |
— |
| Depth policy / guided phases | — | depth_policy.py, guided_crawl_phases |
— |
| Graph MSAL sync | — | org/sync_service.py |
Device sign-in |
| Contact linker | — | contact_link.py |
— |
| Read-only guarantee | READONLY_POLICY.md, task read_only: true |
_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_SCAN_MAX_PEOPLE cap |
explorer_harvest.py |
| Crawl state, NDJSON live events | Cockpit crawl_state.py, API |
| MSAL / Graph device sync | Cockpit org/sync_service.py |
contact.org_graph_id linker |
Cockpit contact_link.py |
Data sources (priority)
- Delve GraphQL (
useOrgExplorerQueryRef_OrganizationQuery) — recursive JSON walk personaphoto?aadObjectId=URLs — stablegraph_id- DOM
innerTextfallback — names/titles only (nograph_id)
Wire shape (summary)
{
"ok": true,
"focused_graph_id": "…",
"people": [{ "graph_id": "…", "display_name": "…", "job_title": "…", "photo_bytes_b64": "…" }],
"edges": [{ "src_graph_id": "…", "rel": "reports_to", "dst_graph_id": "…" }]
}
Edge rel values: reports_to, peer_of, works_with.
Profile-enrich harness vs guided crawl
Guided ingest (explorer_harvest.py) focuses people via chart click / URL when the crawl policy allows — dropdown search is not the primary path. Profile-enrich harness (COCKPIT_ORG_SEARCH_ENTER_ONLY=1, Cockpit scripts/org-profile-enrich/) types mail or display name and submits a single CDP Enter with no suggest-row poll; prepare_person_focus(..., open_view_profile=False) leaves View Profile to a later phase. LCDL exposes both behaviors via org_search_enter_only() and open_view_profile on prepare_person_focus; consumers choose per workflow.
Related
- OUTLOOK-PEOPLE-DUAL-ARCHITECTURE.md — parallel People scanner
- ORG-DIRECTORY-CONSUMER-BOUNDARY.md
- ORG-EXPLORER-READ-STRATEGIES.md
- READONLY_POLICY.md
- EDGE-CDP-ATTACH.md
- Cockpit org-explorer-dual-architecture.md
- Cockpit org-explorer-harvest.md
- Cockpit org-profile-enrich-e2e.md — profile-enrich operator loop