# Offline Legacy Observation Adapter

Phase 8B converts existing local legacy evidence into the Phase 8
`LegacyObservation` JSONL schema. It is read-only with respect to evidence and
has no AMI, network, database, Vtiger, WebSocket, archive, or AI dependency. The
production listener and listener configuration remain unchanged.

Related architecture: MPBX-004 and MPBX-006. Release: `cem-legacy-observation-offline-v0.8.1`. Commit `5ee7c88` is the implementation-maintenance correction that makes `--max-records` reader and summary counters describe the same globally bounded record set across manifest sources.

The legacy listener remains authoritative for production CRM effects. Offline conversion and parity comparison are evidence workflows only and create no CRM side effect.

## Evidence inventory

Repository inspection found dated listener logs under `logs/listener*.log` and a
WebSocket bridge log at `logs/server.log`. Listener source code confirms the
exact emitted INIT, RING, ANSWER, HANGUP, FINAL, duplicate, role-ignore, and
missed-skip phrases. WebSocket source confirms targeted `EVENT=... | ext=... |
sent=...` and broadcast log formats.

No sanitized CRM/PBXManager CSV or JSONL export was present. CRM export support
is therefore limited to the documented synthetic CSV fixture format. Existing
sanitized AMI captures and CEM files are not legacy listener evidence and are
not accepted by this adapter. Historical source snapshots and package evidence
are documentation/code, not supported runtime input.

Supported CLI types are exactly:

- `listener-log`
- `websocket-log`
- `crm-export`
- `combined-manifest`

## Parsing and normalization

Listener parsing uses anchored patterns matching active listener messages. Raw
caller numbers appearing in INIT and FINAL lines are recognized only so the line
can be parsed; they are never retained. Unknown lines are ignored, while broken
CSV shapes and unsafe files are rejected.

WebSocket parsing accepts only the confirmed targeted ring and broadcast answer
delivery formats with a positive sent count. These records lack stable call
identity in the actual bridge log and therefore begin at low confidence.

The CRM CSV columns are configurable and may supply a sanitized legacy call
reference, Linkedid, canonical correlation hint, completed/missed/ignored
status, extension, queue, direction, and timestamps. Unsupported statuses do
not produce observations.

Normalized evidence retains only action facts, stable call identifiers awaiting
hashing, safe timestamps, scope, and safe provenance. Raw lines are never stored.

## Assembly and confidence

Evidence is sorted by time and grouped in this order:

1. exact legacy call reference;
2. Linkedid;
3. Uniqueid;
4. unique queue/extension/time-window candidate.

Aliases let a CRM reference containing the same Linkedid enrich listener log
evidence. Caller identity is never used. Multiple time-window candidates are
marked ambiguous; evidence with no safe candidate is unmatched.

- High: direct stable call identity plus timestamp and explicit action.
- Medium: stable identity without time, or low-confidence evidence uniquely
  joined by the bounded window.
- Low: partial, unmatched, or ambiguous evidence.

`minimum_confidence` may exclude lower classifications without changing parity
policy automatically.

## Sanitization and provenance

Linkedid, Uniqueid, and explicit legacy references are HMAC-hashed before
output. The salt comes from a restrictive local salt file or the
`MARVELPBX_LEGACY_OBSERVATION_HMAC_SALT` environment variable and is never
printed. The sample contains only a path reference, not a secret.

Output adds these controlled provenance fields to the existing observation
schema:

- `source_type`
- `source_file_basename`
- `source_line_or_record`
- `evidence_count`
- `evidence_types`
- `confidence`

Absolute paths and raw source lines are excluded. Phone- and secret-like values
are discarded or redacted. Output is append-only, flushed per record, mode
`0600`, idempotent by evidence fingerprint, and monotonically sequenced per
logical call across restart.

## CLI

```sh
php tools/cem_legacy_observation_offline.php \
  --source-type combined-manifest \
  --input tests/fixtures/cem/legacy_offline/manifest.json \
  --output /tmp/legacy-evidence/observations.jsonl \
  --config /tmp/legacy-evidence/offline-config.php
```

Time bounds exclude evidence without a timestamp because its membership cannot
be proven. `--max-records` bounds source reads. `--dry-run` parses and assembles
but creates no output.

Combined manifests use schema version `1`, list each source explicitly, allow
paths relative to the manifest, perform no recursive scan or shell expansion,
and reject URLs, nested manifests, unreadable paths, and non-regular files.

## Parity workflow

```sh
php tools/cem_crm_parity.php \
  --legacy-observations /tmp/legacy-evidence/observations.jsonl \
  --shadow-decisions tests/fixtures/cem/parity_shadow_synthetic.jsonl \
  --report /tmp/legacy-evidence/parity-report.jsonl
```

Canonical correlation hints in the sanitized CRM fixture bridge offline calls
to the existing shadow fixture. Confidence remains visible in observations;
the adapter never changes parity thresholds or CRM policy.

## Limitations

Historical listener logs often do not contain outbound/internal decisions that the listener silently returned from, nor explicit queue IDs. Historical offline logs therefore often cannot prove outbound/internal ignored decisions. The optional disabled-by-default live observation hook can emit a safely classified root outbound observation, but the offline adapter does not invent such a fact and still does not infer an unavailable queue ID. WebSocket-only evidence remains low confidence without stable call identity. CRM export support is a narrow fixture-backed CSV contract, not an arbitrary Vtiger export parser.
There is no directory discovery, live tailing, retention, or automatic policy
promotion in Phase 8B.
