Skip to content

MWPW-183572: MAS Studio — AI assistant, product catalog, release flow, OST monorepo integration#776

Closed
Axelcureno wants to merge 89 commits into
mainfrom
MWPW-183572
Closed

MWPW-183572: MAS Studio — AI assistant, product catalog, release flow, OST monorepo integration#776
Axelcureno wants to merge 89 commits into
mainfrom
MWPW-183572

Conversation

@Axelcureno
Copy link
Copy Markdown
Member

@Axelcureno Axelcureno commented Apr 21, 2026

MAS Studio AI assistant, product catalog, release-flow enhancements, and the mas-ost monorepo workspace integration.

Resolves https://jira.corp.adobe.com/browse/MWPW-183572
QA Checklist: https://wiki.corp.adobe.com/display/adobedotcom/M@S+Engineering+QA+Use+Cases

What is in this PR

Two commits, both tagged with the Jira ticket:

  1. MWPW-183572: AI chat assistant, product catalog, release flow + IO Runtime — the full feature delivery plus the pre-review audit fixes baked in.
  2. MWPW-183572: integrate mas-ost as a monorepo workspace — OST source relocated from tacocat.js/mas-ost into this repo as the 4th npm workspace. Distinct architectural change, separate commit so reviewers can isolate the build/workspace question.

Feature commit contents

Studio frontend

  • Conversational AI chat (mas-chat.js + friends): Bedrock-backed, persistent sessions via chat-session-manager, operation preview + confirmation gating, destructive-tool allowlist enforced on the client.
  • Product catalog (mas-product-catalog.js / mas-product-detail.js): multi-select filters, surface-scoped editor/settings access (groups.js introduces isMasAdmin, canAccessSettings, getUserSurfaces), DRAFT landscape toggle, applied-filter chips.
  • MCS-integrated release flow: deterministic fragment content sourced from MCS merchandising data, dual-OSI plumbing for plans variants, multi-select OST hookup in rte/ost.js.
  • services/mcp-client.js: defensive guards on MCP response shapes (publish/get/copy/update contract drifts), AbortController on the AI chat fetch, URL-scrubbed logError helper applied across 14 call sites.
  • utils/ai-card-mapper.js: XSS-safe badge HTML via escape helpers; preserves explicit falsy field values (0, '', false); trim-aware title fallback chain.
  • utils/mas-chat-helpers.js: pure helpers extracted for direct unit testing.
  • markdown-parser.js: LRU-bounded cache to short-circuit repeat renders.

IO Runtime — AI chat backend (io/studio/src/ai-chat/)

  • AWS Bedrock integration with prompt-injection defense via wrapUntrusted sentinel envelopes. Attached-card fragment IDs wrapped element-wise instead of raw JSON.stringify to close a breakout vector.
  • Modules: bedrock-client, operations-handler, operations-prompt, prompt-templates, response-parser, variant-configs, variant-knowledge-builder, knowledge-client, validation, index.js.

IO Runtime — MCP server (io/mcp-server/)

  • 31 runtime actions covering card + collection + offer + product CRUD/search.
  • Surface-scoped authz on fragment-creating actions (create-release-cards, create-card, create-collection, copy-card) via new helpers in lib/ims-validator.jsderiveSurfaceFromPath, fetchUserGroups, canEditSurface, requireSurfaceAccess. Prevents cross-surface mutation via direct API calls.

mas-mcp-server (stdio entry for Claude Code / Cursor)

  • Retired the Express HTTP bridge (http-server.js). Dropped cors / express deps + the http npm script.

Web components

  • Small audit touches on aem-fragment, merch-card-collection, Express pricing variants.

mas-ost workspace commit

  • 63-file source tree extracted from tacocat.js/mas-ost@f5a71b9: Lit 3 + Spectrum Web Components, Vite build, WTR tests.
  • Registered in root package.json workspaces. Build script outputs the IIFE bundle and copies to ../studio/ost/index.js in one step.
  • Byte-for-byte identical bundle output validated via in-browser smoke test (window.ost.openOfferSelectorTool exposed, all 5 custom elements registered, no JS errors at load).

Testing

  • Full studio suite: 1240 pass / 0 fail / 2 skipped (same as baseline on main).
  • 10 new characterization tests in studio/test/** covering every new service/util + 2 XSS + 1 prompt-injection repro.
  • 2 new tests in io/studio/test/ai-chat/ (bedrock-client, operations-handler).
  • 1 new file in io/mcp-server/test/lib/ with 23 tests: canEditSurface matrix, fetchUserGroups edge cases, full requireSurfaceAccess middleware integration.
  • mas-ost suite: 209 pass / 4 pre-existing upstream failures (3 country-picker SWC lifecycle + 1 OstStore.addOffer.tryBuy). Not regressions from the integration. Tracked as follow-up.

Please do the steps below before submitting your PR for a code review or QA

  • C1. Cover code with Unit Tests — 20+ new characterization tests; 1240 studio tests green.
  • C2. Add a Nala test (double check with #fishbags if nala test is needed).
  • C3. Verify all Checks are green (unit tests, nala tests) — studio suite green locally; CI will confirm.
  • C4. PR description contains working Test Page link where the feature can be tested.
  • C5: you are ready to do a demo from Test Page in PR.
  • C.6 read your Jira one more time to validate that you have addressed all AC and nothing is missing.

Follow-up (filed as deferred, not PR-blocking)

  • Per-fragment surface authz on id-based MCP mutations (update-card, delete-card, publish-card, bulk-*). This PR gates path-based creation; id-based mutations currently rely on AEM's own ACLs to scope access.
  • Dev-namespace 14257-merchatscale-axel remains hardcoded in studio/src/constants.js, studio/src/services/product-api.js, and io/studio/src/ai-chat/knowledge-client.js with a TODO pointing at the post-merge swap to masstudio once that deploy completes.
  • 4 pre-existing mas-ost tests inherited from upstream (country-picker SWC + OstStore tryBuy) need fixing.
  • io/knowledge (the AI knowledge service / RAG backend) was intentionally scoped out of this PR. It's a separate Runtime app with its own lifecycle; RAG_ENABLED=false keeps the chat fully functional without it.

Test URLs:

Screenshots

To be added: OST dialog opened from AI chat, product catalog with multi-select filters, AI chat confirmation summary, release card preview.

@aem-code-sync
Copy link
Copy Markdown

aem-code-sync Bot commented Apr 21, 2026

Hello, I'm the AEM Code Sync Bot and I will run some actions to deploy your branch.
In case there are problems, just click the checkbox below to rerun the respective action.

  • Re-sync branch
Commits

Axelcureno and others added 3 commits April 21, 2026 11:54
…ntime

Delivers the MAS Studio AI assistant experience and its supporting
infrastructure. Everything ships in one commit so reviewers see a
coherent feature set.

Studio frontend (studio/src/):
- AI chat UI: mas-chat.js + mas-chat-message.js + mas-chat-input.js +
  mas-chat-product-cards.js + mas-chat-session-selector.js +
  mas-chat-confirmation-summary.js + mas-chat-button-group.js +
  mas-chat-drawer.js + mas-chat-fab.js. Persistent sessions via
  chat-session-manager, markdown rendering with an LRU-bounded cache,
  operation-preview and destructive-tool confirmation gating.
- Product catalog (mas-product-catalog.js + mas-product-detail.js) with
  multi-select filters, surface-scoped editor/settings access via
  groups.js (isMasAdmin + canAccessSettings + getUserSurfaces), DRAFT
  landscape toggle, applied-filter chips.
- MCS-integrated release flow: deterministic fragment content from
  MCS merchandising data, dual-OSI plumbing for plans variants,
  multi-select OST hooked through rte/ost.js.
- Shared services: mcp-client.js (defensive guards on response shape,
  AbortController on chat fetch, error-log scrubbing via logError).
- utils/ai-card-mapper.js: XSS-escaped badge rendering, falsy-value
  preservation on fragment fields, trim-aware title fallbacks.
- utils/mas-chat-helpers.js: pure helpers extracted for unit testing.

IO Runtime - AI chat backend (io/studio/src/ai-chat/):
- bedrock-client.js with prompt-injection defense via wrapUntrusted
  sentinel envelopes; attached-card fragment IDs wrapped element-wise
  instead of raw JSON.stringify.
- operations-handler, operations-prompt, prompt-templates,
  response-parser, variant-configs, variant-knowledge-builder,
  knowledge-client, validation, index.js.

IO Runtime - MCP server actions (io/mcp-server/):
- 31 runtime actions (card + collection + offer + product CRUD/search).
- Surface-scoped authz on fragment-creating actions
  (create-release-cards, create-card, create-collection, copy-card):
  deriveSurfaceFromPath + fetchUserGroups + canEditSurface +
  requireSurfaceAccess helpers in lib/ims-validator.js. Prevents
  cross-surface mutation via direct API calls.

mas-mcp-server (stdio entry for Claude Code / Cursor):
- Retired the Express HTTP bridge (http-server.js). Dropped
  cors/express deps + the 'http' npm script.

Web-components:
- Small audit touches on aem-fragment, merch-card-collection,
  variants/full-pricing-express, variants/simplified-pricing-express.

Testing:
- 10 new characterization-test files in studio/test/** covering every
  new service/util plus 2 XSS + 1 prompt-injection repro.
- 2 new test files in io/studio/test/ai-chat/.
- 1 new test file in io/mcp-server/test/lib/ covering surface authz
  (23 tests: canEditSurface matrix, fetchUserGroups, full middleware
  integration).

Out of scope (deferred to follow-ups):
- Per-fragment surface authz on id-based MCP mutations
  (update/delete/publish/bulk-*) - currently rely on AEM's own ACLs.
- Translation work that entered this branch via a main merge is
  included as-is from main (no ticket changes to the translation
  subsystem).
Moves the OST (Offer Selector Tool) source from the separate
tacocat.js/mas-ost tree into this repo as the 4th npm workspace
alongside studio/, web-components/, and io/studio/.

Changes:
- mas-ost/ tree with Lit 3 + Spectrum Web Components source, Vite
  build config, WTR tests, and dev HTML pages. 63 source + test
  files extracted from tacocat.js commit f5a71b9.
- Root package.json registers mas-ost in the workspaces array.
- mas-ost/package.json build script outputs the IIFE bundle and
  copies it to ../studio/ost/index.js in one step (replaces the
  previous cross-repo deploy:local workflow).
- Root package-lock.json regenerated by npm install to wire the
  new workspace and dedupe Lit/SWC against the root.

Bundle output (studio/ost/index.js) is already in the previous
commit alongside the frontend consumer changes. This commit adds
the source side so the OST is maintained in-repo going forward.

Known: 4 mas-ost unit tests fail upstream (3 country-picker SWC
lifecycle + 1 OstStore.addOffer.tryBuy). Inherited from the source,
not regressions from the relocation. Tracked as follow-up.
- Deterministic router for bare offer IDs, arrangement codes, and OSIs
  so PA codes and arrangement slugs (e.g. cptv_direct_individual) no
  longer misclassify as OSIs.
- Offer-id fallback in resolveOfferSelector for 32-hex IDs that OST
  surfaces in the OSI slot for draft offers.
- Trial-CTA yes/no step inserted on the OST Use shortcut path so users
  can still add a free-trial offer when seeding from a single offer.
- Rename "Annual, paid monthly" to "Annual, billed monthly" (ABM).
- Product card auto-selects its single match via productCardsSelectedValue
  so the preview card renders as confirmed, not clickable.
- OST: pin filled-slot colors to light-palette hex so labels stay
  readable against saturated-green backgrounds in dark themes.
- OST: hide the "BOTH" landscape badge (kept per-offer source badges).
- Replace sp-icon-offer with sp-icon-label for the Search offers chip.
…ent_code

Replace LLM-mediated list_products call in release flow with a direct
get_product_by_arrangement_code IO action — prevents hallucinated
arrangement codes from selecting the wrong product.
Adds a hybrid frontend/backend search path that bypasses the LLM for
high-confidence patterns (UUID, OSI, "find cards titled X") and surfaces
results as clickable Studio deep-links instead of hydrated card previews.

Frontend
- studio/src/utils/ai-chat-search-router.js: pure intent classifier with
  UUID / OSI / offer-id / quoted-title / titled-verb detectors and slot
  resolution; returns either a deterministic dispatch or a missing-slot
  follow-up prompt.
- studio/src/utils/ai-chat-search-telemetry.js: dark-launch flag
  (localStorage.masChatDeterministicSearch) and dataLayer events.
- studio/src/mas-chat.js: tryDeterministicSearch hook in handleSendMessage,
  pendingSearchIntent state for surface follow-up, get_card 404 graceful
  conversion, polite empty-result pivots.
- studio/src/mas-operation-result.js: new mode=links rendering path that
  renders an sp-table of anchor-tag rows with no aem-fragment hydration,
  plus a "View all N in Studio" surface-folder link.
- studio/src/utils.js: buildStudioFolderHref helper.
- studio/src/utils/mas-chat-helpers.js: extractKnownSurfaceFromPath now
  handles the short URL forms used by Studio's hash router.

Backend (MerchAtScaleMCP)
- io/mcp-server/src/actions/search-by-id.js: new fast-path action for
  UUID/OSI lookups with structured 5s timeout and graceful empty-result
  on miss (no throw on 404).
- io/mcp-server/src/actions/search-cards.js: 5s default / 15s title-search
  Promise.race timeout, hard 200-result cap, id/osi short-circuit.
- io/mcp-server/src/lib/studio-operations.js: searchById method,
  mapWithConcurrency helper replacing 3 unbounded Promise.all fan-outs,
  extractFieldValue helper deduplicating OSI extraction, structured
  SURFACE_REQUIRED error replacing throw, new two-stage title-substring
  search (full-text narrow with strongest non-stopword token + local
  title-field substring filter).
- io/mcp-server/src/lib/aem-client.js: AbortSignal.timeout per page in
  findFragmentsByTitleFallback.
- io/mcp-server/app.config.yaml: register search-by-id action.

Tests
- 27 router unit tests (UUID/OSI/offer-id/quoted/titled-verb/slot/resume).
- 4 component tests verifying no merch-card hydration in links mode.
- 9 backend tests for searchById and structured surface error.

End-to-end verified against the deployed action: title-substring search
returns 52 "Wide Card" matches in ~3.5s, 40 "Photoshop" matches in ~4.6s,
real UUID lookup returns the fragment in less than 1s, fake UUID returns
polite "No card found" with two pivots, LLM fallback intact for non-search
asks.
The deterministic search router now extracts the locale slot from the
message itself instead of always using the user's current locale:
- 'in all locales' / 'across all locales' / 'across every locale' → 'all'
  (backend scans /content/dam/mas/<surface> across every locale folder)
- 'in fr_FR' / 'for de_DE' → that explicit locale
- otherwise → currentLocale from context

Verified end-to-end: 'Find me all cards with fragment title "CC Plans
Merch Card: Firefly Pro Plus: Individuals: 50-percent-promo" in all
locales' returns 24 matches across sv_SE, tr_TR, ko_KR, fr_FR, lt_LT,
he_IL etc. — instead of being silently scoped to the current locale.
The router is now on for everyone instead of behind a localStorage flag.
This was a dark-launch gate for the initial rollout; testers on the
staged branch (mwpw-183572--mas--adobecom.aem.live) hit the LLM-only
path and saw the same unreliability the router was built to fix.

Emergency opt-out: localStorage.masChatDeterministicSearch = 'off'
Telemetry continues to record source: 'router' vs 'llm' for monitoring.
User reported a result-count discrepancy: AEM admin returned 50+ matches
for a card title across all locales but the AI assistant capped at 24.

Root cause was twofold:
1. We passed the strongest single token (e.g. 'firefly') to AEM's
   full-text search instead of the full title — letting the index
   narrow on the wrong axis and pulling thousands of unrelated cards
   into the candidate pool before the local title-substring filter.
2. The candidate pool was capped at 4 offset pages of 50 = 200 items,
   so for any common token we silently dropped real matches.

Fix mirrors what the production studio search already does in
studio/src/aem/aem.js: pass the user's full query as fullText.text and
iterate AEM's cursor until exhausted.

aem-client.js
- searchFragments now accepts cursor and includeCursor params.
  Backwards compatible: returns array by default; returns
  {items, cursor} only when includeCursor=true.
- When cursor is provided, sends it as a query param in place of offset.

studio-operations.js
- Title-search branch replaces 4-page Promise.all fan-out with a cursor
  loop bounded at 40 pages (safety net inside the action's 15s timeout).
- Sends the user's full query as fullText.text so AEM does the actual
  narrowing.
- Removes pickStrongestToken (now dead code).

Verified end-to-end: the user's exact prompt
  'Find me all cards with fragment title "CC Plans Merch Card: Firefly
   Pro Plus: Individuals: 50-percent-promo" in all locales'
now returns 78 cards spanning 39 locales (sv_SE, tr_TR, ko_KR, fr_FR,
lt_LT, he_IL, vi_VN, ro_RO, ja_JP, de_DE, ...) in ~9.5s — exceeding
the 50+ AEM admin shows because we also include the OPT-40208 promo
variants in every locale.

Tests: 49 mcp-server tests passing (44 prior + 5 new cursor tests).
…inks

Two issues from a user report on the AI assistant search results:

1. The "Copy all links" button only appeared on the legacy (LLM-driven)
   render path. Deterministic-router results in the new lightweight mode
   were missing it.
2. The copy payload was markdown ([title](url)) — paste targets like
   Slack, Outlook, Confluence don't render markdown, so users got the
   raw text.

Fix
- mas-operation-result.js: lightweight render now shows all three
  actions consistently — Show N more, Copy all links, View all in
  Studio →. The Copy all links button is no longer conditional.
- copyAllCardLinks now writes both text/html and text/plain to the
  clipboard via ClipboardItem when supported. The HTML payload is a
  <ul><li><a href="...">title</a></li></ul> list, so rich-text editors
  paste real clickable hyperlinks. Plain-text fallback (title \n url
  per entry) keeps backwards compatibility for terminal-style targets
  and older browsers.
- Tooltip wording updated from "as a markdown list" to "Copy all card
  links".
- Adds local escapeHtml helper to prevent broken HTML when titles
  contain & < > " '.

Component tests still pass (4 lightweight-mode component tests, 32
router unit tests).
User feedback: action row was cramped (no gap), View all was an
underlined link instead of a button, and all visible buttons looked
identical (same secondary variant).

mas-operation-result.js
- View all in Studio is now an sp-button (primary variant, href +
  target=_blank) instead of a styled anchor.
- Show more uses treatment=outline for low-emphasis disclosure.
- Copy all links keeps the secondary fill for medium emphasis.
- Pattern applied to both lightweight and legacy renderers so
  searches look the same regardless of which path produced them.

style.css
- .search-results-actions: gap 12px (size-150) between buttons,
  flex-wrap for narrow chat panes, align-items: center.

mas-operation-result-links.test.js
- View-all assertion updated from anchor.view-all-link to
  sp-button[variant=primary] with the same href contract.
- New tests: Copy and View buttons always render; Show-more renders
  only when results exceed displayCount.
Previous attempt added gap to studio/style.css, but the chat panel
loads styles from studio/src/styles/chat.css which had its own
duplicated .search-results-actions rule without gap — so buttons
stayed touching.

Fix: dedupe the two rules in chat.css, fold in the user's manual
margin-left workaround as a sibling-selector fallback for older
Spectrum versions, bump gap to size-200 (16px) so the spacing is
clearly visible.

Both rules (style.css and chat.css) now match: flex with gap,
align-items: center, flex-wrap for narrow chat panes.
Conflicts resolved:
- web-components/dist/mas.js, merch-card.js, merch-card-collection.js
  (auto-resolved, then rebuilt from source)
- .gitignore: kept both branch-local entries and main's mcp-server.log
- io/studio/app.config.yaml: kept both ai-chat (branch) and bulk-publish
  (main) action declarations
- studio/src/rte/ost.js: kept both EVENT_OST_MULTI_OFFER_SELECT (branch)
  and PLACEHOLDER_CTA_SURFACES (main) imports
- web-components/src/variants/simplified-pricing-express.js: combined
  main's CTA-class refactor (small-font-size-button) with branch's
  requestAnimationFrame-wrapped syncHeights call

Verified post-merge:
- web-components: 1286 tests passing
- io/mcp-server: 49 tests passing (router + searchById + cursor)
- studio router: 38 tests passing (32 router + 6 component)
User feedback: "search for cards containing firefly" should return all
cards that mention firefly anywhere — title, description, CTAs,
prices, etc. — not just title matches.

Frontend: ai-chat-search-router.js
- New CONTENT_VERB_RE matches "find/show/search/list cards
  containing|with|mentioning|about|having|referencing|including X"
- New buildContentDispatch + buildContentDispatchFromSlots helpers
  that emit search_cards WITHOUT titleSearch=true so AEM full-text
  covers all indexed fields
- resumeWithSlot routes to title or content dispatch based on
  pending.intent
- Locale parsing ("in all locales", "in fr_FR") works for content
  search the same as title search

Frontend: mas-chat.js
- maybeOfferSearchPivots now emits "No cards contain X..." for the
  content-search intent and updates the help line to mention "title
  or content" alongside ID/OSI/offer ID

Backend: studio-operations.js
- Default keyword-search path (the non-titleSearch branch) now uses
  AEM cursor pagination instead of single-page offset + N+1
  getFragment. Pulls all matches up to MAX_PAGES=40 (2000 fragments
  max) wrapped by the action timeout.
- Removed the per-fragment getFragment fan-out: searchFragments
  already returns full fields arrays so the round trip was wasted.
- Removed unused requestLimit local.

Backend: search-cards.js
- Renamed TITLE_SEARCH_TIMEOUT_MS to KEYWORD_SEARCH_TIMEOUT_MS
  (15s) and applied it whenever query is set, not just when
  titleSearch is true. Keyword/content/title searches all paginate
  via cursor and benefit from the longer budget.

Tests
- 7 new router unit tests covering "containing", "mentioning",
  "with", "about" phrasings, missing-surface slot fill, locale
  carry-through, and resumeWithSlot routing for content-search.
- All 49 mcp-server tests still passing.
- Verified end-to-end: "search for cards containing firefly" with
  surface=acom locale=en_US returns 115 cards in ~9s, rendered in
  the lightweight links table with three properly spaced action
  buttons.
…ault

The new registry-driven prompt + envelope validator is now primary for
every ai-chat request. Frontend dispatcher reads from envelope.category
and envelope.intent; the old type-based switch is preserved as fallback
for one release cycle so rollback is git revert only.

What this fixes (the four named regression bugs):
- slug-fabrication: validator rejects slug-shaped fragmentIds
- update-verb-with-card-noun misroute: structural rule 3 + validator
- create-flow-drift: validator enforces flow next_intents
- quoted-value misroute: structural rule 3 + intent registry

Live-LLM pass rate: 13/16 = 81% (gate: 75%)
Shadow logging unchanged; old prompt/classifier code retained for Stage 3.3 deletion.

Rollback: git revert <THIS_SHA>.
The deterministic title-search bypass in index.js was matching the
possessive apostrophe in 'card's' as a quote delimiter, then capturing
the gibberish between the apostrophe and the user's actual quoted value
('s description to say:') as the title query. Mutation requests like
"Change the card's description to say: 'X'" got routed to title search
with that captured gibberish, bypassing Bedrock entirely.

Skip the bypass when the message contains a mutation verb. The LLM +
envelope validator already handle these cases correctly — proven by the
4 regression cases in the eval harness.

Fixes the exact bug the user hit in production today: 'Change the card's
description to say: ...' now routes to update_card (with lastOperation)
or ASK_USER (without). Verified end-to-end against deployed action v0.0.103.
@Axelcureno
Copy link
Copy Markdown
Member Author

no longer needed. Work will be merged in split PRs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant