diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index c8b9cd80b..622fe6f01 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -132,6 +132,10 @@ jobs: run: | echo "npmAuthToken: ${{ env.token }}" >> ~/.yarnrc.yml + - name: Assert Publishable Package Boundary + if: steps.check-changes.outputs.has_changes == 'true' + run: node tooling/src/publish.js assert-publishable + - name: Sync and Increment Package Versions id: increment if: steps.check-changes.outputs.has_changes == 'true' diff --git a/.gitignore b/.gitignore index 563a57023..abd5e10a4 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,9 @@ yarn-error.log* .node_repl_history reports/ .generated/ +.worktrees/ +vendor/ +!vendor/ #IDEs .project @@ -39,6 +42,9 @@ reports/ #Local stuff *.json +!prog-mini-js.config.json +!playwright/visual/ai-summary-harness.json +!playwright/tsconfig.ai-summary.json !docs/changelog/logs/*.json !.sdd/manifest.json .env* @@ -54,3 +60,16 @@ node_modules/ # Playwright reports **/test-results/ **/playwright-report + +# Artificer runtime and local requirement/UX inputs +.matrix/ +.prog/ +requirementVersion/ +.ccwidgets/ + +# The implementation DAG is an Artificer design deliverable, not runtime state. +!design/default/implementation_dag.json + +# Reviewed AI-summary admission artifacts. +!design/default/sdk_package_lock.json +!vendor/contact-center-cc-summaries.tgz diff --git a/ai-docs/ARCHITECTURE.md b/ai-docs/ARCHITECTURE.md index e0d1d6c1a..6aafc2335 100644 --- a/ai-docs/ARCHITECTURE.md +++ b/ai-docs/ARCHITECTURE.md @@ -57,6 +57,53 @@ subscribes to store observables → user action (e.g. set state, accept task) → SDK responds and/or emits an event → store updates observables in `runInAction()` → observing widgets re-render. Grounded in `packages/contact-center/store/src/store.ts` and `storeEventsWrapper.ts`. +**AI Summary visual evidence:** `playwright/visual/ai-summary-harness.json` binds the +accepted UX source/state IDs to the existing Playwright project, sample-app +production-component mount, observable assertions, and 2x capture geometry. +`playwright/Utils/aiSummaryUtils.ts` runs browser assertions and writes raw +PNG/DOM/comparison records under `.matrix/results/prog-mini-js/ux-visual//`. +These records do not assert visual fidelity; the prog-mini-js visual task must +inspect the target/render pairs and publish a separate attempt-bound handoff. +The deterministic host owns browser startup and cleanup. Do not turn a +machine-generated comparison score into a claim of model image inspection. + +**UX seal revalidation:** `tooling/src/ai-summary-sdk-lock.js seal-ux` independently +reopens and hashes the admitted target files on every call. If an existing seal +matches the complete validated evidence and authority metadata, it preserves the +original lock-file bytes and `ux.sealedAt`; repeated DAG acceptance must not make +visual receipts stale merely by refreshing a timestamp. Missing, symlinked, or +changed inputs and mismatched sealed metadata fail closed without replacing the +receipt. A genuinely changed authority requires an explicit admission repair, +not an automatic reseal during acceptance. Regression coverage lives in +`tooling/tests/ai-summary-sdk-lock.test.js`. + +**Local UX provenance:** `tooling/src/verify-ai-summary-release.js` checks the +harness descriptor, active visual plan/receipts, and archived JSON evidence. +The exact `figma_mcp_policy: "forbidden"` and `mcp_policy: "forbidden"` declarations +are prohibitions, not evidence of MCP use. Explicit use/transport/tool records +and remote Figma references remain rejected; ambiguous MCP policies fail closed. +Local scene-graph node/file IDs and evidence filenames alone do not establish +remote acquisition. Provenance failures identify the repository-relative file +and JSON field, without printing evidence values or remote credentials. +Regression coverage lives in `tooling/tests/verify-ai-summary-release.test.js`. + +**AI summary SDK validation:** initiating/post-call requests resolve `Promise`; +receiver content uses `task:midCallSummaryReceived` and capability updates use +`task:featureEnablement` on the matching Task. The packed runtime and its extracted declarations +are checked together, including a TypeScript compiler probe explicitly mapped to the candidate. +Legacy seals cannot skip runtime validation or omit required source/build provenance. The combined +lock verification also packs and inspects all six AI-summary workspace boundaries in an isolated +child environment. See [`ai-summary.md`](../ai-summary.md) for the exact API and current sealed-package defect. + +**Release evidence binding:** release manifests are nonempty, hash every named file, and match live +Git changes since an ancestor `baseCommit`. Verification runs on the requirement's promotion branch. +Each gate names a repository-contained receipt with an explicit passing status, matching file hash, +and matching HEAD. The D8b gate also binds the packaging receipt hash; actual tarball file lists must +match that receipt. Executable provenance scanning inspects calls rather than matching explanatory +comments, regular expressions, or test source strings. Privacy scanning permits raw-error variables +used for normalization while rejecting their use as arguments to known logging sinks. These checks +do not turn unexecuted browser/visual or failed SDK checks into passing release evidence. + ## Dependencies | Dependency | Type (internal / external / peer) | How used | Failure / version handling | |---|---|---|---| diff --git a/ai-docs/CONTRACTS.md b/ai-docs/CONTRACTS.md index d0899d3de..557bae464 100644 --- a/ai-docs/CONTRACTS.md +++ b/ai-docs/CONTRACTS.md @@ -25,6 +25,9 @@ The aggregator package `@webex/cc-widgets` re-exports every widget plus the `sto | cc-widgets.store | `@webex/cc-store` | `store` | MobX singleton (`Store.getInstance()`); `init(options: InitParams, setupEventListeners): Promise`; sole SDK access point via `store.cc.*` | stable semver | `packages/contact-center/store/ai-docs/store-spec.md` | `packages/contact-center/store/src/index.ts:1,4` | | store.host-init.enableWxBetterTogether | `@webex/cc-store` | `webexConfig.cc.enableWxBetterTogether` → `store.enableWxBetterTogether` | Host init boolean (default `false`), set **before** `store.init()`: read from `webexConfig.cc` on `{ webexConfig, access_token }` init or `webex.config.cc` on `{ webex }` init; read-only observable after init (Phase 1: re-init to change). Gates wxApp Mute/Keypad visibility and wxApp mute seed; SDK owns mute/DTMF API routing. | stable semver; renaming or removing the init key or store getter is breaking | `packages/contact-center/store/ai-docs/store-spec.md` (`STORE-R-030`); `packages/contact-center/task/ai-docs/task-spec.md` (WXCC-6026) | `packages/contact-center/store/src/store.ts`, `packages/contact-center/store/src/store.types.ts` (`WebexCcInitConfig`) | | store.types | `@webex/cc-store` | Existing queue/entry-point request, response, and entity type re-exports plus `TaskUIControls` | TypeScript exports describing the SDK-backed domain surface; established entity rows and Task destination controls pass through without a widget destination abstraction | stable semver; SDK-shaped types track SDK | `@webex/contact-center` types (`node_modules/@webex/contact-center/dist/types/index.d.ts`) (SDK source); `packages/contact-center/store/ai-docs/store-spec.md` | `packages/contact-center/store/src/store.types.ts` | +| cc-components.CallControlComponent | `@webex/cc-components` | `CallControlComponent` (`CallControlComponentProps`) | React presentation component for call controls. Public props include optional `aiSummary?: CallControlAISummaryProps`, `sendMidCallSummaryBeforeAction(actionType, expectedRevision): Promise<{outcome: 'sent' \| 'failed' \| 'blocked' \| 'stale'}>` through that prop, and `wrapupCall(wrapupReason, wrapupId): Promise`. `handleTargetSelect` waits for the mid-call summary Promise to settle before invoking `consultCall`/`transferCall` once. `handleWrapupCall` resolves a `WrapupCompletionResult` and catches callback throws/rejections into `{wrapup: 'failed'}`. | stable semver; optional `aiSummary` is additive, but the `wrapupCall` migration from synchronous `void` to `Promise` is breaking for typed consumers | `packages/contact-center/cc-components/ai-docs/cc-components-spec.md` (`CC-COMPONENTS-R-006`) | `packages/contact-center/cc-components/src/components/task/task.types.ts`, `packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx`, `packages/contact-center/cc-components/src/components/task/CallControl/call-control.utils.ts` | +| cc-components.AIAssistantComponent | `@webex/cc-components` | `AIAssistantComponent` (`AIAssistantComponentProps`, `AIAssistantReceiverSummary`, `AdaptiveCardRendererProps`) | AI Assistant panel chrome, Real-time Assist transcript/actions, and props-only receiving-agent `View summary` branch with display-only card rendering, receiver copy, and feedback callbacks | stable semver; receiver branch prop shape is a public type surface | `packages/contact-center/cc-components/ai-docs/cc-components-spec.md` (`CC-COMPONENTS-R-020`, `CC-COMPONENTS-R-021`) | `packages/contact-center/cc-components/src/components/AIAssistant/ai-assistant.types.ts` | +| cc-components.AISummary | `@webex/cc-components` | `AISummary`, `AISummaryProps`, `AI_SUMMARY_MESSAGES`, `COPIED_FEEDBACK_MS`, `MID_CALL_SECTION_DEFINITIONS`, `POST_CALL_SECTION_DEFINITIONS`, `POST_CALL_DISPLAY_SECTION_ORDER`, `projectPostCallDisplaySections` | SDK-free shared AI summary surface for mid-call initiator, mid-call receiver, and post-call wrap-up modes; exposes exact copy constants, ordered section key/label definitions, and post-call section projection used by components and tests | stable semver; adding optional props/values is additive, narrowing `AISummaryProps` modes/callback result types is breaking | `packages/contact-center/cc-components/ai-docs/cc-components-spec.md` (`CC-COMPONENTS-R-002`, `CC-COMPONENTS-R-022`) | `packages/contact-center/cc-components/src/components/AISummary/index.ts` | | cc-components.consult-transfer-lists | `@webex/cc-components` | `CallControl` consult/transfer fetch props plus `action`, `availableDestinations`, and optional list-item `presence` | `FetchPaginatedList` / `FetchPaginatedList` and the SDK-ordered destination array from `TaskUIControls`; UI preserves list/category order, passes buddy availability to Momentum Avatar as semantic presence, shows `AddressBookEntry.number` and `EntryPointRecord.number` subtitles, and may only apply host hide overrides | stable semver; entity and control types track store/SDK contracts; optional presentation prop is additive | `packages/contact-center/cc-components/ai-docs/cc-components-spec.md`; `ai-docs/features/consult-transfer-list-policy/spec/feature-spec.md` | `packages/contact-center/cc-components/src/components/task/task.types.ts` | | cc-components.TelephonyActionToast | `@webex/cc-components` | `TelephonyActionToast` (`TelephonyActionToastProps`) | wxApp thick-client telephony error toast (Momentum `Toast` `variant="error"`); consumed by `@webex/cc-task` CallControl / CallControlCAD wrappers | widget-internal / monorepo cross-package only; not a host-facing `@webex/cc-widgets` surface | `packages/contact-center/cc-components/ai-docs/cc-components-spec.md` | `packages/contact-center/cc-components/src/components/task/TelephonyActionToast/telephony-action-toast.tsx` | | cc-components.WxAppOfferActionError | `@webex/cc-components` | `WxAppOfferActionError` (`WxAppOfferActionErrorProps`) | Inline wxApp offer-action error as red text below Accept/Decline; store-backed per `interactionId` | widget-internal / monorepo cross-package only; not a host-facing `@webex/cc-widgets` surface | `packages/contact-center/cc-components/ai-docs/cc-components-spec.md` | `packages/contact-center/cc-components/src/components/task/WxAppOfferActionError/wxapp-offer-action-error.tsx` | @@ -44,7 +47,17 @@ The aggregator package `@webex/cc-widgets` re-exports every widget plus the `sto | `@r2wc/react-to-web-component` | Wraps React widgets as custom elements (`packages/contact-center/cc-widgets/src/wc.ts:1`) | r2wc docs | Bundled with `cc-widgets` | N/A | per `package.json` | ## Compatibility & Deprecation Policy -- **Breaking-change rule:** Semver. A breaking change requires a major version bump. The breaking surface is: the named **package exports** above, the **custom-element tag names** (`widget-cc-*` in `packages/contact-center/cc-widgets/src/wc.ts:68-78`), and the **declared widget event/callback prop names**. Renaming/removing any of these, or changing a callback signature, is breaking. Adding a new widget, export, or optional prop is additive (minor). + +The sealed AI summary SDK contract is documented in [`ai-summary.md`](../ai-summary.md). +Initiating mid-call and post-call results resolve `Promise` from the Task request methods. +Receiving-agent content arrives on `TASK_EVENTS.TASK_MID_CALL_SUMMARY_RECEIVED` +(`task:midCallSummaryReceived`); capability changes arrive on `TASK_EVENTS.TASK_FEATURE_ENABLEMENT` +(`task:featureEnablement`). The store owns subscriptions and response sends. The previous +three summary-event names and separate event-payload type exports are not part of this SDK. +`tooling/src/ai-summary-sdk-lock.js verify-sdk` always validates extracted candidate declarations +and runs the packed runtime suite, including legacy seals without a recorded harness marker. +Admission does not silently convert a failed probe into a successful receipt. +- **Breaking-change rule:** Semver. A breaking change requires a major version bump. The breaking surface is: the named **package exports** above, the **custom-element tag names** (`widget-cc-*` in `packages/contact-center/cc-widgets/src/wc.ts:68-78`, `component-cc-*` in `packages/contact-center/cc-components/src/wc.ts`), and the **declared widget/component event/callback prop names**. Renaming/removing any of these, narrowing a callback signature, or changing a required callback return type is breaking; `CallControlComponentProps.wrapupCall` changing from synchronous `void` to `Promise` is such a breaking migration. Adding a new widget, export, or optional prop is additive (minor). - **Deprecation:** Releases are cut by `semantic-release` (`package.json`, `.releaserc`); commit type drives the version bump (`fix` → patch, `feat` → minor, breaking footer → major). Deprecated surfaces are kept for at least one minor release with a JSDoc `@deprecated` note before removal in a major. ## Maintenance diff --git a/ai-summary.md b/ai-summary.md new file mode 100644 index 000000000..70ecd0299 --- /dev/null +++ b/ai-summary.md @@ -0,0 +1,206 @@ +# Contact Center AI summary SDK contract + +This document describes the concrete SDK API consumed by the widget store. Product behavior, +role-specific rendering, eligibility, counters, accessibility, privacy, and sequencing are governed +by [`requirement.md`](requirement.md). This API alignment was approved during alpha recovery on +2026-09-27; it does not remove any product requirement. + +The inspected source is the clean sibling `webex-js-sdk` checkout, branch `cc-summaries`, commit +`15f3da706d84c10bc632abe252cea202a70ae212`. The sealed package is +`@webex/contact-center@3.12.0-cc-summaries.15f3da706d84`, selected through the private root's +`file:./vendor/contact-center-cc-summaries.tgz` resolution. The exact tarball, installed-file and +declaration hashes live in `design/default/sdk_package_lock.json`; that seal is historical evidence +and is not rewritten merely to claim passing validation. A subsequent approved SDK fix requires +a new honest build/admission record. + +## Public Task methods + +```typescript +requestPostCallSummary(): Promise; +sendPostCallSummaryResponse(response: AISummaryResponse): Promise; +requestMidCallSummary(action: AISummaryAction): Promise; +sendMidCallSummaryResponse(response: AISummaryResponse, action: AISummaryAction): Promise; +readonly aiSummaryCapabilities: Readonly; +``` + +Initiating mid-call and post-call content is delivered through the request Promise. There is no +public `TASK_POST_CALL_SUMMARY`, `TASK_MID_CALL_SUMMARY`, or +`TASK_MID_CALL_SUMMARY_FOR_RECEIVING_AGENT` export in this SDK. The older proposed +`PostCallSummaryEventPayload`, `MidCallSummaryEventPayload`, and +`MidCallSummaryReceivingAgentPayload` exports do not exist either. Consumers use the public +`AISummary` result type and the two Task events below. + +## Task events + +| SDK enum member | Event string | Payload / behavior | +| --- | --- | --- | +| `TASK_EVENTS.TASK_MID_CALL_SUMMARY_RECEIVED` | `task:midCallSummaryReceived` | Receiving-agent `AISummary` content; the store validates correlation and the restricted adaptive-card rendering contract. | +| `TASK_EVENTS.TASK_FEATURE_ENABLEMENT` | `task:featureEnablement` | Interaction-scoped `AISummaryFeatureEnablement`; only exact boolean `true` enables a capability. | + +Subscribe on the matching Task and remove the exact listener at lifecycle cleanup. Receiving-agent +eligibility begins only when its content event arrives. Capability events alone do not establish +receiving-agent content eligibility. SDK TaskManager routes backend post-call/mid-call messages to +the internal request correlation machinery; widgets must not subscribe to invented public aliases. + +```typescript +task.on(TASK_EVENTS.TASK_FEATURE_ENABLEMENT, onFeatureEnablement); +task.on(TASK_EVENTS.TASK_MID_CALL_SUMMARY_RECEIVED, onReceivingSummary); +const initiatingSummary = await task.requestMidCallSummary('CONSULT'); +const postCallSummary = await task.requestPostCallSummary(); +// Matching lifecycle cleanup: +task.off(TASK_EVENTS.TASK_FEATURE_ENABLEMENT, onFeatureEnablement); +task.off(TASK_EVENTS.TASK_MID_CALL_SUMMARY_RECEIVED, onReceivingSummary); +``` + +These examples describe the SDK boundary. Production widgets access it only through `@webex/cc-store`. + +## Exported payload types + +The following fields are read from the packed public declarations. Optional fields remain optional; +widgets must validate untrusted incoming values at runtime. +`AISummaryCapabilities` is the declaration-local alias used by `ITask`; it is not independently +re-exported by the package entrypoint. Consumers can derive it from `ITask['aiSummaryCapabilities']`. + +```typescript +type AISummaryAction = 'CONSULT' | 'TRANSFER'; +type AISummaryFeedback = 'none' | 'thumbs_up' | 'thumbs_down'; +type AISummaryState = 'DEFAULT' | 'EXCLUDED' | 'IGNORED' | 'MID_CALL_CANCELLED' | 'NOT_RECEIVED'; + +type AISummarySections = { + initialContactReason?: string; + additionalContactReasons?: string; + additionalContext?: string; + keyActionsTaken?: string; + nextSteps?: string; + reasonForTransferOrConsult?: string; +}; + +type AISummary = { + conversationId: string; + adaptiveCard?: Record; + adaptiveCardId?: string; + editAdaptiveCard?: Record; + editAdaptiveCardId?: string; + areTranscriptsAvailable?: boolean; + languageCode?: string; + resolution?: string; + sections?: AISummarySections; + suggestedWrapUpCodes?: Array<{name: string; [key: string]: unknown}>; + suggestedWrapUpCodesMessage?: string; + summaryText?: string; + timestamp?: number; + [key: string]: unknown; +}; + +type AISummaryFeatureEnablement = { + interactionId: string; + midCallEnabled?: boolean; + postCallEnabled?: boolean; + actionTimestamp?: number; + [key: string]: unknown; +}; +type AISummaryCapabilities = Required>; + +type AISummaryResponse = { + summary: AISummarySections | string; + feedback: AISummaryFeedback; + state: AISummaryState; + numberOfTimesViewed: number; + numberOfTimesEdited: number; + numberOfTimesCopied: number; + summaryReceived?: boolean; + wrapUpCode?: string; +}; +``` + +`AISummaryResponse` has no timestamp field. The SDK generates transport timestamps when sending. +Result ordering reads `AISummary.timestamp`; feature-enablement ordering reads +`AISummaryFeatureEnablement.actionTimestamp`. The store normalizes these into its internal ordering +model and prevents a lower timestamp or stale ownership generation from replacing current state. + +## Rendering and response composition + +Initiating mid-call and post-call content use supported structured section strings, then nonblank +`summaryText` if structured content is absent. They ignore adaptive cards. Receiving-agent content +uses the restricted adaptive-card renderer and ignores structured/plain alternatives. An unsupported +successful response displays `The summary is not available` under the product eligibility rules. + +Structured response keys are the SDK keys above. Labels are presentation data and never become +wire keys. Preserve original section bytes and accepted edits, including intentionally cleared +values; do not send rendered labels or inject HTML. `resolution` is optional display-only Outcome +metadata outside `AISummarySections` and must not become an additional submitted section. + +The shared `AISummaryState` union applies to both post-call and mid-call responses. The SDK accepts +an empty `summary` string with `state: 'NOT_RECEIVED'`, zero counters, and `feedback: 'none'` for an +unavailable mid-call summary. It does not require a prior successful request to send this response; +it derives correlation from the Task if no retained request context exists. This supports the +required unavailable-response invocation before consult/transfer while preserving telephony progress +after a failed summary response. `summaryReceived` is accepted in the public type but the inspected +SDK does not forward it as a transport field. + +## SDK transport and sequencing + +| Operation | Backend event | +| --- | --- | +| Request post-call | `GET_POST_CALL_SUMMARY` | +| Request CONSULT mid-call | `GET_MID_CALL_CONSULT_SUMMARY` | +| Request TRANSFER mid-call | `GET_MID_CALL_TRANSFER_SUMMARY` | +| Send post-call response | `POST_CALL_SUMMARY_RESPONSE` | +| Send CONSULT mid-call response | `MID_CALL_CONSULT_SUMMARY_RESPONSE` | +| Send TRANSFER mid-call response | `MID_CALL_TRANSFER_SUMMARY_RESPONSE` | + +The SDK sends these through `ApiAIAssistant.sendEvent` as `CTI_EVENT`, using the Task's interaction, +agent, and conversation correlation. Request conversation identity is +`task.data.interaction.mainInteractionId` when present, otherwise the Task interaction ID. +Response sends retain successful request correlation when available. Response metadata contains the +summary, feedback, state and counters; post-call adds `wrapUpCode`, while mid-call adds `agentName`. +Widgets supply the selected wrap-up code ID, not its display label. + +The SDK request/transport timeout is 15,000 ms. There is no public summary cancellation method. +The widget does not automatically retry failures or propagate late results after timeout. A supported +explicit post-call Retry starts a fresh request. The HTTP transport's fulfilled/rejected Promise is +the send confirmation boundary; `webex.request` rejects failed HTTP requests. Tests must simulate a +503 by rejecting that transport Promise, not by resolving a fake success object with status 503. + +Mid-call response submission precedes the existing consult/transfer action. A pending generation +disables further initiation. Complete wrap-up first, using the existing wrap-up API and payload, +then send the frozen post-call response exactly once through the same captured Task reference. +No response-only retry is added. The packed runtime probe verifies that the captured Task can send +after its wrap-up Promise fulfills. This retained operation does not permit arbitrary new summary +requests after terminal cleanup. + +The store must retain interaction/agent/generation ownership, stale-result rejection, per-role +content and counters, and the final frozen post-call response as specified in `requirement.md`. +SDK implementation details do not remove transfer/conference or terminal cleanup requirements. + +## Validation and known sealed-package defect + +`packages/contact-center/store/tests/ai-summary-contract.ts` loads the actual candidate package +entrypoint, with HTTP fakes only at the transport boundary. It checks public method signatures, +feature events, receiver events, CONSULT/TRANSFER correlation, 15-second timeout cleanup, failed +HTTP sends, unavailable mid-call responses, and same-Task post-wrap-up response submission. Its +TypeScript compiler probe resolves `@webex/contact-center` to the same candidate's declarations. + +`tooling/src/ai-summary-sdk-lock.js verify-sdk` always extracts the hash-matched tarball and validates +its reachable public declaration graph before running that suite. Legacy receipts without +`packedRuntimeHarness` do not skip validation. Installed bytes and lock checksum are also checked; +an installed conforming package cannot hide a malformed candidate declaration. + +At the inspected commit, `ApiAIAssistant.requestAndWaitForRtd` clears the previous same-type, +same-conversation request's timer and replaces the pending map entry without rejecting its Promise. +A CONSULT request superseded by TRANSFER therefore remains pending indefinitely. The regression +requiring that superseded Promise to settle remains failing and blocks SDK verification. Fixing this +requires explicit SDK repair and a new built/sealed candidate; changing an event alias or silently +removing the regression does not resolve it. + +## Widget release evidence + +SDK admission, widget package inspection, and final release evidence are separate checks. Local +development may use the private root resolution; SDK descriptors in workspace artifacts and root +dependency maps must be plain registry SemVer. Packing inspects actual archive paths and manifests, +rejects vendored archives, and compares the actual file list to its recorded receipt. + +Release gates require explicit passing status, repository-contained evidence paths, matching SHA-256 +bytes, and the current release HEAD. A hash-shaped string or a missing status is not proof of a pass. +Existing archived flow evidence remains unchanged. Only actually executed passing checks may produce +new success evidence. Widget logs/callbacks never include summary content, identifiers or raw errors. diff --git a/design/default/design_spec.md b/design/default/design_spec.md new file mode 100644 index 000000000..41bc79d5f --- /dev/null +++ b/design/default/design_spec.md @@ -0,0 +1,1303 @@ +# Design Specification: WxCC AI Assistant Mid-Call and Post-Call Summaries + +## Operator-approved layout clarification — 2026-09-25 + +The user approved aligning this one layout detail with REQ-009, REQ-011 and the +sealed screenshots without rerunning design. This clarification supersedes all +earlier single-outer-scrollbar / no-nested-scrollbar clauses and corresponding +scroll-owner test expectations below and in the implementation DAG narratives; +no SDK, state, interaction, task dependency or acceptance command is changed. + +CallControl summary panels remain bounded by the existing 80vh envelope. Keep +the panel heading, summary action row and post-call completion action visible; +long destination/reason lists and the shared summary content region may scroll +independently as shown by the targets. Individual keyed editors still auto-size +without their own scrollbars. Preserve all text and keyboard access, vertical-only +overflow, state and focus. Tooltip overhang must not be clipped by the shell. +The CSS custom property `--cc-summary-panel-width` may supply a host panel width; +the default remains 30rem and available viewport width remains an upper bound. + +Visual targets are panel-relative crops, not whole sample-host viewports. Mount +the same production component chain in a source-sized host, retain any declared +negative-origin tooltip margin, settle geometry before interactions, and assert +panel/heading/action/tooltip bounds before and after each source-sized capture. +Keep intentional content scrolling distinct from accidental frame clipping. +Do not resize, mask or relax the targets or their comparison threshold. + +## Overview + +### Operator-approved acceptance handoff repair — 2026-09-25 + +For D8 and D9, the flow's UX preparation task owns the visual plan before host +rendering and model inspection. Their acceptance commands consume that existing +plan; they must not call `prepare-ux-visual-comparison` and invalidate the +inspection handoff. Final validation requires the current plan-bound handoff +and exits nonzero on any error, never substituting a draft. This supersedes +the acceptance-owned preparation instructions below. All source checks, +browser/unit/build/packaging/release checks, target fidelity, DAG dependencies, +and product behavior remain unchanged. No design-stage rerun is required. + +This design adds voice-only, in-memory AI summaries to the existing Contact Center call-control and AI Assistant surfaces. `@webex/cc-store` remains the only SDK boundary: it validates capabilities and role-specific payloads, owns interaction/agent/ownership state, invokes the four summary methods, routes the receiving-agent event, and appends content-free status transitions to an ordered, observable delivery backlog consumed by call control. `@webex/cc-task` and `@webex/cc-ai-assistant` orchestrate the existing containers. `@webex/cc-components` remains SDK-free and renders normalized props. The existing `` element and every existing host property remain compatible; the only host addition is its optional content-free `onAISummaryStatusChange` function property. That additive custom-element guarantee does not cover direct React consumers: D6b changes the exported `wrapupCall` callback type in the published `@webex/cc-task` and `@webex/cc-components` TypeScript surfaces from a synchronous return to `(wrapUpReason: string, auxCodeId: string) => Promise`. The closed result distinguishes wrap-up failure from irreversible wrap-up success plus `not-required`, `submitted`, or `response-failed` response settlement and the Promise never rejects. Direct React consumers that provide a synchronous callback must migrate, so the release containing this change is SemVer-major for both packages and must carry semantic-release breaking-change metadata. Initiating-agent requests contribute `kind: 'mid-call'` `available`/`unavailable`, receiving-agent content events contribute the same two states after store validation, and post-call generation/submission contributes `kind: 'post-call'` `available`/`unavailable`/`submitted`/`response-failed`. Receiving content is rendered by the AI Assistant surface, but its status follows `SDK event -> @webex/cc-store ordered transition backlog -> observer render -> acknowledgement effect -> onAISummaryStatusChange`; neither widget imports the other. The existing `` tag and all of its properties, callbacks, events, and omission behavior remain backward compatible and receive no new host contract. + +The implementation target branch is `ai-assistant-summary`. Target behavior comes from `requirement.md` (SHA-256 `560cb55ea72f4f70d679214607527a6d5d39e398317dfbc75500b685fe91955f`); `ai-summary.md` (SHA-256 `3f5763c60ca40ec0903ffcfb287120ed1c21a3e12d899c32dd5a8c2dce6f3c28`) and a conforming packed SDK define only the concrete API translation. Requirement decisions override either API source for widget behavior. Existing module specifications are DRAFT and were cross-checked against live source. + +The inspected sibling SDK checkout is `/Users/fsiyavud/Documents/Projects/WebexDevPlatform/webex-js-sdk`, clean on `cc-summaries` at commit `15f3da706d84c10bc632abe252cea202a70ae212`, with repository-declared `yarn@3.4.1`. Its Contact Center source manifest intentionally omits `version`; the SDK release workflow assigns package versions during packaging. Its public surface exposes the intended `TASK_EVENTS.TASK_FEATURE_ENABLEMENT` / `task:featureEnablement` event on the matching `Task`, the unified `AISummary`, `AISummaryResponse`, `AISummaryAction`, `AISummaryFeedback`, `AISummaryState`, `AISummarySections`, and `AISummaryFeatureEnablement` types, the four required Task methods, and the 15,000 ms request timeout. The checkout is therefore an admissible source candidate rather than an already admitted artifact. D0 must leave it clean, copy the exact commit to an isolated stage, derive `-cc-summaries.<12-character-commit>` from the sole publishable `@webex/contact-center` dependency after removing its prerelease suffix, assign that version only in the stage, build and pack there, and prove the public declarations and runtime behaviors including same-Task post-wrap-up response callability. The immutable handoff evidence is the source branch/commit, staged and packed version, Node version, repository-declared Yarn pin plus matching Corepack observation, exact isolated install/build/pack/focused-contract-probe argv, tarball SHA-256, sorted full-file manifest, declaration hashes, and runtime traces. D0 transactionally records that evidence in `design/default/sdk_package_lock.json`, preserves D0a UX evidence, leaves the sole publishable store dependency byte-identical as its plain registry semver, selects the sealed archive only through the private root resolution, and fails closed until every probe passes. D8b validates that actual packed widget manifests contain only registry-resolvable SDK dependencies and exclude both the private override and vendored tarball; this development run does not require the staged SDK version to be registry-published. D2b and all later SDK-bound tasks remain blocked only until D0 seals and verifies the candidate, with no cast, source edit, installed-package fallback, or inherited package-manager configuration allowed. + +The UX authority is the ten local scene-graph/text/PNG triples in `requirement.md`, with expected UX manifest SHA-256 `58d925e6d12c377589120f39fc170f1d80692d1901522fce0959b70e17f8c1b4`, source-manifest SHA-256 `a266410383fc773812cd0a3352313d8db5e3e3828d5e584aaeee1df42709f162`, and source identity SHA-256 `50073d2c12591ac4c682e8599c1ecae9f2797cae9ed85542bfebf068b5ebfecb`. Those values are expected identities, not proof that the implementation checkout contains the inputs: the paths currently resolve only in the parent/common checkout. The one admissible resolution root is the canonical target worktree root returned by `git rev-parse --show-toplevel`; the receipt records that root portably as `ux.resolutionRoot: "."`, and every one of the thirty paths is relative to it. Before D3, D0a derives the canonical common-checkout root from the target worktree's absolute Git common directory and performs the only permitted cross-checkout operation: `stage-ux` copies exactly the thirty named regular files, byte for byte and without symlinks, from their parent/common-checkout-relative paths into the same `.ccwidgets/**` paths under a temporary directory in the target worktree, verifies the expected source and staged hashes, and atomically promotes the staged set. `seal-ux` then closes and reopens the promoted target files and independently rehashes every byte from the target root; it never trusts the copy-time digest or resolves back to the parent. A missing path or mismatch exits nonzero, leaves the receipt unchanged, blocks D3 and every downstream UI/visual task plus AC-15, and requires a fresh stage/seal attempt. Only a complete post-copy rehash atomically records the target-worktree-relative paths and hashes in `design/default/sdk_package_lock.json`. D8 re-resolves and rehashes the already sealed target-root inputs before preparing a plan, passes that same canonical root to every common-checkout MatrixBuilder child as the required absolute `--ux-input-root` argument, and rejects a plan/child whose recorded root differs; Playwright itself runs from that target root. Thus the common checkout owns only the generated `.matrix` output/control plane, never a second input lookup. Figma MCP was not used and is forbidden for this work. + +Externally visible outcomes are: initiating agents can review/edit/copy/rate a consult or transfer summary below destination results; receiving agents open an adaptive-card summary from `View summary` in the existing AI Assistant panel; agents generate/edit/copy/rate a post-call summary between wrap-up reason selection and Complete Wrap-Up; and hosts may observe content-free availability/submission states. On an A-to-B-to-C transfer, the confirmed cross-agent ownership change immediately removes Agent B's summary from the visible projection and gives Agent C an empty receiver view with zero counters while C waits for a new full-history summary. Adding a conference participant is a same-agent regeneration boundary: that agent's prior summary and counters remain visible while generation is pending or fails and are replaced only when a valid newer per-agent summary is admitted; if the conference returns to two participants, it becomes a call and the oldest remaining initiating agent is the host. Visible generation failures show `Having trouble generating summary`, while an otherwise successful role payload that cannot be rendered shows `The summary is not available`. The only `Retry` is the post-call generation-error action and it always starts a fresh `requestPostCallSummary()` call. While its interaction remains current, a frozen failed `sendPostCallSummaryResponse` payload is never re-sent and is discarded only on the matching `AgentWrappedUp`; the independent interaction/session privacy boundary also clears it, and no manual response-resend affordance exists. Non-voice channels, Agent Desktop-native slots, SDK/backend implementation, automatic retries, summary-request cancellation, response-only retry, new telemetry, persistence, clipboard-lifetime control after the OS accepts a user-initiated copy, new custom elements, localization infrastructure, RTL layout, and feature-owned live regions remain outside this widget design. + +## Feature Disposition Matrix + +| Fix # | Disposition | Reference | +| --- | --- | --- | +| PRE-001 | Addressed | requirement.md:L3-L5 (`REQ-013` in the assigned review packet) -> [Change: Deterministic UX and Release Validation](#change-deterministic-ux-and-release-validation); D9 parses the single non-empty value under `## Branch`, requires the flow-authenticated resolved promotion target to equal `ai-assistant-summary`, binds that target to the current HEAD commit, and treats an admissible MatrixBuilder worktree branch only as execution metadata. A wrong target, stale/head-mismatched receipt, or arbitrary branch fails release validation. | +| PRE-002 | Addressed | requirement.md:L7-L9 -> [Component: Interaction Summary Store and SDK Adapter](#component-interaction-summary-store-and-sdk-adapter) | +| PRE-003 | Deferred | requirement.md:L11-L11 (`REQ-015` in the assigned review packet) -> `D2-interaction-summary-store` implements requirement-first normalization and forbids invented SDK contracts. The API-admission half remains Deferred only until D0 stages, packs, and verifies the clean `cc-summaries` candidate exposing the task-owned `task:featureEnablement` API and unified public types. | +| PRE-004 | Deferred | requirement.md:L13-L13 (`REQ-016` in the assigned review packet) -> D0 must assign a deterministic development version only in an isolated stage, then build, pack, hash, declaration-compile, runtime-probe, and record the exact clean source commit without requiring a source-manifest version. | +| PRE-005 | Addressed | requirement.md:L15-L15 -> [Component: UX Evidence Admission](#component-ux-evidence-admission) and [Change: Deterministic UX and Release Validation](#change-deterministic-ux-and-release-validation); D0a seals only repository-local inputs, and D9's `verifyUXEvidence` rehashes every local source plus the flow-enumerated changed/evidence files and rejects executable or structured provenance for a Figma MCP endpoint, tool call, remote file/node URL, or node fetch. Narrative prohibition alone is not acceptance evidence. | +| REQ-001 | Deferred | requirement.md:L17-L35 -> D0 must admit the clean `cc-summaries` source candidate as a deterministically versioned staged package and verify `TASK_EVENTS.TASK_FEATURE_ENABLEMENT` / `task:featureEnablement`, the unified summary methods/types, optional inbound `AISummary.timestamp`, strict receiver action correlation, 15-second cleanup, and same-Task post-wrap-up send callability. D8b validates the publishable widget-package boundary without requiring the development-only SDK version to be registry-published. Before D0 clears, only D0a, D1, D2, and dependency-ready D3 may execute. | +| REQ-002 | Addressed | requirement.md:L36-L45 -> [Component: Interaction Summary Store and SDK Adapter](#component-interaction-summary-store-and-sdk-adapter); D1 fixtures; D2/D2b eligibility, isolated state, a Store/session-owned owner-generation-keyed wrapped-up subscription registry that survives ordinary Task replacement/removal, and exhaustive per-interaction map eviction when the matching event or interaction/session boundary wins; D3 shared hidden/unavailable presentation; D4 receiver, D5 initiating, and D6 post-call role rendering; D6b shared CallControl integration; and D9 release validation. | +| REQ-003 | Addressed | requirement.md:L46-L55 -> [Component: Interaction Summary Store and SDK Adapter](#component-interaction-summary-store-and-sdk-adapter), [Component: Initiating-Agent Call Control Integration](#component-initiating-agent-call-control-integration), and [Component: Shared Summary Presentation](#component-shared-summary-presentation); D2 normalizes initiating typed sections into an ordered array in SDK payload order, and display, clipboard export, and response composition iterate that array rather than presentation definitions. Sections retain their exact SDK keys and are only visually collapsed into the one bottom editor, so each edit round-trips by exact key without parsing labels or delimiters. D2 declares `AISummaryActionType` once in `store/src/store.types.ts`; the existing store index re-exports it through `@webex/cc-store`, and D5/D6b import that type instead of declaring a task- or component-local `CONSULT`/`TRANSFER` alias. The public component uses the single `mode` values `'mid-call-initiator'`, `'mid-call-receiver'`, and `'post-call'` discriminator with mode-specific content and action props, so D4-D6 cannot construct mismatched kind/role props. D2b normalizes the first-invoked response send to an always-fulfilled `sent`/`failed`/`blocked`/`stale` outcome. Every D5/D6b internal confirmation call site awaits that settlement before invoking exactly one unchanged telephony action; `failed`, `blocked`, and `stale` still continue after settlement, and integration tests prove no consult, transfer, or conference API starts while response submission is pending. | +| REQ-004 | Addressed | requirement.md:L56-L63 -> [Component: Receiving-Agent AI Assistant Integration](#component-receiving-agent-ai-assistant-integration) and [Component: Shared Summary Presentation](#component-shared-summary-presentation); the task-bound store listener attributes a normalized receiver error to its recognizable interaction ID and captured owner generation, drops stale/non-current failures, raises unavailable for an accepted unrenderable valid envelope, and forwards content-free availability only through the call-control host callback. The store-owned receiver view raises an exact `View summary` action in the existing AI Assistant chrome trigger slot and opens the established panel branch. In that branch, `AIAssistantComponent` renders the display-only `AdaptiveCardRenderer` and the shared `AISummary` `mid-call-receiver` action surface as DOM siblings: the action controls are never descendants of the restricted renderer. `AISummary` owns the exact copy/feedback labels, controlled selection, direct-gesture copy, promise observation, and focus behavior, and obtains only extracted visible text through a synchronous getter. `AdaptiveCardRenderer` keeps its existing props and security boundary and gains no summary label, feedback, Clipboard, or action responsibility; inside that boundary its pre-parse image sanitizer allows only exact closed-map bundled Momentum asset values, removes every other image-bearing source before DOM creation, renders remaining text/actions, and routes a sanitized-empty image-only card to the exact visible unavailable fallback. Receiver copy and feedback React handlers return `void`, attach both settlement handlers before returning, normalize failures to closed outcomes, and never expose a rejecting Promise to React. | +| REQ-005 | Addressed | requirement.md:L64-L75 -> [Component: Interaction Summary Store and SDK Adapter](#component-interaction-summary-store-and-sdk-adapter) for the retained `AgentWrappedUp` boundary, [Component: Post-Call Wrap-Up Integration](#component-post-call-wrap-up-integration) for wrap-up-first capture/send/failure behavior, and [Component: Shared Summary Presentation](#component-shared-summary-presentation) for labeled structured sections and the unlabeled plain paragraph. Every admitted labeled SDK section retains D2's normalized SDK payload order. A normalized non-empty top-level `resolution` produces one display-only `Outcome` row inserted by its typed ordinal in `POST_CALL_DISPLAY_SECTION_ORDER`, never by named neighbours and never by reordering SDK sections; a missing, `undefined`, or empty normalized value omits the row completely, including any placeholder, divider, or reserved space. D6 retains the current accepted draft, edits, revision, counters, local feedback, and pending description across reason reselection and popover close/reopen. Each explicit reason commit installs a new monotonic generation and makes older in-flight work stale before timestamp comparison; pending, blocked, failed, unsupported, or superseded work cannot clear or mutate the draft. Only a ready response in the installed generation replaces content, preserves edited/copied totals, increments viewed once, and resets the superseded feedback/status. Initial generation may defer completion only while its store-owned request is pending (bounded by the existing 15-second timeout); a terminal generation error or unavailable result with a committed reason enables completion without summary content, and Retry cannot revoke that escape. D6's private `completeWrapupWithSummary` is the sole `task.wrapup` implementation for eligible-with-draft, eligible-without-draft, and legacy branches; D6b's exact-Promise `wrapupCall` delegates to it. All branches normalize wrap-up failure to the non-rejecting `WrapupCompletionResult`, commit/report wrap-up success before any response send, and isolate `response-failed` from wrap-up failure. D2b keeps the matching `AgentWrappedUp` subscription in a Store/session-owned owner registry, independent of ordinary Task listeners, so task removal cannot strand the failed frozen response; the matching event or interaction/session cleanup disposes the tombstone and every retained binding. | +| REQ-006 | Addressed | requirement.md:L76-L85 -> [Component: Interaction Summary Store and SDK Adapter](#component-interaction-summary-store-and-sdk-adapter), [Component: Initiating-Agent Call Control Integration](#component-initiating-agent-call-control-integration), [Component: Receiving-Agent AI Assistant Integration](#component-receiving-agent-ai-assistant-integration), and [Component: Post-Call Wrap-Up Integration](#component-post-call-wrap-up-integration). D5 defines a no-destination existing-party summary surface for the already-consulted party, D6b anchors it to the existing Transfer/Transfer Conference and Merge controls, and only the surface's explicit confirmation invokes the response before `task.transferConference()` or `task.consultConference()`. Transfer open requests `TRANSFER` once; Merge reuses the participant-add `CONSULT` preparation without conference-event request fan-out. D2/D2b and D4 distinguish ownership changes: a cross-agent successor immediately projects no predecessor content and starts zero counters, while a same-agent conference/regeneration successor keeps the current card, feedback, and counters visible during pending or failed preparation and discards them only when valid replacement content is admitted. D6 binds every post-call request and captured draft to the current authenticated agent's ownership generation under the unchanged canonical interaction ID: after A-to-B-to-C, only C can own the wrap-up draft and C starts zero post-call counters, while B's late work is stale. Its conference fixture proves the final SDK-generated post-call content includes the conference segment without widget-side transcript assembly or truncation. | +| REQ-007 | Addressed | requirement.md:L86-L101 -> [Change: Host Status Callback and Existing Web Component](#change-host-status-callback-and-existing-web-component); the Store-lifetime monotonic sequence, ordered observable backlog, exact-head acknowledgement-before-local-invocation, render-time MobX read, and effect drain provide same-agent remount deduplication plus unmounted/batched transition no-drop delivery. The callback is exposed through a property-only bridge and is deliberately absent from r2wc `observedAttributes`, so an HTML attribute cannot resolve a global function. `AISummaryStatusDetail` is type-only re-exported from both `@webex/cc-task` and the host-facing `@webex/cc-widgets` entry, and the AI Assistant host contract is unchanged. D6 pins the legacy/custom-element-backed ineligible wrap-up behavior without editing a public seam; D6b alone owns the separate published React `wrapupCall` Promise migration, proves the existing custom-element path unchanged, and requires a SemVer-major migration for direct React consumers. | +| REQ-008 | Addressed | requirement.md:L102-L112 -> [Component: Interaction Summary Store and SDK Adapter](#component-interaction-summary-store-and-sdk-adapter), [Component: Initiating-Agent Call Control Integration](#component-initiating-agent-call-control-integration), [Component: Post-Call Wrap-Up Integration](#component-post-call-wrap-up-integration), and [Component: Shared Summary Presentation](#component-shared-summary-presentation); initiator action type comes from the owning accepted request and is re-evaluated on consult-to-transfer promotion. Every D5/D6b mid-call feedback callback forwards the rendered `AISummaryActionType` and Store-lifetime-unique content revision with `Exclude`; D2b rejects either mismatch before pending state, composition, or an SDK call, so a queued CONSULT click cannot submit after promotion or owner/content advancement. D5 targets view/edit/copy counters and success-gated mutually exclusive/reselectable feedback to the initiating role, while post-call feedback requires the current revision, paints immediately only when the local boolean guard returns true, and projects `pending` through `aiSummary.feedback.pendingSubmission` as the structural-only description `Pending submission`. Response failure preserves the read-only selection and projects `not-confirmed` through `aiSummary.feedback.submissionNotConfirmed` as the structural-only description `Submission not confirmed`; confirmed success removes it. In either present state, both Like/Dislike controls reference the ordinary visible description via `aria-describedby`, with no live-region semantics; stale/ineligible/captured/frozen input changes nothing. | +| REQ-009 | Addressed | requirement.md:L113-L123 -> [Component: Shared Summary Presentation](#component-shared-summary-presentation), [Component: Receiving-Agent AI Assistant Integration](#component-receiving-agent-ai-assistant-integration), [Component: Initiating-Agent Call Control Integration](#component-initiating-agent-call-control-integration), [Component: Post-Call Wrap-Up Integration](#component-post-call-wrap-up-integration), and [Change: Deterministic UX and Release Validation](#change-deterministic-ux-and-release-validation); `D3-shared-summary-presentation` binds the exact generating/error/unavailable/status copy and implements pre-removal successor capture. The successor search stays inside the same summary subtree. Its closed fallback table names all three live targets: D5's `consultTransferPanelRef.current` for the exact open Consult/Transfer `.agent-popover-content`, D4's `panelRef.current` for `[data-testid="ai-assistant:panel"][role="dialog"]`, and D6's `wrapUpPanelRef.current` for the exact open wrap-up `.agent-popover-content`; each root is connected, ref-scoped, and `tabIndex={-1}`. D8 owns a successor-present case plus one named scenario that removes the last focusable summary control for each surface and asserts the exact ref becomes `document.activeElement`, while close paths still restore their opening triggers and ordinary connected updates retain focus. D5 also preserves the latched destination tree and an 80vh shell with bounded list/content scroll regions and visible headings/actions; D6 preserves accessible reason controls, non-live zero-match text, deduplicated commitment, and a scroll-reachable Complete Wrap-Up at 320px. D9 rejects missing/ambiguous/body/wrong-instance focus receipts. | +| REQ-010 | Addressed | requirement.md:L124-L131 -> [Component: Shared Summary Presentation](#component-shared-summary-presentation), [Component: Interaction Summary Store and SDK Adapter](#component-interaction-summary-store-and-sdk-adapter), [Component: Receiving-Agent AI Assistant Integration](#component-receiving-agent-ai-assistant-integration), [Component: Initiating-Agent Call Control Integration](#component-initiating-agent-call-control-integration), and [Component: Post-Call Wrap-Up Integration](#component-post-call-wrap-up-integration); all request, mid-call feedback, and pre-action store Promises catch/categorize internally and fulfill closed sanitized results, and the D5 popover open/effect path immediately installs explicit fulfillment and rejection observers for its auto-started request so timeout/generation failure maps to normalized state with zero floating rejection. D4-D6 log no raw summary payload, raw error, interaction/agent identifier, or text; at the store normalization boundary only, the existing `ILogger.trace` sink may receive one content-free diagnostic containing an already-defined normalized `AISummaryErrorCategory` or `response-failed` literal plus static module/method context, outside the metrics path; D4's restricted renderer removes every non-allowlisted card image source before parse/DOM creation, permits only exact bundled Momentum values, and therefore opens no image egress channel. `AISummary` uses non-`async` React handlers that catch synchronous throws and attach both settlement arms to Clipboard, `onFeedback`, and `onRetry` Promises before returning `void`, so no UI action can leak an unhandled rejection. | +| REQ-011 | Addressed | requirement.md:L132-L138 -> [Component: UX Evidence Admission](#component-ux-evidence-admission) and [Change: Deterministic UX and Release Validation](#change-deterministic-ux-and-release-validation); D0a copies the thirty parent-only inputs byte-for-byte into the target worktree through the explicit `stage-ux` transaction, then `seal-ux` independently reopens/rehashes them under portable `ux.resolutionRoot: "."` before D3. UX-001 supplies the one voice hierarchy and Consult heading; Transfer remains a product-confirmed structural-only string. Percent-delimited slots and S02 punctuation are resolved through exact DOM/copy assertions and closed correction masks. The complete structural-only table is the D8/D9 denominator, including receiver, hidden, unavailable, all nine mid-call treatment extrapolations, separate Consult/Transfer pending tuples, feedback descriptions, lifecycle, ownership, submission, and S10 override rows. Screenshot comparison is structural and semantic-token-aware: tokenized fill/stroke colors are region-masked, tokenized foreground uses a color-independent coverage gate, and only a non-empty literal RGB mask uses the `<=0.5%` threshold; no whole-root pixel percentage can contradict required Momentum tokens or host-container sizing. Every mask is predeclared by node/role and exact geometry/copy/contrast/forced-colors remain blocking. | +| REQ-012 | Addressed | requirement.md:L139-L146 -> [Component: Initiating-Agent Call Control Integration](#component-initiating-agent-call-control-integration) and [Change: Deterministic UX and Release Validation](#change-deterministic-ux-and-release-validation); D5 names focused component/module cases for pending/removal/close focus, open-request and feedback rejection with zero unhandled rejection, radio/pill filtering parity, and no-destination transfer/conference surfaces. D8 owns both explicitly collected no-auth Playwright specs, their non-overlapping registrations, the exact project `testDir` and two-path `testMatch`, and the sealed-input visual comparison/classification artifacts. Its init script supplies the versioned `store.init({webex})` sample-host bridge. Every MatrixBuilder child runs from the derived common-checkout output root but receives the same canonical target worktree as the required absolute `--ux-input-root`; prepare records that root plus freshly rehashed inputs, render/export/compare reuse it, and validate receives it again. Missing, changed, cwd-relative, or parent-fallback resolution fails before rendering. | +| PRE-006 | Addressed | requirement.md:L148-L150 -> [Change: Deterministic UX and Release Validation](#change-deterministic-ux-and-release-validation) | +| UX-001 | Addressed | requirement.md:L152-L160 -> [Component: Initiating-Agent Call Control Integration](#component-initiating-agent-call-control-integration) (`D5-mid-call-call-control`) for the unconditional voice hierarchy, open-instance-latched voice/non-voice destination layout, resolved delimiter-free source copy, conditional summary append, and responsive bounded-list/content layout. UX-001 supplies only the Consult heading. The Transfer candidate is separately classified structural-only under `aiSummary.midCall.transferHeading`, is never attributed to UX-001, and remains provisional until the AI Assistant product/content owner confirms or replaces it; D8 cannot pass without that confirmation and the named Transfer DOM/copy receipt. A mid-popover capability arrival or revocation cannot swap/reorder the destination picker or discard typed query, selected category/destination, or focus; admitted and verified by [Change: Deterministic UX and Release Validation](#change-deterministic-ux-and-release-validation) (`D0a-ux-evidence-admission`, `D8-browser-ux-validation`, `D9-release-validation`). | +| UX-002 | Addressed | requirement.md:L161-L169 -> [Component: Shared Summary Presentation](#component-shared-summary-presentation) and [Component: Post-Call Wrap-Up Integration](#component-post-call-wrap-up-integration) (`D3-shared-summary-presentation`, `D6-post-call-wrap-up`); verified by [Change: Deterministic UX and Release Validation](#change-deterministic-ux-and-release-validation) (`D8-browser-ux-validation`, `D9-release-validation`). The sealed text and S02 pixels both contain U+005F, recorded as a source artifact; production renders `Just a sec—the details are coming together.` with U+2014 and no underscore, and D8 masks only the fixed separator correction region while asserting the corrected DOM/accessibility string. Complete Wrap-Up is disabled for summary availability only during the transient initial no-draft generation request; terminal generation error or unavailable settlement for a committed reason re-enables it, and a retained draft keeps it available during regeneration. | +| UX-003 | Addressed | requirement.md:L170-L178 -> [Component: Shared Summary Presentation](#component-shared-summary-presentation) and [Component: Post-Call Wrap-Up Integration](#component-post-call-wrap-up-integration) (`D3-shared-summary-presentation`, `D6-post-call-wrap-up`); verified by [Change: Deterministic UX and Release Validation](#change-deterministic-ux-and-release-validation) (`D8-browser-ux-validation`, `D9-release-validation`). Paired fixtures cover a non-empty top-level `resolution` and a key-omitted payload; the former inserts read-only `Outcome` by relative ordinal without sorting SDK sections, while the latter renders no row or reserved space. | +| UX-004 | Addressed | requirement.md:L179-L187 -> [Component: Shared Summary Presentation](#component-shared-summary-presentation) and [Component: Post-Call Wrap-Up Integration](#component-post-call-wrap-up-integration) (`D3-shared-summary-presentation`, `D6-post-call-wrap-up`); verified by [Change: Deterministic UX and Release Validation](#change-deterministic-ux-and-release-validation) (`D8-browser-ux-validation`, `D9-release-validation`). | +| UX-005 | Addressed | requirement.md:L188-L196 -> [Component: Shared Summary Presentation](#component-shared-summary-presentation) and [Component: Post-Call Wrap-Up Integration](#component-post-call-wrap-up-integration) (`D3-shared-summary-presentation`, `D6-post-call-wrap-up`); verified by [Change: Deterministic UX and Release Validation](#change-deterministic-ux-and-release-validation) (`D8-browser-ux-validation`, `D9-release-validation`). | +| UX-006 | Addressed | requirement.md:L197-L205 -> [Component: Shared Summary Presentation](#component-shared-summary-presentation) and [Component: Post-Call Wrap-Up Integration](#component-post-call-wrap-up-integration) (`D3-shared-summary-presentation`, `D6-post-call-wrap-up`); verified by [Change: Deterministic UX and Release Validation](#change-deterministic-ux-and-release-validation) (`D8-browser-ux-validation`, `D9-release-validation`). | +| UX-007 | Addressed | requirement.md:L206-L214 (`S07-UX` / `desktop-light` -> canonical evidence key `S07` / `dl`) -> [Component: Shared Summary Presentation](#component-shared-summary-presentation) and [Component: Post-Call Wrap-Up Integration](#component-post-call-wrap-up-integration) (`D3-shared-summary-presentation`, `D6-post-call-wrap-up`); verified by [Change: Deterministic UX and Release Validation](#change-deterministic-ux-and-release-validation) (`D8-browser-ux-validation`, `D9-release-validation`). | +| UX-008 | Addressed | requirement.md:L215-L223 -> [Component: Shared Summary Presentation](#component-shared-summary-presentation) and [Component: Post-Call Wrap-Up Integration](#component-post-call-wrap-up-integration) (`D3-shared-summary-presentation`, `D6-post-call-wrap-up`); verified by [Change: Deterministic UX and Release Validation](#change-deterministic-ux-and-release-validation) (`D8-browser-ux-validation`, `D9-release-validation`). The direct-gesture Clipboard Promise is observed at the call site: synchronous throw or rejection is caught and normalized to the content-free `failed` outcome, raw error data is discarded, and neither visible state, the copied counter, nor the post-fulfillment recorder changes. | +| UX-009 | Addressed | requirement.md:L224-L232 -> [Component: Shared Summary Presentation](#component-shared-summary-presentation) and [Component: Post-Call Wrap-Up Integration](#component-post-call-wrap-up-integration) (`D3-shared-summary-presentation`, `D6-post-call-wrap-up`); verified by [Change: Deterministic UX and Release Validation](#change-deterministic-ux-and-release-validation) (`D8-browser-ux-validation`, `D9-release-validation`). Confirmed copy treatment lasts exactly `COPIED_FEEDBACK_MS = 1_500` ms and reverts to `idle` on the first of timeout, copy-control focus loss, a second copy gesture, or accepted summary-content replacement; a second fulfilled copy starts a fresh full interval, while rejection leaves the reset state unchanged. Unmount performs timer cleanup without a state update. | +| UX-010 | Addressed | requirement.md:L233-L241 -> [Component: Shared Summary Presentation](#component-shared-summary-presentation) and [Component: Post-Call Wrap-Up Integration](#component-post-call-wrap-up-integration) (`D3-shared-summary-presentation`, `D6-post-call-wrap-up`); verified by [Change: Deterministic UX and Release Validation](#change-deterministic-ux-and-release-validation) (`D8-browser-ux-validation`, `D9-release-validation`). The sealed error source's exact visible title `Having trouble generating summary` already matches REQ-009; its exact subcopy is `It could be a lost connection or something else. Could you check your connection or try again later?`. Those are the resolved production strings for every visible `disabled`, `offline`, `unavailable`, `empty`, `timeout`, and `generic` generation-error category; authorization/initialization stays hidden and valid-unrenderable content uses unavailable copy. The broad subcopy scope is deliberate because `or something else` is non-diagnostic and `or try again later` is category-neutral recovery guidance, so the UI does not claim that a non-transport failure was caused by the connection. The source remains authoritative for hierarchy, copy, spacing, and Retry treatment, but its disabled primary-control treatment is not adopted after terminal generation failure because REQ-005 requires a bounded path out of wrap-up: D6 must prove completion succeeds as `not-required` after an unrecovered error, and D8 records the enabled control as the one structural override. | +| JNY-001 | Addressed | requirement.md:L242-L251 -> [Change: Deterministic UX and Release Validation](#change-deterministic-ux-and-release-validation); design coverage is complete, but implementation is blocked by `EXT-SDK-CC-SUMMARIES` until D0 accepts the external handoff. | +| JNY-002 | Addressed | requirement.md:L252-L261 -> [Change: Deterministic UX and Release Validation](#change-deterministic-ux-and-release-validation); design coverage is complete, but implementation is blocked by `EXT-SDK-CC-SUMMARIES` until D0 accepts the external handoff. | +| REQ-019 | Out-of-Scope | requirement.md:L264-L264 -> Out-of-Scope: eligibility rejects every non-voice task; no non-voice summary state or UI is introduced. | +| REQ-020 | Out-of-Scope | requirement.md:L265-L265 -> Out-of-Scope: this repository owns existing embedded widgets, not Agent Desktop-native slot placement. | +| REQ-021 | Out-of-Scope | requirement.md:L266-L266 -> Out-of-Scope boundary: this widget repository consumes and verifies but never modifies SDK or backend source. That boundary creates the hard external dependency `EXT-SDK-CC-SUMMARIES`: delivery is blocked until the Webex JS SDK Contact Center maintainers supply a conforming, versioned `cc-summaries` commit. See the named D0 precondition and the Deferred PRE-003/PRE-004/REQ-001/AC-01 admission gate. | +| REQ-022 | Out-of-Scope | requirement.md:L267-L267 -> Out-of-Scope: generation is user initiated, requests are not cancelled, and no automatic retry path exists. | +| REQ-023 | Out-of-Scope | requirement.md:L268-L268 -> Out-of-Scope: a frozen post-call response failure is never resent; Retry creates a new pre-wrap-up generation request only. | +| REQ-024 | Out-of-Scope | requirement.md:L269-L269 -> Out-of-Scope: existing wrappers remain, but this feature adds no telemetry event, identifier, duration, dimension, or failure vocabulary. | +| REQ-025 | Out-of-Scope | requirement.md:L270-L270 -> Out-of-Scope: en-US message IDs are retained, while translation infrastructure, feature RTL layout, and feature live regions are intentionally absent. | +| REQ-026 | Out-of-Scope | requirement.md:L271-L271 -> Out-of-Scope applies to custom-element/host compatibility: `widget-cc-call-control` receives only the optional additive callback, `widget-cc-ai-assistant` keeps its complete host contract, and no tag is added. It does not cover the published React props surface: D6b changes exported `wrapupCall` from a synchronous callback to `(wrapUpReason: string, auxCodeId: string) => Promise`, which is breaking for direct `@webex/cc-task` / `@webex/cc-components` React consumers and requires a SemVer-major release with semantic-release breaking-change metadata. | +| REQ-027 | Out-of-Scope | requirement.md:L272-L272 -> Out-of-Scope: summaries stay in memory and clipboard lifetime is controlled by the operating system after an approved user gesture. | +| AC-01 | Deferred | requirement.md:L276-L276 -> The current clean `cc-summaries` commit exposes the intended public API and is an admissible source candidate. AC-01 becomes Addressed after D0 stages a deterministic version, builds, packs, and verifies its immutable source/artifact/declaration/runtime evidence; D8b then proves actual packed widget manifests retain only registry-resolvable SDK dependencies and exclude development-only resolution state. | +| AC-02 | Addressed | requirement.md:L277-L277 -> [Component: Interaction Summary Store and SDK Adapter](#component-interaction-summary-store-and-sdk-adapter); implemented by `D2-interaction-summary-store` / `D2b-interaction-summary-sdk-binding` and verified by `D9-release-validation`. | +| AC-03 | Addressed | requirement.md:L278-L278 -> [Component: Initiating-Agent Call Control Integration](#component-initiating-agent-call-control-integration) and [Component: Shared Summary Presentation](#component-shared-summary-presentation); implemented by `D3-shared-summary-presentation`, `D5-mid-call-call-control`, and `D6b-call-control-summary-integration`, then verified by `D9-release-validation`. | +| AC-04 | Addressed | requirement.md:L279-L279 -> [Component: Receiving-Agent AI Assistant Integration](#component-receiving-agent-ai-assistant-integration) and [Component: Shared Summary Presentation](#component-shared-summary-presentation); D3 supplies the one receiver-capable `AISummary` action contract and D4 places that action surface beside the restricted card renderer as its `AIAssistantComponent` sibling, maps copy/feedback to the exact store actions, consumes all rejection paths, and paints only confirmed outcomes. The controls never enter the renderer boundary. Implemented by `D3-shared-summary-presentation` and `D4-receiving-ai-assistant`, exercised by D8, and verified by `D9-release-validation`. | +| AC-05 | Addressed | requirement.md:L280-L280 -> [Component: Post-Call Wrap-Up Integration](#component-post-call-wrap-up-integration) and [Component: Shared Summary Presentation](#component-shared-summary-presentation); implemented by `D3-shared-summary-presentation`, `D6-post-call-wrap-up`, and `D6b-call-control-summary-integration`, then verified by `D9-release-validation`. | +| AC-06 | Addressed | requirement.md:L281-L281 -> [Component: Interaction Summary Store and SDK Adapter](#component-interaction-summary-store-and-sdk-adapter), [Component: Receiving-Agent AI Assistant Integration](#component-receiving-agent-ai-assistant-integration), [Component: Initiating-Agent Call Control Integration](#component-initiating-agent-call-control-integration), and [Component: Post-Call Wrap-Up Integration](#component-post-call-wrap-up-integration); D2/D2b distinguish cross-agent immediate clear/zero counters from same-agent conference/regeneration carry-forward, D4 keeps the receiver card/counters visible through pending or failed preparation until valid replacement, and D6 proves that wrap-up belongs only to the current per-agent generation and that final SDK content retains the conference segment under the common interaction ID. Owned by `D2-interaction-summary-store`, `D2b-interaction-summary-sdk-binding`, `D4-receiving-ai-assistant`, `D5-mid-call-call-control`, `D6-post-call-wrap-up`, and `D6b-call-control-summary-integration`, then exercised by D8 and verified by `D9-release-validation`. | +| AC-07 | Addressed | requirement.md:L282-L282 -> [Component: Interaction Summary Store and SDK Adapter](#component-interaction-summary-store-and-sdk-adapter); D0 verifies the exact optional inbound `AISummary.timestamp?: number`; D2 orders comparable same-owner-role successful summaries by greatest timestamp/equal-last-arrival and falls back to later owner-role arrival when either candidate omits it, while D2b closes timed-out requests before either rule. Owned by D2, D2b, D6, and D6b, then verified by D9. | +| AC-08 | Addressed | requirement.md:L283-L283 -> [Component: SDK Supply Lock and Contract Admission](#component-sdk-supply-lock-and-contract-admission); D0 must prove on the same captured Task that `sendPostCallSummaryResponse` remains callable and can fulfill after an awaited successful `wrapup`; D1 supplies the explicit post-wrap-up-send fixture. `D2b-interaction-summary-sdk-binding`, `D6-post-call-wrap-up`, and `D6b-call-control-summary-integration` own the once-only frozen response, while `D7-host-status-callback` owns content-free `response-failed` reporting. Its store queue retains that transition through ordinary payload cleanup and a temporarily unmounted CallControl, then the observer render/effect exact-head-acknowledges and delivers it once on same-session remount; D9 verifies the chain. If the exact post-wrap-up probe is unsupported, D0 fails closed and these tasks remain blocked pending a conforming SDK commit; the widget never sends before wrap-up or treats deterministic SDK incompatibility as an ordinary response failure. | +| AC-09 | Addressed | requirement.md:L284-L284 -> [Change: Host Status Callback and Existing Web Component](#change-host-status-callback-and-existing-web-component); `D7-host-status-callback` tests all six valid `{kind,state}` literals, compile-time rejection of mid-call `submitted`/`response-failed`, the aggregator's type-only public export, the property-only/no-global-attribute Web Component surface, render-time MobX observability without prop churn, acknowledgement before locally caught invocation, deterministic zero-emission ownership/authorization boundaries, same-sequence/remount deduplication, batched back-to-back no-drop delivery, and unmounted-transition delivery, and `D9-release-validation` verifies the gate. D6's focused acceptance keeps the existing ineligible/custom-element-backed wrap-up path unchanged; D6b owns the public React callback-type migration and verifies that this internal seam change does not alter the optional host callback or existing custom-element behavior. | +| AC-10 | Addressed | requirement.md:L285-L285 -> [Component: Interaction Summary Store and SDK Adapter](#component-interaction-summary-store-and-sdk-adapter); owned by `D2-interaction-summary-store` / `D2b-interaction-summary-sdk-binding` and the D4-D6b integrations, then verified by `D9-release-validation`. | +| AC-11 | Addressed | requirement.md:L286-L286 -> [Component: Shared Summary Presentation](#component-shared-summary-presentation); `AISummary` owns the sole Clipboard API call for typed, plain, and receiver adaptive-card content directly in the user gesture. For receiver mode it synchronously invokes the visible-text getter that D4 backs with `extractCardText` over the sibling restricted-card branch; `AdaptiveCardRenderer` does not own summary copy or contain its control. D4-D6b receive only synchronous, revision-scoped post-fulfillment recorder calls and never call `navigator.clipboard`; the copied counter advances exactly once only after the write fulfills and the recorder accepts the rendered revision. This is implemented by `D3-shared-summary-presentation`, integrated by D4-D6b, exercised by `D8-browser-ux-validation`, and gated by `D9-release-validation`. | +| AC-12 | Addressed | requirement.md:L287-L287 -> [Component: Shared Summary Presentation](#component-shared-summary-presentation), [Component: Receiving-Agent AI Assistant Integration](#component-receiving-agent-ai-assistant-integration), [Component: Initiating-Agent Call Control Integration](#component-initiating-agent-call-control-integration), and [Component: Post-Call Wrap-Up Integration](#component-post-call-wrap-up-integration); the post-call view exposes only the exact `pending`/`not-confirmed` status token, and D3 resolves it through `aiSummary.feedback.pendingSubmission` / `aiSummary.feedback.submissionNotConfirmed` to the structural-only exact descriptions beneath the feedback controls. While either status exists, both Like/Dislike controls reference the visible description's instance-local ID through `aria-describedby`; confirmed success removes the node and references, and no feature live region is added. D8 owns separate named structural DOM/copy/accessibility scenarios for the pending and not-confirmed states. D4 names the receiver notification, action-row successor behavior, and `[data-testid="ai-assistant:panel"]` dialog fallback and tests notification activation, card replacement, and card-unavailable removal. D5 makes pending destination/action controls focus-retaining, names the popover-root and trigger-return fallbacks, and tests disappearance/replacement focus. D6 supplies a persistent exact accessible name for the reason search, labels the reason `RadioGroup` from its visible heading, and exposes the zero-match result as visible static text without a feature live region. D6b integrates the behavior, and D8/D9 exercise and gate it. | +| AC-13 | Addressed | requirement.md:L288-L288 -> [Component: Interaction Summary Store and SDK Adapter](#component-interaction-summary-store-and-sdk-adapter) and [Component: Shared Summary Presentation](#component-shared-summary-presentation); D2 names role-compatible versus role-incompatible successful-envelope tests for initiator, receiver, and post-call, D3 maps only normalized `unsupported` with no retained accepted content to `The summary is not available`, while D2b/D6 preserve an existing edited post-call draft until a current reason-generation ready response is accepted; older-generation responses are stale before timestamp ordering. D4-D6b integrate it, and D8/D9 exercise and gate the behavior. | +| AC-14 | Addressed | requirement.md:L289-L289 -> Cross-Cutting Concerns and [Change: Host Status Callback and Existing Web Component](#change-host-status-callback-and-existing-web-component); enforced across D2b-D7 and verified by `D9-release-validation`. | +| AC-15 | Addressed | requirement.md:L290-L290 -> [Component: UX Evidence Admission](#component-ux-evidence-admission) and [Change: Deterministic UX and Release Validation](#change-deterministic-ux-and-release-validation); `D0a-ux-evidence-admission` seals all ten target-worktree source triples before D3, `D8-browser-ux-validation` materializes each admissible screenshot comparison as an immutable per-run `target.png` baseline, retains and rehashes S10 as the named requirement-availability structural override, records exact comparison or structural-only evidence for every state, and `D9-release-validation` re-verifies complete coverage without first establishing or updating a baseline. `verifyUXEvidence` also proves the acquisition boundary from the flow-authenticated changed/evidence manifest: every UX input is a sealed local path with the recorded hash, and no executable changed source or structured evidence record contains remote Figma MCP endpoint/tool/node-fetch provenance. | +| AC-16 | Addressed | requirement.md:L291-L291 -> Test Strategy and [Change: Deterministic UX and Release Validation](#change-deterministic-ux-and-release-validation); traced to every implementation/validation task from D1 through D9: `D1-summary-contract-fixtures`, `D2-interaction-summary-store`, `D2b-interaction-summary-sdk-binding`, `D3-shared-summary-presentation`, `D4-receiving-ai-assistant`, `D5-mid-call-call-control`, `D6-post-call-wrap-up`, `D6b-call-control-summary-integration`, `D7-host-status-callback`, `D8-browser-ux-validation`, `D8b-restore-publishable-sdk-dependency`, and `D9-release-validation`. Named failing cases cover blocked/non-local or missing fonts, unsettled animation/device-scale drift, fifth-attempt exhaustion with no success receipt, remote Figma MCP provenance despite valid local hashes, and a wrong promotion target. | +| EXT-SDK-CC-SUMMARIES | Deferred | External source candidate owned by the Webex JS SDK Contact Center maintainers. The current clean `cc-summaries` commit has the intended task-owned event and unified public API; D0 must still stage a deterministic version and independently record build, package, declaration, and runtime evidence before SDK-bound work. | + +The assigned review packet's `REQ-013` label refers to the branch clause at `requirement.md:L3-L5`, whose canonical matrix ID here is PRE-001. Its `REQ-015` and `REQ-016` labels refer to the preamble clauses at `requirement.md:L11` and `requirement.md:L13`; this matrix's canonical IDs for those exact source rows are PRE-003 and PRE-004. `REQ-019` through `REQ-027` are the synthesized matrix identifiers for the nine bullets under `requirement.md`'s `## Out of Scope` heading. They intentionally have no implementation DAG owner; each row above records the concrete exclusion boundary. `AC-01` through `AC-16` each name both a design owner and one or more implementing/verifying DAG tasks. For UX-002 through UX-010 specifically, D3 implements the shared generating, editing, feedback, copy, and error presentation and D6 implements the post-call composition and state transitions; D8 implements only the validation harness and evidence for those states, while D9 adds the read-only verifier, and neither task authors or repairs the product states. + +**Blocking dependency record.** PRE-003, PRE-004, REQ-001, AC-01, and `EXT-SDK-CC-SUMMARIES` remain Deferred only until D0 converts the current clean source candidate into an admitted immutable artifact. Source `package.json` version absence is expected and is not itself a blocker: D0 copies the recorded commit into an isolated stage, assigns the deterministic development version there, and leaves the SDK checkout clean. D0 then proves the task-owned feature event, unified public declarations, packed files, optional inbound summary timestamp, action correlation, timeout cleanup, HTTP response behavior, and same-Task post-wrap-up send callability against the same bytes. Missing or failed evidence keeps SDK-bound work closed; widget casts, source edits, registry fallback, and weakening probes are not recovery paths. D8b validates that the private development resolution cannot leak into actual packed widget manifests; it does not require the staged development SDK version to exist in a registry. + +## Current State and Reuse Analysis + +Live code already supplies the package direction and insertion points; it does not yet contain summary state or views. + +| Behavior or surface | Classification | Live evidence and decision | +| --- | --- | --- | +| SDK ownership | preserve | `packages/contact-center/store/src/store.ts`, `storeEventsWrapper.ts`, and `store.types.ts` own `cc`, Task listeners, MobX state, task replacement, sign-out cleanup, and SDK calls. New summary API access stays here. | +| Contact Center package direction | preserve | `cc-widgets -> task/ai-assistant -> cc-components -> store -> SDK`; presentation must not import `@webex/contact-center` or a container. | +| Task feature-enablement lifecycle | extend | Live `storeEventsWrapper.ts#registerTaskEventListeners` already owns exact Task callback registration and teardown. D2b binds `TASK_EVENTS.TASK_FEATURE_ENABLEMENT` on that same Task seam, retains the exact Task/callback pair, and removes it on Task replacement/removal and `cleanUpStore`. Before relying on future events, registration seeds the same fail-closed normalized state from that exact Task's public `aiSummaryCapabilities` snapshot. This covers capability delivered before widget listener attachment without inventing a client-level event. | +| Store Task and `AgentWrappedUp` lifecycle | extend | Live `storeEventsWrapper.ts#registerTaskEventListeners` binds `TASK_EVENTS.TASK_WRAPPEDUP` directly to `refreshTaskList`, while `handleTaskRemove` detaches that callback and `cleanUpStore` is the global sign-out reset. D2b replaces only that direct binding with a stable owner-bound callback registered in a Store/session-owned `aiSummaryWrappedUpSubscriptions` registry keyed by `AISummaryOwnerKey`, separate from ordinary `aiSummaryTaskListeners`. The registry retains exact Task/callback bindings independently of `taskList`; a Task replacement attaches the same idempotent owner callback to the successor before ordinary listener teardown, and `handleTaskRemove` never removes the logical subscription or its pending/frozen failure. The first matching `AgentWrappedUp` records the backend boundary and attempts owner-only cleanup inside an error-isolation boundary, then invokes the legacy `refreshTaskList` effect from `finally`; an outer `finally` detaches every retained binding for that owner. An interaction change or `cleanUpStore` is the bounded privacy exit when the backend event has not arrived and likewise detaches all retained bindings while clearing that interaction's summary memory. | +| Voice-media predicate | extract and reuse | Live `storeEventsWrapper.ts` has a private `isTelephonyTask` backed by `store.types.ts#MEDIA_TYPE_TELEPHONY_LOWER`, so it cannot be shared with summary eligibility. D2b moves that exact predicate to an exported pure `task-utils.ts#isTelephonyTask`; the existing `store/src/index.ts` wildcard makes this an explicit additive `@webex/cc-store` export. The predicate keeps the existing constant as its one policy value and is imported by `storeEventsWrapper.ts` and every summary eligibility caller; no second voice check or feature-local media constant is permitted. | +| Call-control orchestration | extend | `packages/contact-center/task/src/helper.ts#useCallControl` owns `consultCall`, `transferCall`, `consultTransfer`, `consultConference`, and `wrapupCall`; `CallControl/index.tsx` is already `observer` plus `ErrorBoundary`. The pre-action `sendMidCallSummaryResponse` method is invoked first and its Promise is immediately normalized/observed as an always-fulfilling `AISummaryPreActionSendResult` (`sent`, `failed`, `blocked`, or `stale`); destination confirmation then invokes the captured existing consult/transfer action, while the existing-party `Transfer`/`Transfer Conference` and `Merge` confirmation controls invoke `consultTransfer` and `consultConference`, respectively, exactly once only after the pre-action result settles. Every D5/D6b internal call site awaits the always-fulfilled result. A synchronous throw, rejection including timeout, indeterminate correlation, or ownership-generation invalidation is normalized before telephony begins and cannot abort the later telephony call; non-sent settlements mutate no response-derived state. The separately observed fulfillment of a transfer/handoff or conference-establishment action reports the captured owner key plus its boundary to the store's idempotent generation compare-and-swap; a rejected action reports no boundary. The response Promise is awaited before the telephony Promise is created, and both settlements are observed, so neither can float or become an unhandled rejection and all existing SDK signatures stay unchanged. | +| CallControl and AI Assistant styling/theme | preserve and extend | `packages/contact-center/cc-components/src/components/task/CallControl/call-control.styles.scss` supplies the existing flex/container sizing, bounded `.agent-popover-content` with vertical overflow, and semantic `--mds-color-theme-*` tokens; `packages/contact-center/cc-components/src/components/AIAssistant/ai-assistant.styles.scss` supplies panel/body sizing, wrapped text, vertical scrolling, semantic colors, and token-based `:focus-visible` outlines. `AISummary/*` and its CallControl/AI Assistant integration styles remain container-scoped extensions of those modules: inherit the host `cc-calling-widget` theme and forced-colors/focus behavior, wrap and break dynamic text, use vertical-only bounded overflow, and reuse existing semantic tokens. They add no feature-only default/dark/customer theme map and no screenshot-sized outer container. | +| Consult/transfer UI | replace once for all voice preparations; append summary conditionally | `cc-components/.../CallControl/call-control.tsx` opens the popovers and `CallControlCustom/consult-transfer-popover.tsx` owns two closed variants of the same shell. The destination variant is the only voice destination renderer: the named UX-001 evidence `.ccwidgets/midcall/figma_target_8061_880455_compact.json`, `.ccwidgets/midcall/figma_target_8061_880455_text.json`, and `.ccwidgets/midcall/midcall.png` places radio categories before the resolved search field and results before the optional divider/summary. The existing-party variant contains no destination list and is opened from the already-consulted party's `Transfer`/`Transfer Conference` or `Merge` control so its summary can be reviewed before confirmation. Non-voice behavior retains the legacy pill-category branch; both category affordances consume the same `availableCategories` array and call the same `handleCategoryChange`, so they produce identical filtering while summary capability never selects between them. On each popover open, the component captures a closed `destinationLayout` discriminator from the non-summary interaction context (`voice-radio`, `non-voice-pill`, or `existing-party-action`) and keeps it latched until that popover instance closes; neither later `task:featureEnablement` delivery nor an ownership-generation change can reselect it. Capability controls only whether summary UI is appended. Arrival/revocation leaves the mounted destination nodes, query, selected category/destination, and connected focus intact unless focus was inside a removed summary control, in which case the REQ-009 successor/fallback rule applies without an announcement. Closing discards the latch; a later open derives a fresh value, while a canonical interaction switch resets the scoped controls and request/reveal bookkeeping. While a popover is open, its programmatically focusable `.agent-popover-content` root is the named containing-panel fallback; closing the whole surface returns focus to the exact trigger that opened it. Summary-visible and summary-omitted states plus both mount/remove directions are required component/browser cases, including capability arrival after query entry/category selection with identical destination node identity, filtering, selection, and focus. | +| Wrap-up UI | replace only in the eligible branch | `call-control.tsx` currently renders a Momentum `Select` immediately before `SUBMIT_WRAP_UP`. The eligible searchable-radio replacement is required by the named UX-002/UX-003 evidence `.ccwidgets/postcall_generating/figma_target_5669_263180_compact.json`, `.ccwidgets/postcall_generating/figma_target_5669_263180_text.json`, `.ccwidgets/postcall_summary_edit/figma_target_5669_263754_compact.json`, and `.ccwidgets/postcall_summary_edit/figma_target_5669_263754_text.json`; UX-004 through UX-010 retain that hierarchy while specifying its collapsed/copy/error states. It reuses `wrapupCodes` and selected reason/id; absent/false eligibility preserves the existing `Select` and `SUBMIT_WRAP_UP` path. The search has a persistent accessible name, the `RadioGroup` is labelled by the visible reason heading, and a zero-match filter renders static non-live copy instead of an empty region. Momentum arrow-key selection updates the reason without issuing transport; pointer activation, Enter/Space, or group-focus exit commits the final selected reason through one owner-and-selection-revision-deduplicated request. A reason change or popover close never clears an accepted/edited store draft; that draft is replaced only when a newer committed-reason response is accepted. Late enablement or revocation maps the selected reason by id and focus between the legacy Select/submit controls and the eligible radio/search/Complete Wrap-Up controls, falling forward or to the established panel target only when no semantic counterpart remains; interaction switch resets instead of carrying state. `call-control.utils.ts#handleWrapupCall` awaits an always-resolving `WrapupCompletionResult`, keeps the selected reason only for `{wrapup: 'failed'}`, and clears it for `{wrapup: 'succeeded'}` whether the response is submitted or `response-failed`. Both layouts and both live transition directions retain dedicated tests. | +| AI Assistant panel | extend | `ai-assistant/src/ai-assistant/index.tsx` observes the singleton store and `cc-components/.../AIAssistant/ai-assistant.tsx` owns the established dialog/panel. Receiver content gets a distinct summary branch so real-time-assist chat entries remain isolated. | +| Receiver `View summary` entry point | extend existing AI Assistant trigger | `cc-components/.../AIAssistant/ai-assistant.tsx` already owns the closed launcher, minimized restore control, open panel header, and body switch. D4 reuses that chrome trigger slot for a content-free receiver notification action; the observer passes one optional, discriminated `receiverSummary` object only for a current `content`, `unavailable`, or `generic-error` receiver projection. Absence is the hidden state and renders neither the action nor a receiver panel branch; `unavailable` opens the exact `The summary is not available` fallback rather than a blank panel. The native button accessible name and tooltip are both exactly `View summary`; activation calls `openReceiverSummary`, opens or restores the existing panel, selects the receiver branch, and recovers focus to the first connected focusable control in the new panel DOM when the notification is removed. The existing `[data-testid="ai-assistant:panel"]` `role="dialog"` root receives a ref and `tabIndex={-1}` as the named containing-panel fallback. No IncomingTask dependency, task-to-AI-Assistant import, new custom element, or host prop/event is introduced. | +| Adaptive Card security boundary | preserve and harden | `AdaptiveCardRenderer`, `prepareCardForRender`, `extractCardText`, local image fallbacks, and `ErrorBoundary` remain the restricted rendering boundary. D4 keeps the renderer's existing prop/action contract unchanged, hardens only its pre-parse preparation with the closed bundled-image allowlist and sanitized-empty fallback, and makes `AIAssistantComponent` the composition owner: it renders the display-only receiver card and D3's shared `AISummary` action surface as siblings, with the controls outside and never descended from the renderer DOM. The synchronous receiver copy getter uses the existing `extractCardText` utility against that sibling branch; no summary labels, feedback state, Clipboard Promise, or summary action branch enters `AdaptiveCardRenderer`, and the live real-time-assist caller retains its public behavior and bundled icons under the shared no-remote-resource policy. | +| Existing card feedback/copy semantics | bypass for receiver summary controls | Generic real-time-assist cards retain their existing “Like/Dislike/Copy suggestion” behavior. Receiver summaries do not fork or parameterize that logic: the sibling `AISummary` action surface renders the exact shared labels, sticky mutually exclusive controlled feedback, re-send on re-selection, and the direct-gesture `navigator.clipboard.writeText` flow using text returned by D4's `extractCardText`-backed getter. Unknown embedded card actions stay inert, and the renderer gains no summary-specific control state. | +| Web component | extend/preserve | `packages/contact-center/cc-widgets/src/wc.ts` keeps the existing r2wc mapping for every legacy prop but deliberately does not register `onAISummaryStatusChange` as an r2wc `function` prop. A typed property-only adapter/subclass exposes that optional JavaScript property on the existing `widget-cc-call-control` tag, updates the React adapter through an internal WeakMap-backed subscription, and neither observes nor reflects an HTML attribute. The callback source is not the rendering widget: the store records one content-free monotonic transition for initiating, receiving, and post-call settlements, and a mounted `task/src/CallControl/index.tsx` observer reads and forwards it regardless of whether CallControl or AI Assistant renders the summary. A host that mounts only `widget-cc-ai-assistant` still gets its receiver `View summary` UI but intentionally receives no host status callback; it must also mount `widget-cc-call-control` and assign the optional property to observe statuses. `widget-cc-ai-assistant` gains no property, callback, event, attribute, tag change, or import from task. | +| Installed SDK dependency | seal exact development bytes without polluting publishable manifests | `@webex/cc-store` retains its existing plain registry-semver declaration. D0 assigns a deterministic development version only in an isolated SDK stage, commits `vendor/contact-center-cc-summaries.tgz`, and selects it only through the private root `package.json#resolutions` map. All SDK descriptors must resolve to one checksum-matching local lock record during development. D8b packs the six affected publishable workspaces and proves their actual packed manifests contain only registry-resolvable SDK dependencies and contain neither the private override nor vendored tarball. Registry publication of the development-only staged version is outside this run. | +| Tests | extend with named isolated a11y/visual owners | Live `package.json` and `playwright.config.ts` provide `@playwright/test`, Chromium projects, and one top-level local sample-app `webServer`, but there is no current direct axe integration or screenshot-baseline project to reuse. D8 conditionally appends the project named exactly `AI Summary Deterministic` only when `AI_SUMMARY_E2E=1`; that project has exact `testDir: './playwright'` and `testMatch: ['**/suites/ai-summary-tests.spec.ts', '**/tests/ai-summary-test.spec.ts']`, so it collects both directories with no OAuth/global-setup dependency. `playwright/suites/ai-summary-tests.spec.ts` is the independently collected visual/evidence manager and `playwright/tests/ai-summary-test.spec.ts` is the independently collected browser-journey/accessibility suite; neither imports or registers the other's scenarios, so each mandatory test is collected exactly once. Existing projects retain their own `**/suites/${TEST_SUITE}` matches and add exact ignores for both AI Summary paths; unflagged default, CI, per-project, and shard invocations do not append the AI Summary project, so neither AI Summary file is enumerated there. The flagged run reuses the same config-level sample-app server and never starts a second server. Its init script installs the typed version-1 `window.__WEBEX_CC_AI_SUMMARY_E2E__` bridge before navigating with `ai-summary-e2e=1`; the sample host fails at boot if that bridge is absent and injects its Webex-shaped member only through existing `store.init({webex})`. D8 declares the exact root dev dependency by running `yarn add -D -E axe-core@4.10.2`; `aiSummaryUtils` resolves `axe-core/axe.min.js` through the active Yarn resolver with `require.resolve`, reads the package-owned bytes through `node:fs`, injects them with `page.addScriptTag({content: axeSource})`, and runs `window.axe.run` against the summary/panel root with `runOnly` tags `wcag2a`, `wcag2aa`, `wcag21a`, `wcag21aa`, `wcag22a`, and `wcag22aa`. Any nonempty `violations` array fails, regardless of impact. Promoting the transitive package to a direct root dependency changes `package.json`, the root workspace descriptor in `yarn.lock`, and the generated linker install map even though the resolved archive remains 4.10.2; this is intentional dependency churn, not a zero-churn reuse claim. `playwright/visual/ai-summary-visual-cases.ts` is the typed, version-controlled registry that places every implemented state in exactly one screenshot-backed or structural-only class and carries the required UX-002-through-UX-010 binding for each of the nine mid-call extrapolations. `MB_FLOW_BIN` is the sole executable source; the helper invokes it without a shell, derives the common-checkout render root through `git rev-parse --git-common-dir`, and treats spawn/timeout/non-zero/stderr-without-output failures as test failures. For screenshot-backed cases, `recordAISummaryVisual` reads the D0a receipt and the sealed target-worktree scene/text JSON plus PNG, then writes the unchanged target bytes beneath the common checkout's `.matrix/results/prog-mini-js/ux-visual/implementation///target.png` beside `rendered.png`, `diff.png`, `comparison.json`, `dom.json`, and `accessibility.json`; `target.png` is the per-run baseline artifact, not a writable `.ccwidgets/**` source. Baselines may change only through a reviewed requirement/UX-source change followed by updated expected hashes and a fresh D0a seal, never by accepting rendered output or updating D8 in place. Structural-only cases write `.matrix/results/prog-mini-js/ux-visual/implementation/structural//classification.json` plus DOM/accessibility evidence, with the registry's reason, scenario ID, and any required extrapolation binding. The suite never invokes the final validator; the surrounding acceptance command invokes `validate-ux-visual-comparison` once after Playwright and owns that attempt's authoritative receipt. Tests are behavioral; snapshots alone are not acceptance. | +| Existing source removal | remove none | No existing component, tag, method, callback, or real-time-assist path is obsolete. The real-time-assist renderer contract remains unchanged. `AIAssistantComponent` composes the shared `AISummary` receiver action surface as a sibling of `AdaptiveCardRenderer` rather than adding a second label/copy/feedback mode inside that restricted renderer. | + +New files are limited to responsibilities with no existing home. `store/src/ai-summary.ts` holds pure validation, normalization, timestamp comparison, owner-successor reducers, and response composition so those rules are not duplicated between `store.ts` and `storeEventsWrapper.ts`; it is not a service/factory layer and owns no mutable singleton state. The existing MobX `Store` in `store/src/store.ts`, through its existing `makeAutoObservable` path, owns the keyed `(interactionId, agentId, ownershipGeneration)` summary records, ownership-generation and request sequences, viewed/edited/copied counters, pending feedback, pending requests, and frozen post-call response. Those observable records survive hold/resume and same-agent component remounts; agent replacement and confirmed handoff/conference ownership boundaries start a new generation, with same-agent counters/content carried to the successor and different-agent counters reset. The declared interaction-terminal, `AgentWrappedUp`, and global session-cleanup boundaries clear them. `cc-components/src/components/AISummary/*` is necessary because typed/plain editing plus the common view/copy/feedback/focus controls are used by the CallControl and AI Assistant containers; the control implementation remains shared, while `AIAssistantComponent` owns only the receiver-specific composition that places that shared action surface beside, never inside, `AdaptiveCardRenderer`. `test-fixtures/src/aiSummaryFixtures.ts` centralizes raw SDK shapes. Two tooling scripts own immutable SDK admission and final provenance, while the existing publisher gains only the local-protocol preflight. No workspace or runtime package is added; D8 adds the one test-only root dev dependency named above. + +## Target Architecture and Package Layout + +```text +conforming @webex/contact-center Task methods + cc/task events + -> @webex/cc-store validation, canonical interaction key, owner generation, reducer + |-> @webex/cc-task initiating/post-call observable view models + | -> SDK-free @webex/cc-components AISummary + | -> existing widget-cc-call-control plus optional content-free callback + |-> @webex/cc-ai-assistant receiving observable view model + | -> @webex/cc-components AIAssistantComponent receiver branch + | |-> restricted AdaptiveCardRenderer for display-only card rendering + | `-> SDK-free AISummary receiver action surface as a DOM sibling + | -> existing widget-cc-ai-assistant with unchanged host contract + `-> ordered content-free receiver/initiator/post-call status-transition backlog + -> @webex/cc-task CallControl observer render + acknowledgement effect + -> the same call-control callback +``` + +The canonical state key is `(canonicalInteractionId, agentId, ownershipGeneration)`. `canonicalInteractionId` is a stable main-call identity resolved only from the non-empty `task.data.interaction.mainInteractionId` and existing `findMediaResourceId(task, 'mainCall')` candidates: one candidate is accepted, equal candidates collapse to one value, and conflicting or wholly absent candidates return `undefined`. The leg-scoped `task.data.interaction.interactionId` and `task.data.interactionId` values are never fallbacks. The runtime field mapping is explicit: the requirement's `interactionId` means this canonical main-call key; `task:featureEnablement.interactionId` and every summary payload's `conversationId` must equal it. A valid capability for another ID is unavailable to the current Task and is never re-keyed, while a summary payload with a missing or different `conversationId` is discarded before it can change content, counters, feedback, owner state, or status; a request settlement may clear only its exact captured pending-sequence entry. D0's transfer/conference equality fixtures remain package-admission evidence, not a substitute for these runtime checks. `agentId` is the non-empty authenticated `store.agentId`, not a participant name. + +`ownershipGeneration` is a store-issued monotonic ownership-phase epoch, not `task.data.interaction.owner`: the latter names the primary call owner and cannot identify the receiving agent during consult or every agent after conference establishment. Every async request, response send, and content event captures the full key. The store advances the epoch, even when the canonical ID and authenticated agent are unchanged, after a successful transfer/handoff (including consult-to-transfer promotion), on conference establishment, and when a later transfer/conference topology begins a new per-agent full-history ownership cycle; a different local agent or re-entry after a terminal clear also advances it. Hold/resume, same-agent remount, and a pure Task object rehydration/clone do not. Task end alone retains state as REQ-002 requires; only independently confirmed handoff completion advances the epoch. A same-agent advance creates the successor record atomically with the current visible content, counters, and feedback carried forward so the old summary remains visible until valid replacement content arrives and REQ-008 counts do not reset. An agent change creates an empty successor with zero counters. The former key becomes non-current before any late settlement can commit. Ordinary repeated requests inside one ownership phase do not advance the epoch: request sequence suppresses obsolete timestamp-less failures, the exact optional response `timestamp` orders comparable successes, and owner-role arrival order deterministically handles equal or absent response timestamps. Feature enablement's `actionTimestamp` is never reused as a summary-response timestamp. `nextAISummaryContentRevision` is a Store-lifetime monotonic counter: every accepted content replacement and successful edit receives the next positive value, same-agent successor carry-forward retains that value until replacement, and an empty successor uses `0` only until content is accepted. It is never reset or reused by owner, keyed, sign-out, or session cleanup, so an old rendered `expectedRevision` cannot collide with a later owner; the revision remains separate from and never substitutes for the ownership boundary itself. + +Planned file actions are deliberately package-local: + +| Area | Retained/modified/added files and responsibility | +| --- | --- | +| UX admission | D0a adds the UX-only `seal-ux`/`verify-ux` seams in `tooling/src/ai-summary-sdk-lock.js`, their fail-closed cases in `tooling/tests/ai-summary-sdk-lock.test.js`, and generated `design/default/sdk_package_lock.json`. Its single `.gitignore` edit appends one final reviewed-artifact block after every existing ignore rule (therefore after `*.json`) with exactly `!design/default/sdk_package_lock.json` and `!vendor/contact-center-cc-summaries.tgz`, removing no pattern. The live rules exclude neither `design/`, `design/default/`, nor `vendor/`, so no ignored parent prevents either exact path from being tracked. The receipt exception counters `*.json`; the tarball rule records the deliberate binary-artifact commit decision before D0 produces it. D0a seals all thirty target-worktree inputs before D3 and initializes only `sdk.status: "pending"`. | +| SDK admission | D0 extends the lock tool/tests with isolated staging, deterministic version assignment, SDK build/pack, portable verification, and publishable-boundary verification. It generates and commits the reviewed tarball, preserves the store manifest byte-for-byte, and uses only the private root resolution for integrated testing. The combined receipt records the exact source commit, stage version, packed files, declarations, toolchain, and probes. | +| Fixtures | Add SDK-independent raw `packages/contact-center/test-fixtures/src/aiSummaryFixtures.ts`; modify its `src/index.ts` and `ai-docs/test-fixtures-spec.md`. D1 uses the sealed `ai-summary.md` shapes without importing an unadmitted SDK; D0 later compiles conforming members against the packed declarations. | +| Store | D2 adds the `unknown`-input adapter/normalized model in `packages/contact-center/store/src/ai-summary.ts`, `src/store.types.ts`, and `tests/ai-summary.ts` without importing the SDK. After D0, D2b completes SDK response composition/binding and modifies `src/store.ts`, `src/storeEventsWrapper.ts`, `tests/store.ts`, `tests/storeEventsWrapper.ts`, `tests/setupContactCenterMock.js`, and `ai-docs/store-spec.md`. Existing `src/index.ts` already re-exports `store.types.ts`; it is unchanged. | +| Shared presentation | Add `cc-components/src/components/AISummary/{ai-summary.tsx,ai-summary.types.ts,ai-summary.constants.ts,ai-summary.styles.scss,index.ts}` and `tests/components/AISummary/ai-summary.tsx`; modify `src/index.ts` and `ai-docs/cc-components-spec.md`. D3 exposes the receiver action surface with a synchronous visible-text getter but does not import or render `AdaptiveCardRenderer` and does not modify its implementation, props, utilities, RealTimeAssist, or AI Assistant container files. | +| Task integration | Add disjoint `task/src/{ai-summary-mid-call.ts,ai-summary-post-call.ts}` orchestration modules and focused tests; modify `task/src/{helper.ts,task.types.ts,CallControl/index.tsx}` plus its shared tests/spec only in the D6b integration join. In cc-components, keep the mid-call work in `CallControlCustom/{consult-transfer-popover.tsx,consult-transfer-summary.types.ts,consult-transfer-summary.styles.scss}` and the post-call work in new `CallControlCustom/{wrap-up-summary.tsx,wrap-up-summary.types.ts,wrap-up-summary.styles.scss}` with disjoint tests; D6b alone modifies shared `task.types.ts`, `CallControl/{call-control.tsx,call-control.utils.ts,call-control.styles.scss}`, shared tests, and the module spec. | +| Receiver integration | Modify `ai-assistant/src/{ai-assistant.types.ts,ai-assistant/index.tsx,helper.ts}` and its tests/spec; modify cc-components `AIAssistant/{ai-assistant.tsx,ai-assistant.types.ts,ai-assistant.styles.scss}`, `AdaptiveCardRenderer/{adaptive-card-renderer.tsx,adaptive-card-renderer.utils.ts}`, their focused component/utility tests, and the cc-components module spec. D4 reuses the existing `extractCardText` implementation, leaves `AdaptiveCardRendererProps` unchanged, and confines the closed image-resource policy plus sanitized-empty fallback to the existing renderer boundary. | +| Host compatibility | Modify `task/src/{task.types.ts,index.ts,CallControl/index.tsx}`, their tests/spec, `cc-widgets/src/{index.ts,wc.ts}`, `cc-widgets/tests/wc.ai-summary.ts`, `cc-widgets/{package.json,tsconfig.test.json}`, and `cc-widgets/ai-docs/cc-widgets-spec.md`. The existing package-level Jest configuration discovers the new runtime test; the added test-only TypeScript project and the workspace's updated `test:unit` script make its public-entry type import an enforced compile-time assertion without changing the production TypeScript target/module settings. No `ai-assistant` public type or `WebAIAssistant` registration changes. | +| Sequenced shared-file handoffs | D3 owns the `AISummary` API and focused component test; D4 depends on it and is the sole feature writer of `packages/contact-center/cc-components/src/components/AIAssistant/ai-assistant.types.ts`, the AI Assistant component test, and the focused card-text utility test that proves the sibling integration's extraction order. Their only shared cc-components file is the module specification, which D4 updates after D3. `packages/contact-center/task/src/task.types.ts` and `packages/contact-center/task/src/CallControl/index.tsx` are another serial handoff: D6b first joins the summary flows, then D7, which depends on D6b, adds the host callback surface. Their matching shared tests and module specs follow the same order. These pairs are intentionally dependency-ordered, never parallel task ownership; D4 cannot schedule with D3 and D7 cannot schedule with D6b. | +| UX/release | Add `playwright/Utils/aiSummaryUtils.ts`, the independently collected visual/evidence manager `playwright/suites/ai-summary-tests.spec.ts`, the independently collected browser-journey/accessibility suite `playwright/tests/ai-summary-test.spec.ts`, typed case/classification registry `playwright/visual/ai-summary-visual-cases.ts`, `tooling/src/verify-ai-summary-release.js`, and `tooling/tests/verify-ai-summary-release.test.js`. Modify `playwright.config.ts` so the exact `AI Summary Deterministic` project is appended only under `AI_SUMMARY_E2E=1`, has exact `testDir: './playwright'` plus `testMatch: ['**/suites/ai-summary-tests.spec.ts', '**/tests/ai-summary-test.spec.ts']` and no OAuth/global-setup dependency, reuses the existing top-level sample-app `webServer`, and is absent from existing unflagged CI/default/sharded enumeration; the two specs have complementary top-level registrations and never import one another, so neither can duplicate collection. From the repository root run `yarn add -D -E axe-core@4.10.2` so Yarn writes the exact `package.json` declaration and regenerates `yarn.lock`; resolve `axe-core/axe.min.js` with `require.resolve` through the active Yarn resolver, read the package-owned bytes with `node:fs`, inject them with `page.addScriptTag({content: axeSource})`, run the six WCAG 2.x A/AA tags against the summary/panel root, and fail on any violation. The direct declaration intentionally rewrites the root lock descriptor and linker resolution map despite retaining the transitive 4.10.2 archive. D8 re-verifies the D0a seal before visual work; `recordAISummaryVisual` reads each receipt-listed scene/text JSON and PNG, copies the verified PNG bytes only to the generated per-run `.../implementation///target.png` baseline, and writes render/diff/comparison/DOM/accessibility artifacts beside it. The version-controlled registry records every screenshot-backed case and every implemented no-screenshot state; the latter produces `.../implementation/structural//classification.json` plus DOM/accessibility evidence. Baselines are updated only by an approved requirement/source change, new expected hashes, and a fresh D0a seal—never from implementation output—and declared `.ccwidgets/**` files remain immutable inputs. D8b removes the root local resolution, sets `packages/contact-center/store/package.json` and `yarn.lock` to the exact published SDK registry version, and adds the fail-before-write local-protocol preflight to `tooling/src/publish.js` with `tooling/tests/publish.js` coverage before D9. | + +No TypeScript target, module format, webpack/Babel configuration, package boundary, server/client boundary, or persistence schema changes. The only new direct third-party declaration is the test-only `axe-core@4.10.2`; the archive is already transitively resolved at that version, but adding the root edge still changes `package.json`, the root workspace entry in `yarn.lock`, and the generated Yarn linker install map. The checked-in `.yarnrc.yml#nodeLinker` explicitly resolves to `node-modules`, and `corepack yarn config get nodeLinker` must confirm that value before regenerating the node-modules install state. The axe helper and release verifier remain linker-neutral: they resolve `axe-core/axe.min.js` through the active Yarn resolver, never construct a `node_modules` path, and under PnP use the Yarn-patched resolver/filesystem plus the regenerated PnP install state. D9 validates the exact descriptor, parsed lock resolution/checksum, and resolved artifact hash rather than a directory layout. No generated linker file is committed and axe is not bundled into production. The SDK tarball is deliberately committed repository-only admission evidence, obtained by CI from the Git checkout, and is outside every published workspace's `files`; no published manifest refers to it. Replacing those binary bytes requires a new clean-source rebuild, hash/receipt update, and code review. All implementation tasks update the affected module specification in the same change. + +## Component: UX Evidence Admission + +**Coverage and outcome.** This component covers PRE-005, REQ-009, REQ-011, UX-001 through UX-010, and AC-15. DAG task: `D0a-ux-evidence-admission`. It is independent of `EXT-SDK-CC-SUMMARIES` and is a direct dependency of `D3-shared-summary-presentation`, so percent-slot extraction, resolved consult/transfer headings and placeholders, wrap-up hierarchy, the exact Search topic/Complete Wrap-Up strings, and all other screenshot-derived decisions cannot be implemented from parent-only or unsealed evidence. The parent/common checkout is a staging source only; after the byte-for-byte stage completes, the target worktree is the single resolution root for admission, implementation, browser rendering, comparison, and release verification. + +**Admission and failure behavior.** D0a adds the UX portions of `tooling/src/ai-summary-sdk-lock.js` with three distinct phases. `stage-ux --source-root --target-root ` accepts both canonical absolute roots explicitly, requires them to be the Git-linked common checkout and active target worktree, rejects overlap, symlinks, non-regular files, path escape, extra/glob-selected inputs, and source hash mismatch, and copies only the thirty manifest paths through a target-local temporary tree before atomic promotion. It never writes the receipt. `seal-ux --widgets-root ` then resolves every declared scene-graph, text, and PNG path anew from that target root, rejects a missing/non-regular file, symlink, path escape, parent/common-checkout fallback, environment override, per-file hash mismatch, source-count mismatch, or mismatch of UX manifest `58d925e6d12c377589120f39fc170f1d80692d1901522fce0959b70e17f8c1b4`, source manifest `a266410383fc773812cd0a3352313d8db5e3e3828d5e584aaeee1df42709f162`, or source identity `50073d2c12591ac4c682e8599c1ecae9f2797cae9ed85542bfebf068b5ebfecb`. It hashes all thirty reopened target files in memory before one atomic receipt replacement; failure leaves the prior receipt unchanged and creates no render/evidence output. Success writes `ux.resolutionRoot: "."`, the sealed relative-path/hash member, and `sdk: {status: "pending"}`. `verify-ux --widgets-root ` repeats that target-root-only resolution. D8 runs it after staging/sealing and immediately before any plan or render; drift fails rather than resealing. + +**Files and acceptance.** D0a owns the first edit of `.gitignore`, `tooling/src/ai-summary-sdk-lock.js`, `tooling/tests/ai-summary-sdk-lock.test.js`, and `design/default/sdk_package_lock.json`; D0 depends on D0a before extending those shared tooling/receipt files. The ignored `.ccwidgets/**` replicas are staged inputs, not implementation-source entries in the DAG. The `.gitignore` test requires the two-rule reviewed-artifact block to be last, requires the receipt rule to follow `*.json`, confirms that no `design/`, `design/default/`, or `vendor/` parent rule excludes the targets, and proves both exact paths are visible to Git. Acceptance derives `TARGET_WORKTREE_ROOT`, `COMMON_GIT_DIR`, and `COMMON_CHECKOUT_ROOT`, runs `stage-ux` with both explicit roots, runs `seal-ux --widgets-root "$TARGET_WORKTREE_ROOT"`, performs the trackability checks and focused fail-closed tooling suite, and finishes with `verify-ux --widgets-root "$TARGET_WORKTREE_ROOT"`. The tests mutate one staged byte between copy and seal to prove the post-move rehash catches it, reject a symlink/truncated copy/root mismatch, and prove no admission or verification path reads the parent after staging. Changing expected hashes, symlinking the parent tree, or proceeding with D3 after a failed stage/seal is prohibited. + +## Component: SDK Supply Lock and Contract Admission + +**Coverage and outcome.** This component covers PRE-003, PRE-004, REQ-001, and AC-01. DAG task: `D0-sdk-package-contract`; external source candidate: `EXT-SDK-CC-SUMMARIES`, owned by the Webex JS SDK Contact Center maintainers. The clean `cc-summaries` commit `15f3da706d84c10bc632abe252cea202a70ae212` exposes the intended task-owned feature event, unified summary methods/types, and 15-second timeout. D0 must not demand a version from the source manifest. It must copy that exact clean commit to an isolated stage, derive and assign `-cc-summaries.<12-character-commit>` there, build and pack the stage, then independently verify package bytes, public declarations, correlation/timeout behavior, HTTP response behavior, and same-Task post-wrap-up send callability. SDK-bound descendants remain closed until that evidence is sealed. + +**Disposition gate evidence.** PRE-003, PRE-004, REQ-001, and AC-01 change from Deferred to Addressed only after D0 seals a verified artifact from the exact clean source candidate. The portable identity is the source commit, deterministic staged/packed version, tarball SHA-256, complete sorted file manifest, and reachable public declaration hashes. Audit provenance additionally records the canonical source location, clean-before/clean-after checks, Node 22.14, the observed `yarn@3.4.1` pin, and exact isolated install/build/pack/probe argv. The source checkout remains byte-for-byte clean; all version mutation occurs in the isolated stage. + +**Files, responsibilities, and dependency direction.** D0 extends `tooling/src/ai-summary-sdk-lock.js` with `build-sdk`, `verify-sdk`, `verify-publishable-sdk-boundary`, and final `verify` subcommands. `buildSdkPackage` alone resolves the source checkout, verifies branch/commit/cleanliness, copies the exact commit into a transaction-private stage, derives the development version from the sole publishable store dependency plus the 12-character commit prefix, writes the version only in that stage, and builds/packs there. The source checkout is rechecked clean and unmoved after packing. The repository-owned tarball and receipt are then installed through the private root resolution while the store manifest remains byte-identical. The store workspace owns the packed declaration/runtime contract probe; root tooling owns orchestration and rollback tests. No SDK source or UX-source file is modified. + +The last sentence distinguishes source transformation from evidence staging: D0a may create only byte-identical ignored `.ccwidgets/**` replicas through the explicit stage transaction above. The stage never edits the parent source or changes a UX byte, and the target replica becomes admissible only after the independent target-root seal. + +**Tool API.** The CommonJS module exports `stageUXSources`, `buildSdkPackage`, `verifySdkPackage`, `verifyPublishableSdkBoundary`, `sealUXSources`, `verifyUXSources`, `verifyAllInputs`, and `runCli`. Only `buildSdkPackage` may read `WEBEX_JS_SDK_DIR`; portable verifiers use only repository-owned receipt/archive/lock/install state. `runCli` accepts `stage-ux`, `build-sdk`, `verify-sdk`, `verify-publishable-sdk-boundary`, `seal-ux`, `verify-ux`, and `verify`. Final `verify` composes development SDK verification, publishable-boundary verification, and UX verification. Every child uses explicit argv/cwd and the documented isolated allowlisted environment. + +**Child-process isolation.** The injected `env` is input to one `createChildEnvironment` seam, never the object passed wholesale to a child. It copies only `PATH`, `SSL_CERT_FILE`, `SSL_CERT_DIR`, `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`, and their lowercase proxy spellings, plus Windows process essentials `SystemRoot`, `ComSpec`, and `PATHEXT` when present. It creates a transaction-private home/config/cache/temp tree and synthesizes `HOME`, `USERPROFILE`, `XDG_CONFIG_HOME`, `XDG_CACHE_HOME`, `TMPDIR`, `TMP`, `TEMP`, `COREPACK_HOME`, `COREPACK_ENABLE_PROJECT_SPEC=1`, `COREPACK_ENABLE_STRICT=1`, and `COREPACK_ENABLE_DOWNLOAD_PROMPT=0`. It never forwards caller values for `NODE_OPTIONS`, any case-insensitive `npm_config_*`, any `YARN_*`, or any `COREPACK_*`; `AI_SUMMARY_SDK_EXTRACT_ROOT` is added only to the contract-probe child after containment validation. Allowlisted environment values are used only for the child lifetime and are never serialized into the receipt or diagnostic output. Git identity/cleanliness commands and all SDK `corepack yarn` version/install/build/pack commands run with `cwd` equal to the canonical SDK root. The widgets `corepack yarn --version`, lock regeneration, immutable install, installed-package inspection, and store-workspace contract probe run with `cwd` equal to the canonical widgets root. Any registry metadata/download child used by restoration also runs from the widgets root with that same widgets environment. Before the SDK install and again immediately before the SDK build, `corepack yarn --version` must equal the candidate's package-manager pin and staged `toolchain.yarn`; before any widget lock/install/probe command, the equivalent check must equal the widgets root's exact `packageManager` pin (`yarn@4.5.1` in this checkout). The isolated home prevents user-level Yarn/npm configuration from reintroducing a registry or version, while the explicit CA/proxy allowlist preserves controlled network routing. Tests assert every argv/cwd/env tuple and fail if an unlisted key or parent `YARN_*`, `npm_config_*`, `COREPACK_*`, or `NODE_OPTIONS` value reaches a child. + +**Receipt model and validation.** `design/default/sdk_package_lock.json` retains the sealed UX member and replaces only the pending SDK member after D0 succeeds. The sealed SDK member records the exact 40-character source commit, audit-only source path/branch/clean checks, Node and Yarn identity, deterministic staged and packed package version, exact command argv, repository tarball path/hash/Yarn checksum, complete sorted packed-file hash manifest, and every reachable public declaration hash. The source manifest may omit version; the staged and packed manifests must contain the deterministic value. Portable verification ignores source location and `WEBEX_JS_SDK_DIR`, verifies archive/declaration/install bytes and the one local development resolution, and fails on drift or fallback. Publishable-boundary verification packs the affected widget workspaces and proves their actual package manifests contain only registry-resolvable SDK descriptors and exclude private-root resolution and vendored bytes. + +For the UX member, the concrete portable root field is `resolutionRoot: "."`; it replaces the provisional `worktreeRoot: string` spelling in the schema sketch above. Literal `.` is resolved only against the canonical target root supplied by `--widgets-root` or `--ux-input-root`, never against process cwd, and no absolute UX root is serialized. A receipt with `worktreeRoot`, an absolute `resolutionRoot`, or a value other than `.` is invalid. + +UX-007 keeps exactly one sealed input record in `ux.sources`, under `sourceId: "UX-007"`, with the declared screenshot path and hash. The `S07-UX` / `desktop-light` -> `S07` / `dl` normalization is applied only when D8 derives the visual comparison/evidence key from that sealed record; it never creates a duplicate source entry or a second hash subject. + +**D0 external blocking dependency, owner, and exit contract.** `EXT-SDK-CC-SUMMARIES` identifies the externally owned source candidate, not a requirement for a pre-versioned or pre-published package. D0 admits only a clean immutable `cc-summaries` commit whose staged packed surface exposes `TASK_EVENTS.TASK_FEATURE_ENABLEMENT` / `task:featureEnablement`, the four Task methods, unified `AISummary` / `AISummaryResponse` / `AISummaryAction` contracts, `AISummary.timestamp?: number`, and receiver action correlation restricted to `CONSULT` or `TRANSFER`. The same captured Task must fulfill `sendPostCallSummaryResponse` after its `wrapup` Promise resolves. Exit probes cover source cleanliness and toolchain pin, deterministic stage version, safe archive and complete file/declaration hashes, present/omitted inbound timestamp behavior, feature/action correlation, 15,000 ms timeout cleanup, successful and unsuccessful sends, and same-Task post-wrap-up response with `wrapUpCode === auxCodeId`. No SDK source edit, cast, registry fallback, or unprobed installed package clears D0. + +The exact pre-D0 executable set is `D0a-ux-evidence-admission`, `D1-summary-contract-fixtures`, `D2-interaction-summary-store`, and, after direct D0a/D2 completion (with D1 carried through D2), `D3-shared-summary-presentation`. D1 is raw synthetic fixture work; D2 is restricted to `unknown`-input normalization, local normalized types, pure ordering/ownership reducers, and SDK-free tests; D3 consumes only those normalized types plus sealed UX evidence. `D2b-interaction-summary-sdk-binding` and D4 through D9 are SDK-bound and remain blocked until every D0 exit probe passes. This split permits fixture-only and presentation work to start without weakening or bypassing the external gate. + +**Concrete contract probes.** Under Node 22.14, `build-sdk` first runs `corepack yarn --version` from the canonical SDK checkout in the isolated SDK environment, requires it to match both the candidate commit's pin and staged `toolchain.yarn`, and only then runs `corepack yarn install --immutable`, exact package build `corepack yarn workspace @webex/contact-center run build:src`, and `corepack yarn workspace @webex/contact-center pack --out ` from that same `cwd`. It never runs `corepack yarn workspace @webex/contact-center run test`, `test:style`, `test:unit`, a browser suite, or an integration suite. The inspected manifest proves why `build:src` is sufficient: it chains its package `build` script, and that script invokes TypeScript with `--declaration true --declarationDir ./dist/types`. D0 nevertheless trusts output rather than that historical script text: after each candidate build it enumerates tar headers and extracted bytes, requires exactly one safe `package/` root, rejects links/duplicate/traversing/special entries, records every regular file in `artifact.files`, and requires a regular non-empty `package/dist/types/index.d.ts` before walking imports. An absent entrypoint fails as `declaration-entrypoint-missing`; an unresolved reachable declaration fails as `declaration-graph-unresolved`; only an existing complete graph with changed bytes can fail as `declaration-hash-mismatch`. It checks source path/branch/commit/cleanliness both before and after build/pack and hashes the packed bytes. In the widgets checkout it first proves `corepack yarn --version` equals the root package-manager pin, installs that same tarball through the preserved store registry descriptor plus the private-root override, regenerates the lock with recorded `widgetLock` argv, runs the recorded immutable `widgetInstall`, and invokes `corepack yarn workspace @webex/cc-store test:unit --runInBand --runTestsByPath tests/ai-summary-contract.ts` with only the canonical temporary extraction root added to the widgets child environment. Those commands all use the widgets root as `cwd`. The store-workspace file performs the TypeScript and runtime probes against the tarball; the root lock tool only orchestrates the command. In addition to the method signatures below, the declaration walk from the public `dist/types/index.d.ts` must reach both `WrapupPayLoad.auxCodeId: string`, documented by the SDK as the auxiliary-code identifier associated with the wrap-up state, and the required `AISummaryResponse.wrapUpCode: string`. D0 binds those two public declarations semantically: its fixture uses deliberately unequal values (`wrapUpReason: 'Customer issue resolved'`, `auxCodeId: 'RESOLVED'`), awaits `task.wrapup`, sends a response with `wrapUpCode: 'RESOLVED'`, and asserts the captured response/envelope code is byte-equal to the committed `auxCodeId` and not to the display reason. The probes require these exact public signatures: + +```typescript +task.requestPostCallSummary(): Promise; +task.sendPostCallSummaryResponse(payload: AISummaryResponse): Promise; +task.requestMidCallSummary(actionType: 'CONSULT' | 'TRANSFER'): Promise; +task.sendMidCallSummaryResponse( + payload: AISummaryResponse, + actionType: 'CONSULT' | 'TRANSFER' +): Promise; +``` + +The exact ordering field on both successful response payloads is `timestamp`, not feature enablement's `actionTimestamp`. The packed declarations must expose `AISummary.timestamp?: number` and `AISummary.timestamp?: number`. The type probe assigns both a finite non-negative integer case and a property-omitted case without a cast; the packed-runtime probe delivers both forms and proves the present value reaches the Task settlement unchanged while omission remains omission. A present non-number, non-finite, negative, or non-integer value is a malformed envelope. Absence is permitted and selects the widget's deterministic owner-role arrival-order fallback; it is not synthesized into a time value. + +**Packed runtime harness.** `build-sdk` creates an `fs.mkdtemp` directory beneath the widgets checkout, extracts the just-produced tarball exactly once to `/package`, rejects archive traversal, links, duplicates, and special entries, and hashes the exact extracted regular-file set against `artifact.files` before passing that canonical package directory to the store-workspace child only as `AI_SUMMARY_SDK_EXTRACT_ROOT`. `ai-summary-contract.ts` rejects a missing, non-canonical, escaping, or non-package root; reads that extracted `package.json`; requires its `name`, `version`, `main`, and `types` to match the sealed candidate; and uses `createRequire(/package.json)` to load the absolute `main` file only after proving that file is a regular descendant of the extraction root. The dynamically loaded value is first `unknown` and a no-`any` runtime guard requires the public `Task` and `ApiAIAssistant` constructors. The contract compiler probe resolves type-only imports directly to the same extracted candidate declarations; an assertion that the loaded `main` path and bytes match the complete extracted file manifest prevents a source-tree or pre-existing registry package from satisfying runtime probes. The extraction directory and isolated child home/config/cache/temp tree are removed in `build-sdk`'s `finally` path after the child exits, and the environment variable is never accepted by portable verification. + +The harness constructs a concrete `ProbeTask` subclass of the packed public `Task`, implementing only abstract `accept`; it exercises the inherited `wrapup` and all four inherited summary methods. Constructor arguments are no-`any` deterministic fakes typed from the packed declarations: a contact service whose `wrapup` resolves a synthetic `TaskResponse`, fixed voice `TaskData` with distinct interaction/main-conversation IDs, fixed UI controls, and synthetic agent ID/name. It constructs the packed public `ApiAIAssistant` with a credentials-free `WebexSDK` stub whose `request` is a Jest mock, `credentials` returns synthetic values, service discovery returns a fixed `.invalid` URL, metrics/logger/`once` methods are no-ops, and no method opens a socket or performs OAuth. The Task receives that real packed service through its public `configureAISummary` seam plus an enabled generated-summary flag accessor, and receives matching feature enablement through its public setter. Request cases resolve only when the real service's `resolveFromRtdEvent` receives the matching synthetic payload. Send-success cases make `webex.request` resolve a complete HTTP 202 response; non-success cases deliberately resolve, rather than reject, the same transport Promise with HTTP 503 so the probe proves the SDK itself rejects a non-202 result. The mock captures the emitted request body and the tests require SDK-populated conversation/interaction identity and mid-call `agentName`; UI fixtures never supply those envelope fields. The post-call ordering case awaits the inherited `task.wrapup` against the contact fake, then calls `sendPostCallSummaryResponse` exactly once on that same object. + +The 15,000 ms mid-call timeout uses modern Jest fake timers with a fixed clock. After flushing only microtasks, the test advances `14,999` ms and proves the Promise is still pending, advances exactly `1` ms with `advanceTimersByTimeAsync`, and requires the declared timeout rejection; it performs no real-time wait. Cleanup requires `jest.getTimerCount() === 0`, the relevant public Task event's `listenerCount` to equal its pre-request baseline, and a late `resolveFromRtdEvent` for the same type/correlation ID to return `not-found`; emitting the late public payload and flushing microtasks must not change the single recorded rejection. `afterEach` clears pending RTD requests, removes probe listeners, verifies zero timers, and restores real timers so the focused Jest process cannot hang or leak state. + +They also require a declared public `task:featureEnablement` event whose payload names `interactionId`, `midCallEnabled`, `postCallEnabled`, and UTC `actionTimestamp`, with boolean/number types when supplied; optionality is tolerated because the widget must fail closed on omitted fields. They require initiating mid-call and post-call `Promise` results and receiving-agent `AISummary` content on `TASK_EVENTS.TASK_MID_CALL_SUMMARY_RECEIVED` / `task:midCallSummaryReceived`, including optional string `resolution`; received-response `summary` unions that accept the declared structured section object or `string` for the requirement's plain-text fallback, with the unavailable branch restricted to `summary: ''` and zero counters; the stable interaction identifier across consult/transfer/conference fixtures; a receiver action correlation that resolves only to `CONSULT` or `TRANSFER`; no pre-content receiver eligibility event; no cancellation API; and 15,000 ms mid-call request rejection with listener cleanup. Runtime probes prove response payloads receive SDK-owned envelope fields (conversation/interaction identity and `agentName`) rather than asking UI code to invent them, and prove each send Promise fulfills after a successful transport result while a non-success result rejects. The AC-08 probe additionally captures one Task, awaits that Task's successful `wrapup`, invokes `sendPostCallSummaryResponse` exactly once on the same Task, and requires fulfillment for the successful transport case; probing the send only before wrap-up is insufficient. If the SDK rejects the call because wrap-up completed or otherwise makes the Task non-callable, D0 rejects the package and leaves D2b and D4-D9 blocked until the external owner supplies a conforming commit. The widget does not reorder the response before wrap-up and does not convert this deterministic contract mismatch into `response-failed`. The adapter accepts only the packed declarations actually probed. + +**Control flow and installation.** SDK build and archive inspection happen in temporary output before the first widgets-repository write. Immediately before that first write, `build-sdk` snapshots the exact bytes, existence, and mode of root `package.json`, `yarn.lock`, `vendor/contact-center-cc-summaries.tgz`, and `design/default/sdk_package_lock.json`, and separately records the exact bytes of `packages/contact-center/store/package.json` as a no-write invariant. The four writable paths are one transaction. The tool copies the candidate archive, writes the one private-root dependency selector, regenerates the lock, performs the immutable widgets install, runs the store-workspace contract probe, verifies the candidate receipt in memory, atomically replaces only the receipt's pending SDK member while preserving D0a's sealed UX member, and finally re-reads and verifies the committed state plus the unchanged store manifest. Any throw, rejected Promise, nonzero child exit, or verification mismatch after the first repository mutation—including lock generation, immutable install, contract probe, receipt replacement, or the post-replacement verification—enters one rollback path. Rollback atomically restores every writable snapshot byte/mode, deletes a transaction path that was absent initially, verifies all four writable paths against the snapshots, and verifies the store manifest is still byte-identical before returning the original failure; a restoration mismatch is additionally reported as `rollback-failed`. It then attempts the isolated root-pinned immutable install against the restored root manifest/lock to repair generated node-modules state; failure of that repair is reported separately but cannot suppress or reverse the tracked-file restoration. Thus a failed lock/install step never leaves a partially admitted tarball, receipt, private-root override, or lockfile and never rewrites the publishable store manifest, and recovery does not require first rebuilding the SDK. Those binary bytes, their complete file manifest, and their hash receipt are reviewed and committed together; replacing any of them requires the clean-source rebuild and a new review. + +Normal clean-checkout CI receives the tarball from Git and therefore needs no SDK source checkout to run immutable install or any verification command; only `build-sdk` regeneration uses the external checkout. `verifySdkPackage` starts from the committed receipt, repository-relative tarball, and installed workspace artifacts, ignores `WEBEX_JS_SDK_DIR` and `source.audit`, and uses `source.commit`, `package.version`, the tarball SHA-256, `artifact.files`, and declaration hashes as its portable identity. The resolved root evidence is the checked-in `.yarnrc.yml#nodeLinker` value `node-modules` (with `yarnPath` selecting Yarn 4.5.1 and `checksumBehavior: update`), and D0 independently requires `corepack yarn config get nodeLinker` to return `node-modules`; the PnP label in higher-level repository documentation is not admission evidence. After the committed root tarball selector and lock are present, the exact root consumption/verification sequence for this linker is `corepack yarn install --immutable && node tooling/src/ai-summary-sdk-lock.js verify-sdk`; the latter verifies the installed node-modules package tree and never updates a checksum. Any resolved linker other than `node-modules` fails before install. In `development` mode the shared verifier resolves every root-workspace manifest (including private/sample workspaces), rejects symlink/path escape, and scans all four dependency maps. The only workspace declaration may be the existing `@webex/cc-store` dependency, it must remain byte-for-byte unchanged as a plain registry-resolvable semver, and no publishable manifest may contain a `file:`, `portal:`, `link:`, path, tag, or `npm:` SDK alias. The private root override must be exact `file:./vendor/contact-center-cc-summaries.tgz`. A parsed Yarn lock—not a substring search—must contain exactly one semantic package record for `@webex/contact-center`; every descriptor key for that name, including transitive descriptors, must be grouped into that record, its resolution must canonicalize to the repository tarball, and its checksum/cache identity must equal `artifact.yarnChecksum`. Any second local or registry locator, ungrouped descriptor, missing checksum, or checksum mismatch fails. + +Installed-byte verification runs only through the Yarn-selected `@webex/cc-store` workspace. It resolves the installed SDK root for each workspace dependency graph that names the package, proves every resolution has the same package identity, and walks the package-owned tree without descending into a package-manager-created nested `node_modules` dependency boundary. The exact relative path set must equal `artifact.files`; each row must be a regular non-symlink and its bytes must match the recorded SHA-256, while any missing, extra, linked, or changed package-owned entry fails. Declaration hashes are checked again as the semantic subset. This implements the installed-package-bytes claim for the complete packed payload rather than silently reducing it to declarations. This D0 contract admits only the resolved `node-modules` mode; a PnP cache/zip is not accepted as equivalent evidence, and the workspace child never imports the SDK from the repository root. + +After D8 finishes against the sealed artifact, D8b runs `verify-publishable-sdk-boundary` and the non-publishing six-workspace pack verifier. It does not remove the development override used by this repository run and does not require the staged SDK version to be published. Instead it proves the root-only override cannot appear in a publishable workspace manifest or packed artifact, every published dependency descriptor remains plain registry-resolvable SemVer, and the vendored tarball is absent from every package packlist. The publisher preflight must reject any local protocol before a version write or publish call. + +**Failure, recovery, and lifecycle.** Any mismatch exits nonzero with a content-free diagnostic naming only a stable failure category; it never includes an SDK path, source text, hash value, or raw error. For a source/contract SDK failure, including an invalid-state or non-callable rejection from the required post-wrap-up send probe, the named external owner must supply a new conforming clean `cc-summaries` commit; the widget task does not patch it. For a lock/install/orchestration failure, the transaction first proves the pre-run tarball, receipt, private root manifest, and lock bytes are restored and the store manifest remains byte-identical, so the workspace is not stranded in a half-applied dependency state. After the operator repairs the environment, a new `build-sdk` attempt deliberately rebuilds and re-hashes from source and never trusts failed staged state. For UX failures, the target branch owner reruns the explicit common-checkout-to-target `stage-ux` transaction and the independent target-root `seal-ux`; any direct parent fallback after staging, symlink handoff, transformed byte, or hash-baseline update is prohibited. A failed stage leaves the prior receipt unchanged, and a failed seal never admits partially staged inputs. For D8b failure, the SDK owner must publish the exact admitted version/tarball; the widget must not publish with the development root resolution or a store descriptor other than the exact admitted plain registry semver, select a different version, or publish first. The D0/D8b DAG tasks declare the npm registry needed by install/restoration; if the registry or an equivalent complete local cache is unavailable, the applicable gate stops rather than being skipped. Temporary build, extraction, isolated-home, registry-download, staging, and transaction-backup directories are removed on success or after rollback. Not applicable - no runtime storage or migration is introduced by these build-time gates. + +**Tests.** D0a retains its sealed UX tests. D0 tooling tests cover missing or malformed source/toolchain identity, source movement or dirtiness, isolated deterministic version staging with an unchanged source checkout, safe archive and complete packed-file/declaration hashes, explicit child cwd/environment, transactional rollback, one checksum-matching local lock record, no installed-package fallback, and clean-checkout portable verification. The store contract test compiles and probes the task-owned `task:featureEnablement` event, unified methods/types, optional inbound `AISummary.timestamp`, 15-second timeout cleanup, HTTP send behavior, strict action correlation, and same-Task post-wrap-up response. D8b tests actual packed workspace manifests, local-protocol rejection before writes/publication, vendored-tarball exclusion, and atomic receipt/tarball promotion; no test requires registry publication of the staged development version. + +## Component: Interaction Summary Store and SDK Adapter + +**Coverage and outcome.** This component covers PRE-002, PRE-003, REQ-001, REQ-002, REQ-003, REQ-004, REQ-005, REQ-006, REQ-007, REQ-008, REQ-010, AC-02, AC-06, AC-07, AC-08, AC-09, AC-10, AC-13, and AC-14. DAG tasks: `D1-summary-contract-fixtures`, `D2-interaction-summary-store`, and `D2b-interaction-summary-sdk-binding`. D1 and D2 are SDK-independent work admitted by the sealed `ai-summary.md` hash: they define raw synthetic inputs, `unknown`-input validators, normalized models, owner/counter reducers, and component-facing types without importing or calling `@webex/contact-center`. D2 owns the requirement-authoritative adapter rules: normalize only fields present in the sealed concrete contract, reject unsupported shapes, retain recognizable interaction attribution on normalized errors, and never invent an SDK method, field, event, or retry. D2b is the explicit D0-blocked seam that compiles those boundary shapes against the admitted declarations and adds actual SDK methods/events to the store, including task-bound receiver-error attribution, exact-key mid-call response composition, the ordered observable status backlog with exact-head acknowledgement, exhaustive per-interaction map eviction, and the Store/session-owned `AgentWrappedUp` cleanup boundary for a frozen failed response while its interaction remains current. The independent interaction/session privacy boundary clears that state and its retained bindings if it wins first. The completed component is the sole owner of raw SDK values, capability gating, state isolation, stale-work rejection, counters, response composition, and retention. Containers receive immutable normalized view models and invoke store actions; components never see an SDK Task method. + +**Requirement-precedence enforcement.** PRE-003 (`REQ-015` in the assigned review packet) is not discharged by D0's package-admission check: D0 can prove only that a concrete SDK contract exists. `D2-interaction-summary-store` is the implementing owner for widget behavior. Its pure adapter accepts raw values as `unknown`, recognizes only fields and event shapes present in the sealed/admitted API contract, then applies requirement-governed role precedence, eligibility, identity, failure, timeout, freshness, and retry semantics to the normalized view/state model. If `requirement.md` conflicts with `ai-summary.md` or the SDK, the adapter follows the explicit requirement behavior while rejecting—not synthesizing—unsupported input; it never aliases or invents a method, field, payload key, event, fallback identity, retry, or cancellation. D2 unit cases include each documented source conflict and prove that no client retry is scheduled. D2b may bind only the D0-admitted names and shapes to that already-defined boundary, and cannot weaken or bypass these rules with casts or runtime fallbacks. + +**Files, responsibilities, collaborators, and direction.** Raw deterministic fixtures live in new `test-fixtures/src/aiSummaryFixtures.ts` and are re-exported by its existing `index.ts`. In D2, new `store/src/ai-summary.ts` exports pure validators/reducers over `unknown` plus local normalized types from `store.types.ts`; neither file imports SDK summary declarations. In D2b, the same module gains final response composition proven structurally assignable by the D0 contract test, while `store.ts` adds observable maps/counters through the existing `makeAutoObservable` path. D2b also extracts the existing private voice check into the exported pure selector `task-utils.ts#isTelephonyTask`, still backed solely by `MEDIA_TYPE_TELEPHONY_LOWER`; `storeEventsWrapper.ts` and summary eligibility import it instead of duplicating media policy. `storeEventsWrapper.ts` owns event binding, SDK Promise calls, `runInAction` commits, current Task capture, ownership-boundary compare-and-swap, and cleanup. It adds `handleAISummaryFeatureEnablement(value: unknown): void`; `registerTaskEventListeners` seeds it from the exact Task's public `aiSummaryCapabilities` snapshot and binds it to `TASK_EVENTS.TASK_FEATURE_ENABLEMENT` on that Task; replacement/removal and `cleanUpStore` detach the exact Task/callback pair. The feature handler preserves any stored valid record when an invalid result has no comparable timestamp; only an ordered valid record may replace it. It extends `registerTaskEventListeners`, `handleTaskRemove`, `setCurrentTask`, and `cleanUpStore`, storing receiving-agent callbacks with ordinary Task listeners and wrapped-up callbacks in the separate Store/session-owned owner registry. A receiving callback carries its full owner key and fixed receiver role so a normalized event error can affect only that current role view, independently of Promise request maps. The stable wrapped-up callback remains bound to its captured/replacement Tasks through an earlier generic Task removal and records `AgentWrappedUp`; its summary transition and race-safe map sweep are caught and sanitized, `refreshTaskList` always runs from `finally`, and every retained binding for that owner is detached from an outer `finally`. Thus summary failure cannot regress the existing task-list refresh; interaction cleanup and `cleanUpStore` detach any remaining owner bindings as the per-interaction and global privacy boundaries. Existing `TASK_PARTICIPANT_JOINED`, `TASK_CONFERENCE_STARTED`, `TASK_PARTICIPANT_LEFT`, and `TASK_CONFERENCE_ENDED` handlers preserve their legacy effects and gain only idempotent ownership-boundary bookkeeping; they neither fan out an SDK request nor cancel pending summary work. No manager/factory/package hop is added. + +The fixture module has one data export, `aiSummaryFixtures`, re-exported from `test-fixtures/src/index.ts`. Its readonly groups are `featureEnablement`, `initiatingMidCall`, `receivingMidCall`, `postCall`, `malformed`, `errors`, `ordering`, `transfers`, `conferences`, `feedback`, `counters`, `wrapUp`, and `postWrapUpSend`; D1 authors them as readonly raw object literals from the sealed `ai-summary.md` contract and deliberately malformed members remain `unknown` values consumed only by validation tests. The `postCall` group contains otherwise-identical `resolutionPresent` and `resolutionAbsent` members: the former supplies a non-empty top-level string, and the latter omits the optional property rather than substituting an empty label/value. Separate malformed/empty members exercise `undefined` and empty-string normalization. The wrap-up fixture uses distinguishable `wrapUpReason` and `auxCodeId` values. `postWrapUpSend` explicitly models the captured-Task sequence `wrapup({wrapUpReason, auxCodeId})` resolved -> `sendPostCallSummaryResponse` invoked once with `wrapUpCode === auxCodeId` and `wrapUpCode !== wrapUpReason`, with a conforming successful settlement and a non-callable-after-wrap-up rejection used only to prove D0 fails admission. D1 does not import or cast to the unadmitted SDK. Once D0 succeeds, the store-workspace `packages/contact-center/store/tests/ai-summary-contract.ts` assigns every conforming member to the packed public payload types, fails compilation on drift, and executes the same sequence against the packed implementation. All identities and bodies are synthetic constants. Tests that need mutation spread/clone the selected member locally; the fixture package adds no mutable singleton, factory, SDK client, or runtime export outside test consumers. + +**Field-level models.** The following is the final D2b-complete exported surface in `store.types.ts`; all string unions are closed, all counters start at `0`, optional presentation is `undefined` rather than `null`, and none is serialized. D2 adds only the SDK-independent keys, normalized content/view/state, counters, and transition members. In particular, `AISummaryActionType` has this one declaring module; the existing `store/src/index.ts` `export * from './store.types'` surface re-exports it as `@webex/cc-store`, and D3, D4, D5, D6, and D6b must import shared summary types from there rather than redeclare literal unions. The `AISummaryResponse`-typed frozen field and any other admitted SDK type position are added only by D2b after D0 compiles the D2 structural boundary against the packed declarations; D3 consumes none of those SDK-facing members. + +```typescript +export type AISummaryKind = 'mid-call' | 'post-call'; +export type AISummaryRole = 'initiator' | 'receiver' | 'post-call'; +export type AISummaryActionType = 'CONSULT' | 'TRANSFER'; +export type AISummaryOwnershipBoundary = + | 'handoff-complete' + | 'conference-established' + | 'conference-content-cycle'; +export type AISummaryFeedback = 'none' | 'thumbs_up' | 'thumbs_down'; +export type AISummaryFeedbackStatus = 'pending' | 'not-confirmed'; +export type AISummaryErrorCategory = + | 'unauthorized' + | 'initialization' + | 'offline' + | 'unavailable' + | 'empty' + | 'disabled' + | 'timeout' + | 'generic'; +export type AISummaryLifecycle = + | 'idle' + | 'requesting' + | 'ready' + | 'generation-error' + | 'unsupported' + | 'frozen' + | 'submitted' + | 'response-failed'; +export type AISummarySurface = + | 'omitted' + | 'generating' + | 'content' + | 'unavailable' + | 'generic-error'; + +export type AISummaryCounters = { + viewed: number; + edited: number; + copied: number; +}; +export type AISummaryPostCallSectionKey = + | 'initialContactReason' + | 'additionalContactReasons' + | 'additionalContext' + | 'keyActionsTaken' + | 'nextSteps'; +export type AISummarySectionKey = + | 'reasonForTransferOrConsult' + | AISummaryPostCallSectionKey; +export type AISummaryEditableField = AISummarySectionKey | 'summaryText'; +export type AISummaryDisplaySectionKey = AISummaryPostCallSectionKey | 'resolution'; +export type AISummarySection = {key: AISummarySectionKey; value: string; editable: true}; +export type AISummaryDisplaySection = + | {key: AISummaryPostCallSectionKey; value: string; editable: true} + | {key: 'resolution'; value: string; editable: false}; +export type AISummaryContent = + | {type: 'sections'; sections: AISummarySection[]; resolution?: string} + | {type: 'text'; text: string} + | {type: 'adaptive-card'; card: Record}; +export type AISummaryCapability = { + interactionId: string; + midCallEnabled: boolean; + postCallEnabled: boolean; + actionTimestamp: number; + arrivalSequence: number; +}; +export type AISummaryOwnerKey = { + interactionId: string; + agentId: string; + ownershipGeneration: number; +}; +export type AISummaryViewKey = AISummaryOwnerKey & { + role: AISummaryRole; +}; +type PostCallDraftSnapshot = Readonly<{ + owner: AISummaryOwnerKey; + contentRevision: number; + content: Extract; + counters: AISummaryCounters; + feedback: AISummaryFeedback; + wrapUpCode: string; +}>; +export type PostCallDraftCaptureToken = symbol; +export type PostCallSubmissionResult = 'submitted' | 'response-failed'; +export type AISummaryStatusTransition = { + sequence: number; + detail: + | {kind: AISummaryKind; state: 'available' | 'unavailable'} + | {kind: 'post-call'; state: PostCallSubmissionResult}; +}; +export type AISummaryOwnerStateBase = AISummaryOwnerKey & { + lifecycle: AISummaryLifecycle; + content?: AISummaryContent; + sourceTimestamp?: number; + arrivalSequence: number; + contentRevision: number; + counters: AISummaryCounters; + feedback: AISummaryFeedback; + errorCategory?: AISummaryErrorCategory; + responseAttempted: boolean; +}; +export type AISummaryMidCallOwnerState = AISummaryOwnerStateBase & { + role: 'initiator' | 'receiver'; + actionType: AISummaryActionType; + midCallFeedbackPending: boolean; + postCallGeneration?: never; + postCallFeedbackPending?: never; + feedbackStatus?: never; + postCallCaptureRevision?: never; + frozenResponse?: never; +}; +export type AISummaryPostCallOwnerState = AISummaryOwnerStateBase & { + role: 'post-call'; + actionType?: never; + midCallFeedbackPending?: never; + postCallGeneration?: number; + postCallFeedbackPending: boolean; + feedbackStatus?: AISummaryFeedbackStatus; + postCallCaptureRevision?: number; + frozenResponse?: AISummaryResponse; + agentWrappedUpObserved: boolean; +}; +export type AISummaryOwnerState = AISummaryMidCallOwnerState | AISummaryPostCallOwnerState; +export type AISummaryViewModelBase = Pick< + AISummaryOwnerStateBase, + | 'lifecycle' + | 'content' + | 'contentRevision' + | 'counters' + | 'feedback' +> & { + summarySurface: AISummarySurface; + eligible: boolean; + requestPending: boolean; + controlsDisabled: boolean; +}; +export type AISummaryViewModel = + | (AISummaryViewModelBase & { + role: 'initiator' | 'receiver'; + actionType: AISummaryActionType; + midCallFeedbackPending: boolean; + postCallFeedbackPending?: never; + feedbackStatus?: never; + }) + | (AISummaryViewModelBase & { + role: 'post-call'; + actionType?: never; + midCallFeedbackPending?: never; + postCallFeedbackPending: boolean; + feedbackStatus?: AISummaryFeedbackStatus; + }); +export type AISummaryStoreActionResult = + | {outcome: TSuccess} + | {outcome: 'failed'} + | {outcome: 'blocked'} + | {outcome: 'stale'}; +export type AISummaryRequestResult = AISummaryStoreActionResult<'accepted'>; +export type AISummaryPostCallRequestTrigger = 'reason-commit' | 'retry'; +export type AISummaryPreActionSendResult = AISummaryStoreActionResult<'sent'>; +export type AISummaryFeedbackResult = AISummaryStoreActionResult<'confirmed'>; +export type NormalizedAISummaryResult = + | { + kind: 'ready'; + interactionId: string; + timestamp?: number; + content: AISummaryContent; + } + | {kind: 'unsupported'; interactionId: string; timestamp?: number} + | {kind: 'discarded'; reason: 'interaction-mismatch'} + | {kind: 'error'; interactionId: string; category: AISummaryErrorCategory}; +``` + +`contentRevision` is a named field of every `AISummaryViewModel`, not a component-local argument invented by D4. A ready content commit obtains a positive token by incrementing the Store-lifetime `nextAISummaryContentRevision`; every accepted edit obtains the next token, while a same-agent ownership successor carries the current token unchanged until content or an edit changes it. The counter is never reset or reused by interaction, ownership, sign-out, or session cleanup. Every action-bearing component closes over the rendered `contentRevision` and passes it as `expectedRevision`; a store action is current only when that value equals the selected role view's current `contentRevision` after the kind/role/current-owner checks. A mismatch is stale and performs no counter mutation, feedback paint, response composition, or SDK call; for copy, the component-owned Clipboard attempt has already fulfilled before the guarded recorder is notified, so only the copied counter is rejected as stale. + +At runtime, `aiSummaryCapabilities: Map` is keyed by the validated capability payload's `interactionId`; because capability can precede Task creation, that map key is not assumed canonical until a Task's independently derived stable main-call key matches it exactly. `aiSummaryOwners: Map` uses an internal non-logged key encoding the three `AISummaryOwnerKey` fields plus `role`, exactly the `AISummaryViewKey`; initiating, receiving, and post-call records therefore cannot share content, revisions, counters, feedback, wrap-up observations, or failure state across an interaction, agent, generation, or role. Every map, sequence, role-view record, counter, pending flag, request, capture guard, and frozen response named here is an observable field of the existing `Store` through `makeAutoObservable`; React components receive projections and never own durable summary state in `useState`. + +`midCallFeedbackPending` means that a mid-call `sendMidCallSummaryResponse` feedback update is in flight and is used only for the matching role view's disable state; it never produces post-call description copy. `postCallFeedbackPending` means that a locally selected post-call value still awaits the one final response and projects `feedbackStatus: 'pending'`. A final response failure clears that Boolean and projects `feedbackStatus: 'not-confirmed'`; confirmed success clears both. `postCallCaptureRevision` is installed atomically by a successful `capturePostCallDraft` and makes that exact post-call role view/revision immutable while wrap-up is pending without prematurely changing the lifecycle; a wrap-up failure clears the matching guard, while successful freeze/submission supersedes it. + +`postCallGeneration` is absent until the first reason commit, then holds an internal positive token allocated from a Store-lifetime monotonic counter for each explicit `reason-commit`; it is never exposed to presentation or used as a substitute for `contentRevision`. A `retry` captures the installed token rather than allocating one and returns `{outcome: 'blocked'}` without an SDK call if no token exists. Every pending post-call request records its request sequence, generation, deadline, and settlement-closed marker. A newer reason commit immediately makes every older-generation request stale before freshness comparison, while open requests within the same current generation use source timestamp/equal-last-arrival when both values exist and later owner-role arrival when either is absent. A ready candidate in the installed generation therefore replaces a retained draft from an older generation without comparing their source timestamps; generation precedence implements the user's newer explicit request. Superseded or timeout-closed requests remain settlement-observed for resource cleanup, but they are excluded from the current generation's `requestPending` projection and cannot mutate content, feedback, counters, errors, completion escape, or status transitions. + +`currentAISummaryOwnerByInteraction: Map` points only to an accepted owner generation, while a request may create a non-visible pending owner key; projection additionally requires that key's `agentId === store.agentId`, so a prior agent's content is never exposed while a successor waits. `nextAISummaryOwnershipGeneration` starts at `0` and is incremented before every new owner epoch, including the in-session ownership boundaries enumerated above; it is never derived from, reset by, or serialized with the backend's `interaction.owner`. Same-agent epoch advancement migrates each existing role view's visible content/counters/feedback independently into the successor epoch, whereas agent replacement starts every applicable role view at zero. `postCallResponseFailures` retains exact keyed frozen failed records separately from ordinary Task lifecycle state only while their interaction remains current; interaction/session cleanup deletes them. + +`nextAISummaryArrivalSequence` is a monotonically increasing in-memory integer stamped on each candidate, but it is compared only inside the exact `AISummaryViewKey` owner-role scope. When both the current and candidate payloads carry a valid SDK `timestamp`, the greater value wins and equal values use the later owner-role arrival. When either payload omits `timestamp`, the later owner-role arrival wins deterministically; an omitted value is never converted to a clock value or compared across owners. `nextAISummaryContentRevision` supplies the Store-lifetime collision-free positive revision tokens used by every mutator guard and is retained across all cleanup. `nextAISummaryPostCallGeneration` supplies the equally collision-free reason-generation tokens and is not reset by popover close, owner cleanup, sign-out, or session cleanup. `nextAISummaryRequestSequence`, `pendingAISummaryRequests: Map`, and `latestAISummaryRequestByOwnerRole: Map` associate each Promise with its role-scoped pending indicator, its validated initiating action type or post-call generation when applicable, the admitted 15,000 ms monotonic deadline, and an idempotent settlement fence; the latest-request map uses the encoded `AISummaryViewKey`. `deadlineAt` is an acceptance fence derived from the SDK bound, not a second widget timer. Every fulfillment/rejection handler must atomically claim an open record before normalization or mutation. Surfacing timeout first sets `settlementClosed: true`; any later settlement for that request sequence is consumed as `stale` regardless of `timestamp`, cannot clear another pending entry, and cannot recreate content, counters, feedback, status, or callbacks. A request sequence otherwise never determines successful content freshness. For mid-call and for post-call requests within one reason generation, it does not invalidate another same-owner-role success; for post-call across reason generations, the generation check runs first and invalidates every superseded result before timestamp/arrival ordering. Because Promise rejections have no source timestamp, only rejection of the latest-started open request in the current owner/role/generation may paint `generation-error`, while an older, closed, or superseded rejection is consumed. Event-delivered normalized errors do not use those request maps: their required `interactionId` plus the Task listener's captured full owner key and receiver role provide attribution and the current-generation check. + +`aiSummaryTaskListeners: Record void; receiving: (value: unknown) => void}>` stores feature-enablement, receiving, and ordinary task-scoped callbacks plus the owner generation captured at binding. Registration seeds capability from that exact Task's public `aiSummaryCapabilities` snapshot, hydration/replacement rebinds callbacks to the replacement object, and generic `handleTaskRemove` detaches the exact Task/callback pairs. Separately, `aiSummaryWrappedUpSubscriptions: Map void; bindings: ReadonlyArray<{task: ITask; callback: () => void}>}>` stores one stable idempotent logical callback and every exact Task/callback binding for that owner outside `taskList`. Replacement attaches the successor binding before ordinary teardown, generic Task removal cannot detach these bindings, the first matching event detaches all of them after recording the boundary, and interaction/session cleanup removes every remainder. + +`feedbackStatus` is the presentation-safe post-call state source; it contains only `pending` or `not-confirmed`, never visible copy. Components never infer it from `lifecycle` or the existence of a frozen payload. The post-call projection sets it to `pending` exactly while `postCallFeedbackPending` is true, changes it to `not-confirmed` when the one final response fails, and removes it immediately on confirmed submission. It is absent from every mid-call view. `@webex/cc-components` resolves the token through the stable message IDs `aiSummary.feedback.pendingSubmission` and `aiSummary.feedback.submissionNotConfirmed`; the resulting ordinary text renders directly beneath the post-call feedback group, including the read-only completion state after wrap-up has committed. The token remains addressable only by that exact owner view and is removed with the matching failed record when the backend wrapped-up boundary or interaction/session cleanup wins. + +`agentWrappedUpObserved` exists only inside each `AISummaryPostCallOwnerState`, whose map key contains canonical `interactionId`, authenticated `agentId`, `ownershipGeneration`, and role. It is never a Store-global Boolean. Each `wrappedUp` listener captures that full owner key when it is bound and compare-marks only the matching post-call owner/failure record; an event for interaction X cannot mark, discard, or sweep interaction Y's record. A same-interaction stale owner generation likewise cannot mark its successor. The exact captured owner is retained alongside the task/callback entry even if `handleTaskRemove` runs first. + +Post-call draft bytes and the exact pre-wrap-up `ITask` never cross the Store API. A successful capture creates a fresh opaque `PostCallDraftCaptureToken` (`symbol`) and stores `{owner, contentRevision, content, counters, feedback, wrapUpCode, task}` in the non-projected `postCallDraftCaptures` map keyed by that token. The task-side orchestrator can only return the token to `releasePostCallDraft` or `freezeAndSendPostCallSummary`; it cannot read or mutate the snapshot or substitute a Task. Both methods validate token identity and one-time state against the store-owned record, and an unknown, released, or reused token makes no SDK call or cross-owner mutation and resolves through the documented sanitized failure path. Tokens, snapshots, and Task references are removed on release, terminal send settlement, or global privacy cleanup and are never logged, serialized, persisted, or projected. + +`nextAISummaryStatusSequence` starts at `0` and is incremented before every status publication. It is monotonic for the lifetime of the Store singleton: ownership changes, `evictAISummaryInteraction`, keyed-content cleanup, sign-out, and session cleanup never reset or decrement it. `pendingAISummaryStatusTransitions: readonly AISummaryStatusTransition[]` is explicitly annotated `observable.ref` in the existing `makeAutoObservable` configuration, starts as `Object.freeze([])`, and is replaced with a newly frozen array whenever a current accepted request/event appends `available`, `unavailable`, `submitted`, or `response-failed`; it is never a latest-wins slot or a mutated-in-place observable array. ID-mismatched, stale-generation, lower-timestamp, edit, counter, feedback, hold/remount, and render-only changes append nothing. Every accepted semantic transition receives the next sequence and is appended in mutation order, including multiple transitions produced inside one MobX/React batch, so one eventual render still sees the complete ordered snapshot. `getPendingAISummaryStatusTransitions()` returns the stable readonly array identity until append/acknowledgement changes it, and `acknowledgeAISummaryStatusTransition(sequence)` succeeds only when `sequence` equals the current queue head, then assigns `Object.freeze(current.slice(1))` in the same store action; callers cannot skip, reorder, or acknowledge a transition they did not receive. The transition contains only its local sequence and exact callback-safe detail, with no interaction/agent ID, counters, feedback, error, or content. The queue survives CallControl unmount/remount and keyed interaction/content cleanup, so a same-session transition recorded while no CallControl is mounted remains deliverable; an acknowledged entry is absent on a same-agent remount. Global sign-out/session privacy cleanup discards any still-undelivered content-free entries without resetting the sequence, preventing one authenticated session's statuses from being delivered to another while ensuring the next session's sequence cannot collide. + +An ownership-generation advance does not wait for the interaction-terminal sweep: after every applicable role-view successor has been seeded and the common owner epoch installed as current, all predecessor `aiSummaryOwners` and `latestAISummaryRequestByOwnerRole` entries are deleted in the same action. Any predecessor `pendingAISummaryRequests` entries remain only until their already bounded settlement/timeout cleanup; because their owner key is no longer current, they cannot recreate a predecessor record or projection. This prevents repeated transfer/conference epochs inside one long interaction from accumulating owner records while preserving independently keyed initiating and receiving views in the successor epoch. + +`evictAISummaryInteraction(interactionId, boundary)` is the one internal per-interaction sweep, with `boundary` distinguishing interaction/terminal cleanup, the matching captured-owner wrapped-up event, and global privacy cleanup. Every invocation deletes that ID's capability, current-owner pointer, ordinary role views, latest-request entries, pending requests, `postCallResponseFailures`, and wrapped-up registry bindings; a removed Promise or late event may still settle but cannot recreate state. For a matching wrapped-up event, the callback compare-marks and deletes only failure/capture entries whose full owner key matches before completing the interaction sweep and detaching all bindings for that owner. If that event arrived while the response was pending, the owner-local `agentWrappedUpObserved` fence survives only until settlement; a later rejection may queue its content-free failure transition but immediately discards the frozen response and cannot reinstall a listener. Interaction/terminal cleanup clears the same content-bearing state and registry even if the backend event has not arrived. Global privacy cleanup additionally deletes every interaction's remaining entries and discards the pending status queue. Cleanup resets only state-local ownership/request/arrival sequences; the content-free status, content-revision, and post-call-generation counters remain monotonic so later callback/revision/generation guards cannot collide after cleanup. Thus no capability, owner, role view, request, capture, failure, or retained binding survives beyond its own stated boundary, and no object is JSON-stringified, persisted, placed in a URL, or passed to analytics. + +For a `response-failed` record whose interaction remains current, the applicable backend boundary is only that record's matching captured-owner `AgentWrappedUp`/`TASK_WRAPPEDUP` event, never a different Task's event. Ordinary Task replacement/removal is not an interaction boundary and therefore retains the frozen record plus Store/session subscription. A visible interaction switch, terminal interaction cleanup, or session cleanup is the independent REQ-002 privacy boundary: it clears the failed record and every registry binding immediately rather than hiding a tombstone for a later event. Normal mid-call and successfully submitted post-call state use the same stated interaction-change boundary. + +`AISummaryViewModel.summarySurface` is the closed, presentation-safe branch selector; containers never inspect `errorCategory`. `requestPending` is defined for every returned role view, not only mid-call: for mid-call it is `true` while at least one pending entry exists for that `AISummaryViewKey`, and for post-call it is `true` only while an entry also matches the installed `postCallGeneration`. Missing eligibility or lifecycle and every unauthorized/initialization rejection produce `eligible: false` plus `summarySurface: 'omitted'`. A current request without retained content produces `generating`; ready/frozen/submitted/response-failed content produces `content`; only a normalized `{kind: 'unsupported'}` valid envelope produces `unavailable`; and every other visible generation failure produces `generic-error`. Concurrent post-call requests in the same generation therefore collapse into one `requestPending: true` projection and one visible `generating` panel rather than duplicate panels; each settlement/categorization removes only its own sequence, and the flag becomes `false` only after the last outstanding current-generation request ends. A superseded-generation request never contributes to that flag. If accepted content arrives while another current-generation request remains, `content` takes presentation precedence but `requestPending` remains true so Retry/re-entrant generation stays disabled until the final current-generation settlement. Post-call reason selection and popover lifecycle never clear accepted content or increment its revision: an existing edited draft remains the projected `content` while a committed-reason request is pending and after a blocked, stale, failed, or unsupported settlement. Only an accepted current-owner/current-generation `ready` response obtains a new revision and replaces it; that atomic replacement preserves the owner-generation viewed/edited/copied counters, increments viewed once for the successful reveal, and resets the superseded draft's local feedback to `none`, `postCallFeedbackPending: false`, and no `feedbackStatus`. This lets D6 preserve exact edit bytes and pending feedback until the explicit reason commit succeeds without moving durable content into component-local state. Thus `The summary is not available` is reachable only from `unavailable` when no accepted content is retained, `Having trouble generating summary` is reachable only from `generic-error` when no accepted content is retained, and `omitted` mounts no summary section or receiver trigger. `getAISummaryView` may return `undefined` when there is no current interaction/owner at all; a returned omitted view is still never rendered. It validates the selector pair (`mid-call` with `initiator`/`receiver`, or `post-call` with `post-call`) before resolving the role-keyed record. + +For a matching current post-call owner, `response-failed` remains a `content` surface with `controlsDisabled: true` and `feedbackStatus: 'not-confirmed'`: its previously shown content and selected feedback may remain visible read-only, but the frozen SDK response object is never exposed, editable, or re-sendable. If another interaction becomes visible, the interaction-boundary sweep clears that content, tombstone, and its wrapped-up bindings, so no old content or status can project into the new view. A `submitted` view has `feedbackStatus: undefined` immediately, even if its successful content view remains until interaction change. + +For a ready receiver view, projection re-evaluates the D0-admitted Task action correlation. If a previously resolved correlation becomes indeterminate while that content is still current, the store preserves the card and prior feedback but projects `controlsDisabled: true`; D4 passes that state to the shared action row so Copy/Like/Dislike use the existing disabled styling and native disabled semantics exposed to assistive technology. No handler is invoked while disabled. An indeterminate correlation before content acceptance still cannot create a ready receiver view. This makes the fail-closed state visible and operable rather than leaving apparently active controls that silently do nothing. + +The pure module's final exact named exports are below. Before D0, D2 implements the `unknown`-input normalization, ordering, owner, and error portions with local structural inputs/outputs; D2b adds the `ITask`- and SDK-response-typed signatures only after D0 proves assignability, without a cast. Voice detection is deliberately not another `ai-summary.ts` export: D2b exports the extracted `isTelephonyTask(task: ITask | null | undefined): boolean` from the existing `task-utils.ts` selector owner and makes both legacy mute behavior and summary gating consume it. + +```typescript +normalizeFeatureEnablement( + value: unknown, + arrivalSequence: number +): {kind: 'valid'; capability: AISummaryCapability} | {kind: 'invalid'; interactionId?: string}; +normalizeAISummaryPayload( + role: AISummaryRole, + value: unknown, + expectedInteractionId: string +): NormalizedAISummaryResult; +getCanonicalAISummaryInteractionId(task: ITask): string | undefined; +shouldAcceptAISummaryCandidate( + current: Pick | undefined, + timestamp: number | undefined, + arrivalSequence: number +): boolean; +composeMidCallResponse( + state: AISummaryMidCallOwnerState, + feedback: AISummaryFeedback +): AISummaryResponse; +composePostCallResponse(capture: PostCallDraftSnapshot): AISummaryResponse; +normalizeAISummaryError(error: unknown): AISummaryErrorCategory; +``` + +`normalizeFeatureEnablement` requires a non-empty string interaction ID. Optional `actionTimestamp`, when present, must be a finite non-negative integer UTC Unix-epoch-millisecond value; it is stored unchanged and is never parsed through a local timezone. A missing interaction ID returns invalid without an ID and never enables an interaction. A malformed supplied timestamp makes the event invalid and cannot remove or replace an accepted capability. For two valid event observations with timestamps, greater wins and equal uses later `arrivalSequence`; if either valid observation omits the timestamp, later arrival on the same exact Task wins. The public `task.aiSummaryCapabilities` snapshot has no timestamp: registration normalizes it as a Task-bound initial observation using the independently derived canonical Task interaction ID, before later events, without manufacturing an SDK event field. Each capability is enabled only by the literal boolean `true`; missing, non-boolean, or false fields normalize to `false`, so a later valid Task observation can revoke enablement. At projection/request time the store rechecks the current canonical key. A record whose `interactionId` differs is unavailable for that Task, is never re-keyed, and cannot enable either flag. + +`normalizeAISummaryPayload(role, value, expectedInteractionId): NormalizedAISummaryResult` treats the concrete SDK field `conversationId` as the requirement's `interactionId` at this adapter boundary. `expectedInteractionId` is the already resolved canonical main-call key captured when the request/listener was bound. A missing/empty `conversationId` or one not byte-equal to that key returns `{kind: 'discarded', reason: 'interaction-mismatch'}` before body normalization; callers terminally consume that result with no visible error, availability transition, owner reassignment, counter, feedback, or content mutation. A Promise-backed caller clears only the exact pending request sequence it captured; an unsolicited event has no pending state to clear. D0 proving fixture equality does not remove this check. After the ID matches, every malformed/empty payload result is `{kind: 'error', interactionId: expectedInteractionId, category}`; the raw envelope is never retained. + +For ready or unsupported results, the exact optional SDK response field is `timestamp`. If the property is present, the adapter requires a finite non-negative integer and stores it unchanged as `sourceTimestamp`; a present malformed value produces an attributed error. If the property is absent, the normalized result omits `timestamp` and the state omits `sourceTimestamp`. `shouldAcceptAISummaryCandidate` compares only candidates for the same owner-role: two valid timestamps use greatest-timestamp/equal-last-arrival ordering, while any comparison in which either side lacks a timestamp uses later owner-role `arrivalSequence`. Thus omission has a deterministic fallback and can never borrow feature enablement's `actionTimestamp`, a local wall clock, or another owner's ordering. + +For an initiating payload, the adapter validates the `sections` object and visits its own enumerable keys exactly once in the SDK payload's ECMAScript property-enumeration order. It appends only declared mid-call keys whose values are strings with `.trim().length > 0` to the normalized `AISummarySection[]`; unknown keys are dropped at this store boundary and can neither reserve an order slot nor reach presentation/response code. If no declared section remains, it accepts a `summaryText` string passing the same non-empty test and drops cards. Post-call performs the same single-pass, source-order-preserving normalization against its declared section-key allowlist/plain fallback and drops cards. The resulting editable array is the sole SDK section-order authority consumed by response composition and by D3's display projection; neither role's presentation-definition tuple may sort it or synthesize absent keys. When post-call sections exist, a non-empty declared top-level `resolution` is retained separately as `content.resolution`; D2 does not assign it a neighbour or insert it into the editable SDK-section array. D3 projects it as the read-only `Outcome` row by the deterministic typed ordinal declared in `POST_CALL_DISPLAY_SECTION_ORDER`, while preserving the relative order of every admitted SDK section. It is omitted when absent and never inserted into the SDK response `summary` object. `resolution` alone does not suppress the required plain-`summaryText` fallback. The original accepted string bytes, including leading/trailing whitespace and embedded newlines, are retained; trimming is validation only, so display/edit/copy/response composition do not silently rewrite SDK content. Receiver requires an object adaptive card and discards `sections`/`summaryText` as render sources. A matching valid envelope with no role-compatible render content returns `{kind: 'unsupported', interactionId: expectedInteractionId}` plus `timestamp` only when the SDK supplied it; that normalization result is the only route into the `unsupported` lifecycle. Promise transport rejection bypasses payload normalization and is attributed by its captured request/owner; `normalizeAISummaryError` returns its sanitized category and can never return `unsupported`. Text is retained as text, not HTML. + +**Store API signatures.** `IStoreWrapper` gains only concrete actions consumed by current containers: + +```typescript +getAISummaryView(kind: AISummaryKind, role: AISummaryRole): AISummaryViewModel | undefined; +getPendingAISummaryStatusTransitions(): readonly AISummaryStatusTransition[]; +acknowledgeAISummaryStatusTransition(sequence: number): boolean; +getCurrentAISummaryOwnerKey(): AISummaryOwnerKey | undefined; +advanceAISummaryOwnership( + expected: AISummaryOwnerKey, + boundary: AISummaryOwnershipBoundary +): AISummaryOwnerKey | undefined; +requestMidCallSummary(actionType: AISummaryActionType): Promise; +requestPostCallSummary(trigger: AISummaryPostCallRequestTrigger): Promise; +sendMidCallSummaryBeforeAction(actionType: AISummaryActionType): Promise; +setMidCallSummaryFeedback( + role: 'initiator' | 'receiver', + feedback: Exclude, + actionType: AISummaryActionType, + expectedRevision: number +): Promise; +setPostCallSummaryFeedback( + feedback: Exclude, + expectedRevision: number +): boolean; +editAISummary( + kind: AISummaryKind, + role: AISummaryRole, + field: AISummaryEditableField, + value: string, + expectedRevision: number +): boolean; +recordAISummaryViewed(kind: AISummaryKind, role: AISummaryRole, expectedRevision: number): boolean; +recordAISummaryCopied(kind: AISummaryKind, role: AISummaryRole, expectedRevision: number): boolean; +capturePostCallDraft( + wrapUpCode: string, + expectedRevision: number +): PostCallDraftCaptureToken | undefined; +releasePostCallDraft(token: PostCallDraftCaptureToken): boolean; +freezeAndSendPostCallSummary(token: PostCallDraftCaptureToken): Promise; +``` + +These exact signatures are declared in `packages/contact-center/store/src/store.types.ts` by D2. For D4, the receiver bindings map one-to-one without aliases: `openReceiverSummary` calls `recordAISummaryViewed('mid-call', 'receiver', expectedRevision)` before a successful content reveal, `recordReceiverSummaryCopied` returns the boolean from `recordAISummaryCopied('mid-call', 'receiver', expectedRevision)` only after the `AISummary`-owned Clipboard write fulfills, and `setReceiverSummaryFeedback` returns the always-fulfilled result from `setMidCallSummaryFeedback('receiver', feedback, actionType, expectedRevision)`. `openReceiverSummary` is the only UI/chrome action; it is not a second store method. + +The four Promise-returning request/feedback/pre-action actions above have one settlement convention: they catch synchronous throws, Promise rejections, and the store-owned timeout, perform all error categorization internally, and always fulfill with an `outcome`-discriminated sanitized object. `accepted`, `sent`, and `confirmed` are the respective success discriminants; `failed` is a current-owner SDK/transport failure, `blocked` is an ineligible or indeterminate contract guard, and `stale` is an interaction/owner/revision invalidation. No result contains a raw error or payload, and no caller needs a rejection handler to prevent an unhandled rejection. Containers still observe the returned Promise so the closed outcome is deliberately consumed. Every D5/D6b pre-action caller must `await` that fulfillment before creating the telephony Promise; after settlement, D5 continues telephony for every outcome and may branch on `outcome: 'failed'` only for local completion bookkeeping, never as a suppressing gate. + +The capture methods use the token only as an opaque Map key. `capturePostCallDraft` obtains and stores the exact current Task itself; neither the snapshot nor a caller-supplied `ITask` appears in `IStoreWrapper`. `releasePostCallDraft` returns `false` for an unknown/released/reused token. `freezeAndSendPostCallSummary` always fulfills: for such a token it makes no SDK call, exposes no raw error or payload, mutates no owner record, and returns the sanitized `response-failed` result; the conforming D6 path can receive only its just-issued one-time token. A valid token remains bound to its captured Task/owner even when visibility changes, so the required post-wrap-up send cannot be redirected to whichever Task is current later. + +`getCurrentAISummaryOwnerKey` is an internal orchestration capture used immediately before a transfer/conference action; it is never threaded to a component, host callback, log, or persistence surface. `advanceAISummaryOwnership` compare-and-swaps that exact key after independently observed successful action/event confirmation and returns the successor or `undefined` for a stale/duplicate boundary. `editAISummary`, `recordAISummaryViewed`, and `recordAISummaryCopied` all validate the same legal `kind`/`role` selector pairs as `getAISummaryView` and resolve the exact `AISummaryViewKey`; there is no implicit "current role." This is required when the authenticated agent has both an initiating preparation view and a receiving-agent card view in one owner generation. `editAISummary` accepts only `AISummaryEditableField`, requires that role view's current owner/revision, compares the exact string, commits one text change, and increments only that view's `edited` counter and `contentRevision` once. It also requires the field to belong to the selected role and content variant: only declared mid-call keys are accepted for initiating sections, only declared post-call section keys for post-call sections, and `summaryText` only for plain content. Every normalized typed section value and plain text is editable; the separately retained presentation-only `content.resolution`/`Outcome` value and receiver cards are not. Runtime callers are still checked because custom-element and event input is untrusted. Empty text is allowed as an edit but cannot turn an otherwise empty response into a renderable success. Invalid kind/role pairs, cross-role fields, unknown fields, read-only resolution, unchanged values, stale revisions, frozen state, or ineligibility return `false` without mutating the other coexisting role view. View/copy recorders likewise return `false` on an invalid pair or stale/ineligible/hidden action and increment exactly once only on the selected role view. `setMidCallSummaryFeedback` requires the rendered role view's explicit `actionType` and Store-lifetime-unique `expectedRevision`; before setting `midCallFeedbackPending`, composing a payload, or calling the SDK, it requires both to equal the current eligible `AISummaryMidCallOwnerState`. A revision mismatch is a stale owner/content action, and an action-type mismatch is a stale consult/transfer rendering, including a click queued across consult-to-transfer promotion. Either fulfills `{outcome: 'stale'}` without mutation or an SDK call. `setPostCallSummaryFeedback` applies the same runtime boundary: it returns `true` only when the Store-lifetime-unique `expectedRevision` is the current eligible post-call role view's revision and that view is editable rather than captured/frozen/submitted/response-failed; because revisions are never reused across owner generations, that comparison is also the owner-generation guard; only then does it set the local mutually exclusive feedback and `postCallFeedbackPending`. A stale revision or ownership generation, wrong role, hidden/ineligible surface, frozen capture/response, or invalid runtime feedback returns `false` without changing feedback, pending state, content, counters, or the eventual frozen response. Copy content itself remains in the component and never crosses a logging boundary. + +**Capability, request, and ordering control flow.** + +1. `registerTaskEventListeners` derives the canonical interaction identity, seeds a Task-bound initial capability observation from the exact Task's public `aiSummaryCapabilities`, then binds the stable `handleAISummaryFeatureEnablement` callback to `TASK_EVENTS.TASK_FEATURE_ENABLEMENT` on that same Task. `aiSummaryTaskListeners` retains the exact Task/callback pair with the receiving and ordinary task-bound events; Task replacement/removal and `cleanUpStore` detach those exact pairs. This preserves capability emitted before widget attachment without subscribing to or inventing a `cc`-owned feature event. A distinct Store/session-owned `aiSummaryWrappedUpSubscriptions` registry keys one stable logical `AgentWrappedUp`/`TASK_WRAPPEDUP` callback by the full `AISummaryOwnerKey` and retains its exact Task/callback bindings outside `taskList`. Task replacement attaches the idempotent owner callback to the successor before ordinary listener teardown; generic `handleTaskRemove` detaches feature/receiving/ordinary callbacks but cannot detach these bindings or discard pending/frozen failure state. The first matching wrapped-up callback atomically marks `agentWrappedUpObserved`, runs the owner-only summary transition/cleanup inside a catch boundary that consumes any synchronous summary failure after sanitization, and makes duplicate bindings no-op. `refreshTaskList` is invoked from `finally`, so malformed summary state, a cleanup guard, or a throwing transition observer cannot suppress the current legacy refresh effect; an outer `finally` detaches every retained binding for that owner even if refresh itself throws. An interaction change detaches and clears the owner registry entry as the per-interaction privacy boundary; `cleanUpStore` does the same globally. Existing conference participant handlers preserve their legacy refresh behavior and add only idempotent ownership-boundary advancement; they still issue no summary request and cancel no pending work. Initiating and post-call content are consumed from request Promises, preventing a late public event after timeout from being rendered. +2. `setCurrentTask` calls `getCanonicalAISummaryInteractionId(task: ITask): string | undefined`. The helper trims neither value and accepts only a non-empty stable `task.data.interaction.mainInteractionId` or `findMediaResourceId(task, 'mainCall')`; if both exist they must be byte-equal. Missing/conflicting stable candidates return `undefined`, and the leg-scoped `task.data.interaction.interactionId` and `task.data.interactionId` are never consulted. The resulting key is paired with non-empty `store.agentId`. Current-agent authorization requires the existing registered SDK session (`store.isAgentLoggedIn === true`) and that non-empty authenticated agent ID; SDK unauthorized/initialization failures revoke the applicable projection. The D2b-extracted `task-utils.ts#isTelephonyTask` is the single media gate used here and by existing store behavior. Empty/conflicting canonical identity, a non-telephony task, a capability record whose `interactionId` is not exactly the canonical key, a false flag, absent authorization, unauthorized/init failure, or missing lifecycle makes `eligible: false` and projects `summarySurface: 'omitted'`; no role-name, leaf-ID, participant-name, owner-field guess, or duplicate media predicate is enabled. +3. `StoreWrapper.getOrCreateAISummaryOwnerKey(interactionId: string, agentId: string): AISummaryOwnerKey` creates the first epoch and reuses it only while the same ownership phase remains current. `advanceAISummaryOwnership(expected: AISummaryOwnerKey, boundary: AISummaryOwnershipBoundary): AISummaryOwnerKey | undefined` compare-and-swaps only the expected current key, increments `nextAISummaryOwnershipGeneration`, and makes the old key non-current before returning the successor; duplicate/late boundary notifications therefore no-op. These stateful methods live in `storeEventsWrapper.ts`; `ai-summary.ts` keeps only the pure successor-state reducer. Successful transfer/handoff completion, including consult-to-transfer promotion, reports `handoff-complete`; the first conference-establishment transition reports `conference-established`; and each later topology-driven per-agent full-history cycle reports `conference-content-cycle`. The Task action settlement/event route must provide one of those confirmed boundaries—Task object replacement or `TASK_END` alone is not proof. For the same agent, the pure reducer copies visible content, counters, feedback, and revision into the successor so REQ-006 retains the old view until replacement and REQ-008 counters continue. For a different authenticated agent or re-entry after a terminal clear it creates empty state with zero counters. Hold/resume, same-agent remount, and pure task hydration do not advance. Every async operation captures the full key; a settlement whose key is not the current or matching pending key for that exact agent is consumed and ignored without a counter, error, callback, or unhandled rejection. Repeated user requests inside the same phase retain the epoch and use request/timestamp ordering instead. +4. Each explicit user request calls the matching SDK method and receives a unique pending sequence used to pair settlement/cleanup with that request and to suppress an older request's timestamp-less rejection after a newer request began. At request start the store reconciles the explicit initiating `CONSULT`/`TRANSFER` input with the D0-verified Task relationship/action correlation and records the resolved value and role with that pending request; participant-add preparation resolves to `CONSULT`, direct/consult transfer resolves to `TRANSFER`, and an invalid, mismatched, or indeterminate correlation returns `{outcome: 'blocked'}`, calls no SDK method, and cannot create or paint an owner/view. The pending record also captures `deadlineAt` from the monotonic test-injectable clock as invocation time plus the admitted 15,000 ms SDK bound and starts with `settlementClosed: false`. Settlement re-evaluates the same correlation before committing: a verified consult-to-transfer promotion owns the candidate as `TRANSFER`, while an indeterminate result is consumed without painting. + + Every fulfillment or rejection handler must compare-and-set that exact record from open to closed before normalization, timestamp comparison, or visible mutation. The SDK timeout rejection closes the record before surfacing timeout. If a fulfillment handler runs after `deadlineAt` while the record is still open, it performs the same close-and-surface-timeout transition and drops the payload; the deadline is an acceptance fence, not a second timer. Once timeout has been surfaced, every later fulfillment, event-to-request settlement, rejection, or cleanup attempt for that sequence returns/records `stale` and is a no-op regardless of a newer or greater `timestamp`. The closed marker remains captured by the installed handlers after the pending-map entry is removed, so a late callback cannot recreate the entry or state. This rule precedes owner, generation, and freshness ordering. + + Mid-call permits one in-flight request for the initiating owner-role view and exposes `requestPending: true` until the SDK Promise settles or rejects at 15 seconds; containers use that flag to disable further consult/transfer initiation, while `controlsDisabled` remains reserved for stale/frozen summary actions. If accepted mid-call content already exists during a conference-participant preparation request, its ready lifecycle, actions, and counters remain projected while the request is pending. Each explicit post-call request adds its own sequence before invoking the SDK; the post-call view projects the aggregate `requestPending` OR for that exact owner/role and installed generation, the Retry control is disabled immediately, and direct concurrent calls still render only one generating state when no content is retained. A reason-regeneration request with retained accepted content leaves that exact content, edits, revision, and Complete Wrap-Up capture path projected while pending. Settlement or error categorization removes only the matching sequence, so one completion cannot clear the flag while another request remains. With no retained content, a recorded error is held behind the generating projection until the aggregate becomes false; an accepted ready candidate may move the surface to content while the flag continues to prevent another request. Every request catches all failure paths and fulfills `AISummaryRequestResult`; a current accepted normalized settlement returns `{outcome: 'accepted'}`, a current transport/SDK failure returns `{outcome: 'failed'}`, an eligibility/action-correlation guard returns `{outcome: 'blocked'}`, and invalidated ownership or a closed settlement fence returns `{outcome: 'stale'}`. No widget timeout beyond the store-owned SDK settlement bound, `AbortSignal`, cancellation, or retry timer is added. All open same-owner-role ready post-call settlements within the installed generation remain eligible to compete by source timestamp/arrival even if a later request was started first; request sequence never overrides that content rule except for the terminal timeout fence. + + For post-call, that final freshness rule is scoped to one current `postCallGeneration`. `requestPostCallSummary('reason-commit')` allocates and installs a new generation before transport, so every request from a prior reason generation is superseded and resolves `stale` before its timestamp can compete, even if its response has the numerically greatest timestamp or arrives last. `requestPostCallSummary('retry')` reuses the installed generation, so concurrent current-generation retries use greatest source timestamp/equal-timestamp last arrival when both values exist and later owner-role arrival when either is absent. A timeout-closed retry is stale before either rule. `requestPending` is the OR only of open pending entries in the installed generation; an older superseded or closed transport is still observed and removed by its own sequence but cannot keep Retry or reason controls disabled. +5. The request/listener passes its captured canonical key as `expectedInteractionId` to `normalizeAISummaryPayload`; an ID-mismatched `discarded` result clears only that Promise's exact captured pending-sequence entry and cannot clear another request, reassign an owner, or mutate visible state. The timeout fence is checked before this normalization. Mid-call and receiver ready candidates then use the ordinary freshness rule within the exact owner-role: when both sides have a valid `timestamp`, greater replaces, equal uses later local arrival, and lower loses; when either side omits `timestamp`, later owner-role arrival wins. A present malformed timestamp has already normalized to error and never enters the comparator. Accepted replacements allocate a new `contentRevision` while preserving same-owner-role counters and confirmed feedback. A post-call candidate first requires its captured `postCallGeneration` to equal the installed token. An older-generation candidate is stale before freshness comparison; a current-generation candidate replaces retained content from an older generation regardless of their relative timestamps, while candidates already competing within the installed generation use the ordinary timestamp/owner-role-arrival rule. A current post-call ready acceptance allocates a new revision, preserves the owner-generation counters, increments viewed once for the successful reveal, and resets the superseded content's local feedback and `feedbackStatus`. A matching post-call unsupported candidate may establish `unavailable` only when no accepted content is retained; it never replaces an existing draft or increments its revision. The receiving-agent listener additionally captures the full owner key and receiver role when it is bound. A matching `{kind: 'error', interactionId}` from that event may record mid-call `unavailable` and paint `generic-error` only when the result ID equals the captured interaction, that exact owner key is still current, and the receiver view has no accepted visible content; it never consults `pendingAISummaryRequests` or `latestAISummaryRequestByOwnerRole`. A stale/rebound owner or non-current interaction drops the error, and a timestamp-less error never replaces accepted content. A matching receiver `{kind: 'unsupported', interactionId}` with an optional normalized `timestamp` uses the ordinary timestamp/owner-role-arrival freshness rule and, when accepted with no retained receiver content, records `unavailable` plus the `The summary is not available` surface. Thus malformed and unrenderable receiver events are both attributable without allowing either to mutate the initiating view or whichever interaction happens to be visible. +6. A Promise rejection maps to one internal category. Unauthorized/initialization sets `eligible: false` and `summarySurface: 'omitted'`; the category is retained only for internal cleanup/test assertions and never projected. For an initial mid-call request or a post-call request with no retained accepted content, disabled/offline/unavailable/empty/timeout/generic becomes lifecycle `generation-error` plus `summarySurface: 'generic-error'`; only a freshness-accepted normalized `{kind: 'unsupported'}` valid-envelope result with no retained accepted content becomes lifecycle `unsupported` plus `summarySurface: 'unavailable'`, and no `AISummaryErrorCategory` maps to that lifecycle. A failed conference-participant preparation or post-call reason-regeneration request with already accepted content clears only its pending record and keeps that content, edits, revision, counters, and feedback ready with `summarySurface: 'content'`; no unavailable transition or visible error replaces a still-available summary. Raw errors and payloads are discarded after categorization. Only when a current SDK generation or response failure is accepted, `StoreWrapper` invokes `store.logger.trace('CC-Widgets: AI summary failure: ', {module: 'storeEventsWrapper.ts', method: 'observeAISummaryFailure'})` exactly once, substituting only the closed category token, where `category` is restricted to the already-defined `AISummaryErrorCategory | 'response-failed'` literals. The trace carries no summary/customer text, raw error, payload, interaction ID, agent ID, timestamp, duration, or dynamic context, and it never routes through `withMetrics`/`metricsLogger`; blocked, stale, discarded, and Clipboard failures emit no trace. +7. A lower-timestamp settlement, SDK-timeout late event, stale owner settlement, failed action, and caught unmount settlement are terminally consumed into the applicable closed result; none of the four Promise-returning store actions rethrows into the event loop. + +**Transfers, conferences, and retention.** For A-to-B-to-C, all state uses the common canonical interaction ID. Confirmed transfer/handoff completion advances the outgoing local phase before a late pre-handoff settlement can commit. Because C is a different authenticated agent, that cross-agent boundary immediately makes B's owner key non-current, removes B's summary from the visible projection, and creates C's empty receiver view with zero counters; B content is never shown while C waits, and no later B settlement can repopulate C's view. Direct transfer and consult-to-transfer report the same `handoff-complete` boundary. Adding a conference participant uses the existing initiating-agent `requestMidCallSummary('CONSULT')` during consult preparation; it does not introduce a conference-only SDK call. That Promise result is the initiator's per-agent summary. Before `consultConference()`, the initiating agent sends the edited response through the existing mid-call response method; the SDK/backend then delivers the D0-declared receiving-agent content event independently to each other participating agent. Each local client first requires the event's `conversationId` to match its stable canonical key, then keys accepted content to its own authenticated `store.agentId`; after establishment there is no shared receiving-agent slot. Conference establishment advances to a successor epoch. For the same agent, the successor is seeded with the prior visible summary/counters/feedback, so the old summary and counters remain visible while the replacement request is pending and after a failed preparation; only valid newer per-agent content that wins the timestamp comparison replaces and discards the carried-forward card. When a later topology change begins another full-history cycle, that cycle advances once more and carries the same-agent counters forward. A newly receiving agent starts a new owner generation and counters at zero. Existing `TASK_PARTICIPANT_JOINED`, `TASK_CONFERENCE_STARTED`, `TASK_PARTICIPANT_LEFT`, and `TASK_CONFERENCE_ENDED` handling performs only this idempotent epoch bookkeeping in addition to its legacy refresh behavior: it does not fan out `requestMidCallSummary`, clear the carried-forward view, or cancel pending generation, and duplicate task hydration cannot duplicate either a request or an epoch. The SDK/backend remains authority for whole-call history, continued generation as participants join/leave, conference-to-two-party conversion, and oldest-remaining-host selection. Concurrent Promise results and content events first require exact canonical ID equality, then correlate on local agent ID and owner generation; only candidates in the current generation compete by timestamp/arrival sequence. + +Mid-call content/counters survive hold, resume, same-agent remount, and task end; `TASK_WRAPPEDUP`, interaction terminal cleanup, sign-out, or SDK session cleanup removes them. When the visible current interaction changes to a different canonical ID, the old interaction's mid-call state, successfully submitted post-call state, failed frozen response, and Store-owned wrapped-up subscription entry are cleared immediately and can never project into the new view. While that same interaction remains current, a failed frozen post-call record and its exact owner-bound subscription survive ordinary Task replacement/removal until the matching backend `AgentWrappedUp`/`TASK_WRAPPEDUP` event. While that owner is still the current visible post-call view, the existing summary may remain read-only and `feedbackStatus: 'not-confirmed'` resolves to `Submission not confirmed` beneath its disabled feedback group; "retained tombstone" never means that content is editable or resendable. + +`handleTaskRemove` is not the failed-response cleanup boundary while the interaction remains current: even if it runs first and the Task has disappeared from `taskList`, the Store/session registry retains the captured Task/callback binding, captured `AISummaryOwnerKey`, and exact pending/frozen failure state. A replacement Task is attached to the same idempotent logical callback before ordinary predecessor teardown, and all required bindings remain owned by the registry until the first matching event or interaction cleanup. Every newly created post-call owner state initializes its own `agentWrappedUpObserved: false`. A matching registry callback compare-checks canonical interaction ID, authenticated agent ID, and ownership generation before setting only that record's flag. If `TASK_WRAPPEDUP` for interaction X arrives while X's one response Promise is pending, only X's owner record is marked and retained until settlement; Y's flag/tombstone is untouched. On X response failure, the store emits X's content-free failure transition and retains X's snapshot only when X's event has not arrived. If X's event was already observed, it discards X and performs only X's per-interaction sweep immediately after the failure transition; if failure settled first, X's later event performs that cleanup. The callback does this state work before its legacy `refreshTaskList` call and then detaches every Task binding for that owner; duplicate delivery is a no-op. If an interaction change wins the race first, the per-interaction sweep clears the tombstone and detaches the same bindings before any late event can recreate state. A successful submitted response ignores its owner-local observation flag and retains only its documented state until interaction change. `cleanUpStore` on sign-out/session termination remains the global privacy boundary: it detaches every retained callback and clears every summary/capture map without pretending that Task removal or another interaction's event was the matching `AgentWrappedUp`. + +**Response composition and failures.** `composeMidCallResponse` accepts only `AISummaryMidCallOwnerState` and an explicit feedback value. For initiating `{type: 'sections'}` content it never receives or reparses a flattened label string: it iterates the already validated `state.content.sections` entries in their normalized SDK payload order, defensively allowlists each key against the three mid-call keys, and emits each retained key with its current exact edited value in that same order in the SDK section object. It does not import or iterate presentation constants. Labels are presentation metadata and cannot create, rename, reorder, or delete a response key; if an agent clears an admitted value, that retained key is emitted with `''` rather than inferred from label text. For `{type: 'text'}`, it emits the current exact `summaryText` string. Ready content otherwise uses `summaryReceived: true`, numeric counters, the explicit feedback, and valid state; unavailable generation uses `summaryReceived: false`, `summary: ''`, zero counters, `feedback: 'none'`, and `state: 'NOT_RECEIVED'`. + +For an initiator, the authoritative `actionType` is the D0-correlation-resolved value captured by the pending request whose freshness-accepted settlement owns the current content; request start and settlement both require that correlation, and the resolved value is committed atomically with content rather than inferred from a label, popover name, or conference shorthand. Participant-add preparation therefore starts as `CONSULT`. Before any later feedback update or pre-action response, the store re-evaluates the same Task relationship/action correlation: a confirmed consult-to-transfer promotion updates the current same-owner state to `TRANSFER` before composition, while an indeterminate correlation blocks the SDK call, returns `{outcome: 'blocked'}`, and does not paint feedback or new content. A stale consult settlement cannot overwrite a promoted transfer owner. Receiver state follows the same fail-closed rule and is created only after that Task correlation resolves to `CONSULT` or `TRANSFER`; an unknown correlation cannot create a ready mid-call owner/view. Thus neither role uses a non-null assertion, invented default, or permanent `CONSULT` alias for a promoted transfer. `composePostCallResponse` similarly iterates only the normalized editable section array in SDK payload order and emits the exact `PostCallSummarySections` keys with their current values, or emits the plain string; the separately stored presentation-only `resolution` value is structurally unavailable to that loop and is never serialized into `summary`. Closing a popover invalidates its view token but sends no cancellation response and never uses `MID_CALL_CANCELLED`. + +`setMidCallSummaryFeedback(role, feedback, actionType, expectedRevision)` first resolves the exact current `AISummaryMidCallOwnerState` and compares the caller's rendered action type and revision with that state. A mismatch fulfills `{outcome: 'stale'}` before pending state, payload composition, or an SDK call; this covers a queued click from a prior owner/content revision and a rendered CONSULT control that settles after promotion to TRANSFER. Only after those guards pass does the store re-evaluate the initiator/receiver Task correlation, require its resolved action type to equal the explicit/current value, capture the Task/owner/revision, compose the payload with the newly selected feedback value, and invoke exactly `capturedTask.sendMidCallSummaryResponse(payload, resolvedActionType)`. Each accepted selection, opposite selection, or re-selection makes a new call; calls are never coalesced. `midCallFeedbackPending` is true only while that Promise is in flight and disables the mid-call feedback controls without showing post-call pending copy. The declared `Promise` exposes fulfillment or rejection, not an HTTP status code, so fulfillment is the admissible proxy for REQ-004's 200 confirmation. Only after that Promise fulfills may the selected value commit, provided the captured owner, revision, and action type are still current, and the store then fulfills `{outcome: 'confirmed'}`; rejection/timeout fulfills `{outcome: 'failed'}`, an advanced owner/revision/action type fulfills `{outcome: 'stale'}`, and an ineligible or indeterminate correlation fulfills `{outcome: 'blocked'}`. Every non-confirmed result preserves the prior highlight and clears only that call's in-flight flag. Later, every consult/transfer/conference telephony attempt invokes a separate `capturedTask.sendMidCallSummaryResponse(composeMidCallResponse(capturedState, capturedState.feedback), resolvedActionType)` exactly once through `sendMidCallSummaryBeforeAction`, immediately before invoking the existing telephony API. Earlier feedback-update calls neither satisfy nor suppress this one pre-action submission; the pre-action call uses the latest confirmed feedback and is invoked after every feedback call already completed before the action click. No defaulted or optional action type is used on either path. + +`sendMidCallSummaryBeforeAction` captures the current Task, canonical interaction, agent, ownership generation, correlation-validated action type, and response payload before calling the store-owned SDK method. It catches a synchronous throw or Promise rejection, including an SDK timeout rejection, and always fulfills `AISummaryPreActionSendResult`: `{outcome: 'sent'}` on current success, `{outcome: 'failed'}` on throw/rejection/timeout, `{outcome: 'blocked'}` when the action correlation is indeterminate, or `{outcome: 'stale'}` when ownership or interaction changed. It never exposes a rejection to `useCallControl`. A non-sent result discards response-derived mutation and does not suppress the later telephony continuation. The task-side orchestrator and every D5/D6b internal confirmation call site `await` this normalized Promise; only after its `sent`, `failed`, `blocked`, or `stale` result settles does the orchestrator invoke the captured existing consult/transfer/conference action exactly once through its existing guard. The independently observed telephony fulfillment reports `handoff-complete` for direct/consult transfer or `conference-established` for establishment, using the pre-action owner key as the compare-and-swap expectation; a rejection reports no boundary, and the Task event route may report the same confirmed boundary without double advancement. A consult that has not transferred or established a conference does not advance ownership. Settlement order—not mere invocation order—satisfies response-before-action: while the response remains pending, no telephony API has been called; a blocked, rejected, timed-out, or stale result delays telephony only until normalization settles and never aborts the one subsequent action. The outer orchestration Promise and later telephony Promise are both observed, and no automatic response retry is added. + +`capturePostCallDraft` succeeds only for the current eligible ready post-call role view/revision, a non-empty selected wrap-up code identifier, and an exact current Task. The caller passes the exact `auxCodeId` from `useCallControl.wrapupCall(wrapUpReason, auxCodeId)` as the `wrapUpCode` argument; the human-readable `wrapUpReason` is not serialized into the summary response. The method atomically installs `postCallCaptureRevision: expectedRevision`, deep-copies normalized content/counters/local feedback/`wrapUpCode`, retains the Store's exact Task reference with that private snapshot, and returns only a fresh opaque token. `controlsDisabled` becomes true and every mutator, including `setPostCallSummaryFeedback`, rejects that role view/revision until the token is released or frozen; the lifecycle remains ready so a failed wrap-up can restore the unchanged draft. The task-side orchestrator retains the distinct `(wrapUpReason, auxCodeId)` pair solely for the existing wrap-up call and failed-wrap-up UI restoration; it never receives summary content or supplies a Task to the summary-send API. + +`releasePostCallDraft(token)` looks up the internal capture by symbol identity, compare-clears only its matching current owner/revision guard after wrap-up failure, deletes that record, and returns `false` for an unknown, stale, released, or reused token. After wrap-up succeeds, `freezeAndSendPostCallSummary(token)` resolves the same internal record, passes its module-private `PostCallDraftSnapshot` to `composePostCallResponse`, emits the captured sections or string, exact numeric counters/feedback, `state: 'DEFAULT'`, and `wrapUpCode` unchanged, deep-freezes the payload, and calls only that record's captured pre-wrap-up Task once. This mapping is grounded in D0's reachable public declarations: `WrapupPayLoad.auxCodeId` is the auxiliary-code identifier used by `task.wrapup`, while `AISummaryResponse.wrapUpCode` is the required response field; D0's unequal-label/code fixture proves the response takes the committed identifier. Visibility changes do not redirect or invalidate a valid committed token. A transport rejection sets `response-failed`, retains that keyed frozen copy, and resolves with the sanitized result so no response-only retry or unhandled rejection occurs; an invalid token makes no SDK call and cannot select a different capture/Task. Not applicable - the widget adds no response cancellation, backoff, persistence migration, or telemetry. + +**Tests.** D1 supplies raw feature, typed/plain/card, malformed, unsupported, every normalized failure, transfer, conference, feedback, counter, timestamp, and race fixtures without requiring the SDK; its wrap-up fixture deliberately pairs human-readable `wrapUpReason: 'Customer issue resolved'` with identifier `auxCodeId: 'RESOLVED'`. D2 covers the pure cases `stable mainInteractionId only`, `stable mainCall media ID only`, `equal stable candidates`, `conflicting stable candidates fail closed`, `leg IDs never become canonical`, `summary conversationId mismatch returns discarded`, `strict unknown-input normalization`, `role precedence`, `mid-call role requires actionType`, `post-call role forbids actionType`, `malformed capability fail closed`, `same-agent successor carries counters`, `different-agent successor resets counters`, `greatest timestamp and equal-last-arrival`, `counter guards`, `both mid-call roles coexist for the same owner and role-discriminated edit/view/copy changes only the selected view's revision and counters`, `read-only resolution separation`, `surface maps authorization and initialization to omitted`, `surface maps unsupported only to unavailable`, `other generation failures map to generic-error`, `unsupported normalization is the sole unsupported lifecycle entry`, `sanitized error mapping never returns unsupported`, and `normalized generation or response failure emits one content-free trace category with no summary, interaction, agent, or raw-error sentinel while blocked, stale, discarded, and Clipboard paths do not log`. After D0, D2b covers `packed declarations accept all conforming fixtures and adapter outputs`, `pre-listener capability is seeded from the exact Task snapshot`, `Task listener rebind removes the prior exact pair`, `cleanUpStore removes the exact Task callback`, `canonical shared voice predicate accepts telephony and rejects non-voice`, `runtime canonical main interaction matches capability and conversation`, `non-voice never eligible`, `missing capability false`, `capability interaction mismatch is unavailable without re-key`, `initiating/post/receiver conversation mismatch mutates nothing`, `same owner hold/remount retention`, `successful consult-to-transfer advances generation`, `late pre-handoff settlement is stale`, `conference establishment advances exactly once`, `same-agent conference successor retains each role view's content/counters until replacement`, `new-agent successor starts zero counters`, `terminal clear then same-agent re-entry advances generation`, `task clone/end alone retains generation and wrapped-up cleanup`, `task-removal-before-AgentWrappedUp retains the Store/session-owned subscription and frozen failure, then an event emitted from the removed Task clears exactly that owner and detaches every retained binding`, `AgentWrappedUp performs cleanup before refresh and detaches every owner binding`, `throwing AgentWrappedUp summary transition is consumed and still calls refreshTaskList before all-binding detachment`, `cleanUpStore detaches every outstanding wrapped-up binding`, `AgentWrappedUp before response rejection causes immediate post-transition discard`, `interaction switch clears the failure and retained bindings`, `sign-out/session cleanup`, `A-B-C generation isolation`, `prior agent is never projected while successor waits`, `conference preparation failure preserves ready content and counters`, `conference task events do not fan out SDK requests`, `participant leave does not cancel generation`, `repeated explicit requests retain generation and each call SDK`, `concurrent post-call requests project one generating state and requestPending clears only after the final settlement`, `Retry is disabled synchronously while post-call requestPending is true`, `older rejection cannot replace newer success`, `older success in the same phase still competes by timestamp`, `15-second rejection ignores late event`, `unmount and owner switch consume settlement`, `role-targeted view/edit/copy success-only counters`, `each mid feedback selection calls the exact SDK method and action type`, `mid feedback paints the selected state only after sendMidCallSummaryResponse fulfills and rejection leaves the prior selection unchanged`, `mid feedback uses only midCallFeedbackPending`, `pre-action send remains a separate once-per-action call`, `post feedback remains local under postCallFeedbackPending`, `captured wrapUpCode equals the committed auxCodeId and differs from the display wrapUpReason`, `frozen response exactness`, `response attempted once`, and `zero unhandled rejection`. + +The D2b suite additionally names `all four Promise store actions fulfill sanitized discriminated results on synchronous throw, rejection, timeout, block, and staleness`, `initiator actionType is owned by the accepted request and promoted from CONSULT to TRANSFER by verified Task correlation`, `indeterminate initiator correlation blocks and does not paint`, and `post-call feedback stale revision, advanced owner, frozen capture, and ineligible surface return false without mutation`. Store transition tests prove that the status sequence increments once per accepted semantic transition and never for stale or presentation-only changes; two transitions appended inside one `runInAction` remain as two ordered queue entries; exact-head acknowledgement removes one entry without skipping its successor; ordinary keyed cleanup retains undelivered entries; global session cleanup discards the queue but does not reset the sequence; and an acknowledged entry cannot reappear. + +D2b concurrency coverage also names `new reason generation supersedes every older request before timestamp comparison`, `superseded higher-timestamp and later-arriving responses are stale no-ops`, `current-generation retries retain greatest-timestamp and equal-last-arrival ordering`, and `superseded pending requests do not hold current requestPending`. The replacement-state case begins with an edited, copied, locally rated post-call draft and proves that reason commit itself preserves its exact bytes, revision, counters, selected feedback, and `pending` description; only a current-generation ready acceptance replaces the draft, allocates a fresh content revision, preserves the accumulated edited/copied counters, increments viewed exactly once for the successful reveal, and resets feedback to `none` with no status description. Every superseded/blocked/failed/unsupported settlement increments no counter and changes no feedback or status transition. + +The role-contract and feedback behaviors have explicit named coverage rather than relying on generic fixture assertions. D2 adds `initiator accepts typed sections and plain text but card-only success normalizes to unsupported`, `receiver accepts an adaptive card but typed/plain-only success normalizes to unsupported`, and `post-call accepts typed sections and plain text but card-only success normalizes to unsupported`; each asserts `summarySurface: 'unavailable'` and exact `The summary is not available` presentation mapping rather than `generation-error`. D2b adds `mid-call like and dislike are mutually exclusive for initiator and receiver`, `successful opposite mid-call feedback replaces the highlight`, and `re-selecting highlighted mid-call feedback sends another exact SDK update and leaves it highlighted`. It also adds `task removal before AgentWrappedUp cannot strand the matching failed tombstone`, `AgentWrappedUp for interaction X does not discard interaction Y retained failed tombstone`, `stale owner-generation AgentWrappedUp does not mark its successor`, `opaque capture token binds the internal snapshot and Task`, and `unknown released or reused capture token cannot send or mutate another capture`. + +`setMidCallSummaryFeedback` is an `@webex/cc-store` action name, not an SDK method. For receiver feedback, `@webex/cc-store` is the sole caller: after the owner/action/revision guards pass, it composes a typed `AISummaryResponse` from the current receiver state and invokes exactly `capturedTask.sendMidCallSummaryResponse(payload, resolvedActionType)`. Neither the AI Assistant observer, `useAiAssistant`, `AIAssistantComponent`, nor `AISummary` receives a Task or calls any SDK method. + +## Component: Shared Summary Presentation + +**Coverage and outcome.** This component covers REQ-002, REQ-003, REQ-004, REQ-005, REQ-008, REQ-009, REQ-010, UX-002 through UX-010, AC-03, AC-04, AC-05, AC-11, AC-12, AC-13, AC-14, and AC-16. DAG task: `D3-shared-summary-presentation`; evidence prerequisite: `D0a-ux-evidence-admission`. It gives both host containers one SDK-free visual/action contract, owns the shared generating/editing/feedback/copy/error/unavailable presentation states, leaves omission to the containers, and keeps receiving adaptive cards inside their established security boundary. No screenshot-derived literal, hierarchy, or styling is authored until D0a verifies the source files in the target worktree. + +**Files and responsibilities.** New `AISummary/ai-summary.types.ts` declares only presentation props/types; `ai-summary.constants.ts` holds stable en-US message IDs and exact strings; `ai-summary.tsx` renders loading, ready, error, unsupported, and the single shared action row; `ai-summary.styles.scss` uses existing semantic tokens; `index.ts` and the package `src/index.ts` export the component/types. `@webex/cc-store` is the single source of truth for every normalized summary model consumed by D3, including `AISummaryContent`, `AISummaryEditableField`, `AISummaryDisplaySectionKey`, `AISummaryFeedback`, `AISummaryFeedbackStatus`, `AISummaryFeedbackResult`, and `AISummaryRequestResult`; D3 must not redeclare structurally equivalent section, field, feedback, or result contracts in cc-components. `ai-summary.types.ts` and `ai-summary.constants.ts` use `import type` for those symbols, and generated JavaScript must contain no D3-introduced runtime import of `@webex/cc-store` or `@webex/contact-center`. `packages/contact-center/cc-components/package.json` is therefore in D3's file/acceptance boundary: it must retain the inspected `dependencies["@webex/cc-store"] = "workspace:*"` declaration (or add that exact declaration if absent) so emitted declarations have a legal package edge, with no second alias, local path, peer relocation, or direct SDK dependency. The existing dependency means D3 introduces no new architectural direction; it makes the already-authorized `cc-components -> cc-store` edge explicit and type-only for this feature. These files cannot live in CallControl because AI Assistant also consumes them, and the controls cannot live in `AdaptiveCardRenderer` because labels, feedback state, Clipboard settlement, and focus recovery are summary responsibilities rather than card-rendering responsibilities. D3's receiver mode is an action surface only: it accepts a synchronous visible-text getter, never receives card JSON, and neither imports nor renders `AdaptiveCardRenderer`. D4 later composes that completed action surface as a sibling of the renderer inside `AIAssistantComponent`; D4 also owns the focused `extractCardText` integration test and the internal pre-parse image-resource hardening. RealTimeAssist and the renderer's public prop/action contract remain unchanged. + +**Presentation contracts and signatures.** The public package types are normalized and contain no SDK Task or raw error. `AISummaryFeedback` and the other normalized model types below are imported from `@webex/cc-store`; `ai-summary.types.ts` does not redeclare their string unions: + +```typescript +import type { + AISummaryContent, + AISummaryEditableField, + AISummaryFeedback, + AISummaryFeedbackResult, + AISummaryFeedbackStatus, + AISummaryRequestResult, +} from '@webex/cc-store'; + +export function AISummary(props: AISummaryProps): React.ReactElement; + +export type AISummaryPresentationState = 'loading' | 'ready' | 'generation-error' | 'unsupported'; +export type AISummaryCopyVisualState = 'idle' | 'hover' | 'confirmed'; +export type AISummaryMode = 'mid-call-initiator' | 'mid-call-receiver' | 'post-call'; +type EditableSummaryContent = Extract< + AISummaryContent, + {type: 'sections' | 'text'} +>; +interface AISummaryPropsBase { + state: AISummaryPresentationState; + feedback: AISummaryFeedback; + controlsDisabled: boolean; + onView: () => void; + onCopy: () => boolean; + containingPanelFocusTarget: React.RefObject; +} +type AISummaryReceiverPropsBase = Omit & { + mode: 'mid-call-receiver'; + content?: never; + onEdit?: never; + onCopyVisualStateChange?: never; + feedbackStatus?: never; + onFeedback: ( + feedback: Exclude + ) => Promise; + onRetry?: never; +}; +type AISummaryReceiverProps = AISummaryReceiverPropsBase & + ( + | { + state: 'ready'; + contentRevision: number; + getReceiverCopyText: () => string; + } + | { + state: Exclude; + contentRevision?: never; + getReceiverCopyText?: never; + } + ); +export type AISummaryProps = + | (AISummaryPropsBase & { + mode: 'mid-call-initiator'; + content?: EditableSummaryContent; + onEdit: (field: AISummaryEditableField, value: string) => boolean; + onCopyVisualStateChange?: never; + feedbackStatus?: never; + onFeedback: ( + feedback: Exclude + ) => Promise; + onRetry?: never; + }) + | AISummaryReceiverProps + | (AISummaryPropsBase & { + mode: 'post-call'; + content?: EditableSummaryContent; + onEdit: (field: AISummaryEditableField, value: string) => boolean; + onCopyVisualStateChange: (state: AISummaryCopyVisualState) => void; + feedbackStatus?: AISummaryFeedbackStatus; + onFeedback: (feedback: Exclude) => boolean; + onRetry?: () => Promise; + }); +``` + +`AISummary` is the single exported summary presentation and control component for typed/plain content and the shared receiver action surface. All four `AISummaryPresentationState` values render visible output, so the component always returns `React.ReactElement`; omission is solely the container's responsibility, before invoking `AISummary`. Loading/error/content/action rows remain private render functions in `ai-summary.tsx`; they are not additional public abstractions. Containers derive presentation solely from `AISummaryViewModel.summarySurface`: `omitted` renders neither this component nor the receiver trigger, `generating` maps to `loading`, `content` maps to `ready`, `unavailable` maps to `unsupported`, and `generic-error` maps to `generation-error`. They do not infer these branches from `eligible`, `lifecycle`, missing content, or the internal error category. D4 passes only `mode: 'mid-call-receiver'`, D5 passes only `mode: 'mid-call-initiator'`, and D6 passes only `mode: 'post-call'`. `AISummary` selects every private render/action branch from that one discriminator, so no caller or renderer must synchronize or choose between a `kind` and a `role`; receiver mode forbids card content and editing, and post-call-only status/Retry props cannot be supplied to either mid-call branch. The package exports no generic `AISummaryAction` union: the dedicated callbacks in `AISummaryProps` type each action and the exact labels live once in `ai-summary.constants.ts`. + +Receiver mode never accepts card JSON and never imports or renders `AdaptiveCardRenderer`. For ready content, it renders only the shared exact-label action row and synchronously calls `getReceiverCopyText()` during the direct copy gesture; `contentRevision` is the sole receiver source-change token used to reset copy confirmation. D4 owns the validated display-only card projection and passes a getter backed by `extractCardText` over the sibling receiver branch in `AIAssistantComponent`. Embedded card actions are excluded from that projection, so known and unknown card actions are inert and cannot duplicate the shared row. `AdaptiveCardRendererProps`, its restricted DOM/security boundary, real-time-assist labels, toggle behavior, legacy copy helper, `onUserAction`, and `onAction` routing are unchanged. It gains no summary-mode discriminator, label map, selected-feedback prop, Clipboard callback, feedback responsibility, child slot, or feature-control descendant. + +**Rendering and edit mapping.** `ai-summary.constants.ts` exports closed `MID_CALL_SECTION_DEFINITIONS` and `POST_CALL_SECTION_DEFINITIONS` tuples only as key-to-message metadata registries; tuple position is not a display, clipboard, or response-order authority. It separately exports the D2-type-checked display ordinal `POST_CALL_DISPLAY_SECTION_ORDER = ['initialContactReason', 'additionalContactReasons', 'additionalContext', 'keyActionsTaken', 'resolution', 'nextSteps'] as const satisfies readonly AISummaryDisplaySectionKey[]`. A compile-time/runtime invariant requires every post-call display key exactly once. `AISummary` starts with the normalized editable `content.sections` array in the exact SDK payload order established by D2. It creates an `Outcome` row if and only if `content.resolution` is a normalized non-empty string. For that present case, it finds the `resolution` ordinal and inserts one read-only display row immediately before the first admitted section whose key has a greater ordinal; if no admitted key has a greater ordinal, it appends the row. This is a stable insertion: no admitted SDK section is sorted or moved, and missing, renamed, or reordered neighbours cannot make the position undefined. If the source key is missing, `undefined`, or normalizes from an empty string to absence, `AISummary` emits no `Outcome` row, placeholder, empty label, divider, or reserved layout space. The response composers iterate the original editable array without importing presentation constants. The mid-call-only entry is `reasonForTransferOrConsult` (`aiSummary.midCall.section.reasonForTransferOrConsult`, `Reason for transfer or consult`). The keys shared across roles reuse both the screenshot-derived post-call wording and role-neutral message IDs: `additionalContext` is (`aiSummary.section.additionalContext`, `Additional context`) and `keyActionsTaken` is (`aiSummary.section.keyActionsTaken`, `Key Actions Taken`) in both tuples. The remaining post-call-only entries are `initialContactReason` (`aiSummary.postCall.section.initialContactReason`, `Initial contact reason`), `additionalContactReasons` (`aiSummary.postCall.section.additionalContactReasons`, `Additional contact reason(s)`), and `nextSteps` (`aiSummary.postCall.section.nextSteps`, `Next Steps`); optional top-level `resolution` uses (`aiSummary.postCall.section.resolution`, `Outcome`) as display/copy-only metadata. This shared-label choice is the explicit REQ-011 extrapolation: UX-001 does not show structured-detail labels, so the common mid-call keys reuse the corresponding strings visibly present in UX-004 through UX-009 instead of humanizing SDK keys into a second visual language. Role-qualified IDs remain only where wording/role is genuinely distinct. The five editable post-call keys and three editable mid-call keys are the complete declaration keys admitted by D0; `resolution` is a declared payload field but not a response-section key. Initial normalization omits missing/`undefined`/empty-string source values without reordering the remaining fields, and drops unknown keys at the store boundary before presentation or serialization. Once a non-empty source value admits a keyed editable section, clearing it is an intentional edit: the section/key stays in state and response composition emits its current empty string. + +Initiator sections retain `{key, value}` entries in store state and are collapsed only for presentation: the bottom-most bordered text-box surface contains labels in normalized SDK order and one controlled editable value per retained SDK key. The surface has no aggregate editable string and no label-delimited parser. Editing a value calls `onEdit(section.key, nextValue)` directly, so removing, duplicating, or reordering visible label text is impossible and cannot affect payload shape; `contentRevision` records the accepted keyed edit but is not a reconstruction mechanism. Post-call rendering creates an `AISummaryDisplaySection[]` through the stable ordinal insertion above, so every present SDK label/value keeps its relative normalized order and a present `Outcome` row has a defined position even when the sections on either side of its canonical ordinal are absent. An absent resolution produces the same flow as if no display row had ever been projected: later sections close the gap and neither rendering nor clipboard export includes `Outcome`. Labels are resolved in `@webex/cc-components` by key from the closed metadata tuples rather than stored in the SDK-facing store or obtained by iterating tuple position. The `Outcome` row is rendered as text and cannot emit `onEdit`. Plain content is a single controlled unlabeled text area for post-call and the same bottom editor for initiator, and its only edit field is `summaryText`. For clipboard export only, structured content is flattened to `Label: value` blocks separated by exactly two newline characters in projected display order, including `Outcome` only when resolution is present; that copy string is never fed to either edit state or a response composer. Plain content is copied byte-for-byte from the current edited string; receiver content uses the newline-ordered visible text returned synchronously by `getReceiverCopyText`, whose D4 implementation delegates to `extractCardText` over the sibling card branch. Empty output blocks the copy. Values are React text/textarea values, never `dangerouslySetInnerHTML`, and the dynamic content root uses `dir="auto"`. `undefined` typed/plain content is legal only in non-ready states; a ready initiator/post-call branch without compatible content renders unsupported copy, while ready receiver props require both `contentRevision` and the text getter. + +Loading uses message ID `aiSummary.generating` (`Generating summary...`) plus `aiSummary.generatingDescription` (`Just a sec—the details are coming together.`), with one U+2014 em dash and no surrounding spaces. The UX-002 text/compact JSON and the higher-precedence S02 pixels were both inspected: each visibly contains U+005F at that separator position. That agreement records the source artifact rather than making low-line valid en-US punctuation; the correctly preserved em dash in the same admitted evidence family and the sentence grammar resolve the production character to U+2014. D8 preserves the sealed S02 PNG and excludes only the fixed separator correction region from pixel scoring while DOM/accessibility assertions require the full corrected string and reject U+005F. + +UX-010's text JSON and PNG both show the exact visible title `Having trouble generating summary` and subcopy `It could be a lost connection or something else. Could you check your connection or try again later?`. The title already equals REQ-009's mandated string, so the resolved production binding has no precedence conflict: `aiSummary.generationError` is exactly `Having trouble generating summary`, and `aiSummary.generationErrorDescription` is exactly that observed subcopy. Every non-hidden normalized error category (`disabled`, `offline`, `unavailable`, `empty`, `timeout`, and `generic`) uses this one title/subcopy pair; `unauthorized` and `initialization` remain omitted, while a valid but unrenderable payload remains the distinct unsupported presentation. Applying the source subcopy to all six visible categories is intentional: `or something else` avoids asserting a network cause, and `or try again later` supplies a category-neutral recovery action, so the sentence is guidance rather than a diagnosis even for disabled, empty-payload, unavailable, timeout, and generic failures. Unsupported uses `aiSummary.unavailable` (`The summary is not available`). + +`AI_SUMMARY_FEEDBACK_STATUS_MESSAGES` is a closed `Record`: `pending` maps to `aiSummary.feedback.pendingSubmission` (`Pending submission`) and `not-confirmed` maps to `aiSummary.feedback.submissionNotConfirmed` (`Submission not confirmed`). These are REQ-008-authored, screenshot-absent structural-only descriptions, not strings inferred from a visual source. No public prop or store projection carries either en-US string. Exact action labels and identical tooltip strings are `View summary`, `This is helpful`, `This isn't helpful`, `Copy Summary`, and post-call `Retry`. + +Every complete sealed Figma token with a matched outer `%...%` pair is uniformly an unresolved variable slot, never literal user copy; this rule covers static-looking search/completion tokens and dynamic agent/user/queue/number tokens alike. The raw per-capture divergence (`Search topic` versus `%Search topic%`, and `Complete wrap-up` versus `%Complete wrap-up%`) is a source-capture artifact, not UI state. Evidence binding removes the matched pair and applies requirement capitalization before any constant, fixture, DOM assertion, or pixel comparison reaches production. Accordingly, `ai-summary.constants.ts` records only the canonical en-US values `aiSummary.midCall.searchPlaceholder` (`Search by name, queue, entry point or phone number`), `aiSummary.midCall.consultHeading` (`Here’s a consult summary—they’ll get a copy`), `aiSummary.postCall.searchPlaceholder` (`Search topic`), and the requirement-controlled `aiSummary.postCall.completeAction` (`Complete Wrap-Up`); the last value is identical for visible text, accessible name, and tooltip. These are the sole constants for their controls across generating, editing, error, default, hover, selected, and copied states; production performs no state-dependent marker insertion or label mutation. Evidence slots for agent name, user type, queue, and number resolve to deterministic D8 fixture values before render and never expose their marker text. + +`ai-summary.constants.ts` also reserves the distinct message ID `aiSummary.midCall.transferHeading` for the provisional structural-only candidate `Here’s a transfer summary—they’ll get a copy`. UX-001 contains only the Consult sentence, so neither the candidate nor its wording is extracted, parameterized, or extrapolated from UX-001. The AI Assistant product/content owner must confirm or replace that exact en-US value before D8 can pass; until then it is unapproved copy. D8's named `mid-call:transfer-summary-panel` structural classification and `Transfer summary panel exposes the product-confirmed structural heading` DOM/copy scenario must record the message ID, final exact string, confirmation reference, and action-parity rationale, with no `extrapolatedFrom` field or source-authority claim. + +**Action and focus control flow.** `AISummary` is the sole clipboard-write owner for typed, plain, and receiver adaptive-card content. Copy is invoked only from a button click/key activation; that non-`async` React handler synchronously derives the current edited text or calls the receiver-only `getReceiverCopyText` supplied by `AIAssistantComponent`, invokes `navigator.clipboard.writeText(text)` during the same direct gesture, and immediately attaches both fulfillment and rejection handlers at that call site before returning `void`, preserving transient user activation without returning a Promise to React. The getter only extracts visible text from the sibling restricted-card branch; it neither moves the Clipboard call into the container nor passes card JSON into `AISummary`. Only after fulfillment does `AISummary` invoke the content-free synchronous boolean `onCopy()` revision recorder exactly once; `true` normalizes to the internal `confirmed` outcome and paints success, while `false` normalizes to `stale` and does not paint. A synchronous getter/Clipboard throw, Promise rejection, empty output, or recorder throw normalizes to the content-free `failed` outcome and is never rethrown. Clipboard failure itself produces no visible-state transition, never invokes `onCopy`, and therefore cannot increment the copied counter; its raw value is neither stored, logged, passed through props, nor rendered. The container/store callback receives no summary text, never calls the Clipboard API, and cannot cause a second write; there is no `execCommand` or automatic copy fallback. + +`COPIED_FEEDBACK_MS` is the literal constant `1_500`. A fulfilled write plus accepted recorder paints `confirmed` for exactly 1,500 ms unless an earlier reset trigger occurs. The complete visible reset set is: timer expiry; `blur` when focus leaves the copy control; a new copy gesture; or accepted summary-content replacement. A second gesture synchronously clears the old timer and prior confirmation before issuing its write; a second accepted fulfillment starts a fresh full 1,500 ms interval, while its throw/rejection leaves that gesture-reset state unchanged and still records no copy. Content/source replacement clears the timer and reports `idle` before the replacement is painted. Unmount clears the timer without issuing a state update. Pointer entry/exit reports `hover`/`idle` through the required post-call-only `onCopyVisualStateChange` only while confirmation is not active; the two mid-call variants forbid that callback. The reported state is an input only to the copy control's icon/treatment: it is not forwarded to, read by, or allowed to select props or messages for the reason search or Complete Wrap-Up control. Confirmation paints the screenshot check treatment, but the copy button's accessible name and tooltip both remain exactly `Copy Summary`; the check icon supplies a non-color visual state without a feature announcement. Feedback buttons are native buttons with `aria-pressed`; selected state includes icon/label semantics as well as color. Mid-call selection is controlled and changes only after the always-fulfilled feedback Promise reports `{outcome: 'confirmed'}`. Post-call changes immediately to local pending selection only when the revision-guarded callback returns `true`, remains mutually exclusive, and re-selection calls the action again. When `feedbackStatus` exists, `AISummary` gives the ordinary visible description an instance-local `useId()`-derived `id` and puts that same `id` in `aria-describedby` on both the `This is helpful` and `This isn't helpful` buttons; this direct control association remains present when the completion group becomes read-only. When the status clears, the description and both references are removed together. The description has no `aria-live`, status, or alert semantics. + +`AISummary` owns one event-loop contract for copy, feedback, and Retry: each React handler returns `void`, catches a synchronous throw, and attaches an explicit rejection arm to every Promise before returning; the component reduces settlement to the closed internal outcomes `confirmed`, `failed`, `blocked`, or `stale` and never rethrows. Clipboard fulfillment plus an accepted boolean recorder is `confirmed`; Clipboard throw/rejection is `failed`; a rejected direct-consumer feedback Promise is also normalized to `failed`; and the store's always-fulfilled `AISummaryFeedbackResult` supplies the same four outcomes without raw errors. A failed/rejected mid-call feedback callback preserves the prior controlled selection, a failed post-call Boolean callback is treated as `false`, and a failed/rejected Retry leaves the current generation-error UI unchanged until new controlled props arrive. The store/container still owns SDK categorization and state projection, while the component owns safe observation even if a direct React consumer violates the always-fulfilled callback contract. No `Promise` or `Promise` is returned from a React click handler, so SDK and Clipboard rejection cannot become an unhandled rejection. + +For post-call, the container passes `AISummaryViewModel.feedbackStatus` through unchanged. `AISummary` resolves `pending` through `aiSummary.feedback.pendingSubmission`, rendering `Pending submission` directly beneath the enabled feedback controls before Complete Wrap-Up and associating it with both controls as specified above. After wrap-up commits and the response fails, the same location remains as a disabled/read-only completion group and resolves `not-confirmed` through `aiSummary.feedback.submissionNotConfirmed`, rendering and associating `Submission not confirmed` even though no frozen payload object or resend/edit action is exposed. Confirmed submission removes the token, node, and both `aria-describedby` references atomically. The component never derives either token or string from `lifecycle`, `postCallFeedbackPending`, missing content, or an SDK error. + +`AISummary` captures the focus snapshot while the DOM still exists: an `onFocusCapture` handler on the summary subtree stores the focused control and the first later enabled/tabbable element in document order in refs. A normal blur to another connected element clears or replaces that snapshot so later rendering cannot steal focus. After a prop/state commit, `useLayoutEffect` acts only when the recorded focused control is no longer `isConnected` and focus has not already moved to another connected control; it focuses the recorded successor when that element remains connected and focusable, otherwise it focuses `containingPanelFocusTarget.current`, then clears the snapshot. If the recorded control remains connected, the effect does nothing. In D4, the receiver action row is a sibling of the card renderer under `AIAssistantComponent` and is not keyed by `contentRevision`, so a valid card replacement updates only the renderer sibling and leaves the focused Copy/Like/Dislike node connected. A transition to unavailable, generic error, omission, or a different interaction removes those controls and therefore advances to the captured next focusable control or the named AI Assistant panel fallback supplied by D4. The `focus moves forward only when removed` test must first drive this `onFocusCapture` path, rerender with that exact control removed, and assert the captured successor; the fallback test removes both captured elements and asserts the panel target. The wrap-up container extends this rule for an interaction-scoped eligibility branch replacement: it captures a closed semantic focus key (`search`, selected wrap-up reason id, primary action, or panel target), restores the corresponding connected control, and uses successor/fallback only when no counterpart exists. Consult/transfer has no capability-selected destination branch: category, search, and destination nodes remain mounted while only the summary subtree is appended or removed. If removal takes focus from that subtree, the shared successor/fallback rule applies; otherwise the stable destination control retains focus. Shared query/category/reason values are carried only within the same interaction. An interaction switch never maps focus or values across identities: it resets them before render and, when focus was inside the outgoing interaction subtree, moves focus to the established panel target. Loading-to-content, text replacement, feedback paint, expand/collapse, and a summary mount whose focused control remains connected do not otherwise move focus. No `aria-live`, `role=status`, alert announcement, or custom screen reader message is introduced. Tooltip and button remain keyboard reachable in DOM order. + +The successor search is bounded to the same `AISummary` subtree and uses the pre-update document order: focus moves to the first later enabled/tabbable summary control that survives. It never escapes to unrelated host or page chrome. When that focused control was the last focusable summary control, or every recorded later summary control is removed/disabled/disconnected, the fallback is the exact live containing-surface node below. Each owner passes the node by ref; `AISummary` never performs a global selector lookup. + +| Host surface | Required `containingPanelFocusTarget` | Focusability/ownership | Named D8 fallback scenario | +| --- | --- | --- | --- | +| Consult/Transfer destination or existing-party popover | `consultTransferPanelRef.current`, the outer `.agent-popover-content` element of that exact open popover instance | D5 attaches the ref and `tabIndex={-1}` to that root; whole-popover close still returns to the exact opening trigger | `Removing the last focused mid-call summary control falls back to the consult/transfer panel root` focuses the final enabled summary action, removes the entire summary subtree with no surviving summary successor, and requires `document.activeElement === consultTransferPanelRef.current`, never `document.body` or another popover. | +| Receiving-agent AI Assistant panel | `panelRef.current`, the existing `[data-testid="ai-assistant:panel"][role="dialog"]` root | D4 attaches `panelRef` and `tabIndex={-1}` to that exact expanded panel; notification activation still prefers the newly opened header Minimize successor | `Removing the last focused receiver summary control falls back to the AI Assistant dialog root` focuses the final receiver summary action, removes the receiver summary/action branch and all later summary controls, and requires `document.activeElement === panelRef.current`. | +| Post-call wrap-up popover | `wrapUpPanelRef.current`, the outer `.agent-popover-content` element of that exact open wrap-up popover instance | D6 attaches the ref and `tabIndex={-1}` to that root; popover close retains its separate trigger-return rule | `Removing the last focused post-call summary control falls back to the wrap-up panel root` focuses the final enabled post-call summary action, replaces/removes the summary branch with no surviving summary successor, and requires `document.activeElement === wrapUpPanelRef.current`, not the mid-call popover root. | + +D8 runs all three last-control scenarios in addition to successor-present cases. Each receipt records the removed control identity, the pre-update ordered successor list, the exact ref/selector identity of the resolved fallback, its connected/focusable state, and the final active element; D9 rejects a missing surface, a class-only unscoped lookup, body focus, or a fallback belonging to another popover instance. + +**Layout/theme/lifecycle.** The summary uses the established flex/container and `.agent-popover-content` sizing/vertical-overflow contract in `CallControl/call-control.styles.scss` and the panel/body, wrapping, scrolling, semantic-color, and `:focus-visible` contract in `AIAssistant/ai-assistant.styles.scss`. Its co-located `AISummary` stylesheet is only a container-scoped extension of those surfaces: it inherits the existing `cc-calling-widget` width and active host theme, reuses existing `--mds-color-theme-*` tokens, and does not hard-code screenshot bitmap dimensions or add feature-only default/dark/customer maps. `min-inline-size: 0`, overflow wrapping, and word breaking prevent horizontal scroll. In AI Assistant mode the established bounded panel/text body remains the vertical scroll owner. Both CallControl modes use the approved 80vh shell with separately bounded list and summary-content scrolling; headings, summary actions and the primary action remain visible outside those scroll regions. Existing focus and forced-colors behavior is preserved and extended with the same semantic tokens rather than replaced. The `AISummary`-owned copied visual reset timer follows the exact timeout/focus-loss/second-gesture/content-replacement reset contract above and is always cleared on unmount. Reduced motion inherits existing Spinner/button behavior; no feature animation is added. Not applicable - there is no configuration, persistence, schema, network call, or telemetry in this component. + +**Tests.** D3's type/build gate proves `EditableSummaryContent` is derived with `Extract` from store-owned `AISummaryContent`, every named normalized model import is syntactically `import type`, the built JavaScript adds no runtime `@webex/cc-store`/`@webex/contact-center` import, emitted declarations resolve through the manifest-declared `@webex/cc-store` workspace edge, and no duplicate local section/editable-field/feedback/result union exists. The focused suite then covers `render clipboard and response preserve normalized SDK order when payload order differs from metadata tuple order`, `unknown section keys are dropped before render copy and response composition`, `POST_CALL_DISPLAY_SECTION_ORDER is unique and exhaustive`, `resolution ordinal projection never reorders admitted SDK sections`, `present resolution renders read-only Outcome at its relative ordinal`, `absent resolution renders no Outcome row placeholder divider copy block or reserved space`, `shared mid-call keys reuse post-call message IDs and exact labels`, `aiSummary.generatingDescription is Just a sec—the details are coming together. with U+2014 at the separator and no U+005F`, `all visible error categories resolve the UX-010 title and subcopy exactly`, `initiator collapse is presentation-only and edits exact section keys without parsing labels`, `cleared retained section round-trips as an empty keyed value`, `plain paragraph has no synthetic label`, `typed/plain text cannot inject HTML`, `dir auto`, `exact labels equal tooltips`, `resolved variable slots contain no percent marker`, `copy recorder runs only after the Clipboard Promise fulfills and only true paints confirmed`, `receiver mode accepts no card JSON or renderer child and uses its synchronous visible-text getter with the same Copy Summary control`, `Clipboard synchronous getter throw/write throw/rejection leaves visual state and copied counter unchanged, exposes no raw error, and produces zero unhandled rejection`, `copy confirmation remains through 1,499 ms and resets at 1,500 ms`, `copy focus loss resets confirmation`, `second copy resets then starts a fresh interval only after fulfillment`, `receiver contentRevision replacement resets confirmation`, `unmount clears the timer without a state update`, `copy hover/confirmed/idle callback`, `all three modes reject incompatible content/action props at compile time`, `sticky feedback reselect`, `opposite feedback selection`, `synchronous and rejected onFeedback leave controlled state unchanged with zero unhandled rejection`, `synchronous and rejected onRetry leave generation-error unchanged with zero unhandled rejection`, `pending token resolves through aiSummary.feedback.pendingSubmission beneath post-call feedback before completion`, `not-confirmed token resolves through aiSummary.feedback.submissionNotConfirmed in the read-only completion group after response failure`, `confirmed submission removes feedbackStatus`, `all four presentation states render and the component has no hidden or null branch`, `no live region`, `onFocusCapture snapshot moves focus forward only when its captured control is removed`, `receiver unavailable removal chooses the successor then panel fallback`, `updates retain focus`, `vertical not horizontal overflow`, and `forced colors and non-color selection` run in D3. D4 owns the renderer-sibling, inert-card-action, extracted-text order, connected-focus-on-card-replacement, unchanged-props, and unchanged-real-time-assist integration cases. Counter mutation is outside this SDK-free component: D2/D2b store tests assert that a valid role/revision `recordAISummaryCopied` call increments only the targeted role view once and that stale/invalid calls do not increment; D8 holds the real Clipboard Promise pending/rejected/fulfilled and asserts the integrated store counter remains unchanged until fulfillment, then advances exactly once. + +The D3 suite additionally names `pending description uses aiSummary.feedback.pendingSubmission and describes both feedback controls`, `not-confirmed description uses aiSummary.feedback.submissionNotConfirmed and describes both read-only feedback controls`, `confirmed feedback removes the description and both references atomically`, and `canonical search and completion labels do not vary by presentation or copy state`. Each description test resolves both `aria-describedby` IDREFs to the one visible node and rejects `aria-live`, status, or alert semantics. The all-category error test covers `disabled`, `offline`, `unavailable`, `empty`, `timeout`, and `generic`, while separately proving authorization/initialization omission and the unsupported-content branch. + +## Component: Initiating-Agent Call Control Integration + +**Coverage and outcome.** This component covers REQ-002, REQ-003, REQ-006, REQ-008, REQ-009, REQ-010, UX-001, JNY-001, AC-03, AC-06, AC-10, AC-11, AC-12, AC-13, and AC-14. DAG tasks: `D5-mid-call-call-control` for the disjoint initiating feature module/view and `D6b-call-control-summary-integration` for the shared CallControl join. Consult, direct transfer, consult-to-transfer, and conference-participant preparation use the same normalized summary flow while retaining existing telephony method names and arguments. Unauthorized/initialization failure omits the summary region, while an otherwise successful typed/plain payload that cannot be rendered shows the approved unavailable copy in that location. + +**Files and integration points.** D5 adds `task/src/ai-summary-mid-call.ts` and its focused test for open-request observation plus the response-before-action orchestration consumed by the existing `consultCall`, `transferCall`, `consultTransfer`, and `consultConference` paths. It modifies only `CallControlCustom/consult-transfer-popover.tsx`, its co-located `consult-transfer-summary.types.ts` and `consult-transfer-summary.styles.scss`, and the focused popover test. The co-located types give that component a closed `destination` versus `existing-party-action` surface discriminator instead of inferring a mode from missing destination props. For every voice Consult or Transfer destination popover, the `destination` branch renders one shared tree: the installed Momentum `RadioGroup`/`Radio` categories backed by `availableCategories` and `handleCategoryChange`, the existing `TextInput` backed by `searchQuery` and the resolved placeholder constant, and the unchanged fetch/search/result renderer. Each state value and handler has exactly one voice renderer; summary capability never selects a second search-first pill-button implementation. The retained non-voice pill controls derive from the same `availableCategories` and invoke the same `handleCategoryChange`, so the radio conversion cannot change category availability or filtering. When the initiating summary is eligible/visible, the destination branch appends its divider, action-specific heading, editor, and actions after the same destination results; otherwise it omits only that appended subtree. The `existing-party-action` branch reuses the same popover shell and `AISummary` mapping but deliberately renders no category, search, result, or destination-selection nodes; it holds the already-consulted party's TRANSFER preparation or current CONSULT conference summary plus one explicit confirmation control. D6b mounts that branch from the existing standalone consultation controls in shared `call-control.tsx`. `call-control.tsx` must not render or reimplement any category, search, filtering, result, divider, editor, collapse/reopen, or popover-focus logic: it supplies trigger and telephony props and mounts `ConsultTransferPopoverComponent`. The composition therefore has one implementation and one DOM owner, rather than two consumers whose behavior could diverge. The sealed UX-001 scene graph/text/screenshot triple named in the reuse table is authority for the unconditional voice destination hierarchy; the no-screenshot existing-party branch is structural-only. Capability arrival or revocation therefore neither refetches, clears, nor remounts destination state; it only mounts or removes summary UI, while a new interaction resets the shared values before render. + +The UX-001 header -> radio categories -> search -> destinations order is required by the sealed source, not an incidental eligible-state reordering. That one voice tree is rendered for both summary-eligible and summary-ineligible destination popovers; the capability flag controls only the appended divider/summary subtree. The previous search-first pill layout remains only for the separately identified non-voice behavior and is never a second voice rendering or test surface. + +The destination-layout decision is instance-stable. At popover open, `ConsultTransferPopoverComponent` captures a closed `destinationLayout` value from the non-summary surface context: `voice-radio`, `non-voice-pill`, or `existing-party-action`. That latch is not recomputed until the popover closes, and summary eligibility, capability timestamp, request settlement, and ownership generation are deliberately absent from its inputs. If `task:featureEnablement` arrives or reverses while an agent is typing, or the same interaction advances ownership generation while the surface remains open, only the summary subtree and its owner-bound actions may change. The category/search/results tree remains mounted in the same order with the same node identities, query, selected category/destination, and connected focus. Close/unmount discards the latch; the next open derives a new value from the then-current non-summary context, and a canonical interaction switch still performs the documented scoped reset. + +D6b later imports the completed D5/D6 orchestration modules into a focused internal `task/src/helper.ts#useAISummaryActions(ownerKey)` hook. That hook owns only the summary view/action adapters and observation of their closed async results; `useCallControl` composes it once, retains telephony and Task-lifecycle ownership, and threads the returned fields through `task/src/CallControl/index.tsx`, cc-components `task.types.ts`, and `CallControl/call-control.tsx`. Summary state, SDK composition, failure normalization, and mid/post-call sequencing remain in the Store and the two focused feature modules rather than accumulating in `useCallControl`. This is the size boundary: a later summary action extends `useAISummaryActions` or its feature module, never the telephony hook directly. D6b alone edits those shared files, their shared tests/styles, and both module specifications after D4, D5, and D6 finish. Presentation remains SDK-free. + +**Internal signatures and compatibility.** `ControlProps`, `CallControlComponentProps`, `ConsultTransferPopoverComponentProps`, `useAISummaryActions`, and `useCallControl` use `requestMidCallSummary` as the single canonical request callback name. Both `task/src/task.types.ts` and `cc-components/src/components/task/task.types.ts` import `AISummaryActionType` and `AISummaryFeedback` from `@webex/cc-store`; neither file declares a local literal alias. Existing host `CallControlProps` consult/transfer properties do not change. The required internal fields are: + +```typescript +midCallSummary?: AISummaryViewModel; +requestMidCallSummary: (actionType: AISummaryActionType) => Promise; +editMidCallSummary: (field: AISummaryEditableField, value: string, revision: number) => boolean; +recordMidCallSummaryCopied: (revision: number) => void; +setMidCallSummaryFeedback: ( + feedback: Exclude, + actionType: AISummaryActionType, + revision: number +) => Promise; +sendMidCallSummaryBeforeAction: (actionType: AISummaryActionType) => Promise; +``` + +`recordMidCallSummaryCopied` is a synchronous, revision-only post-fulfillment recorder: it accepts neither summary text nor a Clipboard Promise, returns the store's boolean acceptance result, and never invokes `navigator.clipboard`; `AISummary` is the sole write owner and calls this adapter through its content-free `onCopy()` callback only after fulfillment. Compatibility with either synchronous-void or Promise-returning suppliers is limited to the non-wrap-up consult/transfer telephony adapters; their invocation is normalized and both synchronous throw and asynchronous rejection are observed at the adapter boundary. `wrapupCall` and every post-call completion prop instead use the one exact `(wrapUpReason: string, auxCodeId: string) => Promise` contract. The D5 open/effect request starter never discards `requestMidCallSummary(...)`: it immediately installs explicit fulfillment and rejection observers. The store-owned adapter catches SDK synchronous throws, generation rejections, and the 15-second timeout, commits the captured owner's normalized omitted/generic-error state as applicable, and fulfills a closed `AISummaryRequestResult`; the defensive rejection observer routes an unexpected callback rejection through that same sanitized failure observation and resolves without exposing or rethrowing the raw error. In D6b, `handleTargetSelect(...)` and the single `handleExistingPartyConfirm(...)` adapter each return `Promise`, and every internal React/helper call site explicitly observes that returned Promise. Each adapter first `await`s `sendMidCallSummaryBeforeAction`; only after its always-fulfilled `sent`, `failed`, `blocked`, or `stale` result settles does it invoke and await/catch its captured telephony callback exactly once. A non-sent outcome is terminally consumed summary state and cannot throw, return early, or suppress telephony, but telephony is never started concurrently with the response send. The later telephony callback remains the only source of a telephony failure. The D5 module preserves exactly `task.consult(payload)`, `task.transfer(payload)`, `task.transferConference()`, and `task.consultConference()` with their existing signatures and payloads. + +**Voice popover overflow contract.** The operator-approved layout clarification above governs this detail. The existing voice panel retains an 80vh bound and responsive host width; headings/categories/search and summary actions remain in frame. The destination list and shared summary content each own bounded vertical overflow, with no horizontal scroll and no individually scrolling keyed editors. Preserve destination DOM identity, category/query/selection/focus, and keyboard access in summary-visible and summary-omitted states. The popover shell must permit the declared tooltip overhang. + +**Popover and request control flow.** + +1. Opening a new current-owner/action Consult or Transfer preparation retains current destination loading, filtering, pagination, selection, and quick-action callbacks and captures the popover instance's `destinationLayout` latch before rendering. The voice DOM always orders heading, radio categories, the search field with exact resolved placeholder `Search by name, queue, entry point or phone number`, and destination results. When a summary surface is present, divider, action-specific summary heading, editor, and actions append after those results. Consult resolves to the sealed-source `Here’s a consult summary—they’ll get a copy`; Transfer resolves through the distinct `aiSummary.midCall.transferHeading` structural-only message, whose candidate value is unapproved until the product/content confirmation required by D8. Initiating content is shown directly and is not labeled as a receiver notification. Percent-delimited values in the Figma text JSON are unresolved variable markers: evidence binding strips the matched outer pair, and the bound search/agent/user-type/queue/number value is supplied through existing props. The delimiters and marker labels are never visible text, placeholders, accessible names, or product constants. When voice capability is true, the first open (or first late capability enablement while still open) starts `requestMidCallSummary('CONSULT' | 'TRANSFER')` exactly once only if that same owner/action preparation has neither an in-flight request nor a current settled result. The D5 starter immediately attaches explicit fulfillment and rejection handlers; failure is consumed through the captured-owner normalized request-error path, never as a floating Promise. Capability arrival/revocation mounts or removes only the summary subtree and never recomputes `destinationLayout`, leaving category/search/results nodes, values, and focus intact. A new canonical interaction or a genuine new action preparation resets its request/reveal bookkeeping before it may start one new request. +2. While the SDK Promise is pending (up to its 15-second timeout), destination-selection/quick-action controls, an existing-party confirmation control, and starting a different consult/transfer preparation are activation-disabled from `requestPending`; search, close, reopening of the same preparation, and the single popover scroll region remain operable. Pending alone never applies the native `disabled` attribute to a destination, quick-action, or existing-party confirmation control. It keeps the node in the tab order with `aria-disabled="true"` and makes both pointer and keyboard activation handlers return before selection or telephony, so a control that already owns focus retains it instead of dropping focus to `document.body`. An independently unavailable SDK control may still use its pre-existing native disabled state before focus reaches it. Reopening the same owner/action preparation while pending reattaches the surface to the existing store view and in-flight request without issuing an SDK call. Closing invalidates only the UI reveal token; it does not cancel the request or send `MID_CALL_CANCELLED`, and a valid settlement may commit to same-owner store state while closed. The `.agent-popover-content` root is held by a ref, has `tabIndex={-1}` and its heading association, and is the consult/transfer popover's `containingPanelFocusTarget` while connected. If a focused summary control is removed, the pre-removal snapshot moves focus to its connected next focusable control in document order or to that root; if closing removes the entire popover, the close/onHide path restores focus to the exact Consult, Transfer, Transfer Conference, or Merge trigger ref that opened it. Reopening after a current same-owner/action content, unavailable, or generation-error result reveals that result and does not re-request; this prevents an implicit retry, while a genuinely new owner/action preparation may start its one request. A conference-participant preparation request keeps an existing ready summary and its edit/copy/feedback actions usable until valid replacement content arrives; an initial request without content shows loading. A stale generation cannot commit. +3. On a current valid result, sections are collapsed in SDK order into labeled rows in the bottom-most editor. If sections are absent, non-empty `summaryText` fills one text editor. `adaptiveCard` and edit-card JSON are dropped. The wrapper passes the typed/plain component only `mode: 'mid-call-initiator'`; there is no separate presentation `kind` or `role` prop to synchronize. Loading-to-content, unavailable/error-to-content, or other summary replacement beneath a connected focused category/search/destination/control does not move focus; if the replacement removes the focused summary control, it uses the snapshot successor/root fallback just declared. The first successful transition that makes content visible for that popover opening records one view through `recordAISummaryViewed('mid-call', 'initiator', revision)`; every later explicit close/reopen that successfully reveals it records another on that same role view. +4. Each accepted text change routes through `editAISummary('mid-call', 'initiator', field, value, revision)`. `AISummary` performs the one direct-gesture Clipboard write; its content-free synchronous boolean `onCopy()` adapter returns `recordMidCallSummaryCopied(revision)` only after that write fulfills, and the adapter performs no Clipboard call of its own. The `onFeedback` adapter closes over the same rendered view's `actionType` and `contentRevision` and forwards all three values to `setMidCallSummaryFeedback(feedback, actionType, revision)`; every selection or re-selection composes only the initiating view's counters/content with the requested feedback and calls `task.sendMidCallSummaryResponse(payload, resolvedActionType)` only if both tokens are still current. A queued click from an older revision or CONSULT rendering fulfills `stale` after owner/content advancement or TRANSFER promotion and makes no SDK call; the controlled highlight changes only for `{outcome: 'confirmed'}`. A coexisting receiver card under the same owner generation is untouched. `midCallFeedbackPending` disables those feedback controls while the call is in flight but never renders `Pending submission`. Failed, blocked, or stale results preserve the prior highlight. Re-selecting sends again; opposite selection replaces after success. This feedback update is independent of and cannot replace the later once-per-telephony-attempt response call immediately before the telephony action. +5. In the `destination` branch, selecting a destination is the confirmation gesture: it captures the current Task/owner/generation/action and invokes `sendMidCallSummaryBeforeAction(actionType)` first, using edited sections/string or the SDK `NOT_RECEIVED` variant after generation failure. In the `existing-party-action` branch, the separately labeled `Transfer`/`Transfer Conference` or `Merge` confirmation control owns that same sequence; opening or closing the surface alone never calls telephony. Each confirmation `await`s the always-fulfilling `AISummaryPreActionSendResult` and only after that settlement invokes the unchanged captured consult/transfer callback exactly once through its existing current-task guard. The confirmation click observes the returned action-orchestration Promise at the component boundary so its rejection cannot float. The REQ-003 rule is settlement order: with the response Promise deliberately held pending, the telephony spy remains uncalled; after any closed response outcome settles, the captured telephony action starts once. A synchronous send throw is already converted to `{outcome: 'failed'}` before return; later rejection/timeout is `failed`, indeterminate correlation is `blocked`, ownership invalidation is `stale`, and only `sent` may commit response-derived state. Each delays telephony until its closed outcome exists, but none returns early or aborts the one subsequent telephony action. There is no automatic response retry; the response is awaited first and the later telephony Promise is observed, so neither can become unhandled. + +**Consult-to-transfer, additional transfer, and conference flow.** A later consult-to-transfer has no destination-selection gesture and does not reopen the destination tree. For an eligible summary, activating the existing standalone `transferConsult` control (visibly/accessibly `Transfer` or `Transfer Conference`) opens the `existing-party-action` popover anchored to that control, enters transfer preparation, and requests `TRANSFER` exactly once; the popover holds loading/current/error summary state and its explicit `Transfer`/`Transfer Conference` confirmation is the only gesture that runs the pre-action send sequence and then the existing `consultTransfer` callback that reaches `task.transferConference()`. Direct transfer continues to use destination confirmation. The owning accepted request records `TRANSFER`, and the verified Task correlation also promotes any still-current consult-owned state to `TRANSFER` before subsequent feedback or response composition; an absent/ambiguous correlation blocks that operation without painting rather than retaining `CONSULT` by default. The fulfilled transfer/handoff action compare-and-swaps from its captured owner key to a new `handoff-complete` generation before any late pre-handoff result can mutate state. For an additional transfer to C, C's different authenticated-agent successor starts with zero counters, so B's result cannot settle into or project in C's view. + +Participant addition uses exactly one existing `CONSULT` preparation request from the initiating agent; that value belongs only to that consult preparation and is not a permanent alias if the relationship is later promoted. The consulted participant is the receiving agent until `consultConference()` establishes the conference. For an eligible summary, activating the existing standalone `conference` control (the Merge action in the active-consult UI) opens the same `existing-party-action` shell anchored to that control and holds the current/in-flight CONSULT view; opening it does not issue a conference-lifecycle request. Its explicit `Merge` confirmation awaits `sendMidCallSummaryBeforeAction('CONSULT')` with the current edited response, then invokes the captured existing `consultConference` callback that reaches `task.consultConference()` exactly once after settlement. Closing either action popover makes no telephony call and restores focus to its trigger. If the summary surface is omitted because the action is ineligible, both standalone controls retain their legacy immediate single-click callbacks and no empty confirmation popover is introduced. The initiator consumes its Promise result, and every other participating client waits for its own SDK receiving-agent content event; the widget does not synthesize requests from conference lifecycle events. Conference establishment advances exactly once even if both the action fulfillment and Task event observe it. Afterwards there is no global receiving agent and each client accepts only a matching-`conversationId` payload keyed to that local authenticated agent. A same-agent successor carries its old content/counters/feedback until valid newer per-agent content commits, while a newly receiving agent starts counters at zero. Later topology-driven full-history cycles advance similarly, but ordinary repeated requests within one phase retain the epoch and use timestamp/arrival ordering. Participant join/leave and conference-to-call transitions do not clear or cancel carried-forward state/in-flight generation; the Task/SDK remains authority for whole-call summary generation, oldest-remaining host selection, and participants. The final post-call request uses the unchanged canonical interaction, so its SDK-generated content includes the conference segment rather than a widget-truncated local transcript. + +**Failure, security, configuration, and lifecycle.** Unauthorized/initialization states omit the block. Other request failures use the generic error text with no automatic Retry control for mid-call. A role-incompatible success uses `The summary is not available`. All action callbacks capture owner/revision, and mid-call feedback additionally captures `AISummaryActionType`; a mismatched token makes the callback a no-op with a sanitized stale result after interaction, agent, content, or consult/transfer-state change. No raw payload/error, summary text, interaction ID, or agent ID is logged or sent to the host callback; only the store normalization boundary may emit the content-free existing trace category defined under REQ-010. Not applicable - no new feature flag, persistence, migration, telemetry, process, worker, stream, or AbortSignal is added; the server capability and SDK timeout are authoritative. + +**Tests.** D5's disjoint module/component tests cover `one voice category-search-results renderer in summary-visible and summary-omitted states`, `identical category filtering destination selection and pagination across summary visibility`, `summary appends after shared destination results`, `voice popover is capped at 80vh with bounded list/content scroll regions and visible headings/actions`, `result lists and shared summary content scroll independently while individual editors auto-size`, `long summary rows wrap without clipping or ellipsis`, `resolved search placeholder and consult/transfer headings contain no percent delimiter`, `late enablement preserves destination nodes values and focus`, `revocation removes only summary and applies successor/fallback only for focus inside it`, `interaction switch resets shared and request state`, `open-time request attaches explicit fulfillment/rejection handlers`, `SDK generation rejection and 15-second timeout map normalized error with zero unhandled rejection`, `pending close/reopen reattaches without a second SDK request`, `settled close/reopen reveals current same-owner result without re-request`, `new action preparation requests exactly once`, `consult request and placement after results`, `transfer action type`, `typed order and plain fallback`, `card ignored`, `unauthorized/init omitted`, `unrenderable success unavailable`, `15-second pending disables new initiation`, `timeout re-enables without late render`, `close does not cancel`, `reopened content records one new view only after reveal`, `edited send is invoked before consult`, `telephony remains uncalled while send remains pending and starts once only after settlement`, `unavailable response invocation precedes transfer`, `response rejection/timeout normalizes before one continuing telephony attempt`, `stale-generation response settlement mutates no summary state and then the existing telephony guard runs once`, `no floating open-request response or telephony Promise`, `component owns Clipboard write and revision-only recorder runs only after fulfillment`, `shared store action/feedback aliases replace local literal unions`, `feedback forwards rendered action type and revision`, `stale revision/action type makes no SDK call and paints nothing`, `copy and feedback success gates`, `consult-to-transfer requests TRANSFER`, `A-B-C stale settlement`, `conference addition issues one initiating CONSULT request`, `other conference agents wait for receiving content events`, `conference lifecycle events issue no extra request`, `conference old content until per-agent replacement`, `leave does not cancel generation`, `same interaction ID`, and `existing consult/transfer arguments and callbacks unchanged`. D6b reruns the shared CallControl/helper integration assertions after threading both parallel feature modules and proves `useCallControl` receives the one `useAISummaryActions` result without reimplementing summary actions. + +The same D5 suite names `initiator actionType comes from the owning accepted request`, `consult-to-transfer promotion re-evaluates actionType before feedback and pre-action send`, `queued CONSULT feedback with stale actionType or revision does not send`, `indeterminate initiator actionType blocks SDK send and does not paint`, `auto-start rejection is explicitly observed and produces zero unhandled rejection`, and `request and feedback synchronous throws/rejections always fulfill closed outcomes, and every pre-action outcome settles before telephony continues once`. + +The focus, rejection, category-parity, and existing-party cases are not left implicit. D5 additionally names `pending transition on a focused destination, quick action, or existing-party confirmation keeps that exact node focused and aria-disabled while pointer/keyboard activation is a no-op`, `focused summary removal moves to the captured document-order successor and then the programmatically focusable agent-popover-content fallback`, `whole-popover close restores the exact opening trigger instead of document.body`, and `loading/content replacement beneath a connected focused control does not move focus`. It names `open-time request rejection plus feedback callback synchronous throw or returned-Promise rejection are captured with zero unhandledrejection events`, plus `voice radio categories and the retained non-voice pill categories derive identical availableCategories and handleCategoryChange filtering results`. For REQ-006 it names `Transfer or Transfer Conference opens the no-destination existing-party surface, requests TRANSFER once, and only its confirmation awaits response settlement before one task.transferConference call`, `Merge opens that surface with the current CONSULT view without another request, and only its confirmation awaits response settlement before one task.consultConference call`, `closing either action surface calls neither telephony method and returns focus to its trigger`, and `omitted summary preserves the legacy immediate standalone action`. + +The mandatory dynamic-branch case opens a voice destination popover without an admitted capability, types a non-empty query, selects a non-default category and destination, and leaves focus inside the search/results tree before delivering a matching `task:featureEnablement` event and then an ownership-generation update. It asserts that the original `voice-radio` latch, category/search/results node identities, query, selections, result ordering, scroll owner, and focused node survive while only the summary subtree mounts or rebinds. The reverse capability event removes only that subtree. A separate close/reopen assertion proves the latch is released and derived anew, preventing this instance-stability rule from leaking across popover lifetimes. + +## Component: Receiving-Agent AI Assistant Integration + +**Coverage and outcome.** This component covers REQ-002, REQ-004, REQ-006, REQ-008, REQ-009, REQ-010, JNY-001, AC-04, AC-06, AC-10, AC-11, AC-12, AC-13, AC-14, and AC-16. DAG task: `D4-receiving-ai-assistant`. A current attributed receiver event projects one of the closed visible states (`content`, `unavailable`, or `generic-error`) and a content-free `View summary` notification into the existing AI Assistant chrome trigger slot; only that user action opens or restores the established panel and reveals the SDK card or the state-specific fallback. Unauthorized/initialization failure omits the optional receiver contract, section, and trigger. A current owner-attributed malformed event may show generic generation error when no content exists, while an otherwise successful unrenderable card shows `The summary is not available`; stale/unattributable event failures show neither. + +**Files and ownership.** `ai-assistant/src/ai-assistant/index.tsx` observes the receiver view from the store alongside existing real-time-assist state and passes only the normalized current view plus actions. `helper.ts#useAiAssistant` composes summary open/copy/feedback actions but does not call a summary SDK method; it calls the store. `ai-assistant.types.ts` adds internal view props without changing public AI Assistant callbacks. In cc-components, `AIAssistantComponent` already owns the root chrome, closed launcher, minimized restore control, panel header, and body switch; its types/styles extend that same trigger slot with the notification action, add the receiver panel branch, and provide the panel-root focus ref. In the content branch, `AIAssistantComponent` directly composes the existing `AdaptiveCardRenderer` and D3's shared `AISummary` `mid-call-receiver` action surface as DOM siblings. The renderer remains the restricted display-card boundary and receives its unchanged render/fallback props; it does not receive, contain, or own the summary labels, feedback controls, copy control, Clipboard handling, or focus policy. D4 may modify only `AdaptiveCardRenderer/adaptive-card-renderer.tsx` and `adaptive-card-renderer.utils.ts` inside that existing boundary to enforce the closed image-resource policy below, with focused renderer/utility tests; it adds no prop, action, callback, summary mode, or new renderer component. Existing real-time-assist chat semantics, public callbacks, feature flag, launcher open/restore semantics, `IAIAssistantProps`, and the `widget-cc-ai-assistant` r2wc registration remain unchanged; existing locally bundled Momentum icons continue to render through the same resolver. The AI Assistant never imports IncomingTask or other task/call-control code and never forwards a host status itself; the store-owned content-free transition is independently observed by CallControl. + +**Restricted Adaptive Card image-resource policy.** Sanitization runs synchronously inside `AdaptiveCardRenderer` before `AdaptiveCard.parse` or any DOM/image creation. The sole allowed image sources are the exact immutable strings returned by the closed `resolveMomentumIconUrl` map for recognized bundled Momentum icon basenames; a recognized basename is rewritten to that local bundled value before the renderer sees it. No caller-supplied URL is passed through, including `http:`, `https:`, protocol-relative, relative/root-relative, `blob:`, `file:`, `cid:`, or arbitrary `data:` values, and there is no allowed remote origin. The recursive sanitizer covers every Adaptive Card image-bearing slot used by the schema: `Image.url` (including images nested in `ImageSet`), action `iconUrl`, and string/object `backgroundImage.url`. An unrecognized/disallowed `Image` node is removed; a disallowed action icon or background image property is removed while its labeled action/container/text remains. The sanitizer does not rewrite text, action IDs, or other content and never creates a fetch. + +After sanitization, a card with remaining visible supported text/layout renders normally without its blocked images. If the card was image-only or the sanitized render contains no visible card text/content, the renderer deliberately enters its existing `ErrorBoundary` fallback and shows the supplied exact `The summary is not available` text; it does not silently produce a blank content state or reclassify the envelope as a transport error. D1 supplies both a remote-image-with-text fixture and a remote-image-only fixture. D4's focused utility/renderer tests prove zero remote image assignment/request, preservation of visible text and labeled actions for the first fixture, and the explicit unavailable fallback for the second. + +**Internal contracts.** `AIAssistantComponentProps` is an exported `@webex/cc-components` type, so D4 adds exactly one optional property rather than new required top-level props: + +```typescript +type ReceiverSummaryProps = + | { + summarySurface: 'content'; + content: Extract; + actionType: AISummaryActionType; + contentRevision: number; + selectedFeedback: AISummaryFeedback; + midCallFeedbackPending: boolean; + controlsDisabled: boolean; + openReceiverSummary: () => void; + recordReceiverSummaryCopied: (expectedRevision: number) => boolean; + setReceiverSummaryFeedback: ( + feedback: Exclude, + actionType: AISummaryActionType, + expectedRevision: number + ) => Promise; + } + | { + summarySurface: 'unavailable' | 'generic-error'; + openReceiverSummary: () => void; + content?: never; + actionType?: never; + contentRevision?: never; + selectedFeedback?: never; + midCallFeedbackPending?: never; + controlsDisabled?: never; + recordReceiverSummaryCopied?: never; + setReceiverSummaryFeedback?: never; + }; + +export interface AIAssistantComponentProps { + // All existing properties remain unchanged. + receiverSummary?: ReceiverSummaryProps; +} +``` + +Omitting `receiverSummary` is the defined default for every existing caller and test: `AIAssistantComponent` renders no receiver notification and no receiver body branch, preserving all existing AI Assistant behavior and callbacks without caller changes. The observer constructs the object only after narrowing the store view: `content` carries the validated adaptive card and its content-only actions, `unavailable` renders `The summary is not available`, and `generic-error` renders `Having trouble generating summary`. `omitted`, `generating`, and `undefined` produce no object. The component switches exhaustively on `receiverSummary.summarySurface`; no Boolean can contradict the payload and no present branch may render a blank panel. For `content`, `AIAssistantComponent` renders `AdaptiveCardRenderer` and the shared `AISummary` `mid-call-receiver` action surface as siblings under the receiver branch. For `unavailable` and `generic-error`, it renders only the matching shared `AISummary` presentation and no card renderer. No summary body or identifier is added to `IAIAssistantProps`. The notification is rendered by `AIAssistantComponent` in its existing chrome trigger region and contains only a native action plus availability styling; both its accessible name and tooltip are exactly `View summary`. It is not an IncomingTask notification and does not add a cross-package dependency. A receiver surface is eligible only after the D0-verified event is attributed to the current canonical interaction/agent/generation; feature enablement alone cannot create the notification. + +**Control flow.** + +1. `storeEventsWrapper` binds the receiving callback to the Task's canonical interaction and full current owner key, then validates the event. A ready adaptive card commits only after both captured identifiers remain current and records the allowlisted `{kind: 'mid-call', state: 'available'}` transition. A normalized `{kind: 'error', interactionId}` is accepted only when that ID and captured owner still match and there is no accepted content; it records `{kind: 'mid-call', state: 'unavailable'}` and commits the generic-error view without using Promise request sequences. A normalized unsupported envelope must additionally pass timestamp/arrival freshness, then records unavailable and commits the unavailable view. Typed sections and `summaryText` are discarded as receiver render candidates. An ID mismatch, stale/rebound owner, lower timestamp, or timestamp-less error after content is ignored and records no transition. Ownership determines retention before any new event: on a cross-agent handoff from B to C, B's key becomes non-current immediately, the visible receiver projection is cleared, and C starts an empty view with zero counters; B content is never displayed while C waits. On a same-agent conference establishment or later regeneration cycle, the successor is seeded with that agent's current card, feedback, revision, and counters, and pending or failed preparation leaves them visible; only a valid freshness-winning replacement discards the carried-forward card. That current projection is the sole signal consumed by the AI Assistant observer to raise or retain the notification; no task package callback or host event mediates it. +2. The observer omits `receiverSummary` for hidden states and otherwise renders the exact `View summary` action in the established AI Assistant chrome trigger slot without automatically opening the panel. Activation captures the focused notification and its document-order successor before calling the sole receiver binding `openReceiverSummary`. For `content`, that binding closes over the rendered `contentRevision`, calls `recordAISummaryViewed('mid-call', 'receiver', expectedRevision)`, and only on `true` invokes the existing open/restore chrome action and selects the receiver card branch; for `unavailable` or `generic-error`, it opens/restores and selects the corresponding typed fallback branch without incrementing a content view counter. After the notification is removed, a layout effect focuses the first connected enabled successor in the new panel DOM—the existing header Minimize control is first in the current open-panel order—or, if none exists, the existing `[data-testid="ai-assistant:panel"]` `role="dialog"` root held by `panelRef` and made programmatically focusable with `tabIndex={-1}`. Each later successful content reveal increments only that receiver role view. Stale/cleared notification removal uses the same successor then panel-root fallback and never leaves focus on `document.body`. +3. In the `content` branch, `AIAssistantComponent` owns one receiver-branch ref and directly renders two sibling children beneath it: first the display-only `` with only its existing card/fallback rendering inputs, then `` with selected feedback, disabled state, `contentRevision`, `recordReceiverSummaryCopied`, `setReceiverSummaryFeedback`, `panelRef`, and a synchronous `getReceiverCopyText` closure. That closure calls the existing `extractCardText` over the receiver branch; because the utility selects only rendered Adaptive Card text nodes, the sibling action labels are excluded. `AISummary` receives no card JSON and renders only the exact-label receiver action row. The renderer's props, `ErrorBoundary`, and DOM ownership remain unchanged; its existing preparation path gains only the pre-parse closed image-resource sanitizer, and it gains no child slot, summary labels, selected state, feedback callback, Clipboard callback, or summary-mode action branch. The display-only projection removes embedded actions before rendering, and no `onUserAction` or arbitrary `onAction` is supplied. A blocked image cannot issue a request: text-bearing cards render with that image removed, while an image-only/sanitized-empty card and every parse/render failure enter the existing renderer fallback using the shared exact unavailable text and never fall back to receiver `sections` or `summaryText`. The typed `unavailable` branch renders exactly `The summary is not available`, the typed `generic-error` branch renders exactly `Having trouble generating summary`, and absence renders neither branch. Real-time-assist callers retain the same public contract and locally bundled icon behavior under the shared no-remote-resource boundary. +4. From the direct gesture, `AISummary` synchronously calls `getReceiverCopyText`, then invokes `navigator.clipboard.writeText` itself before returning. The click handler returns `void` and attaches fulfillment and rejection handlers immediately. Only fulfillment calls the sole synchronous binding `recordReceiverSummaryCopied(expectedRevision)` with the rendered `contentRevision`; that binding returns `recordAISummaryCopied('mid-call', 'receiver', expectedRevision)` and never invokes the Clipboard API itself. The shared control paints success only when that return is `true`; an extraction/Clipboard throw, Clipboard rejection, or stale `false` invokes no accepted counter update and no success paint. It never copies HTML, card JSON, action-label text, or the ignored `summaryText` fallback line. +5. Like/dislike closes over the rendered receiver view's required `actionType` and named `contentRevision`, forwards them with `Exclude` through the sole receiver binding `setReceiverSummaryFeedback(feedback, actionType, expectedRevision)`, and that binding returns the store's always-fulfilled `setMidCallSummaryFeedback('receiver', feedback, actionType, expectedRevision)` result. Before composition or an SDK call, the store requires both action type and expected revision to match the current discriminated receiver owner/view and re-validates the D0-verified `CONSULT`/`TRANSFER` correlation. `@webex/cc-store` then composes a typed `AISummaryResponse` from the current rendered-card text, counters, and requested feedback and, as the sole SDK caller, invokes exactly `capturedTask.sendMidCallSummaryResponse(payload, resolvedActionType)`. `AISummary` invokes this callback from a non-`async` React handler, catches a synchronous throw, and attaches both Promise settlement handlers before returning `void`; an unexpected direct-consumer rejection normalizes to `failed`. Only the always-fulfilled `{outcome: 'confirmed'}` result produced after the SDK method's `Promise` fulfills is sufficient to paint. SDK rejection maps inside the store to `{outcome: 'failed'}` and leaves the previous selection unchanged; `blocked` and `stale` do the same without rejecting. A queued prior-content-revision/action-type click returns stale without an SDK call. Promise fulfillment is the only observable proxy for REQ-004's 200 confirmation. Unknown action correlation before acceptance cannot create a ready receiver owner/view. If a previously resolved correlation becomes indeterminate while content remains current, projection sets `controlsDisabled: true`; the shared Copy/Like/Dislike buttons use native disabled semantics plus the existing non-color disabled styling and invoke no handler. The store makes no SDK call and preserves the prior selection, so the fail-closed state is visible and assistive-technology exposed rather than a silent no-op. +6. The shared receiver action row is a sibling of, not a descendant of, `AdaptiveCardRenderer` and is not keyed by card revision. A valid replacement updates only the renderer sibling, passes the new `contentRevision` to reset copy confirmation, and leaves connected Copy/Like/Dislike controls and focus intact. If an unavailable, generic-error, omitted, interaction-change, or cross-agent transition removes the focused control, `AISummary` advances to its captured next enabled focusable control in document order; if that successor is also gone, it focuses `panelRef.current`. This same named target covers notification activation and action-row removal, while controls that remain connected never receive programmatic focus. + +**Failures, isolation, and lifecycle.** Unauthorized/init hides the notification and panel branch. Current owner-attributed malformed input with no accepted content maps to generic generation behavior; an otherwise valid freshness-accepted unrenderable card shows unavailable. An unattributable or stale event is dropped instead of mutating the current receiver. Closing/minimizing the panel preserves same-owner state and does not cancel SDK work. Task replacement/unmount removes React subscriptions; store listener cleanup remains at the task/session boundary. The real-time-assist transcript and its existing card callbacks are not overwritten or relabeled. Not applicable - receiver UI adds no persistence, config, telemetry, remote asset/script, new card action, retry, timer, worker, or process. + +**Tests.** D4 covers `existing AIAssistantComponentProps callers omit receiverSummary and retain legacy rendering`, `receiverSummary absent has no notification or receiver branch`, `unauthorized or initialization failure hides both the receiver notification and panel branch`, `content/unavailable/generic-error are exhaustively distinct and no branch is blank`, `content event is first eligibility`, `store view raises notification in existing AI Assistant trigger slot`, `exact View summary accessible name equals tooltip`, `closed and minimized View summary opens/restores existing panel and counts`, `focused View summary removal advances to the open-panel header successor then the focusable ai-assistant:panel fallback`, `canonical openReceiverSummary recordReceiverSummaryCopied and setReceiverSummaryFeedback bindings map to the exact store signatures`, `no IncomingTask or host-surface dependency`, `card only ignores typed/plain`, `AIAssistantComponent renders AdaptiveCardRenderer and the shared receiver action row as siblings`, `the renderer DOM contains none of View summary Copy Summary This is helpful or This isn't helpful controls`, `AdaptiveCardRendererProps and the existing renderer security ownership remain unchanged`, `recognized Momentum image basenames rewrite only to exact bundled resolver values`, `remote image with text performs zero remote assignment or request while preserving visible text and labeled action`, `remote image-only card performs zero request and renders exact The summary is not available fallback rather than blank content`, `caller-supplied http https protocol-relative relative blob file cid and arbitrary data image sources are removed before parse`, `AdaptiveCardRenderer receives only existing render/fallback inputs and embedded actions are inert`, `extractCardText preserves visible newline order while excluding sibling action labels`, `current attributed malformed event shows generic error`, `fresh unrenderable event shows unavailable`, `stale or rebound event error is omitted`, `accepted content survives a later timestamp-less error`, `copy uses sibling-extracted card text after fulfillment and passes rendered contentRevision as expectedRevision`, `rejected Clipboard write leaves the receiver copied counter unchanged, paints no success, and produces no unhandledrejection`, `feedback waits for SDK success`, `rejected SDK and direct-consumer feedback Promises leave the previous selection painted and produce no unhandledrejection`, `feedback forwards rendered action type and contentRevision as expectedRevision`, `stale feedback tokens make no SDK call and do not paint`, `unknown action type after ready content disables the Copy/Like/Dislike row with native disabled semantics and existing non-color styling, sends nothing, and preserves the prior selection`, `reselect sends`, `opposite replaces`, `valid card replacement updates only the renderer sibling and retains connected action focus`, `a focused receiver control disappearing on unavailable, omission, interaction change, or cross-agent replacement moves focus to the captured document-order successor and then the panel root`, `cross-agent successor clears immediately with zero counters`, `same-agent conference preparation pending or failure retains the current card and counters until valid replacement`, `conference per-agent isolation`, `RTA bundled icons and entries retain local rendering`, and `unmount/interaction switch ignores stale event`. + +## Component: Post-Call Wrap-Up Integration + +**Coverage and outcome.** This component covers REQ-002, REQ-005, REQ-006, REQ-007, REQ-008, REQ-009, REQ-010, UX-002 through UX-010, JNY-002, AC-05, AC-07, AC-08, AC-09, AC-10, AC-11, AC-12, AC-13, and AC-14. DAG tasks: `D6-post-call-wrap-up` for the disjoint post-call feature module/view and `D6b-call-control-summary-integration` for the shared CallControl join. Eligible voice wrap-up implements the nine sealed post-call screenshot states by showing generation/edit/actions after reason selection and before Complete Wrap-Up, performs wrap-up first, then sends exactly one frozen response. Unauthorized/initialization failure omits the region, while an otherwise successful unrenderable payload shows the approved unavailable copy in that location. D6 owns the package-internal wrap-up orchestration and legacy-behavior compatibility assertion; D6b alone changes the published React callback type and owns its SemVer-major migration proof, while the existing custom-element wrap-up path remains behaviorally compatible. + +**Files and signatures.** D6 adds `task/src/ai-summary-post-call.ts` and its focused test for capture, wrap-up-first, and one-response orchestration. That module owns package-internal `completeWrapupWithSummary`, the sole implementation allowed to invoke `task.wrapup` for the eligible-with-draft, eligible-without-draft, and ineligible legacy branches; it is not exported from a package entry, a component prop, or a second public entry point. D6 does not edit `task/src/helper.ts`, `task.types.ts`, `CallControl/index.tsx`, either package entry, or the custom-element wrapper; D6b is the sole writer that later changes the published callback seam. D6 also adds the single-consumer `CallControlCustom/wrap-up-summary.tsx`, `wrap-up-summary.types.ts`, and `wrap-up-summary.styles.scss` plus a focused test. Before implementing structure or copy, D6 resolves each UX-002 through UX-010 reference to its exact D0a-sealed scene-graph JSON, text JSON, and PNG triple named by `requirement.md`; all nine triples are implementation inputs, not merely D8 comparison inputs. The component reuses the existing `wrapupCodes`, reason selection inputs, and installed Momentum `Input`, `RadioGroup`, and `Radio` components to implement that sealed screenshot hierarchy; its false/absent-capability output preserves the legacy `Select` and `SUBMIT_WRAP_UP` behavior. The `Input` retains the screenshot placeholder but receives a separate persistent accessible name, and the `RadioGroup` is labelled by the existing visible reason heading rather than by placeholder or surrounding layout. The structural replacement is grounded in the named UX-002/UX-003 scene/text/PNG triples in the reuse table and the same sealed hierarchy in UX-004 through UX-010. `wrap-up-summary.styles.scss` bounds the shell at 80vh and independently scrolls the reason list and shared summary content, keeping the heading, summary actions and primary button visible in expanded and collapsed layouts. A same-interaction eligibility change preserves selected reason/id, query, and the store-owned accepted/edited draft while remapping focus between semantic counterparts. An interaction switch resets branch-local state and clears the prior interaction's draft only at its existing retention boundary. + +Neither `wrap-up-summary.tsx` nor its styles choose an inline size from summary eligibility, expanded/collapsed state, or UX source ID. Production follows one sizing rule: retain the existing cc-calling-widget host popover's inline size and adapt within it. The 400, 440, and 442 CSS-pixel measurements below are deterministic visual-fixture container widths supplied by D8, not three production width branches. + +D6b changes `task/src/helper.ts#wrapupCall` and the corresponding exported `task.types.ts` callback surfaces from `(wrapUpReason: string, auxCodeId: string) => void` to the exact `(wrapUpReason: string, auxCodeId: string) => Promise` contract; no `void | Promise<...>` compatibility union is exposed. `wrapupCall` is a thin adapter that returns D6's `completeWrapupWithSummary(...)` result for both eligibility branches and never invokes `task.wrapup` itself. The Promise always fulfills with a closed result: `{wrapup: 'failed'}` or `{wrapup: 'succeeded', response: 'not-required' | 'submitted' | 'response-failed'}`. Its focused `useAISummaryActions(ownerKey)` hook imports that sole D6 orchestration, captures the Task before the single `task.wrapup` call site, and supplies the observable post-call view/store callbacks from `CallControl/index.tsx`; `useCallControl` composes that hook once and mounts the D6 component through shared `call-control.tsx`, `call-control.utils.ts`, types, and styles. Existing `onWrapUp` still fires from the backend wrapped-up callback with the same `{task, wrapUpReason}` payload. Because `CallControlComponentProps` and the related task props are published TypeScript contracts, direct React consumers that supply a synchronous callback or a rejecting Promise must migrate to return the normalized result; this is a breaking change for `@webex/cc-task` and `@webex/cc-components`, and the release must be classified SemVer-major with semantic-release breaking-change metadata. The custom-element host contract remains additive because hosts neither supply nor observe this internal callback. + +The post-call fields threaded through the component contracts are: + +```typescript +export type WrapupCompletionResult = + | {wrapup: 'failed'} + | { + wrapup: 'succeeded'; + response: 'not-required' | 'submitted' | 'response-failed'; + }; + +postCallSummary?: AISummaryViewModel; +requestPostCallSummary: (trigger: AISummaryPostCallRequestTrigger) => Promise; +editPostCallSummary: (field: AISummaryEditableField, value: string, revision: number) => boolean; +recordPostCallSummaryCopied: (revision: number) => void; +onCopyVisualStateChange: (state: AISummaryCopyVisualState) => void; +setPostCallSummaryFeedback: ( + feedback: Exclude, + expectedRevision: number +) => boolean; +wrapupCall: (wrapUpReason: string, auxCodeId: string) => Promise; +``` + +`AISummaryFeedback` and `AISummaryPostCallRequestTrigger` are imported from `@webex/cc-store`; `Exclude` derives the selectable subset from that single canonical type and no component/task contract repeats the two selectable feedback literals inline. `AISummaryCopyVisualState` and the required `onCopyVisualStateChange` callback come from D3's `ai-summary.types.ts`; the D6 wrapper uses that callback to update only its local `copyVisualState`. Private `completeWrapupWithSummary` is intentionally absent from the prop block: only the exact-Promise `wrapupCall` adapter crosses the hook/component boundary. + +**Transfer/conference ownership.** Every store `requestPostCallSummary(trigger)` call captures the current `AISummaryOwnerKey` (`interactionId`, authenticated `agentId`, and `ownershipGeneration`) plus the installed post-call generation before invoking the argument-free SDK method, and only a settlement matching both may create or replace the post-call role view. After an A-to-B-to-C transfer, C's confirmed handoff generation is current at wrap-up: C alone owns the post-call request, editable draft, capture token, and frozen response; any pending B settlement or draft is stale and cannot project or be captured. Because this is a different-agent owner, C's post-call viewed/edited/copied counters start at zero and never inherit B's counters or either agent's separate mid-call role counters. The common canonical interaction ID remains unchanged. + +For a conference, the widget neither assembles nor filters transcript segments. It requests the final summary from the SDK for that same canonical interaction and current per-agent generation, admits only a matching response, and preserves the returned structured/plain bytes including the conference segment through edit, capture, and response composition. Participant joins/leaves and conference-to-call conversion do not select a post-call owner in UI code; the Task/SDK topology remains authoritative, including the oldest-remaining-initiating-agent host rule, and the authenticated agent that actually reaches wrap-up supplies the current owner generation. A late response captured for a predecessor agent or generation is consumed as stale before rendering or counter mutation. + +**Generation and render flow.** + +1. The eligible branch adds local `wrapupReasonQuery: string` (initial `''`), `copyVisualState: AISummaryCopyVisualState` (initial `'idle'`), `completionEscapeAvailable: boolean` (initial `false`), and a monotonically increasing reason-selection revision plus the last requested revision for the current owner. It renders screenshot copy `Wrap up interaction`, `Choose a code, and click the summary to edit it if needed.`, and `Choose a reason to wrap up`. The search `Input` keeps the visible `Search topic` placeholder and also receives exact `aria-label` text `Search wrap-up reasons` from stable message ID `aiSummary.postCall.reasonSearchLabel`, so typed input never removes its accessible name. The visible `Choose a reason to wrap up` heading resolves from `aiSummary.postCall.chooseReason`, receives an instance-local DOM id from `useId()`, and that id is the `RadioGroup`'s `aria-labelledby`. The input filters `wrapupCodes` by case-insensitive en-US substring without reordering. If no code matches, the results slot contains no radio rows and instead renders ordinary visible text `No wrap-up reasons match your search.` from stable message ID `aiSummary.postCall.noReasonMatches`; the node has no `role="status"`, `role="alert"`, `aria-live`, or programmatic announcement. + + Momentum `RadioGroup` change events, including every arrow-key move, call the existing `handleWrapupChange(code.name, code.id, logger)` and advance the local selection revision but never call the SDK. A single `commitReasonSelection` seam invokes and observes `requestPostCallSummary('reason-commit')`: pointer activation or Enter/Space on the selected radio commits immediately, while focus leaving the radio group commits the final arrow-selected radio. It deduplicates the native `change`/activation/blur event sequence by current owner plus selection revision, so one selection revision starts at most one request; arrowing through any number of radios and then pressing Enter/Space or leaving the group starts exactly one request for the final selected reason. While that current-generation request is pending, the controlled group exposes focus-retaining `aria-disabled`; its change, pointer, keyboard, and whole-group-blur handlers are all guarded, so the selected reason and selection revision cannot advance until settlement and focus is not forcibly moved. Search input alone does not request. Every later explicit Retry invokes and observes `requestPostCallSummary('retry')`; it calls the SDK again in the same reason generation and is never a response retry. Direct programmatic duplicate/race calls at the store boundary remain independently observed. Open calls in the same current generation collapse into the aggregate pending projection and use greatest source timestamp/equal-last-arrival when both values exist, otherwise later owner-role arrival; a later `reason-commit` supersedes older-generation calls before that ordering rule, and timeout closure precedes both. One current-generation settlement cannot clear `requestPending` while another current-generation request remains, and blocked/stale/failed outcomes are consumed without rejection. The store sets `requestPending` before transport; with no retained content the first committed reason immediately projects the one screenshot-backed generating panel. Late same-interaction enablement maps a valid legacy Select reason into the matching radio and preserves it; revocation maps it back to the Select. The focused Select/search/radio/primary action is mapped to its semantic counterpart or the successor/fallback target, with no announcement. Interaction change clears the reason/query/copy/commit state before selecting a branch. +2. Structured response sections render every admitted typed label in D2's normalized SDK order and all SDK response-section fields are editable. Optional `resolution` is projected as the read-only `Outcome` row through `POST_CALL_DISPLAY_SECTION_ORDER`: insert it before the first admitted key with a greater ordinal, otherwise append it, without naming or requiring either adjacent section. With no non-empty sections, non-empty `summaryText` renders as one editable unlabeled paragraph. Adaptive cards are discarded. A role-compatible response records one viewed action when that accepted response is revealed by the agent's committed reason-selection request or Retry; stale, hidden, or unsupported results do not increment. Completion availability is not conditioned on receiving that response. Once content exists, changing or committing another reason does not clear, recreate, or downgrade that draft: its exact accepted edits, `contentRevision`, viewed/edited/copied counters, selected local feedback, and `Pending submission` status remain projected and capturable while the replacement request is pending and after a blocked, stale, failed, or unsupported settlement. Only a role-compatible response accepted for the current owner and current reason generation replaces the draft with a new Store-lifetime revision; generation precedence means its timestamp is not compared with the retained older-generation draft. That atomic acceptance preserves the accumulated edited/copied counters, increments viewed exactly once for the successful reveal, and resets the old draft's feedback to `none` with no pending description because the selection rated the superseded content. A response from an older reason generation is stale before freshness comparison and cannot replace the draft even with a greater timestamp or later arrival. Within the current generation only, two present timestamps use greatest/equal-last-arrival; if either is absent, later owner-role arrival selects the winner. A timeout-closed settlement cannot enter either comparison. If Complete Wrap-Up is activated before a replacement is accepted, capture freezes the currently visible edited draft, counters, feedback, and currently selected `(wrapUpReason, auxCodeId)`; its capture guard makes every later generation settlement stale. +3. With no retained draft, the initial loading state matches S02 and may keep Complete Wrap-Up disabled only while the exact committed-reason request is pending; the store-owned 15-second timeout is the hard upper bound. A terminal generation error or unavailable result sets `completionEscapeAvailable` for that owner plus reason-selection revision and enables Complete Wrap-Up with the committed reason even though there is no summary content. Error Retry has the exact name/tooltip and starts a fresh generation request, but cannot clear that escape or disable completion again. A reason change resets the escape for the new revision and starts a separately bounded request. Thus the primary control is disabled only when no valid reason is selected, while that first bounded request has neither retained content nor an escape, or while the wrap-up submission itself is awaiting settlement. During a reason-change request with retained content, that content remains visible and editable rather than being replaced by the generating or error panel, and Complete Wrap-Up continues to capture the visible draft. Unsupported success shows unavailable in the summary location only when there is no retained accepted draft; it cannot erase existing edited content. Unauthorized, initialization failure, false capability, or non-voice omits the entire summary region and preserves the original reason-to-submit behavior. +4. Each store edit/copy-notification/view operation still passes its valid selector pair `kind: 'post-call'`, `role: 'post-call'`, and the rendered `contentRevision`, so it cannot mutate either coexisting mid-call role view. At the presentation boundary, the wrapper passes only `mode: 'post-call'` to `AISummary`; no redundant component `kind`/`role` pair exists. `AISummary` owns the Clipboard write and its content-free post-fulfillment synchronous boolean `onCopy()` adapter returns `recordPostCallSummaryCopied(contentRevision)`; the wrapper/store never receives summary text or writes the clipboard. Post feedback is local only: selecting or re-selecting calls `setPostCallSummaryFeedback(feedback, contentRevision)` and paints mutual exclusion only when it returns `true`; the wrapper passes the store-projected `feedbackStatus` token directly beneath that group. The accepted local selection produces `pending`, which the component resolves through `aiSummary.feedback.pendingSubmission` to `Pending submission`; `midCallFeedbackPending` is not present on this role and no SDK feedback method is called before wrap-up. A stale owner/revision, ineligible/hidden surface, wrong role, or captured/frozen/submitted/response-failed owner returns `false` with no feedback or pending mutation, so a delayed custom-element/event click can never enter the frozen response. + +**Screenshot-state layout/copy mapping.** The PNG and matching JSON establish each visible structure. Percent-delimited Figma text values are variable markers; the common slot-resolution rule strips the matched outer pair at the rendering boundary and supplies delimiter-free product copy. S02 (`postcall_generating.png`), S03 (`postcall_summary_edit.png`), and S10 keep the reason search/list expanded above the divider with visible `Search topic`; the selected radio remains selected and the primary label and accessible name are always `Complete Wrap-Up`. S04 through S07 collapse the list to the search trigger with the same resolved `Search topic` placeholder, and S08 (`postcall_summary_hover_copy.png`) / S09 (`postcall_summary_selected_copy.png`) retain `Search topic` and `Complete Wrap-Up` during copy hover/confirmation. `onCopyVisualStateChange` changes only the copy control's hover/confirmed/idle visual treatment; its state is never an input to the primary control, whose text, accessible name, and tooltip source remain the single `aiSummary.postCall.completeAction` constant across generating, content, error, unavailable, copy-hover, and copy-confirmed states. `ai-summary.constants.ts` therefore has one stable delimiter-free ID/value for each screenshot-derived control, while the D6 wrapper co-locates the three reason-control message IDs and never inserts a marker into runtime copy. D6 walks rendered visible text plus accessible names/tooltips for every post-call state and fails if any string matches `/^%.*%$/`. S10 remains authoritative for the exact `Having trouble generating summary` / `It could be a lost connection or something else. Could you check your connection or try again later?` hierarchy but not its disabled primary treatment: terminal error sets the bounded escape and D8 records the resulting enabled-control delta as structural evidence instead of coupling completion to summary success. The zero-match text is also a structural-only state because the sealed screenshots do not depict a zero-result query; D8 records DOM/accessibility classification evidence rather than inventing a pixel baseline. Collapse/removal of a focused search/radio uses the shared next-focus rule; otherwise list/content/copy-state updates do not move focus. Popover close resets the query, provisional/selected reason and id, reason-selection/request revisions, copy visual state, and completion escape, but it never mutates the store-owned accepted/edited draft or its revision; reopening projects that retained draft while requiring a reason selection before wrap-up. Interaction change resets both branch-local state and the interaction-scoped draft at its existing retention boundary. The selected reason remains through a wrap-up failure as required. + +**Post-call scroll ownership.** The operator-approved layout clarification above governs this detail. Expanded and collapsed layouts share an 80vh-bounded panel with persistent heading and Complete Wrap-Up action. The filtered reason list and summary content may scroll independently; shared copy/feedback actions stay outside content scrolling. Individual keyed editors auto-size, and narrow layouts preserve every control and keyboard access without horizontal overflow. The shell must not clip hover/focus tooltips. + +**Complete-wrap-up sequencing.** On click, exported `wrapupCall(wrapUpReason, auxCodeId)` delegates exactly once to private `completeWrapupWithSummary`; no other helper or component calls `task.wrapup`. The private function synchronously captures the current Task and selects one of three preparations before reaching its single `task.wrapup({wrapUpReason, auxCodeId})` call site. For an eligible ready post-call view it calls `capturePostCallDraft(auxCodeId, revision)` first; the store verifies the same current Task exists, copies the draft/counters/feedback and exact `wrapUpCode` into its private record, installs the capture-revision mutation guard, retains that Task internally, and returns only `captureToken`, so the task container never receives summary bytes. For an eligible terminal generation-error/unavailable view with `completionEscapeAvailable`, it captures no summary, marks the response disposition `not-required`, and records the exact owner/reason-selection revision so a late generation settlement is stale after successful wrap-up; this branch exists solely to prevent summary availability from gating required wrap-up. For an ineligible legacy view it likewise creates no summary capture and marks `not-required`, while preserving the existing Select/Submit UI and the same SDK wrap-up arguments. The function retains the selected `(wrapUpReason, auxCodeId)` pair and invokes its captured Task exactly once, so the display reason and committed identifier preserve their existing SDK meanings. + +The same `try`/Promise-rejection boundary surrounds that sole invocation for all three preparations. A synchronous throw or rejected `task.wrapup` is categorized and reported through the existing sanitized wrap-up failure path, never exposes the raw reason, and fulfills `{wrapup: 'failed'}`. In the eligible-with-draft branch it also calls `releasePostCallDraft(captureToken)`, exposes the unchanged store draft only when that token resolves and its compare-and-clear succeeds for the current owner, and retains the selected reason/id. If another interaction became current during that failure, the stale release returns `false`; the old draft remains non-visible and is cleared by its retention boundary rather than restored into the new interaction. The two no-token branches retain the reason/id and their unchanged error/unavailable or legacy UI for another full wrap-up attempt. No summary response is submitted after a failed wrap-up and no branch can reject. After a successful eligible-without-draft or ineligible wrap-up, the function commits the existing success bookkeeping, invalidates any later settlement for the completed owner/reason revision, and fulfills `{wrapup: 'succeeded', response: 'not-required'}` without calling either summary capture/send action. + +After an eligible captured wrap-up fulfills, the private implementation treats wrap-up as irrevocably successful, commits the existing task/state switch and reports that success through the existing CallControl bookkeeping before any summary send. It passes only `captureToken` to `freezeAndSendPostCallSummary`; the store resolves the private snapshot and Task, composes and deep-freezes the exact SDK payload with the captured `wrapUpCode`, and calls that Task's `sendPostCallSummaryResponse(frozenPayload)` once. The human-readable `wrapUpReason` is absent from the response. This implementation path is admitted only after D0 proves that exact same-Task call can fulfill after an awaited successful `wrapup`, using D1's explicit `postWrapUpSend` fixture whose distinct values assert `frozenPayload.wrapUpCode === committedWrapup.auxCodeId` and not the display label. It never re-reads mutable UI state, accepts a caller-owned payload/Task pair, or redirects the call because visibility changed. The internally retained Task reference is the sole immediate post-wrap-up response call; no later retry assumes the Task is callable. The eligible-without-draft branch never enters this paragraph and therefore cannot fabricate or send an empty summary response. + +Success sets `submitted` only on the token's captured post-call owner record, clears `postCallFeedbackPending`, sets `feedbackStatus: undefined` immediately, removes the private capture, and fulfills with `{wrapup: 'succeeded', response: 'submitted'}`. Rejection of an admitted runtime transport attempt is caught and normalized: it cannot undo or reclassify the committed wrap-up, clears `postCallFeedbackPending`, preserves the same frozen object/counters/feedback in its keyed tombstone, sets only `response-failed` plus `feedbackStatus: 'not-confirmed'` for that owner, produces the content-free callback transition, performs no resend, and fulfills with `{wrapup: 'succeeded', response: 'response-failed'}`. The wrapper keeps a disabled/read-only feedback completion group mounted for that current owner after wrap-up so `aiSummary.feedback.submissionNotConfirmed` resolves and renders `Submission not confirmed`; it exposes no payload object, editor, Complete Wrap-Up, or Retry action. `handleWrapupCall` clears the selected reason/id for either successful-wrap-up result after the normalized Promise settles, but that clearing does not remove the owner-keyed completion status. While the interaction remains current, the frozen response is retained until the matching owner generation's `AgentWrappedUp`/`TASK_WRAPPEDUP`; if that exact backend event raced ahead of response rejection, only that record's `agentWrappedUpObserved` causes immediate post-callback discard. Another interaction's event cannot affect it, while an interaction/session boundary clears the record and retained bindings without waiting for the event. If the candidate SDK instead makes every Task non-callable after wrap-up, D0 rejects it and D6/D6b remain blocked pending a conforming external SDK commit; the fallback is not to send before wrap-up and not to ship an unreachable success path whose deterministic result is `response-failed`. + +**Failures, storage, observability, and lifecycle.** A reason-regeneration failure, block, stale settlement, or unsupported result leaves prior accepted content and edits projected; only the no-retained-content path shows the generic or unavailable state. Response errors and frozen payload bytes are not exposed, logged, or mapped to a new telemetry vocabulary; only the closed `not-confirmed` token is projected, and presentation resolves its stable message ID to `Submission not confirmed`. Frozen data and opaque capture tokens remain memory-only. A successful submitted state is retained until interaction change; while the same interaction remains current, a failed record follows its exact owner-bound backend event, and interaction change or global sign-out/session cleanup clears it and its bindings if that privacy boundary wins first. Not applicable - no schema migration, persistence, cancellation, response backoff, background worker, or new feature configuration is required. + +**Tests.** D6's disjoint module/component tests cover `reason search filters without reordering or requesting`, `zero-match reason query renders static empty state`, `reason search has the persistent Search wrap-up reasons accessible name`, `reason RadioGroup is labelled by Choose a reason to wrap up`, `pointer and Enter/Space reason commits issue one request`, `arrow-key traversal of reason list issues one request`, `duplicate activation and blur for one selection revision issue no second request`, `reason change after edit does not silently discard the draft`, `popover close and reopen retains the edited draft while resetting reason controls`, `accepted current committed-reason response is the only draft replacement boundary`, `capture during reason regeneration freezes the visible edited draft and rejects the late settlement`, `eligible hierarchy implements every D0a-sealed UX-002 through UX-010 scene-graph/text/PNG input`, `legacy Select and SUBMIT_WRAP_UP path retains its existing task.wrapup arguments and observable behavior`, `package-internal orchestration is absent from package entry exports and D6 leaves the exported useCallControl/task prop declarations untouched`, `late enablement maps reason and focus`, `revocation maps reason and focus/fallback`, `interaction switch resets both branches`, `every Retry is new GET`, `initial loading disables Complete Wrap-Up only while the bounded request is pending`, `unrecovered generation error enables and completes wrap-up as not-required`, `Retry cannot revoke the completion escape`, `typed order/edit mapping`, `resolution Outcome is read-only and omitted from response`, `resolutionPresent position when adjacent sections are absent`, `resolutionAbsent omits Outcome and all reserved space`, `plain unlabeled fallback`, `card ignored`, `unrenderable success unavailable without erasing retained content`, `expanded and collapsed layouts share an 80vh shell with independent reason/content scrolling and keep Complete Wrap-Up visible at 320px`, `Search topic and Complete Wrap-Up use one resolved delimiter-free constant in every visual state`, `copy hover and confirmation never change the Complete Wrap-Up visible label or accessible name`, `timestamp winner`, `A-to-B-to-C wrap-up binds the request draft capture and frozen response only to C's current owner generation, starts C's post-call counters at zero, and rejects B's late settlement`, `conference final post-call content retains the common interaction ID and the SDK fixture's conference segment without widget-side assembly or truncation`, `post feedback uses the store-exported AISummaryFeedback-derived selectable type with no inline thumbs union`, `post feedback local pending projects pending and resolves aiSummary.feedback.pendingSubmission`, `confirmed response removes feedbackStatus`, `failed response projects not-confirmed and resolves aiSummary.feedback.submissionNotConfirmed after wrap-up without edit retry or resend controls`, `wrapupCall delegates once to completeWrapupWithSummary and only that implementation invokes task.wrapup`, `eligible-with-draft synchronous throw and rejection fulfill failed result, release the exact capture, retain reason, and produce zero unhandled rejection`, `eligible-without-draft synchronous throw and rejection fulfill failed result, retain reason, and produce zero unhandled rejection`, `ineligible synchronous throw and rejection fulfill the same failed result, retain reason, and produce zero unhandled rejection`, `terminal-error eligible-without-draft success calls task.wrapup once, fulfills not-required, and performs no capture or response send`, `ineligible success fulfills not-required without capture or response send`, `wrapup success bookkeeping precedes one response`, `opaque token keeps draft and captured Task inside the store`, `released reused and unknown tokens cannot send`, `captured Task used after success commit`, `response success fulfills submitted result`, `response rejection fulfills response-failed result and clears reason`, `response failure exact freeze and no resend`, `AgentWrappedUp after failure cleanup`, `AgentWrappedUp before rejection immediate cleanup`, `AgentWrappedUp for another interaction leaves the retained tombstone and status intact`, `unauthorized/init omission preserves legacy wrapup UI`, `callback outcome handoff`, and `zero unhandled rejection`. The unrecovered-error case starts after the final failed generation attempt with no draft, asserts Complete Wrap-Up is enabled, clicks it without invoking Retry, and proves the sole `task.wrapup` call succeeds as `{wrapup: 'succeeded', response: 'not-required'}` with zero summary-response calls. The zero-match node is asserted visible while absent from live-region/alert roles, and the accessible-name cases use role/name queries rather than placeholder queries. This D6 acceptance pins compatibility at the disjoint boundary: no published hook/prop changes occur yet, the ineligible/custom-element-backed path keeps its legacy UI and SDK arguments, and the orchestration symbol is package-internal. D6b alone reruns the shared helper/container/CallControl assertions after threading both parallel feature modules and adds compile-time coverage that accepts only the always-resolving `Promise` callback and rejects a synchronous/`void` supplier, plus runtime coverage that defensively consumes a nonconforming direct-consumer synchronous throw or rejected Promise without unlocking early, leaking the raw error, or dispatching an unhandled rejection. The D0 suite, rather than a D6 mock, remains the acceptance owner for actual post-wrap-up SDK callability. + +The resolution cases are paired, not inferred from one always-populated response. `resolutionPresent` proves read-only/copyable Outcome insertion at the typed relative ordinal when adjacent ordinal sections are missing; `resolutionAbsent` proves no Outcome label/value/row/edit target/copy block/response key and no blank layout gap, while all remaining SDK sections retain their incoming relative order. + +The mandatory D6 reason-change concurrency group starts from a generated draft that has been edited, copied, and locally rated. It proves `reason-commit` preserves the exact draft/revision, all counters, selected feedback, and visible `Pending submission` while its current-generation request is pending or ends blocked/failed/unsupported; a second reason generation makes the first request stale even when the first returns later with a greater timestamp; and only a ready response in the installed generation replaces content. That replacement gets a new revision, preserves edited/copied totals, increments viewed once, clears the old feedback selection and pending description, and clears the copy-confirmation timer through the normal content-change path. Separate cases prove same-generation Retry responses still use greatest-timestamp/equal-last-arrival ordering, every stale/no-op settlement changes no counter or feedback, and capture during regeneration freezes the displayed draft/counters/feedback and makes every later request settlement stale. + +The D6/store negative suite explicitly names `post-call feedback rejects stale revision, advanced owner, ineligible surface, and captured/frozen state without mutating pending feedback or the frozen payload`; it also proves an accepted current revision returns `true` and a rejected action returns `false`. + +## Component: Shared Call-Control Summary Integration + +**Coverage and outcome.** This narrow join covers REQ-002, REQ-003, REQ-005, REQ-006, REQ-007, REQ-008, REQ-009, REQ-010, JNY-001, JNY-002, AC-03, AC-05 through AC-09, and AC-10 through AC-14. DAG task: `D6b-call-control-summary-integration`. D4, D5, and D6 can proceed concurrently once D2b and D3 are complete; D6b begins only after all three finish and is the sole task that edits their shared CallControl types, orchestration shell, tests, styles, and module specifications. + +**Join boundary.** D6b imports the D5 mid-call and D6 post-call orchestration modules into one focused `useAISummaryActions(ownerKey)` hook in `helper.ts`; `useCallControl` composes that hook exactly once, then threads its normalized props/actions through `task.types.ts`, `CallControl/index.tsx`, cc-components `task.types.ts`, and `call-control.tsx` to mount the two disjoint presentation branches. The hook is the only React orchestration owner for summary request/edit/copy/feedback/pre-action/wrap-up adapters and closed-result observation. `useCallControl` remains the telephony and Task-lifecycle owner and may not duplicate summary state, SDK payload composition, error normalization, or mid/post-call sequencing. It owns `call-control.utils.ts`, the shared CallControl stylesheet and tests, `task/ai-docs/task-spec.md`, and `cc-components/ai-docs/cc-components-spec.md`. D4 is a dependency only because it legitimately updates the latter specification first; neither D5 nor D6 edits that shared document. The join adds no SDK call outside the store, no second summary lifecycle authority, and no third summary implementation. `call-control.tsx` mounts D5's `ConsultTransferPopoverComponent` but contains none of its category/search/filter/results/divider/editor composition or collapse/focus algorithms. The initiating callback is named `requestMidCallSummary` at every hook/component boundary. `handleTargetSelect` and every existing-party confirmation await the normalized pre-action result before creating the telephony Promise; `sent`, `failed`, `blocked`, and `stale` all continue to one separately observed existing action after settlement, so response success is not a suppressing gate but response settlement is a strict ordering boundary. The exact-Promise `wrapupCall` adapter delegates eligible-with-draft, eligible-without-draft, and ineligible submissions to private `completeWrapupWithSummary`, the sole `task.wrapup` caller. `handleWrapupCall` keeps the completion control locked only while that Promise settles, handles the closed result by retaining reason/id only for `wrapup: 'failed'` and clearing them for every `wrapup: 'succeeded'` result, and uses an explicit defensive rejection arm around the published callback boundary. A nonconforming direct consumer's synchronous throw or rejection is sanitized to the same failed-wrap-up UI, never rethrown, and cannot become an unhandled rejection; the conforming internal supplier always fulfills, including `response: 'not-required'` for both the ineligible path and the eligible terminal-error/unavailable path with no draft. Its module-spec updates must identify the always-resolving result-bearing `wrapupCall` change as a breaking published React contract and give direct consumers the migration (return/await the Promise and return the closed result rather than throwing); semantic-release must receive breaking-change metadata so both affected published packages participate in a major release. This package-level consequence is separate from the compatible custom-element host surface. + +**Acceptance.** Focused D5 and D6 tests must already pass. D6b then runs the existing task helper/container and cc-components CallControl/unit suites with both feature branches mounted, proving the single `useAISummaryActions(ownerKey)` composition and responsibility boundary, the canonical `requestMidCallSummary` name through every type/component seam, the popover's sole ownership of the mid-call category/search/filter/results/divider/editor DOM and behavior, eligible and ineligible behavior, late enablement/revocation value and focus mapping, hidden versus unavailable rendering, response-send settlement before one telephony attempt for every `sent`/`failed`/`blocked`/`stale` outcome, with a deferred response proving all destination, Transfer/Transfer Conference, and Merge telephony spies remain uncalled while pending and each begins exactly once afterward, wrap-up-success bookkeeping before response send, one `wrapupCall -> completeWrapupWithSummary -> task.wrapup` path for both eligibility branches, normalized wrap-up/response results and reason clearing, defensive zero-unhandled-rejection handling at the component boundary, the exact exported `Promise` signature with compile-time rejection of a `void` supplier, canonical `AISummaryFeedback`-derived post-call feedback typing, migration coverage, stable host props, and unchanged ineligible call-control UI behavior. Because the fan-out tasks have disjoint file lists, they require no scheduling edge between one another. + +## Change: Host Status Callback and Existing Web Component + +**Coverage and outcome.** This change covers REQ-007, REQ-010, AC-08, AC-09, and AC-14. DAG task: `D7-host-status-callback`. It adds one optional JavaScript function property to the existing `widget-cc-call-control` tag and no event/attribute/custom-element alternative. That single property emits status for initiating mid-call, receiving mid-call, and post-call lifecycles, including AC-08's content-free `response-failed` report. `widget-cc-ai-assistant` remains a rendering surface only and its existing host contract is unchanged. + +**Type and files.** `packages/contact-center/task/src/task.types.ts` retains the existing mapped utility portion of `CallControlProps` unchanged, adds and exports `AISummaryStatusDetail`, and adds only `onAISummaryStatusChange?: (detail: AISummaryStatusDetail) => void` to the existing final intersection. The following block is the illustrative resulting expansion, not an instruction to rewrite the retained declaration: + +```typescript +export type AISummaryStatusDetail = + | {kind: 'mid-call' | 'post-call'; state: 'available' | 'unavailable'} + | {kind: 'post-call'; state: 'submitted' | 'response-failed'}; + +export type CallControlProps = { + [K in + | 'onHoldResume' + | 'onEnd' + | 'onWrapUp' + | 'onRecordingToggle' + | 'callControlClassName' + | 'callControlConsultClassName' + | 'onToggleMute' + | 'conferenceEnabled' + | 'consultTransferOptions']?: ControlProps[K]; +} & { + onAISummaryStatusChange?: (detail: AISummaryStatusDetail) => void; +}; +``` + +The mapped optional-key form is shown only to make the resulting public surface readable; the normative edit is the new status-detail export and the one new intersection member above. + +`task/src/CallControl/index.tsx` remains wrapped in `observer` and calls `store.getPendingAISummaryStatusTransitions()` during render, not only from an effect. The accessor reads the store's observable immutable-array field, so an append changes the returned array identity and establishes the MobX dependency even when no unrelated prop or view-model field changes. A `useEffect` includes that rendered snapshot and `onAISummaryStatusChange` in its dependency array. When the callback is present, the effect iterates every snapshot entry in ascending sequence order; for each entry it first calls `acknowledgeAISummaryStatusTransition(sequence)` and, only when that exact-head acknowledgement succeeds, invokes `onAISummaryStatusChange(transition.detail)` inside a local `try/catch` in that same effect. The queue has therefore advanced before any host code runs. The local catch discards a synchronous host throw, does not delegate it to the repository `ErrorBoundary`, never retries that sequence, and continues the loop so later snapshot entries are acknowledged and offered once. If a callback synchronously causes another transition, the immutable queue identity changes and `observer` schedules a later render/effect for the newly appended tail rather than mutating the snapshot being drained. + +No mount-time sequence seeding or direct `mobx.reaction` is used. Two transitions appended inside one MobX/React batch remain two entries, so one render/effect drains both exactly once in sequence order. If CallControl unmounts before an effect can acknowledge an entry, or a transition is recorded while it is unmounted, the store retains that entry through ordinary keyed cleanup and the next same-session mount drains it. Once acknowledged, removal from the queue prevents replay on a same-agent remount, re-render, or callback identity change. An omitted or removed callback performs no acknowledgement, so its same-session backlog remains pending; global sign-out/session cleanup is the explicit discard boundary. This queue-plus-acknowledgement rule is the complete delivery contract and does not depend on React commit count. + +Initiating and post-call request/submission settlements are recorded by the store calls originating in CallControl. A receiving-agent content settlement is recorded by the store's Task listener even though `@webex/cc-ai-assistant` renders the card; any same-session CallControl with the callback mounted now or mounted later forwards that queued transition through the same property, so no AI Assistant-to-task dependency or second host surface is introduced. Rendering the receiver card is therefore independent of callback registration. If a host mounts only `widget-cc-ai-assistant`, it observes the receiver UI and no host status callback; the content-free transition remains pending only until a CallControl callback consumes it or global session cleanup discards it. Only a freshness-accepted mapped settlement advances the sequence; request start, owner/interaction boundary bookkeeping, stale settlement, edit/copy/feedback, and ordinary re-render do not. Because the transition is content-free and independent of retained summary bodies, `response-failed` remains observable after ordinary keyed payload cleanup and while CallControl is temporarily absent. + +The inspected baseline `packages/contact-center/task/src/index.ts` contains only component value exports and does **not** already re-export `CallControlProps`; D6b does not edit that entry file. D7 therefore adds exactly one new statement, `export type {AISummaryStatusDetail, CallControlProps} from './task.types';`; it neither duplicates nor replaces an existing type export and adds no runtime symbol. Because the custom element is published by the aggregator package, `packages/contact-center/cc-widgets/src/index.ts` additionally adds exactly `export type {AISummaryStatusDetail} from '@webex/cc-task';`, also as a type-only re-export. A TypeScript host depending solely on `@webex/cc-widgets` can consequently type its handler without importing `@webex/cc-task` or using `any`. + +The repository pins `@r2wc/react-to-web-component` 2.0.3 and the lock resolves its `@r2wc/core` dependency to 1.1.0. The inspected core implementation places every configured prop in `observedAttributes`, derives `on-aISummary-status-change` with `toDashedCase` (normalized by an HTML document to `on-aisummary-status-change`), parses a `function` attribute by looking its string value up on `window` and then `global`, and reflects a JavaScript function assignment to the attribute through `Function.name`. D7 must therefore **not** add `onAISummaryStatusChange: 'function'` to the r2wc props map. + +Instead, `cc-widgets/src/wc.ts` keeps the existing `WebCallControl` r2wc prop map byte-for-byte for legacy props and passes a typed React adapter to r2wc. A module-private `WeakMap` stores only the optional handler and its in-process subscribers. The adapter receives r2wc's existing `container` reference, reads that map with `useSyncExternalStore`, removes its subscription on React unmount, and passes the current handler to `CallControl`; it adds no DOM event. A typed subclass of the generated constructor defines the `onAISummaryStatusChange` getter/setter: assigning a function or `undefined` updates the WeakMap and notifies current subscribers without calling `setAttribute`, while any other runtime value is rejected as a type error. Setting the property before connection is observed on first mount, replacing/removing it updates the mounted adapter, and reconnecting the same element preserves ordinary JavaScript-property semantics without retaining a detached element after garbage collection. The final constructor's `observedAttributes` therefore excludes both `on-aisummary-status-change` and the undashed lowercase `onaisummarystatuschange`; setting either spelling is inert and performs no global-name lookup. `WebAIAssistant` and `IAIAssistantProps` are byte-for-byte unchanged. The task and cc-widgets module specifications record the property-only public surface; no new tag, event, attribute, or bundle entry is added. + +**Transition mapping and failure behavior.** A freshness-accepted initiating request with renderable typed/plain content records exactly one mid-call `available`; its current generation failure, malformed result, or unsupported result records exactly one mid-call `unavailable`. An accepted receiving content event with a renderable adaptive card records exactly one mid-call `available`; its current malformed/unsupported/unavailable settlement records exactly one mid-call `unavailable`. A freshness-accepted post-call generation records exactly one post-call `available` or `unavailable`; response fulfillment records exactly one `submitted`; its one failed attempt records exactly one `response-failed`. Authorization denial, capability-disabled omission, and initialization failure admit no request/event lifecycle and record **no** status transition while the surface remains hidden. An interaction switch or ownership-generation change also records **no** transition by itself: a same-agent successor carrying visible content does not repeat `available`, a cross-agent successor reset does not synthesize `unavailable`, and a late predecessor settlement records nothing. Only the first later freshness-accepted request/event settlement for that successor records the mapped state above. Ordinary re-render, counter/edit/copy/feedback changes, hold/resume, same view revision, and stale settlement likewise create no transition. These rules expose no identifiers. + +A throwing host callback is contained by the effect-local `try/catch` described above, not by `ErrorBoundary`. Because exact-head acknowledgement occurs before entering host code, the throwing sequence is delivered at most once across subsequent renders/remounts; the catch adds no log, metric, error payload, or vocabulary, cannot roll back store state or reject an SDK Promise, and does not prevent the effect from draining later acknowledged entries. + +The detail object is built as an allowlisted literal. Summary text/card, customer data, interaction/agent ID, counters, feedback, timestamps, generations, and raw errors are structurally absent. Omitting the property invokes no callback and acknowledges nothing; the ordinary observer/effect lifecycle leaves pending transitions in the store until a same-session callback or the global privacy boundary handles them. The adapter's only subscription is internal to the mounted React bridge and is cleaned up on unmount. Not applicable - the property-only bridge adds no persistence, HTML-attribute serialization or parsing, global-name resolution, telemetry, configuration, migration, DOM event, or external listener. + +**Tests.** D7's compile-time exact-union assertions instantiate all six valid detail literals: mid-call `available`, mid-call `unavailable`, post-call `available`, post-call `unavailable`, post-call `submitted`, and post-call `response-failed`. Separate negative type assertions require `{kind: 'mid-call', state: 'submitted'}` and `{kind: 'mid-call', state: 'response-failed'}` to fail compilation. Runtime tests cover initiating mid-call, store-routed receiving mid-call, and post-call transitions; authorization denial, capability-disabled omission, and initialization failure producing zero transitions; interaction switch, cross-agent reset, and same-agent carried-content successor producing zero boundary transitions; the successor's first accepted settlement producing exactly its mapped transition while a late predecessor produces none; the named `observable queue mutation alone fires the callback without a prop change` MobX dependency case; the named `available then unavailable in one batch before a React commit emits both once in sequence order` no-drop case; and the named `response-failed recorded while CallControl is unmounted is delivered once on same-session remount` AC-08 case. The throw-containment test proves exact-head acknowledgement is visible before invocation, the invocation is wrapped by the effect's local `try/catch` rather than `ErrorBoundary`, a persistently throwing handler is called once for that sequence across subsequent renders/remount, the widget remains mounted, and the drain continues to later entries. Additional cases cover same-sequence/re-render and acknowledged same-agent remount deduplication, unacknowledged retention through keyed cleanup, callback omission/removal and later drain, global-cleanup discard with monotonic next sequence, and recursive rejection of forbidden callback keys. + +`cc-widgets/tests/wc.ai-summary.ts` imports `AISummaryStatusDetail` only from the aggregator's public `src/index.ts`, assigns a typed handler with no `any`, and proves the emitted declaration exposes that type-only symbol. It also pins the reviewed r2wc behavior guard: the final call-control constructor does not observe `on-aisummary-status-change` or `onaisummarystatuschange`; setting either attribute to the name of an arbitrary `globalThis` function before or after connection never resolves or invokes it; assigning the JavaScript property forwards the handler without creating either attribute; replacing/removing the property updates delivery; reconnecting retains the assigned property without leaking a subscriber; and `WebAIAssistant` registration/public props remain unchanged. `cc-widgets/tsconfig.test.json` extends the production project only to include the test for no-emit type-checking, and `cc-widgets/package.json` adopts the repository-standard `tsc --project tsconfig.test.json && ... jest` unit script. Acceptance runs the focused `@webex/cc-task` unit/type test, builds `@webex/cc-task` and its `@webex/cc-widgets` consumer, and invokes the focused cc-widgets unit command, whose type-check precedes Jest, so both the public declaration path and `wc.ts` are checked against fresh task declarations rather than stale distribution output. + +## Change: Deterministic UX and Release Validation + +**Coverage and outcome.** This change covers PRE-001, REQ-009, REQ-011, REQ-012, PRE-005, PRE-006, UX-001, UX-002, UX-003, UX-004, UX-005, UX-006, UX-007, UX-008, UX-009, UX-010, JNY-001, JNY-002, AC-12, AC-15, and AC-16. DAG tasks: `D8-browser-ux-validation`, `D8b-restore-publishable-sdk-dependency`, and `D9-release-validation`; D0a is the prerequisite evidence owner. D3 is the implementing owner for UX-002 through UX-010's shared generating, editing, feedback, copy, and error presentation, and D6 is the implementing owner for their post-call composition and state transitions. D8 is an implementation-plus-evidence task limited to the validation dependency/lock, sample-host bridge, Playwright harness/configuration, and typed visual registry; it edits no product source, public prop, SDK call, component unit test, or module specification. Before evidence is recorded, D8 reruns the complete `@webex/cc-task` and `@webex/cc-ai-assistant` unit suites plus focused `@webex/cc-components` coverage. A product mismatch returns to D3/D6 or the other named D2-D7 implementation owner for correction under that owner's acceptance, after which D8 reruns in full. D9 adds a read-only verifier and runs all release gates; it edits no product or evidence files. D8 is the owning check for REQ-009 / AC-12 keyboard focus-successor behavior, semantic focus mapping across late enablement/revocation of both dual layouts, exact `View summary` reachability, WCAG 2 A/AA, 320px wrapping/vertical overflow, and forced-colors behavior. D8b transactionally removes the implementation-only root resolution, updates the preserved store registry dependency to the exact admitted version if necessary, verifies the complete installed file manifest, and installs the publisher's fail-before-write local-protocol preflight. D9 records and verifies `ai-assistant-summary` as the promotion/release target from the requirement; a transient MatrixBuilder worktree branch name is execution metadata and is not mistaken for the final target branch. + +**Files and tools.** `playwright/Utils/aiSummaryUtils.ts` supplies deterministic browser actions/SDK event fixtures; `playwright/tests/ai-summary-test.spec.ts` independently registers the browser journeys and scoped accessibility checks, while `playwright/suites/ai-summary-tests.spec.ts` independently registers the visual/evidence-manager cases. Neither spec imports the other. `playwright.config.ts` conditionally appends the project named exactly `AI Summary Deterministic` only when `AI_SUMMARY_E2E=1`, with exact `testDir: './playwright'`, `testMatch: ['**/suites/ai-summary-tests.spec.ts', '**/tests/ai-summary-test.spec.ts']`, `fullyParallel: false`, no project dependency/global setup, bundled Desktop Chromium, `locale: 'en-US'`, `colorScheme: 'light'`, and `reducedMotion: 'reduce'`. The same configuration change adds exact `testIgnore: ['**/suites/ai-summary-tests.spec.ts', '**/tests/ai-summary-test.spec.ts']` to every pre-existing project object: the OAuth setup project and every `USER_SETS`-derived project. The conditional AI Summary project does not inherit that exclusion. Consequently, neither AI Summary spec is collected by a pre-existing project, even if a dynamic `TEST_SUITE` value would otherwise select one, while the flagged project collects each exactly once. Its flagged acceptance run fixes `--workers=1` and reuses the one existing top-level local sample-app `webServer`; it does not declare or start a second server. Existing CI, default full-suite, named-project, and sharded invocations remain unchanged and omit `AI_SUMMARY_E2E=1`; the conditional project is absent and the pre-existing-project exclusions keep both AI Summary specs undiscovered. Before application code runs, `page.addInitScript` installs the versioned `window.__WEBEX_CC_AI_SUMMARY_E2E__` bridge and the suite navigates with exact `ai-summary-e2e=1`; `widgets-samples/cc/samples-cc-react-app/src/aiSummaryE2E.ts` validates it during initial render and `App.tsx` passes its Webex-shaped member to existing `store.init({webex})`. Missing/invalid seam state throws before render, and all non-local HTTP(S) requests are rejected, so there is no bundled-module rebinding, OAuth, Webex, calling-sample, external backend dependency, or network fallback. This is explicit harness extension, not a claim that accessibility/visual support already exists: from the repository root D8 runs `yarn add -D -E axe-core@4.10.2`; that Yarn command, rather than a hand edit, writes exact dev dependency `axe-core: "4.10.2"`, rewrites the root workspace descriptor/checksum in `yarn.lock`, and refreshes the active linker install state even though the archive already resolves transitively at 4.10.2. D8 acceptance then begins with `yarn install --immutable` before any axe resolution or browser test. The browser helper resolves the package-owned runtime with `require.resolve('axe-core/axe.min.js')` through the active Yarn resolver, reads those bytes with `node:fs`, injects them with `page.addScriptTag({content: axeSource})`, asserts the typed `window.axe` global, and invokes `axe.run` against the supplied summary/panel include context with `runOnly: {type: 'tag', values: ['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa', 'wcag22a', 'wcag22aa']}`. The threshold is exactly zero violations: any `violations.length !== 0` fails regardless of impact; the full result, including the collection of checks requiring further review, is retained in `accessibility.json`. No screenshot is copied into product assets. D8 begins with `ai-summary-sdk-lock.js verify-ux`, which re-resolves every receipt-listed `.ccwidgets/**` path strictly beneath the target-branch worktree, rejects missing/non-regular/symlinked/escaping paths and all parent/common-checkout or environment fallbacks, and compares every raw hash plus the three aggregate identities without writing or resealing the receipt. The browser helper reads the immutable `.ccwidgets/**` PNG target paths only from that verified receipt. It requires an executable absolute `MB_FLOW_BIN`, derives the common-checkout root from the target worktree's absolute `git rev-parse --git-common-dir`, requires the visual plan to name that root's `.matrix/results/prog-mini-js/ux-visual/implementation` tree, and uses argv-based child processes with fail-closed timeout/exit/output handling. UX-007 has one explicit evidence-key normalization: the source-declared screenshot/variant `S07-UX` / `desktop-light` is hashed under UX-007 exactly as declared, while the canonical render/comparison/receipt key is `S07` / `dl`; no second screenshot artifact is permitted. D8's file list is limited to the validation dependency/lock, the sample-app-only bridge and its App consumer, Playwright harness/configuration, and typed visual registry. A product mismatch fails D8 and returns to its implementing owner (D2b-D7, specifically D3/D6 for UX-002 through UX-010) for a focused correction before D8 reruns; D8 itself edits no product source, component unit test, or module specification. D8b transactionally modifies the root `package.json`, `packages/contact-center/store/package.json`, and `yarn.lock`, restoring all three on a lock/install/final-verification failure, and adds the preflight plus focused coverage in `tooling/src/publish.js` and `tooling/tests/publish.js`. `tooling/src/verify-ai-summary-release.js` is a CommonJS, read-only verifier; its Jest test proves fail-closed behavior. Configured registry access or a complete local Yarn cache for the exact axe addition, an explicit executable `MB_FLOW_BIN`, and Playwright Chromium are D8 DAG prerequisites, not silently assumed daemons or machine-specific paths. + +**Single UX resolution root.** D8 names `TARGET_WORKTREE_ROOT="$(git rev-parse --show-toplevel)"` before `verify-ux` and supplies it through the CLI's required `--widgets-root`; the receipt must contain `ux.resolutionRoot: "."`. After verification, every MatrixBuilder call launched from `COMMON_CHECKOUT_ROOT` receives `--ux-input-root "$TARGET_WORKTREE_ROOT"` as an argv pair. The prepared visual plan records the canonical absolute `uxInputRoot`, the thirty target-root-relative paths, their freshly recomputed hashes, and the aggregate identities. `recordAISummaryVisual` requires that exact plan root and passes the same argument to plan/export/compare children; final validation receives it again. A child that omits the argument, resolves a `.ccwidgets` path relative to its common-checkout cwd, records a different root, or observes post-plan drift fails before Playwright renders or copies `target.png`. The absolute value is runtime handoff data only and is not committed. + +**Deterministic capture environment.** The conditional Playwright project pins ordinary interaction journeys at device scale 1; the screenshot-evidence suite uses the descriptor-declared source scale 2 alongside the sealed CSS viewport, locale, color scheme, and reduced-motion settings width and height, and the helper asserts both `window.devicePixelRatio === 1` and the requested CSS viewport before capture. These widths are host-fixture inputs to the one responsive production component: S02 and S10 use 440x620, S03 uses 442x620, and S04 through S09 use 400x620. The component must not change width when reason selection collapses, summary lifecycle changes, or capability changes. Viewport/root geometry has zero CSS-pixel tolerance and sealed target PNG bytes/dimensions match exactly. The comparator then applies the closed semantic-token region contract: structure, mapped boxes, text, token identity, contrast, forced colors, and foreground coverage are independently blocking; `<=0.5%` is evaluated only for each color-independent foreground coverage mask and for the non-empty literal RGB mask, never for tokenized flat fills or the whole root. Font loading remains inside the existing local sample-app server: every text or icon font used by the summary subtree must be a repository- or installed-package-owned asset emitted by the sample app's existing webpack font rule and served from that same origin. `ai-summary-visual-cases.ts` owns a closed font manifest of family, style, weight, sample glyphs, emitted URL path, source-package path, and SHA-256. Request interception rejects every non-local HTTP(S) request and also rejects a local font response that is not in that manifest, has different bytes, or fails to decode; a CDN allowlist and system-font fallback are not admissible. + +Immediately before every screenshot, `recordAISummaryVisual` invokes one bounded `awaitAISummaryCaptureReady` helper. It awaits `document.fonts.ready`, requires `document.fonts.status === 'loaded'`, checks every manifest face and sample glyph through `document.fonts.check`, and verifies that the observed font response hashes match the manifest. It then freezes only registry-declared continuous animations at their declared deterministic time, awaits every other summary-subtree animation to reach `finished` or `idle`, and observes two subsequent `requestAnimationFrame` ticks with no new running or pending animation. The readiness wait has a 10-second ceiling; timeout, missing face, fallback, hash drift, font decode error, unexpected animation, viewport drift, or device-scale drift fails before `page.screenshot` and produces no passing comparison. Each successful `comparison.json` records viewport, device scale, browser revision, font-manifest hash, loaded font-response hashes, and `fontsReady` / `animationsSettled` booleans, and D9 re-verifies those fields. + +The helper's public test surface is closed over the fixture and evidence registries rather than accepting open strings or cross-context Playwright objects: + +```ts +type AISummaryFixtureGroup = keyof typeof aiSummaryFixtures; +type AISummaryFixtureSelection = { + [Group in AISummaryFixtureGroup]: Readonly<{ + group: Group; + member: Extract; + }>; +}[AISummaryFixtureGroup]; +export type AISummaryBrowserScenario = readonly [ + AISummaryFixtureSelection, + ...AISummaryFixtureSelection[], +]; + +export type AISummaryAxeInclude = Readonly<{include: readonly [selector: string]}>; +export type UXSourceId = + | 'UX-001' | 'UX-002' | 'UX-003' | 'UX-004' | 'UX-005' + | 'UX-006' | 'UX-007' | 'UX-008' | 'UX-009' | 'UX-010'; +export type UXScreenshotId = + | 'S01' | 'S02' | 'S03' | 'S04' | 'S05' + | 'S06' | 'S07-UX' | 'S08' | 'S09' | 'S10'; +export const uxScreenshotBySource = { + 'UX-001': 'S01', + 'UX-002': 'S02', + 'UX-003': 'S03', + 'UX-004': 'S04', + 'UX-005': 'S05', + 'UX-006': 'S06', + 'UX-007': 'S07-UX', + 'UX-008': 'S08', + 'UX-009': 'S09', + 'UX-010': 'S10', +} as const satisfies Readonly>; +export type UXVisualPair = { + [Source in UXSourceId]: readonly [ + sourceId: Source, + screenshotId: (typeof uxScreenshotBySource)[Source], + ]; +}[UXSourceId]; + +export declare function installAISummarySDKMock( + page: Page, + scenario: AISummaryBrowserScenario, +): Promise; +export declare function driveAISummaryState( + page: Page, + stateId: AISummaryBrowserStateId, +): Promise; +export declare function auditAISummaryAccessibility( + page: Page, + context: AISummaryAxeInclude, +): Promise; +export declare function recordAISummaryVisual( + page: Page, + ...pair: UXVisualPair +): Promise; +``` + +`aiSummaryFixtures` is D1's single readonly export, so `AISummaryFixtureGroup` is exactly its declared `featureEnablement`, `initiatingMidCall`, `receivingMidCall`, `postCall`, `malformed`, `errors`, `ordering`, `transfers`, `conferences`, `feedback`, `counters`, `wrapUp`, and `postWrapUpSend` groups. The discriminated non-empty selection tuple lets one browser scenario compose multiple exact D1 members while rejecting an unknown group or a member from the wrong group. Named browser-scenario constants for timeout, stale timestamp, ownership change, and response failure use `satisfies AISummaryBrowserScenario`, so each required REQ-012 path is compile-time connected to its D1 fixture member. + +`AISummaryBrowserStateId` remains the closed union of the ten declared journey state IDs plus named structural-only states. `auditAISummaryAccessibility` accepts only the serializable single-selector include object; callers wait for the corresponding `Locator` but pass its stable selector, never the `Locator`, across `page.evaluate`. In the page, the helper validates that the selector is syntactically valid and resolves to exactly one connected `Element`, then passes `{include: [selector]}` to `axe.run` with the exact six-tag WCAG rule selection. An invalid, missing, detached, or ambiguous root throws and fails the audit; it never drops the context or widens the run to `document`. The helper resolves `axe-core/axe.min.js` with `require.resolve` under the active Yarn linker, reads it through `node:fs`, injects the resulting `axeSource` bytes after navigation for the current document, serializes the complete result, and throws when the violation count is nonzero; it does not rely on a Playwright wrapper or a pre-existing page global. + +The `UXVisualPair` union is the compile-time source-to-declared-screenshot pairing and the helper repeats that check at runtime before creating a result directory or invoking MatrixBuilder. In particular, UX-007 accepts only `S07-UX`; after verifying that declared source identity, the existing registry normalization alone emits canonical `S07` / `dl` render, comparison, and evidence keys. The declared S01-S06, S07-UX, and S08-S10 keys all receive real captures and measured differences; UX-010 / S10 additionally retains structural acceptance for its requirement-owned enabled completion override and cannot reinstate a disabled-button acceptance baseline. The two specs export no production symbol and have non-overlapping top-level registrations. Together with every pre-existing project's exact two-path `testIgnore`, only the flagged deterministic project discovers them, each exactly once, and no scenario runs under OAuth or a `USER_SETS` project. + +The SDK takeover is a concrete sample-host injection seam, not an attempted rebinding of the bundled `@webex/contact-center` module. D8 adds `widgets-samples/cc/samples-cc-react-app/src/aiSummaryE2E.ts`, which declares and validates the versioned `window.__WEBEX_CC_AI_SUMMARY_E2E__` bridge, and updates `widgets-samples/cc/samples-cc-react-app/src/App.tsx` to resolve it during the first render whenever the URL contains the exact opt-in `ai-summary-e2e=1`. The bridge's `webex` member is typed from the existing `store.init` host-supplied `{webex}` parameter rather than with `any` or a second SDK shape. `installAISummarySDKMock(page, scenario)` uses `page.addInitScript` before navigation to install bridge version `1` with only the selected synthetic D1 Webex/CC methods, event emitters, deterministic Promise ordering, and fake-clock settlement. In opt-in mode, a missing bridge, wrong version, or malformed `webex` member throws before the sample app renders or invokes any SDK initializer; the captured `pageerror` fails the scenario. After navigation, the helper also asserts the opt-in marker and bridge identity before clicking Init Widgets. The sample host then calls the already-existing `store.init({webex: bridge.webex})` path, which owns event setup and registration; it never calls bundled `Webex.init`, OAuth, or a network fallback in this mode. Outside the exact opt-in URL, the bridge is ignored and the sample app retains its ordinary initialization behavior. This sample-only bridge adds no production widget, store, SDK, or published package surface. + +**Browser and visual control flow.** Tests install deterministic SDK mocks before widget initialization, drive both roles/journeys in a real browser, and collect uncaught errors/unhandled rejections. Before those tests or any MatrixBuilder preparation, D8 verifies all ten D0a-sealed target-worktree source triples in `design/default/sdk_package_lock.json` without changing the receipt. If that gate fails, D8 creates no render directory or structural receipt and D9 treats AC-15 as unmet. MatrixBuilder resolution is portable and singular: acceptance and `playwright/Utils/aiSummaryUtils.ts` require a non-empty absolute `MB_FLOW_BIN` whose real path is a regular executable, and fail before preparation, navigation, or output creation when it is unset or invalid. Every Node-side invocation uses `execFile`/`spawn` with that executable and an argument array, never a shell command string or a repository-committed machine path. + +The target worktree root is `git rev-parse --show-toplevel`. From that cwd, D8 resolves `git rev-parse --path-format=absolute --git-common-dir`, requires the canonical result to be a `.git` directory, and defines its parent as the common-checkout root. Preparation, plan lookup, export, comparison, and final validation use that common-checkout root as cwd, and the plan's render directory must canonicalize to exactly `/.matrix/results/prog-mini-js/ux-visual/implementation`; an absent, escaping, or conflicting render directory fails before a write. This common-checkout derivation is only the output/control-plane rule: the sealed `.ccwidgets/**` inputs resolve exclusively as `TARGET_WORKTREE_ROOT + receipt-relative-path`. Only after the post-stage target rehash and root gates pass does acceptance invoke `$MB_FLOW_BIN prog-mini-js prepare-ux-visual-comparison --ux-input-root "$TARGET_WORKTREE_ROOT"`, and the helper invokes `$MB_FLOW_BIN prog-mini-js ux-visual-plan --phase implementation --ux-input-root "$TARGET_WORKTREE_ROOT"`; export, compare, and final validation repeat the same explicit argv pair. Neither cwd nor an environment variable may substitute for the argument, and no command invents a target or comparison policy. + +For each captured tuple declared by S01-S06, S07-UX, and S08-S10, Playwright writes `rendered.png`, `diff.png`, `comparison.json`, `dom.json`, and `accessibility.json` beneath `///`, exports the sealed target with `$MB_FLOW_BIN prog-mini-js export-ux-screenshot --source --screenshot --phase implementation --ux-input-root "$TARGET_WORKTREE_ROOT" --output //target.png`, and invokes `$MB_FLOW_BIN prog-mini-js compare-ux-images --source --screenshot --phase implementation --ux-input-root "$TARGET_WORKTREE_ROOT" --rendered --diff --report ` with the exact plan-approved arguments. The UX-007 tuple is addressed only as canonical `S07` / `dl` at these render and receipt boundaries; its receipt also records the declared `S07-UX` / `desktop-light` aliases and proves both names resolve to the one sealed UX-007 screenshot hash. The latter three paths are absolute or common-checkout-root-relative paths contained by the plan's render directory; the export output alone is relative to that directory. Each helper child has a 60-second timeout, bounded stdout/stderr capture, and one awaited close path. Spawn failure, signal exit, timeout, non-zero exit, missing output, invalid root, or invalid report rejects the Playwright test; bounded sanitized stderr is attached to the failed test and is never converted into a clean visual result. S10 also writes structural classification/DOM/accessibility evidence with reason `requirement-availability-override`, rehashes its sealed triple from the same explicit root, asserts the source-derived error hierarchy/copy, and proves the committed-reason primary is enabled and completes with `response: 'not-required'`. + +The two Playwright specs own only per-tuple render/comparison/classification/DOM/accessibility artifacts and never invokes `validate-ux-visual-comparison`. The surrounding D8 or D9 acceptance command is the single validator owner for its acceptance attempt: after a successful Playwright process it invokes `$MB_FLOW_BIN prog-mini-js validate-ux-visual-comparison --ux-input-root "$TARGET_WORKTREE_ROOT"` exactly once from the common-checkout root, and that invocation produces the authoritative visual gate receipt consumed by the remainder of that attempt. These generated, gitignored `.matrix` acceptance artifacts are not implementation source files and therefore are not DAG `files` entries. Each acceptance attempt rehashes the applicable source JSON/text/PNG against the receipt and renders at the sealed CSS viewport/device scale. A mismatch fails and returns to the named implementation task; after that separate task's focused correction and tests, D8 may start another complete acceptance attempt, for at most five attempts. A screenshot-backed state passes only when its structural, behavior, accessibility, semantic-token identity/contrast/forced-colors, foreground-coverage, and region-masked literal gates all pass; `<=0.5%` is not a whole-image gate and applies only to each declared foreground-coverage result and a non-empty literal RGB denominator. Implemented states with no screenshot receive the exact structural-only receipt named in the closed inventory table. Across every screenshot-backed and structural-only state, DOM/accessibility copy evidence enumerates visible strings, accessible names, descriptions, and tooltips and fails if any rendered string matches `/^%.*%$/`. Browser assertions also require the expanded and collapsed post-call states to share an 80vh shell at 320px with independent reason/content scrolling, no horizontal overflow and visible headings/actions, and require copy hover/confirmation to leave the primary visible label and accessible name unchanged. + +The structural registry gives `post-call:feedback-pending` and `post-call:feedback-not-confirmed` their own scenario IDs and records the exact status token, message ID, visible string, both feedback-control IDREFs, enabled/read-only state, and absence of live-region semantics. The named browser scenarios are `Pending submission describes both post-call feedback controls without a live region` and `Submission not confirmed describes both read-only post-call feedback controls without a live region`. It also gives `mid-call:transfer-summary-panel` the named scenario `Transfer summary panel exposes the product-confirmed structural heading`; that record must contain `aiSummary.midCall.transferHeading`, the product-confirmed exact string, a non-empty confirmation reference, the action-parity rationale, and no `extrapolatedFrom` or UX-001 copy-authority claim. Missing confirmation leaves D8 failed. Across UX-002 through UX-010, the separate `Search topic and Complete Wrap-Up remain canonical across visual states` scenario checks visible text, accessible names, placeholders, and tooltips after source normalization; hover, selected, copied, generating, editing, and error states cannot select a different value. + +**Bounded refine-loop terminal condition.** One visual attempt covers the complete registry and has a fresh attempt identifier; a prior attempt's gate receipt can never satisfy the current run. Attempts one through four may retain explicitly failing comparison diagnostics and return the mismatch to the named implementation owner, but they write no visual-gate receipt. If any screenshot-backed state has a non-empty literal RGB result or foreground-coverage result above `0.5%`, an undeclared/changed semantic-token mask, or any hierarchy, mapped-box, token identity, contrast, forced-colors, interaction, focus, exact-copy, overflow, font-readiness, animation-settlement, viewport, or device-scale failure on attempt five, D8 exits nonzero and surfaces that state as a hard visual-gate failure. It writes no visual-gate receipt of any status, never marks a failed `comparison.json` complete, and never reclassifies a screenshot-backed state as structural-only. `validate-ux-visual-comparison` writes its receipt transactionally only when every state passes in the current attempt; D9 requires that current successful receipt and rejects exhausted, missing, stale, failed, or downgraded evidence. + +For UX-007, the alias pair belongs to the generated visual comparison record (`comparison.json` / the MatrixBuilder evidence receipt), not to the `sdk_package_lock.json` source-entry schema. Before writing canonical `S07` / `dl` evidence, D8 resolves the declared `S07-UX` / `desktop-light` tuple and requires its target hash to equal the already sealed UX-007 screenshot hash. + +**Release verifier and publish preflight.** D9 validates the required promotion target `ai-assistant-summary`, requirement, design, DAG, the combined SDK/UX input receipt, target-worktree UX manifest/source/screenshots, installed sealed development dependency state, package dependency direction, exact changed-file allowlist, module-spec currency, privacy scans, task acceptance receipts, browser/a11y/visual records, and zero skipped mandatory checks. For axe, D9 requires the exact root descriptor, parses the Yarn lock resolution/checksum, resolves `axe-core/axe.min.js` through the active install state, and hashes that resolved package artifact; it never asserts a `node_modules` directory layout. For SDK receipt, tarball, complete packed-file manifest, declaration, installed-artifact, and lock validation, `verify-ai-summary-release.js` imports D0's exported `verifySdkPackage` and implements `verifySDKReceipt(context): Promise` only as a thin adapter that awaits `verifySdkPackage({widgetsRoot: context.repositoryRoot, resolutionMode: 'development'})`. It neither reparses that receipt contract nor reimplements any SDK hash or lock rule; the standalone lock-tool CLI and the D9 binary therefore exercise the same fail-closed implementation. D9 independently rehashes requirement/design/DAG and UX evidence, scans root `resolutions` plus every dependency map in every non-private workspace manifest, and parses the hash-bound packaging-gate receipt and reopens the receipt-listed tarballs to read actual packed `package/package.json` files from exactly `@webex/cc-store`, `@webex/cc-components`, `@webex/cc-task`, `@webex/cc-ai-assistant`, `@webex/cc-widgets`, and `@webex/test-fixtures`. Every `@webex/contact-center` occurrence in a packed `dependencies`, `optionalDependencies`, or `peerDependencies` map must be a plain registry-resolvable SemVer version/range with no local/path/workspace alias; `@webex/cc-store` must equal its unchanged source-workspace registry descriptor. The D0 private-root resolution and sealed tarball remain permitted repository-development state; no publishable source or packed manifest may reference them, and the tarball must be absent from all six packlists. The development lock must still resolve the sealed artifact exactly. Independently, the D8b change to `tooling/src/publish.js` performs the local-protocol scan for the root resolution and every publishable Contact Center manifest before it removes `stableVersion`, rewrites any version, or invokes `yarn npm publish`; a violation exits nonzero with zero writes and zero publish calls. D9 also requires release evidence to classify D6b's exported always-resolving `Promise` `wrapupCall` signature as breaking for both `@webex/cc-task` and `@webex/cc-components`, with semantic-release breaking-change metadata that produces a SemVer-major release; an additive/minor-only classification fails release validation even though the custom-element change is additive. It rejects a pending receipt member, any UX source path outside the verified worktree, live UX hash drift, an absent or inconsistent UX-007 `S07-UX` / `desktop-light` -> `S07` / `dl` mapping, or an external dependency/D0/D8b state that is not accepted. `verify-ai-summary-release.js` additionally exports synchronous, read-only `verifyUXEvidence(context): void`, `verifyPublishableDependencies(context): void`, `verifyPrivacyAllowlist(context): void`, `verifyDAGTraceability(context): void`, and `verifyGateReceipts(context): void`, plus asynchronous `runReleaseVerification(context): Promise`; `context` contains explicit repository root, requirement/design/DAG/UX-manifest paths, and result directory. `runReleaseVerification` awaits `verifySDKReceipt` before the local checks. The D9-local checks and Jest suite never shell out or write; `verifyGateReceipts` and `verifyPublishableDependencies` only parse and assert current, hash-bound receipts emitted by the separately executed acceptance gates, including both Playwright specs and the six-workspace packaging gate. The delegated D0 seam remains read-only, may run its already-defined awaited installed-package inspection, and never reads the SDK source audit path. The asynchronous CLI catches once to set its exit code. The CLI runs after the acceptance command separately executes build/type/unit/style, both browser-spec/a11y/visual, publisher-preflight, and packaging gates; no Jest case recursively invokes Yarn, Playwright, MatrixBuilder, packing, publishing, or the release CLI. It never edits source or fabricates manual evidence. Failures name only file/gate/hash categories. Not applicable - it adds no production configuration, runtime storage, network API, telemetry, migration, worker, or service. + +**Release-target and local-evidence proof.** Immediately before D9, the MatrixBuilder flow resolves the target worktree's symbolic HEAD and commit plus the requested promotion target and atomically writes `/.matrix/results/prog-mini-js/release/ai-assistant-summary.json`; the flow gate records the SHA-256 of those exact receipt bytes. The closed version-1 receipt contains `headCommit`, `executionBranch`, `resolvedPromotionTarget`, the path and SHA-256 of `requirement.md`, and sorted path/SHA-256 arrays for the exact changed-file set and every evidence record consumed by D9. D9 rehashes the receipt against the flow gate, rehashes every enumerated file, and directly resolves the target worktree's `.git` indirection, `HEAD`, and loose or packed ref without spawning a process to prove the current branch/commit equals the receipt. It parses exactly one non-empty branch value under `requirement.md`'s `## Branch` heading (currently line 5) and requires `resolvedPromotionTarget` to equal it. `executionBranch` must either equal that target or match the anchored transient pattern `^mb-worktree-(?:design|design_fix)-[0-9]+-[0-9a-f]{8}-[0-9a-f]{8}$`; matching the transient pattern never substitutes for the required resolved promotion target. A missing/stale receipt, wrong target, arbitrary branch, detached or mismatched HEAD, changed-file hash mismatch, or unbound receipt hash fails release validation. + +`verifyUXEvidence` uses the same authenticated changed/evidence manifest to verify both halves of AC-15. It requires all thirty UX inputs to remain regular repository-relative local paths with the D0a-recorded individual and aggregate hashes and requires every visual record to name only those local identities. It AST-inspects executable changed JS/TS and parses structured JSON evidence for remote Figma host/file/node URLs, MCP client/server or tool-call provenance, and file/node-fetch records; any such executable or structured provenance fails even when all local UX hashes match. The four named local MatrixBuilder `figma-json-*` inspection operations and narrative statements of the prohibition are not remote-MCP evidence. The scan performs no network request and does not infer non-use merely from a boolean claim in a receipt. + +D9's UX coverage check requires exactly ten measured source comparison records for declared keys S01-S06, S07-UX, and S08-S10; raw scenario IDs match the harness descriptor, while only UX-007 retains hash-checked S07 / dl alias metadata in local sidecars, plus one declared S10 structural record with `reason: 'requirement-availability-override'` bound to the still-sealed UX-010 triple, and the exact expanded `(stateId, mode, action?)` tuple set in the complete structural-only inventory table—no more and no fewer. That denominator includes the receiving adaptive-card/unrenderable states, hidden authorization/initialization tuple, omitted Consult/Transfer tuples, nine named mid-call treatment extrapolations with exact source/mode bindings, separately actioned pending Consult and Transfer records, unavailable/zero-match/Outcome-absent/feedback/submission/legacy states, and lifecycle/ownership rows. The distinct `mid-call:transfer-summary-panel` record must use `reason: 'no-sealed-screenshot'`, name `aiSummary.midCall.transferHeading`, bind the final exact string to a non-empty product/content-owner confirmation reference, include the action-parity justification, and make no claim that UX-001 supplied, parameterized, or authorized that string. D9 rejects an S10 pixel baseline, a missing enabled-completion assertion, any unsealed/mismatched S10 source, any missing/extra/duplicate table tuple or changed extrapolation binding, broken feedback-description evidence, an absent/unconfirmed/source-claimed Transfer receipt, state-dependent canonical search/completion copy, or any attempt to classify a different sealed state with the override reason. + +**Tests.** D0a owns source-admission failures and the exact-ten-triple atomic seal. D8 begins with read-only missing/drift/symlink/path-escape/aggregate verification and exact UX-007 evidence-key normalization cases, then proves that the unflagged shared config does not enumerate `AI Summary Deterministic`, every pre-existing project carries exact ignores for both AI Summary paths, the `AI_SUMMARY_E2E=1` config enumerates both the journey and visual-manager specs only under `AI Summary Deterministic`, each path is independently collected exactly once, and the flagged project uses the existing top-level server. Compile-time cases accept every valid source/screenshot tuple, require UX-007 / `S07-UX`, reject UX-007 / `S07` and every cross-source mismatch, accept fixture selections only from their discriminated D1 group, and instantiate the timeout, stale-timestamp, ownership-change, and response-failure scenarios. Accessibility cases pass a serializable include selector, reject invalid/zero/multiple/detached matches without calling a document-wide audit, resolve `axe-core/axe.min.js` through the active Yarn resolver without a `node_modules` path, inject the resolved package-owned bytes, run the exact six WCAG tags, fail on one or more violations, pass only at zero, and write every named comparison artifact beneath the declared MatrixBuilder result directory. D8 also verifies that `yarn add -D -E axe-core@4.10.2` generates the exact root declaration and lock descriptor/checksum and that the acceptance-leading `yarn install --immutable` reproduces the install before browser execution. D8b owns packed-manifest registry-descriptor checks, private-resolution and vendored-tarball exclusion from publishable artifacts, atomic packaging receipt generation, and publisher fail-before-write cases for each forbidden local protocol; it does not require registry publication of the staged development version. D0's verifier tests remain the sole owners of missing/tampered SDK receipt, stale complete-file/declaration data, portable audit-path independence, and all-manifest/all-descriptor store lock/install mismatch cases. D9 unit tests mock that exported seam and prove `verifySDKReceipt` invokes it exactly once in `development` mode, awaits it, propagates its content-free rejection, and contains no second receipt/hash/lock implementation; the clean integration case exercises the real delegate. D9-local cases cover a pending gate, linker-neutral axe descriptor/lock/checksum/resolved-artifact verification, forbidden root resolution or publishable-manifest descriptor, a local/path/workspace SDK specifier in any of the six actual packed manifests, a non-registry SDK version/range, a missing/stale/mismatched packed-manifest receipt hash, vendored evidence entering any package packlist, target-worktree UX drift, absent/mismatched `S07-UX` / `desktop-light` -> `S07` / `dl` evidence mapping, visual threshold failure, missing structural receipt, privacy token match, omitted DAG acceptance, skipped suite, and clean all-gates pass. Those D9 unit cases consume synthetic recorded receipts only and assert that no child-process gate is launched from Jest. Four additional named failing fixtures are mandatory: an upstream import that violates the package dependency direction, a touched module whose module specification is stale, a changed path outside the exact DAG/evidence allowlist, and a release receipt whose resolved promotion target is wrong. Each fixture must reach the corresponding real verifier branch and fail; a stubbed or always-pass assertion does not satisfy AC-16. + +D8 additionally names browser cases `unrecovered generation error enables and completes wrap-up as not-required`, `S10 availability override retains exact title and subcopy for every visible error category and enables the stable Complete Wrap-Up control`, `Pending submission describes both post-call feedback controls without a live region`, `Submission not confirmed describes both read-only post-call feedback controls without a live region`, `Transfer summary panel exposes the product-confirmed structural heading`, `Search topic and Complete Wrap-Up remain canonical across visual states`, `no rendered visible or accessible string matches /^%.*%$/`, `Clipboard rejection changes no visible copy state or copied counter and exposes no raw error`, `copy confirmation remains through 1,499 ms and resets at 1,500 ms`, `copy confirmation resets on focus loss`, `second copy clears confirmation and only fulfilled settlement starts a fresh 1,500 ms interval`, `summary replacement resets copy confirmation`, `copy hover and confirmation do not mutate the primary label or accessible name`, `expanded and collapsed post-call layouts have an 80vh shell with bounded list/content scroll regions and visible headings/actions at 320px`, and the three exact last-summary-control fallback scenarios named in the focus-target table. The S10 case separately exercises `disabled`, `offline`, `unavailable`, `empty`, `timeout`, and `generic`, verifies the common exact description as non-diagnostic recovery guidance, and rejects that presentation for hidden authorization/initialization or unsupported content. Geometry cases set S02/S10 to 440x620, S03 to 442x620, and S04-S09 to 400x620 as external host fixtures, require zero CSS-pixel viewport/root drift, and prove the component does not select width from expanded/collapsed or summary state. Raw tokenized colors are never subjected to a global image percentage: each color-independent foreground-coverage result and any non-empty literal RGB mask alone permits `<=0.5%`, while structure, node boxes, token identity, contrast, and forced-colors remain hard gates. Capture-readiness cases reject a blocked external font, an unmanifested or hash-mismatched local font, an unresolved/failed required face, system fallback, device scale different from the descriptor-declared source scale, viewport drift, and an animation that cannot reach the declared settled/frozen state. Refine-loop cases force the fifth attempt to retain one numeric failure and one non-numeric failure, and prove each exits nonzero with no authoritative gate receipt and no structural-only reclassification. D9-local coverage rejects missing, duplicate, or extra source comparisons, an S10 structural record with the wrong reason/source hash or wrong title/subcopy/category scope, a missing completion-escape assertion, either feedback-description record or control association, missing copy-rejection/counter evidence, missing copied-state duration/reset evidence, an unconfirmed or source-claimed Transfer classification, an individually scrolling keyed editor or a clipped panel heading/action, a state-selected container width, nonzero CSS geometry tolerance, an undeclared/diff-derived semantic-token mask, a missing token/coverage/contrast gate, state-dependent search/primary copy, absent/false capture-readiness fields, a missing per-surface last-control focus receipt, or a stale/exhausted visual receipt. It also injects remote Figma MCP provenance into an otherwise hash-valid local UX evidence fixture and requires `verifyUXEvidence` to fail, and supplies a flow-authenticated receipt with the wrong resolved promotion target (plus invalid transient-pattern and HEAD-mismatch variants) and requires release validation to fail. + +D8's post-call concurrency journey also changes the reason after edit/copy/local feedback, injects an older-generation late response with a greater timestamp, and then settles the current generation. DOM/state evidence must show the old draft, counters, selected control, and `Pending submission` remain until current-generation acceptance; the superseded response produces no counter/status mutation; and the accepted replacement alone changes content, preserves edited/copied totals, adds one viewed action, clears the old selection/description and both IDREFs, and leaves `Complete Wrap-Up` unchanged. The selected-feedback journey separately proves post-call Like/Dislike paint immediately after local guarded acceptance with `Pending submission`, whose one visible node is directly associated with both enabled controls; the mid-call extrapolated states paint only after the SDK Promise fulfills. Post-call response failure renders `Submission not confirmed` and associates it with both read-only controls, while success removes the description and both references. Neither state has live-region semantics. Copy/DOM/accessibility evidence must render exactly `Search by name, queue, entry point or phone number`, `Search topic`, and `Complete Wrap-Up` in every visual state and must reject every visible text, accessible name, placeholder, or tooltip containing a percent marker. + +## UX Evidence and Productionization + +Figma MCP was not used. Acquisition mode is sealed local `json`; the only JSON inspection commands used were `mb-flow prog-mini-js figma-json-node`, `figma-json-children`, `figma-json-search`, and `figma-json-text`. Each source was queried at its declared root, text was searched for summary/action/error copy, and every PNG was inspected at original resolution. Those inspections established the expected hashes below but do not admit parent-checkout files for implementation. D0a must copy exactly these thirty paths from the canonical common checkout into the same target-worktree-relative paths through `stage-ux`, then independently reopen/rehash the target copies through `seal-ux --widgets-root` and record portable `ux.resolutionRoot: "."` before D3; D8 later verifies that target-root seal and hands the same absolute root to the common-checkout comparison children without rewriting it: + +| Source/state | Scene graph, text JSON, screenshot | Observed and retained | Productionization | +| --- | --- | --- | --- | +| UX-001 / JNY-001:mid-call-summary | `.ccwidgets/midcall/figma_target_8061_880455_compact.json`; `.ccwidgets/midcall/figma_target_8061_880455_text.json`; `.ccwidgets/midcall/midcall.png` | 480x580 consult popover; header, radio categories, resolved `Search by name, queue, entry point or phone number` placeholder, and results above a divider; `Here’s a consult summary—they’ll get a copy`; 448x183 bottom editor; copy left and AI-generated/feedback right. | The sealed hierarchy requires one unconditional voice category/search/results renderer for eligible and ineligible states, with the shared editor appended only when summary state is visible. The source supplies only the Consult sentence; the Transfer heading has its own structural-only message ID, is not a source substitution, and remains provisional until product/content-owner confirmation. | +| UX-002 / JNY-002:generating | `.ccwidgets/postcall_generating/figma_target_5669_263180_compact.json`; `.ccwidgets/postcall_generating/figma_target_5669_263180_text.json`; `.ccwidgets/postcall_generating/postcall_generating.png` | 440x620 wrap-up popover; reason selected; divider; centered 32x32 progress indicator; generating title/subcopy; `Complete Wrap-Up` disabled. | Render after reason selection, preserve header/reason layout, use existing Spinner/tokens, and disable completion only during the initial bounded request. Every reason commit allocates a generation token; a later commit supersedes older in-flight work before timestamp ordering, while the current draft, counters, and pending feedback remain until a current-generation ready response is accepted. | +| UX-003 / JNY-002:editing | `.ccwidgets/postcall_summary_edit/figma_target_5669_263754_compact.json`; `.ccwidgets/postcall_summary_edit/figma_target_5669_263754_text.json`; `.ccwidgets/postcall_summary_edit/postcall_summary_edit.png` | 442x620 popover; sparkle/info summary title; bordered scrollable plain editor; action bar; enabled `Complete Wrap-Up`. | Map to controlled plain/structured editor with vertical overflow and unchanged wrap-up controls. | +| UX-004 / JNY-002:like-hover | `.ccwidgets/postcall_summary_hover_thumbsup/figma_target_5669_264384_compact.json`; `.ccwidgets/postcall_summary_hover_thumbsup/figma_target_5669_264384_text.json`; `.ccwidgets/postcall_summary_hover_thumbsup/postcall_summary_hover_thumbsup.png` | 400x620 compact popover; collapsed `Search topic` trigger; 368x380 structured summary with `Outcome`; `This is helpful` tooltip; normal `Complete Wrap-Up`. | Strip the Figma variable-marker delimiters before render, then use the exact resolved search/primary copy and feedback label/tooltip with the inherited hover token. | +| UX-005 / JNY-002:like-selected | `.ccwidgets/postcall_summary_selected_thumbsup/figma_target_5669_264503_compact.json`; `.ccwidgets/postcall_summary_selected_thumbsup/figma_target_5669_264503_text.json`; `.ccwidgets/postcall_summary_selected_thumbsup/postcall_summary_selected_thumbsup.png` | Same geometry/copy; helpful icon has persistent selected treatment. | Post-call paints the selected `aria-pressed=true` state immediately after the revision-guarded local setter returns `true`, with filled icon and non-color semantics; no SDK acknowledgement exists before wrap-up. The mid-call extrapolation paints only after SDK success. REQ-008's pending/not-confirmed descriptions are separate structural-only states with their own message IDs and control associations; they are not attributed to this screenshot. | +| UX-006 / JNY-002:dislike-hover | `.ccwidgets/postcall_summary_hover_thumbsdown/figma_target_5669_264622_compact.json`; `.ccwidgets/postcall_summary_hover_thumbsdown/figma_target_5669_264622_text.json`; `.ccwidgets/postcall_summary_hover_thumbsdown/postcall_summary_hover_thumbsdown.png` | Same structured/collapsed hierarchy and exact resolved `Search topic`; `This isn't helpful` tooltip; normal `Complete Wrap-Up`. | Exact label/tooltip with the same shared action component and delimiter-free source-resolved wrap-up copy. | +| UX-007 / JNY-002:dislike-selected (`S07-UX` / `desktop-light` -> `S07` / `dl`) | `.ccwidgets/postcall_summary_selected_thumbsdown/figma_target_5669_264860_compact.json`; `.ccwidgets/postcall_summary_selected_thumbsdown/figma_target_5669_264860_text.json`; `.ccwidgets/postcall_summary_selected_thumbsdown/postcall_summary_selected_thumbsdown.png` | Same geometry/copy; dislike selected and like unselected. The source aliases normalize to canonical evidence key `S07` / `dl` without changing the sealed screenshot bytes/hash. | Post-call applies mutually exclusive selected treatment immediately after the guarded local setter accepts it; the mid-call extrapolation waits for SDK success. REQ-008's pending/not-confirmed descriptions are separately classified structural-only; D8 also writes and D9 verifies the declared-to-canonical evidence-key mapping. | +| UX-008 / JNY-002:copy-hover | `.ccwidgets/postcall_summary_hover_copy/figma_target_4920_216835_compact.json`; `.ccwidgets/postcall_summary_hover_copy/figma_target_4920_216835_text.json`; `.ccwidgets/postcall_summary_hover_copy/postcall_summary_hover_copy.png` | Copy hover tooltip `Copy Summary`; exact resolved `Search topic` and `Complete Wrap-Up`; otherwise same structured hierarchy. | Exact button label/tooltip and delimiter-free source-resolved copy; invoke the direct Clipboard Promise only from the gesture and attach fulfillment plus rejection handlers at that call site. A throw/rejection is caught and normalized to content-free `failed`, exposes no raw error, invokes no recorder, increments no copied counter, and causes no failure-driven visible-state change; there is no automatic export or fallback. | +| UX-009 / JNY-002:copy-selected | `.ccwidgets/postcall_summary_selected_copy/figma_target_4920_216954_compact.json`; `.ccwidgets/postcall_summary_selected_copy/figma_target_4920_216954_text.json`; `.ccwidgets/postcall_summary_selected_copy/postcall_summary_selected_copy.png` | Copy changes to success/check treatment while exact `Search topic` and `Complete Wrap-Up` remain visible. | Paint success only after Clipboard fulfillment and recorder acceptance. Keep it for exactly `COPIED_FEEDBACK_MS = 1_500` ms, reverting to `idle` on the first of timeout, copy-control focus loss, another copy gesture, or accepted summary-content replacement; a fulfilled second copy starts a fresh interval, while rejection produces no further state/counter change. Unmount clears the timer without updating state. Retain the same resolved control copy and do not announce the paint. | +| UX-010 / JNY-002:error | `.ccwidgets/postcall_summary_error/figma_target_5669_264979_compact.json`; `.ccwidgets/postcall_summary_error/figma_target_5669_264979_text.json`; `.ccwidgets/postcall_summary_error/postcall_summary_error.png` | 440x620 popover; centered error icon; exact title `Having trouble generating summary`; exact subcopy `It could be a lost connection or something else. Could you check your connection or try again later?`; outlined `Retry`; source depicts `Complete Wrap-Up` disabled. | The observed title exactly reconciles with REQ-009 and is the one production `aiSummary.generationError` string for every visible `disabled`, `offline`, `unavailable`, `empty`, `timeout`, and `generic` generation-error category; the exact observed subcopy is `aiSummary.generationErrorDescription`. Its `or something else` / `or try again later` wording is treated as non-diagnostic, category-neutral guidance rather than a connection-cause assertion. Preserve that hierarchy/copy and fresh-generation Retry, but after terminal failure enable completion for the committed reason without summary. REQ-005 availability overrides only the source's disabled-control state; D6 proves the unrecovered-error completion returns `not-required` with no response send, and D8 retains the sealed triple while recording the enabled control as the named structural override. | + +All screenshot-backed states receive the structural and region-masked comparison contract below. UX-010/S10 is the sole sealed-source structural override because its disabled primary treatment conflicts with REQ-005's bounded completion escape; its source hashes, hierarchy, copy, and geometry remain verified, but no misleading disabled-button pixel target is imposed on the enabled production control. The following table is the complete structural-only denominator. Every row is a distinct typed registry entry; a row with multiple modes still produces one receipt per listed mode, and a row with an action tuple produces one receipt per action. D8 emits the named classification/DOM/accessibility receipt for every row and D9 compares the exact expected `(stateId, mode, action?)` tuples rather than trusting a count. The declared screenshot-backed keys S01-S06, S07-UX, and S08-S09 may not appear here, and no row may be silently merged, omitted, or downgraded. + +| Structural state ID | Mode/surface/action | Required evidence or source binding | Named D8 browser/DOM/a11y scenario gate | +| --- | --- | --- | --- | +| `receiver:adaptive-card-ready` | `mid-call-receiver` | REQ-004; no sealed receiver screenshot | `View summary opens the receiver adaptive card with a sibling summary action row` asserts the notification, card/action sibling containment, exact actions, and accessible panel structure. | +| `receiver:unrenderable` | `mid-call-receiver` | REQ-002; no `extrapolatedFrom` | `Unrenderable receiver payload shows The summary is not available` asserts exact unavailable copy and no editable/card-action fallback. | +| `summary:hidden-authorization-initialization` | initiator, receiver, post-call | REQ-009; no screenshot | `Unauthorized and initialization failures omit every summary surface and receiver trigger` asserts absence, no blank region, and no focus move. | +| `mid-call:summary-omitted` | `mid-call-initiator`; `CONSULT`, `TRANSFER` | REQ-001/UX-001 capability absent or false | `Capability omission preserves the one destination tree` asserts no summary subtree while category/search/results identity, values, order, and actions remain. | +| `mid-call:transfer-summary-panel` | `mid-call-initiator`; `TRANSFER` | No sealed copy source and no `extrapolatedFrom`; `aiSummary.midCall.transferHeading`, exact product-confirmed copy, and non-empty confirmation reference | `Transfer summary panel exposes the product-confirmed structural heading` asserts final copy and action-parity rationale and rejects any UX-001 authority claim. | +| `mid-call:generating` | `mid-call-initiator` | `extrapolatedFrom: UX-002/S02 generating` | `Mid-call generating uses the shared spinner and corrected generating copy` asserts no post-call reason or primary-control semantics. | +| `mid-call:editing` | `mid-call-initiator` | `extrapolatedFrom: UX-003/S03 editing` | `Mid-call editing preserves keyed fields in one bordered editor` asserts exact field order/edit mapping and action-row placement. | +| `mid-call:like-hover` | initiator, receiver | `extrapolatedFrom: UX-004/S04 like-hover` | `Mid-call helpful hover uses the shared label tooltip and stable geometry` runs once per mode. | +| `mid-call:like-selected` | initiator, receiver | `extrapolatedFrom: UX-005/S05 like-selected` | `Mid-call helpful selection paints only after confirmed feedback` asserts `aria-pressed=true`, mutual exclusion, and non-color semantics in each mode. | +| `mid-call:dislike-hover` | initiator, receiver | `extrapolatedFrom: UX-006/S06 dislike-hover` | `Mid-call not-helpful hover uses the shared label tooltip and stable geometry` runs once per mode. | +| `mid-call:dislike-selected` | initiator, receiver | `extrapolatedFrom: UX-007/S07-UX desktop-light`, canonical S07/`dl` | `Mid-call not-helpful selection is mutually exclusive and preserves the evidence alias` runs once per mode. | +| `mid-call:copy-hover` | initiator, receiver | `extrapolatedFrom: UX-008/S08 copy-hover` | `Mid-call Copy Summary hover keeps its exact name and tooltip` runs once per mode with the deterministic portal gate. | +| `mid-call:copy-confirmed` | initiator, receiver | `extrapolatedFrom: UX-009/S09 copy-selected` | `Mid-call copy confirmation follows fulfilled Clipboard settlement` asserts check treatment, stable accessible name, timer/reset behavior, and counter sequencing per mode. | +| `mid-call:generation-error` | initiator, receiver | `extrapolatedFrom: UX-010/S10 error` | `Mid-call generation error uses the shared exact hierarchy without post-call controls` asserts title/subcopy and absence of Retry/Complete Wrap-Up. | +| `mid-call:initiator-unavailable` | `mid-call-initiator` | REQ-002; no `extrapolatedFrom` | `Unrenderable initiating payload shows The summary is not available` asserts the exact copy in the summary location. | +| `mid-call:request-pending-disabled-initiation:consult` | `mid-call-initiator`; `CONSULT` | REQ-001; no screenshot | `Pending CONSULT disables re-initiation without moving focus or issuing a second request` asserts the 15-second bound and focus-retaining `aria-disabled`. | +| `mid-call:request-pending-disabled-initiation:transfer` | `mid-call-initiator`; `TRANSFER` | REQ-001; no screenshot | `Pending TRANSFER disables re-initiation without moving focus or issuing a second request` asserts the same contract independently. | +| `post-call:generation-error-availability-override` | `post-call` | Sealed UX-010/S10 triple; `reason: requirement-availability-override` | `S10 availability override retains exact error copy and enables Complete Wrap-Up` exercises all six visible error categories, no-draft completion, and zero response send. | +| `post-call:unavailable` | `post-call` | REQ-002; no `extrapolatedFrom` | `Unrenderable post-call payload shows The summary is not available` asserts exact copy before the stable enabled completion control after settlement. | +| `post-call:reason-search-empty` | post-call reason picker | REQ-009; no screenshot | `Zero-match reason search renders static non-live empty text` asserts `No wrap-up reasons match your search.` and no status/alert/live semantics. | +| `post-call:outcome-absent` | `post-call` | REQ-011; paired D1 key-omitted fixture | `Missing resolution omits Outcome row copy response key and reserved space` asserts remaining SDK section order and geometry closure. | +| `post-call:feedback-pending` | `post-call` | REQ-008; `aiSummary.feedback.pendingSubmission` | `Pending submission describes both post-call feedback controls without a live region` asserts exact copy, both resolvable IDREFs, and enabled controls. | +| `post-call:feedback-not-confirmed` | `post-call` | REQ-008; `aiSummary.feedback.submissionNotConfirmed` | `Submission not confirmed describes both read-only post-call feedback controls without a live region` asserts exact copy, both IDREFs, frozen selection, and no resend/edit surface. | +| `post-call:response-submitted` | post-call completion/status | REQ-007/REQ-008; no screenshot | `Successful post-call response clears feedback description and emits submitted once` asserts atomic node/IDREF removal and the content-free status transition. | +| `post-call:response-failed` | post-call completion/status | REQ-007/REQ-008; no screenshot | `Failed post-call response preserves read-only selection and emits response-failed once` binds the transition to the not-confirmed presentation without retry. | +| `post-call:legacy-ineligible` | legacy wrap-up | REQ-005; no summary screenshot | `Ineligible wrap-up preserves the legacy reason and completion flow` asserts no summary nodes and unchanged single wrap-up call. | +| `lifecycle:interaction-switch-reset` | all summary surfaces | REQ-006/REQ-009 | `Interaction switch clears scoped values and focuses the named containing surface` asserts no cross-interaction focus/value mapping. | +| `lifecycle:hold-resume-retained` | initiator and receiver | REQ-006 | `Hold and resume retain current summary state without an extra request` asserts content, counters, selection, and connected focus retention. | +| `ownership:cross-agent-transfer-cleared` | receiver and post-call | REQ-006 | `Cross-agent successor clears predecessor content and starts zero counters` asserts stale predecessor settlement cannot reappear. | +| `ownership:same-agent-conference-retained` | initiator and receiver | REQ-006 | `Same-agent conference retains current summary until a valid replacement` asserts pending/failure retention and one freshness-winning replacement. | + +Transitions such as reason-change retention, pending/settled reopen, card replacement, and expand/collapse are exercised by the named scenario attached to their before/after registry rows; they do not create a second classification when their rendered DOM/accessibility state is byte-for-byte the same. Any transition that introduces a distinct rendered state must add a new row and scenario in the same D8 change. This rule makes the table, rather than an open-ended prose phrase, the denominator used by `verifyUXEvidence`. + +Hover/tooltip capture is a closed deterministic sequence for every screenshot-backed or structural hover registry entry, including UX-004/S04, UX-006/S06, UX-008/S08, and their mid-call/receiver counterparts. Before `driveAISummaryState` reports the state ready, it installs the sealed viewport and root geometry, settles viewport-relative panel framing and content-region scroll offsets, parks the pointer at the registry's neutral coordinate, and then moves the real pointer to the registry's fixed CSS coordinate at the center of the named native trigger. The fixture gives the portal enough room for the source-declared `top` placement and rejects auto-flip, collision shift, a second tooltip, or an anchor mismatch. Capture-only CSS disables the installed Momentum tooltip's entry animation and freezes trigger/portal transitions; the helper also finishes or freezes any Web Animation owned by those nodes. It then requires the trigger rectangle, portal tooltip rectangle, arrow orientation, and placement token to be identical for two consecutive `requestAnimationFrame` ticks while the pointer remains fixed. `recordAISummaryVisual` uses a page clip rather than a component-only screenshot so the body-level portal is included, and records the pointer coordinate, anchor/tooltip rectangles, placement, portal identity, `animationsDisabled: true`, `transitionsFrozen: true`, and the two-frame stability result in `comparison.json`. A missing field, unstable rectangle, placement change, or tooltip timeout fails before pixel scoring. + +`playwright/visual/ai-summary-visual-cases.ts` is the single typed classification record. Its nine readonly screenshot-backed entries map UX-001 through UX-009 to the exact D0a receipt keys for scene JSON, text JSON, PNG, source ID, screenshot ID/variant, and render dimensions. Its structural-only entries enumerate every implemented no-screenshot state with `reason: 'no-sealed-screenshot'` and S10 with `reason: 'requirement-availability-override'`; the S10 row still references and rehashes its sealed source triple and asserts the source-derived hierarchy/copy plus the requirement-derived enabled primary control. The Transfer panel row records its message ID, exact product-confirmed string, non-empty confirmation reference, and action-parity justification, deliberately omits `extrapolatedFrom`, and may not present UX-001 as copy evidence or authority. The feedback-pending and feedback-not-confirmed rows record their message IDs, exact strings, direct Like/Dislike IDREF assertions, enabled/read-only semantics, and absence of live-region behavior. Each of the nine shared-treatment mid-call rows additionally records the exact `extrapolatedFrom` UX/source key and applicable mode tuple, and D8 rejects a missing, extra, or mismatched binding. Every structural entry names its owning browser scenario and required DOM/accessibility assertions. A state ID may appear in exactly one collection. D8 compares the registry with its exercised-state inventory and fails before release if an implemented state is absent, duplicated, unconfirmed where required, or uses a reason outside that closed pair; adding or reclassifying an implemented state therefore requires updating this file and its scenario in the same task. + +Production semantics are native buttons, radio groups, labels, textareas/paragraphs, and existing dialog/popover containers. Every voice mid-call destination DOM uses header, radio categories, search, and destinations; eligible/visible summary state conditionally appends divider, summary content, and summary actions to that same tree. Post-call DOM order is header, reason selection, summary content/actions, then Complete Wrap-Up. Hover is additive and does not hide keyboard focus; selected state uses `aria-pressed`. There are no custom shortcuts. Dynamic content uses `dir="auto"`; the overall layout remains LTR. No feature live region or announcement is added. If a focused Retry, feedback, copy, notification, or other summary control disappears, the update removes that control and a layout effect moves focus to the first connected focusable successor after its pre-update position within the same summary subtree. If no such successor remains, focus moves through the exact ref-scoped `containingPanelFocusTarget`: `consultTransferPanelRef.current` for the open mid-call `.agent-popover-content`, `panelRef.current` for `[data-testid="ai-assistant:panel"][role="dialog"]`, or `wrapUpPanelRef.current` for the open post-call `.agent-popover-content`; every root is connected and programmatically focusable with `tabIndex={-1}`. A connected control retains focus across content replacement, and loading, content updates, and expand/collapse do not otherwise move it. In CallControl popovers, long content wraps inside the approved 80vh shell with independently bounded list and shared summary-content scroll regions. Headings, summary actions and Complete Wrap-Up remain visible in document order; individual keyed editors auto-size without their own scrollbars. Compact-height layouts reserve readable content space and retain keyboard reachability. + +## Figma JSON Reconstruction Assessment + +Figma MCP was not used. All scene/text pairs were inspected through the bounded MatrixBuilder query surface and matched to their exact PNG. Raw hashes below are the expected subjects; they become admitted implementation inputs only when D0a reproduces them from the exact target-worktree paths and records that result in the shared receipt. D8 only verifies the admitted bytes before comparison. + +### State and Node Mapping + +| Source | Root node and CSS size | Scene raw SHA-256 | Text raw SHA-256 | Screenshot ID, pixels, SHA-256 | Source scale; root-relative crop `[x, y, width, height]` in CSS px | +| --- | --- | --- | --- | --- | --- | +| UX-001 | `8061:880455`, 480x580 | `7183f1b874339e30a7c820179d63fabc500a1721f2206194a3fe9c4317880603` | `804b66441dcd6a1ae3c43d3907bd255c3db2a1457539b3f7e3888d593320d666` | S01, 964x1164, `5fee49d4111bb9b5a950e441821abc001aa99a39fdd6607b7169e2754654db0f` | 2; `[-1, -1, 482, 582]` | +| UX-002 | `5669:263180`, 440x620 | `2c1ade086aaf3bea8f9719f37b6e3a06ca1d7e93737cea58b368bd49efbffd4a` | `42921d9839a89fa948acd22563579a62472b14a8cc6121b2d0c7883d5b71f3ce` | S02, 884x1242, `0de0aeb692ae83a922898009577b11d9d027b21c8653b05c947ba03e64bc8a21` | 2; `[-1, 0, 442, 621]` | +| UX-003 | `5669:263754`, 442x620 | `9e9378e384df92e816a3c05e7faff8c800da2e1f6334e149a9522e24c837aa66` | `58457c269bf1c9efff9f43e63a92f33a7b08f8f18ad43653403084b5cb46fc50` | S03, 886x1242, `92b2ef6fe8473c96f422ebcfece7699d5e3360503921ff40e36b281e355ba6f1` | 2; `[-1, 0, 443, 621]` | +| UX-004 | `5669:264384`, 400x620 | `e084f1942b911286d4eb58e7db3082130e808e12c5197fba565d069860966e13` | `c4e6025190ce7f9f6b0b81fabb3eb80c4686f69f5fb4b2ac3769896d203000b1` | S04, 806x1248, `3f9013a7a0772aeef98b55bf8e301957c74dca912d500e87dfeaf12f55fc50ca` | 2; `[-2, -2, 403, 624]` | +| UX-005 | `5669:264503`, 400x620 | `4330bf697349d52df2493b1675343d5d27c60075eddc0a6a96258b29f0fa79fb` | `6a08810d6325a5cb7290712373047c61ed1044e7ca6f708d3ab202f80ad4ba00` | S05, 804x1244, `3628b688d6d84ee61ed1c22e1b77ee6aabb7026656ff8d9d15739b549bdeeeca` | 2; `[-1, 0, 402, 622]` | +| UX-006 | `5669:264622`, 400x620 | `41f147d96db6b2133f0f6994561cc577b0ae121ad9cf966cfdcc300f8b6d9f8e` | `f22fa4cce01a2cc8cb9d7ce334c261176fce196116ea190ef06d5f9c26242268` | S06, 866x1242, `098c803e608ce2cc2f8c79ec12208a41ce5ba8ee2ffc66f2071801e2b4872641` | 2; `[-2, -1, 433, 621]` | +| UX-007 | `5669:264860`, 400x620 | `d78d2465ad40c21102a167d5cce976f1541e9a26e415f45069f91297c2ca6550` | `5dfe180ec35e38af7df336bbe1acba5b6a5e58c7c476a5fa7d60aacf96e1cc6d` | declared `S07-UX` / `desktop-light` -> canonical `S07` / `dl`, 806x1244, `7510c740d754cd9e612fc0cfb8ec080682f66a927e941c705cfe13d167920203` | 2; `[-1, -1, 403, 622]` | +| UX-008 | `4920:216835`, 400x620 | `40d15e6e6e951c6ff05d451add94cde1e2a6ed1517489ba3c7153d104631eb61` | `15756650eb613cc81bfdc0d5aaa8f75f7e2009c66a79c28dd108ad39aab0caf7` | S08, 882x1264, `3b1e78288b4316b4c1575f38112d924c319431f8f07bdcd88a11343a9fee6e21` | 2; `[-39, -9, 441, 632]` | +| UX-009 | `4920:216954`, 400x620 | `7b8b3f8148adb46ae59cf36ffdb63e9f81f0035f7d14a433840183df46926a7a` | `059a61503ef5c9eb452b7c03478bb94387143f9d2d84bc0bb433e18b8e003e33` | S09, 806x1248, `71e19e3be8da9045fce78e674006cd8963bd95f15642ff6a3884686db5c5e4b1` | 2; `[-1, -3, 403, 624]` | +| UX-010 | `5669:264979`, 440x620 | `3ab596f96b5a49eb56822816be31f9d4f739bf79f4a50ad09521ccfc491b8de7` | `90de99f4579e9eacadf068fa6fb48eff70eadad76e97d342094363b22fe0dbaa` | S10, 886x1242, `6d54cafe81f3693a16017e19adff9aaaf4a1adbae67b779a3f49d6c1fda15ff5` | 2; `[-1, 0, 443, 621]` | + +Root-node `node --depth 2` and `children` queries establish one primary popover/frame per state. UX-001 contains a 448-pixel content column and 448x183 summary editor. UX-002/003/010 use approximately 408-pixel inner columns within 440/442 roots. UX-004 through UX-009 consistently expose a 368x456 body, 368x380 text area, and 352-pixel inner content layout. Every PNG is an exact scale-2 source export; the final column is the complete export rectangle in CSS coordinates relative to the declared root border-box origin, so doubling all four values yields the PNG registration and dimensions exactly. UX-006 is not a 433-pixel component: its `[-2, -1, 433, 621]` source crop is the 400x620 root plus 2 px left, 1 px top, and 31 px right for the portalled `This isn't helpful` tooltip. UX-008 is likewise not 441x632: its `[-39, -9, 441, 632]` crop retains the left-overhanging portalled `Copy Summary` tooltip plus the fixed 9 px top, 2 px right, and 3 px bottom export padding. The other rows make their smaller shadow/canvas padding explicit rather than calling it “roughly 2x.” None of these crop extents is a production width or a state-selected layout value. + +Visual comparison is structural-first and semantic-token-aware; there is no whole-root raw-RGB pass percentage. D8 first verifies the unchanged raw `target.png` hash and dimensions, the exact CSS root geometry, source scale `2`, and the registry crop tuple. With the descriptor-declared browser `deviceScaleFactor: 2`, it captures that same root-relative CSS crop, including the portal when present, at the sealed PNG's original physical resolution. Target and rendered PNG dimensions must match exactly; geometry observations remain in CSS pixels. Neither raster is downsampled. There is no stretch, auto-trim, best-fit translation, additional resampling, or threshold-derived crop. Before any pixel metric, D8 requires exact root/viewport/portal geometry, exact mapped-node DOM order and border boxes, exact text and accessible names, exact overflow/scroll ownership, and the named interaction/a11y assertions. Container-driven sizing is tested at each source's external host width; it is never compared by forcing the component to a bitmap width. + +The root border box plus the registry's exact source-bound tooltip body/arrow bounds is the `baseInclusionMask`. Two closed masks declared in `ai-summary-visual-cases.ts` before rendering are then applied. The existing `sourceCorrectionMask` contains only the sealed outer-percent variable slots for S01/S04-S09 and the S02 U+005F/U+2014 separator region. The new `semanticTokenMask` contains only sealed-node/render-node regions whose color is intentionally supplied by an existing Momentum semantic token rather than literal screenshot RGB. Every mask row records source node ID, rendered locator, semantic role, source/render rectangles, reason, token/custom-property identity, and hash/pixel count; mask rows cannot be inferred or expanded from a diff. Geometry, presence, text, and semantics of a masked node remain blocking. + +| Semantic role | Raw-RGB treatment | Replacement gate that remains blocking | +| --- | --- | --- | +| Surface/control fills (`popover/panel`, editor, tooltip, primary/secondary, hover, selected, disabled) | Exclude the interior fill pixels from raw-RGB scoring. | Exact node presence/order/box/radius/padding; stylesheet uses the allowlisted existing `--mds-color-theme-*` role; computed contrast and forced-colors checks pass. | +| Borders, dividers, focus outlines, scroll track/thumb, and elevation/shadow | Exclude token-owned stroke/shadow pixels from raw-RGB scoring. | Exact border/divider/scroll-owner geometry and width, visible `:focus-visible`, allowlisted token identity, and forced-colors behavior pass. | +| Token-colored text, icons, spinner, selection accent, and error glyph | Exclude literal foreground RGB but include the predeclared node in a color-independent foreground-coverage comparison. | Exact text/icon identity, font face/weight/size/line box or icon name/box, accessible semantics, contrast, and foreground coverage difference `<=0.5%` pass. | + +After both masks, `literalRgbMask = baseInclusionMask - sourceCorrectionMask - semanticTokenMask`. A non-empty literal mask must have `literalRgbDifferencePercent <= 0.5%`; if it is empty, `comparison.json` records `literalRgbDifferencePercent: null` and `literalRgbReason: "no-literal-authority"` rather than fabricating a zero. `foregroundCoverageDifferencePercent` is independently required for every declared foreground node and is color-independent. There is no aggregate `pixelDifferencePercent` field and no pass can be obtained from a masked screenshot alone. `comparison.json` records raw/normalized hashes and dimensions, crop/scale, all three mask identities and denominators, mapped-node geometry, token identities, coverage results, literal result/null reason, and the structural assertions; `diff.png` visualizes the literal and coverage failures without creating a new denominator. S10 records the same geometry/mask metadata in its structural override but receives neither a literal nor fabricated pixel score. Any undeclared exclusion, diff-derived mask, changed token-role mapping, geometry drift, text mismatch, contrast/forced-colors failure, foreground-coverage failure, or non-empty literal score above `0.5%` fails validation. + +### Component Mapping + +UX-001 maps to existing `CallControlComponent -> ConsultTransferPopoverComponent -> AISummary`. UX-002, UX-003, and UX-010 map to `CallControlComponent` wrap-up content and shared loading/editor/error states. UX-004 through UX-009 map to the one `AISummary` action row reused by initiator, receiver, and post-call modes. For the receiver, `AIAssistantComponent` renders the display-only `AdaptiveCardRenderer` and that shared `AISummary` `mid-call-receiver` action row as siblings: the controls are outside and never descendants of the restricted renderer. The hierarchy is explicitly `AIAssistantComponent -> {AdaptiveCardRenderer, AISummary action row}`, not `AIAssistantComponent -> AISummary -> AdaptiveCardRenderer`; the renderer's props, error boundary, and security ownership remain unchanged, while its existing sanitizer is hardened by D4 with the closed no-remote-image policy. Existing Momentum Button/Text/Spinner/Tooltip components and current CallControl theme tokens are reused. There is no screenshot for the receiving panel, so this sibling composition and its containment assertions are structural-only. + +### Layout Interpretation + +The visual hierarchy is a popover header, selectable main content, a divider, summary state/content, action row, then the existing primary completion action. For every voice mid-call Consult/Transfer popover, UX-001 places radio categories before search and results; the divider and summary subtree append only when summary state is visible, with no alternative search-first pill renderer. Post-call preserves reason selection above its divider. The structured examples show compact label/value groups inside one bordered region; production uses field rows in the same region rather than independent cards. The desktop-light targets use a white/neutral surface, neutral gray borders/dividers/scroll tracks and secondary text, black high-emphasis text and enabled primary pill, blue selected-radio/sparkle accents, gray disabled primary treatment, and a dark-red error icon. Production maps those roles to existing semantic Momentum tokens instead of literal screenshot colors, retaining forced-colors behavior and tested contrast; S10's disabled primary styling is the one requirement-overridden state and is not applied after terminal failure. For mid-call and post-call CallControl popovers, the 80vh shell contains independently bounded destination/reason-list and summary-content scroll regions; headings, summary actions and the primary action remain visible. The AI Assistant retains its established bounded panel-body owner. Scene measurements retain the visible 16px popover/body insets and 12px structured-editor inset where those existing tokenized spacings match; at narrow widths, controls wrap without horizontal scrolling, and CSS uses the existing container's inline size rather than fixed 400/440/480 widths. + +The post-call 400/440/442 spread is reconciled as input geometry, not component behavior. Production has one deterministic width rule: use the existing cc-calling-widget host container's inline size, and never select a width from eligible/legacy, expanded/collapsed, or lifecycle state. D8 supplies the sealed host fixture size per comparison—440x620 for S02/S10, 442x620 for S03, and 400x620 for S04-S09—so the same responsive component is exercised under each source condition. The sealed CSS root/viewport dimensions allow zero-pixel measurement drift and target PNG bytes/dimensions remain exact. Internal layout, text line boxes, and control order are gated structurally at that supplied width; semantic-token-owned colors use the token-role/contrast/coverage gates above, while `<=0.5%` applies only to a non-empty literal RGB mask and the color-independent foreground-coverage masks. Container responsiveness is therefore not judged as an unexplained whole-image repaint. + +### Source Conflicts and Ambiguities + +The requirement resolves role payload precedence: initiator/post-call ignore adaptive cards, receiver ignores typed/plain fields. `ai-summary.md` describes cancel submission and object-only edited bodies, while the requirement forbids cancellation and the inspected concrete SDK union permits section objects or strings; requirement plus admitted declarations govern. + +The structured Figma sample includes an `Outcome` label that is not a `PostCallSummarySections` response key, but the admitted event payload has optional top-level string `resolution`. Production retains a normalized non-empty value separately, then maps it to a non-editable/non-serialized `Outcome` display row using the `resolution` ordinal in `POST_CALL_DISPLAY_SECTION_ORDER`; it inserts before the first present higher-ordinal key or appends when none exists, so no named neighbour or synthetic SDK response key is invented and admitted SDK sections keep their relative order. When the optional key is missing, `undefined`, or empty after normalization, the row is omitted with no placeholder or reserved space. D1 supplies paired present/key-omitted payloads and D3/D6 exercise both. Sample values remain deterministic fixture content, not hard-coded UI. The Transfer variant has no separate mid-call screenshot; it retains UX-001 layout, while its distinct action-specific message ID and provisional copy follow the structural-only confirmation gate below. + +UX-001's named `.ccwidgets/midcall/figma_target_8061_880455_compact.json`, `.ccwidgets/midcall/figma_target_8061_880455_text.json`, and S01 `.ccwidgets/midcall/midcall.png` place categories before search, unlike the current search-first pill-button layout. Because destination functionality is identical with or without summary capability, production adopts that category/search/results hierarchy for every voice Consult/Transfer popover and conditionally appends only the summary subtree. The outer percent marks around the search token and agent/user-type/queue/number tokens are unresolved-variable delimiters, not glyphs: the placeholder resolves to `Search by name, queue, entry point or phone number`, while D8 supplies concrete synthetic result values through existing list props. The Transfer form uses the same hierarchy, but UX-001 supplies no Transfer heading. `aiSummary.midCall.transferHeading` and its candidate `Here’s a transfer summary—they’ll get a copy` value are therefore explicitly structural-only, provisional product copy requiring AI Assistant product/content-owner confirmation; neither is text extracted, parameterized, or extrapolated from UX-001. UX-002/UX-003 likewise establish the searchable radio reason hierarchy. The destination-layout discriminator is captured from non-summary context when the popover opens and held until close, so a mid-popover capability or ownership-generation change cannot swap between the voice hierarchy and the retained non-voice pill behavior. Capability arrival/revocation leaves the latched destination renderer, typed query, selected category/destination, and connected focus mounted and changes summary visibility only. Interaction change resets scoped state in both components. + +Across S01 and S04 through S09, every complete text token wrapped by `%` is classified uniformly as an unresolved Figma variable, including `%Search topic%`, `%Complete wrap-up%`, and agent/user/queue/number tokens. S02/S03 expose the same controls as undelimited `Search topic` and `Complete wrap-up`; that raw divergence is a source-capture artifact and is unrelated to hover, selected, copied, generating, editing, or error state. Evidence binding strips exactly the matched outer pair, then requirement precedence normalizes the primary action's capitalization, before authoring a production constant or deterministic fixture value. Static slots therefore render one canonical value per control: `Search by name, queue, entry point or phone number`, `Search topic`, and the REQ-005-controlled `Complete Wrap-Up`; visible text, accessible name, and tooltip agree in every state, and dynamic slots render deterministic fixture values. D8 preserves every sealed PNG unchanged for provenance, normalizes these source-only text differences before comparison, subtracts only the registry-fixed affected variable-slot regions from the pixel denominator, and asserts the exact canonical text and absence of percent artifacts through DOM/accessibility evidence. The mask cannot authorize product copy or hide any non-slot geometry. + +The UX-002 text JSON and compact scene JSON each extract `Just a sec_the details are coming together.` with exactly one U+005F ASCII underscore/low line (`0x5F`) between `sec` and `the`, and direct inspection of the higher-precedence S02 pixels visibly corroborates that glyph. The resolution is recorded rather than hidden: U+005F is a source punctuation artifact, not valid en-US sentence punctuation, while the admitted evidence pipeline demonstrably preserves U+2014 in `Here’s a consult summary—they’ll get a copy`. Production therefore binds `aiSummary.generatingDescription` to `Just a sec—the details are coming together.` with one U+2014 and no surrounding spaces. D3 asserts the full string, U+2014 at the separator index, and absence of U+005F; D8 keeps S02 immutable, subtracts the fixed source-underscore/rendered-em-dash separator region from its pixel score, and asserts the corrected DOM/accessibility copy. Screenshot geometry still wins any rounded measurement disagreement between instance/root JSON; scene JSON remains the authority for internal spacing when the screenshot is not legible. + +UX-010's text JSON and PNG resolve the visible error title to `Having trouble generating summary` and the subcopy to `It could be a lost connection or something else. Could you check your connection or try again later?`. The observed title and REQ-009 mandate are identical, so no screenshot/text/requirement precedence substitution is needed. Production binds those exact strings once as `aiSummary.generationError` and `aiSummary.generationErrorDescription` for all visible `disabled`, `offline`, `unavailable`, `empty`, `timeout`, and `generic` error categories; authorization/initialization remains hidden, and a valid unrenderable payload remains the separate unavailable presentation. This intentionally broad binding is acceptable because `or something else` makes the sentence non-diagnostic and `or try again later` remains a valid recovery option across the non-transport categories; the UI never asserts that an empty payload, disabled service, or generic backend failure was caused by the agent's connection. + +### Typography and Exact Text + +Scene/text queries show 16px/24px large headings and 14px/20px body text aligned with existing Momentum tokens; production references those tokens rather than copied font declarations. After unresolved-variable binding and punctuation-artifact correction, exact en-US strings retained are `Search by name, queue, entry point or phone number`, `Here’s a consult summary—they’ll get a copy`, `Wrap up interaction`, `Choose a code, and click the summary to edit it if needed.`, `Choose a reason to wrap up`, `Search topic`, `Summary of your conversation`, `AI-generated`, `Outcome`, `Generating summary...`, `Just a sec—the details are coming together.` (U+2014 separator), `Having trouble generating summary`, `It could be a lost connection or something else. Could you check your connection or try again later?`, and `Complete Wrap-Up`. None contains a visible percent delimiter or U+005F punctuation artifact. Under REQ-011, missing mid-call structured-detail labels reuse the post-call evidence strings `Additional context` and `Key Actions Taken`; the role-specific `Reason for transfer or consult` has no shared post-call counterpart. Requirement-supplied exact control strings override missing metadata: `View summary`, `This is helpful`, `This isn't helpful`, `Copy Summary`, `Retry`, and `The summary is not available`. + +The complete requirement-owned feedback-description inventory is `aiSummary.feedback.pendingSubmission` (`Pending submission`) and `aiSummary.feedback.submissionNotConfirmed` (`Submission not confirmed`). Both are structural-only because no declared screenshot contains them. In `post-call:feedback-pending` and `post-call:feedback-not-confirmed`, respectively, the visible description's instance-local ID is referenced by `aria-describedby` on both Like and Dislike controls; the pending controls remain enabled, the not-confirmed controls are read-only, and neither description has live-region semantics. D8 owns a named DOM/copy/accessibility assertion for each state, and confirmed success removes both the description and references. + +The separate `aiSummary.midCall.transferHeading` candidate (`Here’s a transfer summary—they’ll get a copy`) is also structural-only, but unlike the requirement-owned feedback descriptions it is not yet authoritative product copy. It requires an AI Assistant product/content-owner confirmation or replacement before D8 passes. The named D8 Transfer scenario records the confirmed exact value and message ID and must not claim sealed-source or extrapolation authority. + +The retained-string inventory is not a blanket provenance claim: the Consult heading and other cited screenshot strings retain their stated sealed-source provenance; the two feedback descriptions are requirement-owned structural-only strings; and the Transfer heading is a provisional structural-only candidate pending product confirmation. + +### Interaction and Accessibility + +The source states demonstrate default/editing, loading, error, helpful/dislike hover and selected, and copy hover/success. Requirement supplies keyboard/focus, pending feedback, permission, timeout/offline, receiver, and ownership states. Controls are native and tab in document order. Tooltip text equals the stable accessible name. Reaction buttons use `aria-pressed`; while either REQ-008 feedback status exists, both buttons directly reference the one visible description through `aria-describedby`, including in the read-only not-confirmed state. The description is ordinary text, not a live region. Copy success adds only the visible check treatment and retains `Copy Summary` as both name and tooltip, without a feature live region. Confirmation lasts exactly 1,500 ms unless timeout, copy-control focus loss, a second gesture, or summary replacement resets it earlier; unmount only clears resources. Removed-focus recovery follows the closed target table: a surviving later summary control wins, otherwise the exact open consult/transfer `.agent-popover-content` ref, AI Assistant dialog `panelRef`, or wrap-up `.agent-popover-content` ref receives focus, with a named last-control browser scenario for each surface. Other state changes retain focus. Dynamic text is `dir="auto"`; source locale is en-US. Existing focus, forced-colors, contrast, and reduced-motion behavior is inherited and verified rather than replaced. + +### Render Compare Refine Plan + +Operator-approved evidence-contract reconciliation (2026-09-27): all ten sealed sources require actual production captures and code-computed diff images/percentages. These measurements are observations, never automatic visual approval. S10 retains its requirement-availability structural override: enabled Complete Wrap-Up and fresh-generation Retry are intentional differences from the target; source hashes, error copy, geometry and accessibility remain checked. Keep all existing thresholds and budgets unchanged. UX-007 raw scenario identity is `UX-007/S07-UX`; `S07/dl` is a hash-equal local directory/sidecar alias only. Final comparison receipts use only flow-schema fields, with differences explicitly disclosed by the subsequent visual reviewer. Source-less structural cases remain unscored. No target image or SDK contract is changed by this reconciliation. + +D0a first stages the parent-only bytes, then independently admits and atomically seals all thirty target-worktree paths/hashes in the combined input receipt. D8 begins by re-resolving and rehashing that sealed set under the explicit target root without mutation; only after it still matches and the `MB_FLOW_BIN` plus common-checkout-root gates pass does acceptance prepare the implementation comparison contract with `--ux-input-root "$TARGET_WORKTREE_ROOT"` and the helper read a plan carrying that same canonical root and post-stage hashes. For each screenshot-backed registry entry, `recordAISummaryVisual` resolves the three inputs only through `design/default/sdk_package_lock.json` relative to that plan root, reads both sealed JSON inputs to establish node identity, dimensions, structure, exact copy, and semantic-role masks, and copies the verified PNG bytes unchanged to the plan-verified common-checkout render root at `.matrix/results/prog-mini-js/ux-visual/implementation///target.png`. That generated `target.png` is the comparison baseline; the same directory holds `rendered.png`, `diff.png`, `comparison.json`, `dom.json`, and `accessibility.json`, and `comparison.json` records the source-triple hashes, registry key, input-root identity, structural results, token/mask identities, color-independent foreground coverage, and literal RGB result/null reason. It renders declared S01-S06, S07-UX, and S08-S10 at the declared state/variant dimensions with deterministic fonts/data/clock. Raw scenario IDs match the descriptor; S07 / dl remains only the UX-007 local sidecar/directory alias. Each iteration uses the portable fail-closed child-process contract and official export/comparison argv above to record target, actual, diff, DOM hierarchy, mapped boxes, computed overflow, focus order, token identity/contrast/forced-colors evidence, coverage/literal metrics, and automated accessibility result. When a comparison exposes a presentational mismatch, D3/D6 or the other named implementation owner may refine only its already-owned component/CSS surface under focused acceptance and then D8 rerenders; D8 itself never edits product source. That correction changes no product logic, public props, or SDK calls; any such logic/contract/SDK change returns to its D2-D7 owner before validation resumes. Targets are never refined. Baseline updates are prohibited during comparison: a target can change only after an approved `requirement.md`/UX-source change updates the expected source hashes and D0a creates a fresh post-stage seal; copying a rendered image into `.ccwidgets/**` or `target.png` fails provenance validation. A screenshot-backed state passes only when every structural/DOM/copy/action/overflow/behavior/a11y/token gate passes, every foreground-coverage result is at most 0.5%, and its non-empty literal RGB mask is at most 0.5%; tokenized fill/stroke/foreground colors are never judged as literal screenshot RGB. Before comparison, the evidence adapter normalizes the declared source-capture artifacts so `Search topic` and `Complete Wrap-Up` are the sole DOM/accessibility expectations in UX-002 through UX-010; no hover, selection, copied, editing, generation, or error state may select different control copy. For every exact tuple in the complete structural-only registry, including S10, D8 writes `.matrix/results/prog-mini-js/ux-visual/implementation/structural//classification.json` with registry reason/scenario/assertion IDs and required mode/action/source binding, and writes its DOM/accessibility results beside it; no fabricated pixel score is allowed; S10 separately emits a diagnostic PNG and real code-computed diff to satisfy the ten-source flow capture contract, without a disabled-button acceptance baseline or automatic visual approval. S10 additionally records and rehashes its sealed triple, asserts the exact title `Having trouble generating summary` and exact subcopy `It could be a lost connection or something else. Could you check your connection or try again later?` for all six visible error categories, and proves that the rendered terminal error enables Complete Wrap-Up for the committed reason. After Playwright exits successfully, acceptance invokes `validate-ux-visual-comparison --ux-input-root "$TARGET_WORKTREE_ROOT"` once as the final visual gate and authoritative receipt producer; the suite never invokes it. The loop stops after at most five complete acceptance attempts; an unresolved source/platform discrepancy fails release with a concrete classification. A missing/mismatched target-worktree source does not enter D3 or the visual loop. + +“Declared state/variant dimensions” in that plan means the exact CSS root plus the per-source scale/crop tuple above, never the PNG dimensions divided into a replacement component width. `driveAISummaryState` owns the pointer/anchor/portal readiness sequence, and `recordAISummaryVisual` refuses to capture until its deterministic metadata is complete. The comparator preserves raw `target.png`, performs only the declared 2-to-1 source normalization, applies the predeclared correction and semantic-token masks, and computes separate color-independent foreground-coverage and non-empty literal-RGB results. D9 recomputes the normalized hashes, mask identities, structural evidence, coverage scores, and literal result/null reason; it rejects reliance on a legacy/global pixel percentage as sole acceptance. + +The `mid-call:transfer-summary-panel` record is an additional structural-only entry alongside the nine extrapolations. It records `aiSummary.midCall.transferHeading`, the final exact product-confirmed string, the non-empty confirmation reference, and action-parity rationale; it has no `extrapolatedFrom` or UX-001 authority field, and D8 fails while confirmation is absent. The same mandatory inventory also contains `mid-call:initiator-unavailable`, `post-call:unavailable`, `post-call:feedback-pending`, `post-call:feedback-not-confirmed`, and `mid-call:request-pending-disabled-initiation` with both `CONSULT` and `TRANSFER` action tuples, plus the receiver/lifecycle/ownership states enumerated above. The two feedback records name `aiSummary.feedback.pendingSubmission` / `aiSummary.feedback.submissionNotConfirmed`, assert their exact strings, resolve both Like/Dislike `aria-describedby` references to the one visible description, distinguish enabled pending from read-only not-confirmed controls, and reject live-region semantics. Each classification carries its exact reason, mode/action tuple, copy and semantic assertions, carries `extrapolatedFrom` only where the table requires it, and produces DOM/accessibility evidence without a fabricated pixel score. Only source-bound S10 also has a separately classified diagnostic PNG and measured diff; no source-less structural state acquires a pixel target. + +## Cross-Cutting Concerns + +- **Configuration and rollout:** There is no widget-side feature flag or staged fallback. Eligibility requires voice media, the current authorized agent, a matching strict interaction capability, and the applicable lifecycle/content. Deployment is gated by the sealed SDK artifact, D0a's target-worktree UX receipt, and D8b's publishable-package boundary verification. Every voice Consult/Transfer popover adopts the one UX-001 destination layout; when capability is absent/false, only the summary subtree remains absent and the same destination data/actions continue. Other existing behavior remains when the capability is absent/false or the optional callback is unset. `EXT-SDK-CC-SUMMARIES` is the explicit external blocker owned by the Webex JS SDK Contact Center maintainers. Its required input is the clean source commit exposing public `task:featureEnablement`, the unified summary methods and types, exact optional response `timestamp`, strict receiver action correlation, the admitted 15-second cleanup behavior, and same-Task post-wrap-up send callability. D0 assigns a deterministic version only in the isolated stage. D0 clears only after the named source/package/archive/declaration/timestamp/correlation/timeout/send probes all pass against one sealed artifact. Before that exit, the exact executable set is D0a, raw-fixture D1, SDK-free adapter/type/reducer D2, and SDK-free D3 after its direct D0a/D2 prerequisites, with D1 carried transitively through D2; D2b and D4-D9 fail closed. +- **Persistence and migration:** Summary state remains in MobX memory only. The vendored SDK tarball and private root resolution are repository-local development evidence; publishable workspace manifests retain plain registry SemVer and D8b proves actual packed packages contain neither local descriptors nor the tarball. No data migration is introduced. +- **Concurrency:** Every Promise/event captures canonical interaction ID, agent ID, ownership generation, role, and a pending request sequence; initiating request records also capture their validated action type. Each request record additionally captures the admitted monotonic `deadlineAt` and an initially open `settlementClosed` marker. Before normalization or timestamp comparison, a handler must atomically claim that marker. Surfacing the SDK's 15-second timeout closes it first; a handler arriving past the deadline closes it through the same timeout path. Every later success or failure for that request is dropped as stale regardless of its `timestamp`, so a late success with the greatest value cannot render after timeout. This fence schedules no widget timer and remains captured after pending cleanup. Runtime settlement next requires the payload `conversationId` (where present) to equal the captured canonical ID and the captured owner key to remain current. A confirmed handoff/conference boundary compare-and-swaps to a new generation before late prior-phase work can commit; same-agent successor creation carries each role view's counters/content forward independently, while agent replacement resets them. The request sequence pairs Promise cleanup and suppresses an older timestamp-less rejection after a newer same-role request starts; outside the terminal timeout fence it neither establishes successful response freshness nor replaces the generation guard. The exact optional SDK success field is `timestamp`: when both same-owner-role candidates carry a valid value, greater wins and equal uses later arrival; when either omits it, later owner-role arrival is the deterministic fallback. Feature `actionTimestamp`, request sequence, and local wall-clock time never substitute for the response field. There is at most one mid-call preparation request per initiating owner/action preparation: reopening while it is pending observes the existing request, and reopening with its current settled state reveals that state without an SDK call; only a genuine new owner/action preparation may start another request. Its pending state disables a different initiation, not close/reopen of the same preparation. Post-call explicit requests are individually issued but aggregate to one role-scoped `requestPending` Boolean and one visible generating state; each removes only its own pending sequence and Retry/re-entry remains disabled until the aggregate reaches zero. No `AbortController` is passed because the SDK contract has no cancellation; timeout-closed, ID-mismatched, and invalidated-owner settlements are caught and ignored. Requests, mid-call feedback, and the pre-telephony response all fulfill closed `outcome` objects for success/failure/block/staleness, with raw categorization internal. The auto-start call site nevertheless installs explicit fulfillment/rejection observers immediately. Every internal pre-telephony caller awaits the result before the telephony Promise is created; while response submission is pending, no consult/transfer/conference API is called. A failed, blocked, timed-out, or stale result still continues to exactly one telephony attempt after settlement and therefore cannot cancel it. Each accepted status transition increments the Store-lifetime sequence and appends to the immutable observable backlog; batching may coalesce renders but cannot coalesce queue entries. CallControl reads the backlog during its `observer` render and its effect exact-head-acknowledges and drains the rendered snapshot, so transitions recorded while unmounted remain pending and acknowledged transitions cannot replay. The complete-wrap-up Promise always settles as `WrapupCompletionResult`; a response rejection is a successful-wrap-up result and cannot enter wrap-up error handling. No Promise is left unobserved. +- **Security and privacy:** Boundary validation allowlists fields and section keys. Typed/plain values render as text. Cards cross only the existing restricted `AdaptiveCardRenderer` boundary, whose pre-parse image policy permits the exact bundled values returned by the closed Momentum icon resolver and blocks every caller-supplied remote, relative, `blob:`, `file:`, `cid:`, or arbitrary `data:` source. Disallowed image nodes/properties are removed before Adaptive Cards can create an image request; remaining text renders, while a sanitized-empty image-only card renders the exact unavailable fallback. Clipboard export requires a direct gesture and copies text only; after `navigator.clipboard.writeText` fulfills, clipboard lifetime is exclusively the operating system's and the widget schedules no clearing write. Host callbacks, logs, error boundaries, telemetry, URLs, test names, and visual fixtures contain no live summary/customer/interaction/agent content. Browser and visual captures use only deterministic synthetic D1/D8 fixtures; production/customer summary data is never written to a screenshot. Agent identity is an in-memory key and never rendered/logged by this feature. +- **Errors and recovery:** Unauthorized/init hides; valid-but-unrenderable shows unavailable; every visible `disabled`, `offline`, `unavailable`, `empty`, `timeout`, or `generic` generation failure shows the exact `Having trouble generating summary` title and source-observed connection/retry subcopy. That common description is intentionally non-diagnostic: `or something else` avoids claiming network causality and `or try again later` remains valid recovery guidance for non-transport categories. Raw SDK and Clipboard errors are normalized and discarded. For a current SDK generation or response failure only, StoreWrapper emits one lowest-level `ILogger.trace` diagnostic containing the already-defined closed normalized category plus static module/method context; Clipboard, blocked, stale, and discarded paths do not log. There is no automatic generation retry, cancellation, or response resend; reopening a settled mid-call preparation is a reveal, not a retry. Post-call Retry is a new request and remains disabled while the role-scoped pending aggregate is nonzero. Request, mid-call feedback, and pre-action response throws/rejections/timeouts fulfill sanitized results; the mid-call open/effect request also has an explicit rejection observer that maps to normalized state and resolves, and a pre-action failed/blocked/stale outcome is awaited and never aborts the one consult/transfer/conference action that follows settlement. `AISummary` catches a Clipboard synchronous throw/rejection and a direct consumer's synchronous throw or rejected `onFeedback`/`onRetry` Promise. Clipboard rejection invokes no recorder, changes no failure-driven visible state or copied counter, exposes no raw reason, and cannot become unhandled; rejected feedback/Retry leaves controlled visible state unchanged. Revision/owner/ineligible/frozen rejection of post-call feedback returns `false` without mutation. A wrapped-up summary-transition throw is sanitized and consumed so `refreshTaskList` still runs. Wrap-up failure fulfills the closed failed result and retains the entire user action; after wrap-up success, response failure sets only `response-failed`, clears the completed reason through the successful-wrap-up result, and retains the exact frozen response until the matching backend event or interaction/session cleanup. +- **Observability:** Existing `withMetrics` wrappers, `metricsLogger`, and logger interface remain unchanged. Summary paths add no event, metric, identifier, dimension, duration tracking, or failure vocabulary. The explicit product decision is to permit one content-free debug-equivalent call through the existing lowest-level `store.logger.trace` only after a current SDK generation or response failure has been reduced to an existing `AISummaryErrorCategory` or `response-failed` literal; the message has a static prefix, the context has only static module/method values, and no summary/customer text, raw error, payload, interaction ID, agent ID, timestamp, or duration is passed. A privacy unit test seeds distinct sentinels in all forbidden inputs, asserts the exact trace arguments/category, and rejects every sentinel; no `withMetrics`/`metricsLogger` call occurs. Release evidence remains build/test provenance, not runtime telemetry. +- **Compatibility:** Existing custom-element tags, existing r2wc props, Task consult/transfer calls, host callbacks, real-time-assist behavior, theme tokens, package export names, ESM/CommonJS transpilation, React 18/MobX ownership, Yarn 4.5.1, and the checked-in `.yarnrc.yml#nodeLinker` value `node-modules` remain. D0 verifies that resolved value with `corepack yarn config get nodeLinker`, consumes the committed SDK tarball with `corepack yarn install --immutable`, and verifies the resulting node-modules install with `node tooling/src/ai-summary-sdk-lock.js verify-sdk`; PnP is not an accepted mode for this design. The new callback is optional and exists only as a property on call control; its typed bridge preserves the legacy r2wc prop map and adds no observed/reflected attribute or global-name resolution path. Receiver state reaches the exact `View summary` action through the existing AI Assistant trigger slot and reaches the host status callback through the store, while the AI Assistant element's public properties/callbacks/events remain unchanged. The internal pre-action handler's Promise return is not treated as provider-only source compatibility: every internal destination, Transfer/Transfer Conference, and Merge call site is async, awaits its settlement before invoking telephony, and has a pending-order assertion. Separately, changing exported `wrapupCall` from `void` to the always-resolving `Promise` breaks direct React consumers of the `@webex/cc-task` and `@webex/cc-components` props surface; those consumers must return/await the Promise and return the closed result rather than reject, and the affected packages require a SemVer-major release with semantic-release breaking-change metadata. No `any` type is added; D0 removes the need for SDK contract casts. +- **Accessibility/localization/theme:** Message IDs are stable en-US constants but no translation stack is added. In particular, store/public props expose `pending`/`not-confirmed` tokens and `ai-summary.constants.ts` owns the structural-only `aiSummary.feedback.pendingSubmission` / `aiSummary.feedback.submissionNotConfirmed` descriptions plus their exact strings. While either description exists, both Like/Dislike controls directly reference its instance-local ID through `aria-describedby`; confirmed success removes the node and references atomically. Percent-wrapped evidence slots are resolved before constants or fixtures reach the component, so visible copy and accessible names never contain the delimiter markers or vary by visual state. Dynamic content uses `dir="auto"`; surrounding RTL layout is not implemented. No feature live region exists. Focus recovery, tooltips/names, descriptions, non-color selection, forced colors, contrast, keyboard order, reduced motion, and responsive vertical overflow are automated gates. Existing default/dark/customer theme behavior is inherited; no feature theme map is created. +- **Resource lifecycle:** Each exact Task listener set is seeded from public `aiSummaryCapabilities` and binds `TASK_EVENTS.TASK_FEATURE_ENABLEMENT`; replacement/removal and `cleanUpStore` detach its exact Task/callback pairs. Task replacement also rebinds the exact receiving callback through ordinary task lifecycle handling. Wrapped-up cleanup instead belongs to the Store/session-owned owner-keyed registry: it attaches the stable idempotent owner callback to a replacement before ordinary teardown, retains every required exact Task/callback binding independently of `taskList`, and generic Task removal cannot detach it while the same interaction has a pending/frozen failed response. The first matching `AgentWrappedUp` isolates and consumes summary-transition failure, always attempts `refreshTaskList` from `finally`, and detaches all bindings for that owner from an outer `finally`; if the interaction changes first, the per-interaction sweep clears the tombstone and bindings. `cleanUpStore` detaches every remainder, clears all content-bearing memory and the pending content-free delivery queue as the global privacy boundary, and retains only the monotonic status, content-revision, and post-call-generation counters. CallControl uses only its ordinary observer/render/effect lifecycle: unmount or callback removal invokes nothing and leaves same-session entries pending, while exact-head acknowledgement releases each delivered entry. The Web Component property bridge unsubscribes its `useSyncExternalStore` listener on React unmount and keeps host state in a WeakMap, so a detached element is not retained; reconnecting the same live element preserves its assigned JavaScript property. The one copy-success timer is cleared on timeout settlement, copy-control focus loss, a second gesture, accepted summary replacement, and unmount; the unmount path performs no state update. DOM card content clears on unmount. Store maps clear on their specified boundaries. Temporary SDK build/extraction and visual output directories are cleaned or retained only as sealed test evidence outside production assets. No worker, stream, socket, or daemon is added by the widget. +- **Performance:** Content state is bounded to active/current owners plus at most one failed post-response tombstone and its owner-keyed binding set for the current interaction; a matching wrapped-up event or interaction/session change clears both, while ordinary Task removal cannot strand them. The status backlog holds only content-free two-field details, removes each entry immediately on acknowledgement, and is cleared at session cleanup; it deliberately has no latest-wins coalescing or capacity eviction that could lose AC-08/AC-09 delivery. Pure normalization runs once per candidate; components consume observable view models rather than duplicate bodies. Long content uses browser layout/scrolling. No polling, client retry loop, external subscription, or conference-event request fan-out is added; participant addition reuses one explicit consult-preparation request and SDK-delivered per-agent content. + +## Test Strategy + +**D7 contract-test traceability.** Item 10's concrete runtime paths are `packages/contact-center/task/tests/CallControl/index.tsx` and `packages/contact-center/cc-widgets/tests/wc.ai-summary.ts`; its compile-only project is `packages/contact-center/cc-widgets/tsconfig.test.json`. The cc-widgets file is both the focused Jest path and a `tsc --project tsconfig.test.json --noEmit` fixture imported only through the `@webex/cc-widgets` public entry. It uses an exact type-equality assertion for the six-member `AISummaryStatusDetail` union plus `@ts-expect-error` assignments for mid-call `submitted`, mid-call `response-failed`, extra callback-detail keys, and forbidden values; an unexpected widening makes those directives unused and fails compilation. The D7 gate is runnable by path as `yarn workspace @webex/cc-task test:unit --runInBand --runTestsByPath tests/CallControl/index.tsx`, followed by the task and cc-widgets builds, then `yarn workspace @webex/cc-widgets test:unit --runInBand --runTestsByPath tests/wc.ai-summary.ts`; that cc-widgets script runs the no-emit compile before Jest. + +**D8 browser-clock prerequisite and proof boundary.** Item 11 requires `@playwright/test >=1.45.0`, the first release with `page.clock`; the current lock resolves `1.51.1`. The timeout journey installs `page.clock` before navigation and before `installAISummarySDKMock` creates any timer, holds the mock-backed request through 14,999 ms, advances the final 1 ms without a wall-clock wait, and asserts the widget's pending-to-timeout handling and late-settlement suppression. This browser case proves widget behavior against the installed SDK mock only. Item 2's packed-runtime test remains the sole proof that the real SDK enforces its 15,000 ms timeout and cleans up its timer/listener resources. D8 therefore requires the clock-capable runner, its matching Chromium binary, and the Playwright-managed sample-app server as separate prerequisites. + +1. **UX source admission (`tooling/tests/ai-summary-sdk-lock.test.js`, D0a):** from the target-branch worktree, resolve and hash all ten scene/text/PNG triples; match every file plus UX manifest `58d925e6d12c377589120f39fc170f1d80692d1901522fce0959b70e17f8c1b4`, source manifest `a266410383fc773812cd0a3352313d8db5e3e3828d5e584aaeee1df42709f162`, and source identity `50073d2c12591ac4c682e8599c1ecae9f2797cae9ed85542bfebf068b5ebfecb`; reject missing, parent-only, symlinked, escaping, or mismatched inputs; and prove atomic no-write-on-failure before D3. +2. **SDK declaration/runtime contract (`packages/contact-center/store/tests/ai-summary-contract.ts`, with orchestration coverage in `tooling/tests/ai-summary-sdk-lock.test.js`, D0):** after the external owner supplies a candidate, read its Yarn pin at the recorded commit, run Corepack with `cwd` fixed to that checkout and the allowlisted isolated environment, require the observed version to match both the pin and staged `toolchain.yarn` before build, and build/pack without the upstream full test gate. All widget-side Yarn children run from the canonical widgets root after the root pin check; the test reads `.yarnrc.yml#nodeLinker`, requires `corepack yarn config get nodeLinker` to equal `node-modules`, and rejects the higher-level PnP label as evidence. With the preserved store registry-semver descriptor, committed private-root resolution, tarball, and lock in place, the clean-checkout consumption gate runs the exact root sequence `corepack yarn install --immutable && node tooling/src/ai-summary-sdk-lock.js verify-sdk` and proves the verifier resolves the complete installed tree through the `@webex/cc-store` node-modules dependency. Run the SDK-importing test only through `corepack yarn workspace @webex/cc-store`, whose manifest retains the plain registry-semver `@webex/contact-center` declaration while the private-root override selects the sealed archive. Fail separately on an absent packed `dist/types/index.d.ts`, an unresolved reachable declaration graph, or a hash mismatch. Require `artifact.files` to cover every safe packed regular entry and compare the exact installed package-owned path/hash set, including non-declaration files. The named test loads runtime constructors from the canonical temporary tarball extraction rather than source or registry code; compiles the exact four signatures, fixture assignments, adapter outputs, payload/event imports, reachable `WrapupPayLoad.auxCodeId`, and required `AISummaryResponse.wrapUpCode` against the packed declarations; and uses a real packed `Task`/`ApiAIAssistant` with typed contact/Webex service fakes and a no-network request mock. Resolved HTTP 202 and HTTP 503 results prove send success/non-success handling and captured bodies prove SDK-injected identity/`agentName`. Modern fake timers hold the request through 14,999 ms, reject it at 15,000 ms, and prove zero timers, baseline listeners, `not-found` late resolution, and no second settlement without a wall-clock wait. The same concrete Task awaits its inherited successful `wrapup` with a human-readable reason and a deliberately different auxiliary-code identifier, then receives exactly one `sendPostCallSummaryResponse` whose `wrapUpCode` equals that committed identifier and not the reason; an invalid-state/non-callable rejection or mismatched captured code fails SDK admission. The root tooling suite never imports the SDK; with child processes stubbed, it proves every argv/cwd/env tuple, contamination stripping, package/version/pin checks, distinct archive/declaration failures, byte-identical store registry-semver descriptor plus private-root override, no second or local-protocol workspace declaration, exactly one checksum-matching local lock record for every descriptor, complete installed-file verification, extraction cleanup on success and injected failure, lock/install/post-replacement transaction rollback, absence of any upstream full-suite spawn, and fail-closed no-fallback behavior. Named cases also require `build-sdk` to reject a `WEBEX_JS_SDK_DIR` whose canonical path is not recorded in `source.audit.repositoryPath`; require separate missing- and unknown-subcommand `runCli` cases to set a nonzero `process.exitCode` and print only a content-free category; remove every temporary directory on the failure path; and assert output contains no SDK path, source text, or raw error. +3. **Raw fixture build (`test-fixtures/src/aiSummaryFixtures.ts`, D1 / AC-16):** typed/plain initiating, adaptive receiver, and structured/plain post-call cases are readonly and synthetic. The structured group contains an otherwise-identical pair: `resolutionPresent` carries a non-empty top-level `resolution` value for the read-only `Outcome` projection, while `resolutionAbsent` omits the optional key entirely; empty/`undefined` normalization remains a separate malformed/empty boundary case. The remaining fixtures cover strict feature flags, every sanitized error, malformed/unsupported/empty values, lower/equal/higher timestamps, A-B-C, consult-to-transfer, conference join/leave/two-party host, feedback, counters, timeout, wrap-up with distinct display reason and auxiliary-code identifier, explicit resolved-wrap-up-then-send success asserting response `wrapUpCode === auxCodeId` and `!== wrapUpReason`, unsupported traces, response failure, and AgentWrappedUp. The fixture workspace build proves discoverability/export correctness without importing the SDK; D0 later proves public-type compatibility and executes the post-wrap-up sequence against the packed runtime. +4. **Adapter and store unit/event routing (`store/tests/ai-summary.ts`, `store.ts`, `storeEventsWrapper.ts`, and `task-utils.ts`, D2/D2b / AC-16):** before D0, test stable-main candidates, conflict/absence fail-closed behavior, explicit `conversationId` mapping, mismatch discard, matching error results retaining `interactionId`, normalized models including the closed `AISummaryActionType`, `AISummaryFeedback`, and `AISummaryFeedbackStatus` exports, source payload section order preserved even when it differs from presentation-metadata definition order, unknown section keys dropped without reserving a position, owner/timestamp reducers, same-agent per-role counter carry-forward, different-agent counter reset, role precedence, simultaneous initiator/receiver views under one owner, role-discriminated edit/view/copy isolation, valid `recordAISummaryCopied` calls incrementing only the targeted role's copied counter exactly once while stale/invalid calls do not increment, keyed mid-call section composition in normalized SDK order, the three closed sanitized store-action result types, and sanitized errors. After D0, test the one extracted/exported voice predicate; voice/capability/authorization conjunction; capability arrival before Task listener registration; nonmatching capability unavailability without re-key; invalid untimestamped capability before/after a valid record never enabling or evicting it; ordered valid false revocation; initiating, receiver, and post-call mismatch no-mutation; exact Task feature callback rebinding and `cleanUpStore` removal; receiving malformed-event attribution by captured interaction/agent/generation/role, stale-owner discard, accepted-content preservation, and unsupported-event unavailable projection; same-agent remount/hold/task-clone retention; handoff, consult-promotion, conference-establishment, and later topology-cycle generation advancement; duplicate-boundary idempotence; stale prior-generation settlement discard; exact receiving-Task callback bind/rebind; Store/session-owned owner-keyed wrapped-up registry binding; replacement attach-before-ordinary-teardown; `task-removal-before-AgentWrappedUp` with the Task already absent from `taskList`; matching-event cleanup-before-refresh plus all-binding detachment; an injected throwing summary transition still invoking refresh before all-binding detachment; event-before-response-rejection cleanup; per-interaction eviction of capability/current-owner/all-role-view/pending/latest-request/failure/registry entries with late-settlement and late-event no-recreation; global cleanup of retained callbacks/maps while the status sequence remains monotonic and the pending status queue is discarded; every remaining retention boundary; every explicit SDK call; request, feedback, and pre-action synchronous-throw/rejection/timeout normalization into always-fulfilled outcomes; exact `ILogger.trace` arguments for each existing normalized generation/response category with forbidden summary, interaction, agent, raw-error, timestamp, and duration sentinels absent and no metrics call; initiator action type owned by its accepted request and re-evaluated on consult-to-transfer promotion; mid-call feedback requires the explicit rendered action type plus revision and makes no SDK call for either mismatch, including a CONSULT click queued across TRANSFER promotion; indeterminate initiator/receiver correlation block-and-do-not-paint; no conference-event request fan-out; no auto retry/cancel; timeout/late/unmount races; concurrent post-call pending aggregation with one generating projection and final-settlement clearing; post-call draft/revision/counter/pending-feedback survival through non-accepted reason regeneration, older-generation staleness before timestamp ordering, and current-generation replacement resetting feedback while preserving counters; capture during replacement pending making the late settlement stale; feedback sequencing; current-revision post feedback success plus stale-revision/advanced-owner/ineligible/frozen false-with-no-mutation cases; exact response unions including edited/cleared keyed mid-call sections; captured/frozen `wrapUpCode` equality with the committed `auxCodeId`; frozen response once-only behavior; one status sequence increment and one ordered append per transition; two appends retained in one batch; exact-head acknowledgement removing only one entry; unacknowledged retention through ordinary keyed cleanup; and no unhandled rejection. + Mandatory named D2/D2b cases additionally distinguish role-compatible success from role-incompatible successful envelopes for initiator typed/plain, receiver adaptive-card, and post-call typed/plain normalization; assert unavailable copy rather than generation error; prove like/dislike exclusivity, successful replacement, and same-option re-selection issuing another SDK update; prove capture-token opacity/task binding; prove the mandatory `task-removal-before-AgentWrappedUp` race retains the logical Store/session subscription, accepts the event from the removed Task, clears exactly the matching tombstone, and detaches every binding; and prove an X wrapped-up event cannot mark or discard Y's failed tombstone. + + D1's `conferenceTwoPartyHost` member is consumed, not declaration-only. D2b's named store integration case `conference leave to two-party call selects the oldest remaining initiating host once` drives its join, participant-left, and conference-ended topology sequence under one canonical interaction. The fixture's authenticated local agent is the oldest remaining initiating agent and its SDK-owned topology projection identifies that agent as the host when exactly two participants remain. The test asserts a call projection, selection of that host's owner key, exactly one ownership-generation advance across duplicate leave/end notifications, same-agent content/counter carry-forward, and rejection of departed-agent, non-host, and predecessor-generation settlements. It does not derive or override host order in UI code; the Task/SDK topology remains authoritative. + + The phrase `every remaining retention boundary` above is a closed reference to these mandatory, separately named D2b store cases: + + | Named store case | Required assertion | + |---|---| + | `hold/resume and same-agent remount retain mid-call state` | Content, counters, feedback, revision, and the current owner generation survive hold/resume, a component remount, and a pure Task clone without a new request. | + | `task end retains mid-call state` | The ordinary task-end/removal path does not clear the interaction's mid-call role views, counters, feedback, or still-observed request/cleanup state. | + | `wrap-up completion clears mid-call state` | The matching production wrap-up-completed boundary removes all mid-call role views, pending work, and retained bindings for that interaction, and a late settlement cannot recreate them. | + | `interaction change clears prior-interaction state` | Selecting a different canonical interaction evicts every prior-interaction capability, owner/role view, pending/latest request, failure, capture, frozen response, and retained callback entry while preserving Store-lifetime monotonic guard counters. | + | `agent sign-out clears session summary state` | The explicit sign-out route independently clears all content-bearing maps, pending requests, frozen/tombstone state, retained listeners, and the undelivered status queue; late callbacks are inert and monotonic sequence/revision/generation guards are not reused. | + | `SDK session end clears session summary state` | The distinct SDK session-end/client-teardown route performs the same global privacy cleanup and exact listener detachment without relying on sign-out having run. | + | `submitted post-call state remains until interaction change` | A successfully submitted post-call summary remains projected across ordinary Task end/removal and is cleared only when the canonical interaction changes or the global session boundary wins. | + | `failed post-call response remains until matching AgentWrappedUp` | Task end/removal retains the exact frozen failed response and Store/session-owned binding; only the matching owner event, interaction change, or global cleanup discards it, and another interaction's event is inert. | + + Mandatory concurrency/contract rows are: + + | Named case | Fixture/probe owner | Required assertion | + |---|---|---| + | `response timestamp field present` | D0 declaration/runtime probe plus D1 ordering fixtures | `AISummary` request results and receiving-agent content expose exact optional `timestamp?: number`; a valid value is preserved unchanged and greater/equal/lower same-owner-role ordering uses greatest/equal-last-arrival. | + | `response timestamp field omitted` | D0 omitted-property runtime probe plus D1 ordering fixtures and D2 pure reducer | No timestamp is invented; when either comparable candidate omits it, later arrival within the exact owner-role wins, and no candidate is compared across owners. | + | `late-success-after-timeout` | D1 `lateSuccessAfterTimeout` fixture plus D2b store/event-routing unit | Hold the request through 14,999 ms, surface timeout at 15,000 ms, then deliver a same-request success with the greatest timestamp. Assert the request's closed marker drops it as stale with no content, lifecycle, counter, feedback, pending, status-transition, callback, or unhandled-rejection mutation. | + | `timeout-versus-on-time-success boundary` | D2b store/event-routing unit | A fulfillment that claims the request before the deadline may compete normally; a fulfillment observed after the deadline closes through the timeout path even if the SDK rejection callback has not run yet. Exactly one terminal result is surfaced. | + +5. **Shared component unit (`cc-components/tests/components/AISummary/ai-summary.tsx`, D3 / AC-16):** after D0a, test exact copy/tooltip names, shared role-neutral message IDs and exact `Additional context` / `Key Actions Taken` labels for both roles, source message IDs including `aiSummary.feedback.pendingSubmission` and `aiSummary.feedback.submissionNotConfirmed`, the uniform unresolved-percent-variable rule, resolved mid-call/post-call constants with no visible or accessible `%` marker, the full `aiSummary.generatingDescription` value `Just a sec—the details are coming together.` with U+2014 at its separator index and no U+005F, render/copy order following a normalized section array deliberately ordered differently from the metadata tuples, `POST_CALL_DISPLAY_SECTION_ORDER` containing every `AISummaryDisplaySectionKey` exactly once, present-resolution insertion by ordinal without reordering SDK sections, key-omitted/`undefined`/empty resolution producing no Outcome row/copy block/placeholder/divider/reserved space, the single visual initiator editor with per-key controlled values, exact-key edits and clear-to-empty without label/delimiter parsing, read-only Outcome display/copy only for the present case, plain rendering, HTML injection resistance, `dir=auto`, all three mode discriminants, receiver action-only mode accepting no card JSON/renderer child, the synchronous receiver visible-text getter, component-owned Clipboard Promise success/reject, boolean post-fulfillment recorder acceptance, visual-state timer cleanup, compile-time rejection of mode-incompatible props, sticky mutual feedback, synchronous-throw and rejected-Promise `onFeedback`/`onRetry` cases with unchanged controlled state and zero unhandled rejection, token-to-message mapping plus removal after success, unsupported/error states, `onFocusCapture` successor snapshot/fallback/no-spurious-move, no live region, overflow, and forced colors. +6. **Receiving integration (`ai-assistant/tests/ai-assistant/index.tsx`, `helper.ts`, cc-components AI Assistant tests, `adaptive-card-renderer.utils.test.ts`, and `adaptive-card-renderer.test.tsx`, D4 / AC-16):** test content-event-only eligibility, unauthorized/initialization failure hiding both notification and panel branch, store-to-existing-chrome notification ownership, exact `View summary` name/tooltip, closed/minimized open/restore/view counter targeted to receiver role, focused notification activation advancing to the first open-panel focusable control and then `[data-testid="ai-assistant:panel"]` fallback, `AIAssistantComponent` rendering the display-only `AdaptiveCardRenderer` and shared `AISummary` receiver action row as siblings, renderer containment excluding every receiver view/copy/like/dislike control, unchanged `AdaptiveCardRendererProps` and security ownership, existing render/fallback props only, and `extractCardText` newline order excluding sibling action labels. The closed image-policy fixtures prove recognized icon basenames rewrite only to exact bundled resolver values; every unrecognized HTTP(S), protocol-relative, relative/root-relative, blob, file, cid, and caller data source is absent before parse/DOM creation; the remote-image-with-text card makes zero request while retaining its text/labeled action; and the remote-image-only card makes zero request and renders exact `The summary is not available` rather than blank content. Also test AISummary-owned copy plus post-fulfillment receiver recorder/counter targeting, rejected Clipboard write with the copied counter unchanged, no success paint, and zero `unhandledrejection`, 200-confirmed feedback through a store-composed `AISummaryResponse` and the sole `capturedTask.sendMidCallSummaryResponse(payload, resolvedActionType)` SDK call, rejected SDK/direct-consumer feedback Promise leaving the previous selection painted with zero `unhandledrejection`, unknown action type disabling the shared row with native disabled semantics and sending nothing, same-owner initiating-view isolation, inert embedded card actions, bundled-icon RTA behavior, current task-bound malformed/unrenderable event surfaces, stale event-error omission, cross-agent immediate clear/zero counters, same-agent conference pending/failure retention until valid replacement, connected-focus retention when only the renderer sibling is replaced, captured-successor/panel-root focus whenever the focused receiver control disappears, RTA isolation, unmount cleanup, unchanged host surface, and unavailable fallback. +7. **Initiating feature (`task/tests/ai-summary-mid-call.ts` and the focused consult-transfer popover test, D5 / AC-16):** test one unconditional voice radio-category/search/results renderer with identical filtering, pagination, and destination selection whether the summary is present or omitted; the voice radio and retained non-voice pill branches deriving identical `availableCategories`/`handleCategoryChange` filtering results; conditional summary placement after results; canonical `requestMidCallSummary` callback naming; resolved delimiter-free search and consult/transfer heading constants; the 80vh popover cap and bounded list/content scroll regions with wrapping auto-sized editors and visible headings/actions; late enablement/revocation preserving destination node identity, values, and focus; interaction-switch reset; consult/transfer request types; hidden versus unavailable states; pending activation represented with focus-retaining `aria-disabled` plus guarded pointer/keyboard handlers rather than native disable; focused-control summary removal moving to the captured successor or the named `.agent-popover-content` target; whole-popover close returning focus to its exact trigger; content replacement leaving connected focus unchanged; the open/effect request's immediate explicit fulfillment/rejection observation; timeout/generation-failure plus feedback synchronous throw or returned-Promise rejection normalization with zero `unhandledrejection`; pending close/reopen reattachment with no second SDK call; settled same-owner/action reopen revealing current state with no implicit request; new preparation requesting once; presentation-only structured collapse; the sole `mode: 'mid-call-initiator'` presentation discriminator; imports of `AISummaryActionType`/`AISummaryFeedback` from `@webex/cc-store` with no local literal aliases; role-targeted exact-key edit/clear and plain `summaryText` round trips; role-targeted view/post-fulfillment revision-only copy notification with no container Clipboard call; feedback forwarding of the rendered action type/revision; stale action/revision no-send/no-paint; same-owner receiver-view isolation; owning-request action type; consult-to-transfer promotion re-evaluation; indeterminate action type block-and-do-not-paint; the no-destination `Transfer`/`Transfer Conference` surface requesting TRANSFER once and its confirmation invoking the response before one `task.transferConference()` call; the `Merge` surface reusing CONSULT without another request and its confirmation invoking the response before one `task.consultConference()` call; action-surface close invoking neither method; omitted-summary legacy immediate actions; response-send settlement before one telephony invocation; a deliberately pending send keeping every consult/transfer/conference spy at zero calls; `sent`, `failed`, `blocked`, and `stale` each settling before exactly one continuing telephony call; observation of the outer orchestration and later telephony work; no cancel; direct/consult-to-transfer/additional transfer/conference flows; stable ID/owner guards; and unchanged telephony payloads/signatures. + The mandatory latch case opens an initially ineligible voice popover, types a query, chooses a non-default category and destination, and focuses the search/results tree before a matching capability event arrives mid-popover. It then exercises revocation and an ownership-generation update without closing. The `voice-radio` layout value, category/search/results node identities and order, query, selections, result set, scroll owner, and connected focus must remain unchanged while only the summary subtree mounts, rebinds, or unmounts. Close/reopen must derive a fresh latch, proving it is scoped to one popover instance. +8. **Post-call feature (`task/tests/ai-summary-post-call.ts` and the focused wrap-up-summary test, D6 / AC-16):** test eligible search/radio hierarchy and untouched legacy Select, named-evidence layout, late enablement/revocation reason and semantic-focus mapping, interaction-switch reset, search filtering without SDK request, the static non-live zero-match message from `aiSummary.postCall.noReasonMatches`, exact persistent `Search wrap-up reasons` input naming from `aiSummary.postCall.reasonSearchLabel`, the reason `RadioGroup` named by the `Choose a reason to wrap up` heading, pointer and Enter/Space commits, `arrow-key traversal of reason list issues one request`, activation/blur deduplication by owner plus selection revision, fresh Retry, immediate first-commit post-call `requestPending`, Retry/re-entry disabled while pending, concurrent direct requests collapsing to one generating panel until the final settlement, every response form, hidden versus unavailable states, expanded/collapsed state mapping, `reason change after edit does not silently discard the draft`, pending/blocked/stale/failed/unsupported replacement preserving the exact edited bytes and revision until a newer ready response is accepted, popover close/reopen retaining the draft while resetting reason controls, capture during regeneration freezing the visible draft and rejecting the late settlement, resolved delimiter-free `Search topic`/`Complete Wrap-Up` copy remaining stable across hover/confirmed states, `copy hover and confirmation never change the Complete Wrap-Up visible label or accessible name`, placement before Complete Wrap-Up, initial loading disabling Complete Wrap-Up only during the bounded no-draft request, `generation error still permits wrap-up completion`, Retry preserving that escape, eligible-without-draft success returning `not-required` with no capture/response send, `resolutionPresent position when adjacent sections are absent`, `resolutionAbsent total Outcome row/space/copy/response omission`, read-only present Outcome omission from response, expanded and collapsed 320px layouts sharing an 80vh shell with bounded reason/content scrolling, no horizontal overflow and a visible primary, the sole `mode: 'post-call'` presentation discriminator, current-revision local feedback success projecting `pending` and resolving `aiSummary.feedback.pendingSubmission`, stale-revision/advanced-owner/ineligible/captured-or-frozen feedback rejection with no pending/frozen-payload mutation, role-targeted edit/view/post-fulfillment copy-notification isolation, A-to-B-to-C wrap-up binding request/draft/capture/frozen response only to C's current generation with zero post-call counters and B's late settlement rejected, conference final-summary content preserving the common interaction ID and the SDK fixture's conference segment without widget-side assembly or truncation, wrap-up rejection fulfilling `{wrapup: 'failed'}` with draft/reason restoration, wrap-up success bookkeeping before the store-owned captured-task send, opaque token validation with no caller-visible snapshot/Task pair, distinct reason/id capture proving frozen `wrapUpCode` equals the committed `auxCodeId` rather than the display label, exact freeze and one response, response success fulfilling `submitted` and removing `feedbackStatus`, response rejection fulfilling `response-failed` without undoing wrap-up and projecting `not-confirmed` through `aiSummary.feedback.submissionNotConfirmed`, frozen failure no-resend/callback, different-interaction wrapped-up isolation, and matching backend cleanup. Its SDK mock assumes only the D0-admitted post-wrap-up callability and payload-declaration contract; D0 owns proof that the real packed Task remains callable and carries the identifier mapping. + D6 must consume D1's paired `resolutionPresent` and `resolutionAbsent` fixtures. The present fixture proves relative-ordinal insertion even when both neighboring ordinal sections are absent; the key-omitted fixture proves zero `Outcome` text/row/edit target/copy block/response key and no reserved layout space while the remaining SDK sections preserve their relative order. +9. **Shared CallControl join (existing task and cc-components CallControl tests, D6b / AC-16):** after D4/D5/D6 complete, test the one-time single-mode/feedback-status prop threading without redundant component kind/role or visible-copy props, the single mid-call destination renderer with conditional summary mount/removal, the post-call eligible/legacy transition, shared helper/container sequencing, including CallControl integration assertions that every widened pre-action handler call site is awaited, a deferred response keeps destination, Transfer/Transfer Conference, and Merge telephony mocks uncalled, and each closed success/failure outcome then permits exactly one correctly ordered SDK call, the exact always-resolving `Promise` exported signature and direct-consumer migration expectation, module-spec currency, unchanged host behavior outside the adopted voice UX-001 layout, and absence of overlapping files among the three fan-out tasks. +10. **Public type/runtime compatibility (`task` and `cc-widgets` tests, D7 / AC-16):** instantiate all six valid `AISummaryStatusDetail` literals; require negative compile-time failures for mid-call `submitted` and mid-call `response-failed`; import that type from the `@webex/cc-widgets` public entry in `wc.ai-summary.ts` and assign a no-`any` handler under the new no-emit test TypeScript project; assert all legacy custom-element props/tags; preserve the existing r2wc map and expose the new callback only through the typed JavaScript-property bridge; assert neither derived nor undashed lowercase callback attribute is observed, reflected, or able to resolve an arbitrary global; route a receiving-agent event from store to a mounted CallControl callback; and prove the AI Assistant host contract unchanged. Cover a store queue mutation firing without unrelated prop change, callback absence/removal without acknowledgement, authorization/init plus ownership/interaction boundaries producing no transition, the first accepted successor settlement producing exactly one mapped transition, exact-head acknowledgement before callback invocation, effect-local throw containment without ErrorBoundary involvement or retry and with continued drain, same-sequence/re-render deduplication, batched `available` then `unavailable` delivery exactly once each from one React commit, a `response-failed` transition recorded while CallControl is unmounted and delivered once on same-session remount, acknowledged remount non-replay followed by a higher transition, ordinary keyed-cleanup retention, global-cleanup discard with monotonic next sequence, and recursive rejection of forbidden callback keys/values. +11. **Browser journeys (`playwright/tests/ai-summary-test.spec.ts`, independently collected alongside `playwright/suites/ai-summary-tests.spec.ts`, D8 / AC-16):** `AI Summary Deterministic` exists only when `AI_SUMMARY_E2E=1` and has exact `testDir: './playwright'` and `testMatch: ['**/suites/ai-summary-tests.spec.ts', '**/tests/ai-summary-test.spec.ts']`, covering both directories. The OAuth setup project and every `USER_SETS`-derived pre-existing project have exact `testIgnore: ['**/suites/ai-summary-tests.spec.ts', '**/tests/ai-summary-test.spec.ts']`; the conditional project alone omits that ignore. The `playwright/tests/` file registers every browser journey and scoped accessibility case at top level, while the `playwright/suites/` file registers only visual/evidence-manager cases; neither imports or registers the other's scenarios. Unflagged project enumeration must omit the AI Summary project and collect neither path. Flagged all-project and project-specific enumeration of both explicit paths must contain only `[AI Summary Deterministic]` entries and list each spec exactly once; the D8 and D9 browser commands must name and execute both paths together with one worker. The scenarios use the closed D1-member selection tuple, including compile-time-connected timeout, stale-timestamp, ownership-change, and response-failure selections, then start with read-only verification of the D0a UX seal and run real-browser JNY-001 initiator and receiver plus JNY-002 generating/editing/hover/selected/copy/error/submission states, including immediate first-committed-reason generating state, disabled re-entrant Retry while pending, the 15-second hard bound, `generation error still permits wrap-up completion`, Retry not revoking that escape, and one panel under concurrent direct post-call requests; resolved delimiter-free source copy with copy hover/confirmation leaving the Complete Wrap-Up visible label and accessible name unchanged; reason-list expansion/collapse; static zero-match copy with no live-region semantics; exact persistent reason-search and heading-derived RadioGroup accessible names; arrow-key traversal followed by Enter/Space or focus exit issuing exactly one request for the final reason; reason change after edit/copy/local feedback retaining exact draft, counters, selection, and Pending submission until current-generation acceptance while a later/higher-timestamp superseded response is a no-op; paired present-resolution ordinal positioning and absent-resolution total row/space/copy/response omission; exact `View summary` trigger reachability and activation focus advancing to the open-panel header control or named dialog-root fallback; receiver card replacement retaining connected copy/feedback focus and unavailable/removal choosing the next document-order control or panel root; the open-instance-latched mid-call destination layout with summary absent/present and mid-popover capability arrival/revocation plus ownership-generation change preserving destination node identity/order/query/category/destination/focus while only summary descendants change; pending and settled mid-call close/reopen issuing no duplicate request; the post-call initial eligible/ineligible layouts plus live transitions; keyboard traversal and disappearing-focus recovery; expanded and collapsed post-call states sharing an 80vh shell at 320px with bounded reason/content scrolling, no horizontal overflow and Complete Wrap-Up visible; direct clipboard permission/success/failure with the copied counter unchanged while pending or rejected and incrementing exactly once only after fulfillment for initiator, receiver, and post-call; rejected receiver SDK feedback and Clipboard Promises producing no `unhandledrejection`; feedback; inherited forced colors; selector-scoped automated WCAG 2 A/AA accessibility that fails rather than widening when its root is unresolved; rapid interaction switch; lower/equal timestamp races; same-agent remount; zero telephony calls while the pre-action response remains pending, followed by exactly one continuation after sent/rejection/timeout/stale-generation settlement; cross-agent immediate clear/zero counters and same-agent conference pending/failure retention until replacement; normalized wrap-up/response failures; and global unhandled-rejection capture. These checks are the D8 verification owner for REQ-009 / AC-12. + The integrated mid-popover arrival case starts ineligible, types a query, selects category/destination, and holds focus before capability arrival, revocation, and ownership-generation change; it asserts the open-instance layout latch and destination node identity/order/value/focus invariants while only summary descendants change. Paired present/absent resolution scenarios assert deterministic insertion versus total `Outcome` omission/no gap. The generating state asserts exact `Just a sec—the details are coming together.` visible/accessibility copy with no U+005F, while all screenshot-backed and structural states reject percent markers and require the same canonical `Search topic` and `Complete Wrap-Up` strings, including the primary visible label, accessible name, and tooltip, regardless of visual state. Separate pending/not-confirmed journeys resolve both Like/Dislike `aria-describedby` IDREFs to the exact visible requirement-owned description, distinguish enabled from read-only controls, and reject live-region semantics. The Transfer journey cannot pass until its structural receipt binds `aiSummary.midCall.transferHeading` and the exact DOM copy to a non-empty product/content-owner confirmation reference. +12. **Visual fidelity (`playwright/visual/ai-summary-visual-cases.ts`, the independently collected `playwright/suites/ai-summary-tests.spec.ts` visual/evidence manager, the sample-host bridge, and MatrixBuilder visual receipts, D8 / AC-16):** before rendering, require the ten exact declared screenshot keys S01-S06, S07-UX, and S08-S10 and their already staged-and-sealed target-worktree scene/text/PNG triples to still match under `ux.resolutionRoot: "."`; missing, escaping, or drifted inputs fail without resealing or visual output. Derive `TARGET_WORKTREE_ROOT` and the common-checkout output root once, pass the former as the required absolute `--ux-input-root` argv to prepare/plan/export/compare/validate, and reject any plan/root/hash disagreement. The visual-receipt mapping must enumerate declared keys S01-S06, S07-UX, and S08-S10 exactly; only UX-007's declared `S07-UX` / `desktop-light` may map to local sidecar/render key `S07` / `dl`, and both names must resolve to the same sealed screenshot hash; raw scenario IDs remain descriptor-owned and final receipts must not carry schema-forbidden alias properties. Preserve each PNG as immutable `target.png` and compare the screenshot-backed declared keys S01-S06, S07-UX, and S08-S09 through the five-attempt structural/region-masked policy: exact DOM/order/boxes/copy/behavior/a11y, predeclared source-correction masks, predeclared semantic-token role masks, token identity/contrast/forced-colors checks, color-independent foreground coverage `<=0.5%`, and `<=0.5%` only for a non-empty literal RGB mask. A global/legacy whole-image percentage alone is invalid as visual acceptance; the existing flow's real whole-image measurement remains required diagnostic evidence for every sealed screenshot, including S10. S10 remains the sole sealed-source structural override and proves enabled terminal-error completion with no fabricated pixel score. The suite produces only per-tuple artifacts; acceptance alone invokes the validator once with the same input root. Snapshot-only assertions cannot satisfy this gate. + The exact complete structural denominator is the table under UX Evidence and Productionization. The completeness test expands every table row into its required mode/action tuples, deletes each tuple in turn from an otherwise valid registry, and requires failure; it also rejects an extra/duplicate tuple, absent Transfer confirmation, changed extrapolation, broken feedback IDREF, or missing named scenario. The three surface-specific last-focusable-control scenarios must resolve to `consultTransferPanelRef.current`, `panelRef.current`, and `wrapUpPanelRef.current`, respectively. + The screenshot harness pins the descriptor-declared `deviceScaleFactor: 2`, verifies the sealed CSS viewport, serves only manifest-hashed text/icon fonts from the local sample origin, awaits `document.fonts.ready`, and checks every required face. Hover/tooltip states also fix pointer/placement/portal geometry and settled animation. It validates every scale-2/root-relative crop tuple, performs the single specified 2-to-1 normalization, records base/source-correction/semantic-token/literal/foreground mask hashes and denominators, and refuses diff-derived masks. Tests fail before or during comparison on external/fallback fonts, geometry/viewport/crop drift, unstable tooltip/animation, unregistered masks, token/contrast/forced-colors failure, foreground-coverage failure, or a non-empty literal score above `0.5%`. A forced failure on the fifth complete attempt exits nonzero, emits no authoritative gate receipt, and leaves screenshot-backed states in their screenshot class. + +13. **Publishable dependency boundary (`tooling/tests/ai-summary-sdk-lock.test.js` and `tooling/tests/publish.js`, D8b / AC-16):** verify the private root development override never enters a publishable workspace manifest or actual packed package; require registry-resolvable SDK SemVer in each applicable packed manifest; exclude the vendored tarball from packlists; and prove publisher preflight rejects local protocols before any write or publish call. Retain six hash-bound non-published workspace tarballs plus one atomic verification receipt for D9. +14. **Privacy/provenance/release (`tooling/tests/verify-ai-summary-release.test.js`, D9 / AC-16):** the Jest suite supplies recorded receipt/hash fixtures to the read-only verifier functions and asserts their fail-closed parsing; it never spawns Yarn, Playwright, MatrixBuilder, packing, publishing, or the release CLI. The verifier scans production/log/callback/persistence surfaces, root resolutions, and publishable source manifests; verifies no summary telemetry or local-path dependency; and validates sealed development lock/install state and publishable packlists, DAG/source/spec currency, exact hashes, and SemVer-major/breaking release evidence. The D9 acceptance command separately executes the build/type/style/focused/full-unit, both-browser-spec/a11y/visual, publisher-preflight, and packaging gates, which emit current hash-bound receipts consumed by `verifyGateReceipts` and `verifyPublishableDependencies`; zero skipped mandatory suites and zero unhandled rejections remain required. The packaging receipt must cover actual packed `package/package.json` files for exactly `@webex/cc-store`, `@webex/cc-components`, `@webex/cc-task`, `@webex/cc-ai-assistant`, `@webex/cc-widgets`, and `@webex/test-fixtures`; every packed `@webex/contact-center` dependency/optionalDependency/peerDependency must be a plain registry-resolvable SemVer version/range, and any local `file:` tarball or path/workspace alias is release-blocking; the admitted tarball may appear only in the private-root pre-D8b resolution and never in a publishable source or packed manifest. Rehash the flow-authenticated release receipt, current worktree HEAD/ref, exact changed/evidence file set, and requirement branch scalar; accept a direct target branch or only the anchored MatrixBuilder transient pattern while independently requiring the resolved target `ai-assistant-summary`. `verifyUXEvidence` must reject executable/structured remote Figma MCP endpoint, tool-call, or node-fetch provenance even when local hashes pass. Named failures cover the wrong resolved target, arbitrary transient name, HEAD mismatch, remote-MCP evidence, and an otherwise valid local-only evidence set as the clean case. + + D9 also recomputes each raw-PNG dimension/scale/crop identity, base/source-correction/semantic-token/literal/foreground mask table, mapped-node geometry, token identity, coverage result, and literal score/null reason; it rejects reliance on a global pixel percentage as sole acceptance, an unrecorded resize/translation/trim, diff-derived/expanded exclusion, or S06/S08 component-width substitution. It independently requires exact delimiter-free, state-invariant copy so masks cannot waive production semantics, one receipt for every exact structural table tuple, both feedback descriptions with valid direct IDREFs, product-confirmed Transfer copy, and browser evidence for successor-present focus plus all three named last-control containing-panel fallbacks. + + The D9 suite must also name and execute failing cases for an upstream-import dependency-direction violation, a stale touched module specification, and an out-of-allowlist changed file; the wrong-promotion-target case above remains a separate required failure. Each mutation is applied to an otherwise valid fixture so the asserted rejection proves the declared verifier branch is fail-closed rather than masked by an earlier error. + +The exact-copy, feedback-description, and UX-008/UX-009/UX-010 correction gates are mandatory within those existing commands. D3 uses fake timers to prove confirmed through 1,499 ms, reset at 1,500 ms, independent focus-loss/second-copy/content-replacement resets, and unmount cleanup; its rejected Clipboard case proves no recorder call, failure paint, raw-error exposure, or `unhandledrejection`. It also proves both REQ-008 descriptions resolve through their stable message IDs and directly describe both feedback controls without live semantics. D6 keeps `Search topic` and `Complete Wrap-Up` canonical across UX-002 through UX-010. It starts from an unrecovered terminal generation error with no draft, proves Complete Wrap-Up is enabled, activates it without Retry, and requires exactly one `task.wrapup` call resolving to `{wrapup: 'succeeded', response: 'not-required'}` with zero capture or summary-response calls. D8 repeats the Clipboard rejection/counter and copied-state reset cases in the integrated browser for all applicable roles, records the two named feedback-description structural scenarios, requires the product-confirmed Transfer heading receipt, and binds S10 DOM evidence to the exact observed title/subcopy across all six visible error categories plus enabled completion override. D9 rejects release evidence missing any of those focused/browser receipts or containing state-dependent control copy, broken feedback-description IDREFs, unconfirmed/source-claimed Transfer copy, divergent error scope/copy, a terminal-error dead end, a raw rejection leak, or a nondeterministic copied-state interval. + +## Implementation DAG Summary + +| Task | Requirement/acceptance trace | Depends on | Rationale | +| --- | --- | --- | --- | +| `EXT-SDK-CC-SUMMARIES` (external, Deferred) | REQ-001, AC-01, AC-08 | external owner: Webex JS SDK Contact Center maintainers | Current clean source candidate exposes the intended task-owned event and unified public API. Exit after D0 assigns a deterministic version in an isolated stage and seals source/build/pack/declaration/runtime evidence, including same-Task post-wrap-up send callability. | +| `D0a-ux-evidence-admission` | PRE-005, REQ-009, REQ-011, UX-001-UX-010, AC-15 | none | Derive the linked common/target roots, transactionally copy exactly the thirty parent-only inputs into the same target-worktree-relative paths with `stage-ux`, independently reopen/rehash the target copies with `seal-ux`, record portable `ux.resolutionRoot: "."`, and block D3 on stage/seal absence or drift. | +| `D0-sdk-package-contract` | PRE-003, PRE-004, REQ-001, AC-01, AC-08 | `EXT-SDK-CC-SUMMARIES`, D0a, D2 | From the exact clean candidate commit, create an isolated stage; derive and assign the deterministic development version there; build, pack, hash, compile declarations, and run focused task-event/method/type/timestamp/correlation/timeout/send probes. Leave SDK source clean, preserve the store registry dependency, select the sealed tarball only through the private root, and transactionally roll back repository writes on failure. | +| `D1-summary-contract-fixtures` | REQ-001, REQ-002, REQ-003, REQ-004, REQ-005, REQ-006, REQ-008, REQ-010, REQ-012, AC-01, AC-02, AC-03, AC-04, AC-05, AC-06, AC-07, AC-08, AC-10, AC-11, AC-13, AC-14, and AC-16 | none | Raw readonly fixtures include response timestamp greater/equal/lower/present/omitted cases, `lateSuccessAfterTimeout`, remote-image-with-text and remote-image-only receiver cards, otherwise-identical `resolutionPresent` and key-omitted `resolutionAbsent` structured post-call members, a distinct display reason/auxiliary-code identifier, and the explicit resolved-wrap-up-then-send sequence asserting `wrapUpCode === auxCodeId`. They may proceed from sealed `ai-summary.md`; D0 later proves conforming members assignable to admitted declarations and executes the SDK-bound sequences against the packed SDK. | +| `D2-interaction-summary-store` | PRE-002, PRE-003 (`REQ-015` in the assigned review packet), REQ-001, REQ-002, REQ-003, REQ-004, REQ-005, REQ-006, REQ-008, REQ-010, AC-02, AC-06, AC-07, AC-08, AC-10, AC-13, AC-14, and AC-16 | D1 | Only the SDK-independent `unknown`-input adapter, stable-main identity resolver, explicit `conversationId`-to-`interactionId` reconciliation, attributed error union, non-destructive invalid-capability rule, normalized keyed section/result types that preserve SDK payload order while dropping unknown keys, optional exact response-`timestamp` validation, and the pure same-owner-role comparator that uses greatest/equal-last-arrival when both timestamps exist and later owner-role arrival when either is absent proceed before D0. The task also owns the single `store.types.ts` declarations for `AISummaryActionType`/`AISummaryFeedback`/`AISummaryPostCallRequestTrigger` plus optional `postCallGeneration`, the named `AISummaryViewModel.contentRevision` field, the exact `recordAISummaryViewed`, `recordAISummaryCopied`, and `setMidCallSummaryFeedback` signatures re-exported by `@webex/cc-store`, role-view keys and role-discriminated edit/view/copy guards, opaque capture-token and explicit `AISummaryFeedbackStatus` token contracts, and pure owner-successor/per-role-counter reducers. D2 explicitly covers initiating and receiver views coexisting under one owner and names role-compatible versus role-incompatible successful-envelope normalization for initiator, receiver, and post-call. It is the enforcing owner for requirement precedence: requirement behavior governs normalization on conflict, unsupported API shapes are rejected, and the adapter invents no SDK method, field, event, fallback identity, cancellation, client retry, or time value. No SDK import, event binding, or method call is admitted here. | +| `D3-shared-summary-presentation` | REQ-002, REQ-003, REQ-004, REQ-005, REQ-008, REQ-009, REQ-010, UX-002, UX-003, UX-004, UX-005, UX-006, UX-007, UX-008, UX-009, UX-010, AC-03, AC-04, AC-05, AC-11, AC-12, AC-13, AC-14, and AC-16 | D0a, D2 | The SDK-free components consume normalized summary model types from the single `@webex/cc-store` source of truth through `import type` only; D3 owns the cc-components manifest assertion that the existing `@webex/cc-store: workspace:*` dependency remains declared, and it forbids duplicate local section/editable-field/feedback/result contracts or a direct SDK edge. The editable presentation subset is derived from store-owned `AISummaryContent`, while presentation-only props/states remain local. After that boundary is fixed, D3 consumes admitted evidence and uniformly resolves the raw per-capture marker/case divergence to exact state-invariant `Search by name, queue, entry point or phone number`, `Search topic`, and `Complete Wrap-Up` constants. Initiating typed sections stay keyed while one visual editor presents labels/values in normalized SDK order, with no flatten-and-parse edit path. Definition tuples are metadata-only; a separate exhaustive `POST_CALL_DISPLAY_SECTION_ORDER` typed by D2's `AISummaryDisplaySectionKey` inserts a non-empty read-only resolution before the first present greater ordinal or appends it, never naming neighbours or reordering admitted SDK sections, while missing/`undefined`/empty resolution emits no row or reserved space. Shared mid/post keys reuse post-call evidence labels and role-neutral IDs. `aiSummary.generatingDescription` uses the corrected U+2014 string `Just a sec—the details are coming together.` and rejects the sealed source's U+005F artifact. Every visible error category uses the exact UX-010 title/subcopy pair as intentionally non-diagnostic recovery guidance; hidden and unsupported branches remain distinct. `AISummaryProps` uses the one `mid-call-initiator` / `mid-call-receiver` / `post-call` discriminator and owns the exact action labels, direct-gesture copy, role-specific controlled feedback, promise observation, and focus successor/fallback behavior for every role. Post-call `pending` and `not-confirmed` resolve through their stable message IDs to structural-only exact descriptions; both Like/Dislike controls reference the one present description via `aria-describedby`, including when read-only, and success removes the node/references without a live region. `aiSummary.midCall.transferHeading` remains a provisional structural-only candidate, not UX-001-derived authority, until D8 records product confirmation. Copy visual state drives only the copy affordance. Receiver mode is an action-only surface: ready props require `contentRevision` plus a synchronous visible-text getter, forbid card JSON, and neither import nor render `AdaptiveCardRenderer`. D4 owns the renderer sibling and supplies the getter. The component extends existing CallControl/AI Assistant sizing, wrapping, vertical-overflow, focus, forced-colors, and semantic-token surfaces without a feature theme map; its Clipboard observer calls a synchronous boolean counter recorder only after fulfillment, and `onFocusCapture` records the pre-removal successor consumed by `useLayoutEffect`. | +| `D2b-interaction-summary-sdk-binding` | REQ-001-REQ-008, REQ-010, store-related ACs including AC-08 and AC-09, AC-16 | D0 | This is the first SDK-bound task: extract the shared voice predicate; seed capability from each exact Task's public `aiSummaryCapabilities`; bind `TASK_EVENTS.TASK_FEATURE_ENABLEMENT` on that same Task with exact replacement/removal/`cleanUpStore` teardown; and never invent a client-owned feature event. Require exact stable identity; keep owner-role records, owner-local wrapped-up observations, the Store/session-owned owner-keyed wrapped-up subscription registry, and request bookkeeping isolated; attribute receiver errors; bind initiator action type to the accepted request and re-evaluate consult-to-transfer promotion; make all four Promise actions fulfill sanitized closed results; and expose aggregate post-call `requestPending` plus exact `feedbackStatus`. Each pending request records the monotonic admitted 15-second `deadlineAt` and `settlementClosed`; handlers atomically claim the open record, timeout closes before surfacing, and a later same-request success is stale regardless of timestamp. Outside that terminal fence, comparable successes use exact optional response `timestamp` and owner-role arrival fallback. Preserve accepted/edited/copied/rated post-call content, revision, counters, selection, and pending description until a current-generation ready response is accepted; stale older-generation work never competes by freshness, while the capture guard rejects a later settlement after wrap-up begins. Keep draft bytes and the captured Task behind a one-time opaque token; enforce Store-lifetime-unique revision/capture/frozen post-feedback guards and identifier-correct response capture; retain a Store-lifetime status sequence plus an ordered immutable observable queue, expose render-readable pending snapshots and exact-head acknowledgement, preserve undelivered entries through ordinary keyed cleanup, and discard them only at acknowledgement or global session cleanup; evict interaction state without resetting the status, content-revision, or post-call-generation monotonic guard counters; retain the wrapped-up logical subscription across ordinary Task replacement/removal and clear its tombstone/bindings on the matching event or interaction/session boundary; isolate wrapped-up summary failure so legacy refresh always runs and another interaction's event cannot discard it; emit only the content-free existing-category `ILogger.trace` diagnostic for current generation/response failure and prove all content/identity/raw-error sentinels absent; compile against the admitted package; and run the named late-success-after-timeout plus ordering/lifecycle/concurrency/feedback tests. | +| `D4-receiving-ai-assistant` | REQ-002, REQ-004, REQ-006, REQ-008, REQ-009, REQ-010, JNY-001, AC-04, AC-06, AC-10-AC-14, AC-16 | D2b, D3 | Project the store-owned receiver view, including only current-owner attributed error/unsupported states, into one optional discriminated `receiverSummary` prop and an exact `View summary` action in the existing AI Assistant chrome trigger slot. Absence preserves existing callers and renders no receiver UI; `content`, `unavailable`, and `generic-error` are exhaustive present states. Use only `openReceiverSummary`, boolean `recordReceiverSummaryCopied`, and always-fulfilled `setReceiverSummaryFeedback`, with the documented exact mapping to D2's store signatures and `contentRevision` passed as `expectedRevision`; `setMidCallSummaryFeedback` remains an internal store action, and `@webex/cc-store` alone composes `AISummaryResponse` and calls `capturedTask.sendMidCallSummaryResponse(payload, resolvedActionType)`. In `AIAssistantComponent`, render the display-only `AdaptiveCardRenderer` and D3's `AISummary` receiver action surface as DOM siblings, pass only the renderer's unchanged render/fallback props, and supply `AISummary` a synchronous `extractCardText`-backed getter over the sibling branch. The controls must never be descendants of the restricted renderer, and neither its props nor implementation may acquire a summary action mode. Inside the existing renderer boundary, extend only the preparation utility/render-empty check: permit exact closed-map bundled Momentum values, remove all other image URLs/icon URLs/background images before parse, render remaining text/actions, and route sanitized-empty image-only cards to the exact unavailable fallback with zero egress. Every React action handler returns `void` and observes rejection, so rejected SDK/Clipboard Promises cannot float. A cross-agent boundary clears the predecessor projection immediately with zero counters, while same-agent conference/regeneration retains current content/counters through pending or failure until valid replacement. Notification activation advances focus to the open-panel successor, receiver control removal uses the next document-order successor, and both fall back to the focusable `[data-testid="ai-assistant:panel"]` dialog root; replacing only the renderer sibling leaves connected controls focused. D4 explicitly gates remote-image-with-text preservation, remote-image-only fallback, zero remote request, unauthorized/init omission, rejected-copy counter stability, rejected-feedback prior selection, DOM sibling/containment assertions, unchanged renderer props/security ownership, and focused-control disappearance. Its files are disjoint from D5 and D6, and D4 depends on D3 rather than reimplementing its controls. | +| `D5-mid-call-call-control` | REQ-002, REQ-003, REQ-006, REQ-008, REQ-009, REQ-010, UX-001, JNY-001, AC-03, AC-06, AC-10, AC-11, AC-12, AC-13, AC-14, and AC-16 | D2b, D3 | Build one unconditional named-evidence voice destination renderer plus the closed no-destination existing-party action variant in the same popover shell; append the initiating summary only when visible, use exact delimiter-free `Search by name, queue, entry point or phone number` and heading constants, and preserve an 80vh-bounded popover with independently scrolling list/content regions and visible headings/actions. Capture the voice/non-voice/existing-party `destinationLayout` from non-summary context at popover open and latch it until close; capability or ownership-generation changes may mount/rebind/remove only summary descendants and must preserve destination node identity/order, typed query, category/destination selection, results, and connected focus. Close/reopen derives a fresh latch. The voice radio and retained non-voice pill categories share `availableCategories`/`handleCategoryChange`. Canonical `requestMidCallSummary` starts once per new owner/action preparation; its open/effect path explicitly observes fulfillment/rejection, pending reopen reattaches without transport, and current settled reopen reveals without implicit retry. Pending destination/action controls retain focus through guarded `aria-disabled`; disappearing summary controls use the captured successor then named popover-root fallback, close returns to the opening trigger, and content replacement does not move connected focus. Use imported `AISummaryActionType`/`AISummaryFeedback`, only `mode: 'mid-call-initiator'`, component-owned Clipboard writes followed by revision-only recorder notifications, and revision/action-type-guarded feedback. The standalone Transfer/Transfer Conference control opens TRANSFER preparation and its explicit confirmation owns response-before-`task.transferConference()`; Merge holds the current CONSULT view without another request; only an eligible fresh view with no request/result starts initial CONSULT preparation. Its confirmation owns response-before-`task.consultConference()`. Await the always-fulfilled response result at every internal confirmation call site before starting telephony, while allowing `failed`, `blocked`, and `stale` to continue exactly once after settlement; assert pending response means zero telephony calls and observe the outer/later Promises, zero unhandled request/feedback rejection, owning-request action type, consult-to-transfer promotion, indeterminate/stale block/no-paint behavior, and exact-key edit/clear round trips in files disjoint from D4 and D6. | +| `D6-post-call-wrap-up` | REQ-002, REQ-005, REQ-006, REQ-007, REQ-008, REQ-009, REQ-010, UX-002, UX-003, UX-004, UX-005, UX-006, UX-007, UX-008, UX-009, UX-010, JNY-002, AC-05, AC-06, AC-07, AC-08, AC-09, AC-10, AC-11, AC-12, AC-13, AC-14, and AC-16 | D2b, D3 | Build the post-call states from every D0a-sealed UX-002 through UX-010 scene-graph/text/PNG triple, implementing their hierarchy with only `mode: 'post-call'` at the presentation boundary, live eligible/ineligible transitions, one generating panel driven by current-reason-generation post-call pending state, revision-guarded feedback and capture/release behavior, and normalized wrap-up-first orchestration in files disjoint from D4 and D5. Give the search and radio group persistent accessible names, render the zero-match query as stable static non-live text, and route pointer/Enter/Space/group-exit reason commitment through an owner-and-selection-revision dedupe that calls `requestPostCallSummary('reason-commit')`, while Retry alone calls `requestPostCallSummary('retry')` so arrow traversal starts exactly one request for the final reason. Consume D1's paired resolution fixtures: the present case proves relative-ordinal insertion with adjacent sections absent, and the key-omitted case proves no Outcome row/text/edit target/copy block/response key/reserved space while SDK section order remains unchanged. Make copy visual state affect only the copy control and keep `Search topic` plus the `Complete Wrap-Up` label/name/tooltip canonical throughout UX-002 through UX-010. Retain accepted/edited/copied/rated content, counters, and pending feedback through reason changes and close/reopen until current-generation ready acceptance, with older generations stale before timestamp ordering, and freeze the visible draft if wrap-up begins first. Project the exact pending/not-confirmed tokens through D3's structural-only message IDs so both Like/Dislike controls remain directly described in enabled/read-only state without a live region. Bound the initial no-draft completion gate by the store's 15-second request timeout; a generation error or unavailable settlement enables eligible-without-draft completion, Retry cannot revoke the escape, and successful no-draft wrap-up returns `not-required` without response send. Both expanded and collapsed layouts use an 80vh `.agent-popover-content` shell with bounded reason/content scrolling and a visible primary at 320px. Bind every request/draft/capture/frozen response to the current authenticated agent and ownership generation under the common interaction ID, reset post-call counters for a different-agent successor, reject predecessor settlements, and prove the SDK fixture's final conference segment is preserved without widget-side assembly or truncation. Capture the committed `auxCodeId` and exact Task internally behind an opaque token only when a draft exists, commit wrap-up success before the one store-owned captured-Task send, make send rejection fulfill `response-failed` without undoing it, and keep the read-only `not-confirmed` status rendered through its stable message ID until matching owner-bound cleanup while exposing no resend/edit surface. Its focused acceptance additionally proves the orchestration stays package-internal, the public hook/prop declarations remain untouched in D6, and the legacy/custom-element-backed ineligible path keeps its existing UI, SDK arguments, and one-call behavior; D6b is the sole owner of the published callback migration. | +| `D6b-call-control-summary-integration` | REQ-002, REQ-003, REQ-005, REQ-006, REQ-007, REQ-008-REQ-010, JNY-001, JNY-002, AC-03, AC-05-AC-09, AC-10-AC-14, AC-16 | D4, D5, D6 | Join the completed feature modules and their closed non-rejecting results/guards through one focused `useAISummaryActions(ownerKey)` hook composed once by `useCallControl`, then through the shared types, container, shell, tests, styles, and module specifications; import the canonical store action/feedback types into both shared prop modules, thread canonical `requestMidCallSummary`, forward the rendered action type/revision for guarded mid-call feedback, keep Clipboard writes in the presentation components, and carry the single typed/plain summary mode plus closed feedback-status token without reintroducing redundant discriminants or visible-copy props. Thread D6's completion escape without widening the public result: `not-required` covers both successful ineligible legacy wrap-up and successful eligible wrap-up after terminal generation failure with no draft, while a ready draft still owns capture/send. In shared `call-control.tsx`, mount D5's existing-party variant from the eligible standalone Transfer/Transfer Conference and Merge controls, pass the exact trigger ref/focus return, request TRANSFER only on transfer preparation, reuse an existing CONSULT view for Merge (or start initial CONSULT preparation for an eligible fresh view), and keep only absent/ineligible summary views on their legacy immediate callbacks. A present eligible view whose pre-request surface is `omitted` still opens the existing-party preparation panel. Every destination and existing-party confirmation call site awaits the widened pre-action handler before invoking telephony; the CallControl integration suite holds that Promise pending to prove zero consult/transfer/conference calls, then settles both success and failure outcomes and requires exactly one subsequent action. As the sole public-seam owner, record the always-resolving `Promise` React type change as breaking for both published packages, prove the custom-element host path remains compatible, and require SemVer-major release metadata plus the direct-React migration. D6b is the first writer in the shared `task.types.ts` / `CallControl/index.tsx` handoff; D7 is dependency-ordered after it. | +| `D7-host-status-callback` | REQ-007, REQ-010, AC-08, AC-09, AC-14, AC-16 | D6b | Test all six valid callback detail literals and reject the two invalid mid-call terminal-state literals; expose the callback through an `observer` render-time queue read and dependency-driven effect that exact-head-acknowledges before an effect-local caught invocation, drains every ordered entry, retains unmounted transitions, and suppresses acknowledged same-agent remount replay. Authorization/init and interaction/owner boundaries emit nothing by themselves; only an accepted successor settlement emits its mapped state. Keep the new property out of the pinned r2wc function transform and use the typed property-only bridge so no attribute/global lookup exists. Type-only re-export `AISummaryStatusDetail` from both package entries and compile the cc-widgets public-entry import in `wc.ai-summary.ts`. D7 is the serial second writer of `task.types.ts`, `CallControl/index.tsx`, and their shared test/spec after D6b. | +| `D8-browser-ux-validation` | REQ-009, REQ-011, REQ-012, PRE-005, PRE-006, UX-001, UX-002, UX-003, UX-004, UX-005, UX-006, UX-007, UX-008, UX-009, UX-010, JNY-001, JNY-002, AC-11, AC-12, AC-13, AC-15, and AC-16 | D7 | Implementation plus evidence: re-verify every D0a-sealed target-worktree triple, pass that canonical root as required `--ux-input-root` argv to every common-checkout MatrixBuilder child, and reject root/hash drift before render. Collect the flag-gated journey and visual-manager specs exactly once each, install the sample-host bridge, run selector-bounded axe, and enforce the closed source/screenshot mapping. Materialize immutable targets for declared keys S01-S06, S07-UX, and S08-S10 and apply the structural/semantic-token comparison contract: exact DOM/order/boxes/copy/behavior/a11y, closed source-correction and semantic-token masks, token/contrast/forced-colors evidence, foreground coverage, and literal RGB only where authority remains. Emit the exact structural table tuples, including receiver/hidden/unavailable, nine extrapolations, separate Consult/Transfer pending, feedback, lifecycle/ownership/submission, Transfer-confirmation, and S10 override receipts. Run successor-present focus plus the three named last-control fallbacks to `consultTransferPanelRef`, `panelRef`, and `wrapUpPanelRef`. Before recording evidence, rerun the complete `@webex/cc-task` and `@webex/cc-ai-assistant` unit suites plus the focused `@webex/cc-components` cases. Fail on input drift, missing/extra tuple, undeclared/diff-derived mask, global pixel score, wrong focus target, keyboard/WCAG/overflow/forced-colors failure, or integrated regression; acceptance alone invokes the validator once with the same input root. | +| `D8b-restore-publishable-sdk-dependency` | REQ-001, REQ-012, AC-01, AC-16 | D8 | Validate the publishable boundary: actual packed widget manifests keep registry-resolvable SDK SemVer, exclude local protocols/private root resolution/vendor tarball, and pass publisher preflight before writes. Registry publication of the staged development SDK version is not required. | +| `D9-release-validation` | all in-scope traces (including Deferred PRE-003, PRE-004, REQ-001, and AC-01 after their D0 gate flips), journeys, UX sources, acceptance criteria including AC-12/AC-16, and target branch metadata PRE-001 / assigned-packet REQ-013 | D8b | The last task adds a read-only verifier and runs all release gates; it edits no product or evidence files. It validates hash-bound receipts emitted by separately executed gates and the flow-authenticated promotion target/current HEAD, exact changed/evidence manifest, local-only UX provenance, staged/sealed target-root identity, current successful deterministic-render receipt, and all publishability/package/privacy/test/release gates, including both independently collected Playwright specs and the six actual packed workspace manifests. It recomputes the structural/token/coverage/literal evidence, rejects a global pixel score used as sole acceptance, requires the exact structural-table tuple set, and requires all three last-control focus-target receipts in addition to the existing copy, feedback, error, Transfer-confirmation, and Figma-MCP-nonuse gates. | + +Dependency arrays name only immediate predecessors; transitive prerequisites are intentionally omitted so the critical path stays visible. D0a and D1 can begin independently, D2 follows D1, and after D0a plus D2 complete, D0 and D3 can run concurrently. D0 therefore carries D1 transitively through D2 and directly gates D2b; D3 likewise names only D0a and D2. Once D2b and D3 complete, D4, D5, and D6 fan out with disjoint file lists. Shared-file handoffs remain ordered by reachability: D3 establishes the shared `AISummary` contract/spec before D4's receiver integration update; D6b joins D4-D6 and writes the shared CallControl types/container/test/spec before D7 adds the host callback to those task files; D8b follows D8 for the root dependency/lock handoff. This allocation keeps the receiver, initiator, and post-call integrations parallel without redundant edges or documentation-only serialization. D8 then implements only the validation harness/evidence layer and reruns the complete task and AI Assistant unit suites before browser evidence; D9 follows D8b with its read-only verifier and full release gates. D0a, D1, and D2 may proceed while `EXT-SDK-CC-SUMMARIES` is Deferred; D3 may also proceed after D0a and D2, but none can claim SDK or feature integration. D2b and D4-D9 remain blocked until D0 records and verifies the external handoff, including the post-wrap-up send callability probe; D8b remains blocked only until D8 completes and then verifies the publishable-package boundary; registry publication of the staged development version is not required. PRE-003, PRE-004, REQ-001, and AC-01 remain Deferred until the named external handoff and D0 evidence flip their shared gate; a fail-closed design does not by itself make those dispositions Addressed. diff --git a/design/default/implementation_dag.json b/design/default/implementation_dag.json new file mode 100644 index 000000000..2185a76aa --- /dev/null +++ b/design/default/implementation_dag.json @@ -0,0 +1,781 @@ +{ + "tasks": [ + { + "id": "D0a-ux-evidence-admission", + "title": "Admit and seal target-worktree UX evidence before UI implementation", + "design_review_fixes": { + "D8/ux-source-paths-unresolved-across-worktree-and-parent": "Derive the linked common-checkout and target-worktree roots explicitly. stage-ux is the only cross-checkout reader and transactionally copies exactly the thirty named regular files into the same target-relative paths; seal-ux then closes/reopens and independently rehashes only the target copies before recording ux.resolutionRoot as literal dot." + }, + "description": "PRE-005, REQ-009, REQ-011, UX-001 through UX-010, and AC-15 / Component: UX Evidence Admission - before D3 or any screenshot-derived task, add stageUXSources, sealUXSources, and verifyUXSources plus stage-ux/seal-ux/verify-ux CLI contracts to the CommonJS lock tool and its fail-closed Jest suite. Derive TARGET_WORKTREE_ROOT from git rev-parse --show-toplevel and COMMON_CHECKOUT_ROOT from its canonical absolute Git common directory. stage-ux requires both explicit absolute roots, validates their Git relationship, rejects overlap, path escape, symlinks, non-regular or extra/glob-selected inputs, verifies the expected source hashes, copies only the thirty manifest paths through a target-local temporary tree, verifies the staged bytes, atomically promotes them, and never writes the receipt. seal-ux accepts only --widgets-root TARGET_WORKTREE_ROOT, reopens and independently rehashes all thirty target files after the move, rejects any parent/common-checkout or environment fallback and all individual/aggregate drift, and only then atomically writes sdk.status=pending plus ux.status=sealed, resolutionRoot='.', and canonical target-relative paths/hashes. verify-ux repeats the target-root-only check. A failed stage or seal leaves the prior receipt unchanged and creates no visual output. Tests cover wrong roots, partial/symlinked stage, mutation between stage and seal, no parent read after stage, literal-dot schema, and atomic failure. Keep the reviewed-artifact .gitignore block and Figma MCP prohibition. D0 depends on this task; D8 verifies but never establishes the seal.", + "files": [ + ".gitignore", + "tooling/src/ai-summary-sdk-lock.js", + "tooling/tests/ai-summary-sdk-lock.test.js", + "design/default/sdk_package_lock.json" + ], + "dependencies": [], + "acceptance": "TARGET_WORKTREE_ROOT=\"$(git rev-parse --show-toplevel)\" && COMMON_GIT_DIR=\"$(git rev-parse --path-format=absolute --git-common-dir)\" && test \"$(basename \"$COMMON_GIT_DIR\")\" = .git && COMMON_CHECKOUT_ROOT=\"$(dirname \"$COMMON_GIT_DIR\")\" && node tooling/src/ai-summary-sdk-lock.js stage-ux --source-root \"$COMMON_CHECKOUT_ROOT\" --target-root \"$TARGET_WORKTREE_ROOT\" && node tooling/src/ai-summary-sdk-lock.js seal-ux --widgets-root \"$TARGET_WORKTREE_ROOT\" && ! git check-ignore -q design/default/sdk_package_lock.json && ! git check-ignore -q vendor/contact-center-cc-summaries.tgz && yarn test:tooling --runInBand --runTestsByPath tooling/tests/ai-summary-sdk-lock.test.js && node tooling/src/ai-summary-sdk-lock.js verify-ux --widgets-root \"$TARGET_WORKTREE_ROOT\"" + }, + { + "id": "D0-sdk-package-contract", + "title": "Verify, admit, and seal the externally supplied cc-summaries SDK package", + "design_review_fixes": { + "REQ-001:D0-blocking-dependency-exit-criteria": "Treat the clean cc-summaries commit as an external source candidate. D0 assigns a deterministic development version only in an isolated stage and clears only after source-cleanliness, archive, declaration, task-event, unified-type, inbound-timestamp, correlation, timeout, HTTP-send, and same-Task post-wrap-up probes pass against one sealed artifact.", + "REQ-001:response-timestamp-field-identity": "Compile exact optional AISummary.timestamp?: number and run packed-runtime present-value-preservation plus omitted-property probes before admission; AISummaryResponse does not carry this inbound ordering field.", + "REQ-001:tarball-install-linker-mode": "Use the checked-in .yarnrc.yml#nodeLinker value and corepack yarn config get nodeLinker as the resolved evidence, require node-modules before any widget install, consume the committed tarball with corepack yarn install --immutable, and run verify-sdk against the resulting node-modules tree; PnP or any other drift fails before install.", + "D0-sdk-package-contract/unsatisfiable-root-prerequisite": "The current clean cc-summaries commit exposes the intended task-owned event and unified API. D0 remains Deferred only until it stages a deterministic version and seals the required package and runtime evidence.", + "D0-sdk-package-contract/vendored-tarball-in-published-manifest": "Keep the publishable store dependency byte-identical as plain registry SemVer, select the sealed tarball only through the private root, and have D8b verify actual packed widget manifests exclude the private override and vendored tarball without requiring publication of the development-only SDK version." + }, + "description": "PRE-003, PRE-004, REQ-001, AC-01, and AC-08 / Component: SDK Supply Lock and Contract Admission - use the clean cc-summaries source candidate at the recorded commit without requiring version in its source workspace manifest. build-sdk must verify branch, commit, and cleanliness; copy the exact commit into a transaction-private isolated stage; derive -cc-summaries.<12-character-commit> from the sole existing publishable @webex/contact-center dependency after stripping its prerelease suffix; assign that version only in the staged package manifest; and leave the source checkout byte-for-byte clean and unmoved. Use the candidate repository yarn@ packageManager pin through Corepack for SDK install/build/pack, use the widgets yarn@4.5.1 pin for repository operations, and execute every child with explicit argv/cwd plus the documented isolated allowlisted environment. Pack one safe package root, reject unsafe entries, and record the tarball hash, Yarn checksum, complete sorted regular-file hash manifest, and every reachable public declaration hash. The packed contract probe in @webex/cc-store must verify TASK_EVENTS.TASK_FEATURE_ENABLEMENT / task:featureEnablement on the matching Task; AISummaryFeatureEnablement; unified AISummary, AISummaryResponse, AISummaryAction, AISummaryFeedback, AISummaryState, and AISummarySections declarations; all four Task methods; optional inbound AISummary.timestamp preservation/omission; CONSULT/TRANSFER correlation; the exact 15-second timeout and listener cleanup; successful and unsuccessful sends; reachable WrapupPayLoad.auxCodeId and AISummaryResponse.wrapUpCode; and one sendPostCallSummaryResponse fulfillment after awaited wrapup on the same captured Task with wrapUpCode equal to the committed auxCodeId. Preserve D0a UX evidence, atomically replace only the pending SDK receipt member after every probe passes, commit vendor/contact-center-cc-summaries.tgz, leave the sole @webex/cc-store dependency byte-identical as plain registry SemVer, and select the sealed bytes only through the private root resolution. Require one checksum-matching local lock record and exact installed package-owned bytes, never a registry or source-tree fallback. Snapshot and transactionally restore root package.json, yarn.lock, tarball, and receipt on any post-write failure while proving the store manifest unchanged. Portable verify-sdk must ignore WEBEX_JS_SDK_DIR and source.audit. Add verify-publishable-sdk-boundary for D8b so actual packed widget manifests retain registry-resolvable SDK descriptors and exclude the private root resolution and vendored tarball; registry publication of the staged development version is not required. Tests cover isolated staging/version derivation, source cleanliness, toolchain mismatch, archive/declaration drift, event/type/method probes, timeout/send behavior, lock/install integrity, rollback, child isolation, and content-free diagnostics.", + "files": [ + "tooling/src/ai-summary-sdk-lock.js", + "tooling/tests/ai-summary-sdk-lock.test.js", + "packages/contact-center/store/tests/ai-summary-contract.ts", + "design/default/sdk_package_lock.json", + "vendor/contact-center-cc-summaries.tgz", + "package.json", + "yarn.lock" + ], + "dependencies": [ + "D0a-ux-evidence-admission", + "D2-interaction-summary-store" + ], + "external_dependencies": [ + "EXT-SDK-CC-SUMMARIES" + ], + "acceptance": "test \"$(corepack yarn config get nodeLinker)\" = \"node-modules\" && node tooling/src/ai-summary-sdk-lock.js build-sdk && test -f vendor/contact-center-cc-summaries.tgz && ! git check-ignore -q vendor/contact-center-cc-summaries.tgz && corepack yarn test:tooling --runInBand --runTestsByPath tooling/tests/ai-summary-sdk-lock.test.js && corepack yarn install --immutable && node tooling/src/ai-summary-sdk-lock.js verify-sdk", + "prerequisites": [ + { + "kind": "tool", + "name": "Node.js 22.14", + "command": "node", + "check_command": [ + "node", + "--version" + ], + "version_regex": "^v22\\.14\\.", + "required": true, + "fix_hint": "Activate Node.js 22.14 before building either repository." + }, + { + "kind": "tool", + "name": "Corepack", + "command": "corepack", + "check_command": [ + "corepack", + "--version" + ], + "required": true, + "fix_hint": "Enable Corepack so build-sdk can activate and verify the Yarn version declared by the candidate SDK commit's packageManager pin." + }, + { + "kind": "tool", + "name": "Git", + "command": "git", + "check_command": [ + "git", + "--version" + ], + "required": true, + "fix_hint": "Install Git so source identity and cleanliness can be verified." + }, + { + "kind": "service", + "name": "Build-only webex-js-sdk cc-summaries checkout configured by WEBEX_JS_SDK_DIR", + "service": "filesystem-git-checkout", + "check_command": [ + "node", + "-e", + "const {execFileSync}=require('node:child_process');const sdkRoot=process.env.WEBEX_JS_SDK_DIR;if(!sdkRoot)throw new Error('WEBEX_JS_SDK_DIR is required for build-sdk');execFileSync('git',['-C',sdkRoot,'rev-parse','--verify','HEAD'],{stdio:'ignore'});" + ], + "working_directory": ".", + "timeout_seconds": 10, + "required": true, + "required_env": [ + "WEBEX_JS_SDK_DIR" + ], + "fix_hint": "Set WEBEX_JS_SDK_DIR to the clean cc-summaries source candidate. Its source package manifest may omit version; build-sdk assigns the deterministic development version only in an isolated stage." + }, + { + "kind": "service", + "name": "npm registry for pinned SDK and widget installs", + "service": "https://registry.npmjs.org", + "check_command": [ + "node", + "-e", + "require('https').get('https://registry.npmjs.org/-/ping',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))" + ], + "working_directory": ".", + "timeout_seconds": 20, + "required": true, + "fix_hint": "Allow the SDK's pinned Yarn install to reach its declared npm registry or provide a complete local cache." + } + ] + }, + { + "id": "D1-summary-contract-fixtures", + "title": "Add SDK-independent raw summary contract fixtures", + "design_review_fixes": { + "UX-003/outcome-resolution-row-absence-and-ordering": "Add otherwise-identical postCall.resolutionPresent and postCall.resolutionAbsent raw fixtures. The present member carries a non-empty top-level resolution; the absent member omits the optional property entirely. Keep undefined/empty input in separate normalization-boundary fixtures so D3/D6 can prove omit-with-no-space independently of malformed handling.", + "REQ-001:post-timeout-response-suppression": "Add lateSuccessAfterTimeout with a post-timeout success carrying the greatest timestamp so D2b can prove the request-local closed marker wins before freshness ordering.", + "REQ-001:response-timestamp-field-identity": "Add present greater/equal/lower timestamp fixtures, an omitted-timestamp pair, and a mixed present/omitted pair for deterministic owner-role arrival fallback.", + "REQ-004:adaptive-card-local-image-policy-definition": "Add receiver remote-image-with-text and remote-image-only cards for the restricted renderer's zero-egress preservation/fallback tests.", + "REQ-006:two-party-host-fixture-has-no-asserting-test": "Name the conference join/leave/two-party member conferenceTwoPartyHost and require D2b's store integration suite to consume it. The fixture records participant initiation order, the SDK-owned two-party call projection, and the oldest remaining initiating agent as host under the unchanged canonical interaction; it is not satisfied by the D1 build alone." + }, + "description": "REQ-001, REQ-002, REQ-003, REQ-004, REQ-005, REQ-006, REQ-008, REQ-010, REQ-012, AC-01, AC-02, AC-03, AC-04, AC-05, AC-06, AC-07, AC-08, AC-10, AC-11, AC-13, AC-14, and AC-16 / Component: Interaction Summary Store and SDK Adapter and Test Strategy - against the sealed ai-summary.md hash, add the single readonly aiSummaryFixtures raw-object export and re-export it from test-fixtures/src/index.ts for strict feature enablement, initiating typed/plain payloads, receiver cards including remote-image-with-text and remote-image-only, structured/plain post-call payloads including otherwise-identical resolutionPresent and key-omitted resolutionAbsent cases for the optional top-level resolution/Outcome projection, matching and mismatched interaction/conversation identities, malformed/unsupported/empty input, normalized failures, response timestamp greater/equal/lower/present/omitted and mixed ordering, transfers, conferences, feedback, counters, timeout plus lateSuccessAfterTimeout, wrap-up with deliberately distinct human-readable wrapUpReason and auxiliary-code identifier auxCodeId, response failure, stale settlements, and AgentWrappedUp. Add an explicit postWrapUpSend fixture group modeling the same captured Task's resolved wrapup followed by exactly one sendPostCallSummaryResponse call whose wrapUpCode equals the committed auxCodeId and differs from wrapUpReason, with a conforming fulfilled settlement and a non-callable-after-wrap-up rejection used only to prove D0 fails admission. Do not import or cast to the unadmitted SDK. Update the fixture module specification. D0 later owns packages/contact-center/store/tests/ai-summary-contract.ts in the dependency-declaring store workspace; that file must compile every conforming member against the packed public declarations and execute the post-wrap-up sequence against the packed runtime before D2b. D1 can proceed while EXT-SDK-CC-SUMMARIES is Deferred but cannot claim SDK compatibility.", + "files": [ + "packages/contact-center/test-fixtures/src/aiSummaryFixtures.ts", + "packages/contact-center/test-fixtures/src/index.ts", + "packages/contact-center/test-fixtures/ai-docs/test-fixtures-spec.md" + ], + "dependencies": [], + "acceptance": "yarn workspace @webex/test-fixtures build" + }, + { + "id": "D2-interaction-summary-store", + "title": "Implement the SDK-independent summary adapter boundary and state model", + "design_review_fixes": { + "post-call-wrapup:resolution-outcome-row-hardcoded-between-named-sections": "Preserve admitted editable post-call sections in SDK payload order and retain only a validated non-empty top-level resolution separately on section content; missing, undefined, or empty input normalizes to absence. D2 exports AISummaryDisplaySectionKey for D3's exhaustive ordinal list but assigns no presentation neighbour and never places resolution in editable/response keys.", + "UX-002:wrap-up-reason-change-after-generation-transition-unspecified": "Add the closed AISummaryPostCallRequestTrigger reason-commit/retry type and role-discriminated postCallGeneration field. Reason generations are distinct from contentRevision and counters: a reason commit allocates a Store-lifetime monotonic token, Retry reuses it, and pure state contracts require older-generation candidates to be stale before timestamp ordering.", + "REQ-001:response-timestamp-field-identity": "Normalize only the exact optional response field timestamp. A present value must be a finite non-negative integer; absence stays absent. Compare only within one owner-role: both-present uses greatest/equal-last-arrival and either-absent uses later owner-role arrival, with no feature timestamp or wall-clock substitute." + }, + "description": "PRE-002, PRE-003 (REQ-015 in the assigned review packet), REQ-001, REQ-002, REQ-003, REQ-004, REQ-005, REQ-006, REQ-008, REQ-010, AC-02, AC-06, AC-07, AC-08, AC-10, AC-13, AC-14, and AC-16 / Component: Interaction Summary Store and SDK Adapter - against sealed ai-summary.md and D1 raw fixtures, add ai-summary.ts pure unknown-input validators, local normalized contract shapes, discriminated valid/invalid capability normalization, fail-closed stable-main identity resolution, role payload normalization with an expected canonical interaction parameter and an explicit discarded interaction-mismatch result, source-order-preserving normalized section arrays that drop unknown keys at the store boundary, exact optional response timestamp validation, same-owner-role greatest/equal-last-arrival ordering when both values exist, deterministic later owner-role arrival fallback when either is absent, owner-generation successor reducers, AISummaryViewKey owner-plus-role indexing, per-role counter carry-forward/reset guards, role-discriminated edit/view/copy guards, exact-key edit guards, resolution display separation, an opaque post-call capture-token contract, an explicit post-call feedback-status token projection, and sanitized error mapping. Invalid feature-enablement results retain a recognizable interactionId when present but have no comparable timestamp and therefore never remove or replace an existing valid capability; only an ordered valid record, including valid false flags, may revoke enablement. AISummaryErrorCategory excludes unsupported: only a valid-envelope NormalizedAISummaryResult with kind unsupported may enter the unsupported lifecycle, while transport, malformed, and empty failures remain error categories. Every payload-normalization error after conversationId matches carries interactionId so an event listener can attribute it without Promise request state. Define AISummaryOwnerState as a role-discriminated union: initiator and receiver owners require actionType plus midCallFeedbackPending and forbid post-call capture state, while post-call owners forbid actionType and own optional postCallGeneration, postCallFeedbackPending, the exact optional feedbackStatus: AISummaryFeedbackStatus token, an optional postCallCaptureRevision mutation guard, and an owner-local agentWrappedUpObserved flag rather than any global observation Boolean. Project the closed AISummarySurface discriminator on AISummaryViewModel so unauthorized/initialization becomes eligible false plus omitted, normalized unsupported alone becomes unavailable, and every other visible generation failure becomes generic-error; containers never inspect errorCategory. Define requestPending on every role view, with the post-call value true only while a request sequence for that exact owner, role, and installed postCallGeneration remains outstanding; superseded generations are still settlement-observed but do not hold the UI pending, and concurrent current-generation calls collapse into one generating projection. This task, not D0's package gate, is the behavioral enforcement owner for requirement precedence: when requirement.md conflicts with ai-summary.md or SDK input, explicit requirement behavior governs the normalized widget state while unsupported API shapes are rejected. Accept canonical identity only from non-empty task.data.interaction.mainInteractionId and findMediaResourceId(task, 'mainCall'); equal candidates collapse, conflicting/absent candidates fail closed, and task.data.interaction.interactionId plus task.data.interactionId are never fallbacks. Map summary conversationId to the requirement interactionId and discard a missing/nonmatching value before body normalization. Translate only fields and event shapes in the sealed concrete contract and never invent or alias an SDK method, field, payload key, event, fallback identity, cancellation, or client retry; add SDK-independent AISummaryKind, AISummaryActionType, AISummaryOwnerKey, AISummaryViewKey, AISummaryOwnerStateBase, AISummaryMidCallOwnerState, AISummaryPostCallOwnerState, AISummaryOwnerState, AISummarySurface, AISummaryViewModel, AISummaryOwnershipBoundary, requestPending, AISummaryPostCallSectionKey, AISummarySectionKey, AISummaryDisplaySectionKey, AISummaryDisplaySection, AISummaryEditableField, AISummaryCounters, AISummaryFeedback, AISummaryFeedbackStatus, AISummaryPostCallRequestTrigger, AISummaryStoreActionResult, AISummaryRequestResult, AISummaryPreActionSendResult, AISummaryFeedbackResult, PostCallSubmissionResult, opaque PostCallDraftCaptureToken, and AISummaryStatusTransition types to store.types.ts, with AISummaryActionType re-exported through the existing @webex/cc-store index and no task/component-local alias; the content-bearing PostCallDraftSnapshot remains module-private and never crosses IStoreWrapper; AISummaryStatusTransition reuses AISummaryKind and PostCallSubmissionResult rather than duplicating their literal sets. Retain exact accepted string bytes and SDK payload section order, drop unknown section keys before state, retain validated top-level resolution separately from the editable section array, keep it out of editable/response keys, and define initiating section collapse as presentation-only: edits mutate the exact retained key and never reconstruct payload shape from labels or delimiters. Unit-test every documented source conflict, no-retry scheduling, stable-only/conflicting/leg identity candidates, mismatch discard, matching error interaction attribution, invalid-after-valid capability preservation plus ordered valid false revocation, strict validation, source payload order preserved when it differs from presentation-metadata definition order, unknown section-key dropping, same-agent successor per-role counter carry-forward, different-agent counter reset, present greater/equal/lower timestamp ordering, omitted and mixed present/omitted owner-role arrival fallback, prior-agent non-projection, initiating and receiver views coexisting under one owner with role-discriminated edit/view/copy isolation, valid copied-recorder calls incrementing only the targeted role counter exactly once while stale/invalid calls do not increment, exact-key edit/clear behavior, counters, errors, rejection of unsupported shapes, named cases `initiator accepts typed sections and plain text but card-only success normalizes to unsupported`, `receiver accepts an adaptive card but typed/plain-only success normalizes to unsupported`, and `post-call accepts typed sections and plain text but card-only success normalizes to unsupported`, the sole normalized-result route into the unsupported lifecycle, the single exported AISummaryActionType plus required mid-call/forbidden post-call actionType, role-specific pending/feedback-status/capture-guard/owner-local-wrapped-up fields, the opaque capture-token boundary, closed outcome-discriminated request/pre-action/feedback result types, the exact revision-guarded boolean post-call feedback setter contract, the omitted/unavailable/generic-error surface mapping, and proof that sanitized error mapping never returns unsupported. Do not import @webex/contact-center, bind an SDK event, call a Task method, or claim runtime integration in this task. It may proceed while EXT-SDK-CC-SUMMARIES is Deferred; D2b completes the SDK-bound store after D0 without weakening these rules.", + "files": [ + "packages/contact-center/store/src/ai-summary.ts", + "packages/contact-center/store/src/store.types.ts", + "packages/contact-center/store/tests/ai-summary.ts", + "packages/contact-center/store/ai-docs/store-spec.md" + ], + "dependencies": [ + "D1-summary-contract-fixtures" + ], + "acceptance": "yarn workspace @webex/cc-store test:unit --runInBand --runTestsByPath tests/ai-summary.ts" + }, + { + "id": "D2b-interaction-summary-sdk-binding", + "title": "Bind the admitted summary SDK contract into the interaction store", + "design_review_fixes": { + "post-call-wrapup:reason-reselection-silently-discards-edited-summary": "Preserve accepted post-call content, accepted edits, and contentRevision while a reason-triggered replacement is pending or settles blocked, stale, failed, or unsupported. Only a newer ready response accepted for the current owner replaces it; capture during pending replacement freezes the existing draft and makes later settlement stale.", + "post-call-wrapup:complete-disabled-during-generation-and-error-no-escape": "Keep the existing 15-second request timeout as the hard bound for the initial no-draft completion gate. After eligible-without-draft wrap-up succeeds, mark the captured owner/reason revision complete so a late generation settlement is consumed as stale and cannot recreate visible content or trigger a response send.", + "AC-09/transition-mapping-nondeterministic-may": "Implement a closed transition boundary: authorization denial, capability-disabled omission, initialization failure, interaction switch, and ownership-generation change append no status entry by themselves. Same-agent carried content does not repeat available, cross-agent reset does not synthesize unavailable, and stale predecessor settlement appends nothing; only a later freshness-accepted successor settlement appends exactly its mapped available/unavailable/submitted/response-failed state.", + "UX-002:wrap-up-reason-change-after-generation-transition-unspecified": "On requestPostCallSummary('reason-commit'), allocate and install a monotonic post-call generation before transport; on 'retry', capture the installed token. Exclude superseded pending entries from current requestPending and reject every older-generation settlement as stale before timestamp comparison. Preserve the visible draft, revision, counters, selected local feedback, and pending status until current-generation ready acceptance; then replace content with a fresh revision, preserve edited/copied totals, increment viewed once, and reset feedback/status. Superseded, blocked, failed, unsupported, and no-op work mutates no counter, feedback, status, or callback transition.", + "REQ-001:post-timeout-response-suppression": "Record deadlineAt and settlementClosed on every request. Atomically close before surfacing the 15-second timeout and before normalizing any fulfillment observed after the deadline; all later same-request settlements are stale regardless of timestamp and mutate no state, pending record, counter, status, callback, or rejection channel.", + "REQ-001:response-timestamp-field-identity": "After D0 verifies timestamp?: number on both response payloads, preserve it when present and use the D2 owner-role arrival fallback when absent. The timeout-closed and owner/generation guards run before freshness comparison.", + "REQ-010:summary-path-diagnosability": "After reducing a current SDK generation or response failure to an already-defined AISummaryErrorCategory or response-failed literal, emit exactly one content-free store.logger.trace diagnostic with only a static prefix and static module/method context. Add a privacy test with summary, interaction, agent, raw-error, timestamp, and duration sentinels and assert no metrics call; blocked, stale, discarded, and Clipboard paths do not log.", + "REQ-005:failed-response-tombstone-cleanup-listener": "Own the logical AgentWrappedUp subscription in a Store/session-level AISummaryOwnerKey registry separate from ordinary Task listeners. Retain exact Task/callback bindings across Task replacement/removal until the first matching event or interaction/session cleanup, detach them together, and add the task-removal-before-AgentWrappedUp race test.", + "REQ-006:two-party-host-fixture-has-no-asserting-test": "In packages/contact-center/store/tests/storeEventsWrapper.ts and the existing store integration paths, consume D1's conferenceTwoPartyHost member in the named case conference leave to two-party call selects the oldest remaining initiating host once. Drive join, participant-left, and conference-ended under one canonical interaction; require the SDK topology to project a call with the oldest remaining initiating local agent as host, select that host's owner key, advance ownershipGeneration exactly once despite duplicate notifications, carry same-agent content/counters, and reject departed-agent, non-host, and predecessor-generation settlements. Do not derive or override host ordering in UI code.", + "test-strategy-item3:all-retention-boundaries-unenumerated": "Implement the design's closed REQ-002 lifecycle table as separately named D2b store tests: hold/resume and same-agent remount retain mid-call state; task end retains mid-call state; wrap-up completion clears mid-call state; interaction change clears prior-interaction state; agent sign-out clears session summary state; SDK session end clears session summary state; submitted post-call state remains until interaction change; and failed post-call response remains until matching AgentWrappedUp. Exercise sign-out and SDK session end through distinct production cleanup routes and prove late work cannot recreate cleared state." + }, + "description": "PRE-002, REQ-001, REQ-002, REQ-003, REQ-004, REQ-005, REQ-006, REQ-007, REQ-008, REQ-010, AC-01, AC-02, AC-06, AC-07, AC-08, AC-09, AC-10, AC-13, AC-14, and AC-16 / Component: Interaction Summary Store and SDK Adapter - only after D0 verify-sdk passes, compile the D1 conforming fixtures and D2 boundary outputs against the packed public declarations with no casts; complete response composers in ai-summary.ts; keep ai-summary.ts pure and add the mutable keyed summary state to the existing MobX Store through makeAutoObservable. StoreWrapper owns role-view records keyed by canonical interactionId + authenticated agentId + monotonic ownershipGeneration + role, allowing initiating and receiver views to coexist under one owner without sharing revisions or viewed/edited/copied counters, role-discriminated required mid-call actionType, midCallFeedbackPending for in-flight SDK feedback, postCallGeneration for the installed reason generation, postCallFeedbackPending for locally queued final-response feedback, exact feedbackStatus token projection for pending/not-confirmed/confirmed settlement, postCallCaptureRevision for the capture-time mutation guard, owner-local agentWrappedUpObserved on each post-call record, Store-lifetime monotonic content-revision and post-call-generation counters whose positive tokens never collide across owner/session cleanup, pending-request sequence/maps carrying role plus the validated initiating action type or post-call generation, monotonic deadlineAt, and settlementClosed, an aggregate requestPending projection per owner role, restricted to the installed generation for post-call concurrency, a private postCallDraftCaptures map binding an opaque one-time symbol token to the deep-copied snapshot and exact captured Task, frozen post-call response, a private Store/session-owned aiSummaryWrappedUpSubscriptions registry keyed by AISummaryOwnerKey and retaining exact Task/callback binding sets independently of taskList, Store-lifetime monotonic status sequence plus an observable.ref immutable pending-transition queue and exact-head acknowledgement, and role-discriminated store action signatures, including setMidCallSummaryFeedback(role, feedback, actionType, expectedRevision); none of those values lives in React component state. Derive canonical identity only from the stable mainInteractionId/mainCall-media candidates and fail closed on absence or conflict; never fall back to either leg-scoped interactionId. Keep validated capabilities by their own interactionId for capability-before-Task delivery, but make a nonmatching record unavailable to the current canonical Task and never re-key it. An invalid untimestamped feature-enablement result leaves any stored valid capability unchanged; only a greater-timestamp or equal-timestamp later-arrival valid record, including valid false flags, may replace it. Pass the captured canonical key to every initiating, receiving, and post-call payload normalizer; discard a missing/nonmatching conversationId with no content, counter, feedback, pending-owner, error, or status mutation. Every normalized error after a matching conversationId carries that interactionId. The task-bound receiver listener also captures the full owner key and may paint an empty current owner only when both identifiers still match; it drops stale/rebound owners and never replaces accepted content or consults Promise request maps. Freshness-accepted unsupported receiver content alone paints unavailable. Hold/resume, same-agent remount, and pure Task clone/end retain the current owner record. Add compare-and-swap owner advancement on confirmed successful transfer/handoff including consult-to-transfer promotion, conference establishment, and each later topology-driven per-agent full-history cycle. A same-agent successor carries each existing role view's visible content/counters/feedback independently until valid replacement; a different authenticated agent or terminal re-entry starts every applicable role view at zeroed state. Make the old key non-current before late settlement and coalesce duplicate action/event boundary notifications. Ordinary repeated requests retain the epoch. Mid-call and same-post-call-generation work use request sequence for timestamp-less failure suppression, exact optional response timestamp for both-present freshness, and later owner-role arrival when either value is absent; a new post-call reason generation supersedes every older request before that comparison. Every request handler atomically claims its open settlement marker before normalization: timeout closes before surfacing, a fulfillment observed after deadline closes through the same timeout path, and every later same-request settlement is stale regardless of timestamp. Implement one per-interaction eviction sweep: at the matching backend event or interaction/session boundary delete the capability, current-owner pointer, every eligible owner and post-call-failure record, every latest-request entry, every pending request owned by that interaction, and every wrapped-up registry binding; ordinary Task replacement/removal is not a failed-response boundary, while an interaction switch clears rather than merely hides the response-failed tombstone; late settlements and late events are consumed and cannot recreate state. AgentWrappedUp clears a retained failed frozen response at its race-safe boundary before the sweep, a submitted response retains only its documented state until interaction change, and cleanUpStore clears every interaction, discards any undelivered status entries at the global privacy boundary, and resets only state-local ownership/request/arrival sequences and never resets the content-free status, content-revision, or post-call-generation counters. Extract the existing private voice check into exported task-utils.ts#isTelephonyTask, keep MEDIA_TYPE_TELEPHONY_LOWER as its sole policy value, and make legacy store behavior plus every summary eligibility caller reuse it. Add stable handleAISummaryFeatureEnablement and bind TASK_EVENTS.TASK_FEATURE_ENABLEMENT on each exact Task through the existing Task-listener registration seam; seed the same fail-closed normalized capability state from that Task's public aiSummaryCapabilities snapshot so an event emitted before widget listener attachment is not lost; retain the exact Task/callback pair, detach it on Task replacement/removal and global cleanup, and never subscribe to or invent a cc-owned feature event. Keep the receiving callback task-bound with the full AISummaryOwnerKey captured at binding. Own AgentWrappedUp separately in the Store/session registry: replacement attaches the same stable idempotent owner callback to the successor before ordinary listener teardown; generic handleTaskRemove detaches receiving/ordinary listeners but cannot detach the logical wrapped-up subscription or discard the exact pending/frozen failed response; the registry retains every required exact Task/callback binding until the first matching event or interaction/session cleanup. Compare interaction ID, agent ID, and ownership generation before marking only that post-call record, so AgentWrappedUp for interaction X or a stale generation cannot discard interaction Y or its successor. Replace only the direct TASK_WRAPPEDUP -> refreshTaskList listener with the registry-owned wrapper that records the boundary and attempts summary cleanup inside a catch boundary, consumes any sanitized summary-transition failure, always invokes refreshTaskList from finally, and detaches every binding for that owner from an outer finally; interaction cleanup is the per-interaction privacy exit and cleanUpStore is the global privacy exit, each detaching all applicable retained callbacks and clearing content-bearing memory while retaining only the monotonic status, content-revision, and post-call-generation counters. Preserve existing conference participant handlers' legacy refresh behavior while adding only idempotent owner-boundary bookkeeping; prove they neither fan out a request nor cancel pending work. Make @webex/cc-store the sole caller/listener for the four Task methods and summary events; enforce voice/capability/session/agent gates, runtime identity reconciliation, stable canonical interaction and owner generations, timestamp ordering, retention, revision-guarded post-call feedback, capture guard/release/freeze/once-only response, AgentWrappedUp-before/after-failure races, and the monotonic content-free transition used by the one call-control callback. For every accepted semantic status change, increment the Store-lifetime sequence and append an AISummaryStatusTransition by assigning a newly frozen pendingAISummaryStatusTransitions array annotated observable.ref in makeAutoObservable; never overwrite or coalesce an undelivered entry, including when multiple changes occur in one runInAction. Expose getPendingAISummaryStatusTransitions() as the stable readonly observable snapshot used during D7's observer render and acknowledgeAISummaryStatusTransition(sequence) as an action that succeeds only for the exact current head and removes only that entry. Retain pending entries across ordinary keyed cleanup and CallControl unmount, discard them only after acknowledgement or global sign-out/session cleanup, and retain the sequence counter across every cleanup. For initiating section content, composeMidCallResponse iterates the normalized section array in SDK payload order, defensively allowlists each key, emits exactly the retained keys with their current values including intentional empty edits, and never imports or iterates presentation definitions or parses labels/flattened copy text; post-call composition follows the same normalized order and plain content emits the exact current summaryText. Capture the module-private PostCallDraftSnapshot.wrapUpCode and exact current Task inside the store from the second useCallControl.wrapupCall argument auxCodeId, return only PostCallDraftCaptureToken, never serialize the human-readable wrapUpReason into the response, and after wrap-up resolve that token to emit the captured identifier unchanged as the D0-admitted AISummaryResponse.wrapUpCode. releasePostCallDraft and freezeAndSendPostCallSummary accept only the token; an unknown, released, or reused token makes no SDK call or cross-owner mutation, and callers can neither mutate the snapshot nor substitute a Task. Ground the mapping in D0's reachable WrapupPayLoad.auxCodeId and response wrapUpCode declarations plus the unequal-label/code fixture. Project AISummarySurface so unauthorized/initialization sets eligible false and omitted, valid-envelope unsupported maps to unavailable only when no accepted content is retained, all other visible generation errors map to generic-error only when no accepted content is retained, and content/requesting map without exposing errorCategory. For an existing post-call ready draft, pending, blocked, stale, failed, or unsupported replacement work preserves content, edit bytes, contentRevision, counters, local feedback, and feedbackStatus. A reason commit installs a new monotonic generation and supersedes older settlements before timestamp ordering. Only current-owner/current-generation ready acceptance replaces content; it preserves edited/copied totals, increments viewed once, resets feedback/status, and a capture installed first makes every later settlement stale. Keep the existing 15-second request timeout as the hard bound for the initial no-draft completion gate; close the request record before surfacing timeout and reject a later success even if it has the greatest timestamp. After eligible-without-draft wrap-up succeeds, record the completed owner/reason revision so any late generation settlement is stale and cannot recreate content or cause a response send. For every mid-call like/dislike selection or re-selection, require the caller's rendered Exclude value, AISummaryActionType, and Store-lifetime-unique revision; reject an action-type or revision mismatch as stale before pending state, composition, or an SDK call, including a CONSULT click queued across TRANSFER promotion. For an accepted guarded selection, resolve initiator actionType from the D0-verified Task correlation at request start and settlement, store it on the accepted owning request, and re-evaluate it for consult-to-transfer promotion before later composition; call exactly capturedTask.sendMidCallSummaryResponse(payload, resolvedActionType); because the admitted signature is Promise and exposes no HTTP status, treat fulfillment as the admissible proxy for REQ-004's 200 confirmation, commit the highlight only after fulfillment produces outcome confirmed, preserve it for rejection-derived failed plus blocked/stale outcomes, and use only midCallFeedbackPending for the in-flight disable. An indeterminate initiator correlation mirrors the receiver rule: call no SDK method, return blocked, and paint no content or feedback. Keep the later pre-action call distinct: it calls the same SDK method once per telephony attempt with the latest confirmed feedback immediately before the existing telephony API, and prior feedback-update calls never satisfy or suppress it. Make requestMidCallSummary, requestPostCallSummary(trigger: AISummaryPostCallRequestTrigger), sendMidCallSummaryBeforeAction, and setMidCallSummaryFeedback catch synchronous throws/rejections/timeouts and always fulfill sanitized outcome-discriminated results with categorization internal; each post-call request installs its role-scoped pending sequence plus generation, deadlineAt, and open settlement marker before the SDK call, atomically closes before settlement/categorization, removes only that sequence, excludes superseded generations from requestPending, and keeps one generating projection plus disabled Retry until the final current-generation sequence clears when no content is retained, while retained post-call content remains projected and capturable; pre-action returns sent/failed/blocked/stale and feedback returns confirmed/failed/blocked/stale. Failed/blocked/stale outcomes mutate no response-derived state, expose no raw error, and never reject. After a current SDK generation or response failure is reduced to an existing AISummaryErrorCategory or response-failed literal, invoke the existing lowest-level store.logger.trace exactly once with a static prefix and static module/method context; pass no summary/customer text, raw error, payload, interaction/agent identifier, timestamp, or duration, never call withMetrics/metricsLogger, and emit nothing for blocked, stale, discarded, or Clipboard paths. Implement setPostCallSummaryFeedback(feedback, expectedRevision): boolean with a Store-lifetime-unique revision token serving as the owner-generation guard plus eligibility/role/capture/frozen checks, make capturePostCallDraft install postCallCaptureRevision and the internal token record while releasePostCallDraft compare-clears it only after wrap-up failure, and project feedbackStatus as pending while local feedback awaits the final response, not-confirmed after response failure, and undefined after confirmed submission; visible strings stay in cc-components message constants. Test stable-only canonical derivation, capability mismatch unavailability, invalid-before/after-valid capability preservation, ordered valid false revocation, all role payload mismatch no-mutation, receiver malformed-event attribution and stale-owner discard, accepted-content preservation, unsupported-event unavailable projection, the named role-incompatible successful-envelope cases for initiator typed/plain, receiver adaptive-card, and post-call typed/plain, exact client bind/rebind/cleanup, handoff/conference advancement and duplicate-boundary idempotence, same-agent per-role counter carry-forward, different-agent reset, simultaneous initiator/receiver views with role-targeted edit/view/copy isolation, stale prior-generation discard, the named task-removal-before-AgentWrappedUp race proving the removed Task stays in the Store/session registry until its matching event clears exactly that tombstone and detaches every binding, `AgentWrappedUp for interaction X does not discard interaction Y retained failed tombstone`, stale-generation wrapped-up isolation, complete per-interaction map eviction and late-settlement no-recreation, cleanup-before-refresh ordering, a throwing summary transition still refreshing before all-binding detachment, session cleanup, the shared voice predicate, required actionType without assertions/defaults, owning-request actionType plus consult-to-transfer promotion and indeterminate block/no-paint, exact-key structured edit/clear, source-order response composition when payload order differs from presentation metadata, unknown-key dropping, and plain response composition, mid-call like/dislike mutual exclusivity for both roles, explicit rendered action-type/revision forwarding with stale mismatch no-send/no-paint, selected feedback painting only after sendMidCallSummaryResponse fulfillment with rejection leaving the prior selection unchanged, successful opposite-selection highlight replacement, and re-selection issuing another exact SDK call while leaving the option highlighted, all four Promise actions always fulfilling closed results, guarded post-call feedback success and stale-owner/revision-token collision/ineligible/captured/frozen negative cases, role-specific pending/capture semantics including concurrent post-call aggregation and final-settlement clearing, exact accepted/edited/copied/rated post-call draft, counter, and pending-feedback retention across pending/blocked/stale/failed/unsupported replacement work until a current-generation ready response is accepted; superseded responses are stale before timestamp comparison, and feedback resets only on replacement, capture-before-settlement stale rejection, the independent once-per-action pre-action call and its ordering, opaque capture-token snapshot/Task binding plus unknown/released/reused-token no-send isolation, captured wrapUpCode equality with the committed auxCodeId and inequality with wrapUpReason, all five surface branches, all four pre-action send outcomes, exact content-free trace arguments plus forbidden summary/interaction/agent/raw-error/timestamp/duration sentinel absence and zero metrics calls, Store-lifetime status, content-revision, and post-call-generation counter non-reset, two ordered status appends in one batch, exact-head acknowledgement, ordinary-cleanup retention, global-cleanup queue discard, repeated-request present/omitted timestamp ordering, the named late-success-after-timeout and timeout-versus-on-time boundary cases, and zero unhandled rejections. A receiving-agent event records mid-call available/unavailable in the store without any AI Assistant-to-task import. Reject automatic retry, cancellation, persistence, raw/content/identifier logging, new metrics/events/dimensions/durations/failure vocabulary, runtime fallback, and unhandled rejections; permit only the specified content-free existing-category trace. Modify the existing store utilities/tests/mocks/spec after the SDK-independent D2 edits.", + "files": [ + "packages/contact-center/store/src/ai-summary.ts", + "packages/contact-center/store/src/store.ts", + "packages/contact-center/store/src/store.types.ts", + "packages/contact-center/store/src/storeEventsWrapper.ts", + "packages/contact-center/store/src/task-utils.ts", + "packages/contact-center/store/tests/ai-summary.ts", + "packages/contact-center/store/tests/store.ts", + "packages/contact-center/store/tests/storeEventsWrapper.ts", + "packages/contact-center/store/tests/task-utils.ts", + "packages/contact-center/store/tests/setupContactCenterMock.js", + "packages/contact-center/store/ai-docs/store-spec.md" + ], + "dependencies": [ + "D0-sdk-package-contract" + ], + "acceptance": "node tooling/src/ai-summary-sdk-lock.js verify-sdk && yarn workspace @webex/cc-store test:unit --runInBand --runTestsByPath tests/ai-summary-contract.ts tests/ai-summary.ts tests/store.ts tests/storeEventsWrapper.ts tests/task-utils.ts" + }, + { + "id": "D3-shared-summary-presentation", + "title": "Add SDK-free shared summary presentation and controls", + "design_review_fixes": { + "post-call-wrapup:resolution-outcome-row-hardcoded-between-named-sections": "Keep D2's editable SDK sections in source order and retain top-level resolution separately. In ai-summary.constants.ts define one unique exhaustive POST_CALL_DISPLAY_SECTION_ORDER typed by the D2-exported AISummaryDisplaySectionKey. Insert a non-empty Outcome before the first present greater ordinal or append it, without requiring named neighbours or reordering admitted sections; omit missing, undefined, or empty resolution with no row or reserved space.", + "UX-003/outcome-resolution-row-absence-and-ordering": "Project Outcome if and only if normalized resolution is a non-empty string. Present resolution uses the typed relative ordinal without sorting SDK sections; missing, undefined, or empty resolution emits no row, label, placeholder, divider, clipboard block, or reserved layout space. Focused tests consume D1's paired present/absent fixtures.", + "post-call-wrapup:copy-visual-state-mutates-primary-submit-label": "AISummaryCopyVisualState drives only the Copy Summary control's idle, hover, and confirmed treatment. It has no data path to reason-search or primary-action props; Complete Wrap-Up always resolves from the one stable aiSummary.postCall.completeAction message in every summary state.", + "post-call-wrapup:onCopyVisualStateChange-referenced-but-not-declared": "Declare onCopyVisualStateChange: (state: AISummaryCopyVisualState) => void as a required member of the post-call AISummaryProps variant in ai-summary.types.ts and forbid it in both mid-call variants.", + "post-call-wrapup:percent-delimited-figma-placeholders-treated-as-literal-copy": "Resolve matched outer percent slots before authoring product copy and define one stable message ID/value per control: aiSummary.postCall.searchPlaceholder is Search topic and aiSummary.postCall.completeAction is Complete Wrap-Up. No product constant retains the delimiters.", + "UX-002..UX-010/addressed-rows-trace-only-to-validation-tasks": "D3 is the implementing owner for the shared generating, editing, like/dislike hover and selected, copy hover/success, and error presentation required by UX-002 through UX-010; its production files and focused component tests are completed before D8 verification.", + "UX-001:transfer-variant-copy-invented-without-structural-only-receipt": "Reserve aiSummary.midCall.transferHeading as a distinct structural-only message ID with the provisional candidate Here’s a transfer summary—they’ll get a copy. Do not describe it as source extraction, action-noun substitution, or approved copy; D8 must bind the final exact value to product/content-owner confirmation before passing.", + "UX-004+UX-006+UX-008+UX-009:literal-percent-delimited-figma-placeholder-shipped-as-copy": "Treat every complete matched outer-percent token in sealed Figma text uniformly as an unresolved variable, resolve it before authoring product constants, and author only delimiter-free values: Search topic, Search by name, queue, entry point or phone number, and requirement-controlled Complete Wrap-Up. Unit tests inspect rendered text, placeholders, accessible names, and tooltips and reject percent artifacts.", + "UX-004..UX-009/percent-delimited-placeholder-strings-as-production-copy": "Never place sealed outer-percent marker strings in component constants or props. Bind Search topic and Complete Wrap-Up once as delimiter-free values, using the latter identically for visible label, accessible name, and tooltip; D8 owns fixed source-variable comparison masks rather than requiring product markers.", + "UX-005+UX-007:post-call-selected-state-gated-on-success-contradicts-local-pending": "Make feedback paint role-specific: post-call paints immediately only after the revision-guarded local callback returns true and renders the requirement-owned structural-only Pending submission description; both mid-call modes paint only after outcome confirmed from SDK fulfillment. Final post-call success removes the description and both control IDREFs, while response failure preserves the read-only selection and renders the structural-only Submission not confirmed description directly associated with both feedback controls.", + "UX-004+UX-006-vs-UX-008+UX-009:complete-wrap-up-copy-treated-inconsistently": "Define aiSummary.postCall.completeAction exactly once as Complete Wrap-Up and reuse it for visible text, accessible name, and tooltip across default, hover, selected, copy-confirmed, generating, unavailable, and error states.", + "UX-008+UX-009:clipboard-promise-rejection-handling-unspecified": "Observe navigator.clipboard.writeText at the direct-gesture call site with explicit fulfillment and rejection arms. A synchronous throw or rejection normalizes to content-free failed, never invokes the copied recorder, causes no failure-driven visual transition or counter increment, discards the raw value, and produces zero unhandledrejection.", + "UX-009:copy-success-treatment-duration-and-revert-trigger-unspecified": "Define COPIED_FEEDBACK_MS as exactly 1_500. Confirmed resets to idle on the first of 1,500 ms expiry, copy-control focus loss, a second copy gesture, or accepted summary replacement; a fulfilled second write starts a fresh interval, rejection leaves the gesture-reset state unchanged, and unmount only clears the timer.", + "UX-010:missing-having-trouble-generating-summary-exact-copy-binding": "Bind the UX-010 text JSON/PNG title exactly to aiSummary.generationError = Having trouble generating summary and its subcopy exactly to aiSummary.generationErrorDescription = It could be a lost connection or something else. Could you check your connection or try again later?; the title is identical to REQ-009, and every non-hidden generation-error category uses this pair.", + "UX-002/generating-description-underscore-artifact": "Record that the higher-precedence S02 pixels visibly corroborate the JSON U+005F artifact, then bind aiSummary.generatingDescription to the corrected en-US Just a sec—the details are coming together. with U+2014 and no surrounding spaces. Unit tests assert the full string, separator code point, and absence of U+005F; D8 masks only the fixed separator correction region.", + "REQ-008/pending-submission-strings-absent-from-ux-inventory": "Define aiSummary.feedback.pendingSubmission = Pending submission and aiSummary.feedback.submissionNotConfirmed = Submission not confirmed as requirement-owned structural-only descriptions. Give the one present description an instance-local ID, reference it from both Like/Dislike controls with aria-describedby in enabled pending and read-only not-confirmed states, remove the node and both IDREFs on success, and add no live-region semantics.", + "UX-002..UX-010/same-control-label-differs-by-visual-state": "Treat Search topic versus %Search topic% and Complete wrap-up versus %Complete wrap-up% as source-capture artifacts, not visual-state semantics. Author only canonical aiSummary.postCall.searchPlaceholder = Search topic and requirement-controlled aiSummary.postCall.completeAction = Complete Wrap-Up, and prove neither value changes across generating, editing, error, hover, selected, or copied states.", + "UX-010/connection-specific-subcopy-applied-to-all-error-categories": "Use the exact UX-010 title/subcopy pair for disabled, offline, unavailable, empty, timeout, and generic visible failures; keep authorization/initialization hidden and unsupported content separate. The broad scope is intentional because or something else is non-diagnostic and or try again later is category-neutral recovery guidance.", + "transfer-variant/heading-copy-without-source-authority": "Reserve aiSummary.midCall.transferHeading for the structural-only Transfer candidate, make no UX-001 extraction or extrapolation claim, and require the D8 Transfer scenario to bind the final exact DOM copy to a non-empty product/content-owner confirmation reference before acceptance.", + "D3-shared-summary-presentation/store-type-consumption-boundary-unspecified": "Use @webex/cc-store as the sole normalized summary-model type authority. D3 lists cc-components/package.json, retains or adds its exact workspace dependency, uses import type plus an Extract-derived editable content subset, and forbids duplicate local model unions, a direct SDK dependency, or a D3-introduced runtime store import." + }, + "description": "Operator-approved layout clarification (2026-09-25, design_spec.md): supersede earlier single-outer-scrollbar/no-nested-scrollbar clauses and scroll-owner test expectations in this task. Keep the 80vh panel heading, summary actions and completion action visible; destination/reason lists and shared summary content may scroll independently without individually scrolling keyed editors or horizontal overflow. Preserve source-aligned panel capture, declared tooltip overhang, all text, keyboard access, interaction state and focus. All other task obligations, dependencies and acceptance commands remain unchanged. REQ-002, REQ-003, REQ-004, REQ-005, REQ-008, REQ-009, REQ-010, UX-002, UX-003, UX-004, UX-005, UX-006, UX-007, UX-008, UX-009, UX-010, AC-03, AC-04, AC-05, AC-11, AC-12, AC-13, AC-14, and AC-16 / Component: Shared Summary Presentation - only after D0a admits the target-worktree UX evidence, add the single exported AISummary component with mode-discriminated AISummaryProps and closed AISummaryCopyVisualState in ai-summary.types.ts. Treat @webex/cc-store as the single source of truth for AISummaryContent, AISummaryEditableField, AISummaryDisplaySectionKey, AISummaryFeedback, AISummaryFeedbackStatus, AISummaryFeedbackResult, and AISummaryRequestResult. In ai-summary.types.ts and ai-summary.constants.ts use import type only, derive the typed/plain EditableSummaryContent subset with Extract, use Exclude for selectable actions, and do not redeclare structurally equivalent section, field, feedback, or result unions. Add packages/contact-center/cc-components/package.json to D3 and retain or add exactly dependencies[\"@webex/cc-store\"] = \"workspace:*\" so emitted declarations have a declared package edge; add no alias, peer relocation, direct @webex/contact-center dependency, or runtime store/SDK import. The three modes are mid-call-initiator, mid-call-receiver, and post-call: initiator/post-call accept only typed or plain editable content; receiver is an action-only surface whose ready variant requires contentRevision plus a synchronous getReceiverCopyText getter, forbids card JSON and editing, and never imports or renders AdaptiveCardRenderer; post-call alone accepts feedbackStatus and Retry. All four presentation states return React.ReactElement; omission remains a container decision. Add the exact en-US message IDs and strings, including pending/not-confirmed feedback messages, role-neutral shared section IDs and exact capitalization, the corrected aiSummary.generatingDescription value Just a sec—the details are coming together. with U+2014 and no U+005F, and the uniform unresolved-percent-variable rule with delimiter-free constants. Definition tuples are label metadata only. Define one unique exhaustive POST_CALL_DISPLAY_SECTION_ORDER in ai-summary.constants.ts typed by D2's AISummaryDisplaySectionKey; retain resolution separately, insert a non-empty read-only Outcome before the first present greater ordinal or append it, and omit a missing, undefined, or empty resolution with no row or reserved space, while every admitted SDK section keeps its relative normalized order. Edits use exact retained keys, cleared keys remain present, flattened strings are clipboard-only, and Outcome remains absent from response sections. Treat existing CallControl and AI Assistant styles as the host contracts; use container-scoped semantic-token extensions, dir=auto, wrapping, bounded vertical-only overflow, focus-visible, and forced-colors behavior without a feature theme map or screenshot-sized outer shell. AI Assistant retains its panel-body scroll owner, while both CallControl modes use an 80vh shell with independently bounded list and summary-content regions, visible headings/actions, and auto-sizing keyed editors. AISummary owns the single shared action row and exact labels for all roles, the sole direct-gesture navigator.clipboard.writeText call for typed, plain, and receiver content, controlled mutually exclusive/reselectable feedback with immediate guarded local paint and pending description for post-call but SDK-success-gated paint for mid-call, copy visual state/timer cleanup, and post-call status/Retry rendering. In receiver mode the same non-async click handler synchronously calls getReceiverCopyText before the Clipboard write; it receives extracted visible text only and no untrusted card JSON. Copy visual state changes only the copy affordance and cannot select reason-search or primary-action messages. Its non-async React handlers return void, catch synchronous throws, attach explicit fulfillment and rejection handlers before returning, and reduce copy/feedback/Retry settlement to closed confirmed/failed/blocked/stale handling. The synchronous boolean onCopy recorder is called exactly once only after Clipboard fulfillment; false is stale and does not paint, while extraction, Clipboard, or callback failure cannot become unhandled. Mid-call feedback paints only an always-fulfilled AISummaryFeedbackResult outcome confirmed; failed/blocked/stale preserve the prior selection. Capture the focused control and next enabled document-order successor in onFocusCapture before mutation, then use useLayoutEffect only when that exact control is disconnected, choosing the connected successor and then containingPanelFocusTarget; connected controls never move. Keep the receiver action row mounted across valid card replacements so focus survives, reset copied confirmation from contentRevision, and use successor/fallback when unavailable/error/omission removes it. Containers branch only on summarySurface. D4, not D3, owns the display-only AdaptiveCardRenderer sibling and the extractCardText-backed getter; AdaptiveCardRenderer and RealTimeAssist gain no summary discriminator, label map, selected-feedback state, Clipboard callback, feedback responsibility, child slot, or feature-control descendant and retain their current contracts. Type/build acceptance proves the manifest edge, type-only normalized-model imports, Extract-derived editable subset, absence of duplicate local model unions, and no D3-introduced runtime @webex/cc-store or @webex/contact-center import; focused tests cover exact labels/tooltips and message IDs, the unique exhaustive display ordinal, paired present/absent resolution behavior without SDK-section reordering or absent-row space, corrected U+2014 generating copy, rejection of U+005F and percent artifacts, section order/edit/copy behavior, all three compile-time mode branches, receiver rejection of card JSON/renderer children, synchronous visible-text getter and contentRevision behavior, direct-gesture Clipboard pending/reject/fulfill sequencing with boolean recorder acceptance, feedback/Retry synchronous throw and rejection with zero unhandledrejection, controlled selection/reselection, token messages, all presentation states, dir=auto, focus successor/panel fallback, and overflow/forced colors. D4 owns renderer sibling/containment, inert embedded actions, extraction order, connected focus across card replacement, and unchanged RealTimeAssist integration tests. D1 supplies deterministic role-compatible content and D2 supplies normalized contracts. This task imports no SDK and may proceed while EXT-SDK-CC-SUMMARIES is Deferred only after its direct D0a and D2 dependencies complete, with D1 carried transitively through D2; it cannot claim container/runtime integration until D2b.", + "files": [ + "packages/contact-center/cc-components/package.json", + "packages/contact-center/cc-components/src/components/AISummary/ai-summary.tsx", + "packages/contact-center/cc-components/src/components/AISummary/ai-summary.types.ts", + "packages/contact-center/cc-components/src/components/AISummary/ai-summary.constants.ts", + "packages/contact-center/cc-components/src/components/AISummary/ai-summary.styles.scss", + "packages/contact-center/cc-components/src/components/AISummary/index.ts", + "packages/contact-center/cc-components/src/index.ts", + "packages/contact-center/cc-components/tests/components/AISummary/ai-summary.tsx", + "packages/contact-center/cc-components/ai-docs/cc-components-spec.md" + ], + "dependencies": [ + "D0a-ux-evidence-admission", + "D2-interaction-summary-store" + ], + "acceptance": "yarn workspace @webex/cc-components build && yarn workspace @webex/cc-components test:unit --runInBand --runTestsByPath tests/components/AISummary/ai-summary.tsx" + }, + { + "id": "D4-receiving-ai-assistant", + "title": "Integrate receiver summaries in the existing AI Assistant panel", + "focus_fallback": { + "ref": "panelRef.current", + "selector": "[data-testid=\"ai-assistant:panel\"][role=\"dialog\"]", + "focusable_contract": "tabIndex -1", + "last_control_removal_scenario": "Removing the last focused receiver summary control falls back to the AI Assistant dialog root" + }, + "design_review_fixes": { + "REQ-004/action-controls-inside-adaptive-card-renderer-boundary": "Make AIAssistantComponent the receiver composition owner. Render the display-only AdaptiveCardRenderer and D3's shared AISummary receiver action surface as DOM siblings, assert that no receiver action control is contained by the renderer, keep AdaptiveCardRendererProps and security ownership unchanged, and pass AISummary only a synchronous extractCardText-backed visible-text getter.", + "REQ-004:adaptive-card-local-image-policy-definition": "Inside the existing AdaptiveCardRenderer boundary, allow only exact resolveMomentumIconUrl bundled values; remove all other Image.url, action iconUrl, and backgroundImage sources before parse/DOM creation. Preserve remaining text/labeled actions, route sanitized-empty image-only cards to exact unavailable fallback, and prove zero remote request with both D1 fixtures.", + "D4-receiving-ai-assistant+D5-mid-call-call-control/missing-req009-req010-trace": "Keep REQ-008, REQ-009, and REQ-010 in both D4 and D5 task descriptions and visible summary trace cells so each rendering integration owns its feedback/counter, accessibility/error/focus, and in-memory/no-log acceptance obligations before aggregate D8/D9 validation." + }, + "description": "REQ-002, REQ-004, REQ-006, REQ-008, REQ-009, REQ-010, JNY-001, AC-04, AC-06, AC-10, AC-11, AC-12, AC-13, AC-14, and AC-16 / Component: Receiving-Agent AI Assistant Integration - extend useAiAssistant and the observer container with one optional receiverSummary object. Its sole receiver action keys are openReceiverSummary, recordReceiverSummaryCopied, and setReceiverSummaryFeedback; no aliases are permitted. Import AISummaryContent, AISummaryActionType, AISummaryFeedback, and AISummaryFeedbackResult from @webex/cc-store, use Exclude for selectable feedback, make recordReceiverSummaryCopied return the exact boolean from recordAISummaryCopied('mid-call', 'receiver', expectedRevision), and make setReceiverSummaryFeedback return the store's always-fulfilled outcome-discriminated setMidCallSummaryFeedback('receiver', feedback, actionType, expectedRevision) Promise. That name is an internal store action, not an SDK method: @webex/cc-store composes a typed AISummaryResponse and is the sole caller of capturedTask.sendMidCallSummaryResponse(payload, resolvedActionType); no AI Assistant layer receives a Task or calls the SDK. Existing AIAssistantComponentProps callers may omit receiverSummary; omission renders no receiver notification or panel branch. Define a closed union: content requires validated adaptive-card content, contentRevision, actionType, selected feedback, pending/disabled state, and all three actions; unavailable and generic-error carry only their surface plus openReceiverSummary and forbid content/copy/feedback fields; omitted, generating, and undefined produce no object. Reuse the existing AI Assistant root chrome, closed launcher, minimized restore control, open header, and body switch. Render a native View summary notification in the established trigger slot with accessible name and tooltip exactly View summary. On activation capture the focused trigger/successor before mutation, call openReceiverSummary, record a content view only when recordAISummaryViewed('mid-call', 'receiver', renderedRevision) returns true, then open/restore and select the receiver branch. After the trigger disappears, focus the first connected enabled successor in the new panel DOM, which is the current header Minimize control, or fall back to the existing data-testid ai-assistant:panel role=dialog root held by panelRef and made focusable with tabIndex -1. In AIAssistantComponent's content branch, directly render the display-only AdaptiveCardRenderer and D3's AISummary mid-call-receiver action surface as DOM siblings beneath one receiver-branch ref. Give AdaptiveCardRenderer only its existing card/fallback render inputs; do not add or change renderer props, ErrorBoundary ownership, child slots, summary-mode action contracts, summary labels, feedback state, Clipboard callbacks, or RealTimeAssist opt-ins. Inside the existing renderer boundary only, extend prepareCardForRender and the render-empty check with a recursive pre-parse image-resource sanitizer: rewrite recognized icon basenames to exact resolveMomentumIconUrl outputs; remove every other Image.url, action iconUrl, and string/object backgroundImage.url before Adaptive Cards can create DOM or a request; preserve remaining text and labeled actions; and route sanitized-empty image-only cards to the existing exact The summary is not available fallback. Remove embedded actions from the display-only projection and supply neither onUserAction nor arbitrary onAction. Pass AISummary contentRevision and a synchronous getReceiverCopyText closure that invokes the existing extractCardText utility over the sibling branch; AISummary receives no card JSON and owns the exact labels, shared copy/feedback controls, direct-gesture Clipboard call, controlled paint, dir=auto, and action-row focus. Its copy and feedback React handlers are non-async, return void, catch synchronous throws, and attach both Promise settlement handlers before returning. Clipboard fulfillment calls recordReceiverSummaryCopied(renderedRevision) once; only true paints success. Extraction/Clipboard throw, Clipboard rejection, a false stale recorder, SDK rejection normalized by the store to outcome failed, or an unexpected direct-consumer rejection produces no unhandledrejection and preserves the prior paint. Feedback forwards the rendered actionType/revision, paints only outcome confirmed after SDK fulfillment, and preserves selection for failed/blocked/stale. Projection revalidates Task correlation; an indeterminate value before acceptance creates no ready view, while one arising after ready content sets controlsDisabled so the shared action row has native disabled semantics and existing non-color styling, invokes no handler, sends nothing, and preserves the prior selection. Keep the sibling action row mounted across a valid card replacement so only the renderer updates and connected focus remains; pass the new contentRevision to reset copied confirmation. When unavailable, generic-error, omission, interaction change, or cross-agent ownership removes a focused control, advance to its captured next enabled document-order successor and then panelRef. Split retention explicitly: a confirmed cross-agent handoff makes the predecessor key non-current immediately, clears its visible receiver card, and starts the new agent with zero counters; a same-agent conference establishment or regeneration successor retains the current card, feedback, revision, and counters while preparation is pending or fails and discards them only after valid freshness-winning replacement content is admitted. Current attributed malformed input with no content may show generic-error, freshness-accepted unrenderable input shows unavailable, and stale/unattributable input is dropped. Add no IncomingTask dependency or AI Assistant host property/event/callback; preserve IAIAssistantProps, WebAIAssistant, RTA behavior, and store-owned status forwarding through mounted CallControl only. Tests cover legacy omission, unauthorized or initialization failure hiding both notification and panel branch, exhaustive states, trigger reachability/name/tooltip, closed/minimized open/restore and view counts, exact store mappings, AIAssistantComponent sibling composition, explicit renderer non-containment of every receiver view/copy/like/dislike control, unchanged AdaptiveCardRendererProps and security ownership, existing render/fallback props only, exact bundled-image allowlist behavior, zero remote assignment/request for remote-image-with-text and remote-image-only fixtures, preservation of remaining text/labeled actions, exact unavailable fallback for sanitized-empty cards, extractCardText newline order excluding sibling action labels, inert embedded actions, copy pending/reject/fulfill counter sequencing including rejected Clipboard leaving the copied counter unchanged, rejected SDK/direct-consumer feedback Promises leaving the previous selection painted with zero unhandledrejection, stale action/revision no-send/no-paint, unknown action type disabled-and-no-send behavior, selection/reselection, notification activation successor/panel fallback, renderer-only replacement retaining connected action focus, focused-control disappearance successor/panel fallback, cross-agent immediate clear/zero counters, same-agent conference pending/failure retention until valid replacement, event attribution/error surfaces, RTA isolation, unmount cleanup, and unchanged host surfaces. Modify only the listed helper/container/component/styles/tests/module specifications plus AdaptiveCardRenderer/adaptive-card-renderer.tsx, adaptive-card-renderer.utils.ts, and their two focused tests. D2b and D3 are required; D4 remains disjoint from D5/D6, and D6b later serializes the shared cc-components specification edit.", + "files": [ + "packages/contact-center/ai-assistant/src/ai-assistant.types.ts", + "packages/contact-center/ai-assistant/src/ai-assistant/index.tsx", + "packages/contact-center/ai-assistant/src/helper.ts", + "packages/contact-center/ai-assistant/tests/ai-assistant/index.tsx", + "packages/contact-center/ai-assistant/tests/helper.ts", + "packages/contact-center/ai-assistant/ai-docs/ai-assistant-spec.md", + "packages/contact-center/cc-components/src/components/AIAssistant/ai-assistant.tsx", + "packages/contact-center/cc-components/src/components/AIAssistant/ai-assistant.types.ts", + "packages/contact-center/cc-components/src/components/AIAssistant/ai-assistant.styles.scss", + "packages/contact-center/cc-components/src/components/AIAssistant/AdaptiveCardRenderer/adaptive-card-renderer.tsx", + "packages/contact-center/cc-components/src/components/AIAssistant/AdaptiveCardRenderer/adaptive-card-renderer.utils.ts", + "packages/contact-center/cc-components/tests/components/AIAssistant/ai-assistant.tsx", + "packages/contact-center/cc-components/tests/components/AIAssistant/adaptive-card-renderer.test.tsx", + "packages/contact-center/cc-components/tests/components/AIAssistant/adaptive-card-renderer.utils.test.ts", + "packages/contact-center/cc-components/ai-docs/cc-components-spec.md" + ], + "dependencies": [ + "D2b-interaction-summary-sdk-binding", + "D3-shared-summary-presentation" + ], + "acceptance": "yarn workspace @webex/cc-ai-assistant test:unit --runInBand --runTestsByPath tests/ai-assistant/index.tsx tests/helper.ts && yarn workspace @webex/cc-components test:unit --runInBand --runTestsByPath tests/components/AIAssistant/ai-assistant.tsx tests/components/AIAssistant/adaptive-card-renderer.test.tsx tests/components/AIAssistant/adaptive-card-renderer.utils.test.ts" + }, + { + "id": "D5-mid-call-call-control", + "title": "Build the disjoint initiating summary module and consult-transfer view", + "focus_fallback": { + "ref": "consultTransferPanelRef.current", + "selector": ".agent-popover-content", + "focusable_contract": "tabIndex -1 on the open consult/transfer instance", + "last_control_removal_scenario": "Removing the last focused mid-call summary control falls back to the consult/transfer panel root" + }, + "design_review_fixes": { + "post-call-wrapup:one-consumer-claim-contradicted-by-mid-call-search-radio-layout": "ConsultTransferPopoverComponent is the sole owner of the voice category, search, filtering, results, divider, editor, collapse/reopen, and popover-focus composition. D6b may mount it from call-control.tsx and pass trigger or telephony props, but call-control.tsx must not implement a second copy. The focused popover and shared CallControl suites must assert one renderer and identical node identity, values, filtering, selection, and focus with the summary present or omitted.", + "UX-001:eligible-and-legacy-consult-popover-layout-duplication": "Treat UX-001's header, radio categories, search, and destination-result order as required sealed-source structure for every voice destination popover. Capture the voice/non-voice/existing-party destinationLayout from non-summary context at open and latch it until close. Summary capability may append or remove only the divider and summary subtree; it never selects a search-first pill voice tree. Retain pills solely for distinct non-voice behavior and test node/value/filter/focus continuity across mid-open eligibility and ownership-generation transitions.", + "UX-001/eligibility-conditional-popover-reordering-unlatched": "Latch destinationLayout when each popover instance opens and discard it only on close/unmount; feature eligibility, capability timestamp, request settlement, and ownership generation are not latch inputs. A capability arrival/revocation while query/category/destination/focus state exists may change only summary descendants. Focused tests assert destination node identity/order, values, filtering, selection, scroll owner, and focus, then prove close/reopen derives a fresh latch.", + "UX-001:transfer-variant-copy-invented-without-structural-only-receipt": "Use the distinct aiSummary.midCall.transferHeading only for a verified TRANSFER preparation. Treat its candidate string as provisional structural-only copy rather than UX-001-derived authority; D8 must record the final product-confirmed exact value, confirmation reference, and action-parity justification.", + "UX-004+UX-006+UX-008+UX-009:literal-percent-delimited-figma-placeholder-shipped-as-copy": "For the shared mid-call search surface, treat the outer percent pair as a Figma variable marker stripped at the rendering boundary and render the exact delimiter-free placeholder Search by name, queue, entry point or phone number. Focused tests reject percent markers in visible or accessible output.", + "transfer-variant/heading-copy-without-source-authority": "Render Transfer through aiSummary.midCall.transferHeading only for the TRANSFER preparation, never claim UX-001 supplied its text, and leave D8 fail-closed until the structural receipt binds the final exact DOM copy to product/content-owner confirmation." + }, + "description": "Operator-approved layout clarification (2026-09-25, design_spec.md): supersede earlier single-outer-scrollbar/no-nested-scrollbar clauses and scroll-owner test expectations in this task. Keep the 80vh panel heading, summary actions and completion action visible; destination/reason lists and shared summary content may scroll independently without individually scrolling keyed editors or horizontal overflow. Preserve source-aligned panel capture, declared tooltip overhang, all text, keyboard access, interaction state and focus. All other task obligations, dependencies and acceptance commands remain unchanged. REQ-002, REQ-003, REQ-006, REQ-008, REQ-009, REQ-010, UX-001, JNY-001, AC-03, AC-06, AC-10, AC-11, AC-12, AC-13, AC-14, and AC-16 / Component: Initiating-Agent Call Control Integration - add ai-summary-mid-call.ts as the focused open-request observation and response-settlement-before-action orchestration consumed later by D6b's useAISummaryActions(ownerKey) hook, with no SDK access outside store actions. The initiating callback uses the single canonical name requestMidCallSummary in the feature module and co-located popover contract. Import AISummaryActionType and AISummaryFeedback from @webex/cc-store in the task and cc-components contracts; declare no local CONSULT/TRANSFER or thumbs union, and type selectable feedback as Exclude. Extend ConsultTransferPopoverComponent through co-located closed types and feature-local styles. The named sealed UX-001 scene graph/text/screenshot triple establishes one unconditional voice Consult/Transfer destination hierarchy: Momentum radio categories, the resolved delimiter-free Search by name, queue, entry point or phone number placeholder, and the existing results renderer. Render availableCategories, handleCategoryChange, searchQuery, filtering, pagination, and selection exactly once in the voice branch; capability may only append or remove divider, the resolved consult/transfer heading, bottom editor, and summary actions after those same results. Retain legacy pill categories only for non-voice behavior; they consume the same availableCategories and handleCategoryChange as the voice RadioGroup/Radio controls, and tests require identical filtering results. Apply the matched-percent-slot extraction rule to static and D8 dynamic fixture values so no delimiter reaches visible or accessible copy. For every voice state, .agent-popover-content retains an 80vh bound with headings and actions visible; .consult-list-container and the shared summary content use independent bounded vertical scroll regions, while individual editors wrap and auto-size using min-inline-size: 0, white-space: pre-wrap, overflow-wrap: anywhere, and word-break: break-word. Multi-row editors auto-size and never ellipsize, clamp, or create an inner scrollbar. On popover open, capture a closed destinationLayout value from non-summary context and latch it until close; capability timestamp, eligibility, request settlement, and ownership generation are not inputs. Capability arrival/revocation or same-interaction ownership-generation change keeps category/search/result nodes, typed query, category/destination selection, result order, scroll owner, and connected focus mounted; only summary descendants may mount, rebind, or unmount, and only focus inside a removed summary subtree uses the REQ-009 successor/fallback. Close releases the latch, reopen derives it anew, and a new canonical interaction resets scoped state. Pending request state keeps destination, quick-action, and existing-party confirmation nodes focusable with aria-disabled plus guarded pointer/keyboard handlers rather than applying native disabled, so a focused control never falls to document.body. Make the heading-associated, tabIndex -1 .agent-popover-content ref the named connected containing-panel fallback after the captured document-order successor; whole-popover close restores the exact Consult, Transfer, Transfer Conference, or Merge trigger, and loading/content replacement never moves still-connected focus. For a new owner/action preparation with voice capability, start requestMidCallSummary exactly once only when no same-preparation request is pending and no current settled result exists, and immediately attach explicit fulfillment and rejection handlers at the open/effect call site. The store adapter maps SDK synchronous throw, generation rejection, and 15-second timeout into normalized omitted/generic-error state and a closed result; the defensive rejection arm consumes any unexpected callback rejection through the same sanitized failure observation without exposing or rethrowing raw error. Closing does not cancel. Reopening the same preparation while pending reattaches to the in-flight store view without another SDK call; reopening current content, unavailable, or generation-error reveals it without an implicit request, while a genuinely new owner/action preparation may start once. Branch only on summarySurface: omitted removes the summary region, unavailable shows The summary is not available, and generic-error shows Having trouble generating summary. Pass only AISummary mode mid-call-initiator at the presentation boundary, with no redundant kind/role props. Render initiating sections as one visual bottom editor with immutable labels in D2's normalized SDK payload order and per-key controlled values; call editAISummary with kind mid-call, role initiator, and the exact section key/value, retain cleared keys as empty values, and never parse a flattened label-delimited string. Plain content edits only summaryText. Discard cards. AISummary owns the one Clipboard write; the popover maps its synchronous boolean post-fulfillment onCopy recorder to the revision-only copied recorder, returns that acceptance result so stale input does not paint, passes it no Clipboard Promise, and performs no Clipboard write itself. Test both structured exact-key edit/clear round trips and the plain-string round trip into the SDK composer. The discriminated initiator owner always carries the D0-correlation-resolved actionType of the freshness-accepted owning request; request start and settlement both block without SDK call or paint when the correlation is mismatched or indeterminate; participant-add starts as CONSULT, and the store re-evaluates the D0-verified Task correlation so a confirmed consult-to-transfer promotion becomes TRANSFER before feedback or pre-action composition. An indeterminate correlation returns blocked, calls no response SDK method, and does not paint. The internal setMidCallSummaryFeedback(feedback, actionType, revision) callback receives the rendered action type and content revision with every feedback selection/re-selection; the store rejects either mismatch as stale before pending state, composition, or an SDK call, including a queued CONSULT click after TRANSFER promotion. Each guarded current selection/re-selection calls the store path that invokes capturedTask.sendMidCallSummaryResponse(payload, resolvedActionType), uses midCallFeedbackPending only for the in-flight disable, observes the always-fulfilled AISummaryFeedbackResult, and paints only for outcome confirmed. That update is distinct from and cannot replace the once-per-telephony-attempt response send invoked immediately before the existing action. Invoke and await the store send's always-fulfilling outcome-discriminated sent, failed, blocked, or stale result, consuming synchronous throw, rejection including timeout, indeterminate action correlation, and ownership-generation invalidation; failed/blocked/stale mutate no response-derived state. Only after that settlement invoke the captured existing consultCall, transferCall, consultTransfer, or consultConference action exactly once through its existing guard and unchanged SDK arguments, then observe the telephony Promise; while the response is pending every telephony spy remains uncalled, and a non-sent result never suppresses the one later action or becomes unhandled. Give consult-transfer-popover.tsx a closed destination versus existing-party-action surface discriminator in its co-located types. The destination branch keeps destination selection as its confirmation gesture. Define the no-destination existing-party branch for D6b to mount from an eligible standalone Transfer or Transfer Conference trigger: opening starts one TRANSFER preparation request and holds its summary, while only the explicit Transfer confirmation runs the pre-action response invocation before the existing consultTransfer callback reaches task.transferConference(). Define the eligible Merge trigger to open the same branch with the current or in-flight CONSULT view without another request; only its explicit Merge confirmation runs the response invocation before consultConference reaches task.consultConference(). Closing either branch calls neither method and returns focus to its trigger; an omitted summary preserves both legacy immediate standalone callbacks. Define the fulfilled-action boundary reporting consumed by D6b: direct/consult transfer reports handoff-complete and successful conference establishment reports conference-established with the pre-action owner key; rejection reports no boundary, and duplicate Task-event reporting must compare-and-swap no-op. A consult that has not transferred or established conference retains its generation. Apply the same fail-closed stable interaction and owner-generation rules for direct transfer, consult-to-transfer, additional transfer, and conference addition. Name tests for one shared destination renderer and identical filtering/selection/pagination with summary present or omitted, resolved delimiter-free placeholder/headings, conditional summary placement after results, and a mid-popover capability arrival/revocation plus ownership-generation change after query entry/category/destination selection that preserves the latched layout, destination node identity/order/value/filtering/selection/scroll owner/focus while only summary descendants change; close/reopen derives a fresh latch; canonical requestMidCallSummary naming at the module/popover boundary, explicit open/effect fulfillment/rejection observation, SDK rejection/15-second timeout normalized with zero unhandled rejection, pending close/reopen with no second SDK call, settled same-owner/action reopen with no implicit request and a new view count only on successful reveal, a new preparation requesting exactly once, the 80vh voice popover cap, bounded list/content scroll regions, visible headings/actions, and auto-sized editors, wrapped unclipped summary rows, the sole mid-call-initiator presentation mode, initiating-role view/edit/post-fulfillment revision-only copied-recorder targeting with a coexisting receiver view left unchanged and no container-owned Clipboard write, shared store type imports with no local aliases, feedback actionType/revision forwarding and stale no-send/no-paint, owning-request actionType, consult-to-transfer re-evaluation, indeterminate block-and-do-not-paint, and request/feedback/pre-action synchronous throw, rejection, timeout, and staleness always fulfilling closed outcomes before telephony continues exactly once after settlement. Name explicit tests for a focused pending destination/quick-action/confirmation retaining focus with aria-disabled and guarded activation; summary removal choosing the captured successor then .agent-popover-content; whole-popover close returning to the exact trigger; replacement under connected focus causing no movement; open-request rejection plus feedback synchronous throw or returned-Promise rejection producing zero unhandledrejection events; radio/pill category-filter parity; the Transfer or Transfer Conference surface requesting TRANSFER once, keeping task.transferConference at zero while the response is pending, and calling it exactly once after settlement; the Merge surface reusing CONSULT with no request, keeping task.consultConference at zero while pending, and calling it exactly once after settlement; close invoking neither method; and omitted-summary immediate-action compatibility. D2b/D3 provide admitted state/presentation. This task edits no shared CallControl hook/type/container/test/spec file and has no dependency on D4 or D6; D6b performs the shared integration after all three parallel feature tasks finish.", + "files": [ + "packages/contact-center/task/src/ai-summary-mid-call.ts", + "packages/contact-center/task/tests/ai-summary-mid-call.ts", + "packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx", + "packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-summary.types.ts", + "packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/consult-transfer-summary.styles.scss", + "packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx" + ], + "dependencies": [ + "D2b-interaction-summary-sdk-binding", + "D3-shared-summary-presentation" + ], + "acceptance": "yarn workspace @webex/cc-task test:unit --runInBand --runTestsByPath tests/ai-summary-mid-call.ts && yarn workspace @webex/cc-components test:unit --runInBand --runTestsByPath tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx" + }, + { + "id": "D6-post-call-wrap-up", + "title": "Build the disjoint post-call orchestration module and wrap-up view", + "focus_fallback": { + "ref": "wrapUpPanelRef.current", + "selector": ".agent-popover-content", + "focusable_contract": "tabIndex -1 on the open wrap-up instance", + "last_control_removal_scenario": "Removing the last focused post-call summary control falls back to the wrap-up panel root" + }, + "design_review_fixes": { + "post-call-wrapup:setPostCallSummaryFeedback-inline-union-duplicates-AISummaryFeedback": "Import AISummaryFeedback from @webex/cc-store and derive the selectable parameter as Exclude; no task or component contract may repeat the thumbs literal union.", + "post-call-wrapup:wrapupCall-and-completeWrapupWithSummary-relationship-unspecified": "Keep completeWrapupWithSummary as a package-internal export of ai-summary-post-call.ts and the sole implementation/sole task.wrapup call site. D6 does not edit a package entry or public hook/prop; D6b alone adds the published wrapupCall adapter that delegates to this implementation for eligible and ineligible branches.", + "post-call-wrapup:wrapup-prop-void-or-promise-union-breaks-awaited-submit": "D6's package-internal orchestration has the exact Promise return, catches and sanitizes task.wrapup synchronous throws and Promise rejections in both eligible and ineligible branches, always fulfills a closed result, and tests both branches for locked-until-settlement behavior and zero unhandled rejection. D6b alone threads that exact return through published props.", + "D6-post-call-wrap-up/exported-hook-signature-change-untraced": "Trace REQ-007 and AC-09 to D6's legacy-path compatibility assertion and to D6b's sole public-seam ownership. D6 proves its orchestration remains outside package entries and leaves shared hook/prop files untouched; D6b proves custom-element compatibility, documents the breaking direct-React migration, and requires SemVer-major metadata.", + "D6-post-call-wrap-up/missing-ux-002-010-trace": "Trace UX-002 through UX-010 directly to D6 and require it to resolve every state to the exact D0a-sealed scene-graph JSON, text JSON, and PNG triple before implementing the post-call hierarchy; D8 compares the completed structure rather than retrofitting it.", + "post-call-wrapup:empty-search-result-renders-nothing": "Render zero matches as ordinary visible text from stable message ID aiSummary.postCall.noReasonMatches with exact en-US copy No wrap-up reasons match your search. The node is not a status, alert, live region, or programmatic announcement, and D6 owns the structural-only component test.", + "post-call-wrapup:radio-selection-change-fires-sdk-request-per-keystroke": "RadioGroup change updates the selected reason and a local selection revision without transport. Pointer activation, Enter/Space, or focus leaving the group commits through one owner-and-selection-revision-deduplicated request seam; any arrow-key traversal therefore issues exactly one request for the final committed reason, and duplicate activation/blur events issue none.", + "post-call-wrapup:reason-reselection-silently-discards-edited-summary": "Keep accepted/edited post-call content and its revision across reason changes and popover close/reopen. Pending, blocked, stale, failed, or unsupported replacement work cannot clear it; only a newer ready response accepted after explicit reason commit may replace it, and capture first freezes the visible draft and makes a later settlement stale.", + "post-call-wrapup:reason-search-and-radiogroup-lack-specified-accessible-names": "Give the Input exact aria-label Search wrap-up reasons from stable message ID aiSummary.postCall.reasonSearchLabel. Resolve the existing Choose a reason to wrap up heading from aiSummary.postCall.chooseReason, assign it an instance-local useId DOM id, and reference that id from RadioGroup aria-labelledby; D6 tests both names by role.", + "post-call-wrapup:resolution-outcome-row-hardcoded-between-named-sections": "Consume D3's typed POST_CALL_DISPLAY_SECTION_ORDER projection and D1's paired resolution fixtures. The present case positions Outcome when adjacent sections are absent; the key-omitted case renders no row, text, edit target, copy block, response key, or reserved space. Admitted SDK sections retain their relative order.", + "UX-003/outcome-resolution-row-absence-and-ordering": "Run otherwise-identical resolutionPresent and resolutionAbsent post-call fixtures through the full wrap-up component. Assert stable relative-ordinal insertion for the present value and total row/space/copy/response omission for the absent value without reordering SDK sections.", + "post-call-wrapup:complete-disabled-during-generation-and-error-no-escape": "The initial no-draft request may disable completion only while pending, bounded by the store-owned 15-second timeout. Terminal generation error or unavailable sets an owner-and-reason-revision escape; Complete Wrap-Up then succeeds without a draft as response not-required, Retry cannot revoke the escape, and the exact test generation error still permits wrap-up completion is mandatory.", + "post-call-wrapup:combined-reason-list-plus-summary-overflow-unspecified": "Use the existing .agent-popover-content as an 80vh shell in expanded and collapsed layouts. Reason lists and summary content scroll independently without horizontal overflow; headings, summary actions and Complete Wrap-Up remain visible and keyboard reachable at 320px. Individual editors auto-size.", + "post-call-wrapup:copy-visual-state-mutates-primary-submit-label": "onCopyVisualStateChange may update only the Copy Summary control treatment. Every post-call state sources the primary visible label and accessible name from the single stable Complete Wrap-Up constant, with an exact hover/confirmed stability test.", + "post-call-wrapup:onCopyVisualStateChange-referenced-but-not-declared": "List the required onCopyVisualStateChange: (state: AISummaryCopyVisualState) => void wrapper prop and use it only to update local copyVisualState.", + "post-call-wrapup:percent-delimited-figma-placeholders-treated-as-literal-copy": "Use the delimiter-free Search topic and requirement-controlled Complete Wrap-Up message constants in every state, keep the primary visible label/name/tooltip identical, and add a component assertion that no rendered visible or accessible string contains an unresolved percent marker.", + "UX-002..UX-010/addressed-rows-trace-only-to-validation-tasks": "D6 is the implementing owner for UX-002 through UX-010's post-call composition, eligible/legacy transitions, reason controls, generating/editing/feedback/copy/error state orchestration, and focused component coverage; D8 and D9 consume the completed behavior only as verification gates.", + "UX-002..UX-010:post-call-popover-width-400-440-442-unreconciled": "Preserve the existing cc-calling-widget host popover inline size as the sole production sizing rule. Neither eligibility, expanded/collapsed reason state, lifecycle, nor UX source ID may select a 400, 440, or 442 pixel component width; those values are external D8 fixture widths for the same responsive component.", + "UX-002:wrap-up-reason-change-after-generation-transition-unspecified": "Call requestPostCallSummary('reason-commit') for each explicit reason commit and requestPostCallSummary('retry') only for Retry. Preserve the visible edited/copied/rated draft, revision, counters, selected feedback, and Pending submission while replacement is pending or non-accepted. A newer reason generation supersedes every older in-flight settlement before timestamp ordering; only current-generation ready acceptance replaces content, preserves edited/copied totals, increments viewed once, resets feedback/status, and clears copy confirmation through the normal content-change path. Capture freezes the visible draft/counters/feedback and makes later settlements stale.", + "UX-004+UX-006+UX-008+UX-009:literal-percent-delimited-figma-placeholder-shipped-as-copy": "Treat every complete sealed outer-percent token uniformly as an unresolved Figma variable and resolve it before component input. Every post-call state renders exact Search topic and requirement-controlled Complete Wrap-Up strings, and focused DOM/accessibility assertions reject delimiter artifacts.", + "UX-004..UX-009/percent-delimited-placeholder-strings-as-production-copy": "Render only resolved Search topic and Complete Wrap-Up constants; never forward or author percent-delimited marker strings. Keep Complete Wrap-Up identical across visible label, accessible name, and tooltip, while D8 handles source-only delimiter pixels through fixed comparison masks.", + "UX-005+UX-007:post-call-selected-state-gated-on-success-contradicts-local-pending": "Post-call Like/Dislike paints immediately when setPostCallSummaryFeedback returns true, remains mutually exclusive, and renders the requirement-owned structural-only Pending submission description without an SDK call. Both controls reference it through aria-describedby. Confirmed final submission removes the description and both references; response failure preserves the read-only selected state and renders/associates the structural-only Submission not confirmed description. Mid-call selected treatment remains SDK-success-gated in D3/D4/D5.", + "UX-004+UX-006-vs-UX-008+UX-009:complete-wrap-up-copy-treated-inconsistently": "Use the single aiSummary.postCall.completeAction value Complete Wrap-Up for the visible primary text, accessible name, and tooltip in every UX-002 through UX-010 state; copy hover/confirmation and feedback state cannot vary it.", + "UX-009:copy-success-treatment-duration-and-revert-trigger-unspecified": "Consume D3's exact 1,500 ms confirmed-state contract and keep onCopyVisualStateChange scoped to the copy affordance. Popover close or accepted summary replacement clears the visual state; second-copy and focus-loss resets are owned by AISummary and covered by the focused shared-component test.", + "UX-010:complete-wrap-up-permanently-disabled-on-generation-error": "Disable Complete Wrap-Up for summary availability only during the initial bounded no-draft request. After an unrecovered terminal generation error or unavailable settlement, enable it for the committed reason; the mandatory D6 test clicks completion without Retry, proves one task.wrapup call fulfills {wrapup: succeeded, response: not-required}, and proves zero capture/summary-response calls.", + "UX-010:missing-having-trouble-generating-summary-exact-copy-binding": "Render D3's exact UX-010 production title Having trouble generating summary and exact connection/retry subcopy for generic-error. Keep unauthorized/initialization hidden and valid-unrenderable content on the distinct unavailable copy.", + "UX-002/generating-description-underscore-artifact": "Render D3's corrected aiSummary.generatingDescription value Just a sec—the details are coming together. in the post-call generating state. Focused component tests require U+2014, reject U+005F, and leave the sealed-source punctuation mask to D8.", + "REQ-008/pending-submission-strings-absent-from-ux-inventory": "Project pending and not-confirmed only through D3's stable message IDs and structural-only exact descriptions. Focused post-call tests require both Like/Dislike controls to resolve aria-describedby to the one visible enabled/read-only description, reject live semantics, and prove success removes the node and both references.", + "UX-002..UX-010/same-control-label-differs-by-visual-state": "Use the same canonical Search topic and Complete Wrap-Up constants for every UX-002 through UX-010 state. Treat raw marker/case divergence as a source-capture artifact; feedback hover/selection and copy hover/confirmation cannot mutate visible text, accessible name, placeholder, or tooltip.", + "UX-010/connection-specific-subcopy-applied-to-all-error-categories": "Consume D3's common exact title/subcopy for disabled, offline, unavailable, empty, timeout, and generic visible failures; keep hidden and unsupported branches separate. Focused fixtures prove the broad description remains non-diagnostic recovery guidance rather than a transport-cause assertion." + }, + "description": "Operator-approved layout clarification (2026-09-25, design_spec.md): supersede earlier single-outer-scrollbar/no-nested-scrollbar clauses and scroll-owner test expectations in this task. Keep the 80vh panel heading, summary actions and completion action visible; destination/reason lists and shared summary content may scroll independently without individually scrolling keyed editors or horizontal overflow. Preserve source-aligned panel capture, declared tooltip overhang, all text, keyboard access, interaction state and focus. All other task obligations, dependencies and acceptance commands remain unchanged. REQ-002, REQ-005, REQ-006, REQ-007, REQ-008, REQ-009, REQ-010, UX-002, UX-003, UX-004, UX-005, UX-006, UX-007, UX-008, UX-009, UX-010, JNY-002, AC-05, AC-06, AC-07, AC-08, AC-09, AC-10, AC-11, AC-12, AC-13, AC-14, and AC-16 / Component: Post-Call Wrap-Up Integration - add ai-summary-post-call.ts as the package-internal focused capture, wrap-up-first, and one-response orchestration consumed later by D6b's useAISummaryActions(ownerKey) hook; do not export it from a package entry or edit the public hook/prop declarations in D6. Add a single-consumer wrap-up-summary component with closed co-located types and feature-local styles. Import AISummaryFeedback from @webex/cc-store and type selectable post-call feedback as Exclude rather than redeclaring the thumbs literals. Own the post-call composition and state transitions for UX-002 through UX-010 while D3 owns their shared rendering/actions; before implementation, resolve each named state to its exact D0a-sealed scene-graph JSON, text JSON, and PNG triple from requirement.md, treat all nine triples as D6 implementation inputs, and pass only AISummary mode post-call at the presentation boundary. The named UX-002 through UX-010 sealed scene/text/PNG evidence requires the eligible Momentum Input/RadioGroup/Radio reason controls, en-US substring filtering, selected reason state, screenshot header/help/divider hierarchy, expanded generating/plain/error list, collapsed structured trigger, and read-only Outcome resolution. Give the Input exact aria-label Search wrap-up reasons from stable message ID aiSummary.postCall.reasonSearchLabel while retaining the Search topic placeholder; resolve the existing Choose a reason to wrap up heading from aiSummary.postCall.chooseReason, assign it an instance-local useId DOM id, and pass that id to RadioGroup aria-labelledby. A zero-match query renders exact ordinary visible text No wrap-up reasons match your search. from stable message ID aiSummary.postCall.noReasonMatches, with no status/alert role, aria-live, or announcement. Apply the uniform unresolved-outer-percent-variable rule once before component input: every state uses resolved Search topic and requirement-controlled Complete Wrap-Up constants with no visible or accessible delimiter, and the primary visible label, accessible name, and tooltip agree. The generating state renders D3's corrected Just a sec—the details are coming together. value with U+2014 and no U+005F. Copy hover/confirmation changes only the copy treatment and never the primary visible label or accessible name; preserve the existing Select and Submit & Wrap up behavior when ineligible. For capability arrival or revocation while mounted, carry a valid same-interaction reason/id/query across branches, map focus by semantic key or successor/fallback, add no announcement, and reset on interaction switch; test both initial layouts and both transition directions. On popover close, reset query, reason/id, copy state, and reason commit bookkeeping but retain the store-owned accepted/edited post-call content and revision so reopen cannot discard work. Branch only on summarySurface: omitted removes the summary region, unavailable shows The summary is not available, and generic-error shows Having trouble generating summary when no accepted draft is retained. The initial no-draft request may disable completion only while pending, bounded by the existing 15-second store timeout; terminal error or unavailable sets an owner-and-reason-revision completion escape that Retry cannot revoke. An accepted/edited/copied/rated post-call draft remains content with its exact revision, counters, selected feedback, and Pending submission during pending, blocked, stale, failed, or unsupported reason regeneration. A reason commit installs a new monotonic generation that makes every older in-flight settlement stale before timestamp ordering. Only a ready response in the current generation replaces content; it preserves edited/copied totals, increments viewed once, resets feedback/status, and clears copied paint through content-change cleanup. Capture first freezes the visible draft/counters/feedback and makes every later settlement stale. Bind every post-call request, draft, capture token, and frozen response to the current canonical interaction, authenticated agent, and ownership generation. After A-to-B-to-C only C owns wrap-up state, C starts zero post-call counters, and B's late settlement is stale. For a conference, preserve the SDK-generated final content, including its conference segment, under the common interaction ID without widget-side transcript assembly or truncation. Momentum RadioGroup change, including each arrow-key move, updates the controlled selected reason and monotonic selection revision through handleWrapupChange but makes no request. Pointer activation, Enter/Space, or focus leaving the whole group invokes one commitReasonSelection seam that calls requestPostCallSummary('reason-commit'); dedupe by current owner plus selection revision, guard change/activation/blur while requestPending with focus-retaining aria-disabled, and prove any arrow traversal issues exactly one request for the final committed reason. The only Retry is the generation-error control and every activation starts and observes the always-fulfilling requestPostCallSummary('retry') result; the post-call view sets current-generation aggregate requestPending before the SDK call, disables Retry/re-entry while nonzero, excludes superseded transports, and collapses concurrent same-generation requests into one generating state until their last settlement/categorization, but Retry cannot revoke an established completion escape. Typed response fields or plain text are editable. Resolution is retained separately: D1's non-empty resolutionPresent fixture is inserted by D3's typed display ordinal without named neighbours, while key-omitted resolutionAbsent plus undefined/empty normalized absence renders no Outcome row, label, placeholder, divider, edit target, clipboard block, response key, or reserved space. Cards are ignored. Both expanded and collapsed layouts use .agent-popover-content as the 80vh shell; lists and summary content scroll independently while headings, summary actions and Complete Wrap-Up remain visible at 320px. AISummary owns the sole Clipboard write and calls the wrapper's synchronous boolean copied recorder only after fulfillment, painting only when the current revision is accepted; the wrapper/store never writes the clipboard. Call setPostCallSummaryFeedback(feedback, expectedRevision) and immediately paint the mutually exclusive local selection plus Pending submission only when its current-owner/revision/eligibility/role/capture/frozen guard returns true; name negative tests for stale revision, advanced owner, ineligible surface, and captured/frozen state with no pending or frozen-payload mutation. When a ready draft exists, capture the exact draft/counters/feedback, wrapUpCode, owner, revision, and current Task inside the store while atomically installing postCallCaptureRevision and locking controls without changing lifecycle; return only an opaque one-time symbol token to the orchestrator, never the content-bearing snapshot or a caller-pairable Task. For terminal error/unavailable with the completion escape, create no token or empty response and allow the same sole wrap-up implementation to return response not-required after success; wrapUpCode is the exact auxCodeId from the second useCallControl.wrapupCall argument, while the first argument wrapUpReason remains the human-readable label used only with the same auxCodeId by task.wrapup and for failed-wrap-up UI restoration. Use the store-projected feedbackStatus as the exact state source: pending resolves through aiSummary.feedback.pendingSubmission before final settlement, not-confirmed resolves through aiSummary.feedback.submissionNotConfirmed in a disabled/read-only completion group after response failure, and the token is undefined immediately after confirmed success; never overload midCallFeedbackPending or infer copy from lifecycle. completeWrapupWithSummary always fulfills WrapupCompletionResult: catch a task.wrapup throw/rejection, release a capture token only when the ready-draft preparation created one so only the matching internal current owner/revision guard is cleared and the private capture is removed, retain the reason and unchanged current-owner draft or error/unavailable surface, and return wrapup failed. After eligible-without-draft success, invalidate late generation settlement and return wrapup succeeded/response not-required without a response send; for the ready-draft branch, after task.wrapup fulfills, irrevocably commit and report the existing wrap-up success bookkeeping before the response send, pass only captureToken to freezeAndSendPostCallSummary so the store resolves its private snapshot/Task and emits snapshot.wrapUpCode unchanged into the D0-admitted AISummaryResponse.wrapUpCode and prove it equals the committed task.wrapup auxCodeId rather than wrapUpReason, freeze the exact composed payload, and call sendPostCallSummaryResponse once on the token-bound captured Task even if visibility changed. Unknown, released, or reused tokens make no SDK call, cannot select another capture/Task, expose no raw data, and resolve through the sanitized failure path. Send success returns wrapup succeeded/submitted and removes feedbackStatus. Send rejection cannot enter wrap-up error handling: it sets only response-failed, retains the frozen response, projects not-confirmed for that owner after wrap-up so the component resolves aiSummary.feedback.submissionNotConfirmed while exposing no editor/resend/Retry, emits the content-free failure transition, performs no resend, and returns wrapup succeeded/response-failed so the selected reason clears without removing the completion description. This path is admitted only after D0 executes D1's postWrapUpSend sequence and proves the packed SDK can fulfill that same-Task call after resolved wrapup. If the Task is non-callable then D0 blocks this task pending a conforming SDK commit; do not send before wrap-up or ship deterministic incompatibility as response-failed. Retain an admitted runtime transport failure until its captured-owner AgentWrappedUp and then discard only that record, including the event-before-rejection race; a different interaction or stale owner generation cannot mark or discard it. Test A-to-B-to-C owner binding and zeroed C counters with B's late settlement rejected; test conference final content retaining the common interaction ID and fixture conference segment without widget-side assembly or truncation; test the one resolved delimiter-free Search topic and Complete Wrap-Up constants across default/hover/confirmed states with copy state unable to change the primary label/name/tooltip, the corrected U+2014 generating description with no underscore, paired `resolutionPresent` relative-ordinal positioning when adjacent sections are absent and `resolutionAbsent` total row/space/copy/response omission without SDK-section reordering, the initial bounded generation gate, `generation error still permits wrap-up completion`, Retry preserving the escape, eligible-without-draft not-required with no send, and an 80vh shell with bounded list/content scroll regions and visible headings/actions for expanded/collapsed 320px layouts, zero-match static non-live copy and both accessible names by role, pointer/Enter/Space request commitment, `arrow-key traversal of reason list issues one request`, activation/blur dedupe, immediate first-commit post-call pending projection, disabled re-entry, concurrent direct requests sharing one generating panel until final settlement, `reason change after edit does not silently discard the draft`, close/reopen draft retention, ready-only replacement, and capture-before-late-settlement preservation, post-call role-targeted edit/view/post-fulfillment copied-recorder isolation with no container-owned Clipboard write, exact pending/not-confirmed/removed feedbackStatus token and message-ID rendering, opaque token capture with unknown/released/reused-token no-send guards, different-interaction wrapped-up isolation, and a distinct reason/id fixture whose frozen response code equals the committed identifier. D2b/D3 provide admitted state/presentation. Compatibility acceptance proves the ineligible legacy/custom-element-backed path retains the existing Select/SUBMIT_WRAP_UP UI, exact task.wrapup arguments, and one-call behavior; proves the D6 orchestration is absent from package entry exports; and proves D6 leaves helper.ts, task.types.ts, CallControl/index.tsx, and package entries untouched. This task edits no shared CallControl hook/type/container/test/spec file and has no dependency on D4 or D5; D6b alone performs the public Promise callback migration, direct-React breaking-change/migration checks, custom-element compatibility check, and shared integration after all three parallel feature tasks finish.", + "files": [ + "packages/contact-center/task/src/ai-summary-post-call.ts", + "packages/contact-center/task/tests/ai-summary-post-call.ts", + "packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/wrap-up-summary.tsx", + "packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/wrap-up-summary.types.ts", + "packages/contact-center/cc-components/src/components/task/CallControl/CallControlCustom/wrap-up-summary.styles.scss", + "packages/contact-center/cc-components/tests/components/task/CallControl/CallControlCustom/wrap-up-summary.tsx" + ], + "dependencies": [ + "D2b-interaction-summary-sdk-binding", + "D3-shared-summary-presentation" + ], + "acceptance": "yarn workspace @webex/cc-task test:unit --runInBand --runTestsByPath tests/ai-summary-post-call.ts && yarn workspace @webex/cc-components test:unit --runInBand --runTestsByPath tests/components/task/CallControl/CallControlCustom/wrap-up-summary.tsx" + }, + { + "id": "D6b-call-control-summary-integration", + "title": "Join the parallel summary features through the shared CallControl seams", + "design_review_fixes": { + "post-call-wrapup:one-consumer-claim-contradicted-by-mid-call-search-radio-layout": "call-control.tsx mounts D5's ConsultTransferPopoverComponent and contains no category, search, filter, results, divider, editor, collapse/reopen, or popover-focus implementation of its own.", + "post-call-wrapup:wrapup-prop-void-or-promise-union-breaks-awaited-submit": "Thread only (wrapUpReason, auxCodeId) => Promise at every wrap-up seam and reject a synchronous or void supplier at compile time. handleWrapupCall keeps the control locked through settlement and defensively catches a nonconforming direct consumer's synchronous throw or rejected Promise, sanitizes it to wrapup failed, and never rethrows.", + "post-call-wrapup:wrapupCall-and-completeWrapupWithSummary-relationship-unspecified": "Implement wrapupCall only as the public adapter that returns private completeWrapupWithSummary for eligible-with-draft, eligible-without-draft, and ineligible branches. Assert one delegation and one task.wrapup invocation; not-required follows a successful ineligible wrap-up or a successful eligible wrap-up after terminal generation failure with no draft.", + "post-call-wrapup:complete-disabled-during-generation-and-error-no-escape": "Thread D6's bounded completion escape without widening WrapupCompletionResult. handleWrapupCall awaits the one wrap-up Promise and clears the reason after eligible-without-draft response not-required just as it does after other successful wrap-up outcomes.", + "REQ-003:midcall-response-before-consult-ordering": "Make every internal destination, Transfer/Transfer Conference, and Merge confirmation call site await the always-fulfilled pre-action response result before creating its telephony Promise. The CallControl integration tests hold the response pending to assert zero telephony calls, then settle sent and failure outcomes and require exactly one subsequent unchanged action." + }, + "description": "REQ-002, REQ-003, REQ-005, REQ-006, REQ-007, REQ-008, REQ-009, REQ-010, JNY-001, JNY-002, AC-03, AC-05, AC-06, AC-07, AC-08, AC-09, AC-10, AC-11, AC-12, AC-13, AC-14, and AC-16 / Component: Shared Call-Control Summary Integration - after the disjoint D4 receiver, D5 initiating, and D6 post-call feature tasks complete, import the D5/D6 orchestration modules into one focused useAISummaryActions(ownerKey) hook in helper.ts. That hook owns the summary view/action adapters and observation of their closed async results; useCallControl composes it exactly once, retains telephony and Task-lifecycle ownership, and threads the returned views/actions through task.types.ts, CallControl/index.tsx, cc-components task.types.ts, and call-control.tsx. Summary state, SDK composition, error normalization, and mid/post-call sequencing must remain in the Store and focused feature modules rather than being reimplemented in useCallControl. Use requestMidCallSummary as the sole initiating callback identifier across useAISummaryActions, useCallControl, ControlProps, CallControlComponentProps, and ConsultTransferPopoverComponentProps. In both task/src/task.types.ts and cc-components/src/components/task/task.types.ts, import AISummaryActionType and AISummaryFeedback from @webex/cc-store, declare no local literal aliases, and expose setMidCallSummaryFeedback(feedback: Exclude, actionType: AISummaryActionType, revision: number) plus setPostCallSummaryFeedback(feedback: Exclude, expectedRevision: number). Mount the consult-transfer and wrap-up feature views without duplicating presentation or store state; preserve existing SDK argument shapes, adopt D5's one unconditional voice destination renderer with a conditional summary subtree, and preserve the post-call ineligible legacy markup/behavior. In call-control.tsx, eligible standalone Transfer or Transfer Conference and Merge controls become stable-ref PopoverNext triggers for D5's existing-party-action branch: transfer open starts one TRANSFER preparation, Merge reuses the current/in-flight CONSULT preparation without a request, close restores the exact trigger and calls no telephony method, and omitted-summary controls retain their legacy immediate callbacks. Change handleTargetSelect and the one handleExistingPartyConfirm adapter to await the always-fulfilling outcome-discriminated sent/failed/blocked/stale pre-action Promise, then start the captured existing telephony callback once through its existing guard and observe that later Promise. Every internal destination, Transfer/Transfer Conference, and Merge call site observes the outer adapter Promise; no telephony Promise is created while response submission is pending, and every non-sent outcome still permits exactly one action after settlement. The component observes/awaits each confirmation adapter Promise so no handler rejection floats; destination selection uses handleTargetSelect, Transfer or Transfer Conference confirmation reaches task.transferConference through the existing consultTransfer callback, and Merge confirmation reaches task.consultConference through the existing consultConference callback. On fulfilled direct/consult transfer or conference establishment, report the D5-defined ownership boundary with the captured pre-action owner key to D2b's idempotent compare-and-swap; on rejection report none, so late prior-generation sends/results cannot mutate the successor and duplicate Task-event observation cannot advance twice. Add and export the closed WrapupCompletionResult union, and change exported wrapupCall surfaces from synchronous void to (wrapUpReason: string, auxCodeId: string) => Promise; the Promise never rejects, handleWrapupCall retains reason/id only for wrapup failed, clears them for every wrapup succeeded response outcome, and the ineligible path and the eligible terminal-error/unavailable path with no draft return not-required without a response send. D6b is the sole public-seam compatibility owner: this is additive and behavior-preserving for the custom-element host, which does not supply wrapupCall, but breaking for direct React consumers of the published @webex/cc-task and @webex/cc-components props; update both module specs with the return/await/closed-result migration, prove the existing widget-cc-call-control wrap-up path remains unchanged, and require semantic-release breaking-change metadata for a SemVer-major release of both packages. For mid-call capability enable/revoke, keep category/search/result nodes and shared values mounted while adding/removing only the summary subtree and apply successor/fallback only when a removed summary control owned focus; retain the post-call branch transition's semantic-value/focus mapping, while interaction changes reset state. Thread the discriminated views and the single AISummary presentation mode; adapt the component's one-argument feedback gesture by closing over and forwarding the exact rendered actionType/revision, and keep copied notifications synchronous/revision-only so no CallControl layer invokes the Clipboard API. Do this without widening actionType or conflating mid-call pending, aggregate post-call requestPending, midCallFeedbackPending, postCallFeedbackPending, exact post-call feedbackStatus, or capture flags, and branch only on summarySurface: omitted removes each summary region, unavailable alone shows The summary is not available, and generic-error alone shows Having trouble generating summary. Own the shared CallControl utility/style/unit files and update the task and cc-components module specifications once, after D4's cc-components spec edit. Add compile-time/runtime coverage for one useAISummaryActions composition with no duplicate summary logic in useCallControl, canonical requestMidCallSummary threading across every named seam, immediate open/effect fulfillment/rejection observation, pending and settled same-owner/action reopen without another transport call, a new preparation requesting exactly once, shared store action/feedback type imports with no local aliases, feedback actionType/revision forwarding plus stale no-send/no-paint across owner/content/promotion changes, component-only Clipboard ownership with revision-only recorder notifications, owning-request mid-call actionType and consult-to-transfer promotion, distinct midCall requestPending/post-call aggregate requestPending/midCallFeedbackPending/postCallFeedbackPending/feedbackStatus/capture-guard meanings, including pending before completion, not-confirmed after response failure, and removal after submission, revision-guarded boolean post-call feedback, exact feedback-update SDK arguments versus the independent once-per-action pre-action call, all surface branches, strict pre-action-settlement-before-telephony ordering, a deferred response proving zero destination/Transfer/Transfer Conference/Merge telephony calls while pending, sent/failed/blocked/stale each followed by exactly one unchanged action, all store Promise outcomes fulfilling and sequential outer/later Promise observation, successful-action boundary reporting, rejected-action no-advance, duplicate-boundary idempotence, stale prior-generation suppression, the exact result-bearing wrap-up Promise signature, non-rejection, reason clearing without dropping the failed-response completion description, opaque capture-token orchestration with no caller-owned snapshot/Task pair, the single mid-call destination layout with conditional summary mount/removal, focus-retaining pending aria-disable and named popover-root/trigger fallbacks, radio/pill category-filter parity, eligible no-destination Transfer/Transfer Conference and Merge open/close/confirm surfaces with response pending at zero telephony calls and exactly one correctly ordered call after settlement and omitted-summary immediate-action compatibility, plus both post-call layouts/transitions and the generation-error completion escape, and run both focused feature suites plus the shared helper/container/component suites. D6b is the first writer in the shared packages/contact-center/task/src/task.types.ts and packages/contact-center/task/src/CallControl/index.tsx handoff, including their shared test/spec; dependent D7 is the serial second writer for the host callback, so those files are never edited in parallel. This join adds no direct SDK access, second summary lifecycle authority, fallback package, or third summary implementation; useAISummaryActions is the one focused hook added at the serial join, and no D4/D5/D6 file overlaps remain.", + "files": [ + "packages/contact-center/task/src/helper.ts", + "packages/contact-center/task/src/task.types.ts", + "packages/contact-center/task/src/CallControl/index.tsx", + "packages/contact-center/task/tests/helper.ts", + "packages/contact-center/task/tests/CallControl/index.tsx", + "packages/contact-center/task/ai-docs/task-spec.md", + "packages/contact-center/cc-components/src/components/task/task.types.ts", + "packages/contact-center/cc-components/src/components/task/CallControl/call-control.tsx", + "packages/contact-center/cc-components/src/components/task/CallControl/call-control.utils.ts", + "packages/contact-center/cc-components/src/components/task/CallControl/call-control.styles.scss", + "packages/contact-center/cc-components/tests/components/task/CallControl/call-control.tsx", + "packages/contact-center/cc-components/tests/components/task/CallControl/call-control.utils.tsx", + "packages/contact-center/cc-components/ai-docs/cc-components-spec.md" + ], + "dependencies": [ + "D4-receiving-ai-assistant", + "D5-mid-call-call-control", + "D6-post-call-wrap-up" + ], + "acceptance": "yarn workspace @webex/cc-task test:unit --runInBand --runTestsByPath tests/ai-summary-mid-call.ts tests/ai-summary-post-call.ts tests/helper.ts tests/CallControl/index.tsx && yarn workspace @webex/cc-components test:unit --runInBand --runTestsByPath tests/components/task/CallControl/call-control.tsx tests/components/task/CallControl/call-control.utils.tsx tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx tests/components/task/CallControl/CallControlCustom/wrap-up-summary.tsx" + }, + { + "id": "D7-host-status-callback", + "title": "Expose the optional content-free AI summary status callback", + "design_review_fixes": { + "D7-host-status-callback/acceptance-missing-workspace-build": "After the focused task test, build @webex/cc-task and then build its @webex/cc-widgets consumer before running the focused cc-widgets test, so wc.ts compiles against fresh task declarations.", + "REQ-007/type-block-exactly-vs-retained-declaration": "Retain the existing CallControlProps mapped utility declaration and add only AISummaryStatusDetail plus the new callback intersection member; the expanded type block in the design is illustrative.", + "AC-08/status-transition-single-slot-delivery-loss": "Replace the latest-wins slot/mount baseline with the store-owned ordered immutable pending queue and exact-head acknowledgement. The observer-rendered snapshot effect drains every batched entry and retains a same-session response-failed transition recorded while CallControl is unmounted, while acknowledged entries cannot replay on remount.", + "REQ-007/aisummarystatusdetail-not-exported-from-cc-widgets": "Add a type-only AISummaryStatusDetail re-export from cc-widgets/src/index.ts, document it in the cc-widgets module spec, and type-check the public-entry handler import in tests/wc.ai-summary.ts without any.", + "REQ-007/mobx-transition-read-outside-render": "Read getPendingAISummaryStatusTransitions() during the observer render and pass that immutable snapshot to the effect dependency array, so a store append alone schedules callback delivery without unrelated prop churn.", + "REQ-007/task-index-callcontrolprops-duplicate-export": "Record that the inspected task/src/index.ts has only value exports and D6b does not edit it; D7 adds one combined type-only export for AISummaryStatusDetail and CallControlProps, with no duplicate or replacement line.", + "AC-09/transition-mapping-nondeterministic-may": "Assert the D2b closed mapping: authorization/init/disabled omission and interaction/owner boundaries emit nothing by themselves; same-agent carried content does not repeat available, cross-agent reset does not synthesize unavailable, stale predecessor settlement emits nothing, and only a freshness-accepted successor settlement emits exactly its mapped state.", + "REQ-007/callback-throw-dedup-ref-ordering": "Inside the CallControl effect, exact-head acknowledge before entering a local try/catch around the host invocation. The catch is not ErrorBoundary handling; a throwing sequence is offered at most once across renders/remount and later entries continue draining. Tests assert ordering, one invocation, continued mount, and continued drain.", + "REQ-007/r2wc-function-prop-attribute-transform-unverified": "The reviewed pins are @r2wc/react-to-web-component 2.0.3 and @r2wc/core 1.1.0. Because the core function transform observes a dashed attribute, resolves its value through window/global, and reflects Function.name, keep onAISummaryStatusChange out of the r2wc props map and expose it through a typed WeakMap/useSyncExternalStore property-only adapter and subclass. Assert both callback attribute spellings are unobserved/inert and cannot resolve an arbitrary global.", + "test-strategy-item8:D7-tests-lack-named-files-and-type-test-mechanism": "Use packages/contact-center/task/tests/CallControl/index.tsx for the focused task runtime route and packages/contact-center/cc-widgets/tests/wc.ai-summary.ts for the focused aggregator runtime plus public-entry type fixture. Include the latter in packages/contact-center/cc-widgets/tsconfig.test.json and run tsc --project tsconfig.test.json --noEmit before Jest. Compile an exact type-equality assertion for the six AISummaryStatusDetail members and @ts-expect-error assignments for both forbidden mid-call states, extra keys, and forbidden values, so widening or narrowing fails the D7 gate." + }, + "description": "REQ-007, REQ-010, AC-08, AC-09, AC-14, and AC-16 / Change: Host Status Callback and Existing Web Component - retain the existing mapped CallControlProps declaration in task.types.ts, add AISummaryStatusDetail plus only onAISummaryStatusChange?: (detail: AISummaryStatusDetail) => void, add the one non-duplicated task type export and the cc-widgets type-only re-export, and document both public surfaces without any runtime export or any type. In observer-wrapped CallControl/index.tsx, read getPendingAISummaryStatusTransitions() during render and include that immutable snapshot plus the optional callback in the effect dependencies. Drain the rendered entries in ascending order; for each exact head, acknowledge first and then invoke the allowlisted detail inside a local try/catch in that effect. The catch is not the repository ErrorBoundary: acknowledgement already makes the throwing sequence non-retriable across render/remount, the widget stays mounted, no log/metric/error payload is added, and the loop continues to later entries. Do not seed a mount cursor or add a MobX reaction. Retain every batched/unmounted entry through ordinary cleanup until exact acknowledgement, acknowledge nothing when the property is absent, discard only on global session cleanup, and keep the Store-lifetime sequence monotonic. Apply D2b's deterministic mapping: authorization denial, capability-disabled omission, initialization failure, interaction switch, and ownership-generation change append no transition by themselves; same-agent carried content does not repeat available, cross-agent reset does not synthesize unavailable, and stale predecessor settlement appends nothing. Only a later freshness-accepted successor settlement appends exactly the mapped mid-call available/unavailable or post-call available/unavailable/submitted/response-failed state. Receiving status still follows SDK Task event -> store queue -> CallControl observer/effect -> optional host property, never AI Assistant -> task; preserve an unmounted response-failed entry and leave WebAIAssistant/IAIAssistantProps unchanged. The reviewed pins are @r2wc/react-to-web-component 2.0.3 and @r2wc/core 1.1.0; because its function transform observes a dashed attribute, resolves window/global names, and reflects Function.name, do not register the new property in r2wc props. In wc.ts keep the legacy WebCallControl r2wc map unchanged, pass a typed React adapter that reads a module-private WeakMap with useSyncExternalStore, and subclass the generated constructor with a typed onAISummaryStatusChange getter/setter that accepts only a function or undefined, updates subscribers, and never calls setAttribute or adds a DOM event. Unmount removes the adapter subscription; WeakMap ownership avoids detached-element retention and reconnect preserves the assigned property. The final constructor observes neither on-aisummary-status-change nor onaisummarystatuschange, and either attribute is inert with no global lookup. Tests instantiate all six valid details, reject the two invalid mid-call terminal states, cover observable-only delivery, batched order, unmounted delivery, zero-emission authorization/init and owner/interaction boundaries, exact first successor emission, acknowledgement-before-local-try/catch invocation, one call for a persistently throwing sequence, continued mount/drain, replay suppression, cleanup/omission behavior, forbidden-key absence, JavaScript-property set/replace/remove/reconnect, no reflected/observed attribute, arbitrary-global non-resolution for both attribute spellings, and the unchanged AI Assistant contract. In cc-widgets/tests/wc.ai-summary.ts import AISummaryStatusDetail only through the aggregator entry and assign a typed handler; add the no-emit test project and make test:unit type-check before Jest. D6b precedes this surface, and D7 remains the serial second writer of task.types.ts, CallControl/index.tsx, and their shared test/spec.", + "files": [ + "packages/contact-center/task/src/task.types.ts", + "packages/contact-center/task/src/index.ts", + "packages/contact-center/task/src/CallControl/index.tsx", + "packages/contact-center/task/tests/CallControl/index.tsx", + "packages/contact-center/task/ai-docs/task-spec.md", + "packages/contact-center/cc-widgets/src/index.ts", + "packages/contact-center/cc-widgets/src/wc.ts", + "packages/contact-center/cc-widgets/tests/wc.ai-summary.ts", + "packages/contact-center/cc-widgets/package.json", + "packages/contact-center/cc-widgets/tsconfig.test.json", + "packages/contact-center/cc-widgets/ai-docs/cc-widgets-spec.md" + ], + "dependencies": [ + "D6b-call-control-summary-integration" + ], + "acceptance": "yarn workspace @webex/cc-task test:unit --runInBand --runTestsByPath tests/CallControl/index.tsx && yarn workspace @webex/cc-task build && yarn workspace @webex/cc-widgets build && yarn workspace @webex/cc-widgets test:unit --runInBand --runTestsByPath tests/wc.ai-summary.ts" + }, + { + "id": "D8-browser-ux-validation", + "title": "Implement the browser-validation harness and collect integrated UX evidence", + "design_review_fixes": { + "REQ-009/containing-panel-focus-target-not-named": "Use the ref-scoped exact fallback targets from the design: consultTransferPanelRef.current for the open mid-call .agent-popover-content, panelRef.current for the expanded [data-testid=ai-assistant:panel][role=dialog], and wrapUpPanelRef.current for the open post-call .agent-popover-content; each has tabIndex -1. Run one named scenario per surface that focuses and removes the last focusable summary control and asserts the exact ref, not body or another popover, becomes document.activeElement.", + "D8/ux-source-paths-unresolved-across-worktree-and-parent": "Require the already staged/sealed target worktree as the one UX resolution root. verify-ux rehashes it before prepare; every common-checkout MatrixBuilder prepare/plan/export/compare/validate child receives the same canonical absolute --ux-input-root argv and must record matching post-stage hashes before any render output.", + "REQ-011/structural-only-state-list-never-enumerated": "Treat the design's complete structural-only table as a closed typed denominator expanded by mode and action. Emit one classification/DOM/accessibility receipt for every exact tuple, reject missing/extra/duplicate tuples, and require each row's named browser scenario rather than accepting an open-ended structural category.", + "render-compare-plan/0.5pct-gate-vs-semantic-token-substitution": "Replace the whole-root RGB gate with structural plus predeclared region masks. Mask Momentum-token-owned fills and strokes from literal RGB, compare token-colored foreground by color-independent coverage, keep exact DOM boxes/copy/token identity/contrast/forced-colors blocking, and apply <=0.5 percent only to each foreground coverage result and a non-empty literal RGB mask; reject a legacy aggregate pixel percentage.", + "post-call-wrapup:empty-search-result-renders-nothing": "Register the zero-match reason query as a structural-only state and assert visible No wrap-up reasons match your search. copy with no status, alert, aria-live, or fabricated pixel baseline.", + "post-call-wrapup:radio-selection-change-fires-sdk-request-per-keystroke": "In the real browser, traverse multiple reason radios with arrow keys, commit the final radio with Enter/Space or group focus exit, and assert exactly one post-call request.", + "post-call-wrapup:reason-reselection-silently-discards-edited-summary": "Exercise reason change after an accepted edit and prove the exact draft remains through pending/non-accepted replacement work and close/reopen until a newer ready response is accepted.", + "post-call-wrapup:reason-search-and-radiogroup-lack-specified-accessible-names": "Assert the Input has exact accessible name Search wrap-up reasons after typing and the RadioGroup is named by the visible Choose a reason to wrap up heading; require the axe subtree audit to remain violation-free.", + "post-call-wrapup:resolution-outcome-row-hardcoded-between-named-sections": "Exercise D1's paired post-call fixtures. resolutionPresent omits both formerly named adjacent sections and proves Outcome occupies the deterministic typed ordinal without reordering remaining SDK sections; resolutionAbsent proves no Outcome row/text/copy block/response key/reserved gap.", + "UX-003/outcome-resolution-row-absence-and-ordering": "Run paired browser scenarios for a non-empty top-level resolution and a key-omitted payload. Assert stable relative-ordinal Outcome insertion for the present case and total row/space/copy/response omission for the absent case while remaining sections retain SDK order.", + "post-call-wrapup:complete-disabled-during-generation-and-error-no-escape": "Keep all ten UX source triples sealed, but classify S10 as the sole source-bound structural requirement-availability override. Prove terminal generation error enables the stable Complete Wrap-Up action, completion returns not-required without a response send, and Retry cannot revoke the escape.", + "post-call-wrapup:combined-reason-list-plus-summary-overflow-unspecified": "At 320px and desktop widths, inspect expanded and collapsed post-call layouts and require an 80vh .agent-popover-content shell with independent reason/content scrolling, no horizontal overflow, and visible keyboard-reachable headings, summary actions and Complete Wrap-Up despite long content.", + "post-call-wrapup:copy-visual-state-mutates-primary-submit-label": "Across copy idle, hover, confirmed, and reset states, assert the copy affordance alone changes while the primary visible label and accessible name remain exactly Complete Wrap-Up.", + "post-call-wrapup:percent-delimited-figma-placeholders-treated-as-literal-copy": "Across every screenshot-backed and structural-only state, enumerate visible and accessibility strings and fail on unresolved percent markers; Search topic and requirement-controlled Complete Wrap-Up must come from their single delimiter-free message IDs. Preserve raw PNGs but exclude only registry-fixed affected variable-slot regions from the pixel denominator.", + "UX-002..UX-010/addressed-rows-trace-only-to-validation-tasks": "Treat D8 as implementation-plus-evidence: its writes are limited to the validation dependency/lock, sample-app-only injection bridge, Playwright harness/configuration, and typed visual registry. It executes the complete @webex/cc-task and @webex/cc-ai-assistant unit suites plus focused cc-components and browser gates, and returns product mismatches to D3/D6 or the other named implementation owner instead of editing production sources, component unit tests, or module specifications.", + "D8-browser-ux-validation/product-source-edits-outside-acceptance": "D8 owns validation-harness and evidence implementation only, never product-source refinement. Before recording browser evidence, acceptance reruns the complete @webex/cc-task and @webex/cc-ai-assistant unit suites plus the focused cc-components coverage; any product correction returns to its D2b-D7 owner and all D8 gates rerun afterward.", + "implementation-dag-summary/total-order-and-redundant-edges": "Keep only immediate predecessors in dependency arrays. D0a and D1 start independently; D2 follows D1; D0 and D3 can run concurrently after D0a and D2; D2b follows D0; D4, D5, and D6 fan out after D2b and D3, then D6b joins them. Shared-file handoffs remain ordered while the three disjoint integrations remain parallel.", + "no-product-behavior-change-vs-production-css-fixes": "D8 changes no product logic, public props, SDK calls, or product source. A visual mismatch returns to D3/D6 or the other D2-D7 implementation owner, which may refine only its already-owned presentational files under focused acceptance before D8 reruns; logic, contract, and SDK-call changes remain with their named implementation task.", + "D8-dependency-added-without-install-step": "Add the exact root dependency from the repository root with yarn add -D -E axe-core@4.10.2 so Yarn generates package.json, yarn.lock, and active linker state. D8 acceptance begins with yarn install --immutable before axe resolution or Playwright, and D8 declares registry access or a complete local Yarn cache as a prerequisite.", + "axe-injection-and-node-modules-vs-pnp": "Resolve axe-core/axe.min.js with require.resolve through the active Yarn resolver, read the package-owned bytes through node:fs, and inject those bytes; never construct a node_modules path. The mechanism works with the configured node-modules linker and a Yarn-PnP-patched process.", + "playwright-suite-discovered-by-existing-projects": "Add exact testIgnore ['**/suites/ai-summary-tests.spec.ts', '**/tests/ai-summary-test.spec.ts'] to the OAuth setup project and every USER_SETS-derived pre-existing project in playwright.config.ts; leave the conditional AI Summary Deterministic project unignored. Acceptance lists both paths across all flagged projects, rejects every test entry not owned by AI Summary Deterministic, and requires each spec exactly once, while the unflagged all-project listing must find neither spec.", + "AISummaryBrowserScenario-undefined": "Define AISummaryBrowserScenario as a readonly non-empty tuple of discriminated group/member selections derived from keyof typeof aiSummaryFixtures and the selected group's exact member keys. Compile-time scenario constants cover timeout, stale timestamp, ownership change, and response failure and reject unknown or cross-group members.", + "axe-run-root-locator-boundary": "Replace the Locator parameter with a serializable AISummaryAxeInclude containing exactly one stable selector. In page context require that selector to be valid and resolve to exactly one connected Element before axe.run({include: [selector]}); invalid, absent, detached, or ambiguous roots fail without widening to document.", + "screenshotId-untyped-vs-sealed-manifest": "Define UXScreenshotId as S01-S06, S07-UX, and S08-S10; define the exhaustive readonly UXSourceId-to-UXScreenshotId map and a union of exact source/screenshot tuples for recordAISummaryVisual. Reject mismatches before output, and normalize only the verified UX-007 / S07-UX pair to canonical S07 / dl evidence.", + "sdk-mock-injection-seam-undefined": "Add the typed, version-1 window.__WEBEX_CC_AI_SUMMARY_E2E__ bridge in widgets-samples/cc/samples-cc-react-app/src/aiSummaryE2E.ts and consume it from App.tsx only for exact ai-summary-e2e=1 navigation. installAISummarySDKMock installs the bridge with page.addInitScript before navigation; sample-app boot throws when it is absent, malformed, or wrong-version, then the Init Widgets action passes bridge.webex through the existing store.init({webex}) host seam with no bundled Webex.init or network fallback.", + "mbflow-cli-invoked-from-test-source-nonportable": "Require an absolute executable MB_FLOW_BIN and invoke it only with execFile/spawn argv. Derive the common-checkout output root from the target worktree's absolute git rev-parse --git-common-dir result, require the plan render root to equal /.matrix/results/prog-mini-js/ux-visual/implementation, and pass the canonical target worktree as the required absolute --ux-input-root argv to prepare, plan, export, compare, and validate. Reject a missing/different/root-relative handoff and fail every helper child on spawn error, signal, 60-second timeout, non-zero exit, or missing/invalid output while attaching bounded sanitized stderr.", + "structural-receipts-omit-midcall-extrapolated-states": "Add separate no-sealed-screenshot structural entries and receipts for mid-call generating->UX-002/S02, editing->UX-003/S03, like hover->UX-004/S04, like selected->UX-005/S05, dislike hover->UX-006/S06, dislike selected->UX-007/S07-UX with canonical S07/dl, copy hover->UX-008/S08, copy confirmed->UX-009/S09, and generation error hierarchy->UX-010/S10. Record the applicable initiator/receiver mode tuple and exact extrapolatedFrom binding for each, and reject a missing or changed binding.", + "UX-004+UX-006+UX-008:hover-tooltip-states-visual-determinism-unaddressed": "Before any hover visual or structural receipt, driveAISummaryState must install the sealed geometry, reset scroll, park then move the real pointer to the registry-fixed trigger coordinate, keep the pointer stationary, require the source-declared top placement with no flip/shift, disable the Momentum portal entry animation, freeze trigger/portal transitions, and observe identical anchor/tooltip/arrow rectangles for two requestAnimationFrame ticks. recordAISummaryVisual must use a page clip that includes the portal and record pointer, anchor, tooltip, placement, portal identity, animationsDisabled, transitionsFrozen, and stability fields before scoring.", + "UX-EVIDENCE:structural-only-state-enumeration-omits-implemented-states": "Implement the design's complete structural-only table as an exact typed (stateId, mode, action) denominator: receiver ready/unrenderable; hidden authorization/initialization; omitted Consult/Transfer; Transfer heading; nine mid-call extrapolations; initiating/post-call unavailable; separately actioned pending Consult/Transfer; S10 override; zero-match; Outcome absent; feedback pending/not-confirmed; response submitted/failed; legacy ineligible; interaction/hold; and transfer/conference ownership. Every tuple emits classification, DOM, accessibility, and its named scenario evidence; deleting, adding, duplicating, or merging a tuple fails coverage.", + "UX-EVIDENCE:production-semantics-omit-focus-move-on-disappearing-control": "Exercise successor-present removal and three separate last-summary-control removals. Require consultTransferPanelRef.current, panelRef.current, and wrapUpPanelRef.current respectively as the connected tabIndex -1 fallback for mid-call, receiver, and post-call; reject body, a class-only global match, a disconnected node, or the other popover instance. Connected controls and ordinary content updates retain focus.", + "UX-006,UX-008/unreconciled-capture-scale-outliers": "Treat every sealed PNG as scale 2 and enforce the exact root-relative crop tuple per source, including S06 [-2,-1,433,621] and S08 [-39,-9,441,632] without changing component width. Preserve raw target.png, use the declared 2-to-1 source normalization, and record base, source-correction, semantic-token, foreground-coverage, and literal mask identities. Reject auto-trim, fit, translation, diff-derived expansion, a global pixel score used as sole acceptance, foreground coverage above 0.5 percent, or a non-empty literal score above 0.5 percent.", + "validate-ux-visual-comparison-invoked-twice": "The Playwright suite produces per-tuple artifacts only and never invokes validate-ux-visual-comparison. The surrounding acceptance command invokes it exactly once after a successful Playwright process from the common-checkout root with the same required --ux-input-root, and that invocation owns the authoritative visual gate receipt for the attempt.", + "visual-determinism-fonts-blocked": "Pin screenshot evidence deviceScaleFactor to the descriptor-declared source scale 2 beside each sealed CSS viewport; ordinary interaction-only journeys use scale 1. Serve every required text/icon font only from the existing sample-app origin through a closed family/style/weight/glyph/path/SHA-256 manifest backed by repository- or installed-package-owned webpack assets; reject all non-local HTTP(S), unmanifested or hash-drifted local fonts, decode/load failure, and system fallback. Before every screenshot, a bounded helper awaits document.fonts.ready, checks every required face, freezes only registry-declared continuous animations at their fixed phase, waits for all other summary-subtree animations plus two requestAnimationFrame ticks to settle, and records the viewport, device scale, browser revision, font manifest/response hashes, fontsReady, and animationsSettled for D9.", + "visual-loop-exhaustion-disposition-unspecified": "Give each complete visual attempt a fresh identity and permit at most five. Attempts one through four may retain explicitly failing diagnostics but write no visual-gate receipt. Any numeric or non-numeric failure on attempt five exits nonzero as a visual-gate failure, writes no visual-gate receipt of any status, never marks a failed comparison complete, and cannot reclassify a screenshot-backed state as structural-only; the validator writes atomically only after every current-attempt state passes, and D9 rejects missing, stale, failed, exhausted, or downgraded evidence.", + "UX-001:eligible-and-legacy-consult-popover-layout-duplication": "Exercise one voice header/radio/search/results DOM in summary-omitted and summary-present states. After query entry and category/destination selection, deliver late enablement/revocation and an ownership-generation update without closing. Assert the open-instance destinationLayout latch plus identical destination node identity/order, values, filtering, selection, scroll owner, and focus; only appended summary descendants may change, and close/reopen derives a fresh latch.", + "UX-001/eligibility-conditional-popover-reordering-unlatched": "Drive matching capability arrival while the agent is typing in an initially ineligible open popover, then revocation and ownership-generation change. Require the layout branch captured at open to remain latched, preserve query/category/destination/focus and node identity/order, and prohibit summary events from remounting the destination picker. Verify latch release on close/reopen.", + "UX-001:transfer-variant-copy-invented-without-structural-only-receipt": "Add mid-call:transfer-summary-panel as a no-sealed-screenshot structural registry entry and receipt with aiSummary.midCall.transferHeading, the final exact product-confirmed copy, non-empty confirmation reference, justification action-parity label for the actual TRANSFER preparation in the shared panel, and no extrapolatedFrom or source-copy claim. Missing confirmation fails D8.", + "UX-002..UX-010:post-call-popover-width-400-440-442-unreconciled": "Treat S02/S10 440x620, S03 442x620, and S04-S09 400x620 as exact external host fixture geometries for the same responsive component. Require zero CSS-pixel viewport/root drift, exact target bytes/dimensions, and structural mapped-box checks; reject state-selected widths. Tokenized color regions use token/contrast/coverage gates, and <=0.5 percent applies only to foreground coverage and a non-empty literal RGB mask.", + "UX-002:wrap-up-reason-change-after-generation-transition-unspecified": "Drive edit, fulfilled copy, and immediate local feedback before a reason change; inject an older-generation later/higher-timestamp response and a current-generation response. Require draft/revision/counters/selection/Pending submission retention until current acceptance, no mutation from the superseded response, then content replacement with edited/copied totals preserved, viewed incremented once, feedback cleared, and Complete Wrap-Up unchanged. Also prove same-generation Retry retains timestamp ordering and capture makes all later settlements stale.", + "UX-004+UX-006+UX-008+UX-009:literal-percent-delimited-figma-placeholder-shipped-as-copy": "Inspect every screenshot-backed and structural state and require exact delimiter-free Search by name, queue, entry point or phone number, Search topic, and Complete Wrap-Up values in rendered text, placeholders, accessible names, and tooltips. Fail on any unresolved percent marker; preserve sealed marker pixels only in immutable targets and exclude the closed registry-declared variable-slot regions from scoring.", + "UX-004..UX-009/percent-delimited-placeholder-strings-as-production-copy": "Classify every matched outer-percent token uniformly as an unresolved Figma variable. Keep immutable target bytes, register fixed sealed-node variable-slot comparison masks before rendering, reject diff-derived mask growth, and separately require delimiter-free Search topic and Complete Wrap-Up DOM/accessibility/tooltip copy with no product marker constants.", + "UX-005+UX-007:post-call-selected-state-gated-on-success-contradicts-local-pending": "Prove post-call selected paint occurs immediately after guarded local acceptance and shows the structural-only Pending submission description without an SDK acknowledgement; both enabled feedback controls reference it directly. Prove the mid-call extrapolated selected states wait for SDK fulfillment. After wrap-up, success removes the description and both IDREFs; response failure preserves the read-only selection and associates the structural-only Submission not confirmed description with both controls. Neither state is live.", + "UX-004+UX-006-vs-UX-008+UX-009:complete-wrap-up-copy-treated-inconsistently": "Across UX-002 through UX-010 and copy idle/hover/confirmed/reset, require the primary visible label, accessible name, and tooltip to remain exactly Complete Wrap-Up from one message ID.", + "UX-008+UX-009:clipboard-promise-rejection-handling-unspecified": "For initiator, receiver, and post-call modes, hold the real Clipboard Promise pending and reject it. Assert no success paint, copied-recorder invocation, copied-counter increment, raw-error output, or unhandledrejection; fulfillment alone may invoke the recorder and increment exactly once.", + "UX-009:copy-success-treatment-duration-and-revert-trigger-unspecified": "With a deterministic browser clock, assert confirmed at 1,499 ms and idle at 1,500 ms, then independently assert reset on copy-control focus loss, a second gesture, and accepted summary replacement. A fulfilled second copy receives a fresh 1,500 ms interval; a rejected second copy remains reset; unmount leaves no timer.", + "UX-010:complete-wrap-up-permanently-disabled-on-generation-error": "Exercise an unrecovered terminal generation error with no draft, assert Complete Wrap-Up is enabled, activate it without Retry, and require one successful wrap-up with response not-required and zero summary-response calls. Preserve S10 as the source-bound requirement-availability structural override.", + "UX-010:missing-having-trouble-generating-summary-exact-copy-binding": "Read and rehash the sealed S10 text JSON/PNG, then assert the rendered title exactly Having trouble generating summary and subcopy exactly It could be a lost connection or something else. Could you check your connection or try again later? for every non-hidden generation-error fixture.", + "UX-002/generating-description-underscore-artifact": "Record that sealed S02 visibly contains the U+005F source artifact, preserve its raw bytes, and register only the fixed source-underscore/rendered-em-dash separator region as source-punctuation-artifact before rendering. Require exact Just a sec—the details are coming together. DOM/accessibility copy with U+2014 and no U+005F, and reject undeclared or expanded masks.", + "REQ-008/pending-submission-strings-absent-from-ux-inventory": "Register post-call:feedback-pending and post-call:feedback-not-confirmed as separate structural-only states with message IDs aiSummary.feedback.pendingSubmission and aiSummary.feedback.submissionNotConfirmed. Named browser scenarios assert exact copy, both resolvable Like/Dislike aria-describedby IDREFs, pending enabled versus not-confirmed read-only controls, atomic removal on success, and no live semantics.", + "UX-002..UX-010/same-control-label-differs-by-visual-state": "Record Search topic/%Search topic% and Complete wrap-up/%Complete wrap-up% as source-capture artifacts, normalize them before comparison, and run the named canonical-label scenario across UX-002 through UX-010. Require Search topic and Complete Wrap-Up in visible text, placeholder, accessible name, and tooltip regardless of hover, selection, copy, generation, editing, or error state.", + "UX-010/connection-specific-subcopy-applied-to-all-error-categories": "Exercise disabled, offline, unavailable, empty, timeout, and generic fixtures with the same exact UX-010 title/subcopy; reject that presentation for hidden authorization/initialization and unsupported content. Record that or something else is non-diagnostic and or try again later is category-neutral recovery guidance.", + "transfer-variant/heading-copy-without-source-authority": "The named Transfer summary panel exposes the product-confirmed structural heading scenario must record aiSummary.midCall.transferHeading, final exact DOM copy, a non-empty product/content-owner confirmation reference, action-parity rationale, and no extrapolatedFrom or UX-001 authority claim.", + "test-strategy-item9:browser-fake-time-timeout-missing-tool-prerequisite": "Require @playwright/test >=1.45.0 for page.clock; the current lock resolves 1.51.1. Install the browser clock before navigation and before installAISummarySDKMock creates timers, assert the mock-backed request remains pending through 14,999 ms, advance the final 1 ms without wall-clock delay, and verify widget-side timeout handling plus late-settlement suppression. This D8 journey does not prove the real SDK timeout; D0's packed-runtime contract test owns that proof.", + "test-strategy-item9:playwright-tests-spec-not-in-any-acceptance-command": "Make the AI Summary Deterministic project use testDir './playwright' and the exact two-path testMatch for playwright/suites/ai-summary-tests.spec.ts and playwright/tests/ai-summary-test.spec.ts. Give the specs non-overlapping top-level ownership, ignore both in every pre-existing project, list each exactly once, and name both paths in the D8 browser command.", + "test-strategy-item10:S01-S10-enumeration-vs-declared-S07-UX-key": "Enumerate the sealed declared keys exactly as S01-S06, S07-UX, and S08-S10. The captured subset is all ten sources S01-S06, S07-UX, and S08-S10; UX-007 raw IDs match the descriptor and its verified S07 / dl alias stays in sidecars; S10 measured difference is diagnostic and does not override enabled completion." + }, + "assigned_packet_contract": { + "precedence": "This contract supersedes any conflicting older wording in this task description or inherited design-review-fix values.", + "ux_input_resolution": { + "single_root": "canonical target worktree root", + "receipt_root": ".", + "staging_owner": "D0a-ux-evidence-admission stage-ux", + "rehash_order": [ + "stage exact regular-file copies into target worktree", + "seal-ux independently reopens and hashes target copies", + "D8 verify-ux rehashes target copies before prepare", + "prepare, plan, export, compare, and validate receive --ux-input-root with that same absolute root" + ] + }, + "focus_fallbacks": [ + { + "surface": "consult-transfer", + "ref": "consultTransferPanelRef.current", + "selector": ".agent-popover-content", + "scenario": "Removing the last focused mid-call summary control falls back to the consult/transfer panel root" + }, + { + "surface": "receiver-ai-assistant", + "ref": "panelRef.current", + "selector": "[data-testid=\"ai-assistant:panel\"][role=\"dialog\"]", + "scenario": "Removing the last focused receiver summary control falls back to the AI Assistant dialog root" + }, + { + "surface": "post-call-wrap-up", + "ref": "wrapUpPanelRef.current", + "selector": ".agent-popover-content", + "scenario": "Removing the last focused post-call summary control falls back to the wrap-up panel root" + } + ], + "structural_inventory": { + "canonical_table": "design/default/design_spec.md#ux-evidence-and-productionization", + "coverage_key": "(stateId, mode, action?)", + "policy": "Expand every table row by listed mode and action; reject missing, extra, duplicate, merged, or unnamed-scenario tuples." + }, + "comparison_gate": { + "whole_image_pixel_threshold": null, + "required_structural_gates": [ + "DOM order and mapped boxes", + "exact copy and accessibility", + "behavior and focus", + "semantic token identity", + "contrast and forced colors" + ], + "predeclared_masks": [ + "source corrections", + "semantic token regions", + "color-independent foreground coverage", + "literal RGB remainder" + ], + "foreground_coverage_max_percent": 0.5, + "nonempty_literal_rgb_max_percent": 0.5, + "empty_literal_policy": "record null with no-literal-authority" + } + }, + "description": "Operator-approved layout clarification (2026-09-25, design_spec.md): supersede earlier single-outer-scrollbar/no-nested-scrollbar clauses and scroll-owner test expectations in this task. Keep the 80vh panel heading, summary actions and completion action visible; destination/reason lists and shared summary content may scroll independently without individually scrolling keyed editors or horizontal overflow. Preserve source-aligned panel capture, declared tooltip overhang, all text, keyboard access, interaction state and focus. All other task obligations, dependencies and acceptance commands remain unchanged. REQ-009, REQ-011, REQ-012, PRE-005, PRE-006, UX-001, UX-002, UX-003, UX-004, UX-005, UX-006, UX-007, UX-008, UX-009, UX-010, JNY-001, JNY-002, AC-11, AC-12, AC-13, AC-15, and AC-16 / Change: Deterministic UX and Release Validation, UX Evidence and Productionization, and Figma JSON Reconstruction Assessment - this implementation-plus-evidence task writes only the validation dependency/lock, sample-host bridge, Playwright harness/configuration, and typed visual registry; before any visual-plan preparation, rendering, classification receipt, or comparison, run ai-summary-sdk-lock.js verify-ux against the ten D0a-sealed scene-graph/text/PNG triples strictly beneath the target-branch worktree. Re-resolve and rehash every receipt-listed file and all three aggregate identities; reject missing/non-regular/symlinked/escaping paths, parent/common-checkout or environment fallback, and any drift without changing or resealing sdk_package_lock.json or creating visual evidence. D0a, not D8, establishes the seal before D3. For UX-007, verify the source-declared S07-UX / desktop-light screenshot exactly as sealed, normalize only its render/comparison/evidence key to S07 / dl, prove both identities resolve to the same sealed artifact, and reject an absent or conflicting mapping. Extend the existing harness with aiSummaryUtils exports installAISummarySDKMock, driveAISummaryState, auditAISummaryAccessibility, and recordAISummaryVisual. Define AISummaryBrowserScenario as a readonly non-empty tuple of discriminated group/member selections derived from the exact D1 aiSummaryFixtures keys, and compile the timeout, stale-timestamp, ownership-change, and response-failure scenario constants against it. Define AISummaryAxeInclude as a serializable single-selector include object. Define the closed UXScreenshotId union S01-S06, S07-UX, and S08-S10, an exhaustive readonly UXSourceId-to-UXScreenshotId map, and recordAISummaryVisual's union of exact source/screenshot argument tuples; UX-007 accepts S07-UX rather than S07 at this boundary. Use two independently collected specs with non-overlapping ownership: playwright/tests/ai-summary-test.spec.ts registers every browser journey and scoped accessibility case, while playwright/suites/ai-summary-tests.spec.ts registers only visual/evidence-manager cases. Neither spec imports or registers the other's scenarios. Conditionally append the exact AI Summary Deterministic project only when AI_SUMMARY_E2E=1, with exact testDir './playwright' and testMatch ['**/suites/ai-summary-tests.spec.ts', '**/tests/ai-summary-test.spec.ts'], covering both directories with no OAuth/global-setup dependency. The OAuth setup project and every USER_SETS-derived pre-existing project retain their own suite-specific match and add exact testIgnore ['**/suites/ai-summary-tests.spec.ts', '**/tests/ai-summary-test.spec.ts']; the conditional project alone omits that exclusion, and unflagged CI/default/named/sharded commands do not append the new project. Acceptance must prove the unflagged all-project two-path listing finds no test, the flagged all-project listing contains only AI Summary Deterministic entries, both paths list exactly once under that project, and the dedicated run explicitly names both paths with one worker, reuses the existing top-level sample-app webServer, and defines no second server. From the repository root run yarn add -D -E axe-core@4.10.2; commit the command-generated package.json root edge and rewritten root workspace descriptor/checksum in yarn.lock, and let Yarn refresh the active linker install state rather than hand-recording either file. D8 acceptance begins with yarn install --immutable before any axe resolution or browser test, and the task requires configured registry access or a complete local Yarn cache. In aiSummaryUtils resolve the package-owned runtime with require.resolve('axe-core/axe.min.js') through the active Yarn resolver, read those bytes with node:fs, inject them with page.addScriptTag({content: axeSource}), assert typed window.axe, and validate that the AISummaryAxeInclude selector is syntactically valid and resolves to exactly one connected Element in page context, then evaluate axe.run({include: [selector]}) with runOnly tags wcag2a, wcag2aa, wcag21a, wcag21aa, wcag22a, and wcag22aa. Write the full audit result and fail on an invalid, missing, detached, or ambiguous include root or any nonempty violations array; never omit the context or widen to document. Add playwright/visual/ai-summary-visual-cases.ts as the typed, version-controlled one-to-one case registry: ten capture entries identify the exact declared S01-S06, S07-UX, and S08-S10 sealed scene/text/PNG receipt keys; structural-only entries identify every implemented no-screenshot state plus S10 as the sole source-bound reason requirement-availability-override, with owning scenario and DOM/accessibility assertions. Then exercise initiator/receiver/post-call UI, exact View summary reachability, the open-instance-latched mid-call destination renderer in summary-present/omitted states, including capability arrival/revocation and ownership-generation change after query/category/destination/focus state exists while preserving destination node identity/order, values, filtering, selection, scroll owner, and focus and changing only summary descendants, plus close/reopen deriving a fresh latch, pending and current mid-call reopen without duplicate transport, explicit auto-start fulfillment/rejection observation and zero unhandled rejection, the post-call eligible/legacy layouts and transitions, exact Search wrap-up reasons Input naming after typing, the RadioGroup name derived from its Choose a reason to wrap up heading, a zero-match structural-only case with visible static copy and no live-region semantics, arrow-key traversal followed by Enter/Space or whole-group focus exit issuing exactly one request for the final committed reason, immediate first-commit post-call generating state, disabled re-entrant Retry while requestPending, one generating panel for concurrent direct post-call requests, terminal generation error enabling eligible-without-draft completion as not-required while Retry cannot revoke the escape, reason change after edit/copy/local feedback retaining exact draft bytes, counters, selection, and Pending submission until a current reason-generation response is accepted, with every superseded response stale before timestamp ordering, paired resolutionPresent deterministic ordinal insertion when adjacent sections are absent and resolutionAbsent total Outcome row/space/copy/response omission without SDK-section reordering, keyboard focus including the onFocusCapture pre-removal successor path, View summary activation advancing to the open-panel header successor or focusable panel-root fallback, receiver card replacement retaining connected copy/feedback focus, and unavailable/removal advancing to the next document-order successor or that panel root; Clipboard fulfillment/rejection with the integrated copied counter unchanged while pending/rejected and incrementing exactly once only after fulfillment for all three roles; rejected receiver SDK/direct-consumer feedback and Clipboard Promises with zero unhandledrejection; copy hover/confirmation leaving the Complete Wrap-Up label/name unchanged; exact post-call immediate local selected paint plus Pending submission and mid-call SDK-success-gated paint, with Submission not confirmed after response failure and description removal after success; expanded and collapsed post-call 320px layouts sharing an 80vh shell with independent reason/content scrolling, no horizontal overflow and a visible primary; forced colors; concurrency; cross-agent immediate clear/zero counters, same-agent conference pending/failure retention until valid replacement, normalized wrap-up/response results, and global uncaught-rejection capture. recordAISummaryVisual must reject any source/screenshot tuple outside the exhaustive pairing before creating output or invoking MatrixBuilder, normalize only verified UX-007 / S07-UX to canonical S07 / dl evidence, and resolve inputs only through the verified receipt, read both sealed JSON files for identity/dimensions/structure/copy, copy the sealed PNG bytes unchanged to .matrix/results/prog-mini-js/ux-visual/implementation///target.png as the per-run baseline, apply only the closed predeclared sealed-node variable-slot masks for S01/S04-S09 and source-underscore/rendered-em-dash separator mask for S02 after forming the root/tooltip base mask, reject any diff-derived or expanded exclusion, and write rendered.png, diff.png, comparison.json, dom.json, and accessibility.json beside it with base/correction/final mask identities; comparison.json records the triple hashes and registry key. Render and measure declared S01-S06, S07-UX, and S08-S10 using descriptor-owned raw scenario IDs, keeping the UX-007 S07 / dl alias only in sidecars; S10 measured difference is diagnostic and enabled completion remains intentional; retain all ten sealed PNG bytes unchanged for provenance, write S10 structural evidence bound to its sealed triple and requirement-availability override, and assert through DOM/accessibility copy evidence that the uniform unresolved-percent-variable rule yields delimiter-free Search by name, queue, entry point or phone number, Search topic, and requirement-controlled Complete Wrap-Up in every state, and that generating copy is exactly Just a sec—the details are coming together. with U+2014 and no U+005F; run export/compare for at most five iterations and <=0.5% difference, and never update a baseline from rendered output. A baseline changes only after an approved requirement/source change, updated expected hashes, and a fresh D0a seal; .ccwidgets remains immutable. For each structural-only entry, including the zero-match reason query and the source-bound S10 availability override, write .matrix/results/prog-mini-js/ux-visual/implementation/structural//classification.json plus DOM/accessibility evidence and no fabricated pixel score. Finish with validate-ux-visual-comparison, which rejects missing or duplicate registry classes, incomplete artifacts, source drift, or any visual/structural coverage gap. Never call Figma MCP. Generated evidence is not implementation source. D7 transitively requires the D4/D5/D6 fan-out and D6b join, so browser evidence targets the complete public integration.", + "files": [ + "package.json", + "yarn.lock", + "widgets-samples/cc/samples-cc-react-app/src/aiSummaryE2E.ts", + "widgets-samples/cc/samples-cc-react-app/src/App.tsx", + "playwright/Utils/aiSummaryUtils.ts", + "playwright/suites/ai-summary-tests.spec.ts", + "playwright/tests/ai-summary-test.spec.ts", + "playwright/visual/ai-summary-visual-cases.ts", + "playwright.config.ts" + ], + "dependencies": [ + "D7-host-status-callback" + ], + "acceptance": "yarn install --immutable && REPOSITORY_ROOT=\"$(git rev-parse --show-toplevel)\" && node tooling/src/ai-summary-sdk-lock.js verify-ux --widgets-root \"$REPOSITORY_ROOT\" && yarn workspace @webex/cc-task test:unit --runInBand && yarn workspace @webex/cc-ai-assistant test:unit --runInBand && yarn workspace @webex/cc-components test:unit --runInBand --runTestsByPath tests/components/AISummary/ai-summary.tsx tests/components/AIAssistant/adaptive-card-renderer.test.tsx tests/components/AIAssistant/ai-assistant.tsx tests/components/task/CallControl/call-control.tsx tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx tests/components/task/CallControl/CallControlCustom/wrap-up-summary.tsx && node -e 'const fs=require(\"node:fs\");const path=require(\"node:path\");const cp=require(\"node:child_process\");const value=process.env.MB_FLOW_BIN;if(!value||!path.isAbsolute(value))process.exit(1);const bin=fs.realpathSync(value);fs.accessSync(bin,fs.constants.X_OK);const result=cp.spawnSync(bin,[\"--help\"],{stdio:\"ignore\"});process.exit(result.status===0?0:1)' && COMMON_GIT_DIR=\"$(git rev-parse --path-format=absolute --git-common-dir)\" && test \"$(basename \"$COMMON_GIT_DIR\")\" = .git && COMMON_CHECKOUT_ROOT=\"$(dirname \"$COMMON_GIT_DIR\")\" && cd \"$COMMON_CHECKOUT_ROOT\" && cd \"$REPOSITORY_ROOT\" && ! env AI_SUMMARY_E2E=0 yarn playwright test --list --project='AI Summary Deterministic' playwright/suites/ai-summary-tests.spec.ts playwright/tests/ai-summary-test.spec.ts >/dev/null 2>&1 && ! env AI_SUMMARY_E2E=0 yarn playwright test --list playwright/suites/ai-summary-tests.spec.ts playwright/tests/ai-summary-test.spec.ts >/dev/null 2>&1 && env AI_SUMMARY_E2E=1 MB_FLOW_BIN=\"$MB_FLOW_BIN\" yarn playwright test --list --project='AI Summary Deterministic' playwright/suites/ai-summary-tests.spec.ts playwright/tests/ai-summary-test.spec.ts >/dev/null && AI_SUMMARY_PROJECT_LIST=\"$(env AI_SUMMARY_E2E=1 MB_FLOW_BIN=\"$MB_FLOW_BIN\" yarn playwright test --list playwright/suites/ai-summary-tests.spec.ts playwright/tests/ai-summary-test.spec.ts)\" && printf '%s\\n' \"$AI_SUMMARY_PROJECT_LIST\" | awk 'BEGIN {seen=0} /^[[:space:]]*[[][^]]+[]][[:space:]]+›/ {seen=1; if($0 !~ /^[[:space:]]*[[]AI Summary Deterministic[]][[:space:]]+›/) exit 1} END {if(!seen) exit 1}' && printf '%s\\n' \"$AI_SUMMARY_PROJECT_LIST\" | grep -Fq 'suites/ai-summary-tests.spec.ts' && printf '%s\\n' \"$AI_SUMMARY_PROJECT_LIST\" | grep -Fq 'tests/ai-summary-test.spec.ts' && env AI_SUMMARY_E2E=1 MB_FLOW_BIN=\"$MB_FLOW_BIN\" yarn playwright test --workers=1 --project='AI Summary Deterministic' playwright/suites/ai-summary-tests.spec.ts playwright/tests/ai-summary-test.spec.ts && cd \"$COMMON_CHECKOUT_ROOT\" && \"$MB_FLOW_BIN\" prog-mini-js validate-ux-visual-comparison --ux-input-root \"$REPOSITORY_ROOT\"", + "prerequisites": [ + { + "kind": "service", + "name": "npm registry for exact axe dependency install", + "service": "https://registry.npmjs.org", + "check_command": [ + "node", + "-e", + "require('https').get('https://registry.npmjs.org/-/ping',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))" + ], + "working_directory": ".", + "timeout_seconds": 20, + "required": true, + "fix_hint": "Allow yarn add/install to reach the configured registry or provide a complete local Yarn cache for axe-core 4.10.2; never hand-edit package.json or yarn.lock." + }, + { + "kind": "tool", + "name": "MatrixBuilder mb-flow prog-mini-js UX query and comparison runtime from MB_FLOW_BIN", + "command": "MB_FLOW_BIN", + "check_command": [ + "node", + "-e", + "const fs=require('node:fs');const path=require('node:path');const cp=require('node:child_process');const value=process.env.MB_FLOW_BIN;if(!value||!path.isAbsolute(value))process.exit(1);const bin=fs.realpathSync(value);fs.accessSync(bin,fs.constants.X_OK);const result=cp.spawnSync(bin,['--help'],{stdio:'ignore'});process.exit(result.status===0?0:1)" + ], + "working_directory": ".", + "timeout_seconds": 60, + "required": true, + "required_env": [ + "MB_FLOW_BIN" + ], + "fix_hint": "Set MB_FLOW_BIN to an absolute executable MatrixBuilder mb-flow path in the developer or CI environment; do not commit a machine-specific fallback. Use it only after D0a seals and D8 verify-ux rechecks the exact target-worktree sources." + }, + { + "kind": "tool", + "name": "Playwright Clock API runtime (@playwright/test >=1.45.0; repository lock resolves 1.51.1)", + "command": "yarn", + "check_command": [ + "yarn", + "node", + "-e", + "const version=require('@playwright/test/package.json').version;const [major,minor]=version.split('.').map(Number);process.exit(major>1||(major===1&&minor>=45)?0:1)" + ], + "working_directory": ".", + "timeout_seconds": 20, + "required": true, + "fix_hint": "Install immutable workspace dependencies with @playwright/test 1.45.0 or newer; page.clock is required for the browser fake-time timeout case." + }, + { + "kind": "tool", + "name": "Playwright Chromium browser binary", + "command": "yarn", + "check_command": [ + "yarn", + "node", + "-e", + "const fs=require('fs');const {chromium}=require('@playwright/test');fs.accessSync(chromium.executablePath())" + ], + "working_directory": ".", + "timeout_seconds": 20, + "required": true, + "fix_hint": "Install the repository-pinned Playwright Chromium binary before running browser or visual acceptance." + }, + { + "kind": "service", + "name": "Playwright-managed samples-cc-react-app web server", + "service": "http://localhost:3000", + "check_command": [ + "yarn", + "workspace", + "samples-cc-react-app", + "webpack", + "--version" + ], + "working_directory": ".", + "timeout_seconds": 20, + "required": true, + "fix_hint": "Install workspace dependencies; Playwright starts the declared sample-app server through playwright.config.ts." + } + ] + }, + { + "id": "D8b-restore-publishable-sdk-dependency", + "title": "Verify the publishable SDK dependency boundary", + "description": "REQ-001, REQ-012, AC-01, and AC-16 / Change: Deterministic UX and Release Validation - after D8 completes against D0's sealed local artifact, run verify-publishable-sdk-boundary without changing the development resolution. Pack exactly @webex/cc-store, @webex/cc-components, @webex/cc-task, @webex/cc-ai-assistant, @webex/cc-widgets, and @webex/test-fixtures into a transaction-local directory. Parse each actual package/package.json and require every @webex/contact-center dependency, optionalDependency, or peerDependency to remain plain registry-resolvable SemVer exactly as declared by the source workspace; reject file:, portal:, link:, workspace:, path aliases, private-root resolution leakage, and any vendored tarball in a package packlist. Hash the six tarballs and packed manifests into one atomic repository-local verification receipt. Harden tooling/src/publish.js so any local protocol in root resolutions or a publishable Contact Center manifest aborts before a stableVersion removal, version write, or publish call, and prove this behavior with tests. This task does not publish or download the development-only SDK version and does not replace the root development override needed by this run.", + "files": [ + "package.json", + "packages/contact-center/store/package.json", + "yarn.lock", + "tooling/src/publish.js", + "tooling/tests/publish.js" + ], + "dependencies": [ + "D8-browser-ux-validation" + ], + "acceptance": "node tooling/src/ai-summary-sdk-lock.js verify-publishable-sdk-boundary && REPOSITORY_ROOT=\"$(git rev-parse --show-toplevel)\" && node tooling/src/publish.js verify-ai-summary-packages --receipt \"$REPOSITORY_ROOT/.matrix/results/prog-mini-js/release/ai-summary-packed-workspaces.json\" && corepack yarn test:tooling --runInBand --runTestsByPath tooling/tests/ai-summary-sdk-lock.test.js tooling/tests/publish.js" + }, + { + "id": "D9-release-validation", + "title": "Add the read-only release verifier and run the complete gate set", + "design_review_fixes": { + "REQ-009/containing-panel-focus-target-not-named": "Verify all three exact ref-scoped fallback targets and their named last-summary-control-removal scenarios. Reject document.body, a disconnected node, a same-class target from another popover, or any focus movement while the original control remains connected.", + "D8/ux-source-paths-unresolved-across-worktree-and-parent": "Rehash all thirty staged inputs under the canonical target-worktree root recorded as resolutionRoot '.', require every MatrixBuilder child receipt to carry the same canonical absolute --ux-input-root, and reject parent-checkout reads, symlinks, pre-copy hashes, or render-before-rehash evidence.", + "REQ-011/structural-only-state-list-never-enumerated": "Use the closed structural-only table in design_spec.md expanded by (stateId, mode, action?) as the exact denominator. Reject any missing, extra, duplicate, merged, unnamed-scenario, or fabricated-screenshot classification.", + "render-compare-plan/0.5pct-gate-vs-semantic-token-substitution": "Reject any aggregate whole-image pixel score. Verify exact structural gates, predeclared source-correction and semantic-token masks, color-independent foreground coverage at <=0.5 percent, and a non-empty literal-RGB remainder at <=0.5 percent; an empty literal remainder must record null and no-literal-authority.", + "post-call-wrapup:complete-disabled-during-generation-and-error-no-escape": "Require ten measured source comparisons, including diagnostic S10, plus the source-bound S10 requirement-availability structural override; reject a disabled terminal-error primary, a missing eligible-without-draft not-required completion receipt, or an S10 pixel baseline that would reinstate the availability regression.", + "post-call-wrapup:combined-reason-list-plus-summary-overflow-unspecified": "Require the integrated 320px evidence for an 80vh post-call shell with independent reason/content scrolling, no horizontal overflow and visible headings/actions in both expanded and collapsed layouts.", + "post-call-wrapup:copy-visual-state-mutates-primary-submit-label": "Reject browser/DOM evidence in which any copy visual state changes the Complete Wrap-Up visible label or accessible name.", + "UX-002..UX-010/addressed-rows-trace-only-to-validation-tasks": "D9 adds a read-only verifier and runs all release gates; it edits no product or evidence files. Verify the D3/D6 task trace, focused acceptance receipts, browser/visual evidence, and product-file ownership.", + "D9-release-validation/read-only-characterization-contradiction": "D9 writes only tooling/src/verify-ai-summary-release.js and its fail-closed Jest test; the verifier is read-only, and the task acceptance runs all release gates without editing product or gate-evidence files.", + "axe-injection-and-node-modules-vs-pnp": "Validate axe linker-neutrally: require the exact root descriptor, parsed lock resolution/checksum, and hash of axe-core/axe.min.js resolved through the active Yarn install state. Do not require, infer, or skip based on a node_modules directory path.", + "sdk-mock-injection-seam-undefined": "Require browser evidence that exact ai-summary-e2e=1 navigation observed the version-1 sample-host bridge before render and initialized the store through the existing host-supplied store.init({webex}) path; reject missing-seam, bundled Webex.init, OAuth, or network-fallback evidence.", + "mbflow-cli-invoked-from-test-source-nonportable": "Require portable MB_FLOW_BIN evidence, the git-common-dir-derived common-checkout render root, the canonical target-worktree UX input root on every child argv, and fail-closed child-process receipts; reject a machine-specific path, a child reading the common checkout's .ccwidgets tree, or any non-zero, signaled, timed-out, missing-output, invalid-report, or root-mismatched command recorded as passing.", + "structural-receipts-omit-midcall-extrapolated-states": "Require every tuple from the closed structural-only table, including the nine named mid-call visual-state extrapolations with exact UX-002-through-UX-010 keys and applicable initiator/receiver modes; reject an absent, extra, duplicate, merged, or changed mapping even when a screenshot-backed sibling state exists.", + "UX-004+UX-006+UX-008:hover-tooltip-states-visual-determinism-unaddressed": "Reject a hover receipt unless it proves the fixed pointer and trigger, source placement without flip/shift, exact portal identity, disabled entry animation, frozen transitions, two-frame stable anchor/tooltip/arrow geometry, and a page clip that includes the portal. Recompute the captured metadata rather than trusting a pass boolean.", + "UX-EVIDENCE:structural-only-state-enumeration-omits-implemented-states": "Require one unique classification/DOM/accessibility receipt for every expanded (stateId, mode, action?) tuple in the design's closed structural-only table. This includes receiving adaptive-card ready/unrenderable, transfer heading, hidden authorization/initialization, every mid-call feedback/copy/edit/generating/error extrapolation, unavailable states, both pending actions, feedback pending/not-confirmed, response outcomes, and lifecycle/concurrency states. Reject any missing, extra, duplicate, merged, screenshot-downgraded, broken-IDREF, or unnamed-scenario entry.", + "UX-EVIDENCE:production-semantics-omit-focus-move-on-disappearing-control": "Require browser evidence that removal of the final focusable summary control on consult/transfer focuses consultTransferPanelRef.current, on receiver focuses panelRef.current, and on wrap-up focuses wrapUpPanelRef.current after no connected successor remains. Reject document.body, disconnected nodes, another same-class popover instance, or focus movement on ordinary connected replacement.", + "UX-006,UX-008/unreconciled-capture-scale-outliers": "Reverify every source's exact scale-2/root-relative crop metadata and immutable raw target hash, then recompute predeclared source-correction and semantic-token masks. Reject S06/S08 width substitution, crop/tooltip geometry drift, unrecorded transforms, changed mask denominators, aggregate whole-image scoring, foreground-coverage error above 0.5 percent, or non-empty literal-RGB error above 0.5 percent.", + "validate-ux-visual-comparison-invoked-twice": "Require the suite to produce per-tuple artifacts without invoking the final validator and require exactly one acceptance-owned validate-ux-visual-comparison receipt after the current Playwright run; reject zero, duplicate, stale, or suite-owned validator invocations.", + "AC-15-figma-mcp-nonuse-unverified": "Make verifyUXEvidence prove both AC-15 halves from the flow-authenticated manifest: re-resolve every UX input to its sealed repository-relative local path and hash, rehash every enumerated changed/evidence file, AST-inspect executable changed JS/TS, and parse structured JSON evidence for remote Figma endpoint/file/node URLs, MCP client/server or tool-call provenance, or file/node-fetch records. Narrative prohibition and a boolean claim are insufficient. Add a failing unit fixture with remote-MCP provenance while all local source hashes remain valid, plus a clean local-only case.", + "REQ-013-promotion-target-check-vacuous": "Read the exact flow-produced, flow-gate-hashed /.matrix/results/prog-mini-js/release/ai-assistant-summary.json receipt containing the current head commit, execution branch, resolved promotion target, requirement hash, and sorted changed/evidence path hashes. Without shelling out, resolve the target worktree .git indirection plus HEAD/loose-or-packed ref and bind it to the receipt; parse the single Branch value from requirement.md and require the resolved target to equal ai-assistant-summary. Allow executionBranch only when it equals that target or matches ^mb-worktree-(?:design|design_fix)-[0-9]+-[0-9a-f]{8}-[0-9a-f]{8}$; the transient name never substitutes for the target. Add wrong-target, arbitrary-pattern, stale-hash, and HEAD-mismatch failures.", + "D9-unit-tests-omit-declared-checks": "Add four otherwise-valid failing fixtures that reach and reject the real branches for an upstream-import dependency-direction violation, a stale touched module specification, an out-of-allowlist changed file, and a wrong resolved promotion target. Keep the clean all-gates fixture, and do not satisfy these cases with stubs or an earlier unrelated failure.", + "UX-001:eligible-and-legacy-consult-popover-layout-duplication": "Require focused/browser receipts for one voice destination tree in summary-omitted and summary-present states, with only the summary subtree changing across capability transitions; reject evidence for a second search-first pill voice implementation.", + "UX-001:transfer-variant-copy-invented-without-structural-only-receipt": "Require the dedicated Transfer-panel structural receipt with aiSummary.midCall.transferHeading, the final exact product-confirmed string, a non-empty confirmation reference, action-parity justification, no extrapolatedFrom field, and no claim that UX-001 supplied or authorized Transfer copy.", + "UX-002..UX-010:post-call-popover-width-400-440-442-unreconciled": "Require each source fixture's exact host width and zero CSS-pixel mapped-box tolerance while production continues to use only the host container's inline size. Reject state-selected product widths, geometry drift, target PNG dimension/hash drift, or an attempt to compensate with an aggregate pixel score.", + "UX-008+UX-009:clipboard-promise-rejection-handling-unspecified": "Reject release evidence unless all three modes prove rejected Clipboard Promises produce no success paint, recorder call, copied-counter increment, raw-error exposure, or unhandled rejection while fulfilled writes increment exactly once.", + "UX-009:copy-success-treatment-duration-and-revert-trigger-unspecified": "Require focused/browser receipts for the exact 1,500 ms confirmation interval and independent timeout, focus-loss, second-copy, summary-replacement, and unmount cleanup cases; reject stale, missing, or nondeterministic copied-state evidence.", + "UX-010:complete-wrap-up-permanently-disabled-on-generation-error": "Require the D6 focused receipt and S10 structural receipt proving an unrecovered no-draft generation error leaves Complete Wrap-Up enabled and completes once as response not-required with no summary response; reject any terminal-error dead end or Retry-dependent escape.", + "UX-010:missing-having-trouble-generating-summary-exact-copy-binding": "Require the sealed S10 copy binding and DOM evidence for exact title Having trouble generating summary plus exact connection/retry subcopy across every non-hidden error category; reject divergent title/subcopy or use of that presentation for hidden authorization/initialization.", + "REQ-008/pending-submission-strings-absent-from-ux-inventory": "Require separate structural-only pending/not-confirmed records with aiSummary.feedback.pendingSubmission = Pending submission and aiSummary.feedback.submissionNotConfirmed = Submission not confirmed. Re-resolve both Like/Dislike aria-describedby IDREFs, require pending enabled and not-confirmed read-only semantics, reject live regions, and require success-removal evidence.", + "UX-002..UX-010/same-control-label-differs-by-visual-state": "Reject release evidence unless every UX-002 through UX-010 receipt uses canonical Search topic and Complete Wrap-Up visible/accessibility/tooltip copy after source-artifact normalization. Any state-selected label, delimiter-bearing production string, or inconsistent primary capitalization fails.", + "UX-010/connection-specific-subcopy-applied-to-all-error-categories": "Require the exact common title/subcopy and category inventory for disabled, offline, unavailable, empty, timeout, and generic, while keeping authorization/initialization hidden and unsupported content separate. Reject an omitted category or evidence that presents the description as a network-cause diagnosis.", + "transfer-variant/heading-copy-without-source-authority": "Reject the Transfer structural receipt unless it names aiSummary.midCall.transferHeading, carries the final exact DOM copy and non-empty product/content-owner confirmation reference, includes the action-parity rationale, and omits extrapolatedFrom and UX-001 authority claims.", + "test-strategy-item9:playwright-tests-spec-not-in-any-acceptance-command": "Require current successful receipts for both independently collected AI Summary specs, and require the D9 browser command to name playwright/suites/ai-summary-tests.spec.ts and playwright/tests/ai-summary-test.spec.ts together; either missing path or a duplicate collection fails release.", + "test-strategy-item10:S01-S10-enumeration-vs-declared-S07-UX-key": "Verify the declared source-key set S01-S06, S07-UX, and S08-S10 and the exact visual-receipt mapping. Reject literal S07 as a sealed-source lookup; accept canonical S07 / dl only after it is bound to the same S07-UX / desktop-light hash.", + "test-strategy-item11:jest-test-runs-build-and-browser-gates": "Keep the Jest suite receipt-only: it passes recorded gate/hash fixtures to read-only verifier functions and never launches Yarn, Playwright, MatrixBuilder, packing, publishing, or the release CLI. The D9 acceptance command executes every gate outside Jest.", + "test-strategy-item11:no-check-that-local-file-tarball-not-published": "Require a hash-bound receipt plus six receipt-listed tarballs, reopen each tarball, and inspect its actual packed package/package.json for the six touched workspaces. Every packed @webex/contact-center dependency/optionalDependency/peerDependency must be a plain registry-resolvable SemVer version/range, and any local tarball, local protocol, path, or workspace alias fails release." + }, + "assigned_packet_verification": { + "precedence": "This verification contract supersedes conflicting older wording in this task description or inherited design-review-fix values.", + "ux_resolution": "resolutionRoot must be '.', all thirty inputs must rehash from the target worktree after staging, and every prepare, plan, export, compare, and validate receipt must record the same canonical --ux-input-root.", + "focus_denominator": [ + "consultTransferPanelRef.current", + "panelRef.current", + "wrapUpPanelRef.current" + ], + "structural_denominator": "Every tuple obtained by expanding the design_spec.md structural-only table by listed mode and action.", + "comparison_denominator": "Exact structural gates plus predeclared source-correction and semantic-token masks; no aggregate whole-image pixel score." + }, + "description": "Operator-approved layout clarification (2026-09-25, design_spec.md): supersede earlier single-outer-scrollbar/no-nested-scrollbar clauses and scroll-owner test expectations in this task. Keep the 80vh panel heading, summary actions and completion action visible; destination/reason lists and shared summary content may scroll independently without individually scrolling keyed editors or horizontal overflow. Preserve source-aligned panel capture, declared tooltip overhang, all text, keyboard access, interaction state and focus. All other task obligations, dependencies and acceptance commands remain unchanged. PRE-001, PRE-002, PRE-003, PRE-004, PRE-005, PRE-006, REQ-001, REQ-002, REQ-003, REQ-004, REQ-005, REQ-006, REQ-007, REQ-008, REQ-009, REQ-010, REQ-011, REQ-012, UX-001, UX-002, UX-003, UX-004, UX-005, UX-006, UX-007, UX-008, UX-009, UX-010, JNY-001, JNY-002, AC-01, AC-02, AC-03, AC-04, AC-05, AC-06, AC-07, AC-08, AC-09, AC-10, AC-11, AC-12, AC-13, AC-14, AC-15, and AC-16 / Change: Deterministic UX and Release Validation and Test Strategy - D9 adds a read-only verifier and runs all release gates; it edits no product or evidence files. After D8b verifies the publishable SDK dependency boundary, add verify-ai-summary-release.js and fail-closed Jest tests. The Jest suite supplies recorded receipt/hash fixtures to the verifier functions and never invokes Yarn, Playwright, MatrixBuilder, packing, publishing, or the release CLI. The verifier exposes asynchronous verifySDKReceipt and runReleaseVerification plus synchronous verifyUXEvidence, verifyPublishableDependencies, verifyPrivacyAllowlist, verifyDAGTraceability, and verifyGateReceipts. Implement verifySDKReceipt only as a thin adapter that imports and awaits D0's exported verifySdkPackage with widgetsRoot=context.repositoryRoot and resolutionMode=development; do not reparse the SDK receipt or reimplement archive, full-file, declaration, all-manifest, all-lock-descriptor, or installed-artifact checks. The standalone ai-summary-sdk-lock CLI and D9 binary intentionally exercise that same implementation. Record/verify ai-assistant-summary as the promotion target while allowing the flow's transient worktree branch, require EXT-SDK-CC-SUMMARIES/D0 acceptance including the same-Task post-wrap-up send probe plus D8b publishable-boundary verification, and reject any pending gate. Independently rehash requirement/design/DAG and all thirty receipt-listed UX sources strictly in the target worktree, and require UX-007's declared S07-UX / desktop-light and canonical S07 / dl evidence keys to resolve to the same sealed screenshot hash. The shared SDK verifier depends only on the portable recorded commit/package/archive/full-file/declaration identity plus development-mode lock and installed sealed artifacts; it ignores WEBEX_JS_SDK_DIR and non-authoritative source.audit, so D9 never requires the build checkout or its absolute path. Scan root package.json resolutions plus every dependencies/devDependencies/optionalDependencies/peerDependencies map in every non-private workspace package.json and reject every file:, portal:, link:, workspace:, or path SDK descriptor; allow the D0 private-root override and local lock locator only in the repository development state; require every publishable source and packed workspace descriptor to remain the original plain registry-resolvable SemVer. `verifyPublishableDependencies` must also parse the current hash-bound D8b packaging receipt, reopen every receipt-listed tarball, and read actual packed package/package.json files from exactly @webex/cc-store, @webex/cc-components, @webex/cc-task, @webex/cc-ai-assistant, @webex/cc-widgets, and @webex/test-fixtures; every packed @webex/contact-center dependencies/optionalDependencies/peerDependencies entry must be a plain registry-resolvable SemVer version/range, the store entry must equal its source workspace registry descriptor, and no local tarball/protocol/path/workspace alias may appear. Rely on the shared verifier for every development lock descriptor and the installed sealed package version, complete package-owned file set, and declarations; require the vendored evidence tarball to be absent from every packlist; and require the D8b publisher tests proving a local protocol fails before any manifest write or publish call. Validate dependency direction/spec currency; require release evidence to classify the exported always-resolving Promise wrapupCall change as breaking for @webex/cc-task and @webex/cc-components with semantic-release metadata producing a SemVer-major release; require the focused/browser receipts for the existing AI Assistant View summary trigger, the one mid-call destination renderer with conditional summary mount/removal, pending/current reopen transport reuse, explicit auto-start rejection consumption with zero unhandled rejection, resolved delimiter-free copy, the post-call eligible/legacy layouts and live transition directions, strict response-settlement-before-telephony ordering with zero calls while pending plus one non-rejecting continuation after each closed outcome, and normalized post-call settlement. Verify that playwright/visual/ai-summary-visual-cases.ts classifies every exercised state exactly once, has ten captured entries tied to declared keys S01-S06, S07-UX, and S08-S10 with descriptor-owned raw scenario IDs and UX-007 alias provenance confined to hash-bound sidecars, has exactly one S10 structural entry with reason requirement-availability-override tied to the still-sealed UX-010 triple and an enabled no-summary completion assertion, and has one structural-only entry plus classification/DOM/accessibility evidence for every other implemented no-screenshot state. For every screenshot entry, require target.png to equal the sealed PNG hash, comparison.json to bind the scene/text/PNG hashes and registry key, and the full render/diff/DOM/accessibility artifact set; reject any target derived from rendered output or any baseline change without a newly admitted source seal. Require proof that unflagged Playwright configuration omits AI Summary Deterministic and both explicit paths, while flagged configuration with testDir './playwright' and the exact two-path testMatch enumerates the journey and visual-manager specs exactly once each only under AI Summary Deterministic; require that neither spec imports the other and only the existing top-level webServer is used, the direct axe declaration is exact and locked as a root edge, the parsed lock/install state resolves axe-core/axe.min.js through the active Yarn resolver with a matching descriptor/checksum/artifact hash and no node_modules path assumption, and every scoped audit used the six declared WCAG tags with a zero-violation threshold. Scan persistence/log/telemetry/callback paths; allow only the exact content-free existing-category store.logger.trace call with static context, require the forbidden-value sentinel privacy receipt and zero metrics emission, and reject any raw error, summary/customer text, interaction/agent identifier, timestamp, duration, or additional failure vocabulary; reject UX path escape, hash drift, evidence-key mismatch, duplicate test collection, an additive-only release classification, skipped suites, incomplete visual artifacts, visual/structural registry gaps, or unhandled rejections. D9 tests mock verifySdkPackage to prove exactly one awaited development-mode delegation and rejection propagation, leave missing/tampered SDK receipt and stale full-file/declaration cases solely in D0's verifier suite, exercise the real delegate in the clean integration case, and parse recorded gate/packaging receipts without launching a child process. Acceptance, outside Jest, runs explicit TypeScript builds for all six touched workspaces before the existing all-package development build, full CC unit/style gates, the six-workspace packing gate, both deterministic browser specs plus accessibility/visual validation, all three focused tooling suites, and the provenance verifier. It runs after D8b; the verifier and Jest suite do not edit product or gate-evidence files, while the separately executed packaging and visual gates own their generated receipts/artifacts.", + "files": [ + "tooling/src/verify-ai-summary-release.js", + "tooling/tests/verify-ai-summary-release.test.js" + ], + "dependencies": [ + "D8b-restore-publishable-sdk-dependency" + ], + "acceptance": "yarn install --immutable && REPOSITORY_ROOT=\"$(git rev-parse --show-toplevel)\" && node tooling/src/ai-summary-sdk-lock.js verify-sdk && node tooling/src/ai-summary-sdk-lock.js verify-publishable-sdk-boundary && node tooling/src/ai-summary-sdk-lock.js verify && node tooling/src/ai-summary-sdk-lock.js verify-ux --widgets-root \"$REPOSITORY_ROOT\" && yarn workspace @webex/test-fixtures build && yarn workspace @webex/cc-store build && yarn workspace @webex/cc-components build && yarn workspace @webex/cc-task build && yarn workspace @webex/cc-ai-assistant build && yarn workspace @webex/cc-widgets build && yarn build:dev && yarn test:cc-widgets && yarn test:styles && node tooling/src/publish.js verify-ai-summary-packages --receipt \"$REPOSITORY_ROOT/.matrix/results/prog-mini-js/release/ai-summary-packed-workspaces.json\" && node -e 'const fs=require(\"node:fs\");const path=require(\"node:path\");const cp=require(\"node:child_process\");const value=process.env.MB_FLOW_BIN;if(!value||!path.isAbsolute(value))process.exit(1);const bin=fs.realpathSync(value);fs.accessSync(bin,fs.constants.X_OK);const result=cp.spawnSync(bin,[\"--help\"],{stdio:\"ignore\"});process.exit(result.status===0?0:1)' && COMMON_GIT_DIR=\"$(git rev-parse --path-format=absolute --git-common-dir)\" && test \"$(basename \"$COMMON_GIT_DIR\")\" = .git && COMMON_CHECKOUT_ROOT=\"$(dirname \"$COMMON_GIT_DIR\")\" && cd \"$COMMON_CHECKOUT_ROOT\" && cd \"$REPOSITORY_ROOT\" && env AI_SUMMARY_E2E=1 MB_FLOW_BIN=\"$MB_FLOW_BIN\" yarn playwright test --workers=1 --project='AI Summary Deterministic' playwright/suites/ai-summary-tests.spec.ts playwright/tests/ai-summary-test.spec.ts && cd \"$COMMON_CHECKOUT_ROOT\" && \"$MB_FLOW_BIN\" prog-mini-js validate-ux-visual-comparison --ux-input-root \"$REPOSITORY_ROOT\" && cd \"$REPOSITORY_ROOT\" && yarn test:tooling --runInBand --runTestsByPath tooling/tests/ai-summary-sdk-lock.test.js tooling/tests/publish.js tooling/tests/verify-ai-summary-release.test.js && node tooling/src/verify-ai-summary-release.js", + "prerequisites": [ + { + "kind": "service", + "name": "npm registry for immutable workspace install", + "service": "https://registry.npmjs.org", + "check_command": [ + "node", + "-e", + "require('https').get('https://registry.npmjs.org/-/ping',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))" + ], + "working_directory": ".", + "timeout_seconds": 20, + "required": true, + "fix_hint": "Allow the immutable Yarn install to reach the configured registry or provide a complete local cache." + }, + { + "kind": "tool", + "name": "MatrixBuilder mb-flow prog-mini-js UX query and comparison runtime from MB_FLOW_BIN", + "command": "MB_FLOW_BIN", + "check_command": [ + "node", + "-e", + "const fs=require('node:fs');const path=require('node:path');const cp=require('node:child_process');const value=process.env.MB_FLOW_BIN;if(!value||!path.isAbsolute(value))process.exit(1);const bin=fs.realpathSync(value);fs.accessSync(bin,fs.constants.X_OK);const result=cp.spawnSync(bin,['--help'],{stdio:'ignore'});process.exit(result.status===0?0:1)" + ], + "working_directory": ".", + "timeout_seconds": 60, + "required": true, + "required_env": [ + "MB_FLOW_BIN" + ], + "fix_hint": "Set MB_FLOW_BIN to an absolute executable MatrixBuilder mb-flow path in the developer or CI environment; do not commit a machine-specific fallback. Use it only after the combined receipt verifies target-worktree UX sources." + }, + { + "kind": "tool", + "name": "Playwright Chromium browser binary", + "command": "yarn", + "check_command": [ + "yarn", + "node", + "-e", + "const fs=require('fs');const {chromium}=require('@playwright/test');fs.accessSync(chromium.executablePath())" + ], + "working_directory": ".", + "timeout_seconds": 20, + "required": true, + "fix_hint": "Install the repository-pinned Playwright Chromium binary before release validation." + }, + { + "kind": "service", + "name": "Playwright-managed samples-cc-react-app web server", + "service": "http://localhost:3000", + "check_command": [ + "yarn", + "workspace", + "samples-cc-react-app", + "webpack", + "--version" + ], + "working_directory": ".", + "timeout_seconds": 20, + "required": true, + "fix_hint": "Install workspace dependencies; Playwright starts the declared local sample-app server." + } + ] + } + ] +} diff --git a/design/default/sdk_package_lock.json b/design/default/sdk_package_lock.json new file mode 100644 index 000000000..f3f43a86c --- /dev/null +++ b/design/default/sdk_package_lock.json @@ -0,0 +1,2422 @@ +{ + "schemaVersion": 1, + "sdk": { + "status": "sealed", + "sealedAt": "2026-09-24T13:07:52.326Z", + "packageName": "@webex/contact-center", + "registryDescriptor": "3.12.0-next.123", + "version": "3.12.0-cc-summaries.15f3da706d84", + "source": { + "branch": "cc-summaries", + "commit": "15f3da706d84c10bc632abe252cea202a70ae212", + "treeSha256": "8690f822c3421bf7a9f855fa74612b4e2df09fc7c6572f334f217437727b8791" + }, + "tarball": { + "path": "vendor/contact-center-cc-summaries.tgz", + "sha256": "62150e40c9a6c729a3dc60d936a05c132ade8311c52c5c69239617326ab01ef5", + "sizeBytes": 3081607 + }, + "yarn": { + "nodeLinker": "node-modules", + "resolution": "@webex/contact-center: file:./vendor/contact-center-cc-summaries.tgz", + "checksum": "10c0/6f679f8e84f10396c39b5a8a30d457d764d27a2ca9702823d9ba2e2317d73c87d67f057ab26439787c58413d88f748a8e3aca3889d2574e174645db39582e35b" + }, + "package": { + "files": [ + { + "path": ".sdd/manifest.json", + "sha256": "558c539515615df00e93714f2dce063b249e027cd01b64d76bd9cd7014e6648c", + "sizeBytes": 37976 + }, + { + "path": "AGENTS.md", + "sha256": "ae85c7cccb2c32a6b23b15199d1551e1f1fd7330735b5f3ef8e10211440d2a6e", + "sizeBytes": 4694 + }, + { + "path": "README.md", + "sha256": "fa0d788dc8dafbf6bf5700ae169b3825908bdbdb321f3988c1b751912f7a14da", + "sizeBytes": 1486 + }, + { + "path": "__mocks__/workerMock.js", + "sha256": "abc2eef0aef52c4bf36d9d44835c192d666f4ae06caefc17fe361c8fa40d08a5", + "sizeBytes": 279 + }, + { + "path": "ai-docs/ARCHITECTURE.md", + "sha256": "f80295440ab56e74f1ba05ca1ee8edc71f7899f3a4b32361f3e74371bca0b55a", + "sizeBytes": 9808 + }, + { + "path": "ai-docs/CONTRACTS.md", + "sha256": "510e9e5584afaadc57e2957f46de164b98012c5ffb212904df7f53603d03769c", + "sizeBytes": 12192 + }, + { + "path": "ai-docs/GETTING_STARTED.md", + "sha256": "a6d13b3ae963c1da466f60fc63301de4b8d7aeeec252061eed2faf700fd0df31", + "sizeBytes": 6542 + }, + { + "path": "ai-docs/GLOSSARY.md", + "sha256": "030fa484d5bbf10a1f9a0ac11006a12a3355fb2cbf6322a7e04e8ea21ede29f3", + "sizeBytes": 2321 + }, + { + "path": "ai-docs/README.md", + "sha256": "ecf5cbce906ddc459eb6d2178b9a1505ea06273f4c224e728e7be7fe953b72d6", + "sizeBytes": 5601 + }, + { + "path": "ai-docs/REVIEW_CHECKLIST.md", + "sha256": "13018ece188852c694e3ba519dbc583622bd7c34075aad1b361c24644e5f27b8", + "sizeBytes": 2110 + }, + { + "path": "ai-docs/RULES.md", + "sha256": "2c77e77c312c59dbdab924b877946aff5d5bde3b52a290046bbfc9c6a5f70906", + "sizeBytes": 13335 + }, + { + "path": "ai-docs/SECURITY.md", + "sha256": "115d68f3dfb9f8cc4688908774e630338ffaa72b138a5f87b6bfda4cf85177a4", + "sizeBytes": 3372 + }, + { + "path": "ai-docs/SERVICE_STATE.md", + "sha256": "7ad6d2ac7cbceb3c1a3df5a6e619d789073b0dbcd958bf086b4c60bd283a7367", + "sizeBytes": 3233 + }, + { + "path": "ai-docs/SPEC_INDEX.md", + "sha256": "aa2290bde4465c1b79ee1f55c569b0540d325f5514f7d1364dff3261d601696d", + "sizeBytes": 4021 + }, + { + "path": "ai-docs/adr/0001-spec-source-policy.md", + "sha256": "f7221fa2656626a829dfc57f5608f60b51d38648c1db508b99864cee19c248ac", + "sizeBytes": 3645 + }, + { + "path": "ai-docs/adr/README.md", + "sha256": "8580f7b9723be83ff257e4e4c618607eccf617db9c46460ac3d0bf44e97af508", + "sizeBytes": 394 + }, + { + "path": "ai-docs/adr/_adr-template.md", + "sha256": "633f61b0fe7159758d47dc29816fe839b68f21b47341db0cf36f527c1774069a", + "sizeBytes": 500 + }, + { + "path": "ai-docs/contact-center-spec.md", + "sha256": "9e5319eb6651c0caacdf69dd3ba1529ee7b5580ce61b7ffa2f4db704c273136b", + "sizeBytes": 37025 + }, + { + "path": "ai-docs/features/consult-transfer-list-policy/spec/feature-spec.md", + "sha256": "f7b474c0265c166c70f100c070fab35950217e588c02faa077c6d6f33332d192", + "sizeBytes": 51435 + }, + { + "path": "ai-docs/features/generated-spec-conformance-fidelity-remediation/spec/feature-spec.md", + "sha256": "64b417ba9602bdbe4828f8e42bc49400ba210aa8d67b98ef272bce658306089c", + "sizeBytes": 7933 + }, + { + "path": "ai-docs/features/residual-warning-coverage-completion/spec/feature-spec.md", + "sha256": "4adebaf5540abd57c41f04c3cee0499e630ec9d385edddf591fb38ad69630c5d", + "sizeBytes": 12593 + }, + { + "path": "ai-docs/features/validator-code-fidelity-drift-fix/spec/feature-spec.md", + "sha256": "085b8ee05682ef1faa7ff1c3f88d1ce7625a52d53f3fa9a171b032c8374a9d64", + "sizeBytes": 25871 + }, + { + "path": "ai-docs/patterns/event-driven-patterns.md", + "sha256": "29363a368decbfff036928a16af248cc7cafa8b41cae874f73dfa5f58e52251d", + "sizeBytes": 13986 + }, + { + "path": "ai-docs/patterns/testing-patterns.md", + "sha256": "2f631458fcd0e63ee330998ad830895577419660b9155c9fe3bd1583ff40afa5", + "sizeBytes": 10792 + }, + { + "path": "ai-docs/patterns/typescript-patterns.md", + "sha256": "ff56817395ca04ef88e8cec9582c7afe82a1cf3abf7a52f11b2fc40417ea8a81", + "sizeBytes": 7872 + }, + { + "path": "ai-docs/templates/README.md", + "sha256": "0e2a575412d04593ff8ea384ee4cd405b3f942c967a5c654a59d725162af8209", + "sizeBytes": 3960 + }, + { + "path": "ai-docs/templates/documentation/create-agents-md.md", + "sha256": "1393086838f04092647cd90ea4897ed770a54df769a5d04ee309d1a4a390235d", + "sizeBytes": 4477 + }, + { + "path": "ai-docs/templates/documentation/create-architecture-md.md", + "sha256": "63dd922f6594e4037486b65dbb4264622c90c844ca21ea4e811dcbf20e684b72", + "sizeBytes": 5890 + }, + { + "path": "ai-docs/templates/existing-service/bug-fix.md", + "sha256": "f77c63fe485044550b180a9efe7823da94459f4dda9849bb70aa18ebb7f13d8a", + "sizeBytes": 6646 + }, + { + "path": "ai-docs/templates/existing-service/feature-enhancement.md", + "sha256": "f0197bba04d6848ca8cd472d3b5d82e8152a02cc458fab7c700992810d231abc", + "sizeBytes": 12516 + }, + { + "path": "ai-docs/templates/new-method/00-master.md", + "sha256": "0fed9b3fa907a25c542641886e8965e0b44a0a128220c6fc9672a36f60648436", + "sizeBytes": 1970 + }, + { + "path": "ai-docs/templates/new-method/01-requirements.md", + "sha256": "22f842def4d5c59812071b4c45cbea13c21c1ec6ef0f0507ede4945c6db257a3", + "sizeBytes": 8192 + }, + { + "path": "ai-docs/templates/new-method/02-implementation.md", + "sha256": "01878b450e8f30ffd5ae40d7fab4b521d838933b67f6f37f99a2ad81b7b3acf4", + "sizeBytes": 8677 + }, + { + "path": "ai-docs/templates/new-method/03-tests.md", + "sha256": "d6ffea407d35a56c88988a8a1c80bcb4b4fa2bde1894f79a159b93a98d728184", + "sizeBytes": 5495 + }, + { + "path": "ai-docs/templates/new-method/04-validation.md", + "sha256": "52142425ce7764904856f18624b309cf7228e69c715cdaf90148ecc262a908a6", + "sizeBytes": 4051 + }, + { + "path": "ai-docs/templates/new-service/00-master.md", + "sha256": "205d11d886b237624072c85ac6287c2ed1148785c9a4437c80986651bc39ee07", + "sizeBytes": 3526 + }, + { + "path": "ai-docs/templates/new-service/01-pre-questions.md", + "sha256": "b42931fc693a2583a8c57b18fafbdd733adee25532fc69336dead1a654d313b5", + "sizeBytes": 6103 + }, + { + "path": "ai-docs/templates/new-service/02-code-generation.md", + "sha256": "e71f42553d2afdb076567f3a285a7aa54a88085abc8f7566dab2ee2f839ebb93", + "sizeBytes": 11652 + }, + { + "path": "ai-docs/templates/new-service/03-integration.md", + "sha256": "8fd2f8c46f29466640998ebefbc47b9aa008ec2f345511617fe84a79e34e07a9", + "sizeBytes": 4631 + }, + { + "path": "ai-docs/templates/new-service/04-test-generation.md", + "sha256": "4853517879af9fa529e4a1ef6dd3f127b83c5bbd71d43c31ccb62837d4114120", + "sizeBytes": 4751 + }, + { + "path": "ai-docs/templates/new-service/05-validation.md", + "sha256": "3f8fd572d242938d73c796ba8d6eebb3a9cded8e5d533e1fbe082ea83cecb5ad", + "sizeBytes": 4526 + }, + { + "path": "babel.config.js", + "sha256": "ec4f1a4152be7c8d443c3a70f5ad008d9953dd10ce40d4dd75477560868af1a9", + "sizeBytes": 272 + }, + { + "path": "dist/cc.js", + "sha256": "49a78b7075faf985cf754a18e5501d7f542262e51176211f4f1daa8035983b50", + "sizeBytes": 102954 + }, + { + "path": "dist/cc.js.map", + "sha256": "f56e03109778bb6bcc71d998a2385b7ec8a67c39b57064208019995fa0eaade6", + "sizeBytes": 176032 + }, + { + "path": "dist/config.js", + "sha256": "8ccee18bda872ea1f3ab98718eb23448514639d7c8b3726789338091e167bf70", + "sizeBytes": 1950 + }, + { + "path": "dist/config.js.map", + "sha256": "6159a55cad8b2a6230f4ace2807691fff81e89379b0a8b06c29cf95fe1bb992c", + "sizeBytes": 2781 + }, + { + "path": "dist/constants.js", + "sha256": "439b3b8e56ce9f9469e9d91d4ab551c736ad6a0f166ab55c9446e9c5b8179265", + "sizeBytes": 5331 + }, + { + "path": "dist/constants.js.map", + "sha256": "870162cf545ca3a532b467bd54510dfc1369fc554a5d7ea16f2557b6410676aa", + "sizeBytes": 8801 + }, + { + "path": "dist/index.js", + "sha256": "3491492df74a26537ebb86e192b232eb74b75f13ffca056a958d4d5a88f45595", + "sizeBytes": 4966 + }, + { + "path": "dist/index.js.map", + "sha256": "54f4c6019b802fdbaaf9e5f44105c59a72af56d8baa0c1c576ddecad536d3a6b", + "sizeBytes": 9112 + }, + { + "path": "dist/logger-proxy.js", + "sha256": "75358af16942f26064e92d0094d6567ea91519d6339ae5d8cbf31754cfe07211", + "sizeBytes": 4387 + }, + { + "path": "dist/logger-proxy.js.map", + "sha256": "1d44a5dc36ce87743d5d813852bde309f9ae8416d81907b4346bca65e11de767", + "sizeBytes": 7839 + }, + { + "path": "dist/metrics/MetricsManager.js", + "sha256": "778a377f788bfd3f4714cd1356ba12c76d7ef52219648b5a84b5da610a9c157b", + "sizeBytes": 14781 + }, + { + "path": "dist/metrics/MetricsManager.js.map", + "sha256": "fbffec1483c0dda68d328433aa8b6c69693251177b7a77e9fade0b3b00d5beca", + "sizeBytes": 27301 + }, + { + "path": "dist/metrics/behavioral-events.js", + "sha256": "c8c41416eeafb5c656f38920a1dc88fbd321cef44cde9b8a07e9086c2b266340", + "sizeBytes": 17210 + }, + { + "path": "dist/metrics/behavioral-events.js.map", + "sha256": "5de8f503674acff96256bc138c0b346895ea4446057de91609786e66a75263f3", + "sizeBytes": 32130 + }, + { + "path": "dist/metrics/constants.js", + "sha256": "35bd57dec8d3ba9b70e7eacd2902e2778b0e4bdc9154a5df1387257ad7a5cdc0", + "sizeBytes": 13972 + }, + { + "path": "dist/metrics/constants.js.map", + "sha256": "b6e624462f878227226c3163731a5aaad1dfa77e67fe3a3801aaee8d9602eadf", + "sizeBytes": 21624 + }, + { + "path": "dist/services/AddressBook.js", + "sha256": "4c8489a37a4df31cf658c99a44ada8f234622822d2260851e49137e814a1bae9", + "sizeBytes": 9849 + }, + { + "path": "dist/services/AddressBook.js.map", + "sha256": "068c16cffa731f43cf6d02812460a2ed584fd7eb73894f41ee3e5934c3d95a09", + "sizeBytes": 17449 + }, + { + "path": "dist/services/AnswerCallOnWebexService.js", + "sha256": "3d6c010b7c7429ff969f018c111f05f38939eedf4b08464aa6110165503e9faf", + "sizeBytes": 6078 + }, + { + "path": "dist/services/AnswerCallOnWebexService.js.map", + "sha256": "0ded9f64af2770a5ba4101da9d0791b89b94a6e9b9bd9109713461801418be5c", + "sizeBytes": 13426 + }, + { + "path": "dist/services/ApiAiAssistant.js", + "sha256": "37a81ce8f1f5f7df4464a35a0b72e6157e98c7b8ef52d3afdaa245b2961f0078", + "sizeBytes": 19576 + }, + { + "path": "dist/services/ApiAiAssistant.js.map", + "sha256": "3cfa3fb9aed7c87355ff184c2c5884389de54f24fe2d4b4f2da2df717d523924", + "sizeBytes": 38922 + }, + { + "path": "dist/services/EntryPoint.js", + "sha256": "0668e90d00490ce4d0c6af395fae480ed9f8ed2f306d659bc6b382c10c4e5fba", + "sizeBytes": 7338 + }, + { + "path": "dist/services/EntryPoint.js.map", + "sha256": "42e4a2aec987a4cd9e65a1c729ff1f1525fac0e8926bb542f87fda7958661d1b", + "sizeBytes": 13822 + }, + { + "path": "dist/services/Queue.js", + "sha256": "4a0389607acc94f44e5b0c3f5d5a93a5b9b117cea406e6182efcea68d2ebe3fc", + "sizeBytes": 9805 + }, + { + "path": "dist/services/Queue.js.map", + "sha256": "40d918fc3d08a973819d8217e9ce7584ba58d47019fcd43bce4ebfe897bf41b6", + "sizeBytes": 17663 + }, + { + "path": "dist/services/UserPreference.js", + "sha256": "9c125eb20e5304a4a8c918624ff2081a9bea9457d8a7269b8fbfe2f1d866801c", + "sizeBytes": 14990 + }, + { + "path": "dist/services/UserPreference.js.map", + "sha256": "1a1411f1c59223a18bd1d8cb2ecf578dbb3836b9e46bb5f1ed74b10e38e9dd32", + "sizeBytes": 25696 + }, + { + "path": "dist/services/WebCallingService.js", + "sha256": "759210f9743fc572579efcbf9b9f73a0c736c5e56116575242baf9b6f67d3b8e", + "sizeBytes": 9888 + }, + { + "path": "dist/services/WebCallingService.js.map", + "sha256": "93fa21ae8b6e5bfb434c36f1602bb118446c51d289b1715d48ad7ca3469964e5", + "sizeBytes": 17933 + }, + { + "path": "dist/services/WebexCrossClientService.js", + "sha256": "bcff9d3d890ec25b63413da97a7f1d3aa11370926d1f4ce8e693574311239437", + "sizeBytes": 8088 + }, + { + "path": "dist/services/WebexCrossClientService.js.map", + "sha256": "ed20f8ce3212f79bb2bba1f48e769996324d53a9871e6457adeef855e9c67397", + "sizeBytes": 16772 + }, + { + "path": "dist/services/WxAppTelephonyMercurySync.js", + "sha256": "716a4649870907ef9eb247617baee402ae25c26926cd224f50e70850ea771077", + "sizeBytes": 4109 + }, + { + "path": "dist/services/WxAppTelephonyMercurySync.js.map", + "sha256": "7ebf1d77fd6a53383a0367c6d1d3ba7b5f4acbf4e5e1941fb8ec7a883e574e87", + "sizeBytes": 8439 + }, + { + "path": "dist/services/agent/index.js", + "sha256": "9e0c59fceff0b4b52581a88d76e0406a8acc08f7caff92991e95fccdca029940", + "sizeBytes": 5277 + }, + { + "path": "dist/services/agent/index.js.map", + "sha256": "169129dd6b5acba84bf15d53826a7f0eac47d30621674f11f647cb0a94f61ee9", + "sizeBytes": 9483 + }, + { + "path": "dist/services/agent/types.js", + "sha256": "8e239cc217fded077303a2ee7ceea413a70fa12b0b2f3cd4809219fe6f64cf40", + "sizeBytes": 5603 + }, + { + "path": "dist/services/agent/types.js.map", + "sha256": "fd207943c84df7b7208a0b3bc80227569aba145bb66c9096172d5a4a564d121f", + "sizeBytes": 16079 + }, + { + "path": "dist/services/config/Util.js", + "sha256": "2fc5c7e75735e487af57354d26ee4f91ad981b7e8785cd43e1da3e4ecff652c6", + "sizeBytes": 8001 + }, + { + "path": "dist/services/config/Util.js.map", + "sha256": "672759985c5c391ad360d156a15f0f70019ddaa2261815b2b1f32d7d8354b65a", + "sizeBytes": 17866 + }, + { + "path": "dist/services/config/constants.js", + "sha256": "a93f752241f7735e44ff82f966f40e08d7290cb10c5d0bddf8ec02bf0a62c612", + "sizeBytes": 11052 + }, + { + "path": "dist/services/config/constants.js.map", + "sha256": "0b12e1f44c7c2935600444545d90c54405417d0a1748ade4d2baea6370290b75", + "sizeBytes": 16656 + }, + { + "path": "dist/services/config/index.js", + "sha256": "84bb14f71ae031b8d9b5490accafe3aa4bbe3adcac31e4a04bbdbd207d7235f7", + "sizeBytes": 29322 + }, + { + "path": "dist/services/config/index.js.map", + "sha256": "1c51306ef962e68f25aa8bf8b8668722902af5717a4ef2b6a30e3c30242c4c4a", + "sizeBytes": 50394 + }, + { + "path": "dist/services/config/types.js", + "sha256": "8f0c5d5d66d34ef8b50fdcf600fcd1dcb443da4c13336d2a69f05a4bc53848d0", + "sizeBytes": 14108 + }, + { + "path": "dist/services/config/types.js.map", + "sha256": "b0f671cfc5131ce71cd405e9339fdd4304800811444ab27a0aa9119072c19fbc", + "sizeBytes": 47211 + }, + { + "path": "dist/services/constants.js", + "sha256": "c28f317e03b064f003f55120314e0dca4428ef7f104f1156fe112918ccb7e22a", + "sizeBytes": 4148 + }, + { + "path": "dist/services/constants.js.map", + "sha256": "cd448a982a44d5bd07de976d8bbd57cf299c152ba16f53701c7f1abd7ef71eb5", + "sizeBytes": 6041 + }, + { + "path": "dist/services/core/Err.js", + "sha256": "15ac1a9d9380e286cfbea776227dcae6cdb6bfbbd82838787c51812b0bb71858", + "sizeBytes": 963 + }, + { + "path": "dist/services/core/Err.js.map", + "sha256": "f7a29358f5f2789f2c3acbadfeca4844ec7ad976899497529fbfdd8f47bbbaf2", + "sizeBytes": 5247 + }, + { + "path": "dist/services/core/GlobalTypes.js", + "sha256": "2908774aac53b6c31ed73de05f42867b7e73567986eeb4492dab667fa28c74fa", + "sizeBytes": 120 + }, + { + "path": "dist/services/core/GlobalTypes.js.map", + "sha256": "209bf3c98ad74e0ad3f70d1d5eec1bd3a8c1c480a4723cecba13996a8aad4f65", + "sizeBytes": 1800 + }, + { + "path": "dist/services/core/Utils.js", + "sha256": "d92c978f3f1774e538fd99f14f6151c99fc314d3d9156fbd16be8c703c107672", + "sizeBytes": 16752 + }, + { + "path": "dist/services/core/Utils.js.map", + "sha256": "a045885a536a54d293d43a754b0329bb06f1261850873cab4804d26e539d8ad2", + "sizeBytes": 28633 + }, + { + "path": "dist/services/core/WebexRequest.js", + "sha256": "7eef678d467f65e91fd7cd1a9f3f96d2bc532ea23f3c00066090eaf52c0df837", + "sizeBytes": 2917 + }, + { + "path": "dist/services/core/WebexRequest.js.map", + "sha256": "31da1055987014232ae82d1bc1d9ba32d423feba01323bc80a551db17231d671", + "sizeBytes": 6078 + }, + { + "path": "dist/services/core/aqm-reqs.js", + "sha256": "08f509f2d070e9ec30143677e7efc9711aba5d658ab1ca3f7af000445e29acc1", + "sizeBytes": 11866 + }, + { + "path": "dist/services/core/aqm-reqs.js.map", + "sha256": "393895fc3ac7abba171c22cd31de97173b4e4a73613647296fcd19eabcd75336", + "sizeBytes": 23557 + }, + { + "path": "dist/services/core/constants.js", + "sha256": "1dd360b26b9ef4684f1f8ba0f82109ba99658789c54b34a8dec55914d34654c3", + "sizeBytes": 4000 + }, + { + "path": "dist/services/core/constants.js.map", + "sha256": "9ff134b006a06732c955b6ced71ccc227ea34c51635ad0627b98ea19bb048f37", + "sizeBytes": 5986 + }, + { + "path": "dist/services/core/types.js", + "sha256": "a09c56ed3cf5baf8b487c370fa4c6af691b08c10c2c1d40e5000dcf276d7da66", + "sizeBytes": 114 + }, + { + "path": "dist/services/core/types.js.map", + "sha256": "2b96cb5412624dada8de1f3f07161c10ec0511a6f4cf5d78088aa66e34775d24", + "sizeBytes": 2470 + }, + { + "path": "dist/services/core/websocket/WebSocketManager.js", + "sha256": "7a0f0a8cc842072febc82e2c236418e94a285bca2e58b77c0d4b908179f8d91d", + "sizeBytes": 8005 + }, + { + "path": "dist/services/core/websocket/WebSocketManager.js.map", + "sha256": "885f5c16ecea45b5b1791c98986bb04ce4e480926edb51a67c97f6da59f6108e", + "sizeBytes": 15536 + }, + { + "path": "dist/services/core/websocket/connection-service.js", + "sha256": "eddcbe001815a3a9973e0692b1df5d0dcf5a5fb59acc5003a3aa4a687a144c00", + "sizeBytes": 4221 + }, + { + "path": "dist/services/core/websocket/connection-service.js.map", + "sha256": "40489974faa34616fb9271554133fcea5b7c99f247f5f19bfa893725689ee17e", + "sizeBytes": 9772 + }, + { + "path": "dist/services/core/websocket/keepalive.worker.js", + "sha256": "dea73aa1cd5c34292cb98e425f3684e5c77672d0d788cc20ef7c67c4c7fc5ada", + "sizeBytes": 2598 + }, + { + "path": "dist/services/core/websocket/keepalive.worker.js.map", + "sha256": "11c0d0d3513743c0b4b60a6e299338d6bcf42104c7910447e90ee560bab76c00", + "sizeBytes": 3197 + }, + { + "path": "dist/services/core/websocket/types.js", + "sha256": "a09c56ed3cf5baf8b487c370fa4c6af691b08c10c2c1d40e5000dcf276d7da66", + "sizeBytes": 114 + }, + { + "path": "dist/services/core/websocket/types.js.map", + "sha256": "92c8ae008eea1e9a6032aa1f050e13dd9f67e433216455fcea3821e459a13fee", + "sizeBytes": 1546 + }, + { + "path": "dist/services/index.js", + "sha256": "895fc0fc1145470ff6abf0e2491c15da4f4bb90b925a144c6fa520ef37330dfd", + "sizeBytes": 2899 + }, + { + "path": "dist/services/index.js.map", + "sha256": "f185733a7553408e4920c925877a0b48ef7e56dceabece07b8a1de5f4b3ae58f", + "sizeBytes": 5044 + }, + { + "path": "dist/services/task/AutoWrapup.js", + "sha256": "7f023829e2b99dcebc7e64fb5fb256bc1146ed3e90343707a9c50c493b31f55a", + "sizeBytes": 2273 + }, + { + "path": "dist/services/task/AutoWrapup.js.map", + "sha256": "333298afb86d7c29aa80d94e269f486e8d3f31d0c0493bbeabf1d1332c60a10b", + "sizeBytes": 4249 + }, + { + "path": "dist/services/task/Task.js", + "sha256": "d2df2858b681832a12397c49beb9889e7a7afb908242d0e90ac2ff19563ca7cf", + "sizeBytes": 34577 + }, + { + "path": "dist/services/task/Task.js.map", + "sha256": "e449c0e073fe1b96455cbb8f0e1cb4f1f356ca8b22a392509fd98c770cddf3ab", + "sizeBytes": 72628 + }, + { + "path": "dist/services/task/TaskFactory.js", + "sha256": "7db225be5fe7a8c519f8f7b54ae659c33bb8d20fb53bbacb884c5b8be533d2d2", + "sizeBytes": 2028 + }, + { + "path": "dist/services/task/TaskFactory.js.map", + "sha256": "db19efc7d8b4c090502eec9991c38964bf97377205c5c993e62135ddb5976a2b", + "sizeBytes": 4375 + }, + { + "path": "dist/services/task/TaskManager.js", + "sha256": "ce2f6fe1cb11439b699ae650480bf24f919c40f55446b4fea76719713c04f9c4", + "sizeBytes": 61954 + }, + { + "path": "dist/services/task/TaskManager.js.map", + "sha256": "b363d3da56530ca96b37f15b5b08c86432c7bd388a072fa7b9087f919e4b4ed5", + "sizeBytes": 119943 + }, + { + "path": "dist/services/task/TaskUtils.js", + "sha256": "a0c65b265e8c35e9664b6a142ceaa0fb9c2086f8f022005d26c539886810212b", + "sizeBytes": 19005 + }, + { + "path": "dist/services/task/TaskUtils.js.map", + "sha256": "a8559289b5869e6175afabaa9703113edc888aa30b096574de4ee586b16e58c9", + "sizeBytes": 33628 + }, + { + "path": "dist/services/task/WebexCallingUtils.js", + "sha256": "14e6c34141ed3d828a797be9777543f6650cfb5581fd7262996f6bb9b38e7178", + "sizeBytes": 3187 + }, + { + "path": "dist/services/task/WebexCallingUtils.js.map", + "sha256": "9348cfc334d41d12e28ffe8894943ca8f4ee5c8af920a91da5c0bbd20e848a23", + "sizeBytes": 7237 + }, + { + "path": "dist/services/task/constants.js", + "sha256": "f912b1b9bb38be895a467eb8bb0238b7df72f6d43de553d4059f0d6998e01933", + "sizeBytes": 6012 + }, + { + "path": "dist/services/task/constants.js.map", + "sha256": "e72b29d69debbdcc758b91dcf723c41ea25bdb5f38a42c9b2b583d9ed634cc38", + "sizeBytes": 9183 + }, + { + "path": "dist/services/task/contact.js", + "sha256": "ea09cb06eb07a44bb9aae1e3bf8ff2d075ef8342ba98089f991b5e40a27d6b8a", + "sizeBytes": 16194 + }, + { + "path": "dist/services/task/contact.js.map", + "sha256": "676de8ee96e3921de026c03d1bf4297714b668cdb9381b7fa352be1dfe18f27b", + "sizeBytes": 29610 + }, + { + "path": "dist/services/task/dialer.js", + "sha256": "55b513cbbd741ec89508511dea46309967bc710548531a080f5de47310c1ae2d", + "sizeBytes": 6576 + }, + { + "path": "dist/services/task/dialer.js.map", + "sha256": "ef7c136dd5e517abf09e0dd76cab67b47f9f9bc326d4330754f0509ba62dea88", + "sizeBytes": 10869 + }, + { + "path": "dist/services/task/digital/Digital.js", + "sha256": "cf926a8a1c49bd4b8886e8c9afd78524cbc3f2e58137a7b54d5258846384e360", + "sizeBytes": 2905 + }, + { + "path": "dist/services/task/digital/Digital.js.map", + "sha256": "d5994640f65b2563a642649e68f0d834e36f163cee8d43c47b4b855698d1adac", + "sizeBytes": 6011 + }, + { + "path": "dist/services/task/state-machine/TaskStateMachine.js", + "sha256": "b81636c302c6496d2c26ae922875cf74043989c02e8bbb92c63e684a4dd08275", + "sizeBytes": 49498 + }, + { + "path": "dist/services/task/state-machine/TaskStateMachine.js.map", + "sha256": "8bbd73c4e0e705356c673a5963908cb7456f4858a9b753e56b5c0b137d3f00cf", + "sizeBytes": 83306 + }, + { + "path": "dist/services/task/state-machine/actions.js", + "sha256": "e43dfa018868e894b5ca20b21fa716752bca7202a05e11536d4b927a6769b70c", + "sizeBytes": 23497 + }, + { + "path": "dist/services/task/state-machine/actions.js.map", + "sha256": "85e4c1d6885cfbd875d0b5c3933c3718dbdd9eea00b06d0b400d2b57bacc4c51", + "sizeBytes": 48923 + }, + { + "path": "dist/services/task/state-machine/constants.js", + "sha256": "3f871cfaa3e986be053451c9f03b09c6ce6006631c786c90edcf54ec7793c32b", + "sizeBytes": 6498 + }, + { + "path": "dist/services/task/state-machine/constants.js.map", + "sha256": "91cd52f4a8baac73527814b50ce3fb8ccc4af692b391e97ed25db5490934d5b8", + "sizeBytes": 7379 + }, + { + "path": "dist/services/task/state-machine/guards.js", + "sha256": "2df59f2edb401547047810368aee7a155f4dde24b6b0cd64d76542e99698b66d", + "sizeBytes": 15814 + }, + { + "path": "dist/services/task/state-machine/guards.js.map", + "sha256": "770fdfc1f2bbeffbcb014ee3f8a347a26e89837d0f2b222361f9f6acfc45201d", + "sizeBytes": 30043 + }, + { + "path": "dist/services/task/state-machine/index.js", + "sha256": "ec7e813d3bc1301cca1f50d152887f5ebce3b1d2cca6ea72aba16a5f6a3cdc50", + "sizeBytes": 1253 + }, + { + "path": "dist/services/task/state-machine/index.js.map", + "sha256": "93ba5829ef32dc2d1916cf61ca1f6a776d150a0a122641907b31dc86ff5f464a", + "sizeBytes": 1038 + }, + { + "path": "dist/services/task/state-machine/types.js", + "sha256": "5b7d4b0519b9bf20a10bd8881ad2c22ad9b53051de2a7c9670fcb6d9ca2aa385", + "sizeBytes": 1025 + }, + { + "path": "dist/services/task/state-machine/types.js.map", + "sha256": "d93f81b5056e81f3dea68320aad1114aa7b1b96d963b5bf374ec5e0bd32564e7", + "sizeBytes": 10489 + }, + { + "path": "dist/services/task/state-machine/uiControlsComputer.js", + "sha256": "91fada5348b14756e71e2727db58b97c49abe053eb9857e8f82e868854cbc58f", + "sizeBytes": 40838 + }, + { + "path": "dist/services/task/state-machine/uiControlsComputer.js.map", + "sha256": "7591aca7b7124ed83b6fed979613a9d219ba7ad3cfe7a90c676cc7554a5a6fe5", + "sizeBytes": 77943 + }, + { + "path": "dist/services/task/taskDataNormalizer.js", + "sha256": "6a5e83564c0f6f820ba55995a7a18e5ecc88e1d715b50c8ecda2a7c27b9c06d1", + "sizeBytes": 3274 + }, + { + "path": "dist/services/task/taskDataNormalizer.js.map", + "sha256": "5bc3a82db304ea20f3a1e289fa8d719ecd66feffca420db2ada6688691772fac", + "sizeBytes": 6713 + }, + { + "path": "dist/services/task/types.js", + "sha256": "c2dee6898714b624ede0385c114623eecd44c337ed49a772d16d5dea32f13f40", + "sizeBytes": 27639 + }, + { + "path": "dist/services/task/types.js.map", + "sha256": "50b36eb8431e67d3e14dcf460d3e66036472a11cf1c3dacf17e69df8928c2bd7", + "sizeBytes": 79615 + }, + { + "path": "dist/services/task/voice/Voice.js", + "sha256": "dbeb155f788e90f30bd36d9115a3932ac6c679d8a80ba9e961995589681a866f", + "sizeBytes": 60631 + }, + { + "path": "dist/services/task/voice/Voice.js.map", + "sha256": "65eae10e2d7a352b39d386e38c45fd9585e1252be0c8aa4b9e443092637e19ac", + "sizeBytes": 116110 + }, + { + "path": "dist/services/task/voice/WebRTC.js", + "sha256": "704daa364ecbca730778687e87738403dd2d79f26000828c8d840ddca71fd4d5", + "sizeBytes": 5476 + }, + { + "path": "dist/services/task/voice/WebRTC.js.map", + "sha256": "bf913a4d572fbcef4fa821141f811d5b93ead36246d5cd8b96cd7bddd9ce4b19", + "sizeBytes": 11363 + }, + { + "path": "dist/services/task/voice/wxAppVoiceMethods.js", + "sha256": "940b3ba250af16088e3abb1b98c6bb5c7178a21c1ff4839a2b21f78ca74c971d", + "sizeBytes": 14643 + }, + { + "path": "dist/services/task/voice/wxAppVoiceMethods.js.map", + "sha256": "3b45d251de561d2e02e1fb05cd3443351c7c5981ae2d0cba2712175041b6f42a", + "sizeBytes": 31222 + }, + { + "path": "dist/services/wxAppDiagnosticLogging.js", + "sha256": "0708fe9ab1c2ff6c77936e8eb48f5ea1d27ee7144a7c9c1075813fc04a3af492", + "sizeBytes": 4103 + }, + { + "path": "dist/services/wxAppDiagnosticLogging.js.map", + "sha256": "f27a7ce9e650d3ebb5999bf4549b836fcc59525db5fab65d17ec3021b95488f5", + "sizeBytes": 9472 + }, + { + "path": "dist/services/wxAppTelephonyUtils.js", + "sha256": "e1a3e9cefde9b13fd9b2b4c188ad4f5863ad199507caee061ae771b7c35a4f33", + "sizeBytes": 631 + }, + { + "path": "dist/services/wxAppTelephonyUtils.js.map", + "sha256": "e26044d06db98a37778b50dd676ae61e6104350514d2dab570b2993b29701860", + "sizeBytes": 1071 + }, + { + "path": "dist/types/cc.d.ts", + "sha256": "b0f696741044226e145189b8e77516319afc74a2bac0f401d617bbfc2b3a5755", + "sizeBytes": 37373 + }, + { + "path": "dist/types/config.d.ts", + "sha256": "75939a49084e42336924b31a3db8dd04865b422921c19bb7589067d7b0e06cbe", + "sizeBytes": 2102 + }, + { + "path": "dist/types/constants.d.ts", + "sha256": "45f82b556af409401cb413f7b6f3092d12e615bfd7ddb839e788373c45c4b9c5", + "sizeBytes": 3519 + }, + { + "path": "dist/types/index.d.ts", + "sha256": "af7d60ef4b784a3f57c304a4eb783ce07c42b188bfbd9c8acdb169f40472e39a", + "sizeBytes": 6791 + }, + { + "path": "dist/types/logger-proxy.d.ts", + "sha256": "3642a40846bfbdd54550b235c51172dd3df1c240bd6601a0f0c92e96be5a6e79", + "sizeBytes": 2513 + }, + { + "path": "dist/types/metrics/MetricsManager.d.ts", + "sha256": "5c8e3c9f9f8ee4c33f4736d419ed1ea28dc41d185ff702beb09cd770ff91a2c3", + "sizeBytes": 7965 + }, + { + "path": "dist/types/metrics/behavioral-events.d.ts", + "sha256": "98096ed7bad87b45acd5ddad765ffeb3a9a9c9b20141365bcaa6035f3286b709", + "sizeBytes": 1207 + }, + { + "path": "dist/types/metrics/constants.d.ts", + "sha256": "1abf0041764d1140285b3e3c4e871b356b03065b04219de5ae127a50942e38a7", + "sizeBytes": 14905 + }, + { + "path": "dist/types/services/AddressBook.d.ts", + "sha256": "33c1d50bbb6f0b5be44ea1db1c689c28fb8a50a6661b2a080ad8997075c75835", + "sizeBytes": 2576 + }, + { + "path": "dist/types/services/AnswerCallOnWebexService.d.ts", + "sha256": "67ce5b96770b43219865517805ea9417963c2a7fc0ed3fae8a64c52012b0a5ab", + "sizeBytes": 1219 + }, + { + "path": "dist/types/services/ApiAiAssistant.d.ts", + "sha256": "ca8d5aac68d6e43c5903d19c8f32e2d217f2d7c8f44b748558dca828b4f499d0", + "sizeBytes": 4070 + }, + { + "path": "dist/types/services/EntryPoint.d.ts", + "sha256": "3f40e060e67889f3787abc8cebd884eca256fc3afad60d901e96dda82972f329", + "sizeBytes": 2369 + }, + { + "path": "dist/types/services/Queue.d.ts", + "sha256": "bdfc7551744cf9ac47d3ed0afb74e1159b4c0f7092570f2232b044b79b960426", + "sizeBytes": 2488 + }, + { + "path": "dist/types/services/UserPreference.d.ts", + "sha256": "11df3284409ebd6e2eb22b7bb378cdaff7f05ba26100125202d1200d45f49f69", + "sizeBytes": 4678 + }, + { + "path": "dist/types/services/WebCallingService.d.ts", + "sha256": "8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881", + "sizeBytes": 11 + }, + { + "path": "dist/types/services/WebexCrossClientService.d.ts", + "sha256": "bed718030f5260c355bb6335887307f644af34fd1e1172ad704cafaf4b27eb62", + "sizeBytes": 1206 + }, + { + "path": "dist/types/services/WxAppTelephonyMercurySync.d.ts", + "sha256": "9a1cd9516b539835f37f04af6d5c7b5e623a81d397f53df956805ce97b6776e3", + "sizeBytes": 926 + }, + { + "path": "dist/types/services/agent/index.d.ts", + "sha256": "43461f28ca595d6793851a6119fcbea5d87483a5921b0ad380b0db73df74bb72", + "sizeBytes": 1314 + }, + { + "path": "dist/types/services/agent/types.d.ts", + "sha256": "28bf51c66b0f8adfed045b4a1b7628509c0151dca412ea4838888af43574bd2a", + "sizeBytes": 15187 + }, + { + "path": "dist/types/services/config/Util.d.ts", + "sha256": "93fce78b6632cd5135182783a7f4e9184e1c4380525f5f33168899282c1f4302", + "sizeBytes": 716 + }, + { + "path": "dist/types/services/config/constants.d.ts", + "sha256": "7a9a12914d33e23bbb999d0e450ffada36c4f81c561777a3657b1363df057f38", + "sizeBytes": 9250 + }, + { + "path": "dist/types/services/config/index.d.ts", + "sha256": "96960b7fbb800c2aae86a7874f19616090815ad27dbe280a177ea8f664528dd2", + "sizeBytes": 10065 + }, + { + "path": "dist/types/services/config/types.d.ts", + "sha256": "79f50aa4f56c2a21403d0dad0908c9a3284f4876008d5e09a978c8eefb958832", + "sizeBytes": 52164 + }, + { + "path": "dist/types/services/constants.d.ts", + "sha256": "56b21b5e6624ab87dbb682f33896d7204ecad31ba0b78f808a07cdf39c148fb0", + "sizeBytes": 2727 + }, + { + "path": "dist/types/services/core/Err.d.ts", + "sha256": "68ae23a8fe9595d906d3b7ea6169eb76138444476587cb59378af34d57b2147c", + "sizeBytes": 3732 + }, + { + "path": "dist/types/services/core/GlobalTypes.d.ts", + "sha256": "09f8719247f439f06f25e9476ef0bd4603638b778a6acd88aa8de6e261459c03", + "sizeBytes": 1691 + }, + { + "path": "dist/types/services/core/Utils.d.ts", + "sha256": "4b70545a6cf31657273e54567e163001ef55d7d23a8379afff80f70a8b7816ac", + "sizeBytes": 5974 + }, + { + "path": "dist/types/services/core/WebexRequest.d.ts", + "sha256": "aca024101dee4bf70cb4c4d4307517e663fb99c52826ea17ee086799f21fbd29", + "sizeBytes": 750 + }, + { + "path": "dist/types/services/core/aqm-reqs.d.ts", + "sha256": "59ad8809b01105f0bd0f57954e2090c6b1ee50e9543a32ee8aecb4724a410eff", + "sizeBytes": 2573 + }, + { + "path": "dist/types/services/core/constants.d.ts", + "sha256": "685079dd3b3e98f763f327a9373bd21d3bb919c5d219028ea0d9e7712cfdaaa5", + "sizeBytes": 2862 + }, + { + "path": "dist/types/services/core/types.d.ts", + "sha256": "943cf2a0ecd40a341f4538da5f7e4aaf6d809cbf1f804233ba125b108fb799b0", + "sizeBytes": 1587 + }, + { + "path": "dist/types/services/core/websocket/WebSocketManager.d.ts", + "sha256": "0e84ccf12cb785b15ace725568ac542c81f4bd5a997b5d025b39fe30194d8d36", + "sizeBytes": 1279 + }, + { + "path": "dist/types/services/core/websocket/connection-service.d.ts", + "sha256": "10738b3a4dd160c07f9307e7661937b98f5883d333975a349bbe9841171f3dc1", + "sizeBytes": 919 + }, + { + "path": "dist/types/services/core/websocket/keepalive.worker.d.ts", + "sha256": "96970057a5e816793c1cf811f1b2024a6183552849155e8d5b76254f2cb3f554", + "sizeBytes": 2471 + }, + { + "path": "dist/types/services/core/websocket/types.d.ts", + "sha256": "973044bd59d7045a40b2e6e1bfe5ef8490ebf33df4516634a149bc69f0bf79db", + "sizeBytes": 1420 + }, + { + "path": "dist/types/services/index.d.ts", + "sha256": "3a2b1fdfe74be17f1d26ee3f8397aa4ae06ed804c35c9b5c3678132e9e97be88", + "sizeBytes": 2301 + }, + { + "path": "dist/types/services/task/AutoWrapup.d.ts", + "sha256": "f2d051ac4ce4f14d4fc592f5c6d74fd6d58e90f7d52a1363960534b6d46305d3", + "sizeBytes": 1282 + }, + { + "path": "dist/types/services/task/Task.d.ts", + "sha256": "3f11db2fa96a54877509a44b232587d31132055f92136a07165f79228bedaee2", + "sizeBytes": 8353 + }, + { + "path": "dist/types/services/task/TaskFactory.d.ts", + "sha256": "554a4657d6c1094450b9cd381a47b79d633cd7b47d7ef2f875eb56fe34591ab6", + "sizeBytes": 710 + }, + { + "path": "dist/types/services/task/TaskManager.d.ts", + "sha256": "8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881", + "sizeBytes": 11 + }, + { + "path": "dist/types/services/task/TaskUtils.d.ts", + "sha256": "07903739e6a2de1518ec85ad705d03c422f945246910c32a0235cddc0f12b7db", + "sizeBytes": 7618 + }, + { + "path": "dist/types/services/task/WebexCallingUtils.d.ts", + "sha256": "a2a5c92fc5e898d0db18363ae2858876d0eb7b9391b3f00f7adc26a5be4a91fe", + "sizeBytes": 776 + }, + { + "path": "dist/types/services/task/constants.d.ts", + "sha256": "19a8b6974786b57f8ca17eb55a0453bb692980262193e12fda2d9137be4dc997", + "sizeBytes": 4147 + }, + { + "path": "dist/types/services/task/contact.d.ts", + "sha256": "f87caf2000648279cfd70b0763feef3481e50b700f5abdf96236c80a6fb8f3ed", + "sizeBytes": 2673 + }, + { + "path": "dist/types/services/task/dialer.d.ts", + "sha256": "faad7b5f0ff455e1e3235be9751f0d636d918ddfe0ac67ff2d26b7553126a967", + "sizeBytes": 2981 + }, + { + "path": "dist/types/services/task/digital/Digital.d.ts", + "sha256": "1ba200a317ab4934bbc8f8c37fd3834b907b2423a3db00d51eab9e04106e0481", + "sizeBytes": 938 + }, + { + "path": "dist/types/services/task/state-machine/TaskStateMachine.d.ts", + "sha256": "b0d22c0f11b4323b8bf7d2f3fc46f09dfb5344ec2bab376a563b9c83135992ec", + "sizeBytes": 50281 + }, + { + "path": "dist/types/services/task/state-machine/actions.d.ts", + "sha256": "39a59133fbf4dfbfc240fe6c77bcc3d7c803985be923fb22ccb4496d5a787be7", + "sizeBytes": 771 + }, + { + "path": "dist/types/services/task/state-machine/constants.d.ts", + "sha256": "fbbd86fee6f11733e08a82b40161340094c58fe3aef3a455b7c265f16a6d84f0", + "sizeBytes": 4087 + }, + { + "path": "dist/types/services/task/state-machine/guards.d.ts", + "sha256": "13f1d149c04f1cdcb568328053dd87589f34044e1adab6dec8129df9b957239f", + "sizeBytes": 5202 + }, + { + "path": "dist/types/services/task/state-machine/index.d.ts", + "sha256": "07a48947c1eac6a55e94e619c94b67da338e834540e82bcb3dc8be7436f2c252", + "sizeBytes": 595 + }, + { + "path": "dist/types/services/task/state-machine/types.d.ts", + "sha256": "648729df89edaf7b52adc7ab6aca67e5c132c8c273944a133efb8d018e086f10", + "sizeBytes": 10424 + }, + { + "path": "dist/types/services/task/state-machine/uiControlsComputer.d.ts", + "sha256": "fabd6f257ade440f8da68340d5ec4bfb4771796c51d254aeb7a91b6341747a7b", + "sizeBytes": 532 + }, + { + "path": "dist/types/services/task/taskDataNormalizer.d.ts", + "sha256": "e45fb875558d7f11a32e52d3a956dc3a11277f3512249578a5e537000562c32d", + "sizeBytes": 510 + }, + { + "path": "dist/types/services/task/types.d.ts", + "sha256": "6ffa63a58c3f241e368de118ad2d9ab84c4052ac9e5879c39c78c23cb545eca9", + "sizeBytes": 72025 + }, + { + "path": "dist/types/services/task/voice/Voice.d.ts", + "sha256": "60dab7f21cbd4e615530cdadfa31d67d02ff251fc2ff205e9dbcf8635f8cdb82", + "sizeBytes": 8941 + }, + { + "path": "dist/types/services/task/voice/WebRTC.d.ts", + "sha256": "d20477a30b1801dd25b0ac2ed1a51d02eee11d74ab6412c398aeb1471daaa6c0", + "sizeBytes": 1756 + }, + { + "path": "dist/types/services/task/voice/wxAppVoiceMethods.d.ts", + "sha256": "fbeb48048ae87e1ea84b143241ce81045f7465bc9fe69fa889be769d97688a6e", + "sizeBytes": 3206 + }, + { + "path": "dist/types/services/wxAppDiagnosticLogging.d.ts", + "sha256": "8543b668aaa3247995e108a9959ff509a20f6aef0589e6175f68e05a58068d8b", + "sizeBytes": 2210 + }, + { + "path": "dist/types/services/wxAppTelephonyUtils.d.ts", + "sha256": "f6daf3b6a8988a7a00732f971dfd3d30054286ae54c9a566d3f0f223d9c0980a", + "sizeBytes": 246 + }, + { + "path": "dist/types/types.d.ts", + "sha256": "5ca805b513392fe59838d931f16eb39ae197dda6cb8fa803046ee2eeff7daad7", + "sizeBytes": 27755 + }, + { + "path": "dist/types/utils/PageCache.d.ts", + "sha256": "485255db6826391bdbad27cb20edd5411395321ef913eb21e46c91fc8b92a794", + "sizeBytes": 5471 + }, + { + "path": "dist/types/webex-config.d.ts", + "sha256": "afce94129a3caa54d735ace0af17032977572e2bb493fff5616abe7c5607d566", + "sizeBytes": 1453 + }, + { + "path": "dist/types/webex.d.ts", + "sha256": "cb1f8ec9e35e25ca5941703b3e8c53523925bd6f5bfcddbc5b9cf12d214b4ed9", + "sizeBytes": 231 + }, + { + "path": "dist/types.js", + "sha256": "8d1f5c745b43a05fe482278263f25614ba54418564b86c11d75e1a6079aa8b4c", + "sizeBytes": 12705 + }, + { + "path": "dist/types.js.map", + "sha256": "704c116397204bcaddc1d8027c8537083a6dd733ac46fe9f8dc9b1978143785c", + "sizeBytes": 37525 + }, + { + "path": "dist/utils/PageCache.js", + "sha256": "be7950adc843c26db0c0b4a28d9974438132b1d682cb6fce444e96c398cd90ed", + "sizeBytes": 5255 + }, + { + "path": "dist/utils/PageCache.js.map", + "sha256": "01a415b51a53d630ce60240698281903f0cc5977f0adc894bbe3cff592cc5556", + "sizeBytes": 10923 + }, + { + "path": "dist/webex-config.js", + "sha256": "1fca7b157e8ffa9bedff75dff2c06016cdf611c4c21942db91c3cdaa437d33e7", + "sizeBytes": 1698 + }, + { + "path": "dist/webex-config.js.map", + "sha256": "1a734a89b57f3bbced7645700ab8727a97de66d4e5e3b5376d66db47ea862f0e", + "sizeBytes": 2471 + }, + { + "path": "dist/webex.js", + "sha256": "c4b247f405c9221dc03635e28254f4d0771fb727cbb707f51614045bb1e69718", + "sizeBytes": 5290 + }, + { + "path": "dist/webex.js.map", + "sha256": "bd7356cfc0aa2b19853a6954fa8638d196af165bad2a6bd4e1b566b559161c24", + "sizeBytes": 6828 + }, + { + "path": "jest.config.js", + "sha256": "9a90f4a184a0bd692d6c280ff44ac961e27ed5e8ed523f0a9930acccbed602ab", + "sizeBytes": 1032 + }, + { + "path": "package.json", + "sha256": "08b16eacf28a7f27b959eeb9f54c3094e6327dd05605888df00956fe80c62e32", + "sizeBytes": 3328 + }, + { + "path": "src/cc.ts", + "sha256": "fe478d1f609f8b12d4be2873c22ced1adeb3672a6ba377a2c391777884b3433f", + "sizeBytes": 104872 + }, + { + "path": "src/config.ts", + "sha256": "60ea1a39b683c6f43d9d58ae36e921070955f6035b7928ad78cd3ceedb31d39e", + "sizeBytes": 1794 + }, + { + "path": "src/constants.ts", + "sha256": "3697fb30ade2bb5a8880935ae7b2409dbfa5978399edb861aeb72e2aac2cfd36", + "sizeBytes": 3951 + }, + { + "path": "src/index.ts", + "sha256": "44308383a987d66849cf01bf6d99a2eff0f56d995ae256c02075986d69439cc4", + "sizeBytes": 7301 + }, + { + "path": "src/logger-proxy.ts", + "sha256": "4f2c835abfb19c182d555faf9bc4d4a940228047e3aa9369109a3ed08ee1a02c", + "sizeBytes": 4467 + }, + { + "path": "src/metrics/MetricsManager.ts", + "sha256": "262bbd37faddb352359be5effc2d420c2fb29a288ec237203ffac435b9bb681e", + "sizeBytes": 15911 + }, + { + "path": "src/metrics/ai-docs/AGENTS.md", + "sha256": "c46dd8ca33e12e62bf8df6c80e7fbdbe9ead19a2bd4d49955ec801a46ada58a8", + "sizeBytes": 13742 + }, + { + "path": "src/metrics/ai-docs/ARCHITECTURE.md", + "sha256": "e8088c65a0f5a760d325a30b8240d0110e53cf6ee967aff0c5ba914b2400a766", + "sizeBytes": 15141 + }, + { + "path": "src/metrics/ai-docs/metrics-spec.md", + "sha256": "77b15e47a63e8f5112ecd7bde8a2a8e34b83155b795dbc461e23bcc49c034bc2", + "sizeBytes": 54756 + }, + { + "path": "src/metrics/behavioral-events.ts", + "sha256": "80a5914088c58f2b123ba510e6310ff2cc9f189f0bcd9dd9e19c5ebe10f8fcaf", + "sizeBytes": 16492 + }, + { + "path": "src/metrics/constants.ts", + "sha256": "e52847609599e86bc4747da1b9a2c774f37ed08854cb44912c6cfe6aaa2b115b", + "sizeBytes": 13959 + }, + { + "path": "src/services/AddressBook.ts", + "sha256": "aeca0107bc43438d0c00fbb429a915a070b1ead9ed9afcfeab12a9ef7a7f88e5", + "sizeBytes": 9229 + }, + { + "path": "src/services/AnswerCallOnWebexService.ts", + "sha256": "8f10892a8103f6d9e3e22a8494a140df292f5a24b21b3ea4bf92e331879075ee", + "sizeBytes": 6762 + }, + { + "path": "src/services/ApiAiAssistant.ts", + "sha256": "5a6e37e464cf62fdba1b7d9977d9c3356704e5c177ddf407fd5c76f48f17775d", + "sizeBytes": 20934 + }, + { + "path": "src/services/EntryPoint.ts", + "sha256": "0bb34b0a5513d2bc2397c53b8f22fb5a8861747dc3c12a74b5e1c63cfd783346", + "sizeBytes": 7495 + }, + { + "path": "src/services/Queue.ts", + "sha256": "4c107a87ccfc254f8409332cbb90f3ff59b9702a20ffa5ec21ab696fbde6a097", + "sizeBytes": 9138 + }, + { + "path": "src/services/UserPreference.ts", + "sha256": "c8e69f8cf4449157572707bd7507411326e41b2463fb3c326ff31a3e127791e9", + "sizeBytes": 14996 + }, + { + "path": "src/services/WebCallingService.ts", + "sha256": "40ddf7067b1748ed52399b36d35848eaa12de40e3846a0f2bafade2251d4fc06", + "sizeBytes": 9980 + }, + { + "path": "src/services/WebexCrossClientService.ts", + "sha256": "fc63d6315b20063733f60da78ae7cf611a368486ee61d255307fe4fedc6f52fd", + "sizeBytes": 8648 + }, + { + "path": "src/services/WxAppTelephonyMercurySync.ts", + "sha256": "59ee788db62b91b9773190d3d3ee8c81a2f5806c278bf741dba0cc9e0d1c88ec", + "sizeBytes": 4194 + }, + { + "path": "src/services/agent/ai-docs/AGENTS.md", + "sha256": "c77816af953056b3c5613f9b7fe3a7df8b932b388ba57ec24253c1967c299990", + "sizeBytes": 6188 + }, + { + "path": "src/services/agent/ai-docs/ARCHITECTURE.md", + "sha256": "578651dcdb7aea8b4727d56304621207417ac1ab592afadd97bfdcaaef23fb8c", + "sizeBytes": 8221 + }, + { + "path": "src/services/agent/ai-docs/agent-spec.md", + "sha256": "dcc1cbd5910a495fd748faf6f9dfe67460ac6465e58356f0f20f4fb0b27e2803", + "sizeBytes": 22465 + }, + { + "path": "src/services/agent/index.ts", + "sha256": "5453dc4536c7c28f014dacec25eaf6a6c6be0c7605dc6a5efd1b7ce6e3c7db4a", + "sizeBytes": 4298 + }, + { + "path": "src/services/agent/types.ts", + "sha256": "1bdfbe380dc46a8c990f4f23aaf7529d369bca3973b90ab507caa4e54502fa81", + "sizeBytes": 14668 + }, + { + "path": "src/services/ai-docs/AGENTS.md", + "sha256": "23b0111e38334a1278f3ae320ecf89de69ee2cae85a4ce90afe81c33c0c13a5a", + "sizeBytes": 22326 + }, + { + "path": "src/services/ai-docs/services-spec.md", + "sha256": "914ca70a29ad7170dc4fecf92526db833c3462bc79d2faff47f68854f9175b83", + "sizeBytes": 37543 + }, + { + "path": "src/services/config/Util.ts", + "sha256": "aea2ff6f52fe7c923dc824a58cfcd10e69dcba7d888add7bfc5c2e65e0668088", + "sizeBytes": 8892 + }, + { + "path": "src/services/config/ai-docs/AGENTS.md", + "sha256": "4750ae61d42310f2e5b399fcb08a9d4efeb63f24ec8cfb648789f641ac5d5e08", + "sizeBytes": 11420 + }, + { + "path": "src/services/config/ai-docs/ARCHITECTURE.md", + "sha256": "bbba921bb3ac9f27b2a4497d63dbc1a3a58f299fa57d005427ebe52f46afdb04", + "sizeBytes": 15875 + }, + { + "path": "src/services/config/ai-docs/config-spec.md", + "sha256": "2eae907e12547dd1c1bfba9ee761e9a8e4131744fe272c5312bf1eb66acc9386", + "sizeBytes": 35502 + }, + { + "path": "src/services/config/constants.ts", + "sha256": "22fd18b97856376dc07fc097a38e3600bdbd7329f512edd5d995d881bc1400ac", + "sizeBytes": 10788 + }, + { + "path": "src/services/config/index.ts", + "sha256": "80633cb31bef5cd17a5d24185e2a7af8483aaad2ffd09c350b58a23245c4f788", + "sizeBytes": 29045 + }, + { + "path": "src/services/config/types.ts", + "sha256": "d23191694781f0efcf0560df0c838272c40cc304cb2b5fd1a346ec0b54891d9d", + "sizeBytes": 39469 + }, + { + "path": "src/services/constants.ts", + "sha256": "7217fcefc26217a2be9de94d039ecd77d96bea079ec6402c4d9b839f640f9cc0", + "sizeBytes": 3272 + }, + { + "path": "src/services/core/Err.ts", + "sha256": "9ac1702a45854afd1d41b2f6f2f78998f90840b043a8abfcc97afcea5205a90c", + "sizeBytes": 4087 + }, + { + "path": "src/services/core/GlobalTypes.ts", + "sha256": "17ab238b8a4fe260a31cc28e0fea86e77505c7bd89bc8ae619e3dca1519109db", + "sizeBytes": 1632 + }, + { + "path": "src/services/core/Utils.ts", + "sha256": "82aad7e5aca1296b2058c0a9de58516e449de28d822052f5027c9c1d22a89edd", + "sizeBytes": 15847 + }, + { + "path": "src/services/core/WebexRequest.ts", + "sha256": "38f19811c01ae8fd7f1452d57506493fddd8c92bcfcadf22c0e11d4dc7249626", + "sizeBytes": 2948 + }, + { + "path": "src/services/core/ai-docs/AGENTS.md", + "sha256": "3c470795da6833be67a34cf54c3e671f09a096de5935b0c04b75630ccb8ed52e", + "sizeBytes": 12747 + }, + { + "path": "src/services/core/ai-docs/ARCHITECTURE.md", + "sha256": "558c092ccfdaee719eda45e51c08e51cfad457d7dfc8cb075e930ea1819b5508", + "sizeBytes": 27799 + }, + { + "path": "src/services/core/ai-docs/core-spec.md", + "sha256": "369f5b11a100cff5c40f56af857c2f43d69fc886e1f0aff2af6c070b31554f17", + "sizeBytes": 35106 + }, + { + "path": "src/services/core/aqm-reqs.ts", + "sha256": "24a0c8e0fcd782edcbb2957e6dfbc8642fcb6c46579947eba99c866c8cb9cf2a", + "sizeBytes": 11821 + }, + { + "path": "src/services/core/constants.ts", + "sha256": "2c7eaacee2ebb26ae15002f9a06bd9fb232379e719b45a4d83a193dc4ba865de", + "sizeBytes": 3144 + }, + { + "path": "src/services/core/types.ts", + "sha256": "562c221ff04d8ecf7c6b144acc5342fadb41e349939c959a6a3c38d0a6594200", + "sizeBytes": 2291 + }, + { + "path": "src/services/core/websocket/WebSocketManager.ts", + "sha256": "774ca8d702f1220aef1d93549197b17029de9f98def7c253749a189bc42e496e", + "sizeBytes": 7971 + }, + { + "path": "src/services/core/websocket/connection-service.ts", + "sha256": "c32334cf10205fe6e0047059f1b4b1fc997d5663c4b76711d1b7f1a1a7ac514f", + "sizeBytes": 4803 + }, + { + "path": "src/services/core/websocket/keepalive.worker.js", + "sha256": "702bcdc234186a0a749e2baac46b06ac09b2e5516f0f860d25e7aeb01cb1eefc", + "sizeBytes": 2430 + }, + { + "path": "src/services/core/websocket/types.ts", + "sha256": "234a34e7148c7b1043e51a9fc4894fb9259dfcd54e312c8c17231a56cf9be182", + "sizeBytes": 1407 + }, + { + "path": "src/services/index.ts", + "sha256": "edff51b2e1304ee814d8647809c6cb116d9a0a7734ff994920eb09c55ead30d9", + "sizeBytes": 3032 + }, + { + "path": "src/services/task/AutoWrapup.ts", + "sha256": "4f118534176be6f321b23f08c8f604eb44af6870dbdc76c39ffc542a45219414", + "sizeBytes": 2179 + }, + { + "path": "src/services/task/Task.ts", + "sha256": "1301ba09d1463ad4936bffd3a0568a3ee0648027a18ab048912db3cab1f5fdd9", + "sizeBytes": 38585 + }, + { + "path": "src/services/task/TaskFactory.ts", + "sha256": "37c044d0f974bf502073568b74d004c879c69641c5c7bd7f88b582f78379b86c", + "sizeBytes": 2152 + }, + { + "path": "src/services/task/TaskManager.ts", + "sha256": "d34e7c7dd350b45a85bb781c5b4aba08bab92d55f284669d4d40111ce23de60b", + "sizeBytes": 65120 + }, + { + "path": "src/services/task/TaskUtils.ts", + "sha256": "266bf7ccbd0cd75368d74d795026ac3a5ad943afdf7d82a00c482ff7aa9f2444", + "sizeBytes": 18629 + }, + { + "path": "src/services/task/WebexCallingUtils.ts", + "sha256": "5cce4a0a24367c55018144b1a8a9f01b4dbefd6e9f924c9d5bb83f7ce8bce2c1", + "sizeBytes": 3870 + }, + { + "path": "src/services/task/ai-docs/AGENTS.md", + "sha256": "61037202c6f7c4579ddc2fecb524f64fa4d3eab87be127eb501db15dd939bce0", + "sizeBytes": 16129 + }, + { + "path": "src/services/task/ai-docs/ARCHITECTURE.md", + "sha256": "4b2d8108d97cfb160b81ecdcc213e2b192eeda30cfefb5dd670f9a1d1415c575", + "sizeBytes": 17738 + }, + { + "path": "src/services/task/ai-docs/task-spec.md", + "sha256": "669bd14b3bac43cdb233ca29e2d843f1c2126b6d54ecb368d3bd9407d3b687ec", + "sizeBytes": 74845 + }, + { + "path": "src/services/task/constants.ts", + "sha256": "1900e2ee633f6247c566f5c916706363ddb36c06094c00848273c8be105cd8a2", + "sizeBytes": 4530 + }, + { + "path": "src/services/task/contact.ts", + "sha256": "5eaa165770c0e724bea2557f3c9256008667e53ac109785511ed27166b10719b", + "sizeBytes": 15168 + }, + { + "path": "src/services/task/dialer.ts", + "sha256": "b131827b5c6c925a363ee0959073d65cc5c6e434542520ef36c35737716a0a68", + "sizeBytes": 6369 + }, + { + "path": "src/services/task/digital/Digital.ts", + "sha256": "6ac11f6984db47d8609cbca4493765b587a761b27b8a0f982ffdea126a1a0e54", + "sizeBytes": 2960 + }, + { + "path": "src/services/task/state-machine/TaskStateMachine.ts", + "sha256": "b78049e61c9fc71ba6fd45df6341c3fc6a89c744b2681210145f523c174f12e9", + "sizeBytes": 51260 + }, + { + "path": "src/services/task/state-machine/actions.ts", + "sha256": "b07208d754e7d45c49c61678273cda521d709d30321d891b5cc1fc111ff9752f", + "sizeBytes": 25202 + }, + { + "path": "src/services/task/state-machine/ai-docs/AGENTS.md", + "sha256": "ffd3c4d673b6a90cbdaeeb633650c9f5304471a2b4f3db53e6c56603277ac2b9", + "sizeBytes": 13933 + }, + { + "path": "src/services/task/state-machine/ai-docs/ARCHITECTURE.md", + "sha256": "cd8fb1eecc53eaab0c0568af6d869b7967fab485bf708627250723ec45b8f183", + "sizeBytes": 60466 + }, + { + "path": "src/services/task/state-machine/ai-docs/task-state-machine-spec.md", + "sha256": "3b602fa744e6ebb84939754af6156af466a888ff99397772707675d25c79583d", + "sizeBytes": 101856 + }, + { + "path": "src/services/task/state-machine/constants.ts", + "sha256": "21af87ec6357226edb2fe7620c3e86e86aac62675053661e692f0d4443fad688", + "sizeBytes": 5071 + }, + { + "path": "src/services/task/state-machine/guards.ts", + "sha256": "9138793e3ddb546ba7afbe65198cde084bc0b5c7cd0a1bc5aef0ea7b4bf6e227", + "sizeBytes": 16352 + }, + { + "path": "src/services/task/state-machine/index.ts", + "sha256": "03eaa50787a5251b11cfdd6836baf2355c954bfcbf3711216eca2e8e9b001de7", + "sizeBytes": 657 + }, + { + "path": "src/services/task/state-machine/types.ts", + "sha256": "17a16cf2fae57edc98dd2fad8551fb673bfe91e9479e10010da4ed75da8d9ea5", + "sizeBytes": 9732 + }, + { + "path": "src/services/task/state-machine/uiControlsComputer.ts", + "sha256": "b5f7622f9290f27735b46693cd7610e2b9eba8435185c876a4df438d47f980e8", + "sizeBytes": 43244 + }, + { + "path": "src/services/task/taskDataNormalizer.ts", + "sha256": "07d95bf5559bb64d2bbd873fca5d3ec89dd708c5c7a0069dbb76456bd9369136", + "sizeBytes": 3562 + }, + { + "path": "src/services/task/types.ts", + "sha256": "288a444c9c7fa8e45a4229efac88873734051107cea016d465bd0ad2a30eecf2", + "sizeBytes": 71567 + }, + { + "path": "src/services/task/voice/Voice.ts", + "sha256": "08f8ff362bbcde168cfa5591b26b6b414e7b10dd2303425960c3b55f5003e300", + "sizeBytes": 62166 + }, + { + "path": "src/services/task/voice/WebRTC.ts", + "sha256": "f4aa962b993a9ca454574768145e71fa3abdc33bacb326a9daa12da761a1ca74", + "sizeBytes": 5815 + }, + { + "path": "src/services/task/voice/wxAppVoiceMethods.ts", + "sha256": "54b6703b36e9b5387ec89ae5b8b74078a6e696d1537ac366c5ab35c0cda68da3", + "sizeBytes": 16037 + }, + { + "path": "src/services/wxAppDiagnosticLogging.ts", + "sha256": "be232c63e24c4df012aee132d5cf854ed5ba6a206ad2e68bc84a49a6bbe1c6f8", + "sizeBytes": 5136 + }, + { + "path": "src/services/wxAppTelephonyUtils.ts", + "sha256": "3718842f75dd52c6c7456f86f7533ec4425a0fb97966c6d30935a555f6d7aba1", + "sizeBytes": 517 + }, + { + "path": "src/types.ts", + "sha256": "e607077860e58090d925acf57b7d0c38dabf0b5459b05b83afe621107d7efafe", + "sizeBytes": 32297 + }, + { + "path": "src/utils/AGENTS.md", + "sha256": "def06884cfcba69078610868294d67885fc8d0983b7a7e238ad50248eb138b42", + "sizeBytes": 10557 + }, + { + "path": "src/utils/PageCache.ts", + "sha256": "1046d000f65ed5a7b3735dd2eb31c571979c600280bba2cfb505e51b0f6a9315", + "sizeBytes": 7096 + }, + { + "path": "src/utils/ai-docs/utils-spec.md", + "sha256": "3b693ff28dbe8b50ff57d9c1361e9404b0e179406edefda150814a834c3e36e3", + "sizeBytes": 21688 + }, + { + "path": "src/webex-config.ts", + "sha256": "94cd164b5631dbbf4c994ac34b84eea4bbd8be5e6d7878f3ff6aa4f3f1f52cf9", + "sizeBytes": 1522 + }, + { + "path": "src/webex.js", + "sha256": "1a1a8c4781206d007ab0e8c11b1a0ce7b8862d0ac5b8b10264c37dabf240706e", + "sizeBytes": 4548 + }, + { + "path": "test/unit/fixtures/aiSummaryTestUtils.ts", + "sha256": "a9ae5cddf43f359c895f6ca4794182b5f608e061420c2b71b46c87daf9cf4675", + "sizeBytes": 1019 + }, + { + "path": "test/unit/spec/cc.ts", + "sha256": "1c1dbac955512cf854c7bd9a5e4b1cdad086e11ea11423d83051e0b8496598df", + "sizeBytes": 202258 + }, + { + "path": "test/unit/spec/index.ts", + "sha256": "f87c3fc8824c9c5b71c1abc4cd9a23ace3ab50f0abe233b25e84f712c647d977", + "sizeBytes": 28185 + }, + { + "path": "test/unit/spec/logger-proxy.ts", + "sha256": "6c8eab4546ef17875b4d611b2632a4d1e2017ef8e3062d3f4cbe9be15eb1595e", + "sizeBytes": 2346 + }, + { + "path": "test/unit/spec/metrics/MetricsManager.ts", + "sha256": "7fc0b68615ee3d9a16863b5d82d485d9f301c8a12503a9bfe9fdde2991010c8f", + "sizeBytes": 18815 + }, + { + "path": "test/unit/spec/metrics/behavioral-events.ts", + "sha256": "9ff35f1f6780cb47856f70a7228f68b0e0ace2770979435c5d215f52982727a6", + "sizeBytes": 6760 + }, + { + "path": "test/unit/spec/services/AddressBook.ts", + "sha256": "38c05a0112e4431a4d834007fde150cab59c1a5850a1a834af9bed60197c0597", + "sizeBytes": 11980 + }, + { + "path": "test/unit/spec/services/AnswerCallOnWebexService.ts", + "sha256": "4e9537cd3f44a98f80746ec554db78ddc118d80ca70e8c4942087d3b48e2b686", + "sizeBytes": 8617 + }, + { + "path": "test/unit/spec/services/ApiAiAssistant.ts", + "sha256": "e388bee6bcfc5e88c563d777895b6b9412824c89749832807bc96c898ed0e018", + "sizeBytes": 18925 + }, + { + "path": "test/unit/spec/services/EntryPoint.ts", + "sha256": "d697e154a55a352926334df637f06264285a6b4a268478cda08d2c302ce8c998", + "sizeBytes": 10808 + }, + { + "path": "test/unit/spec/services/Queue.ts", + "sha256": "bb7615720c0e947cb3c68f194648c73b0fec7df203915a842d1cda93fcfc124c", + "sizeBytes": 15940 + }, + { + "path": "test/unit/spec/services/UserPreference.ts", + "sha256": "bbc1231a1b531e95d29952e56a7e597cb1fb8cecf3a29ce0930c5b86e5c50bf0", + "sizeBytes": 12914 + }, + { + "path": "test/unit/spec/services/WebCallingService.ts", + "sha256": "711172d6ce2ce2774c7edda9a9a6ce3f545fc47549219b84c548483ad2632927", + "sizeBytes": 13550 + }, + { + "path": "test/unit/spec/services/WebexCrossClientService.ts", + "sha256": "67ef1137f10734032476d8ebc61dc11a281454cdbcd57fc301582a1d13068695", + "sizeBytes": 15313 + }, + { + "path": "test/unit/spec/services/WxAppTelephonyMercurySync.ts", + "sha256": "deb283a61fe90077c4bf09d5a9616edb69a7a9527cd35d9470c39aa4c81980b5", + "sizeBytes": 5116 + }, + { + "path": "test/unit/spec/services/agent/index.ts", + "sha256": "460e7c9a7a2342b466776daa9290267d65aabdea78838c7f110c6aaacdf96653", + "sizeBytes": 2133 + }, + { + "path": "test/unit/spec/services/config/Util.ts", + "sha256": "ac1b26e5ef1ac2b67af0ccb6c7d99feb50aac87f90fc45bd63b54f32430f116f", + "sizeBytes": 3740 + }, + { + "path": "test/unit/spec/services/config/index.ts", + "sha256": "23cfca78a42e083b5c89e2d98fea55f1c8c9ea4d8d1b14b43657ecdbac475b43", + "sizeBytes": 53616 + }, + { + "path": "test/unit/spec/services/core/Utils.ts", + "sha256": "cf6afce7a58d0430011daf88dd1c6ad5f7e3c55491d4f4b2100cca79570bd98e", + "sizeBytes": 32241 + }, + { + "path": "test/unit/spec/services/core/WebexRequest.ts", + "sha256": "217bb377d0ccbbe916a88f54712aaee4cb25f9872a19e2a9303a866a19dea55d", + "sizeBytes": 4614 + }, + { + "path": "test/unit/spec/services/core/aqm-reqs.ts", + "sha256": "6da445eb55b9f5665c0c80333c59fb6b1234767f6ed87fa1f4547b6c1eac992b", + "sizeBytes": 20096 + }, + { + "path": "test/unit/spec/services/core/websocket/WebSocketManager.ts", + "sha256": "bebba86cb2645ce7d1b5d913b5f1cb8fbf77a055939aacfcebba382b94c3671b", + "sizeBytes": 15135 + }, + { + "path": "test/unit/spec/services/core/websocket/connection-service.ts", + "sha256": "133e9c9596416908e7902fd61ab7cfd9e905a55b4eee339e9ba704f6800fd3b7", + "sizeBytes": 6772 + }, + { + "path": "test/unit/spec/services/task/AutoWrapup.ts", + "sha256": "4437799af650bdc8965f12c61ccb363b498152b8eed5d58f1643a4ce0ede5823", + "sizeBytes": 1694 + }, + { + "path": "test/unit/spec/services/task/Task.ts", + "sha256": "7a53260add57857d9f5160a184d55b8ecfac57cabe59873f362ccc683e2ff574", + "sizeBytes": 100100 + }, + { + "path": "test/unit/spec/services/task/TaskFactory.ts", + "sha256": "2eb3d386383a10800a3d1eb658f24c1f076c04f06e7887e8d7b0c1e8f6dc4b82", + "sizeBytes": 3804 + }, + { + "path": "test/unit/spec/services/task/TaskManager.ts", + "sha256": "201bccc8169081ddea280c51173cd56e030e76b94285c359f42a7643bc7e664a", + "sizeBytes": 224326 + }, + { + "path": "test/unit/spec/services/task/TaskUtils.ts", + "sha256": "7327930e7fa250e1be68ddf5ff11b0a1a0ab5698ba9fc4dbba37d391b4793a01", + "sizeBytes": 26964 + }, + { + "path": "test/unit/spec/services/task/WebexCallingUtils.ts", + "sha256": "2518b911842b5ca8f566b3168b7dc6cfbc0c5bfbca2cb86a2f22df6e338e3a23", + "sizeBytes": 5052 + }, + { + "path": "test/unit/spec/services/task/contact.ts", + "sha256": "17b338dff940d52d698962136a83c94c88cd20dad9ca770c5ebff624699f3c48", + "sizeBytes": 6829 + }, + { + "path": "test/unit/spec/services/task/dialer.ts", + "sha256": "b991343c28d4878c50ef9492a6e748deed05114d2928a9da441cea3102ad154d", + "sizeBytes": 12986 + }, + { + "path": "test/unit/spec/services/task/digital/Digital.ts", + "sha256": "c7f2cd6f4d0c6f83350c2de719d36849287cb3ca4011712b61d429dc73b5715f", + "sizeBytes": 4147 + }, + { + "path": "test/unit/spec/services/task/state-machine/TaskStateMachine.ts", + "sha256": "991cd67fb8c949eb1fad3bda42c7098b2f50d896466dd17d333d0270170a29b1", + "sizeBytes": 147962 + }, + { + "path": "test/unit/spec/services/task/state-machine/guards.ts", + "sha256": "431316bd9398e665b96a574f906783822e7587de533c409b08d42aaf3b201297", + "sizeBytes": 28901 + }, + { + "path": "test/unit/spec/services/task/state-machine/types.ts", + "sha256": "cbe3de741efb546ed07feb4bf58b5f2db669104d3b7dbc8a07b703f78efa265e", + "sizeBytes": 793 + }, + { + "path": "test/unit/spec/services/task/state-machine/uiControlsComputer.ts", + "sha256": "eb92fa595bbe57d4e44385697c1c30cea797d628ceafb9b04a5164864e14a672", + "sizeBytes": 107863 + }, + { + "path": "test/unit/spec/services/task/taskTestUtils.ts", + "sha256": "a0ceb80198a2d203eab0f534012da3c385c42257d9a839ea1cf988d3e3198da2", + "sizeBytes": 2465 + }, + { + "path": "test/unit/spec/services/task/voice/Voice.ts", + "sha256": "d3b20e9a36b071a21d56c60ac10fd527cf428071045b8c78a596c3b89e278a38", + "sizeBytes": 84108 + }, + { + "path": "test/unit/spec/services/task/voice/WebRTC.ts", + "sha256": "d9c40851993e991be08a60bd8750512b1a761af1e5dda2eb0bfec32c292eb0f2", + "sizeBytes": 9041 + }, + { + "path": "test/unit/spec/services/task/voice/wxAppVoiceMethods.ts", + "sha256": "06ddd0895310ecb10f5df5c2e8d6232bc4c85920427d4183175675a13b9b8d83", + "sizeBytes": 19192 + }, + { + "path": "test/unit/spec/services/wxAppDiagnosticLogging.ts", + "sha256": "bf42c719b0ab7b25f36fb3e3683c912c2b111bb970540c58374a21964efffaf5", + "sizeBytes": 4118 + }, + { + "path": "test/unit/spec/wellnessSampleUtils.ts", + "sha256": "26c6481caf68e56c47d6ab726b255e4ed22bf5868924e3844d95ddd78986e777", + "sizeBytes": 9586 + }, + { + "path": "tsconfig.json", + "sha256": "d00932e411051043c9e92ab16b90d6ec5d93a638a3b2876f4fc4e88b9d4788d7", + "sizeBytes": 82 + }, + { + "path": "typedoc.json", + "sha256": "fe33af679b041092b8d2e258e571fac8db3cf901670f99fb0c2e80324053871b", + "sizeBytes": 1107 + }, + { + "path": "typedoc.md", + "sha256": "34e60c24935da8b734f606db74c7042a698cf7c75e021115bf617736ed487725", + "sizeBytes": 7685 + }, + { + "path": "umd/contact-center.min.js", + "sha256": "ff4edf27f7834438331df4b33bd9504118e981ed3e1f87ec1dbea75ca8ab836c", + "sizeBytes": 1685693 + }, + { + "path": "umd/contact-center.min.js.map", + "sha256": "191eb014749110a0bf0a28e1126e1730b9e8fc911912a8e2d90d43719a1f13d7", + "sizeBytes": 6282659 + } + ] + }, + "declarations": [ + { + "path": "dist/types/cc.d.ts", + "sha256": "b0f696741044226e145189b8e77516319afc74a2bac0f401d617bbfc2b3a5755", + "sizeBytes": 37373 + }, + { + "path": "dist/types/config.d.ts", + "sha256": "75939a49084e42336924b31a3db8dd04865b422921c19bb7589067d7b0e06cbe", + "sizeBytes": 2102 + }, + { + "path": "dist/types/constants.d.ts", + "sha256": "45f82b556af409401cb413f7b6f3092d12e615bfd7ddb839e788373c45c4b9c5", + "sizeBytes": 3519 + }, + { + "path": "dist/types/index.d.ts", + "sha256": "af7d60ef4b784a3f57c304a4eb783ce07c42b188bfbd9c8acdb169f40472e39a", + "sizeBytes": 6791 + }, + { + "path": "dist/types/logger-proxy.d.ts", + "sha256": "3642a40846bfbdd54550b235c51172dd3df1c240bd6601a0f0c92e96be5a6e79", + "sizeBytes": 2513 + }, + { + "path": "dist/types/metrics/MetricsManager.d.ts", + "sha256": "5c8e3c9f9f8ee4c33f4736d419ed1ea28dc41d185ff702beb09cd770ff91a2c3", + "sizeBytes": 7965 + }, + { + "path": "dist/types/metrics/behavioral-events.d.ts", + "sha256": "98096ed7bad87b45acd5ddad765ffeb3a9a9c9b20141365bcaa6035f3286b709", + "sizeBytes": 1207 + }, + { + "path": "dist/types/metrics/constants.d.ts", + "sha256": "1abf0041764d1140285b3e3c4e871b356b03065b04219de5ae127a50942e38a7", + "sizeBytes": 14905 + }, + { + "path": "dist/types/services/AddressBook.d.ts", + "sha256": "33c1d50bbb6f0b5be44ea1db1c689c28fb8a50a6661b2a080ad8997075c75835", + "sizeBytes": 2576 + }, + { + "path": "dist/types/services/AnswerCallOnWebexService.d.ts", + "sha256": "67ce5b96770b43219865517805ea9417963c2a7fc0ed3fae8a64c52012b0a5ab", + "sizeBytes": 1219 + }, + { + "path": "dist/types/services/ApiAiAssistant.d.ts", + "sha256": "ca8d5aac68d6e43c5903d19c8f32e2d217f2d7c8f44b748558dca828b4f499d0", + "sizeBytes": 4070 + }, + { + "path": "dist/types/services/EntryPoint.d.ts", + "sha256": "3f40e060e67889f3787abc8cebd884eca256fc3afad60d901e96dda82972f329", + "sizeBytes": 2369 + }, + { + "path": "dist/types/services/Queue.d.ts", + "sha256": "bdfc7551744cf9ac47d3ed0afb74e1159b4c0f7092570f2232b044b79b960426", + "sizeBytes": 2488 + }, + { + "path": "dist/types/services/UserPreference.d.ts", + "sha256": "11df3284409ebd6e2eb22b7bb378cdaff7f05ba26100125202d1200d45f49f69", + "sizeBytes": 4678 + }, + { + "path": "dist/types/services/WebCallingService.d.ts", + "sha256": "8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881", + "sizeBytes": 11 + }, + { + "path": "dist/types/services/WebexCrossClientService.d.ts", + "sha256": "bed718030f5260c355bb6335887307f644af34fd1e1172ad704cafaf4b27eb62", + "sizeBytes": 1206 + }, + { + "path": "dist/types/services/WxAppTelephonyMercurySync.d.ts", + "sha256": "9a1cd9516b539835f37f04af6d5c7b5e623a81d397f53df956805ce97b6776e3", + "sizeBytes": 926 + }, + { + "path": "dist/types/services/agent/index.d.ts", + "sha256": "43461f28ca595d6793851a6119fcbea5d87483a5921b0ad380b0db73df74bb72", + "sizeBytes": 1314 + }, + { + "path": "dist/types/services/agent/types.d.ts", + "sha256": "28bf51c66b0f8adfed045b4a1b7628509c0151dca412ea4838888af43574bd2a", + "sizeBytes": 15187 + }, + { + "path": "dist/types/services/config/Util.d.ts", + "sha256": "93fce78b6632cd5135182783a7f4e9184e1c4380525f5f33168899282c1f4302", + "sizeBytes": 716 + }, + { + "path": "dist/types/services/config/constants.d.ts", + "sha256": "7a9a12914d33e23bbb999d0e450ffada36c4f81c561777a3657b1363df057f38", + "sizeBytes": 9250 + }, + { + "path": "dist/types/services/config/index.d.ts", + "sha256": "96960b7fbb800c2aae86a7874f19616090815ad27dbe280a177ea8f664528dd2", + "sizeBytes": 10065 + }, + { + "path": "dist/types/services/config/types.d.ts", + "sha256": "79f50aa4f56c2a21403d0dad0908c9a3284f4876008d5e09a978c8eefb958832", + "sizeBytes": 52164 + }, + { + "path": "dist/types/services/constants.d.ts", + "sha256": "56b21b5e6624ab87dbb682f33896d7204ecad31ba0b78f808a07cdf39c148fb0", + "sizeBytes": 2727 + }, + { + "path": "dist/types/services/core/Err.d.ts", + "sha256": "68ae23a8fe9595d906d3b7ea6169eb76138444476587cb59378af34d57b2147c", + "sizeBytes": 3732 + }, + { + "path": "dist/types/services/core/GlobalTypes.d.ts", + "sha256": "09f8719247f439f06f25e9476ef0bd4603638b778a6acd88aa8de6e261459c03", + "sizeBytes": 1691 + }, + { + "path": "dist/types/services/core/Utils.d.ts", + "sha256": "4b70545a6cf31657273e54567e163001ef55d7d23a8379afff80f70a8b7816ac", + "sizeBytes": 5974 + }, + { + "path": "dist/types/services/core/WebexRequest.d.ts", + "sha256": "aca024101dee4bf70cb4c4d4307517e663fb99c52826ea17ee086799f21fbd29", + "sizeBytes": 750 + }, + { + "path": "dist/types/services/core/aqm-reqs.d.ts", + "sha256": "59ad8809b01105f0bd0f57954e2090c6b1ee50e9543a32ee8aecb4724a410eff", + "sizeBytes": 2573 + }, + { + "path": "dist/types/services/core/constants.d.ts", + "sha256": "685079dd3b3e98f763f327a9373bd21d3bb919c5d219028ea0d9e7712cfdaaa5", + "sizeBytes": 2862 + }, + { + "path": "dist/types/services/core/types.d.ts", + "sha256": "943cf2a0ecd40a341f4538da5f7e4aaf6d809cbf1f804233ba125b108fb799b0", + "sizeBytes": 1587 + }, + { + "path": "dist/types/services/core/websocket/WebSocketManager.d.ts", + "sha256": "0e84ccf12cb785b15ace725568ac542c81f4bd5a997b5d025b39fe30194d8d36", + "sizeBytes": 1279 + }, + { + "path": "dist/types/services/core/websocket/connection-service.d.ts", + "sha256": "10738b3a4dd160c07f9307e7661937b98f5883d333975a349bbe9841171f3dc1", + "sizeBytes": 919 + }, + { + "path": "dist/types/services/core/websocket/keepalive.worker.d.ts", + "sha256": "96970057a5e816793c1cf811f1b2024a6183552849155e8d5b76254f2cb3f554", + "sizeBytes": 2471 + }, + { + "path": "dist/types/services/core/websocket/types.d.ts", + "sha256": "973044bd59d7045a40b2e6e1bfe5ef8490ebf33df4516634a149bc69f0bf79db", + "sizeBytes": 1420 + }, + { + "path": "dist/types/services/index.d.ts", + "sha256": "3a2b1fdfe74be17f1d26ee3f8397aa4ae06ed804c35c9b5c3678132e9e97be88", + "sizeBytes": 2301 + }, + { + "path": "dist/types/services/task/AutoWrapup.d.ts", + "sha256": "f2d051ac4ce4f14d4fc592f5c6d74fd6d58e90f7d52a1363960534b6d46305d3", + "sizeBytes": 1282 + }, + { + "path": "dist/types/services/task/Task.d.ts", + "sha256": "3f11db2fa96a54877509a44b232587d31132055f92136a07165f79228bedaee2", + "sizeBytes": 8353 + }, + { + "path": "dist/types/services/task/TaskFactory.d.ts", + "sha256": "554a4657d6c1094450b9cd381a47b79d633cd7b47d7ef2f875eb56fe34591ab6", + "sizeBytes": 710 + }, + { + "path": "dist/types/services/task/TaskManager.d.ts", + "sha256": "8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881", + "sizeBytes": 11 + }, + { + "path": "dist/types/services/task/TaskUtils.d.ts", + "sha256": "07903739e6a2de1518ec85ad705d03c422f945246910c32a0235cddc0f12b7db", + "sizeBytes": 7618 + }, + { + "path": "dist/types/services/task/WebexCallingUtils.d.ts", + "sha256": "a2a5c92fc5e898d0db18363ae2858876d0eb7b9391b3f00f7adc26a5be4a91fe", + "sizeBytes": 776 + }, + { + "path": "dist/types/services/task/constants.d.ts", + "sha256": "19a8b6974786b57f8ca17eb55a0453bb692980262193e12fda2d9137be4dc997", + "sizeBytes": 4147 + }, + { + "path": "dist/types/services/task/contact.d.ts", + "sha256": "f87caf2000648279cfd70b0763feef3481e50b700f5abdf96236c80a6fb8f3ed", + "sizeBytes": 2673 + }, + { + "path": "dist/types/services/task/dialer.d.ts", + "sha256": "faad7b5f0ff455e1e3235be9751f0d636d918ddfe0ac67ff2d26b7553126a967", + "sizeBytes": 2981 + }, + { + "path": "dist/types/services/task/digital/Digital.d.ts", + "sha256": "1ba200a317ab4934bbc8f8c37fd3834b907b2423a3db00d51eab9e04106e0481", + "sizeBytes": 938 + }, + { + "path": "dist/types/services/task/state-machine/TaskStateMachine.d.ts", + "sha256": "b0d22c0f11b4323b8bf7d2f3fc46f09dfb5344ec2bab376a563b9c83135992ec", + "sizeBytes": 50281 + }, + { + "path": "dist/types/services/task/state-machine/actions.d.ts", + "sha256": "39a59133fbf4dfbfc240fe6c77bcc3d7c803985be923fb22ccb4496d5a787be7", + "sizeBytes": 771 + }, + { + "path": "dist/types/services/task/state-machine/constants.d.ts", + "sha256": "fbbd86fee6f11733e08a82b40161340094c58fe3aef3a455b7c265f16a6d84f0", + "sizeBytes": 4087 + }, + { + "path": "dist/types/services/task/state-machine/guards.d.ts", + "sha256": "13f1d149c04f1cdcb568328053dd87589f34044e1adab6dec8129df9b957239f", + "sizeBytes": 5202 + }, + { + "path": "dist/types/services/task/state-machine/index.d.ts", + "sha256": "07a48947c1eac6a55e94e619c94b67da338e834540e82bcb3dc8be7436f2c252", + "sizeBytes": 595 + }, + { + "path": "dist/types/services/task/state-machine/types.d.ts", + "sha256": "648729df89edaf7b52adc7ab6aca67e5c132c8c273944a133efb8d018e086f10", + "sizeBytes": 10424 + }, + { + "path": "dist/types/services/task/state-machine/uiControlsComputer.d.ts", + "sha256": "fabd6f257ade440f8da68340d5ec4bfb4771796c51d254aeb7a91b6341747a7b", + "sizeBytes": 532 + }, + { + "path": "dist/types/services/task/taskDataNormalizer.d.ts", + "sha256": "e45fb875558d7f11a32e52d3a956dc3a11277f3512249578a5e537000562c32d", + "sizeBytes": 510 + }, + { + "path": "dist/types/services/task/types.d.ts", + "sha256": "6ffa63a58c3f241e368de118ad2d9ab84c4052ac9e5879c39c78c23cb545eca9", + "sizeBytes": 72025 + }, + { + "path": "dist/types/services/task/voice/Voice.d.ts", + "sha256": "60dab7f21cbd4e615530cdadfa31d67d02ff251fc2ff205e9dbcf8635f8cdb82", + "sizeBytes": 8941 + }, + { + "path": "dist/types/services/task/voice/WebRTC.d.ts", + "sha256": "d20477a30b1801dd25b0ac2ed1a51d02eee11d74ab6412c398aeb1471daaa6c0", + "sizeBytes": 1756 + }, + { + "path": "dist/types/services/task/voice/wxAppVoiceMethods.d.ts", + "sha256": "fbeb48048ae87e1ea84b143241ce81045f7465bc9fe69fa889be769d97688a6e", + "sizeBytes": 3206 + }, + { + "path": "dist/types/services/wxAppDiagnosticLogging.d.ts", + "sha256": "8543b668aaa3247995e108a9959ff509a20f6aef0589e6175f68e05a58068d8b", + "sizeBytes": 2210 + }, + { + "path": "dist/types/services/wxAppTelephonyUtils.d.ts", + "sha256": "f6daf3b6a8988a7a00732f971dfd3d30054286ae54c9a566d3f0f223d9c0980a", + "sizeBytes": 246 + }, + { + "path": "dist/types/types.d.ts", + "sha256": "5ca805b513392fe59838d931f16eb39ae197dda6cb8fa803046ee2eeff7daad7", + "sizeBytes": 27755 + }, + { + "path": "dist/types/utils/PageCache.d.ts", + "sha256": "485255db6826391bdbad27cb20edd5411395321ef913eb21e46c91fc8b92a794", + "sizeBytes": 5471 + }, + { + "path": "dist/types/webex-config.d.ts", + "sha256": "afce94129a3caa54d735ace0af17032977572e2bb493fff5616abe7c5607d566", + "sizeBytes": 1453 + }, + { + "path": "dist/types/webex.d.ts", + "sha256": "cb1f8ec9e35e25ca5941703b3e8c53523925bd6f5bfcddbc5b9cf12d214b4ed9", + "sizeBytes": 231 + } + ], + "contract": { + "taskFeatureEnablementEvent": "task:featureEnablement", + "timeoutMs": 15000, + "postWrapUpSameTaskProbe": "fulfilled" + } + }, + "ux": { + "status": "sealed", + "resolutionRoot": ".", + "sealedAt": "2026-09-25T15:16:40.738Z", + "manifestSha256": "58d925e6d12c377589120f39fc170f1d80692d1901522fce0959b70e17f8c1b4", + "sourceManifestSha256": "a266410383fc773812cd0a3352313d8db5e3e3828d5e584aaeee1df42709f162", + "sourceIdentitySha256": "50073d2c12591ac4c682e8599c1ecae9f2797cae9ed85542bfebf068b5ebfecb", + "requirementSha256": "31061b7752dfc3e786905ee3a370527144aed1dc68641d641646dff0c1cf4c46", + "files": [ + { + "sourceId": "UX-001", + "kind": "sceneGraph", + "path": ".ccwidgets/midcall/figma_target_8061_880455_compact.json", + "sha256": "7183f1b874339e30a7c820179d63fabc500a1721f2206194a3fe9c4317880603", + "sizeBytes": 3624197 + }, + { + "sourceId": "UX-001", + "kind": "textContent", + "path": ".ccwidgets/midcall/figma_target_8061_880455_text.json", + "sha256": "804b66441dcd6a1ae3c43d3907bd255c3db2a1457539b3f7e3888d593320d666", + "sizeBytes": 89734 + }, + { + "sourceId": "UX-001", + "kind": "screenshot", + "path": ".ccwidgets/midcall/midcall.png", + "sha256": "5fee49d4111bb9b5a950e441821abc001aa99a39fdd6607b7169e2754654db0f", + "sizeBytes": 326905 + }, + { + "sourceId": "UX-002", + "kind": "sceneGraph", + "path": ".ccwidgets/postcall_generating/figma_target_5669_263180_compact.json", + "sha256": "2c1ade086aaf3bea8f9719f37b6e3a06ca1d7e93737cea58b368bd49efbffd4a", + "sizeBytes": 3452432 + }, + { + "sourceId": "UX-002", + "kind": "textContent", + "path": ".ccwidgets/postcall_generating/figma_target_5669_263180_text.json", + "sha256": "42921d9839a89fa948acd22563579a62472b14a8cc6121b2d0c7883d5b71f3ce", + "sizeBytes": 97177 + }, + { + "sourceId": "UX-002", + "kind": "screenshot", + "path": ".ccwidgets/postcall_generating/postcall_generating.png", + "sha256": "0de0aeb692ae83a922898009577b11d9d027b21c8653b05c947ba03e64bc8a21", + "sizeBytes": 170221 + }, + { + "sourceId": "UX-003", + "kind": "sceneGraph", + "path": ".ccwidgets/postcall_summary_edit/figma_target_5669_263754_compact.json", + "sha256": "9e9378e384df92e816a3c05e7faff8c800da2e1f6334e149a9522e24c837aa66", + "sizeBytes": 3899550 + }, + { + "sourceId": "UX-003", + "kind": "textContent", + "path": ".ccwidgets/postcall_summary_edit/figma_target_5669_263754_text.json", + "sha256": "58457c269bf1c9efff9f43e63a92f33a7b08f8f18ad43653403084b5cb46fc50", + "sizeBytes": 100090 + }, + { + "sourceId": "UX-003", + "kind": "screenshot", + "path": ".ccwidgets/postcall_summary_edit/postcall_summary_edit.png", + "sha256": "92b2ef6fe8473c96f422ebcfece7699d5e3360503921ff40e36b281e355ba6f1", + "sizeBytes": 313979 + }, + { + "sourceId": "UX-004", + "kind": "sceneGraph", + "path": ".ccwidgets/postcall_summary_hover_thumbsup/figma_target_5669_264384_compact.json", + "sha256": "e084f1942b911286d4eb58e7db3082130e808e12c5197fba565d069860966e13", + "sizeBytes": 1016889 + }, + { + "sourceId": "UX-004", + "kind": "textContent", + "path": ".ccwidgets/postcall_summary_hover_thumbsup/figma_target_5669_264384_text.json", + "sha256": "c4e6025190ce7f9f6b0b81fabb3eb80c4686f69f5fb4b2ac3769896d203000b1", + "sizeBytes": 15397 + }, + { + "sourceId": "UX-004", + "kind": "screenshot", + "path": ".ccwidgets/postcall_summary_hover_thumbsup/postcall_summary_hover_thumbsup.png", + "sha256": "3f9013a7a0772aeef98b55bf8e301957c74dca912d500e87dfeaf12f55fc50ca", + "sizeBytes": 328615 + }, + { + "sourceId": "UX-005", + "kind": "sceneGraph", + "path": ".ccwidgets/postcall_summary_selected_thumbsup/figma_target_5669_264503_compact.json", + "sha256": "4330bf697349d52df2493b1675343d5d27c60075eddc0a6a96258b29f0fa79fb", + "sizeBytes": 1016754 + }, + { + "sourceId": "UX-005", + "kind": "textContent", + "path": ".ccwidgets/postcall_summary_selected_thumbsup/figma_target_5669_264503_text.json", + "sha256": "6a08810d6325a5cb7290712373047c61ed1044e7ca6f708d3ab202f80ad4ba00", + "sizeBytes": 15397 + }, + { + "sourceId": "UX-005", + "kind": "screenshot", + "path": ".ccwidgets/postcall_summary_selected_thumbsup/postcall_summary_selected_thumbsup.png", + "sha256": "3628b688d6d84ee61ed1c22e1b77ee6aabb7026656ff8d9d15739b549bdeeeca", + "sizeBytes": 316346 + }, + { + "sourceId": "UX-006", + "kind": "sceneGraph", + "path": ".ccwidgets/postcall_summary_hover_thumbsdown/figma_target_5669_264622_compact.json", + "sha256": "41f147d96db6b2133f0f6994561cc577b0ae121ad9cf966cfdcc300f8b6d9f8e", + "sizeBytes": 1016863 + }, + { + "sourceId": "UX-006", + "kind": "textContent", + "path": ".ccwidgets/postcall_summary_hover_thumbsdown/figma_target_5669_264622_text.json", + "sha256": "f22fa4cce01a2cc8cb9d7ce334c261176fce196116ea190ef06d5f9c26242268", + "sizeBytes": 15397 + }, + { + "sourceId": "UX-006", + "kind": "screenshot", + "path": ".ccwidgets/postcall_summary_hover_thumbsdown/postcall_summary_hover_thumbsdown.png", + "sha256": "098c803e608ce2cc2f8c79ec12208a41ce5ba8ee2ffc66f2071801e2b4872641", + "sizeBytes": 353903 + }, + { + "sourceId": "UX-007", + "kind": "sceneGraph", + "path": ".ccwidgets/postcall_summary_selected_thumbsdown/figma_target_5669_264860_compact.json", + "sha256": "d78d2465ad40c21102a167d5cce976f1541e9a26e415f45069f91297c2ca6550", + "sizeBytes": 1016754 + }, + { + "sourceId": "UX-007", + "kind": "textContent", + "path": ".ccwidgets/postcall_summary_selected_thumbsdown/figma_target_5669_264860_text.json", + "sha256": "5dfe180ec35e38af7df336bbe1acba5b6a5e58c7c476a5fa7d60aacf96e1cc6d", + "sizeBytes": 15397 + }, + { + "sourceId": "UX-007", + "kind": "screenshot", + "path": ".ccwidgets/postcall_summary_selected_thumbsdown/postcall_summary_selected_thumbsdown.png", + "sha256": "7510c740d754cd9e612fc0cfb8ec080682f66a927e941c705cfe13d167920203", + "sizeBytes": 316496 + }, + { + "sourceId": "UX-008", + "kind": "sceneGraph", + "path": ".ccwidgets/postcall_summary_hover_copy/figma_target_4920_216835_compact.json", + "sha256": "40d15e6e6e951c6ff05d451add94cde1e2a6ed1517489ba3c7153d104631eb61", + "sizeBytes": 1016801 + }, + { + "sourceId": "UX-008", + "kind": "textContent", + "path": ".ccwidgets/postcall_summary_hover_copy/figma_target_4920_216835_text.json", + "sha256": "15756650eb613cc81bfdc0d5aaa8f75f7e2009c66a79c28dd108ad39aab0caf7", + "sizeBytes": 15399 + }, + { + "sourceId": "UX-008", + "kind": "screenshot", + "path": ".ccwidgets/postcall_summary_hover_copy/postcall_summary_hover_copy.png", + "sha256": "3b1e78288b4316b4c1575f38112d924c319431f8f07bdcd88a11343a9fee6e21", + "sizeBytes": 402341 + }, + { + "sourceId": "UX-009", + "kind": "sceneGraph", + "path": ".ccwidgets/postcall_summary_selected_copy/figma_target_4920_216954_compact.json", + "sha256": "7b8b3f8148adb46ae59cf36ffdb63e9f81f0035f7d14a433840183df46926a7a", + "sizeBytes": 1016800 + }, + { + "sourceId": "UX-009", + "kind": "textContent", + "path": ".ccwidgets/postcall_summary_selected_copy/figma_target_4920_216954_text.json", + "sha256": "059a61503ef5c9eb452b7c03478bb94387143f9d2d84bc0bb433e18b8e003e33", + "sizeBytes": 15399 + }, + { + "sourceId": "UX-009", + "kind": "screenshot", + "path": ".ccwidgets/postcall_summary_selected_copy/postcall_summary_selected_copy.png", + "sha256": "71e19e3be8da9045fce78e674006cd8963bd95f15642ff6a3884686db5c5e4b1", + "sizeBytes": 307333 + }, + { + "sourceId": "UX-010", + "kind": "sceneGraph", + "path": ".ccwidgets/postcall_summary_error/figma_target_5669_264979_compact.json", + "sha256": "3ab596f96b5a49eb56822816be31f9d4f739bf79f4a50ad09521ccfc491b8de7", + "sizeBytes": 3383665 + }, + { + "sourceId": "UX-010", + "kind": "textContent", + "path": ".ccwidgets/postcall_summary_error/figma_target_5669_264979_text.json", + "sha256": "90de99f4579e9eacadf068fa6fb48eff70eadad76e97d342094363b22fe0dbaa", + "sizeBytes": 95936 + }, + { + "sourceId": "UX-010", + "kind": "screenshot", + "path": ".ccwidgets/postcall_summary_error/postcall_summary_error.png", + "sha256": "6d54cafe81f3693a16017e19adff9aaaf4a1adbae67b779a3f49d6c1fda15ff5", + "sizeBytes": 202810 + } + ] + } +} diff --git a/package.json b/package.json index 52fa13a51..287736a21 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@types/babel__plugin-transform-runtime": "^7.9.5", "@webex/cli-tools": "0.0.0-next.2", "@webex/package-tools": "0.0.0-next.6", + "axe-core": "4.10.2", "babel-jest": "^29.7.0", "crypto-browserify": "^3.12.1", "css-loader": "^7.1.2", @@ -38,6 +39,7 @@ "sass": "^1.83.1", "sass-loader": "^16.0.4", "semantic-release": "^24.2.0", + "semver": "^7.7.3", "stream-browserify": "^3.0.0", "style-loader": "^4.0.0", "typescript": "^5.6.3", @@ -48,7 +50,8 @@ }, "resolutions": { "@momentum-design/components": "0.133.23", - "@momentum-design/icons": "0.17.0" + "@momentum-design/icons": "0.17.0", + "@webex/contact-center": "file:./vendor/contact-center-cc-summaries.tgz" }, "scripts": { "clean": "yarn workspaces foreach --all --topological --parallel run clean && rm -rf node_modules", @@ -57,7 +60,7 @@ "test:e2e": "yarn playwright test", "test:styles": "yarn workspaces foreach --all --exclude webex-widgets run test:styles", "test:tooling": "NODE_ENV=test jest --coverage", - "test:cc-widgets": "yarn workspaces foreach --all --exclude webex-widgets --exclude samples-cc-wc-app --exclude samples-cc-react-app run test:unit", + "test:cc-widgets": "yarn workspaces foreach --all --exclude webex-widgets --exclude @webex/test-fixtures --exclude samples-meeting-app --exclude samples-cc-wc-app --exclude samples-cc-react-app run test:unit", "test:meetings-widget": "yarn workspaces foreach --all --verbose --include @webex/widgets run test:unit", "build:dev": "NODE_ENV=development yarn build", "build:prod": "NODE_ENV=production yarn build:serial", diff --git a/packages/contact-center/ai-assistant/ai-docs/ai-assistant-spec.md b/packages/contact-center/ai-assistant/ai-docs/ai-assistant-spec.md index 4b49fc76b..28c4f9787 100644 --- a/packages/contact-center/ai-assistant/ai-docs/ai-assistant-spec.md +++ b/packages/contact-center/ai-assistant/ai-docs/ai-assistant-spec.md @@ -11,7 +11,7 @@ | Doc kind | Module spec | | Coverage score | Pending coverage assessment | | Generated from | `module-spec` @ SDLC template library `0.1.0-draft` | -| generated_by / approved_by / updated_at | generated_by `ai-assistant feature work` / approved_by `pending` / updated_at `2026-07-29` | +| generated_by / approved_by / updated_at | generated_by `ai-assistant feature work` / approved_by `pending` / updated_at `2026-09-24` | | Validation status | not-run | ## Evidence Rules @@ -32,8 +32,8 @@ behavior), then `src/ai-assistant.types.ts` (public props). ## Purpose / Responsibility Owns: assistant chrome state, the real-time assist request lifecycle, chat transcript assembly from -store payloads, and forwarding agent feedback actions to the SDK. Does NOT own: card rendering, panel -markup, SDK transport, or the store's `realTimeAssist` event plumbing. +store payloads, receiving-agent AI summary view projection, and forwarding agent feedback actions to the +store. Does NOT own: card rendering, panel markup, SDK transport, or the store's event plumbing. ## Stack TypeScript 5.6.3, React (peer `>=18.3.1`) function components + hooks, MobX via `mobx-react-lite` @@ -93,12 +93,15 @@ packages/contact-center/ai-assistant/ | `ai-assistant-R-008` | Like / dislike / copy call `sendRealTimeAssistanceUserAction` and return its promise; the card marks the control as selected only after that promise resolves, and a rejection is logged and leaves the control untouched. | The UI must not claim feedback was recorded when it was not. | `src/helper.ts` (`handleRealTimeAssistAction`), `packages/contact-center/cc-components/src/components/AIAssistant/AdaptiveCardRenderer/adaptive-card-renderer.tsx` (`emitUserAction`) | `tests/ai-assistant/feedback.tsx`, `packages/contact-center/cc-components/tests/components/AIAssistant/adaptive-card-renderer.test.tsx` | PRESENT | | `ai-assistant-R-009` | When the action cannot be sent (missing `adaptiveCardId`, ids, or SDK method), the reason is logged via `store.logger.warn` and the returned promise rejects. | A silent no-op leaves both agent and support blind. | `src/helper.ts` | `tests/ai-assistant/feedback.tsx` | PRESENT | | `ai-assistant-R-010` | A render failure is contained by the widget's `ErrorBoundary` and reported through `store.onErrorCallback('AIAssistant', error)`. | A crashing assistant must not take the agent desktop down. | `src/ai-assistant/index.tsx` | `tests/ai-assistant/index.tsx` | PRESENT | +| `ai-assistant-R-011` | Receiving-agent summary state is projected only from `store.getAISummaryViewModel('mid-call', 'receiver', currentTask)` and exposes `openReceiverSummary`, `recordReceiverSummaryCopied`, and `setReceiverSummaryFeedback`; those callbacks pass `currentTask` back to the store, and the package never calls summary SDK methods. Content projections carry the confirmed feedback, required feedback-pending/disabled booleans, and an owner branch key. `openReceiverSummary` returns the store's current-revision view-record result, and the panel selects the receiver branch only when that result accepts the reveal. | The AI Assistant panel can host receiver summaries while `@webex/cc-store` remains the sole summary SDK boundary and while real-time assist remains the default branch for ordinary launcher/restore/close and owner changes. | `src/ai-assistant/index.tsx`, `src/ai-assistant.types.ts` | `tests/ai-assistant/index.tsx`, `packages/contact-center/cc-components/tests/components/AIAssistant/ai-assistant.tsx` | PRESENT | ## Design Overview `src/ai-assistant/index.tsx` is a thin `observer` container: it reads the store, derives `isFeatureEnabled` from `featureFlags[REAL_TIME_ASSIST_FLAG]` and the active interaction from -`currentTask.data.interactionId`, resolves the deprecated callback alias, and hands everything to -`useAiAssistant`, whose result is spread straight onto `AIAssistantComponent`. +`currentTask.data.interactionId`, resolves the current receiver summary view through the store, passes +`currentTask` back on receiver summary view/copy/feedback callbacks so the store can re-check ownership, gates +receiver-branch activation on the store view-record result, and hands everything to `useAiAssistant`, whose +result is spread straight onto `AIAssistantComponent`. `src/helper.ts` holds two independent hooks composed by `useAiAssistant`: diff --git a/packages/contact-center/ai-assistant/src/ai-assistant.types.ts b/packages/contact-center/ai-assistant/src/ai-assistant.types.ts index 1a50f40fb..662b5e36a 100644 --- a/packages/contact-center/ai-assistant/src/ai-assistant.types.ts +++ b/packages/contact-center/ai-assistant/src/ai-assistant.types.ts @@ -1,4 +1,10 @@ -import type {RealTimeAssistPayload} from '@webex/cc-store'; +import type { + AISummaryActionType, + AISummaryContent, + AISummaryFeedback, + AISummaryFeedbackResult, + RealTimeAssistPayload, +} from '@webex/cc-store'; /** * Public props for the `AIAssistant` widget. All callbacks are optional — @@ -26,8 +32,41 @@ export interface UseAiAssistantInput extends IAIAssistantProps { agentId: string; isFeatureEnabled: boolean; realTimeAssist: RealTimeAssistPayload[]; + receiverSummary?: AIAssistantReceiverSummary; } +export type AIAssistantReceiverSummary = + | { + surface: 'content'; + branchKey: string; + content: Extract; + contentRevision: number; + actionType: AISummaryActionType; + selectedFeedback: AISummaryFeedback; + midCallFeedbackPending: boolean; + controlsDisabled: boolean; + openReceiverSummary: () => boolean; + recordReceiverSummaryCopied: (expectedRevision: number) => boolean; + setReceiverSummaryFeedback: ( + feedback: Exclude, + actionType: AISummaryActionType, + expectedRevision: number + ) => Promise; + } + | { + surface: 'unavailable' | 'generic-error'; + branchKey: string; + content?: never; + contentRevision?: never; + actionType?: never; + selectedFeedback?: never; + midCallFeedbackPending?: never; + controlsDisabled?: never; + recordReceiverSummaryCopied?: never; + setReceiverSummaryFeedback?: never; + openReceiverSummary: () => boolean; + }; + export type UseAIAssistantChromeInput = Pick< UseAiAssistantInput, 'onOpen' | 'onMinimize' | 'onRestore' | 'onClose' | 'onFullScreenToggle' diff --git a/packages/contact-center/ai-assistant/src/ai-assistant/index.tsx b/packages/contact-center/ai-assistant/src/ai-assistant/index.tsx index 3435ad694..4ea510328 100644 --- a/packages/contact-center/ai-assistant/src/ai-assistant/index.tsx +++ b/packages/contact-center/ai-assistant/src/ai-assistant/index.tsx @@ -1,17 +1,82 @@ import React from 'react'; import store from '@webex/cc-store'; +import type {AISummaryViewModel} from '@webex/cc-store'; import {observer} from 'mobx-react-lite'; import {ErrorBoundary} from 'react-error-boundary'; import {AIAssistantComponent} from '@webex/cc-components'; import {useAiAssistant, REAL_TIME_ASSIST_FLAG} from '../helper'; -import {IAIAssistantProps} from '../ai-assistant.types'; +import {AIAssistantReceiverSummary, IAIAssistantProps} from '../ai-assistant.types'; + +type ReceiverAISummaryViewModel = AISummaryViewModel & { + midCallFeedbackPending?: boolean; + ownerKey?: {interactionId: string; agentId: string; ownershipGeneration: number}; +}; + +type ScopedAISummaryStore = typeof store & { + recordAISummaryViewed( + kind: 'mid-call', + role: 'receiver', + expectedRevision: number, + task?: typeof store.currentTask + ): boolean; + recordAISummaryCopied( + kind: 'mid-call', + role: 'receiver', + expectedRevision: number, + task?: typeof store.currentTask + ): boolean; + setMidCallSummaryFeedback( + role: 'receiver', + feedback: 'like' | 'dislike', + actionType: 'CONSULT' | 'TRANSFER', + expectedRevision: number, + task?: typeof store.currentTask + ): Promise<{outcome: 'confirmed' | 'failed' | 'blocked' | 'stale'}>; +}; const AIAssistantInternal: React.FunctionComponent = observer((props) => { const {currentTask, agentId, agentProfile, featureFlags, realTimeAssist} = store; + const scopedStore = store as ScopedAISummaryStore; const interactionId = currentTask?.data?.interactionId; const isFeatureEnabled = Boolean(featureFlags?.[REAL_TIME_ASSIST_FLAG]); const activeRealTimeAssist = interactionId ? realTimeAssist?.[interactionId] || [] : []; + const receiverView = store.getAISummaryViewModel?.('mid-call', 'receiver', currentTask) as + | ReceiverAISummaryViewModel + | undefined; + const midCallFeedbackPending = receiverView?.midCallFeedbackPending === true; + const receiverBranchKey = receiverView?.ownerKey + ? `${receiverView.ownerKey.interactionId}:${receiverView.ownerKey.agentId}:${receiverView.ownerKey.ownershipGeneration}` + : `${receiverView?.key ?? 'mid-call:receiver'}:${interactionId ?? 'none'}`; + const receiverSummary: AIAssistantReceiverSummary | undefined = + receiverView?.surface === 'content' && + receiverView.content?.type === 'card' && + receiverView.contentRevision !== undefined + ? { + surface: 'content', + branchKey: receiverBranchKey, + content: receiverView.content, + contentRevision: receiverView.contentRevision, + actionType: receiverView.actionType ?? 'TRANSFER', + selectedFeedback: receiverView.feedback, + midCallFeedbackPending, + controlsDisabled: receiverView.requestPending || midCallFeedbackPending || !receiverView.actionType, + openReceiverSummary: () => + scopedStore.recordAISummaryViewed?.('mid-call', 'receiver', receiverView.contentRevision, currentTask) ?? + false, + recordReceiverSummaryCopied: (expectedRevision: number) => + scopedStore.recordAISummaryCopied?.('mid-call', 'receiver', expectedRevision, currentTask) ?? false, + setReceiverSummaryFeedback: (feedback, actionType, expectedRevision) => + scopedStore.setMidCallSummaryFeedback?.('receiver', feedback, actionType, expectedRevision, currentTask) ?? + Promise.resolve({outcome: 'blocked'}), + } + : receiverView?.surface === 'unavailable' || receiverView?.surface === 'generic-error' + ? { + surface: receiverView.surface, + branchKey: receiverBranchKey, + openReceiverSummary: () => true, + } + : undefined; const hookProps = useAiAssistant({ ...props, @@ -19,6 +84,7 @@ const AIAssistantInternal: React.FunctionComponent = observer agentId, isFeatureEnabled, realTimeAssist: activeRealTimeAssist, + receiverSummary, }); return ( diff --git a/packages/contact-center/ai-assistant/src/helper.ts b/packages/contact-center/ai-assistant/src/helper.ts index 4c126762e..cb2cac962 100644 --- a/packages/contact-center/ai-assistant/src/helper.ts +++ b/packages/contact-center/ai-assistant/src/helper.ts @@ -181,7 +181,7 @@ export const useRealTimeAssist = ({ // Returns the SDK promise so the card only marks like/dislike as selected // once the action has actually reached the backend. const handleRealTimeAssistAction = useCallback( - (event: AIAssistantActionEvent, assist: RealTimeAssistPayload) => { + (event: AIAssistantActionEvent, assist: RealTimeAssistPayload): Promise => { const api = store.cc?.apiAIAssistant; const adaptiveCardId = assist?.data?.adaptiveCardId; if (!interactionId || !agentId || !adaptiveCardId || !api?.sendRealTimeAssistanceUserAction) { @@ -202,6 +202,7 @@ export const useRealTimeAssist = ({ actionId: event.actionId, languageCode: typeof assist?.data?.languageCode === 'string' ? assist.data.languageCode : undefined, }) + .then(() => undefined) .catch((error) => { store.logger?.error(`CC-Widgets: sendRealTimeAssistanceUserAction failed - ${error}`, { module: MODULE, @@ -277,7 +278,10 @@ export const useAiAssistant = (input: UseAiAssistantInput) => { const chrome = useAIAssistantChrome(input); const realTimeAssist = useRealTimeAssist(input); - return useMemo(() => ({...chrome, ...realTimeAssist}), [chrome, realTimeAssist]); + return useMemo( + () => ({...chrome, ...realTimeAssist, receiverSummary: input.receiverSummary}), + [chrome, input.receiverSummary, realTimeAssist] + ); }; export {REAL_TIME_ASSIST_FLAG}; diff --git a/packages/contact-center/ai-assistant/tests/ai-assistant/index.tsx b/packages/contact-center/ai-assistant/tests/ai-assistant/index.tsx index 2a405ab35..5bb8a3002 100644 --- a/packages/contact-center/ai-assistant/tests/ai-assistant/index.tsx +++ b/packages/contact-center/ai-assistant/tests/ai-assistant/index.tsx @@ -1,30 +1,68 @@ import React from 'react'; -import {fireEvent, render, screen, within} from '@testing-library/react'; +import {fireEvent, render, screen, waitFor, within} from '@testing-library/react'; +import {runInAction} from 'mobx'; import {AIAssistant} from '../../src'; import * as helper from '../../src/helper'; import store from '@webex/cc-store'; import '@testing-library/jest-dom'; jest.mock('@webex/cc-store', () => { - return { - __esModule: true, - default: { - cc: { - apiAIAssistant: { - getRealTimeAssistance: jest.fn(), - sendRealTimeAssistanceUserAction: jest.fn(), - }, + const {observable} = jest.requireActual('mobx'); + const storeMock = { + cc: { + apiAIAssistant: { + getRealTimeAssistance: jest.fn(), + sendRealTimeAssistanceUserAction: jest.fn(), }, - currentTask: {data: {interactionId: 'interaction-1'}}, - agentId: 'agent-1', - featureFlags: {isSuggestedResponsesEnabled: true}, - realTimeAssist: {}, - onErrorCallback: undefined, - clearRealTimeAssist: jest.fn(), }, + currentTask: {data: {interactionId: 'interaction-1'}}, + agentId: 'agent-1', + agentProfile: {}, + featureFlags: {isSuggestedResponsesEnabled: true}, + realTimeAssist: {}, + receiverView: undefined, + onErrorCallback: undefined, + clearRealTimeAssist: jest.fn(), + recordAISummaryViewed: jest.fn(), + recordAISummaryCopied: jest.fn(), + setMidCallSummaryFeedback: jest.fn(), + getAISummaryViewModel: jest.fn(function () { + return this.receiverView; + }), + }; + + return { + __esModule: true, + default: observable.object(storeMock, { + cc: observable.ref, + currentTask: observable.ref, + agentProfile: observable.ref, + featureFlags: observable.ref, + realTimeAssist: observable.ref, + receiverView: observable.ref, + onErrorCallback: observable.ref, + getAISummaryViewModel: false, + clearRealTimeAssist: false, + recordAISummaryViewed: false, + recordAISummaryCopied: false, + setMidCallSummaryFeedback: false, + }), }; }); +type ReceiverSummaryView = { + surface: 'omitted' | 'unavailable' | 'generic-error' | 'content'; + eligible: boolean; + requestPending: boolean; + content?: {type: 'card'; adaptiveCard: unknown}; + contentRevision?: number; + counters: {viewed: number; copied: number; edited: number; liked: number; disliked: number}; + feedback: 'none' | 'like' | 'dislike'; + midCallFeedbackPending: boolean; + actionType?: 'CONSULT' | 'TRANSFER'; + ownerKey?: {interactionId: string; agentId: string; ownershipGeneration: number}; +}; + type StoreMock = { cc: { apiAIAssistant: { @@ -34,23 +72,74 @@ type StoreMock = { }; currentTask?: {data: {interactionId: string}}; agentId: string; + agentProfile: Record; featureFlags: {isSuggestedResponsesEnabled: boolean}; realTimeAssist: Record; + receiverView?: ReceiverSummaryView; onErrorCallback?: jest.Mock; clearRealTimeAssist: jest.Mock; + recordAISummaryViewed: jest.Mock; + recordAISummaryCopied: jest.Mock; + setMidCallSummaryFeedback: jest.Mock; + getAISummaryViewModel: jest.Mock; }; const storeMock = store as unknown as StoreMock; +const receiverSummaryView = (surface: ReceiverSummaryView['surface']): ReceiverSummaryView => ({ + surface, + eligible: true, + requestPending: false, + counters: {viewed: 0, copied: 0, edited: 0, liked: 0, disliked: 0}, + feedback: 'none', + midCallFeedbackPending: false, + ownerKey: {interactionId: 'interaction-1', agentId: 'agent-1', ownershipGeneration: 1}, +}); + +const receiverContentView = (overrides: Partial = {}): ReceiverSummaryView => ({ + ...receiverSummaryView('content'), + content: { + type: 'card', + adaptiveCard: { + type: 'AdaptiveCard', + version: '1.5', + body: [{type: 'TextBlock', text: 'Receiver summary'}], + }, + }, + contentRevision: 7, + actionType: 'TRANSFER', + ...overrides, +}); + +const deferred = (): { + promise: Promise; + resolve: (value: T) => void; +} => { + let resolve: (value: T) => void = () => undefined; + const promise = new Promise((promiseResolve) => { + resolve = promiseResolve; + }); + return {promise, resolve}; +}; + describe('AIAssistant widget', () => { beforeEach(() => { jest.clearAllMocks(); jest.restoreAllMocks(); jest.spyOn(console, 'error').mockImplementation(() => {}); + Object.defineProperty(navigator, 'clipboard', { + configurable: true, + value: {writeText: jest.fn().mockResolvedValue(undefined)}, + }); storeMock.currentTask = {data: {interactionId: 'interaction-1'}}; + storeMock.agentProfile = {}; storeMock.featureFlags = {isSuggestedResponsesEnabled: true}; storeMock.realTimeAssist = {}; + storeMock.receiverView = undefined; storeMock.onErrorCallback = undefined; + storeMock.recordAISummaryViewed.mockReturnValue(true); + storeMock.recordAISummaryCopied.mockReturnValue(true); storeMock.cc.apiAIAssistant.sendRealTimeAssistanceUserAction.mockResolvedValue(undefined); + storeMock.setMidCallSummaryFeedback.mockResolvedValue({outcome: 'confirmed'}); }); it('renders launcher when chrome is closed', () => { @@ -106,6 +195,124 @@ describe('AIAssistant widget', () => { expect(container.querySelector('.my-host-class')).toBeInTheDocument(); }); + it('queries the store receiver view model with the current task and omits the trigger for omitted surfaces', () => { + storeMock.receiverView = receiverSummaryView('omitted'); + + render(); + + expect(storeMock.getAISummaryViewModel).toHaveBeenCalledWith('mid-call', 'receiver', storeMock.currentTask); + expect(screen.queryByTestId('ai-assistant:view-summary')).not.toBeInTheDocument(); + }); + + it('re-renders the receiver summary branch when the observable surface changes', async () => { + storeMock.receiverView = receiverSummaryView('unavailable'); + render(); + + fireEvent.click(screen.getByTestId('ai-assistant:view-summary')); + expect(screen.getByTestId('ai-summary:unavailable')).toBeInTheDocument(); + + runInAction(() => { + storeMock.receiverView = receiverSummaryView('generic-error'); + }); + + await waitFor(() => expect(screen.getByTestId('ai-summary:error')).toBeInTheDocument()); + expect(screen.queryByTestId('ai-summary:unavailable')).not.toBeInTheDocument(); + }); + + it('records the receiver summary view before opening the current revision', async () => { + storeMock.receiverView = receiverContentView(); + storeMock.recordAISummaryViewed.mockReturnValue(true); + render(); + + fireEvent.click(screen.getByTestId('ai-assistant:view-summary')); + + expect(await screen.findByTestId('ai-assistant:receiver-summary')).toBeInTheDocument(); + expect(storeMock.recordAISummaryViewed).toHaveBeenCalledWith('mid-call', 'receiver', 7, storeMock.currentTask); + expect(storeMock.recordAISummaryViewed).toHaveBeenCalledTimes(1); + }); + + it('opens a receiver summary even when Real-time Assist is disabled', async () => { + storeMock.featureFlags = {isSuggestedResponsesEnabled: false}; + storeMock.receiverView = receiverContentView(); + render(); + + fireEvent.click(screen.getByTestId('ai-assistant:view-summary')); + + expect(await screen.findByTestId('ai-assistant:receiver-summary')).toBeInTheDocument(); + expect(screen.queryByTestId('ai-assistant:landing')).not.toBeInTheDocument(); + expect(storeMock.recordAISummaryViewed).toHaveBeenCalledWith('mid-call', 'receiver', 7, storeMock.currentTask); + }); + + it('routes receiver copy and feedback through the store with the current revision and action type', async () => { + storeMock.receiverView = receiverContentView(); + render(); + + fireEvent.click(screen.getByTestId('ai-assistant:view-summary')); + fireEvent.click(await screen.findByRole('button', {name: 'Copy Summary'})); + + await waitFor(() => expect(navigator.clipboard.writeText).toHaveBeenCalled()); + expect(storeMock.recordAISummaryCopied).toHaveBeenCalledWith('mid-call', 'receiver', 7, storeMock.currentTask); + + fireEvent.click(screen.getByRole('button', {name: 'This is helpful'})); + + await waitFor(() => + expect(storeMock.setMidCallSummaryFeedback).toHaveBeenCalledWith( + 'receiver', + 'like', + 'TRANSFER', + 7, + storeMock.currentTask + ) + ); + }); + + it('projects receiver feedback pending state as disabled controls without painting a selection', async () => { + storeMock.receiverView = receiverContentView({ + requestPending: true, + midCallFeedbackPending: true, + feedback: 'none', + }); + render(); + + fireEvent.click(screen.getByTestId('ai-assistant:view-summary')); + + const like = await screen.findByRole('button', {name: 'This is helpful'}); + expect(like).toBeDisabled(); + expect(like).toHaveAttribute('aria-pressed', 'false'); + }); + + it('replaces receiver feedback projections only after the store confirms the selection', async () => { + const feedbackSend = deferred<{outcome: 'confirmed'}>(); + storeMock.receiverView = receiverContentView(); + storeMock.setMidCallSummaryFeedback.mockImplementationOnce(() => { + runInAction(() => { + storeMock.receiverView = receiverContentView({ + requestPending: true, + midCallFeedbackPending: true, + feedback: 'none', + }); + }); + return feedbackSend.promise; + }); + render(); + + fireEvent.click(screen.getByTestId('ai-assistant:view-summary')); + const like = await screen.findByRole('button', {name: 'This is helpful'}); + fireEvent.click(like); + + await waitFor(() => expect(like).toBeDisabled()); + expect(like).toHaveAttribute('aria-pressed', 'false'); + + feedbackSend.resolve({outcome: 'confirmed'}); + runInAction(() => { + storeMock.receiverView = receiverContentView({feedback: 'like'}); + }); + + await waitFor(() => + expect(screen.getByRole('button', {name: 'This is helpful'})).toHaveAttribute('aria-pressed', 'true') + ); + }); + it('routes errors thrown in the hook to store.onErrorCallback', () => { const onErrorCallback = jest.fn(); storeMock.onErrorCallback = onErrorCallback; diff --git a/packages/contact-center/ai-assistant/tests/helper.ts b/packages/contact-center/ai-assistant/tests/helper.ts index ed53e7fcf..8ebddd6d7 100644 --- a/packages/contact-center/ai-assistant/tests/helper.ts +++ b/packages/contact-center/ai-assistant/tests/helper.ts @@ -37,6 +37,38 @@ describe('useAiAssistant', () => { expect(result.current.requestStatus).toBe('idle'); }); + it('passes receiver summary state through and replaces it on branch changes', () => { + const firstReceiverSummary = { + surface: 'content' as const, + branchKey: 'interaction-1:agent-1:1', + content: {type: 'card' as const, adaptiveCard: {type: 'AdaptiveCard'}}, + contentRevision: 1, + actionType: 'TRANSFER' as const, + selectedFeedback: 'none' as const, + midCallFeedbackPending: false, + controlsDisabled: false, + openReceiverSummary: jest.fn().mockReturnValue(true), + recordReceiverSummaryCopied: jest.fn().mockReturnValue(true), + setReceiverSummaryFeedback: jest.fn().mockResolvedValue({outcome: 'confirmed' as const}), + }; + const replacementReceiverSummary = { + ...firstReceiverSummary, + branchKey: 'interaction-2:agent-1:1', + contentRevision: 2, + }; + const {result, rerender} = renderHook( + ({receiverSummary}: {receiverSummary: typeof firstReceiverSummary}) => + useAiAssistant({...baseProps, receiverSummary}), + {initialProps: {receiverSummary: firstReceiverSummary}} + ); + + expect(result.current.receiverSummary).toBe(firstReceiverSummary); + + rerender({receiverSummary: replacementReceiverSummary}); + + expect(result.current.receiverSummary).toBe(replacementReceiverSummary); + }); + it('open/close/minimize/restore flips chrome state and fires callbacks', () => { const onOpen = jest.fn(); const onClose = jest.fn(); diff --git a/packages/contact-center/cc-components/ai-docs/cc-components-spec.md b/packages/contact-center/cc-components/ai-docs/cc-components-spec.md index 1ce7f9136..eb80332b5 100644 --- a/packages/contact-center/cc-components/ai-docs/cc-components-spec.md +++ b/packages/contact-center/cc-components/ai-docs/cc-components-spec.md @@ -23,7 +23,7 @@ Every generated requirement below cites concrete source evidence using `file pat | Source doc | Scope | Decision | Detail location or disposition | | --------------------------------------------------------------------------------------------------- | ------------------------------ | ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `ai-docs/_archive/pre-sdlc-migration/packages/contact-center/cc-components/ai-docs/AGENTS.md` | overview / API / examples | migrated / reconciled | Orientation → Overview/Purpose/Stack; component table → Public Surface; examples → Use Cases. Reconciled: archived table listed 7 components; code exports 11 (added Campaign\* and RealTimeTranscript). | +| `ai-docs/_archive/pre-sdlc-migration/packages/contact-center/cc-components/ai-docs/AGENTS.md` | overview / API / examples | migrated / reconciled | Orientation → Overview/Purpose/Stack; component table → Public Surface; examples → Use Cases. Reconciled: archived table listed 7 components; code exports 16 top-level React symbols plus type/value barrels. | | `ai-docs/_archive/pre-sdlc-migration/packages/contact-center/cc-components/ai-docs/ARCHITECTURE.md` | architecture / component table | migrated / reconciled | Component table, file structure, patterns, diagrams → Design Overview, Data Flow, Class/Component Relationships, Folder structure, Pitfalls. Conflict: archived doc imports `@momentum-design/components` and `@momentum-ui/react-collaboration` interchangeably; code uses both (see Stack). | | `packages/contact-center/cc-components/src/` and `tests/` | source / tests | reference-only (ground truth) | All requirements, props, and component inventory grounded against live source and `tests/components/`. | @@ -33,7 +33,7 @@ Every generated requirement below cites concrete source evidence using `file pat The package exports two surfaces from `src/index.ts` (React components + their prop types) and `src/wc.ts` (the same components wrapped as custom elements via `@r2wc/react-to-web-component`). A maintainer should start at `src/index.ts` to see the public component set, then open the component directory under `src/components/` (each has `*.tsx`, a `*.types.ts` or shared `task.types.ts`, a `*.utils.ts(x)` for extracted logic, and a `*.scss`). Shared, cross-component logic lives in `src/utils/index.ts` (`formatTime`, `getMediaTypeInfo`, `isWxAppEngagedCall`, `shouldShowWxAppTelephonyControls`) and `src/hooks/` (`useIntersectionObserver`). `@webex/cc-task` imports the wxApp visibility helpers from this package's public export. -The component set spans the contact-center agent surface: station login (including the E911 emergency-service acknowledgment modal), agent state, the task lifecycle (incoming task, call control with consult/transfer, CAD-enabled call control, task list, outdial), live transcript, and campaign-preview dialing (countdown, error dialog, campaign task card/popover/list-item). +The component set spans the contact-center agent surface: station login (including the E911 emergency-service acknowledgment modal), agent state, the task lifecycle (incoming task, call control with consult/transfer and AI-summary wrap-up, CAD-enabled call control, task list, outdial), live transcript, AI Assistant receiver summaries, the shared `AISummary` surface, and campaign-preview dialing (countdown, error dialog, campaign task card/popover/list-item). ## Purpose / Responsibility @@ -55,6 +55,8 @@ packages/contact-center/cc-components/src/ ├── utils/ │ └── index.ts # Shared utils: formatTime, getMediaTypeInfo └── components/ + ├── AIAssistant/ # AI Assistant panel, receiver summary branch, AdaptiveCardRenderer + ├── AISummary/ # Shared AI summary surface, messages, post-call projection, types ├── StationLogin/ # Agent login / device + team selection UI ├── UserState/ # Agent state dropdown, idle codes, state timer └── task/ @@ -88,46 +90,69 @@ tests/hooks/ # Hook tests | `src/index.ts` | The public React component set and re-exported type barrels — authoritative export list. | | `src/wc.ts` | Custom-element tag names (`component-cc-*`) and the r2wc prop type maps per component. | | `src/components/task/task.types.ts` | Shared task prop interfaces and the `Pick`-derived component prop types (`CallControlComponentProps`, `IncomingTaskComponentProps`, `TaskListComponentProps`, `OutdialCallComponentProps`, `RealTimeTranscriptComponentProps`), plus `MEDIA_CHANNEL`, `TaskState`, `ControlVisibility`, campaign types. | +| `src/components/AIAssistant/` | AI Assistant panel chrome, receiver-summary prop types, display-only Adaptive Card renderer, and receiver summary tests. | +| `src/components/AISummary/` | SDK-free shared AI summary presentation, copy/feedback controls, hover tooltips, post-call display-section projection, and exact en-US summary messages. | | `src/components/StationLogin/station-login.types.ts` | `IStationLoginProps` and the `StationLoginComponentProps` Pick. | | `src/components/UserState/user-state.types.ts` | `IUserState`, `UserStateComponentsProps` Pick, `AgentUserState` enum. | | `src/components/StationLogin/constants.ts` | Login labels/error strings — re-exported from the barrel; never hardcode these elsewhere. | | `src/components/task/constants.ts` | Task UI label/string constants (e.g. `CAMPAIGN_CALL`, `WRAP_UP`). | -| `src/utils/index.ts` | `formatTime` (timer formatting), `getMediaTypeInfo` (media icon/label mapping), and wxApp telephony visibility helpers (`isWxAppEngagedCall`, `shouldShowWxAppTelephonyControls` in `wxapp-telephony.utils.ts`, re-exported from package index). | +| `src/utils/index.ts` | `formatTime` (timer formatting), `getMediaTypeInfo` (media icon/label mapping), and wxApp telephony visibility helpers (`isWxAppEngagedCall`, `shouldShowWxAppTelephonyControls` in `wxapp-telephony.utils.ts`, re-exported from package index). | ## Public Surface Consumed as an imported SDK/code API. The React barrel (`src/index.ts`) is the primary surface; `src/wc.ts` exposes the same components as custom elements for the library build. Exact prop schemas live in the `*.types.ts` files (linked below); the root contract index (`CONTRACTS.md`) documents how the consuming `cc-widgets`/widget layer re-exposes these as custom elements. -| Contract ID | Type | Surface | Purpose | Compatibility / deprecation | Schema / detail link | Root index | -| -------------------------------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | -------------------------------------------------- | -| `cc-components.StationLoginComponent` | SDK | `StationLoginComponent` (`StationLoginComponentProps`) | Agent login: device/team selection, login/logout, multiple-login alert, profile mode | semver; props are `Pick`ed — adding optional props = minor, removing/renaming a picked prop = major | `src/components/StationLogin/station-login.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `cc-components.UserStateComponent` | SDK | `UserStateComponent` (`UserStateComponentsProps`) | Agent state dropdown + idle codes + state timer | semver as above | `src/components/UserState/user-state.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `cc-components.CallControlComponent` | SDK | `CallControlComponent` (`CallControlComponentProps`) | Call control buttons: hold/resume, mute, record, end, wrapup, consult/transfer/conference | semver as above | `src/components/task/task.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `cc-components.consult-transfer-lists` | SDK callback props | `getQueuesFetcher`/`getQueues` use `FetchPaginatedList` and `getEntryPoints` uses `FetchPaginatedList` | Render the SDK's established queue and entry-point records directly in backend order | types track the existing SDK/store entity contracts | `src/components/task/task.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `cc-components.CallControlCADComponent` | SDK | `CallControlCADComponent` (`CallControlComponentProps`) | Call control with customer/queue header and agent-viewable CAD global variables | semver as above | `src/components/task/task.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `cc-components.IncomingTaskComponent` | SDK | `IncomingTaskComponent` (`IncomingTaskComponentProps`) | Incoming task notification with Answer/Decline | semver as above | `src/components/task/task.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `cc-components.TaskListComponent` | SDK | `TaskListComponent` (`TaskListComponentProps`) | Active + incoming task list; renders campaign preview when enabled | semver as above | `src/components/task/task.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `cc-components.OutdialCallComponent` | SDK | `OutdialCallComponent` (`OutdialCallComponentProps`) | Outbound dialpad, ANI selection, address-book search | semver as above | `src/components/task/task.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `cc-components.RealTimeTranscriptComponent` | SDK | `RealTimeTranscriptComponent` (`RealTimeTranscriptComponentProps`) | Renders sorted live transcript entries; empty state when none | semver as above | `src/components/task/task.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `cc-components.CampaignCountdownComponent` | SDK | `CampaignCountdownComponent` (`CampaignCountdownProps`) | Campaign preview offer countdown; fires `onTimeout` at zero | semver as above | `src/components/task/CampaignCountdown/campaign-countdown.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `cc-components.CampaignErrorDialogComponent` | SDK | `CampaignErrorDialogComponent` (`CampaignErrorDialogProps`) | Modal shown when a campaign action (accept/skip/remove/cancel) fails | semver as above | `src/components/task/CampaignErrorDialog/campaign-error-dialog.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `cc-components.CampaignTaskComponent` | SDK | `CampaignTaskComponent` (`CampaignTaskProps`) | Campaign preview card: accept/skip/remove, countdown, error dialog | semver as above | `src/components/task/task.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `cc-components.E911Modal` | SDK | `E911Modal` (`E911ModalProps`) | Emergency-service (E911) acknowledgment modal shown on BROWSER station login; Cancel is the only dismissal path, Save & Continue is gated on the checkbox and disabled while the save is in flight | semver as above; not yet in `wc.ts` (React-only, no custom-element wrapper) | `src/components/StationLogin/E911Modal/e911-modal.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `cc-components.TelephonyActionToast` | SDK | `TelephonyActionToast` (`TelephonyActionToastProps`) | wxApp thick-client telephony error toast (Momentum `Toast` `variant="error"`); consumed by `@webex/cc-task` CallControl / CallControlCAD wrappers, not intended for third-party host apps | semver; widget-internal surface — exported from package index for cross-package use | `src/components/task/TelephonyActionToast/telephony-action-toast.tsx` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `cc-components.WxAppOfferActionError` | SDK | `WxAppOfferActionError` (`WxAppOfferActionErrorProps`) | Inline wxApp offer-action error as red text (`--mds-color-theme-text-error-normal`) on a full-width row below Accept/Decline — no alert background, padding, or border; parent `Task` sets `ListItemBase size="auto"` when an error is present so the row grows naturally; store-backed per `interactionId`, shared between TaskList and IncomingTask via `@webex/cc-store` (immutable map updates for memo-safe sync) | semver; widget-internal surface — exported from package index for cross-package use | `src/components/task/WxAppOfferActionError/wxapp-offer-action-error.tsx` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `cc-components.wxapp-telephony-utils` | SDK | `isWxAppEngagedCall`, `shouldShowWxAppTelephonyControls` | Shared wxApp visibility helpers for mute/keypad gating across cc-components and cc-task | semver; utility exports from `src/utils/wxapp-telephony.utils.ts` | `src/utils/wxapp-telephony.utils.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | -| `cc-components.wc` | SDK | `@webex/cc-components/wc` → custom elements `component-cc-user-state`, `component-cc-station-login`, `component-cc-call-control`, `component-cc-call-control-cad`, `component-cc-incoming-task`, `component-cc-task-list`, `component-cc-out-dial-call`, `component-cc-realtime-transcript` | Custom-element build of the components | tag names are breaking surface; r2wc prop type map is part of the contract | `src/wc.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| Contract ID | Type | Surface | Purpose | Compatibility / deprecation | Schema / detail link | Root index | +| -------------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | -------------------------------------------------- | +| `cc-components.StationLoginComponent` | SDK | `StationLoginComponent` (`StationLoginComponentProps`) | Agent login: device/team selection, login/logout, multiple-login alert, profile mode | semver; props are `Pick`ed — adding optional props = minor, removing/renaming a picked prop = major | `src/components/StationLogin/station-login.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.UserStateComponent` | SDK | `UserStateComponent` (`UserStateComponentsProps`) | Agent state dropdown + idle codes + state timer | semver as above | `src/components/UserState/user-state.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.CallControlComponent` | SDK | `CallControlComponent` (`CallControlComponentProps`), including `aiSummary?: CallControlAISummaryProps`, `wrapupCall(reason, id): Promise`, and `WrapupCompletionResult` | Call control buttons: hold/resume, mute, record, end, async wrap-up, consult/transfer/conference, mid-call summary send-before-action, and post-call AI-summary wrap-up | semver as above; the synchronous `wrapupCall` → async `Promise` migration is breaking for consumers that typed the callback as `void` | `src/components/task/task.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.consult-transfer-lists` | SDK callback props | `getQueuesFetcher`/`getQueues` use `FetchPaginatedList` and `getEntryPoints` uses `FetchPaginatedList` | Render the SDK's established queue and entry-point records directly in backend order | types track the existing SDK/store entity contracts | `src/components/task/task.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.CallControlCADComponent` | SDK | `CallControlCADComponent` (`CallControlComponentProps`) | Call control with customer/queue header and agent-viewable CAD global variables | semver as above | `src/components/task/task.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.IncomingTaskComponent` | SDK | `IncomingTaskComponent` (`IncomingTaskComponentProps`) | Incoming task notification with Answer/Decline | semver as above | `src/components/task/task.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.TaskListComponent` | SDK | `TaskListComponent` (`TaskListComponentProps`) | Active + incoming task list; renders campaign preview when enabled | semver as above | `src/components/task/task.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.OutdialCallComponent` | SDK | `OutdialCallComponent` (`OutdialCallComponentProps`) | Outbound dialpad, ANI selection, address-book search | semver as above | `src/components/task/task.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.RealTimeTranscriptComponent` | SDK | `RealTimeTranscriptComponent` (`RealTimeTranscriptComponentProps`) | Renders sorted live transcript entries; empty state when none | semver as above | `src/components/task/task.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.CampaignCountdownComponent` | SDK | `CampaignCountdownComponent` (`CampaignCountdownProps`) | Campaign preview offer countdown; fires `onTimeout` at zero | semver as above | `src/components/task/CampaignCountdown/campaign-countdown.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.CampaignErrorDialogComponent` | SDK | `CampaignErrorDialogComponent` (`CampaignErrorDialogProps`) | Modal shown when a campaign action (accept/skip/remove/cancel) fails | semver as above | `src/components/task/CampaignErrorDialog/campaign-error-dialog.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.CampaignTaskComponent` | SDK | `CampaignTaskComponent` (`CampaignTaskProps`) | Campaign preview card: accept/skip/remove, countdown, error dialog | semver as above | `src/components/task/task.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.E911Modal` | SDK | `E911Modal` (`E911ModalProps`) | Emergency-service (E911) acknowledgment modal shown on BROWSER station login; Cancel is the only dismissal path, Save & Continue is gated on the checkbox and disabled while the save is in flight | semver as above; not yet in `wc.ts` (React-only, no custom-element wrapper) | `src/components/StationLogin/E911Modal/e911-modal.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.AIAssistantComponent` | SDK | `AIAssistantComponent` (`AIAssistantComponentProps`, `AIAssistantReceiverSummary`, `AdaptiveCardRendererProps`, action event types) | AI Assistant panel chrome, Real-time Assist transcript/actions, and receiving-agent `View summary` branch | semver as above; receiver branch is props-only and React-only | `src/components/AIAssistant/ai-assistant.types.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.AISummary` | SDK | `AISummary` (`AISummaryProps` mode union), `AI_SUMMARY_MESSAGES`, `COPIED_FEEDBACK_MS`, `MID_CALL_SECTION_DEFINITIONS`, `POST_CALL_SECTION_DEFINITIONS`, `POST_CALL_DISPLAY_SECTION_ORDER`, `projectPostCallDisplaySections`, and `components/AISummary` type exports | Shared SDK-free AI summary presentation for mid-call initiator/receiver and post-call wrap-up surfaces | semver as above; adding a mode or narrowing callback/result types is breaking unless optional/backward-compatible | `src/components/AISummary/` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.TelephonyActionToast` | SDK | `TelephonyActionToast` (`TelephonyActionToastProps`) | wxApp thick-client telephony error toast (Momentum `Toast` `variant="error"`); consumed by `@webex/cc-task` CallControl / CallControlCAD wrappers, not intended for third-party host apps | semver; widget-internal surface — exported from package index for cross-package use | `src/components/task/TelephonyActionToast/telephony-action-toast.tsx` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.WxAppOfferActionError` | SDK | `WxAppOfferActionError` (`WxAppOfferActionErrorProps`) | Inline wxApp offer-action error as red text (`--mds-color-theme-text-error-normal`) on a full-width row below Accept/Decline — no alert background, padding, or border; parent `Task` sets `ListItemBase size="auto"` when an error is present so the row grows naturally; store-backed per `interactionId`, shared between TaskList and IncomingTask via `@webex/cc-store` (immutable map updates for memo-safe sync) | semver; widget-internal surface — exported from package index for cross-package use | `src/components/task/WxAppOfferActionError/wxapp-offer-action-error.tsx` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.wxapp-telephony-utils` | SDK | `isWxAppEngagedCall`, `shouldShowWxAppTelephonyControls` | Shared wxApp visibility helpers for mute/keypad gating across cc-components and cc-task | semver; utility exports from `src/utils/wxapp-telephony.utils.ts` | `src/utils/wxapp-telephony.utils.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | +| `cc-components.wc` | SDK | `@webex/cc-components/wc` → custom elements `component-cc-user-state`, `component-cc-station-login`, `component-cc-call-control`, `component-cc-call-control-cad`, `component-cc-incoming-task`, `component-cc-task-list`, `component-cc-out-dial-call`, `component-cc-realtime-transcript` | Custom-element build of the components | tag names are breaking surface; r2wc prop type map is part of the contract | `src/wc.ts` | [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) | Compatibility notes: - Component props are derived with `Pick<...>` over a larger interface (e.g. `IStationLoginProps`, `ControlProps`); only the picked keys are public. Adding an optional picked prop is additive (minor); removing/renaming a picked prop, or narrowing a prop's type, is breaking (major). - Custom-element tag names in `wc.ts` (`component-cc-*`) and their r2wc prop type maps are a breaking surface — renaming a tag or changing a prop's r2wc type (`json`/`string`/`function`/...) breaks host consumers. - `wc.ts` aliases `CallControlCADComponent` to `../CallControl/call-control` (imports `CallControlComponent` under the `CallControlCADComponent` name); the distinct CAD component is `src/components/task/CallControlCAD/call-control-cad.tsx`. See Pitfalls. +- `CallControlComponentProps.wrapupCall` is asynchronous for direct React consumers: hosts must supply `(wrapupReason, wrapupId) => Promise`. The `component-cc-call-control` and `component-cc-call-control-cad` runtime path still accepts legacy fire-and-forget function props; `handleWrapupCall` normalizes undefined/non-result callback returns to `{wrapup: 'succeeded', response: 'not-required'}` so the selected reason clears. Missing selections, thrown callbacks, and rejected callbacks still resolve `{wrapup: 'failed'}` instead of rejecting to the UI. +- `CallControlAISummaryProps.requestMidCallSummary(actionType)` is the canonical popover-open callback and resolves the store `AISummaryRequestResult`; `sendMidCallSummaryBeforeAction(actionType, expectedRevision)` resolves the store `AISummaryPreActionSendResult` and is awaited before the consult/transfer/Transfer Conference/Merge telephony callback runs. `CallControlComponent` invokes `consultCall`, `transferCall`, `consultTransfer`, or `consultConference` once after that Promise settles, including failed/blocked/stale outcomes. Those telephony suppliers may return `void` or `Promise`; the adapter observes fulfillment/rejection, consumes failures, and commits consult target UI state only after `consultCall` fulfills. + +`src/index.ts` public barrel inventory: + +The `AISummary` barrel also exports `MID_CALL_SECTION_DEFINITIONS` and +`POST_CALL_SECTION_DEFINITIONS`, readonly SDK section-key/label registries. +Both are existing public values, alongside the display-order constant and +projection helper; they are not widget or store actions. + +| Export group | Public exports | +| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Top-level React symbols | `UserStateComponent`, `StationLoginComponent`, `CallControlComponent`, `CallControlCADComponent`, `IncomingTaskComponent`, `TaskListComponent`, `OutdialCallComponent`, `CampaignErrorDialogComponent`, `CampaignCountdownComponent`, `CampaignTaskComponent`, `RealTimeTranscriptComponent`, `E911Modal`, `AIAssistantComponent`, `TelephonyActionToast`, `WxAppOfferActionError`, `AISummary` | +| Station/User types and values | `StationLogin/constants`, `E911ModalProps`, `StationLoginComponentProps`/station-login types, `UserStateComponentsProps`/user-state types | +| Task types and values | `task.types` exports including `CallControlComponentProps`, `CallControlAISummaryProps`, `WrapupCompletionResult`, `IncomingTaskComponentProps`, `TaskListComponentProps`, `OutdialCallComponentProps`, `RealTimeTranscriptComponentProps`, `TARGET_TYPE`, `MEDIA_CHANNEL`, campaign/participant/wxApp types | +| Campaign types | `CampaignErrorDialogProps`/campaign error constants and `CampaignCountdownProps` | +| AI Assistant types | `AIAssistantComponentProps`, `AIAssistantReceiverSummary`, `AdaptiveCardRendererProps`, `AIAssistantActionEvent`, `AIAssistantActionKind` | +| AISummary component barrel | `AISummary` default/named export, `projectPostCallDisplaySections`, `AI_SUMMARY_MESSAGES`, `COPIED_FEEDBACK_MS`, `MID_CALL_SECTION_DEFINITIONS`, `POST_CALL_SECTION_DEFINITIONS`, `POST_CALL_DISPLAY_SECTION_ORDER`, and `AISummaryProps`/mode-specific type exports | +| Utilities | `isWxAppEngagedCall`, `shouldShowWxAppTelephonyControls` | ## Requires (dependencies) - `@webex/cc-store` (workspace:\*) — type-only import surface here (`ITask`, `ILogger`, `IContactCenter`, `ContactServiceQueue`, `EntryPointRecord`, `IdleCode`, `IWrapupCode`, `BuddyDetails`, etc.) and constants such as `ERROR_TRIGGERING_IDLE_CODES`, `LoginOptions`, `DESKTOP`. Components consume types/constants, not the store singleton. -- `@webex/cc-ui-logging` (workspace:\*) — `withMetrics` HOC wrapping each top-level component for mount/metrics tracking. +- `@webex/cc-ui-logging` (workspace:\*) — `withMetrics` HOC wrapping widget-facing component exports for mount/metrics tracking. - `@momentum-ui/react-collaboration` (peer `>=26.197.0`) and `@momentum-design/components/dist/react` — UI primitives. - `@r2wc/react-to-web-component` `2.0.3` — custom-element wrapping in `wc.ts`. - `@momentum-ui/illustrations` `^1.24.0` — illustration assets. @@ -136,31 +161,202 @@ Compatibility notes: ## Requirements -| ID | WHAT | WHY | Source Evidence | Test / Example Evidence | Assumptions / Gaps | Confidence | -| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | ---------- | -| `CC-COMPONENTS-R-001` | Components are pure presentational: they receive data + callbacks via props and never read the MobX store or call the SDK directly. | Keeps presentation decoupled from state/SDK so components are testable in isolation and reusable across widgets. | `src/components/StationLogin/station-login.tsx`, `src/components/UserState/user-state.tsx`, `src/components/task/CallControl/call-control.tsx` (props-only; no `@webex/cc-store` singleton import) | `tests/components/StationLogin/station-login.tsx` (renders from props, mocks callbacks) | None | PRESENT | -| `CC-COMPONENTS-R-002` | The public React surface is exactly the 12 components exported from `index.ts` (11 base components plus `E911Modal`) plus the re-exported type barrels. | Defines the supported import surface; consumers must not import internal subcomponents. | `src/index.ts` | `tests/components/StationLogin/station-login.tsx`, `tests/components/task/CampaignTask/campaign-task.test.tsx`, `tests/components/task/RealtimeTranscript/realtime-transcript.tsx`, `tests/components/StationLogin/E911Modal/e911-modal.test.tsx` (import the exported components) | `E911Modal` is not yet wrapped as a custom element in `wc.ts` - React-only for now | PRESENT | -| `CC-COMPONENTS-R-003` | `StationLoginComponent` invokes the supplied callbacks (`login`, `setDeviceType`, `setDialNumber`, `handleContinue`, `saveLoginOptions`) on the matching user action and surfaces `loginFailure`/`saveError` as error UI. | Login intent and errors must propagate to the widget layer without the component owning login logic. | `src/components/StationLogin/station-login.tsx`, `src/components/StationLogin/station-login.utils.tsx` | `tests/components/StationLogin/station-login.tsx` (`calls login function...`, `renders login failure when passed`, `renders save error when passed`) | None | PRESENT | -| `CC-COMPONENTS-R-004` | `StationLoginComponent` hides the Desktop login option when `hideDesktopLogin` is true (in both login and profile mode) and shows it when false/undefined. | Deployments can disable Desktop login; must be honored consistently across modes. | `src/components/StationLogin/station-login.tsx`, `src/components/StationLogin/station-login.utils.tsx` | `tests/components/StationLogin/station-login.tsx` (`hides Desktop login option when hideDesktopLogin is true`, `... when false`, `... when undefined`, `... in profile mode`) | None | PRESENT | -| `CC-COMPONENTS-R-005` | `UserStateComponent` renders idle codes sorted/built into the dropdown, reflects `currentState`/`elapsedTime`, and calls `setAgentStatus(auxCodeId)` on selection; error-triggering idle codes are styled distinctly. | Agent must change state and see correct current state + timing; error idle codes need visual emphasis. | `src/components/UserState/user-state.tsx`, `src/components/UserState/user-state.utils.ts` (`buildDropdownItems`, `sortDropdownItems`, `handleSelectionChange`, `getDropdownClass`) | `tests/components/UserState/user-state.tsx`, `tests/components/UserState/user-state.utils.tsx` | None | PRESENT | -| `CC-COMPONENTS-R-006` | `CallControlComponent` builds its button set from Task controls, passes the matching SDK-ordered `consultTransferDestinations` action array into the popover, and routes button presses to the matching callback; wrapup requires selecting a reason. The popover renders destination categories in supplied order and may only remove Dial Number/Entry Point through explicit host hide options. | Call control must reflect SDK decisions without reconstructing profile/media/direction policy, while retaining supported host presentation overrides. | `src/components/task/CallControl/call-control.tsx`, `src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx`, `src/components/task/CallControl/call-control.utils.ts` | `tests/components/task/CallControl/call-control.tsx`, `tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx`, `tests/components/task/CallControl/call-control.utils.tsx` | Consumers cannot enable a destination omitted by the SDK. | PRESENT | -| `CC-COMPONENTS-R-007` | `CallControlCADComponent` renders the customer/queue/caller header and an agent-viewable CAD global variables panel, and renders the campaign call icon + "Campaign call" label when `isCampaignCall` is true. | CAD/header info and campaign branding must be visible to the agent during a call. | `src/components/task/CallControlCAD/call-control-cad.tsx`, `src/components/task/Task/task.utils.ts` (`getAgentViewableGlobalVariables`) | `tests/components/task/CallControlCAD/call-control-cad.tsx` | None | PRESENT | -| `CC-COMPONENTS-R-008` | `IncomingTaskComponent` renders the standard `Task` with Answer/Decline when an `incomingTask` is present and renders nothing (hidden) when it is absent; Accept/Decline invoke `accept(task)`/`reject(task)`. | Avoids a stray empty notification when no task; routes accept/decline intent up. | `src/components/task/IncomingTask/incoming-task.tsx`, `src/components/task/IncomingTask/incoming-task.utils.tsx` (`extractIncomingTaskData`) | `tests/components/task/IncomingTask/incoming-task.tsx`, `tests/components/task/IncomingTask/incoming-task.utils.tsx` | None | PRESENT | -| `CC-COMPONENTS-R-009` | `TaskListComponent` renders nothing when the task list is empty, otherwise renders one row per task; campaign preview tasks render `CampaignTask` (instead of `Task`) only when `hasCampaignPreviewEnabled` (default true) and the task is a campaign preview. | List must collapse when empty and switch row UI for campaign previews per the feature flag. | `src/components/task/TaskList/task-list.tsx`, `src/components/task/TaskList/task-list.utils.ts` (`isTaskListEmpty`, `getTasksArray`, `isCampaignPreviewTask`, `getActiveCampaignPreviewId`) | `tests/components/task/TaskList/task-list.tsx`, `tests/components/task/TaskList/task-list.utils.tsx` | None | PRESENT | -| `CC-COMPONENTS-R-010` | `OutdialCallComponent` validates the entered destination, supports dialpad / ANI / address-book tabs, disables the outdial action while a telephony task is active, and calls `startOutdial(destination, origin?)`. | Outbound dialing must validate input and not start a second call over an active one. | `src/components/task/OutdialCall/outdial-call.tsx`, `src/components/task/OutdialCall/constants.ts` | `tests/components/task/OutdialCall/out-dial-call.tsx` | None | PRESENT | -| `CC-COMPONENTS-R-011` | `RealTimeTranscriptComponent` sorts `liveTranscriptEntries` by ascending `timestamp` before rendering and shows an empty-state message when there are no entries. | Transcript must read chronologically and degrade gracefully when empty. | `src/components/task/RealTimeTranscript/real-time-transcript.tsx` | `tests/components/task/RealtimeTranscript/realtime-transcript.tsx` | None | PRESENT | -| `CC-COMPONENTS-R-012` | Campaign preview UI: `CampaignTaskComponent` renders accept/skip/remove + countdown and triggers the configured auto-action on timeout; failed actions open `CampaignErrorDialogComponent` with the mapped `CampaignErrorType`; `CampaignCountdownComponent` fires `onTimeout` at zero. | Campaign preview offers are time-boxed; failures and timeouts must be surfaced and auto-handled. | `src/components/task/CampaignTask/campaign-task.tsx`, `src/components/task/CampaignErrorDialog/campaign-error-dialog.tsx` (+ `.types.ts` `CAMPAIGN_ACTION_ERROR_MAP`, `ERROR_TITLES`), `src/components/task/CampaignCountdown/campaign-countdown.tsx` | `tests/components/task/CampaignTask/campaign-task.test.tsx`, `tests/components/task/CampaignErrorDialog/campaign-error-dialog.tsx`, `tests/components/task/CampaignCountdown/campaign-countdown.tsx` | None | PRESENT | -| `CC-COMPONENTS-R-013` | `formatTime` renders `HH:MM:SS` for durations ≥ 1 hour and `MM:SS` otherwise, with zero-padding; `getMediaTypeInfo` maps media type/channel to icon/label/className/brand-visual, falling back to telephony/chat defaults. | Timers and media badges must format consistently across all task components. | `src/utils/index.ts` | `tests/components/task/CallControl/call-control.utils.tsx`, snapshot tests under `tests/components/task/**/__snapshots__/` exercise formatted output | No dedicated `tests/utils/` file found for `formatTime`/`getMediaTypeInfo` (exercised indirectly via component/utils tests) | WEAK | -| `CC-COMPONENTS-R-014` | `useIntersectionObserver` reports element visibility for infinite-scroll/lazy paths (e.g. outdial address-book paging). | Paged lists must load more on scroll without per-component observer wiring. | `src/hooks/useIntersectionObserver.ts` | `tests/hooks/useIntersectionObserver.test.ts` | None | PRESENT | -| `CC-COMPONENTS-R-015` | Each top-level exported component is wrapped with the `withMetrics` HOC so mount/usage metrics are tracked uniformly. | Consistent telemetry across all widgets without per-component instrumentation. | `withMetrics` import + wrap in `src/components/StationLogin/station-login.tsx`, `src/components/UserState/user-state.tsx`, `src/components/task/CallControl/call-control.tsx`, `src/components/task/RealTimeTranscript/real-time-transcript.tsx` | Covered indirectly by each component's render test | No test asserts the HOC wrapping itself | WEAK | -| `CC-COMPONENTS-R-016` | `E911Modal` gates `Save & Continue` on the acknowledgment checkbox, disables both `Save & Continue` and `Cancel` while `onSaveAndContinue` is in flight (guarding against a double-click firing concurrent saves), shows a user-facing error and re-enables the buttons if the save rejects, and only `Cancel` (not the Dialog's built-in close button or Escape) dismisses the modal; checkbox/saving/error state resets when the modal closes. | An emergency-notification acknowledgment must not be skippable, must not double-submit against the preference API, and must give the agent visible recourse on failure. | `src/components/StationLogin/E911Modal/e911-modal.tsx` | `tests/components/StationLogin/E911Modal/e911-modal.test.tsx` (checkbox gating, save-in-flight button disabling, save-error display, close-only-via-Cancel) | None | PRESENT | -| `CC-COMPONENTS-R-017` | `CallControlCADComponent` renders Customer and Participants sections independently from the supplied Drop roster, so Customer departure removes only Customer while any supported non-customer row remains. Roster presence alone controls the trigger; `null` restores the Customer-only UI. It counts visible non-customer rows (including Supervisor and a ringing or answered pre-merge Entry Point destination), keeps the participant count and postfix chevron on one non-wrapping trigger row, removes participant-type sublabels, uses bold headings and aligned name/action rows, marks the primary agent with Momentum's `primary-participant-regular` icon while retaining `meet-regular` for other rows, keeps non-owner/Supervisor rows non-actionable, and shows a disabled owner action before Entry Point conference merge. The compact tertiary participant trigger uses Momentum secondary-button background tokens in normal, hover, pressed, and expanded states so it remains identifiable without custom chrome. The component receives request/confirmation state and handlers from `useCallControl`; it owns only presentation and focus fallback. The roster uses Momentum Design `Button`, `Popover`, `List`, `ListItem`, `Icon`, `Text`, and `Divider`, while Customer confirmation uses Momentum Design `Dialog` and footer `Button` components. Before Customer confirmation opens, the participant Popover closes so only one focus-trapped overlay is active; the Dialog is viewport-positioned and focus is explicitly restored to the stable participant trigger, with end-call fallback if hydration removes the roster. Styling is limited to compact layout, sizing, overflow/truncation, overlay positioning, and Momentum theme-token colors. Exact visible polite success/assertive failure feedback stays outside the roster, and no duplicate screen-reader announcer is added. Standard `CallControlComponent` does not render this roster. | Participant removal must be explicit, accessible, owner-aware, visually consistent with Momentum Design, and resilient when Customer or the final roster row disappears after hydration. | `src/components/task/CallControlCAD/call-control-cad.tsx`, `src/components/task/CallControlCAD/call-control-cad.types.ts`, `src/components/task/task.types.ts` | `tests/components/task/CallControlCAD/call-control-cad.tsx` (`conference participant Drop`, visibility regression) | Eligibility and invocation are supplied by `@webex/cc-store`/`@webex/cc-task`; backend authorization remains mandatory. | PRESENT | -| `CC-COMPONENTS-R-018` | Consult/transfer paginated hooks must keep SDK response rows directly, append later pages without sorting/filtering/reprojection, and render `ContactServiceQueue`, `EntryPointRecord`, and dial-number arrays in received order. | The component must preserve backend-selected order and established SDK entity records instead of introducing a second list policy or destination abstraction. | `src/components/task/task.types.ts`, `src/components/task/CallControl/CallControlCustom/consult-transfer-popover-hooks.ts` | `tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx`, `tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.snapshot.tsx` | Buddy-agent text search remains a presentational filter over the already ordered agent response. | PRESENT | -| `CC-COMPONENTS-R-019` | wxApp thick-client telephony: `CallControlComponent` accepts optional `sendDtmf(digit)` (defaults to no-op), renders `CallControlDtmfKeypad` in the Keypad popover (keyed by `currentTask.data.interactionId` so dial state resets on task switch; popover closes when interaction ID changes), serializes in-flight DTMF transmissions and cancels/discards queued digits on unmount or interaction change via a generation token (stale queue entries must not invoke `onDigitPress`), disables keypad input when the control is disabled, and applies `applyWxAppTelephonyControlVisibility` to force-show Mute/Keypad when wxApp is engaged and SDK `isEnabled` is true (even if `isVisible` is false); suppresses visible+disabled ghost controls for Extension agents when wxApp is not engaged. | DTMF must not drop digits under rapid input; mute/keypad must match wxApp session state without widgets calling telephony REST directly; keypad must not leak prior-call digits across interaction changes; queued DTMF must not transmit on a different interaction after task switch. | `src/components/task/CallControl/call-control.tsx`, `src/components/task/CallControl/call-control-dtmf-keypad.tsx`, `src/components/task/CallControl/call-control.utils.ts`, `src/components/task/task.types.ts` | `tests/components/task/CallControl/call-control-dtmf-keypad.tsx`, `tests/components/task/CallControl/call-control.utils.tsx` | Visibility gating reads `enableWxBetterTogether` + engaged call id from widget props; store owns mute sync events. | PRESENT | +| ID | WHAT | WHY | Source Evidence | Test / Example Evidence | Assumptions / Gaps | Confidence | +| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | +| `CC-COMPONENTS-R-001` | Components are pure presentational: they receive data + callbacks via props and never read the MobX store or call the SDK directly. | Keeps presentation decoupled from state/SDK so components are testable in isolation and reusable across widgets. | `src/components/StationLogin/station-login.tsx`, `src/components/UserState/user-state.tsx`, `src/components/task/CallControl/call-control.tsx` (props-only; no `@webex/cc-store` singleton import) | `tests/components/StationLogin/station-login.tsx` (renders from props, mocks callbacks) | None | PRESENT | +| `CC-COMPONENTS-R-002` | The public React surface is exactly the 16 top-level React symbols exported from `index.ts` plus the re-exported type/value barrels for StationLogin, UserState, task, Campaign, AIAssistant, AISummary, and wxApp telephony utilities. | Defines the supported import surface; consumers must not import internal subcomponents. | `src/index.ts`, `src/components/AISummary/index.ts`, `src/components/AISummary/ai-summary.constants.ts`, `src/components/AISummary/ai-summary.types.ts` | Import sites across `tests/components/**`, including `tests/components/AISummary/ai-summary.tsx` (`exports a unique display ordinal and inserts Outcome without reordering SDK sections`) | No single test asserts the complete barrel inventory; `E911Modal`, `AIAssistantComponent`, `TelephonyActionToast`, `WxAppOfferActionError`, and `AISummary` are not wrapped in `wc.ts` | PRESENT | +| `CC-COMPONENTS-R-003` | `StationLoginComponent` invokes the supplied callbacks (`login`, `setDeviceType`, `setDialNumber`, `handleContinue`, `saveLoginOptions`) on the matching user action and surfaces `loginFailure`/`saveError` as error UI. | Login intent and errors must propagate to the widget layer without the component owning login logic. | `src/components/StationLogin/station-login.tsx`, `src/components/StationLogin/station-login.utils.tsx` | `tests/components/StationLogin/station-login.tsx` (`calls login function...`, `renders login failure when passed`, `renders save error when passed`) | None | PRESENT | +| `CC-COMPONENTS-R-004` | `StationLoginComponent` hides the Desktop login option when `hideDesktopLogin` is true (in both login and profile mode) and shows it when false/undefined. | Deployments can disable Desktop login; must be honored consistently across modes. | `src/components/StationLogin/station-login.tsx`, `src/components/StationLogin/station-login.utils.tsx` | `tests/components/StationLogin/station-login.tsx` (`hides Desktop login option when hideDesktopLogin is true`, `... when false`, `... when undefined`, `... in profile mode`) | None | PRESENT | +| `CC-COMPONENTS-R-005` | `UserStateComponent` renders idle codes sorted/built into the dropdown, reflects `currentState`/`elapsedTime`, and calls `setAgentStatus(auxCodeId)` on selection; error-triggering idle codes are styled distinctly. | Agent must change state and see correct current state + timing; error idle codes need visual emphasis. | `src/components/UserState/user-state.tsx`, `src/components/UserState/user-state.utils.ts` (`buildDropdownItems`, `sortDropdownItems`, `handleSelectionChange`, `getDropdownClass`) | `tests/components/UserState/user-state.tsx`, `tests/components/UserState/user-state.utils.tsx` | None | PRESENT | +| `CC-COMPONENTS-R-006` | `CallControlComponent` builds its button set from Task controls, passes the matching SDK-ordered `consultTransferDestinations` action array into the popover, and routes button presses to the matching callback; wrapup requires selecting a reason. `CallControlComponentProps.aiSummary?: CallControlAISummaryProps` can supply consult/transfer summaries, a post-call `WrapUpSummaryView`, `requestMidCallSummary`, `onPostCallReasonCommit`, and `sendMidCallSummaryBeforeAction`. Voice consult/transfer popovers call `requestMidCallSummary` from their open effect with fulfillment and rejection observers; omitted summaries keep the destination UI mounted until pending/content/error arrives. The latched popover layout is one of voice radios, non-voice pills, or the existing-party action confirmation used by Transfer/Transfer Conference and Merge. For destinations and existing-party confirmation, the shared mid-call adapter awaits the summary send Promise (`sent`/`failed`/`blocked`/`stale`) before calling `consultCall`, `transferCall`, `consultTransfer`, or `consultConference` exactly once; re-entry while the response send is pending is ignored. While a rendered summary request is pending, destination rows, manual quick actions, and existing-party confirmations remain focusable with `aria-disabled` and their activation handlers return before telephony. Content summaries call `onViewed(contentRevision)` for genuine visible content/replacements and remounts; accepted local edit revisions do not count as views. Synchronous and Promise-returning telephony suppliers are normalized at the boundary, rejected telephony Promises are consumed, and consult target name/type are committed only after `consultCall` fulfills. Transfer/Transfer Conference opens TRANSFER preparation; Merge reuses the current CONSULT summary or starts initial CONSULT preparation for an eligible fresh omitted view. Only absent/ineligible views keep the immediate legacy callback path, including the CAD consult surface. Direct React `wrapupCall(reason, id)` returns `Promise`; `handleWrapupCall` keeps explicit closed results authoritative, resolves `{wrapup: 'failed'}` for missing selections, thrown callbacks, and rejected callbacks, and normalizes legacy custom-element undefined/non-result callback returns to `{wrapup: 'succeeded', response: 'not-required'}` so selections clear. UI callers receive a never-rejecting completion Promise. CallControl shares one wrap-up pending latch across `WrapUpSummary` and the legacy Submit button, passes it as `completionPending`, and ignores re-entry until the same completion settles. The popover renders destination categories in supplied order and may only remove Dial Number/Entry Point through explicit host hide options. | Call control must reflect SDK decisions without reconstructing profile/media/direction policy, preserve a stable telephony ordering around AI summary settlement, give direct React hosts an explicit async wrap-up result contract, and preserve legacy custom-element fire-and-forget wrap-up callbacks. | `src/components/task/task.types.ts` (`CallControlAISummaryProps`, `WrapupCompletionResult`, `CallControlComponentProps`, `ConsultTransferDestinationLayout`), `src/components/task/CallControl/call-control.tsx`, `src/components/task/CallControl/call-control.utils.ts`, `src/components/task/CallControl/CallControlCustom/call-control-consult.tsx`, `src/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx`, `src/wc.ts` | `tests/components/task/CallControl/call-control.tsx` (`requests an initiating summary when a voice consult popover opens`, destination response-before-telephony, deferred consult state, existing-party Transfer/Merge confirmations, existing-party rejection consumption, eligible fresh existing-party preparation and absent/ineligible legacy paths, once-only post-call and legacy wrap-up completion), `tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx` (`auto-starts the matching mid-call summary request and observes rejection`, pending destination/quick-action guard, once-per-revision reveal recording, existing-party action variant), `tests/components/task/CallControl/CallControlCustom/call-control-consult.tsx` (CAD consult surface fresh/reused/absent summary paths), `tests/components/task/CallControl/call-control.utils.tsx` (shared adapter ordering/re-entry, async rejection consumption, delayed consult-state commit plus wrap-up contract), `tests/wc.tsx` (component-cc call-control tag/function mapping and void host callback compatibility) | Consumers cannot enable a destination omitted by the SDK. The async `wrapupCall` signature remains required for direct React consumers; custom-element hosts retain the legacy fire-and-forget path. | PRESENT | +| `CC-COMPONENTS-R-007` | `CallControlCADComponent` renders the customer/queue/caller header and an agent-viewable CAD global variables panel, and renders the campaign call icon + "Campaign call" label when `isCampaignCall` is true. | CAD/header info and campaign branding must be visible to the agent during a call. | `src/components/task/CallControlCAD/call-control-cad.tsx`, `src/components/task/Task/task.utils.ts` (`getAgentViewableGlobalVariables`) | `tests/components/task/CallControlCAD/call-control-cad.tsx` | None | PRESENT | +| `CC-COMPONENTS-R-008` | `IncomingTaskComponent` renders the standard `Task` with Answer/Decline when an `incomingTask` is present and renders nothing (hidden) when it is absent; Accept/Decline invoke `accept(task)`/`reject(task)`. | Avoids a stray empty notification when no task; routes accept/decline intent up. | `src/components/task/IncomingTask/incoming-task.tsx`, `src/components/task/IncomingTask/incoming-task.utils.tsx` (`extractIncomingTaskData`) | `tests/components/task/IncomingTask/incoming-task.tsx`, `tests/components/task/IncomingTask/incoming-task.utils.tsx` | None | PRESENT | +| `CC-COMPONENTS-R-009` | `TaskListComponent` renders nothing when the task list is empty, otherwise renders one row per task; campaign preview tasks render `CampaignTask` (instead of `Task`) only when `hasCampaignPreviewEnabled` (default true) and the task is a campaign preview. | List must collapse when empty and switch row UI for campaign previews per the feature flag. | `src/components/task/TaskList/task-list.tsx`, `src/components/task/TaskList/task-list.utils.ts` (`isTaskListEmpty`, `getTasksArray`, `isCampaignPreviewTask`, `getActiveCampaignPreviewId`) | `tests/components/task/TaskList/task-list.tsx`, `tests/components/task/TaskList/task-list.utils.tsx` | None | PRESENT | +| `CC-COMPONENTS-R-010` | `OutdialCallComponent` validates the entered destination, supports dialpad / ANI / address-book tabs, disables the outdial action while a telephony task is active, and calls `startOutdial(destination, origin?)`. | Outbound dialing must validate input and not start a second call over an active one. | `src/components/task/OutdialCall/outdial-call.tsx`, `src/components/task/OutdialCall/constants.ts` | `tests/components/task/OutdialCall/out-dial-call.tsx` | None | PRESENT | +| `CC-COMPONENTS-R-011` | `RealTimeTranscriptComponent` sorts `liveTranscriptEntries` by ascending `timestamp` before rendering and shows an empty-state message when there are no entries. | Transcript must read chronologically and degrade gracefully when empty. | `src/components/task/RealTimeTranscript/real-time-transcript.tsx` | `tests/components/task/RealtimeTranscript/realtime-transcript.tsx` | None | PRESENT | +| `CC-COMPONENTS-R-012` | Campaign preview UI: `CampaignTaskComponent` renders accept/skip/remove + countdown and triggers the configured auto-action on timeout; failed actions open `CampaignErrorDialogComponent` with the mapped `CampaignErrorType`; `CampaignCountdownComponent` fires `onTimeout` at zero. | Campaign preview offers are time-boxed; failures and timeouts must be surfaced and auto-handled. | `src/components/task/CampaignTask/campaign-task.tsx`, `src/components/task/CampaignErrorDialog/campaign-error-dialog.tsx` (+ `.types.ts` `CAMPAIGN_ACTION_ERROR_MAP`, `ERROR_TITLES`), `src/components/task/CampaignCountdown/campaign-countdown.tsx` | `tests/components/task/CampaignTask/campaign-task.test.tsx`, `tests/components/task/CampaignErrorDialog/campaign-error-dialog.tsx`, `tests/components/task/CampaignCountdown/campaign-countdown.tsx` | None | PRESENT | +| `CC-COMPONENTS-R-013` | `formatTime` renders `HH:MM:SS` for durations ≥ 1 hour and `MM:SS` otherwise, with zero-padding; `getMediaTypeInfo` maps media type/channel to icon/label/className/brand-visual, falling back to telephony/chat defaults. | Timers and media badges must format consistently across all task components. | `src/utils/index.ts` | `tests/components/task/CallControl/call-control.utils.tsx`, snapshot tests under `tests/components/task/**/__snapshots__/` exercise formatted output | No dedicated `tests/utils/` file found for `formatTime`/`getMediaTypeInfo` (exercised indirectly via component/utils tests) | WEAK | +| `CC-COMPONENTS-R-014` | `useIntersectionObserver` reports element visibility for infinite-scroll/lazy paths (e.g. outdial address-book paging). | Paged lists must load more on scroll without per-component observer wiring. | `src/hooks/useIntersectionObserver.ts` | `tests/hooks/useIntersectionObserver.test.ts` | None | PRESENT | +| `CC-COMPONENTS-R-015` | Widget-facing component exports are wrapped with the `withMetrics` HOC so mount/usage metrics are tracked uniformly. Shared/inline exports (`E911Modal`, `TelephonyActionToast`, `WxAppOfferActionError`, `AISummary`) are exported without `withMetrics` today. | Consistent telemetry across widgets without requiring leaf surfaces to emit standalone metrics. | `withMetrics` import + wrap in `src/components/StationLogin/station-login.tsx`, `src/components/UserState/user-state.tsx`, `src/components/task/CallControl/call-control.tsx`, `src/components/task/CallControlCAD/call-control-cad.tsx`, `src/components/AIAssistant/ai-assistant.tsx`, and task/campaign component files | Covered indirectly by each component's render test | No test asserts the HOC wrapping itself; unwrapped shared/inline exports must not be treated as widget metric surfaces | WEAK | +| `CC-COMPONENTS-R-016` | `E911Modal` gates `Save & Continue` on the acknowledgment checkbox, disables both `Save & Continue` and `Cancel` while `onSaveAndContinue` is in flight (guarding against a double-click firing concurrent saves), shows a user-facing error and re-enables the buttons if the save rejects, and only `Cancel` (not the Dialog's built-in close button or Escape) dismisses the modal; checkbox/saving/error state resets when the modal closes. | An emergency-notification acknowledgment must not be skippable, must not double-submit against the preference API, and must give the agent visible recourse on failure. | `src/components/StationLogin/E911Modal/e911-modal.tsx` | `tests/components/StationLogin/E911Modal/e911-modal.test.tsx` (checkbox gating, save-in-flight button disabling, save-error display, close-only-via-Cancel) | None | PRESENT | +| `CC-COMPONENTS-R-017` | `CallControlCADComponent` renders Customer and Participants sections independently from the supplied Drop roster, so Customer departure removes only Customer while any supported non-customer row remains. Roster presence alone controls the trigger; `null` restores the Customer-only UI. It counts visible non-customer rows (including Supervisor and a ringing or answered pre-merge Entry Point destination), keeps the participant count and postfix chevron on one non-wrapping trigger row, removes participant-type sublabels, uses bold headings and aligned name/action rows, marks the primary agent with Momentum's `primary-participant-regular` icon while retaining `meet-regular` for other rows, keeps non-owner/Supervisor rows non-actionable, and shows a disabled owner action before Entry Point conference merge. The compact tertiary participant trigger uses Momentum secondary-button background tokens in normal, hover, pressed, and expanded states so it remains identifiable without custom chrome. The component receives request/confirmation state and handlers from `useCallControl`; it owns only presentation and focus fallback. The roster uses Momentum Design `Button`, `Popover`, `List`, `ListItem`, `Icon`, `Text`, and `Divider`, while Customer confirmation uses Momentum Design `Dialog` and footer `Button` components. Before Customer confirmation opens, the participant Popover closes so only one focus-trapped overlay is active; the Dialog is viewport-positioned and focus is explicitly restored to the stable participant trigger, with end-call fallback if hydration removes the roster. Styling is limited to compact layout, sizing, overflow/truncation, overlay positioning, and Momentum theme-token colors. Exact visible polite success/assertive failure feedback stays outside the roster, and no duplicate screen-reader announcer is added. Standard `CallControlComponent` does not render this roster. | Participant removal must be explicit, accessible, owner-aware, visually consistent with Momentum Design, and resilient when Customer or the final roster row disappears after hydration. | `src/components/task/CallControlCAD/call-control-cad.tsx`, `src/components/task/CallControlCAD/call-control-cad.types.ts`, `src/components/task/task.types.ts` | `tests/components/task/CallControlCAD/call-control-cad.tsx` (`conference participant Drop`, visibility regression) | Eligibility and invocation are supplied by `@webex/cc-store`/`@webex/cc-task`; backend authorization remains mandatory. | PRESENT | +| `CC-COMPONENTS-R-018` | Consult/transfer paginated hooks must keep SDK response rows directly, append later pages without sorting/filtering/reprojection, and render `ContactServiceQueue`, `EntryPointRecord`, and dial-number arrays in received order. | The component must preserve backend-selected order and established SDK entity records instead of introducing a second list policy or destination abstraction. | `src/components/task/task.types.ts`, `src/components/task/CallControl/CallControlCustom/consult-transfer-popover-hooks.ts` | `tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx`, `tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.snapshot.tsx` | Buddy-agent text search remains a presentational filter over the already ordered agent response. | PRESENT | +| `CC-COMPONENTS-R-019` | wxApp thick-client telephony: `CallControlComponent` accepts optional `sendDtmf(digit)` (defaults to no-op), renders `CallControlDtmfKeypad` in the Keypad popover (keyed by `currentTask.data.interactionId` so dial state resets on task switch; popover closes when interaction ID changes), serializes in-flight DTMF transmissions and cancels/discards queued digits on unmount or interaction change via a generation token (stale queue entries must not invoke `onDigitPress`), disables keypad input when the control is disabled, and applies `applyWxAppTelephonyControlVisibility` to force-show Mute/Keypad when wxApp is engaged and SDK `isEnabled` is true (even if `isVisible` is false); suppresses visible+disabled ghost controls for Extension agents when wxApp is not engaged. | DTMF must not drop digits under rapid input; mute/keypad must match wxApp session state without widgets calling telephony REST directly; keypad must not leak prior-call digits across interaction changes; queued DTMF must not transmit on a different interaction after task switch. | `src/components/task/CallControl/call-control.tsx`, `src/components/task/CallControl/call-control-dtmf-keypad.tsx`, `src/components/task/CallControl/call-control.utils.ts`, `src/components/task/task.types.ts` | `tests/components/task/CallControl/call-control-dtmf-keypad.tsx`, `tests/components/task/CallControl/call-control.utils.tsx` | Visibility gating reads `enableWxBetterTogether` + engaged call id from widget props; store owns mute sync events. | PRESENT | +| `CC-COMPONENTS-R-020` | `AIAssistantComponent` optionally renders a receiver-summary branch independently from the Real-time Assist flag when the host supplies an active receiver summary. A native `View summary` trigger appears only in the closed launcher slot or minimized chrome slot, calls `open` from closed chrome and `restore` from minimized chrome, and captures the successor/header/panel focus fallback in layout effects for activation, fallback, omission, interaction change, and owner change. The exact open panel ref is passed to shared `AISummary`, and owner-level fallback restores the panel when the entire receiver branch unmounts while focus was inside it. Receiver content renders through a memoized receiver-only display projection that recursively removes `ActionSet`/`Action.*` nodes, `actions` arrays, and `selectAction` without changing generic Real-time Assist cards, and only the card renderer wrapper carries `dir="auto"`, while receiver actions remain LTR and panel chrome remains unchanged. The shared `AISummary` receiver action row is present only after usable rendered content; unavailable/error/generating/fallback states omit copy/feedback controls, abort empty copy text, and preserve connected action nodes across valid content revisions. `AISummary` props accept only visible presentation states (`content`, `unavailable`, `generic-error`, `generating`); `omitted` stays in container view-models and must be narrowed away before mounting the component. | Receiving-agent summaries must share the AI Assistant panel without moving SDK or store responsibilities into presentation code, must not expose embedded card actions, must resolve mixed-direction receiver text locally, and must keep focus deterministic as receiver-only controls appear and disappear. | `src/components/AIAssistant/ai-assistant.tsx`, `src/components/AIAssistant/ai-assistant.types.ts`, `src/components/AIAssistant/ai-assistant.styles.scss`, `src/components/AISummary/ai-summary.tsx`, `src/components/AISummary/ai-summary.types.ts` | `tests/components/AIAssistant/ai-assistant.tsx` (`renders the exact native receiver trigger only in closed and minimized chrome slots`, `opens the receiver branch from closed and minimized chrome and recovers focus into the panel`, `applies automatic direction to mixed-direction receiver summary content only`, `keeps receiver card actions inert and copies only visible rendered card text`, `opens the receiver branch when Real-time Assist is disabled`, `removes receiver actions when the renderer falls back and restores panel focus`, `restores panel focus when the whole receiver summary branch is removed`, `resets the receiver branch for ordinary chrome actions and owner changes`); `tests/components/AISummary/ai-summary.tsx` (`renders visible output for every public presentation state`, `omits receiver actions for %s state`, `recovers focus to the containing panel when receiver actions are removed`) | The store/container owns whether a receiver summary object is present. Browser-level focus/overlay behavior remains in the UX evidence gate. | PRESENT | +| `CC-COMPONENTS-R-021` | `AdaptiveCardRenderer` prepares display-only cards before parse/render with a deny-by-default resource sanitizer: only supported bundled Momentum resource URLs are kept/re-written, while unallowlisted `Image.url`, untyped `ImageSet.images[].url`, `backgroundImage` string/object URLs, `Action.*.iconUrl`, `Media.poster`, and `Media.sources` are removed before Adaptive Cards can create DOM. Renderability is based on visible content only: nonblank `TextBlock`, `RichTextBlock` string or `TextRun` inlines, `FactSet` title/value text, or permitted images; bare metadata/action/title-only shells use the exact caller fallback, and the renderer error boundary resets when replacement card inputs change. | Receiver summaries and existing Real-time Assist cards must avoid remote resource egress while preserving display text and non-feedback card structure. | `src/components/AIAssistant/AdaptiveCardRenderer/adaptive-card-renderer.tsx`, `src/components/AIAssistant/AdaptiveCardRenderer/adaptive-card-renderer.utils.ts` | `tests/components/AIAssistant/adaptive-card-renderer.utils.test.ts` (`strips remote images, image sets, media sources, action icons, and backgrounds`, `classifies %s renderability`); `tests/components/AIAssistant/adaptive-card-renderer.test.tsx` (`shows the unavailable fallback without parsing or requesting a remote image when sanitization leaves no content`, `keeps text renderable after stripping a remote image before parsing`, `strips image-set and media remote resources before parsing`, `resets the error boundary when a replacement card becomes renderable`) | The renderer still delegates card parsing/layout to `adaptivecards`; browser-level network observation remains in the UX evidence gate. | PRESENT | +| `CC-COMPONENTS-R-022` | `AISummary` uses one titled, bordered content/action surface with keyed auto-sized editors, read-only Outcome, loading/error affordances, and bottom-aligned `AI-generated` attribution beside copy/feedback actions. SDK-derived section labels, ordinary values, and inline/block bulleted list containers and items each carry `dir="auto"` at their own dynamic text boundary while surrounding layout wrappers preserve host direction. Momentum icon actions retain `Copy Summary`, `This is helpful`, and `This isn't helpful` accessible names and one custom hover/focus tooltip each, with no native `title` duplicate. Each tooltip stays open while the action trigger or tooltip wrapper is hovered, allows pointer interaction, and Escape dismisses only the tooltip while retaining focus. Feedback status IDREFs use an instance-local React `useId()`-derived id. Feedback renders in a sealed 44px by 20px control group with two 20px controls and a CSS 4px separator after attribution. Selected feedback exposes `aria-pressed`; feedback and Retry handlers catch synchronous callback throws, normalize Promise and non-thenable results, silently terminate fulfillment/rejection errors, preserve prior selection on failed or malformed results, and mid-call feedback paints only when the initiating revision is still mounted and the latest feedback attempt. Structured copy emits exact non-empty `Label: value` blocks separated by two newlines in projected order, empty plain/structured/receiver output never reaches Clipboard, and each Clipboard fulfillment is bound to the mounted component, current content revision, and latest attempt before recording, painting, or starting the 1,500 ms reset timer. Copy confirmation survives pointer re-entry, resets on the exact timer or blur, resets before replacement paint without an initial idle callback, and unmount/superseded attempts clear or suppress timers without state updates. Focus restoration is one-shot: a removed focused control moves to the next still-connected summary control or the required live containing-panel ref only when focus has not already moved to another connected element, and the snapshot is cleared after that decision. | Editable and read-only content must share faithful presentation without changing SDK keys, copy/feedback semantics, or host layout direction. | `src/components/AISummary/ai-summary.tsx`, `src/components/AISummary/ai-summary.styles.scss` | `tests/components/AISummary/ai-summary.tsx` (`copies structured sections as exact label/value blocks with and without Outcome`, `blocks empty plain, structured and receiver copy output before Clipboard access`, `drops delayed clipboard fulfillment after unmount without recording or scheduling timers`, `resets confirmation on content replacement without an initial idle callback`, `ignores superseded clipboard attempts and confirms only the current one`, `clears the accepted copy timer on unmount without reporting idle`, `contains synchronous feedback and Retry throws without changing selection`, `normalizes non-thenable feedback and Retry returns`, `preserves feedback selection for rejected and malformed callback results`, `ignores stale mid-call feedback confirmations after content replacement`, `allows only the latest mid-call feedback attempt to paint`, `settles mid-call feedback confirmation after unmount without a window error`, `applies automatic direction to mixed-direction structured summary text boundaries`, `moves focus to the next summary control when a focused editor is removed`, `keeps external focus after a normal blur out of the summary`, `consumes a panel fallback once and does not steal focus on later renders`), `playwright/tests/ai-summary-test.spec.ts` | Store/container callbacks still own acceptance. | PRESENT | +| `CC-COMPONENTS-R-023` | `WrapUpSummary` renders `Wrap up interaction`, guidance, a Momentum searchable reason picker and the exact `Complete Wrap-Up` action text/name/title. The picker is labelled by a React `useId()` heading, starts with no selection unless `initialReasonId` matches a supplied reason, and uses Momentum `RadioGroupNext` roving focus so arrows only change selection while pointer, Enter/Space, and group exit commit the current revision once. While a summary request is pending, the reason group remains focusable with `aria-disabled` and selection/commit paths return without changing the reason or issuing another commit. Generated structured summaries collapse reasons independently of editability; the selected-reason trigger reopens them, expansion moves focus to the search input, and a reason commit that collapses the list restores focus to the selected-reason trigger. The wrap-up panel ref is the shared `AISummary` fallback and the owner fallback when a focused summary subtree is removed. `CallControlComponent` passes the store-projected post-call `completionEscape`; `WrapUpSummary` defers Complete only for the initial pending no-content `generating` state without that escape, so Retry after terminal no-draft error/unavailable and retained-content regeneration both keep a selected reason completable. `WrapUpSummary` also owns a local completion Promise guard with rejection handling, disables Complete/reason controls while local or parent completion is pending, and treats store-projected `controlsDisabled` as a read-only frozen/not-confirmed selection. | Preserve reason selection and editing without a fixture-only read-only path while keeping the terminal-error completion policy anchored to the store generation that owns it and preventing duplicate wrap-up activation. | `src/components/task/CallControl/call-control.tsx`, `src/components/task/CallControl/CallControlCustom/wrap-up-summary.tsx` | `tests/components/task/CallControl/call-control.tsx` (`uses the projected post-call completion escape while retry is pending`, once-only post-call wrap-up completion), `tests/components/task/CallControl/CallControlCustom/wrap-up-summary.tsx` (`commits only the final selected reason after roving-focus arrow traversal and blur`, pointer/activation-key once-only commit, pending selection/commit guard, `keeps Complete Wrap-Up enabled after a terminal summary error during retry when completion escape is set`, `defers completion only for pending initial generation without retained content`, local completion re-entry guard, frozen not-confirmed read-only state, `moves focus between the collapsed reason trigger and expanded search controls`, `restores focus to the wrap-up panel when the summary subtree is removed`), `playwright/tests/ai-summary-test.spec.ts` | Store owns generation/state data and supplies `completionEscape`/`controlsDisabled`; browser visual render remains in the UX evidence gate. | PRESENT | ## Design Overview -Every component follows the same shape: a typed function component destructures props, derives display data through pure helpers in a co-located `*.utils.ts(x)`, renders Momentum primitives, and calls back through callback props on user interaction. Local `useState` holds only transient UI (open menus, selected-but-not-yet-submitted values, input text) — never domain state. Top-level components are wrapped in `withMetrics`. This keeps each component unit-testable with plain props and jest mocks and is the reason the archived "presentational pattern" guidance still holds. +### AI summary visual repair (2026-09-25) + +AISummary uses one titled shared bordered content/action surface for both editable +and read-only sections. Editors retain SDK keys and auto-size without inner +scrollbars; their internal textarea props explicitly require a controlled string +value, so the resize effect remains typed and lint-validated without disabling +prop checks. Outcome remains read-only, and `POST_CALL_DISPLAY_SECTION_ORDER` +must compile against the full `AISummaryDisplaySectionKey` union while rejecting +duplicate runtime entries before projection can fall back to an unknown ordinal. +Copy/feedback use bundled Momentum icons, retaining accessible labels, +pointer-reachable hover/focus tooltips that Escape dismisses without moving +focus, including when pointer hover opens the tooltip while focus remains +outside the summary. A document capture listener exists only while a tooltip +is visible and is removed on dismissal or unmount. The controls retain selected feedback, stable +`useId()`-derived feedback descriptions, and +clipboard confirmation without native `title` tooltips duplicating the custom +tooltip bubble. Loading and error states render visible affordance icons with +state-specific title typography, and content states keep `AI-generated` +attribution in the same action row as the source-backed screenshots. +The visual fixture uses the real post-call normalizer and controlled edits rather +than read-only flags or unsupported keys to imitate target screenshots. +AI summary label metadata is keyed by the SDK section keys only: mid-call +defines `reasonForTransferOrConsult`, `additionalContext`, and +`keyActionsTaken`, while post-call defines `initialContactReason`, +`additionalContactReasons`, `additionalContext`, `keyActionsTaken`, and +`nextSteps`; `resolution` is the display-only Outcome row. Components resolve +labels from these registries and never use store state, SDK titles, or legacy +local keys to rename or serialize a response field. +Structured sections now display compact inline label/value previews. Activating a +preview by pointer or keyboard opens that section's existing controlled native +textarea; blur returns to the preview. This is production behavior, not a capture +substitution. The SDK key, revision guard, original plain-text bytes and read-only +Outcome remain unchanged. Disabled controls cannot enter editing. Structured +copy uses exact non-empty `Label: value` blocks separated by two newlines, and +plain or receiver empty output is rejected before Clipboard access. Copy +confirmation uses the target's checkmark-only circular treatment with accessible +confirmation text; the accepted current attempt keeps the 1500ms timer and blur +reset, while unmount, replacement, or superseded attempts cannot record, paint, +or schedule/retain a timer. Replacement resets run before paint and the initial +mount does not emit an idle callback. +Feedback and Retry callbacks are invoked inside synchronous guards, settled +through `Promise.resolve`, and ended with terminal rejection handling so thrown +callbacks, non-thenable returns, rejected callbacks, and malformed feedback +results leave the existing visual state unchanged. Mid-call feedback also carries +its own monotonic attempt token and initiating content revision so late +confirmations cannot paint after replacement content, a newer attempt, or +unmount. +The shared summary proof suite uses literal en-US expectations, all-mode/state +rendering checks, role-negative prop checks, escaped hostile text, no-live-region +assertions, copy-reset edge cases, and same-instance focus removal tests so the +component behavior is executable rather than inferred from constants or snapshots. +Every SDK-derived label, ordinary value, and inline/block bulleted list boundary +uses `dir="auto"` on the text-bearing element or item instead of a shared summary +wrapper, so mixed RTL/LTR content resolves locally while the surrounding control +layout keeps the host direction. Receiver adaptive-card summary content similarly +sets `dir="auto"` only around the adaptive-card renderer; its sibling action row +retains LTR layout independently of the card's first strong text direction. +Generated structured summaries collapse the reason list independently of section +editability; the selected-reason trigger reopens it without disabling summary edits. +The expanded reason picker uses the Momentum search input and `RadioGroupNext` +with native roving focus, no implicit first reason, and a pending-state +`aria-disabled` guard for selection and commit handlers. +That collapsed trigger stays visually aligned with the source search field: it +retains a bounded outline, placeholder-colored text and the same accessible +`Search wrap-up reasons` button role used to reopen the reason list. +The operator-approved layout clarification in `design/default/design_spec.md` +requires an 80vh shell with independent bounded destination/reason-list and +summary-content scroll regions, including at a 320 px host width. Headings, +summary actions and Complete Wrap-Up stay outside those regions and visible. +Compact-height layouts reduce spacing and reserve readable summary space rather +than allowing flex children to collapse to zero height. At heights of 330px or +less, the header, reason picker, and summary share an additional scroll region +above the fixed Complete Wrap-Up footer, preserving completion inside the 80vh +shell even when the fixed chrome would otherwise exceed the available height. +Reason-search clearing reuses the shared `CLEAR_SEARCH` message. Keyed +editors still auto-size without individual scrollbars, with neutral action-hover +treatment and a callout arrow matching the source-backed hover states. +Hosts may set `--cc-summary-panel-width` (default 30rem, capped by the viewport); +the production layout has no target-specific pixel dimensions. All summary text, +keyboard access, focus and existing callback semantics are preserved. The sample +harness captures a source-aligned panel crop within a larger host viewport and +checks landmark visibility before and after capture, including hover states. + +Voice consult/transfer uses Momentum RadioGroupNext before search, populated only +from available SDK destinations and existing host hide overrides. The voice +radio labels match the SDK category display copy (`Agents`, `Queues`, +`Dial Number`, `Entry Point`) mapped to the unchanged internal `CategoryType` +values. Voice +search uses the full mid-call placeholder without the legacy reload control in +that row; its 0.875rem input text keeps the full required placeholder readable at +the accepted source-frame width. Agent rows surface the available SDK `dn` as secondary text. +Nonvoice pills, internal category text, reload behavior, and the legacy +`Search...` placeholder are preserved. The operator approved the S01 exception +on 2026-09-25: Organization has no supported SDK destination mapping, so the +four supported destinations are preserved rather than fabricating an action. +This is an intentional target difference and must remain explicit in visual +comparison evidence. + +Open consult/transfer popover instances latch their initial destination layout. +AI-summary capability arrival/revocation changes only the summary subtree: the +destination search node, selected category, loaded result rows, focused action, +and query value remain connected. Voice-radio and nonvoice-pill modes apply the +same SDK destination order and host hide filters. The mounted popover also +issues the initial Agents load when that SDK destination opens with no rows, so +voice evidence does not depend solely on the outer popover callback before the +destination list exists. Pending summary requests leave +destination and existing-party action controls focusable with `aria-disabled` +while their handlers are inert. Summary content replacement restores focus to +the next summary control when possible and to the labelled popover root when the +summary controls disappear; closing the voice popover returns focus to the +opening trigger. + +Existing-party Transfer/Transfer Conference and Merge use optional summary-view +presence as the eligibility boundary: undefined preserves the legacy immediate +callback, while an eligible fresh `omitted` view still opens preparation. +Transfer starts canonical TRANSFER preparation; Merge starts initial CONSULT +preparation only for that fresh view and otherwise retains the existing CONSULT +content, pending request, or settled failure without an implicit retry. Both +main-call and consult-leg controls keep confirmation inert during generation +and response submission, then invoke telephony once after response settlement. + +Consult/transfer, wrap-up and AI Assistant receiver summaries pass live owning +panel refs into `AISummary` instead of render-time element snapshots. The +consult/transfer and wrap-up panel roots are programmatically focusable, +heading-labelled containers; if an owner removes an entire focused summary +subtree, the owner restores focus to that same connected panel root because the +unmounted shared summary cannot run a child fallback effect. + +Receiver card replacement preserves the mounted renderer and sibling action +nodes while rendered content remains usable. Renderer fallback or non-content +states still remove those actions and repair focus. Removing a focused View +summary trigger from closed or minimized chrome restores its surviving +successor, or the remaining launcher/restore action. Connected external focus +is not moved. + +Consult/transfer and post-call wrap-up record each visible content revision +once per panel mount through `onViewed(expectedRevision)`. The optional +`WrapUpSummaryView.onViewed` callback is forwarded by the task helper. Accepted +local edits advance the content revision for stale-action guards but are not +new views; subsequent generated content and a newly opened panel are recorded. +This tracking is presentation-local and adds no store state or SDK calls. + +Unit rejection cases await callback settlement and verify recorder, selection, +and window-error behavior; native unhandled rejections fail Jest's runner. +They do not treat a manually emitted event on Jest's sandbox process as proof +of native rejection observability. Browser rejection coverage must use a real +`Promise.reject` control before asserting production-path event absence. + +The pre-action adapter also invokes and awaits the response callback for +eligible `unavailable` and `generic-error` mid-call summaries, allowing the +store to send its SDK-supported `NOT_RECEIVED` response before telephony. +Generating, pending, disabled, and omitted paths remain guarded. A response +failure still permits exactly one subsequent telephony action. + +Voice headings reset the Momentum Text shadow-part margins; the summary heading +aligns its sparkle and text within that part rather than styling only the host. +Destination radios wrap responsively at the shared body size. A current Momentum +close icon calls the owning popover instance's hide action (the legacy popover +icon loader is not used for voice panels); search retains its real reload action. +Destination selection has a named button visible on hover or keyboard focus. +Summary hover buttons are circular; selected feedback is conveyed by the filled +icon without retaining hover paint. The feedback pair stays in a fixed 44px by +20px group of two 20px controls, separated from attribution by a 4px CSS dot. +Tooltips keep their source-sized padding, and Retry retains a pill outline. Plain +SDK values are never parsed into invented section keys or rich text just to +reproduce screenshot typography. + +Every component follows the same shape: a typed function component destructures props, derives display data through pure helpers in a co-located `*.utils.ts(x)`, renders Momentum primitives, and calls back through callback props on user interaction. Local `useState` holds only transient UI (open menus, selected-but-not-yet-submitted values, input text) — never domain state. Widget-facing component exports are wrapped in `withMetrics`; shared/inline exports are not metric surfaces today. This keeps each component unit-testable with plain props and jest mocks and is the reason the archived "presentational pattern" guidance still holds. Logic that is non-trivial or shared is pulled out of the JSX: per-component utils (`station-login.utils.tsx`, `call-control.utils.ts`, `task-list.utils.ts`, etc.) and library-wide utils (`src/utils/index.ts`: `formatTime`, `getMediaTypeInfo`, wxApp telephony visibility in `wxapp-telephony.utils.ts`). `task.types.ts` is the shared type hub for the task family — the larger `ControlProps`/`TaskProps` interfaces describe the full data set, and each component's public prop type is a `Pick` of the keys it actually uses, which is why the public surface is intentionally narrower than the interfaces. @@ -193,10 +389,11 @@ These components share one interaction pattern (props in → local UI state → Sequence coverage: -| Operation group | Diagram | Failure / recovery coverage | -| ----------------------------------------------------------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | -| User-action components (login, state change, call control, accept/decline, outdial) | "Props-in / callback-out interaction" | Error props (`loginFailure`, `saveError`) rendered as error UI; no internal retry — recovery owned by widget layer | -| Campaign preview offer (countdown + action) | "Campaign preview timeout & error" | Countdown timeout auto-action; failed action opens error dialog (alt branch) | +| Operation group | Diagram | Failure / recovery coverage | +| ----------------------------------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| User-action components (login, state change, call control, accept/decline, outdial) | "Props-in / callback-out interaction" | Error props (`loginFailure`, `saveError`) rendered as error UI; no internal retry — recovery owned by widget layer | +| CallControl consult/transfer with mid-call summary | "AI summary request on open and settlement before telephony" | `requestMidCallSummary` is observed on voice/existing-party popover open where applicable; `sendMidCallSummaryBeforeAction` resolves `sent`/`failed`/`blocked`/`stale` before the destination or existing-party telephony callback; callback still runs once | +| Campaign preview offer (countdown + action) | "Campaign preview timeout & error" | Countdown timeout auto-action; failed action opens error dialog (alt branch) | ```mermaid sequenceDiagram @@ -216,6 +413,20 @@ sequenceDiagram end ``` +```mermaid +sequenceDiagram + participant User + participant CC as CallControlComponent + participant Summary as aiSummary.sendMidCallSummaryBeforeAction + participant Widget as Widget callback + User->>CC: select consult/transfer target + alt summary content is available + CC->>Summary: send(actionType, contentRevision) + Summary-->>CC: outcome sent/failed/blocked/stale + end + CC->>Widget: consultCall(...) or transferCall(...) +``` + ```mermaid sequenceDiagram participant User @@ -254,6 +465,11 @@ graph TD Index --> CampCountdown[CampaignCountdownComponent] Index --> CampError[CampaignErrorDialogComponent] Index --> CampTask[CampaignTaskComponent] + Index --> E911[E911Modal] + Index --> AIAssistant[AIAssistantComponent] + Index --> AISummary[AISummary] + Index --> Toast[TelephonyActionToast] + Index --> OfferError[WxAppOfferActionError] IncomingTask --> Task[Task row] TaskList --> Task @@ -263,6 +479,10 @@ graph TD CallControlCAD --> TaskTimer CallControl --> ConsultPopover[ConsultTransferPopover + CallControlCustom] CallControl --> AutoWrap[AutoWrapupTimer] + CallControl --> WrapUpSummary + WrapUpSummary --> AISummary + AIAssistant --> AdaptiveCardRenderer + AIAssistant --> AISummary CampTask --> CampListItem[CampaignTaskListItem] CampTask --> CampPopover[CampaignTaskPopover] CampTask --> CampCountdown @@ -270,13 +490,13 @@ graph TD CampListItem --> CampCountdown ``` -The exported components are leaves of `index.ts`. Composition is one-directional: `Task` is the shared row reused by `IncomingTask` and `TaskList`; `CallControlCAD` decorates `CallControl` with a CAD header, `GlobalVariablesPanel`, and `TaskTimer`; `CallControl` owns the consult/transfer subtree under `CallControlCustom/` plus `AutoWrapupTimer`; the campaign family composes `CampaignTaskListItem`, `CampaignTaskPopover`, `CampaignCountdown`, and `CampaignErrorDialog`. Prop types are unified in `task.types.ts` via `Pick` over `ControlProps`/`TaskProps`. +The exported components are leaves of `index.ts`. Composition is one-directional: `Task` is the shared row reused by `IncomingTask` and `TaskList`; `CallControlCAD` decorates `CallControl` with a CAD header, `GlobalVariablesPanel`, and `TaskTimer`; `CallControl` owns the consult/transfer subtree under `CallControlCustom/`, `AutoWrapupTimer`, and post-call `WrapUpSummary`; `AIAssistantComponent` and `WrapUpSummary` compose the shared `AISummary` surface for receiver/post-call actions; the campaign family composes `CampaignTaskListItem`, `CampaignTaskPopover`, `CampaignCountdown`, and `CampaignErrorDialog`. Prop types are unified in `task.types.ts` via `Pick` over `ControlProps`/`TaskProps`. ## Use Cases - **UC-1 Agent logs in:** Widget passes `teams`, `loginOptions`, `deviceType`, and handlers → `StationLoginComponent` renders selectors → agent selects device/team, optionally enters DN → clicks Continue/Save → component calls `login`/`saveLoginOptions`; `loginFailure`/`saveError` props render error UI. Evidence: `src/components/StationLogin/station-login.tsx`, `tests/components/StationLogin/station-login.tsx`. - **UC-2 Agent changes state:** `UserStateComponent` shows idle-code dropdown with current state + elapsed time → agent selects a code → `setAgentStatus(auxCodeId)` fires. Evidence: `src/components/UserState/user-state.tsx`, `tests/components/UserState/user-state.tsx`. -- **UC-3 Agent controls an active call:** `CallControlComponent` builds buttons from `controlVisibility` → agent presses hold/mute/record/end/wrapup or opens consult/transfer popover → matching callback fires; wrapup requires a selected reason. Evidence: `src/components/task/CallControl/call-control.tsx`, `tests/components/task/CallControl/call-control.tsx`. +- **UC-3 Agent controls an active call:** `CallControlComponent` builds buttons from `controlVisibility` → agent presses hold/mute/record/end/wrapup or opens consult/transfer/existing-party popover → matching callback fires; when `aiSummary` supplies mid-call content, the summary send Promise settles before consult/transfer/Transfer Conference/Merge telephony is invoked, and wrap-up completes through the asynchronous `wrapupCall(reason, id): Promise` contract after a reason is selected. Evidence: `src/components/task/CallControl/call-control.tsx`, `src/components/task/CallControl/call-control.utils.ts`, `tests/components/task/CallControl/call-control.tsx`, `tests/components/task/CallControl/call-control.utils.tsx`. - **UC-4 Agent answers/declines incoming task:** `IncomingTaskComponent` renders the `Task` row with Answer/Decline → `accept(task)`/`reject(task)` fire; renders nothing when no incoming task. Evidence: `src/components/task/IncomingTask/incoming-task.tsx`, `tests/components/task/IncomingTask/incoming-task.tsx`. - **UC-5 Agent views task list:** `TaskListComponent` renders a row per task (campaign preview rows use `CampaignTask` when enabled), collapsing to nothing when empty. Evidence: `src/components/task/TaskList/task-list.tsx`, `tests/components/task/TaskList/task-list.tsx`. - **UC-6 Agent places an outbound call:** `OutdialCallComponent` validates the destination, selects an ANI/address-book entry, and calls `startOutdial`; outdial disabled when a telephony task is active. Evidence: `src/components/task/OutdialCall/outdial-call.tsx`, `tests/components/task/OutdialCall/out-dial-call.tsx`. @@ -294,7 +514,8 @@ These are UI components; the non-happy-path states are part of the contract. - **StationLogin:** login screen vs. profile mode; device-type select drives dial-number input visibility; Desktop option hidden when `hideDesktopLogin`; multiple-login alert when `showMultipleLoginAlert`; error states from `loginFailure`/`saveError`; Save/Continue disabled until valid. (`src/components/StationLogin/station-login.tsx`) - **UserState:** dropdown of idle/custom states with current state highlighted; `isSettingAgentStatus` shows a busy/disabled state; error-triggering idle codes styled distinctly; state timer renders via `formatTime`. (`src/components/UserState/user-state.tsx`) -- **CallControl / CallControlCAD:** button visibility/enablement driven by `controlVisibility`; consult/transfer popover with Agents/Queues/Dial Number/Entry Point tabs (empty-state when no results, loading spinner while fetching); auto-wrapup countdown bar; wrapup requires reason selection before submit. (`src/components/task/CallControl/`, `src/components/task/CallControlCAD/`) +- **CallControl / CallControlCAD:** button visibility/enablement driven by `controlVisibility`; consult/transfer popover with Agents/Queues/Dial Number/Entry Point tabs (empty-state when no results, loading spinner while fetching) plus the existing-party confirmation surface for Transfer/Transfer Conference and Merge; optional mid-call AI summary content is sent and settled before the selected destination or existing-party telephony callback runs; auto-wrapup countdown bar; wrapup requires reason selection before submit. (`src/components/task/CallControl/`, `src/components/task/CallControlCAD/`) +- **Post-call AI summary wrap-up:** the popover shows guidance and searchable reasons, collapsing the selected reason for structured summaries while retaining keyed editing and keeping the reason list reachable. `CallControlComponent` forwards the store-projected `completionEscape` and frozen `controlsDisabled`; `Complete Wrap-Up` is deferred only for initial no-content generation without an escape, disabled while the shared/local completion latch is pending, and frozen read-only after captured response failure until store cleanup. (`src/components/task/CallControl/call-control.tsx`, `src/components/task/CallControl/CallControlCustom/wrap-up-summary.tsx`, `src/components/AISummary/`) - **IncomingTask / TaskList:** hidden (renders `<>`) when no incoming task / empty list; per-media-type icon and label; campaign preview rows swap UI. (`src/components/task/IncomingTask/`, `src/components/task/TaskList/`) - **OutdialCall:** dialpad / contacts tabs; invalid-number state disables dial; address-book loading spinner and infinite scroll; disabled while a telephony task is active. (`src/components/task/OutdialCall/outdial-call.tsx`) - **RealTimeTranscript:** empty-state message ("No live transcript available.") when no entries; entries sorted chronologically; event vs. message rendering. (`src/components/task/RealTimeTranscript/real-time-transcript.tsx`) @@ -314,6 +535,7 @@ These are UI components; the non-happy-path states are part of the contract. - `IncomingTaskComponent` and `TaskListComponent` return `<>` (render nothing) for the empty case rather than a placeholder — a "blank" component usually means the guarding prop (`incomingTask`/non-empty `taskList`) is missing, not a render bug. - `getMediaTypeInfo` falls back to a telephony or chat default for unknown media types rather than throwing; a wrong icon/label usually means an unmapped `MEDIA_CHANNEL` value, not a render failure. Evidence: `src/utils/index.ts`. - Campaign types (`CampaignCallProcessingDetails`) are bridge types for SDK fields not yet in the installed SDK typings; they can drift from the runtime payload until the SDK is updated. Evidence: `src/components/task/task.types.ts`. +- `completionEscape` is store-projected on `aiSummary.postCall`; `CallControlComponent` must forward it without re-deriving from the current visual state so Retry cannot revoke the terminal no-draft escape. Evidence: `src/components/task/CallControl/call-control.tsx`. ## Module Do's / Don'ts @@ -326,34 +548,54 @@ These are UI components; the non-happy-path states are part of the contract. ## Export Stability -This package is published (`@webex/cc-components`, `main` → `dist/index.js`, `types` → `dist/types/index.d.ts`, and a `./wc` subpath export). The `.d.ts` of the `index.ts` barrel and the `task.types.ts`/`*.types.ts` exports are the type surface. Semver sensitivity: adding an optional prop to a `Pick`ed component type or adding a new exported component is a minor; removing/renaming a picked prop, narrowing a prop type, removing an exported component, or renaming/removing a `component-cc-*` custom element (or changing its r2wc prop type) is a major. Evidence: `package.json` (`exports`, `version`), `src/index.ts`, `src/wc.ts`. +This package is published (`@webex/cc-components`, `main` → `dist/index.js`, `types` → `dist/types/index.d.ts`, and a `./wc` subpath export). The `.d.ts` of the `index.ts` barrel and the `task.types.ts`/`*.types.ts` exports are the type surface. Semver sensitivity: adding an optional prop to a `Pick`ed component type or adding a new exported component is a minor; removing/renaming a picked prop, narrowing a prop type, removing an exported component, or renaming/removing a `component-cc-*` custom element (or changing its r2wc prop type) is a major. The `wrapupCall` migration from a synchronous `void` callback to `Promise` is a direct React callback narrowing; the custom-element runtime preserves legacy void/non-result function props by normalizing them to succeeded/not-required while retaining the unchanged `wrapupCall: 'function'` r2wc map. Evidence: `package.json` (`exports`, `version`), `src/index.ts`, `src/wc.ts`, `src/components/task/task.types.ts`. + +Migration note: direct React consumers must treat the Promise-returning `wrapupCall(reason, id)` callback as a major-release contract change when they typed the return as `void`. Custom-element hosts keep the existing function-property contract; legacy undefined or non-result returns remain normalized to `{wrapup: 'succeeded', response: 'not-required'}` at the component boundary. ## Test-Case Strategy (module) Each component is tested in isolation with React Testing Library: render from a minimal props object, assert rendered UI (positive) and assert callbacks fire on interaction / error props render error UI (negative), with snapshot tests guarding stable markup. Utils have dedicated `*.utils.tsx` tests asserting pure transformations (e.g. button building, sorting, data extraction). Edge cases covered include empty task list/incoming task (hidden render), `hideDesktopLogin` across modes, campaign timeout/error, and transcript empty + sort. Gaps: no dedicated test file for `formatTime`/`getMediaTypeInfo` in `src/utils/index.ts` and no explicit assertion that components are `withMetrics`-wrapped. wxApp visibility helpers have dedicated tests in `tests/utils/wxapp-telephony.utils.test.ts`. -| Behavior / Requirement | Existing test evidence | Gap | -| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | -| `CC-COMPONENTS-R-001` | `tests/components/StationLogin/station-login.tsx`, `tests/components/UserState/user-state.tsx`, `tests/components/task/CallControl/call-control.tsx` | None | -| `CC-COMPONENTS-R-002` | Import sites across `tests/components/**` | No single test asserting the full export list | -| `CC-COMPONENTS-R-003` | `tests/components/StationLogin/station-login.tsx` (actions + failure + save error) | None | -| `CC-COMPONENTS-R-004` | `tests/components/StationLogin/station-login.tsx` (hideDesktopLogin cases) | None | -| `CC-COMPONENTS-R-005` | `tests/components/UserState/user-state.tsx`, `tests/components/UserState/user-state.utils.tsx` | None | -| `CC-COMPONENTS-R-006` | `tests/components/task/CallControl/call-control.tsx`, `tests/components/task/CallControl/call-control.utils.tsx` | None | -| `CC-COMPONENTS-R-007` | `tests/components/task/CallControlCAD/call-control-cad.tsx` | None | -| `CC-COMPONENTS-R-008` | `tests/components/task/IncomingTask/incoming-task.tsx`, `tests/components/task/IncomingTask/incoming-task.utils.tsx` | None | -| `CC-COMPONENTS-R-009` | `tests/components/task/TaskList/task-list.tsx`, `tests/components/task/TaskList/task-list.utils.tsx` | None | -| `CC-COMPONENTS-R-010` | `tests/components/task/OutdialCall/out-dial-call.tsx` | None | -| `CC-COMPONENTS-R-011` | `tests/components/task/RealtimeTranscript/realtime-transcript.tsx` | None | -| `CC-COMPONENTS-R-012` | `tests/components/task/CampaignTask/campaign-task.test.tsx`, `tests/components/task/CampaignErrorDialog/campaign-error-dialog.tsx`, `tests/components/task/CampaignCountdown/campaign-countdown.tsx` | None | -| `CC-COMPONENTS-R-013` | `tests/components/task/CallControl/call-control.utils.tsx`, component snapshots | No dedicated `formatTime`/`getMediaTypeInfo` unit test | -| `CC-COMPONENTS-R-014` | `tests/hooks/useIntersectionObserver.test.ts` | None | -| `CC-COMPONENTS-R-015` | None found (covered indirectly via render tests) | No explicit `withMetrics`-wrapping assertion | -| `CC-COMPONENTS-R-016` | `tests/components/StationLogin/E911Modal/e911-modal.test.tsx` | None | -| `CC-COMPONENTS-R-017` | `tests/components/task/CallControlCAD/call-control-cad.tsx` (Momentum structure/configuration, sections, immediate/confirmed Drop, pending/consult disabled state, read-only rows, focus fallback, live regions) | Momentum overlay keyboard behavior and visual placement receive final manual coverage | -| `CC-COMPONENTS-R-018` | `tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx`, `tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.snapshot.tsx` | None | +CallControl AI-summary tests cover public branch transitions between legacy and eligible post-call wrap-up, retained post-call draft content/selection after the branch reopens, all closed wrap-up result variants, response-before-telephony ordering, and custom-element void-callback compatibility. + +| Behavior / Requirement | Existing test evidence | Gap | +| ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| `CC-COMPONENTS-R-001` | `tests/components/StationLogin/station-login.tsx`, `tests/components/UserState/user-state.tsx`, `tests/components/task/CallControl/call-control.tsx` | None | +| `CC-COMPONENTS-R-002` | Import sites across `tests/components/**`, including `tests/components/AISummary/ai-summary.tsx` (`exports a unique display ordinal and inserts Outcome without reordering SDK sections`) | No single test asserting the full `src/index.ts` export list | +| `CC-COMPONENTS-R-003` | `tests/components/StationLogin/station-login.tsx` (actions + failure + save error) | None | +| `CC-COMPONENTS-R-004` | `tests/components/StationLogin/station-login.tsx` (hideDesktopLogin cases) | None | +| `CC-COMPONENTS-R-005` | `tests/components/UserState/user-state.tsx`, `tests/components/UserState/user-state.utils.tsx` | None | +| `CC-COMPONENTS-R-006` | `tests/components/task/CallControl/call-control.tsx` (voice request, destination response-before-telephony across sent/failed/blocked/stale/rejected/thrown pre-action settlements, existing-party Transfer/Merge confirmations, eligible fresh existing-party preparation and absent/ineligible legacy paths, eligible post-call and legacy wrap-up branches); `tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx` (request observation, latched destination layout, existing-party action variant); `tests/components/task/CallControl/CallControlCustom/call-control-consult.tsx` (CAD consult-surface fresh/reused/absent summary paths); `tests/components/task/CallControl/call-control.utils.tsx` (shared adapter ordering/re-entry plus submitted/response-failed/not-required/failed/rejected/thrown wrap-up result normalization); `tests/wc.tsx` (component custom-element void callback compatibility) | None | +| `CC-COMPONENTS-R-007` | `tests/components/task/CallControlCAD/call-control-cad.tsx` | None | +| `CC-COMPONENTS-R-008` | `tests/components/task/IncomingTask/incoming-task.tsx`, `tests/components/task/IncomingTask/incoming-task.utils.tsx` | None | +| `CC-COMPONENTS-R-009` | `tests/components/task/TaskList/task-list.tsx`, `tests/components/task/TaskList/task-list.utils.tsx` | None | +| `CC-COMPONENTS-R-010` | `tests/components/task/OutdialCall/out-dial-call.tsx` | None | +| `CC-COMPONENTS-R-011` | `tests/components/task/RealtimeTranscript/realtime-transcript.tsx` | None | +| `CC-COMPONENTS-R-012` | `tests/components/task/CampaignTask/campaign-task.test.tsx`, `tests/components/task/CampaignErrorDialog/campaign-error-dialog.tsx`, `tests/components/task/CampaignCountdown/campaign-countdown.tsx` | None | +| `CC-COMPONENTS-R-013` | `tests/components/task/CallControl/call-control.utils.tsx`, component snapshots | No dedicated `formatTime`/`getMediaTypeInfo` unit test | +| `CC-COMPONENTS-R-014` | `tests/hooks/useIntersectionObserver.test.ts` | None | +| `CC-COMPONENTS-R-015` | None found (covered indirectly via render tests) | No explicit `withMetrics`-wrapping assertion; shared/inline exports are intentionally unwrapped today | +| `CC-COMPONENTS-R-016` | `tests/components/StationLogin/E911Modal/e911-modal.test.tsx` | None | +| `CC-COMPONENTS-R-017` | `tests/components/task/CallControlCAD/call-control-cad.tsx` (Momentum structure/configuration, sections, immediate/confirmed Drop, pending/consult disabled state, read-only rows, focus fallback, live regions) | Momentum overlay keyboard behavior and visual placement receive final manual coverage | +| `CC-COMPONENTS-R-018` | `tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.tsx`, `tests/components/task/CallControl/CallControlCustom/consult-transfer-popover.snapshot.tsx` | None | +| `CC-COMPONENTS-R-019` | `tests/components/task/CallControl/call-control-dtmf-keypad.tsx`, `tests/components/task/CallControl/call-control.utils.tsx` | None | +| `CC-COMPONENTS-R-020` | `tests/components/AIAssistant/ai-assistant.tsx` (`renders the exact native receiver trigger only in closed and minimized chrome slots`, `opens the receiver branch from closed and minimized chrome and recovers focus into the panel`, `applies automatic direction to mixed-direction receiver summary content only`, `keeps receiver card actions inert and copies only visible rendered card text`, `opens the receiver branch when Real-time Assist is disabled`, `removes receiver actions when the renderer falls back and restores panel focus`, `restores panel focus when the whole receiver summary branch is removed`, `resets the receiver branch for ordinary chrome actions and owner changes`); `tests/components/AISummary/ai-summary.tsx` (`omits receiver actions for %s state`, `recovers focus to the containing panel when receiver actions are removed`) | Browser-level focus/overlay coverage remains in the UX evidence gate | +| `CC-COMPONENTS-R-021` | `tests/components/AIAssistant/adaptive-card-renderer.utils.test.ts` (`strips remote images, image sets, media sources, action icons, and backgrounds`, `classifies %s renderability`); `tests/components/AIAssistant/adaptive-card-renderer.test.tsx` (`shows the unavailable fallback without parsing or requesting a remote image when sanitization leaves no content`, `keeps text renderable after stripping a remote image before parsing`, `strips image-set and media remote resources before parsing`, `resets the error boundary when a replacement card becomes renderable`) | Browser-level network observation remains in the UX evidence gate | +| `CC-COMPONENTS-R-022` | `tests/components/AISummary/ai-summary.tsx` (`contains synchronous feedback and Retry throws without changing selection`, `normalizes non-thenable feedback and Retry returns`, `preserves feedback selection for rejected and malformed callback results`, `ignores stale mid-call feedback confirmations after content replacement`, `allows only the latest mid-call feedback attempt to paint`, `settles mid-call feedback confirmation after unmount without a window error`, `applies automatic direction to mixed-direction structured summary text boundaries`, `moves focus to the next summary control when a focused editor is removed`, `keeps external focus after a normal blur out of the summary`, `consumes a panel fallback once and does not steal focus on later renders`) | Final browser visual comparison is owned by the UX evidence gate | +| `CC-COMPONENTS-R-023` | `tests/components/task/CallControl/call-control.tsx` (`uses the projected post-call completion escape while retry is pending`, once-only post-call and legacy wrap-up completion), `tests/components/task/CallControl/CallControlCustom/wrap-up-summary.tsx` (`keeps Complete Wrap-Up enabled after a terminal summary error during retry when completion escape is set`, `defers completion only for pending initial generation without retained content`, retained-content regeneration completion, real roving-focus traversal dedupe, local completion re-entry guard, frozen not-confirmed read-only state, `moves focus between the collapsed reason trigger and expanded search controls`, `restores focus to the wrap-up panel when the summary subtree is removed`, `bounds reason and summary scrolling while keeping panel chrome outside the scroll regions`), `playwright/tests/ai-summary-test.spec.ts` | Fresh browser render required after visual tuning | ## Traceability - Repo architecture: [`ARCHITECTURE.md`](../../../../ai-docs/ARCHITECTURE.md) · Registry: [`SPEC_INDEX.md`](../../../../ai-docs/SPEC_INDEX.md) · Contracts: [`CONTRACTS.md`](../../../../ai-docs/CONTRACTS.md) - Coverage state & contracts baseline: `.sdd/manifest.json` +# Post-review accessibility repair (2026-09-27) + +The consult destination list renders list items directly under its list and +keeps its bounded scroll region keyboard-focusable, including while actions +are pending. Voice display labels use Agent, Queues, Dial number and Entry point; +capability IDs and non-voice controls are unchanged. + +When capability revocation removes the whole WrapUpSummary, CallControl's +stable wrap-up container owns fallback focus. It restores focus only when the +previously focused control was disconnected and no connected control already +has focus. Summary-internal removal retains WrapUpSummary's existing fallback. diff --git a/packages/contact-center/cc-components/src/components/AIAssistant/AdaptiveCardRenderer/adaptive-card-renderer.tsx b/packages/contact-center/cc-components/src/components/AIAssistant/AdaptiveCardRenderer/adaptive-card-renderer.tsx index 95eb76108..7f0eaaeca 100644 --- a/packages/contact-center/cc-components/src/components/AIAssistant/AdaptiveCardRenderer/adaptive-card-renderer.tsx +++ b/packages/contact-center/cc-components/src/components/AIAssistant/AdaptiveCardRenderer/adaptive-card-renderer.tsx @@ -9,6 +9,7 @@ import { copySuggestion, detectActionKind, extractCardText, + hasRenderableCardContent, preloadIcons, prepareCardForRender, toggleActionControls, @@ -101,7 +102,12 @@ const AdaptiveCardRendererBody: React.FC = ({ } onActionRef.current?.(action); }; - adaptiveCard.parse(prepareCardForRender(card, publishTimestamp, assistantTitle)); + const preparedCard = prepareCardForRender(card, publishTimestamp, assistantTitle); + if (!hasRenderableCardContent(preparedCard)) { + showBoundary(new Error('Adaptive card has no renderable content after sanitization.')); + return undefined; + } + adaptiveCard.parse(preparedCard); const rendered = adaptiveCard.render(); if (rendered) { container.appendChild(rendered); @@ -156,7 +162,10 @@ const AdaptiveCardRenderer: React.FC = ({ className={`ai-assistant__card${isCustomerStatement ? ' ai-assistant__card--customer-statement' : ''}`} data-testid="ai-assistant:adaptive-card" > - }> + } + resetKeys={[card, assistantTitle, publishTimestamp]} + > = { 'like-filled.svg': likeFilledIcon, 'like-regular.svg': likeRegularIcon, }; +const ALLOWED_MOMENTUM_RESOURCE_URLS = new Set(Object.values(MOMENTUM_ICON_URLS)); /** Format an epoch (ms) into "HH:MM"; empty string on bad input. */ const formatSourceTimestamp = (raw: number | string | undefined): string => { @@ -60,6 +61,17 @@ const extractMomentumIconName = (value: string): string | null => { return match ? match[1].toLowerCase() : null; }; +const resolveAllowedResourceUrl = (value: string): string | undefined => { + const iconName = extractMomentumIconName(value); + if (!iconName) return undefined; + return resolveMomentumIconUrl(iconName) ?? undefined; +}; + +const isAllowedMomentumResourceUrl = (value: string): boolean => ALLOWED_MOMENTUM_RESOURCE_URLS.has(value); + +const isRecord = (value: unknown): value is Record => + Boolean(value && typeof value === 'object' && !Array.isArray(value)); + export const extractCustomerStatementTitle = (card: unknown): string | undefined => { if (Array.isArray(card)) { for (const item of card) { @@ -128,20 +140,71 @@ export const prepareCardForRender = (card: T, publishTimestamp?: number | str const formattedTimestamp = formatSourceTimestamp(publishTimestamp); const cardWithoutDuplicateHeader = removeDuplicateAssistantHeader(card as Record, assistantTitle); - const visit = (node: unknown): unknown => { - if (Array.isArray(node)) return node.map(visit); - if (node && typeof node === 'object') { + const prepareResourceValue = (value: unknown): unknown | undefined => { + if (typeof value === 'string') { + return resolveAllowedResourceUrl(value); + } + if (!isRecord(value)) { + return undefined; + } + const url = value.url; + if (typeof url !== 'string') { + return undefined; + } + const allowedUrl = resolveAllowedResourceUrl(url); + if (!allowedUrl) { + return undefined; + } + const prepared = visit({...value, url: allowedUrl}); + return isRecord(prepared) ? prepared : undefined; + }; + + const isImageUrl = ( + record: Record, + key: string, + context: {parentType?: string; parentKey?: string} + ): boolean => + key === 'url' && (record.type === 'Image' || (context.parentType === 'ImageSet' && context.parentKey === 'images')); + + const visit = (node: unknown, context: {parentType?: string; parentKey?: string} = {}): unknown => { + if (Array.isArray(node)) { + return node + .map((item) => visit(item, context)) + .filter((item): item is Exclude => item !== undefined); + } + if (isRecord(node)) { const out: Record = {}; - for (const [key, value] of Object.entries(node as Record)) { - if (typeof value === 'string') { - const iconName = extractMomentumIconName(value); - if (iconName) { - const iconUrl = resolveMomentumIconUrl(iconName); - if (iconUrl) { - out[key] = iconUrl; - continue; - } + const record = node; + const recordType = typeof record.type === 'string' ? record.type : undefined; + for (const [key, value] of Object.entries(record)) { + if (key === 'backgroundImage') { + const preparedBackground = prepareResourceValue(value); + if (preparedBackground !== undefined) { + out[key] = preparedBackground; + } + continue; + } + if (recordType === 'Media' && key === 'sources') { + continue; + } + if (recordType === 'Media' && key === 'poster') { + const preparedPoster = prepareResourceValue(value); + if (preparedPoster !== undefined) { + out[key] = preparedPoster; } + continue; + } + if ( + isImageUrl(record, key, context) || + (key === 'iconUrl' && typeof recordType === 'string' && recordType.startsWith('Action.')) + ) { + const preparedResource = prepareResourceValue(value); + if (preparedResource !== undefined) { + out[key] = preparedResource; + } + continue; + } + if (typeof value === 'string') { if (value.includes(SOURCE_TIMESTAMP_PLACEHOLDER)) { out[key] = value.split(SOURCE_TIMESTAMP_PLACEHOLDER).join(formattedTimestamp); continue; @@ -151,7 +214,7 @@ export const prepareCardForRender = (card: T, publishTimestamp?: number | str continue; } } - out[key] = visit(value); + out[key] = visit(value, {parentType: recordType, parentKey: key}); } if (out.id === LINE_SEPARATOR_ID) { out.separator = true; @@ -164,6 +227,80 @@ export const prepareCardForRender = (card: T, publishTimestamp?: number | str return visit(cardWithoutDuplicateHeader) as T; }; +export const hasRenderableCardContent = (card: unknown): boolean => { + const hasText = (value: unknown): boolean => typeof value === 'string' && value.trim().length > 0; + + const hasRenderableFact = (fact: unknown): boolean => { + if (!isRecord(fact)) return false; + return hasText(fact.title) || hasText(fact.value); + }; + + const hasRenderableNode = (node: unknown, context: {richTextInline?: boolean} = {}): boolean => { + if (Array.isArray(node)) { + return node.some((item) => hasRenderableNode(item, context)); + } + if (typeof node === 'string') { + return context.richTextInline === true && hasText(node); + } + if (!isRecord(node)) { + return false; + } + const record = node; + if (record.type === 'TextBlock') { + return hasText(record.text); + } + if (record.type === 'TextRun') { + return context.richTextInline === true && hasText(record.text); + } + if (record.type === 'RichTextBlock') { + return hasRenderableNode(record.inlines, {richTextInline: true}); + } + if (record.type === 'FactSet') { + return Array.isArray(record.facts) && record.facts.some(hasRenderableFact); + } + if (record.type === 'Image') { + return typeof record.url === 'string' && isAllowedMomentumResourceUrl(record.url); + } + if (record.type === 'ImageSet') { + return ( + Array.isArray(record.images) && + record.images.some( + (image) => + (isRecord(image) && typeof image.url === 'string' && isAllowedMomentumResourceUrl(image.url)) || + hasRenderableNode(image) + ) + ); + } + if (record.type === 'AdaptiveCard') { + return hasRenderableNode(record.body); + } + if (record.type === 'Container' || record.type === 'Column') { + return hasRenderableNode(record.items); + } + if (record.type === 'ColumnSet') { + return hasRenderableNode(record.columns); + } + if (record.type === 'Table') { + return hasRenderableNode(record.rows); + } + if (record.type === 'TableRow') { + return hasRenderableNode(record.cells); + } + if (record.type === 'TableCell') { + return hasRenderableNode(record.items); + } + return ( + hasRenderableNode(record.body) || + hasRenderableNode(record.items) || + hasRenderableNode(record.columns) || + hasRenderableNode(record.images) || + (Array.isArray(record.facts) && record.facts.some(hasRenderableFact)) + ); + }; + + return hasRenderableNode(card); +}; + /** * Classifies a card action by its `id` / `title` (e.g. `likeButton`). The * icon URL is deliberately not used: it is bundled as an inline data URI, so diff --git a/packages/contact-center/cc-components/src/components/AIAssistant/ai-assistant.styles.scss b/packages/contact-center/cc-components/src/components/AIAssistant/ai-assistant.styles.scss index b9912aa17..5e5b216a5 100644 --- a/packages/contact-center/cc-components/src/components/AIAssistant/ai-assistant.styles.scss +++ b/packages/contact-center/cc-components/src/components/AIAssistant/ai-assistant.styles.scss @@ -35,6 +35,13 @@ } } +.ai-assistant__closed-chrome { + align-items: flex-start; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + // Default chrome: small floating card with border + shadow. .ai-assistant__panel { background: var(--mds-color-theme-background-primary-normal); @@ -503,3 +510,41 @@ border-radius: 1.5rem; } } + +.ai-assistant__summary-trigger { + align-items: center; + background: transparent; + border: 0.0625rem solid var(--mds-color-theme-outline-primary-normal, rgba(0, 0, 0, 0.55)); + border-radius: 6.25rem; + box-sizing: border-box; + color: var(--mds-color-theme-text-primary-normal); + cursor: pointer; + display: inline-flex; + font: inherit; + font-size: 0.8125rem; + font-weight: 500; + min-height: 1.75rem; + padding: 0.25rem 0.75rem; + + &:hover { + background: var(--mds-color-theme-background-secondary-hover, rgba(0, 0, 0, 0.04)); + } + + &:focus-visible { + outline: 0.125rem solid var(--mds-color-theme-outline-focus-default, #0087ea); + outline-offset: 0.125rem; + } +} + +.ai-assistant__summary-trigger--minimized { + min-height: 1.5rem; + padding-block: 0.125rem; +} + +.ai-assistant__receiver-summary { + display: flex; + flex-direction: column; + gap: 0.75rem; + min-inline-size: 0; + overflow: visible; +} diff --git a/packages/contact-center/cc-components/src/components/AIAssistant/ai-assistant.tsx b/packages/contact-center/cc-components/src/components/AIAssistant/ai-assistant.tsx index b87fb7105..195748308 100644 --- a/packages/contact-center/cc-components/src/components/AIAssistant/ai-assistant.tsx +++ b/packages/contact-center/cc-components/src/components/AIAssistant/ai-assistant.tsx @@ -1,13 +1,124 @@ -import React from 'react'; +import React, {useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; import {Button, Text} from '@momentum-design/components/dist/react'; import {withMetrics} from '@webex/cc-ui-logging'; import RealTimeAssist from './RealTimeAssist/real-time-assist'; +import AISummary, {AI_SUMMARY_MESSAGES} from '../AISummary'; import AIAssistantLanding from './ai-assistant-landing'; import CiscoAIAssistantColorIcon from './CiscoAIAssistantColorIcon'; import {AIAssistantComponentProps} from './ai-assistant.types'; +import {extractCardText} from './AdaptiveCardRenderer/adaptive-card-renderer.utils'; +import AdaptiveCardRenderer from './AdaptiveCardRenderer/adaptive-card-renderer'; import {AI_ASSISTANT_TITLE, DISCLAIMER_TEXT} from './constants'; import './ai-assistant.styles.scss'; +type AIAssistantActiveBranch = 'real-time-assist' | 'receiver-summary'; +type ReceiverCardRenderState = 'pending' | 'ready' | 'fallback'; + +const FOCUSABLE_SELECTOR = 'button, [href], input, textarea, select, [tabindex], [role="button"], mdc-button'; + +const isEnabledFocusTarget = (element: HTMLElement): boolean => + !element.hasAttribute('disabled') && + element.getAttribute('aria-disabled') !== 'true' && + element.tabIndex >= 0 && + element.isConnected; + +const isConnectedFocusTarget = (element: HTMLElement): boolean => + !element.hasAttribute('disabled') && element.getAttribute('aria-disabled') !== 'true' && element.isConnected; + +const getEnabledFocusTargets = (root: HTMLElement): HTMLElement[] => + Array.from(root.querySelectorAll(FOCUSABLE_SELECTOR)).filter(isEnabledFocusTarget); + +const isReceiverDisplayActionNode = (value: unknown): boolean => { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return false; + } + const type = (value as Record).type; + return typeof type === 'string' && (type === 'ActionSet' || type.startsWith('Action.')); +}; + +const projectReceiverDisplayCard = (card: unknown): unknown => { + if (Array.isArray(card)) { + return card + .map(projectReceiverDisplayCard) + .filter((item) => item !== undefined && !isReceiverDisplayActionNode(item)); + } + if (!card || typeof card !== 'object') { + return card; + } + if (isReceiverDisplayActionNode(card)) { + return undefined; + } + const projected: Record = {}; + Object.entries(card as Record).forEach(([key, value]) => { + if (key === 'actions' || key === 'selectAction') { + return; + } + const nextValue = projectReceiverDisplayCard(value); + if (nextValue !== undefined) { + projected[key] = nextValue; + } + }); + return projected; +}; + +const pushNonEmptyText = (texts: string[], value: unknown) => { + if (typeof value !== 'string') { + return; + } + const trimmed = value.trim(); + if (trimmed) { + texts.push(trimmed); + } +}; + +const extractReceiverDisplayCardText = (node: unknown): string => { + const texts: string[] = []; + + const visit = (value: unknown) => { + if (typeof value === 'string') { + pushNonEmptyText(texts, value); + return; + } + if (Array.isArray(value)) { + value.forEach(visit); + return; + } + if (!value || typeof value !== 'object') { + return; + } + + const record = value as Record; + switch (record.type) { + case 'TextBlock': + case 'TextRun': + pushNonEmptyText(texts, record.text); + break; + case 'RichTextBlock': + visit(record.inlines); + break; + case 'FactSet': + if (Array.isArray(record.facts)) { + record.facts.forEach((fact) => { + if (!fact || typeof fact !== 'object') { + return; + } + const factRecord = fact as Record; + pushNonEmptyText(texts, factRecord.title); + pushNonEmptyText(texts, factRecord.value); + }); + } + break; + default: + break; + } + + ['body', 'items', 'columns', 'rows', 'cells'].forEach((key) => visit(record[key])); + }; + + visit(node); + return texts.join('\n'); +}; + const AIAssistantComponent: React.FC = ({ chrome, isFullScreen, @@ -29,30 +140,307 @@ const AIAssistantComponent: React.FC = ({ setContextDraft, submitContext, onRealTimeAssistAction, + receiverSummary, logger, className, }) => { + const [activeBranch, setActiveBranch] = useState('real-time-assist'); + const [activeReceiverBranchKey, setActiveReceiverBranchKey] = useState(null); + const [receiverCardRenderState, setReceiverCardRenderState] = useState('pending'); + const rootRef = useRef(null); + const panelRef = useRef(null); + const receiverBranchRef = useRef(null); + const lastFocusedElementRef = useRef(null); + const lastFocusSuccessorRef = useRef(null); + const pendingReceiverActivationFocusRef = useRef(false); + const previousShowReceiverSummaryRef = useRef(false); + const previousReceiverActionsVisibleRef = useRef(false); // Fullscreen is consumer-owned: we emit onFullScreenToggle; the host owns layout. const rootClass = ['ai-assistant', className || ''].filter(Boolean).join(' '); const panelClass = ['ai-assistant__panel', isFullScreen ? 'ai-assistant__panel--full-screen' : ''] .filter(Boolean) .join(' '); - const showLanding = !hasActiveInteraction || !isFeatureEnabled; + const canViewReceiverSummary = Boolean(receiverSummary && hasActiveInteraction); + const showReceiverSummary = Boolean( + receiverSummary && + hasActiveInteraction && + activeBranch === 'receiver-summary' && + activeReceiverBranchKey === receiverSummary.branchKey + ); + const showLanding = !showReceiverSummary && (!hasActiveInteraction || !isFeatureEnabled); + const receiverBranchKey = receiverSummary?.branchKey; + const receiverContentRevision = receiverSummary?.surface === 'content' ? receiverSummary.contentRevision : undefined; + const receiverAdaptiveCard = + receiverSummary?.surface === 'content' ? receiverSummary.content.adaptiveCard : undefined; + const receiverDisplayCard = useMemo( + () => (receiverAdaptiveCard === undefined ? undefined : projectReceiverDisplayCard(receiverAdaptiveCard)), + [receiverAdaptiveCard] + ); + const receiverActionsVisible = Boolean( + showReceiverSummary && receiverSummary?.surface === 'content' && receiverCardRenderState === 'ready' + ); + + useEffect(() => { + if (!receiverBranchKey || activeReceiverBranchKey !== receiverBranchKey) { + setActiveBranch('real-time-assist'); + setActiveReceiverBranchKey(null); + pendingReceiverActivationFocusRef.current = false; + } + }, [activeReceiverBranchKey, receiverBranchKey]); + + const focusTarget = (target: HTMLElement | null | undefined): boolean => { + if (!target || !isConnectedFocusTarget(target)) { + return false; + } + // Momentum reflects its default tab stop asynchronously after connection. + // A newly rendered launcher must already be focusable during layout repair. + if (target.tagName === 'MDC-BUTTON' && !target.hasAttribute('tabindex')) { + target.setAttribute('tabindex', '0'); + } + target.focus(); + HTMLElement.prototype.focus.call(target); + return document.activeElement === target; + }; + + const clearReceiverFocusSnapshot = () => { + lastFocusedElementRef.current = null; + lastFocusSuccessorRef.current = null; + }; + + const recoverReceiverFocus = (preferHeader: boolean) => { + const activeElement = document.activeElement; + if (activeElement instanceof HTMLElement && activeElement !== document.body && activeElement.isConnected) { + clearReceiverFocusSnapshot(); + return; + } + const lastFocusedElement = lastFocusedElementRef.current; + if (lastFocusedElement?.isConnected && lastFocusedElement !== document.body) { + clearReceiverFocusSnapshot(); + return; + } + + if (focusTarget(lastFocusSuccessorRef.current)) { + clearReceiverFocusSnapshot(); + return; + } + + if (preferHeader) { + const headerMinimize = panelRef.current?.querySelector( + '[data-testid="ai-assistant:header-minimize"]' + ); + if (focusTarget(headerMinimize)) { + clearReceiverFocusSnapshot(); + return; + } + } + + focusTarget(panelRef.current); + clearReceiverFocusSnapshot(); + }; + + useLayoutEffect(() => { + if (!pendingReceiverActivationFocusRef.current || chrome !== 'open' || !showReceiverSummary) { + return; + } + pendingReceiverActivationFocusRef.current = false; + recoverReceiverFocus(true); + }); + + useLayoutEffect(() => { + const previousShowReceiverSummary = previousShowReceiverSummaryRef.current; + previousShowReceiverSummaryRef.current = showReceiverSummary; + if (previousShowReceiverSummary && !showReceiverSummary) { + recoverReceiverFocus(false); + } + }); + + useLayoutEffect(() => { + const previousReceiverActionsVisible = previousReceiverActionsVisibleRef.current; + previousReceiverActionsVisibleRef.current = receiverActionsVisible; + if (previousReceiverActionsVisible && !receiverActionsVisible) { + recoverReceiverFocus(false); + } + }); + + useLayoutEffect(() => { + const focused = lastFocusedElementRef.current; + if (chrome === 'open' || !focused || focused.isConnected) { + return; + } + const activeElement = document.activeElement; + if (activeElement instanceof HTMLElement && activeElement !== document.body && activeElement.isConnected) { + clearReceiverFocusSnapshot(); + return; + } + const fallbackSelector = + chrome === 'closed' ? '[data-testid="ai-assistant:launcher"]' : '[data-testid="ai-assistant:minimized-restore"]'; + if (!focusTarget(lastFocusSuccessorRef.current)) { + focusTarget(rootRef.current?.querySelector(fallbackSelector)); + } + clearReceiverFocusSnapshot(); + }); + + useLayoutEffect(() => { + if (!showReceiverSummary || receiverSummary?.surface !== 'content') { + return undefined; + } + const branch = receiverBranchRef.current; + if (!branch) { + return undefined; + } + + const updateReceiverCardRenderState = () => { + const hasFallback = Boolean(branch.querySelector('[data-testid="ai-assistant:adaptive-card-fallback"]')); + const cardHost = branch.querySelector('.ai-assistant__card-host'); + const hasRenderedContent = Boolean(cardHost && cardHost.childElementCount > 0); + const nextState: ReceiverCardRenderState = hasFallback ? 'fallback' : hasRenderedContent ? 'ready' : 'pending'; + setReceiverCardRenderState((current) => (current === nextState ? current : nextState)); + }; + + updateReceiverCardRenderState(); + if (typeof MutationObserver === 'undefined') { + return undefined; + } + const observer = new MutationObserver(updateReceiverCardRenderState); + observer.observe(branch, {childList: true, subtree: true}); + return () => observer.disconnect(); + }, [chrome, receiverContentRevision, receiverSummary?.surface, showReceiverSummary]); + + const handleRootFocusCapture = (event: React.FocusEvent) => { + const target = event.target; + if (!(target instanceof HTMLElement)) { + return; + } + const controls = rootRef.current ? getEnabledFocusTargets(rootRef.current) : []; + const index = controls.indexOf(target); + lastFocusedElementRef.current = target; + lastFocusSuccessorRef.current = + index >= 0 ? (controls.slice(index + 1).find((control) => control !== target) ?? null) : null; + }; + + const openRealTimeAssist = () => { + setActiveBranch('real-time-assist'); + setActiveReceiverBranchKey(null); + open(); + }; + + const restoreRealTimeAssist = () => { + setActiveBranch('real-time-assist'); + setActiveReceiverBranchKey(null); + restore(); + }; + + const closePanel = () => { + setActiveBranch('real-time-assist'); + setActiveReceiverBranchKey(null); + close(); + }; + + const openReceiverSummary = () => { + if (!receiverSummary?.openReceiverSummary()) { + return; + } + setActiveBranch('receiver-summary'); + setActiveReceiverBranchKey(receiverSummary.branchKey); + pendingReceiverActivationFocusRef.current = true; + if (chrome === 'minimized') { + restore(); + return; + } + if (chrome === 'closed') { + open(); + } + }; + + const renderReceiverSummary = () => { + if (!receiverSummary) { + return null; + } + + return ( +
+ {receiverSummary.surface === 'content' ? ( +
+ +
+ ) : null} + {receiverSummary.surface === 'content' && receiverActionsVisible ? ( + { + const renderedText = receiverBranchRef.current ? extractCardText(receiverBranchRef.current).trim() : ''; + const text = renderedText || extractReceiverDisplayCardText(receiverDisplayCard).trim(); + if (!text) { + throw new Error('Receiver summary has no visible copy text.'); + } + return text; + }} + onCopy={receiverSummary.recordReceiverSummaryCopied} + onFeedback={receiverSummary.setReceiverSummaryFeedback} + /> + ) : null} + {receiverSummary.surface === 'unavailable' || receiverSummary.surface === 'generic-error' ? ( + + ) : null} +
+ ); + }; + + const renderReceiverSummaryTrigger = (classNameSuffix = '') => + canViewReceiverSummary ? ( + + ) : null; + + const renderLauncherButton = () => ( + + ); return ( -
+
{chrome === 'closed' ? ( - + canViewReceiverSummary ? ( +
+ {renderReceiverSummaryTrigger()} + {renderLauncherButton()} +
+ ) : ( + renderLauncherButton() + ) ) : chrome === 'minimized' ? (
@@ -60,6 +448,7 @@ const AIAssistantComponent: React.FC = ({ {AI_ASSISTANT_TITLE}
+ {renderReceiverSummaryTrigger(' ai-assistant__summary-trigger--minimized')}
) : ( -
+
{AI_ASSISTANT_TITLE} @@ -113,7 +509,7 @@ const AIAssistantComponent: React.FC = ({ prefix-icon="cancel-bold" aria-label="Close" data-testid="ai-assistant:header-close" - onClick={close} + onClick={closePanel} />
@@ -121,7 +517,9 @@ const AIAssistantComponent: React.FC = ({ className={`ai-assistant__body${showLanding ? ' ai-assistant__body--landing' : ''}`} data-testid="ai-assistant:body" > - {showLanding ? ( + {showReceiverSummary ? ( + renderReceiverSummary() + ) : showLanding ? ( ) : ( ; + contentRevision: number; + actionType: AISummaryActionType; + selectedFeedback: AISummaryFeedback; + midCallFeedbackPending: boolean; + controlsDisabled: boolean; + openReceiverSummary: () => boolean; + recordReceiverSummaryCopied: (expectedRevision: number) => boolean; + setReceiverSummaryFeedback: ( + feedback: Exclude, + actionType: AISummaryActionType, + expectedRevision: number + ) => Promise; + } + | { + surface: 'unavailable' | 'generic-error'; + branchKey: string; + content?: never; + contentRevision?: never; + actionType?: never; + selectedFeedback?: never; + midCallFeedbackPending?: never; + controlsDisabled?: never; + recordReceiverSummaryCopied?: never; + setReceiverSummaryFeedback?: never; + openReceiverSummary: () => boolean; + }; + /** Props for the top-level AIAssistant presentational component. */ export interface AIAssistantComponentProps { /** Current chrome visibility state. */ @@ -44,6 +83,8 @@ export interface AIAssistantComponentProps { agentName?: string; /** Whether the first real-time assist request has completed successfully. */ hasInitialRequestSucceeded: boolean; + /** Optional receiving-agent summary branch. */ + receiverSummary?: AIAssistantReceiverSummary; /** Transition the chrome to `open`. */ open: () => void; /** Transition the chrome to `closed`; preserves chat state. */ diff --git a/packages/contact-center/cc-components/src/components/AISummary/ai-summary.constants.ts b/packages/contact-center/cc-components/src/components/AISummary/ai-summary.constants.ts new file mode 100644 index 000000000..77b5fedcf --- /dev/null +++ b/packages/contact-center/cc-components/src/components/AISummary/ai-summary.constants.ts @@ -0,0 +1,98 @@ +import type {AISummaryPostCallDisplaySectionKey, AISummarySectionKey} from '@webex/cc-store'; + +const assertUniquePostCallDisplaySectionOrder = (order: readonly AISummaryPostCallDisplaySectionKey[]): void => { + if (new Set(order).size !== order.length) { + throw new Error( + 'POST_CALL_DISPLAY_SECTION_ORDER must include each AISummaryPostCallDisplaySectionKey exactly once.' + ); + } +}; + +export const COPIED_FEEDBACK_MS = 1500; + +export const AI_SUMMARY_MESSAGES = { + viewSummary: 'View summary', + generatingTitle: 'Generating summary...', + generatingDescription: 'Just a sec—the details are coming together.', + unavailable: 'The summary is not available', + generationError: 'Having trouble generating summary', + generationErrorDescription: + 'It could be a lost connection or something else. Could you check your connection or try again later?', + copySummary: 'Copy Summary', + copiedSummary: 'Copied', + attribution: 'AI-generated', + plainSummary: 'Summary', + like: 'This is helpful', + dislike: "This isn't helpful", + likeDisplay: 'Like', + dislikeDisplay: 'Dislike', + retry: 'Retry', + editSectionLabel: (label: string) => `Edit ${label}`, + sectionLabels: { + initialContactReason: 'Initial contact reason', + additionalContactReasons: 'Additional contact reason(s)', + additionalContext: 'Additional context', + keyActionsTaken: 'Key Actions Taken', + nextSteps: 'Next Steps', + reasonForTransferOrConsult: 'Reason for transfer or consult', + resolution: 'Outcome', + }, + midCall: { + consultHeading: 'Here’s a consult summary—they’ll get a copy', + transferHeading: 'Here’s a transfer summary—they’ll get a copy.', + searchPlaceholder: 'Search by name, queue, entry point or phone number', + destinationCategory: 'Destination category', + searchDestinations: 'Search destinations', + }, + postCall: { + eyebrow: 'Wrap up interaction', + summaryHeading: 'Summary of your conversation', + searchPlaceholder: 'Search topic', + completeAction: 'Complete Wrap-Up', + instructions: 'Choose a code, and click the summary to edit it if needed.', + chooseReason: 'Choose a reason to wrap up', + reasonSearchLabel: 'Search wrap-up reasons', + noReasonMatches: 'No wrap-up reasons match your search.', + }, + feedback: { + pendingSubmission: 'Pending submission', + submissionNotConfirmed: 'Submission not confirmed', + }, +} as const; + +export const MID_CALL_SECTION_DEFINITIONS = [ + {key: 'reasonForTransferOrConsult', label: AI_SUMMARY_MESSAGES.sectionLabels.reasonForTransferOrConsult}, + {key: 'additionalContext', label: AI_SUMMARY_MESSAGES.sectionLabels.additionalContext}, + {key: 'keyActionsTaken', label: AI_SUMMARY_MESSAGES.sectionLabels.keyActionsTaken}, +] as const satisfies readonly {key: AISummarySectionKey; label: string}[]; + +export const POST_CALL_SECTION_DEFINITIONS = [ + {key: 'initialContactReason', label: AI_SUMMARY_MESSAGES.sectionLabels.initialContactReason}, + {key: 'additionalContactReasons', label: AI_SUMMARY_MESSAGES.sectionLabels.additionalContactReasons}, + {key: 'additionalContext', label: AI_SUMMARY_MESSAGES.sectionLabels.additionalContext}, + {key: 'keyActionsTaken', label: AI_SUMMARY_MESSAGES.sectionLabels.keyActionsTaken}, + {key: 'nextSteps', label: AI_SUMMARY_MESSAGES.sectionLabels.nextSteps}, +] as const satisfies readonly {key: AISummarySectionKey; label: string}[]; + +export const POST_CALL_DISPLAY_SECTION_ORDER = [ + 'initialContactReason', + 'additionalContactReasons', + 'additionalContext', + 'keyActionsTaken', + 'resolution', + 'nextSteps', +] as const satisfies readonly AISummaryPostCallDisplaySectionKey[]; + +type MissingPostCallDisplaySectionKeys = Exclude< + AISummaryPostCallDisplaySectionKey, + (typeof POST_CALL_DISPLAY_SECTION_ORDER)[number] +>; + +const assertCompletePostCallDisplaySectionOrder = ( + missingKeys: Record +): void => { + void missingKeys; +}; + +assertCompletePostCallDisplaySectionOrder({}); +assertUniquePostCallDisplaySectionOrder(POST_CALL_DISPLAY_SECTION_ORDER); diff --git a/packages/contact-center/cc-components/src/components/AISummary/ai-summary.styles.scss b/packages/contact-center/cc-components/src/components/AISummary/ai-summary.styles.scss new file mode 100644 index 000000000..473444cb0 --- /dev/null +++ b/packages/contact-center/cc-components/src/components/AISummary/ai-summary.styles.scss @@ -0,0 +1,440 @@ +.ai-summary { + color: var(--mds-color-theme-text-primary-normal, #1b1c1d); + min-inline-size: 0; + font-size: 0.875rem; + line-height: 1.4286; +} + +.ai-summary__status, +.ai-summary__content, +.ai-summary__actions, +.ai-summary__primary { + min-inline-size: 0; +} + +.ai-summary__status { + display: grid; + gap: 0.5rem; + min-block-size: 13rem; + place-content: center; + text-align: center; + flex: 1 1 auto; +} + +.ai-summary__spinner { + margin-block-start: 1.25rem; + justify-self: center; + inline-size: 1.75rem; + block-size: 1.75rem; + border: 0.1875rem solid var(--mds-color-theme-outline-secondary-normal, #d7d7d8); + border-block-start-color: var(--mds-color-theme-text-primary-normal, #1b1c1d); + border-radius: 50%; + animation: ai-summary-spin 1s linear infinite; +} + +.ai-summary__error-icon { + justify-self: center; + inline-size: 2.25rem; + block-size: 2.25rem; + color: var(--mds-color-theme-text-error-normal, #b4232a); +} + +.ai-summary__status-title { + margin: 0; + font-weight: 700; + line-height: 1.4; +} + +.ai-summary__status-title--generating { + font-size: 1rem; +} + +.ai-summary__status-title--error { + font-size: 0.875rem; +} + +.ai-summary__description, +.ai-summary__feedback-status { + margin: 0; + color: var(--mds-color-theme-text-secondary-normal, #54585c); + font-size: 0.875rem; + line-height: 1.4; + overflow-wrap: anywhere; +} + +.ai-summary__content { + display: grid; + gap: 0.5rem; +} + +.ai-summary__summary-heading { + display: flex; + align-items: center; + gap: 0.375rem; + margin-block: 0 0.5rem; + font-size: 0.875rem; + font-weight: 500; + line-height: 1.35; +} + +.ai-summary__summary-sparkle { + inline-size: 1rem; + block-size: 1rem; + color: var(--mds-color-theme-text-accent-normal, #0b5cad); +} + +.ai-summary__summary-info { + inline-size: 0.875rem; + block-size: 0.875rem; + color: var(--mds-color-theme-text-secondary-normal, #54585c); +} + +.ai-summary__editor-surface { + display: flex; + flex-direction: column; + border: 0.0625rem solid var(--mds-color-theme-outline-primary-normal); + border-radius: 0.5rem; + padding: 0.625rem 0.75rem; +} + +.ai-summary__sr-only { + position: absolute; + inline-size: 0.0625rem; + block-size: 0.0625rem; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; +} + +.ai-summary__section { + display: grid; + gap: 0.25rem; +} + +.ai-summary__section--structured { + gap: 0.125rem; +} + +.ai-summary .ai-summary__section-preview { + display: block; + inline-size: 100%; + min-block-size: 1.5rem; + border: 0; + padding: 0; + margin: 0; + background: transparent; + color: inherit; + font: inherit; + text-align: start; + white-space: normal; + cursor: text; + + > .ai-summary__label { + font-size: inherit; + line-height: inherit; + font-weight: 700; + } + + &:disabled { + opacity: 1; + cursor: default; + } +} + +.ai-summary__label { + font-size: 0.75rem; + font-weight: 600; + line-height: 1.3; +} + +.ai-summary__textarea { + box-sizing: border-box; + inline-size: 100%; + min-block-size: 1.35em; + resize: none; + overflow: hidden; + border: 0; + border-radius: 0; + padding: 0; + background: transparent; + color: inherit; + font: inherit; + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +.ai-summary__readonly, +.ai-summary__text { + margin: 0; + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +.ai-summary__readonly { + display: inline; +} + +.ai-summary .ai-summary__readonly > .ai-summary__label { + display: inline; + margin-inline-end: 0.25rem; + font-size: inherit; + line-height: inherit; + font-weight: 700; +} + +.ai-summary__readonly > .ai-summary__label::after { + content: ':'; +} + +.ai-summary__readonly-value { + white-space: pre-wrap; +} + +.ai-summary__readonly-list { + display: block; + margin: 0; + padding-inline-start: 1rem; + white-space: normal; +} + +.ai-summary__readonly-list > span { + display: list-item; + list-style-type: disc; +} + +.ai-summary__actions { + display: flex; + flex-wrap: wrap; + gap: 0.375rem; + align-items: center; + margin-block-start: auto; + padding-block-start: 0.5rem; +} + +.ai-summary__action { + position: relative; + display: inline-flex; +} + +.ai-summary__action--tooltip-open::before { + position: absolute; + inset-block-end: 100%; + inset-inline-start: 50%; + inline-size: 4rem; + min-inline-size: 100%; + block-size: 0.75rem; + content: ""; + transform: translateX(-50%); +} + +.ai-summary__button { + min-inline-size: 2rem; + border: 1px solid var(--mds-color-theme-outline-primary-normal, #d6d8dc); + border-radius: 6px; + background: var(--mds-color-theme-background-solid-primary-normal, #fff); + color: inherit; + cursor: pointer; + font: inherit; + padding: 0.375rem 0.625rem; +} + +.ai-summary__copy-button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.25rem; + min-inline-size: 1.5rem; + min-block-size: 1.5rem; + border-color: transparent; + padding: 0.25rem; +} + +.ai-summary__feedback-group { + display: inline-flex; + align-items: center; + gap: 4px; + inline-size: 44px; + block-size: 20px; + flex: 0 0 44px; +} + +.ai-summary__feedback-action { + inline-size: 20px; + block-size: 20px; +} + +.ai-summary__feedback-button { + box-sizing: border-box; + display: inline-flex; + align-items: center; + justify-content: center; + inline-size: 20px; + block-size: 20px; + min-inline-size: 20px; + min-block-size: 20px; + border: 0; + border-radius: 50%; + background: transparent; + padding: 2px; +} + +.ai-summary__status .ai-summary__button { + display: inline-flex; + align-items: center; + justify-self: center; + gap: 0.375rem; + border-radius: 999px; +} + +.ai-summary__actions mdc-icon { + inline-size: 1rem; + block-size: 1rem; +} + +.ai-summary__actions-spacer { + flex: 1 1 auto; + min-inline-size: 0; +} + +.ai-summary__attribution { + color: var(--mds-color-theme-text-secondary-normal, #54585c); + white-space: nowrap; +} + +.ai-summary__action-separator { + display: inline-block; + inline-size: 4px; + block-size: 4px; + flex: 0 0 4px; + border-radius: 50%; + background: var(--mds-color-theme-text-secondary-normal, #54585c); + opacity: 0.46; +} + +.ai-summary__tooltip { + position: absolute; + inset-block-end: calc(100% + 0.75rem); + inset-inline-start: 50%; + z-index: 1; + max-inline-size: 14rem; + transform: translateX(-50%); + border: 1px solid var(--mds-color-theme-outline-secondary-normal, #d7d7d8); + border-radius: 8px; + background: var(--mds-color-theme-background-solid-primary-normal); + color: var(--mds-color-theme-text-primary-normal); + box-shadow: 0 0.125rem 0.5rem var(--mds-color-theme-outline-secondary-normal); + font-size: 0.875rem; + line-height: 1.3; + padding: 0.75rem; + pointer-events: auto; + white-space: nowrap; +} + +.ai-summary__tooltip::after { + position: absolute; + inset-block-start: 100%; + inset-inline-start: 50%; + inline-size: 0.75rem; + block-size: 0.75rem; + border-inline-end: 1px solid var(--mds-color-theme-outline-secondary-normal, #d7d7d8); + border-block-end: 1px solid var(--mds-color-theme-outline-secondary-normal, #d7d7d8); + background: inherit; + content: ""; + transform: translate(-50%, -50%) rotate(45deg); +} + +.ai-summary__primary .ai-summary__button:hover, +.ai-summary__status .ai-summary__button:hover { + background: var(--mds-color-theme-background-primary-hover, #eef4ff); + border-color: var(--mds-color-theme-outline-accent-normal, #0b5cad); +} + +.ai-summary__copy-button:hover, +.ai-summary__copy-button--confirmed, +.ai-summary__feedback-button:hover { + background: var(--mds-color-theme-background-secondary-normal, #e9eaed); + border-color: transparent; + border-radius: 50%; +} + +.ai-summary__feedback-button--selected, +.ai-summary__feedback-button--selected:hover { + background: transparent; + border-color: transparent; +} + +.ai-summary__button:disabled, +.ai-summary__button[aria-disabled='true'] { + cursor: not-allowed; + opacity: 0.55; +} + +.ai-summary__button:focus-visible, +.ai-summary__section-preview:focus-visible, +.ai-summary__textarea:focus-visible { + outline: 2px solid var(--mds-color-theme-outline-accent-normal, #0b5cad); + outline-offset: 2px; +} + +.ai-summary__primary { + margin-block-start: 0.5rem; +} + +@keyframes ai-summary-spin { + to { + transform: rotate(360deg); + } +} + +// Constrained CallControl panels keep action rows outside the content scroll +// region. No screenshot-only override: this is also the production layout. +.wrap-up-summary__body, +.consult-transfer-summary { + > .ai-summary { + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-block-size: 0; + } + + .ai-summary__editor-surface { + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-block-size: 0; + } + + .ai-summary__content { + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-block-size: 0; + overflow-x: hidden; + overflow-y: auto; + overscroll-behavior: contain; + gap: 0; + } + + .ai-summary__content > *, + .ai-summary__actions { + flex-shrink: 0; + } +} + +@media (forced-colors: active) { + .ai-summary__button, + .ai-summary__textarea, + .ai-summary__tooltip { + border-color: ButtonText; + } + + .ai-summary__tooltip { + background: CanvasText; + color: Canvas; + } + + .ai-summary__button:focus-visible, + .ai-summary__textarea:focus-visible { + outline-color: Highlight; + } +} diff --git a/packages/contact-center/cc-components/src/components/AISummary/ai-summary.tsx b/packages/contact-center/cc-components/src/components/AISummary/ai-summary.tsx new file mode 100644 index 000000000..bd6d2432b --- /dev/null +++ b/packages/contact-center/cc-components/src/components/AISummary/ai-summary.tsx @@ -0,0 +1,749 @@ +import React, {useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react'; +import {Icon} from '@momentum-design/components/dist/react'; +import type { + AISummaryContent, + AISummaryDisplaySection, + AISummaryDisplaySectionKey, + AISummaryEditableField, + AISummaryFeedback, + AISummarySectionKey, +} from '@webex/cc-store'; +import { + AI_SUMMARY_MESSAGES, + COPIED_FEEDBACK_MS, + MID_CALL_SECTION_DEFINITIONS, + POST_CALL_DISPLAY_SECTION_ORDER, + POST_CALL_SECTION_DEFINITIONS, +} from './ai-summary.constants'; +import type { + AISummaryCopyVisualState, + AISummaryEditorProps, + AISummaryProps, + AISummarySectionEditorProps, +} from './ai-summary.types'; +import './ai-summary.styles.scss'; + +type AISummaryTooltipControl = 'copy' | 'like' | 'dislike'; + +const useReactId = (React as typeof React & {useId: () => string}).useId; + +const postCallOrdinal = new Map( + POST_CALL_DISPLAY_SECTION_ORDER.map((key, index) => [key, index]) +); + +const BULLETED_SECTION_KEYS = new Set([ + 'additionalContactReasons', + 'keyActionsTaken', + 'nextSteps', +]); + +const isSelectableFeedback = (value: AISummaryFeedback): value is Exclude => + value === 'like' || value === 'dislike'; + +const isConfirmedFeedbackResult = (result: unknown): boolean => + typeof result === 'object' && result !== null && (result as {outcome?: unknown}).outcome === 'confirmed'; + +const settleCallbackResult = (result: unknown, onFulfilled: (value: unknown) => void): void => { + void Promise.resolve(result) + .then(onFulfilled) + .catch(() => undefined); +}; + +const getSectionOrdinal = (key: AISummaryDisplaySectionKey): number => + postCallOrdinal.get(key) ?? Number.MAX_SAFE_INTEGER; + +const SECTION_LABELS = new Map([ + ...MID_CALL_SECTION_DEFINITIONS.map((definition) => [definition.key, definition.label] as const), + ...POST_CALL_SECTION_DEFINITIONS.map((definition) => [definition.key, definition.label] as const), + ['resolution', AI_SUMMARY_MESSAGES.sectionLabels.resolution], +]); + +const getAISummarySectionLabel = (section: Pick): string => + SECTION_LABELS.get(section.key) ?? section.key; + +export const projectPostCallDisplaySections = (content: AISummaryContent): AISummaryDisplaySection[] => { + if (content.type !== 'sections') { + return []; + } + const sections = [...content.sections]; + const resolution = content.resolution; + if (typeof resolution !== 'string' || resolution.trim().length === 0) { + return sections; + } + const outcome: AISummaryDisplaySection = { + key: 'resolution', + value: resolution, + editable: false, + }; + const outcomeOrdinal = getSectionOrdinal('resolution'); + const insertionIndex = sections.findIndex((section) => getSectionOrdinal(section.key) > outcomeOrdinal); + if (insertionIndex < 0) { + return [...sections, outcome]; + } + return [...sections.slice(0, insertionIndex), outcome, ...sections.slice(insertionIndex)]; +}; + +const flattenContentForCopy = (content: AISummaryContent): string => { + if (content.type === 'text') { + return content.summaryText; + } + if (content.type === 'card') { + return ''; + } + return projectPostCallDisplaySections(content) + .filter((section) => section.value.trim().length > 0) + .map((section) => `${getAISummarySectionLabel(section)}: ${section.value}`) + .join('\n\n'); +}; + +const splitDisplayLines = (value: string): string[] => value.split('\n').filter((line) => line.trim().length > 0); + +const stripBulletMarker = (line: string): string => line.trim().replace(/^(?:\u2022|[-*])\s*/, ''); + +const renderReadonlyValue = (section: AISummaryDisplaySection, inline = false) => { + const lines = splitDisplayLines(section.value); + if (BULLETED_SECTION_KEYS.has(section.key) && lines.length > 1) { + const List = inline ? 'span' : 'ul'; + const Item = inline ? 'span' : 'li'; + return ( + + {lines.map((line, index) => ( + + {stripBulletMarker(line)} + + ))} + + ); + } + + return ( + + {section.value} + + ); +}; + +// Keep keyed, controlled edits inside one visual surface. Never parse edited labels +// back into SDK keys or introduce a second scrollbar inside the containing panel. +const SummaryEditor: React.FC = ({value, ...props}) => { + const ref = useRef(null); + useLayoutEffect(() => { + const editor = ref.current; + if (!editor) return; + const resize = () => { + if (!editor.isConnected) return; + editor.style.height = 'auto'; + editor.style.height = `${editor.scrollHeight}px`; + }; + resize(); + void document.fonts?.ready.then(resize, () => undefined); + document.fonts?.addEventListener('loadingdone', resize); + let width = editor.clientWidth; + const observer = + typeof ResizeObserver === 'undefined' + ? undefined + : new ResizeObserver(() => { + if (editor.clientWidth !== width) { + width = editor.clientWidth; + resize(); + } + }); + observer?.observe(editor); + return () => { + observer?.disconnect(); + document.fonts?.removeEventListener('loadingdone', resize); + }; + }, [value]); + return