Phase 2 and 3 of the platform rework. - Neutral request/response contract: external_id, metadata, X-AI-Key, run_id in the response; client-specific fields removed. - AI profile is a revision table: saving creates an inactive revision, activation is separate, try-run does not store a run. One chat-completions gateway serves AIHub and OpenAI-compatible profiles. - Add between and in rule operators. - Contracts moved to contracts/lead-scoring/ with a score-response fixture. - Admin Web updated for revisions, runs and the neutral fixture. - Consolidate tests; remove rewrite_architect.md and the stale handoff doc; rewrite spec, CONTEXT and ARCHITECTURE for the current design. Not verified: live AIHub/OpenAI behavior, Omni/EMS integration.
Lead Scoring: client contract
openapi.json is generated from the backend routes (make contract). fixtures/run-request.json and fixtures/score-response.json are generated examples; scripts/export_contract_fixtures.py --check rejects drift. The service knows no specific client: client-owned data travels in metadata.
Request
POST /v1/lead-scoring/score, synchronous.
| Header | Meaning |
|---|---|
Authorization: Bearer <app_key> |
App credential (one per installation). 401 if invalid or revoked, 403 without the lead_scoring permission. |
Idempotency-Key |
Required UUID chosen by the client. Same key with the same body replays the stored result; same key with a different body returns 422. |
X-AI-Key |
Optional AI provider key for AIHub profiles. Held in memory for the call only, never stored or logged. Max 4096 chars, no CR/LF (else 422). |
Body: snapshot (lead profile with external_id, conversations, custom fields), scoring_config (rule_weight, ai_weight, rules[]) and optional metadata (any JSON object up to 10KB, stored and returned unchanged). Rule path may be lead_profile.<name|phone|email|status|external_id>, custom_fields.<key> or metrics.<key>.
Response (200)
run_id, final_score, rule_score, ai_score, result_mode, error_code, breakdown[], ai {summary, suggested_action, signals[]}, metrics, metadata.
result_mode is rules_only, hybrid or rules_only_fallback. In the fallback case error_code says why (ai_timeout, ai_key_missing, ai_profile_missing, or a provider error code) and final_score equals rule_score. The AI model, prompt and timeout belong to the active AI profile, not the request.
Boundaries
No webhook: the caller waits for the response (the AI deadline is the profile timeout_seconds, default 50). Provider tests do not prove client persistence or writeback; verify both sides with a mock AI before enabling a real integration.