# Legacy Observation and CRM Parity

Phase 8 provides the deterministic CRM parity harness, Phase 8B adds the offline observation adapter, and Phase 8D adds an optional live observation hook. The legacy listener remains authoritative for production CRM effects. The hook is additive, failure-isolated and disabled by default; no CEM consumer currently performs production CRM side effects.

Related architecture: MPBX-000, MPBX-004 and MPBX-006.

Implementation releases:

- `cem-crm-parity-v0.8`;
- `cem-legacy-observation-offline-v0.8.1`;
- `cem-legacy-observation-hook-v0.8.2`.

## Exact legacy rules observed

The listener initializes calls from `Newchannel` using `Linkedid`, then
`Uniqueid`. External inbound number detection is required, except that a DAHDI
channel in an `ext-did` context may initialize before a number arrives. A
`from-internal` context is outbound and excluded. Caller lookup accepts a 7–15
digit external value found in CallerIDNum, ConnectedLineNum, CallerID, or
DialString.

Eligible inbound extensions are resolved from PJSIP, SIP, or Local channel
names. With no configured inbound roles, a syntactically resolved extension is
accepted; with roles configured, the extension must belong to an active Vtiger
user in an allowed role. On `DialBegin`, each eligible extension is notified at
most once. A continuing ring cancels pending missed-call timers.

The listener contains no explicit monitored-queue registry or queue-ID allowlist.
Its effective queue behavior comes from eligible `DialBegin` destination legs
sharing the call Linkedid; queue parity therefore depends on separately observed
or canonical queue context and is not inferred from a hidden legacy rule.

The first eligible `Newstate` with `ChannelStateDesc=Up`, or `DialEnd` with
`DialStatus=ANSWER`, becomes the answered extension. It cancels missed timers,
sends answered, and sends taken notifications to other ringing extensions.
Later answer signals are ignored.

Hangup of the answered channel finalizes a completed activity. An unanswered
hangup schedules missed evaluation for 30 seconds; finalization also requires 10
seconds without activity. Calls with no eligible ringing extensions are dropped
instead of producing a missed activity. Finalization uses a latch and one MySQL
transaction, then sends the existing call-end WebSocket notification.

## Observation safety model

`LegacyObserver` is wired into selected existing listener decision points through the Phase 8D hook but remains disabled unless an ignored local configuration explicitly enables it. Observation success is never consulted by a listener business branch. Initialization, sanitization, serialization and local-write failures are caught so the observer cannot alter CRM, MySQL, WebSocket, missed-call or finalization behavior.

The sanitizer hashes Linkedid and Uniqueid with a local salt, drops caller
numbers and unknown fields, restricts queue/extension/reason syntax, redacts
phone- and secret-like values, and emits no AMI packet, CRM data, SIP header,
recording path, or credential. Output is append-only JSONL, mode `0600`, with
monotonic per-call sequence numbers and deterministic observation IDs.

Copy `config/legacy_observation_config.inc.php.sample` only to the ignored local
path. The sample is disabled and contains no salt or credential. A live salt
is read from the restrictive local path named by `hmac_salt_file` and is never written to output.

The hook emits only facts already known by the listener. Reliable root outbound classification requires stable root-leg identity and is tracked by a bounded in-memory terminal guard so the same call cannot later be observed as inbound `missed_no_crm_rings`. Raw runtime identifiers used by the guard are never persisted.

## Controlled real-log result

Phase 8C attempted a bounded read-only reconciliation of real legacy evidence. The result was `OFFLINE_INSUFFICIENT`, not production parity approval. No matching same-window shadow journal/decision data was available, listener evidence did not provide explicit queue IDs, WebSocket evidence lacked stable call identity, and available log timestamps were not reliably timezone-qualified. These gaps prevent high-confidence proof of popup, queue, ring-set and missed-call parity from offline evidence alone.

Phase 8D controlled DEV validation has exercised answered inbound, missed inbound and reliably classified outbound scenarios. This validates hook safety and observation semantics for those controlled cases; it does not establish complete production parity or authorize listener replacement.

## Correlation bridge

Resolution preference is:

1. explicit canonical correlation hint from a controlled mapping;
2. configured hashed-Linkedid to correlation mapping;
3. a unique queue, answered-extension, and timing-window candidate.

Explicit hints that do not exist fail unmatched. Multiple heuristic candidates
are ambiguous. Caller identity is never a correlation key.

## Parity model

The harness aggregates out-of-order records per logical call and compares
direction, queue, ring set, answered extension, popup, activity, missed-call,
ignore and duplicate behavior, plus available lifecycle timings. Reports contain
only aggregate safe fields—not source records.

Mismatch categories are `missing_legacy_observation`, `missing_shadow_decision`,
`ambiguous_correlation`, `queue_mismatch`, `ring_set_mismatch`,
`answered_extension_mismatch`, `popup_action_mismatch`,
`activity_action_mismatch`, `missed_call_mismatch`, `ignore_policy_mismatch`,
`duplicate_behavior_mismatch`, `timing_delta_exceeded`, and
`unsupported_legacy_pattern`.

Default timing tolerances are 1500 ms for ring and answer, 2000 ms for popup,
5000 ms for finalization, and 35000 ms for delayed missed finalization. Timing is
never compared for exact equality.

## CLI

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

Reports are append-only and idempotent by parity record ID. `--summary-only`
writes no detailed report. `--strict` exits nonzero below the configured parity
threshold, on critical queue/answer/action/duplicate differences, or ambiguity.

## Proposed promotion gates

- Stage A: 10 controlled calls and no critical action mismatch.
- Stage B: 100 mixed calls and at least 95% supported-call parity.
- Stage C: 500 or more calls over several days, at least 99% supported-call
  parity, and zero duplicate popup/activity actions.

These are proposed review gates, not automatic production promotion.

## Controlled rollout and rollback

First validate synthetic fixtures and review the already additive listener-hook diff with local observation configuration disabled. Enable only on DEV for controlled calls, confirm output permissions and redaction, then progress through the gates. Rollback consists of disabling or removing the local observation config and restarting through the existing operational procedure. Because no business branch depends on observation success, CRM and WebSocket behavior remains unchanged.

## Limitations

The current CRM decision schema may not carry canonical lifecycle timestamps or
hashed Linkedid, reducing some matches to medium-confidence heuristics. Queue
semantics are compared only when both sources provide them. There is no live
production-parity claim, retention, report compaction, automatic policy adjustment or production side-effecting CEM CRM consumer. The optional observer supplies stronger controlled evidence but remains disabled by default and does not repair missing historical evidence.
