> ## Documentation Index
> Fetch the complete documentation index at: https://docs.findmydata.io/llms.txt
> Use this file to discover all available pages before exploring further.

# ADR 0013 live graph connector

# ADR-0013: Live Microsoft Graph connector (read-only) validated against CDX

**Status:** Accepted · 2026-07-14

## Decision

Implement `GraphM365Connector` behind the existing `Connector` contract and wire it
through `ConnectorRegistry` for `FMD_CONNECTOR_MODE=graph`. Scope of this build:
**read-only**.

* **Auth:** app-only client-credentials (`/oauth2/v2.0/token`, `.default` scope),
  cached with expiry. Secret resolved from env by reference (`FMD_GRAPH_CLIENT_
  SECRET_REF`), never logged or committed.
* **Enumeration:** `discoverScopes` lists sites (`/sites?search=*`) → document-library
  drives. `inventoryPage`/`changes` use the `driveItem` **delta** feed; the opaque
  `@odata.nextLink`/`@odata.deltaLink` URLs are the cursors, preserved verbatim; `410
  Gone` maps to a safe resync.
* **Observation:** `getPermissions` maps Graph permissions to the canonical
  deduplicated permission-set model (link scopes → broad categories; completeness
  marked `not_observable` on 403). `getContent` streams bounded bytes with
  `content-length` guarding. `getLabel` returns `unknown` honestly (reading assigned
  sensitivity labels needs the protected `extractSensitivityLabels` API, not granted).
* **Throttling:** `Retry-After` honored with bounded retries.
* **Writes:** label preview/execute return `unsupported` — the read-only app has no
  write permission, matching the documented posture (ADR-0010, ADR-0011).
* Least privilege chosen for the run: `Sites.Read.All` + `Files.Read.All`.
  `Sites.Selected` (per-site grant) remains the production recommendation
  (permissions-manifest.md); Read.All was used to get a working read-only pilot.

A dependency-free, bounded **DOCX text extractor** (`intelligence/docx.ts`) was added
so real Word documents classify on content: parse the ZIP central directory, inflate
`word/document.xml` (`node:zlib` raw inflate), strip WordprocessingML to text. Other
binary formats remain honest `unsupported` states.

## Context

The MVP prioritized a deep fixture-validated slice (ADR-0012) with the live path
gated. A follow-up session had authenticated CDX access, so the live adapter was
built and validated read-only against the Contoso CDX tenant (run record:
cdx-test-runbook.md §0). Live data is the only way to surface issues that
deterministic fixtures cannot.

## Consequences

* The mock connector stays the default and the CI-tested path; live is opt-in by
  config and constructed only when credentials resolve (else an explicit
  `BlockedError`, never a silent mock fallback).
* Live validation surfaced and fixed a real over-classification defect (generic
  evidence must not create type-specific candidates — see inference.ts
  `hasDiscriminating`). This changed `INFER_IMPL_VERSION`; retained features
  re-score without source I/O.
* Still to validate live: change-sequence deltas, Purview label read/write,
  Teams/Exchange, and write actions. `Sites.Selected` migration is the least-privilege
  follow-up.
