feat(ad-intent): UI v4 Part B — recommendedAction, landingPageMetrics, whatsLikelyHappening pass-through#2678
feat(ad-intent): UI v4 Part B — recommendedAction, landingPageMetrics, whatsLikelyHappening pass-through#2678danieljchuser wants to merge 10 commits into
Conversation
TDD implementation plan for Part B of the ad-intent-mismatch UI v4 spec (adobe/mysticat-architecture#149): persist paidTrafficShare; derive recommendedAction; thread landingPageMetrics via a 4th mapper arg; pass whatsLikelyHappening through; field-population observability log. 7 bite-sized tasks with complete code, grounded in the real source/tests. Incorporates two adversarial plan-review rounds (gaps #3/#4, drifts #1/#2). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…4th mapper arg Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…eploy verification Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
danieljchuser
left a comment
There was a problem hiding this comment.
AI Review
Hey @danieljchuser,
Verdict: Comment - no blocking issues. Clean, additive, well-tested change; the notes below are all optional.
Changes: adds whatsLikelyHappening / recommendedAction / landingPageMetrics to the ad-intent-mismatch opportunity.data, persists paidTrafficShare per predominantly-paid page, and extracts normalizeUrl into a shared utils.js (9 files; ~124 production LOC, test-to-code ratio 2.86).
No Critical or Important findings. The one direct dereference (auditResult.predominantlyPaidPages) is provably safe: the sole production caller in guidance-handler.js early-returns when auditResult is falsy before reaching the mapper, and the inline comment documents that contract. Every new branch (the ?? null / ?? 0 arms, c.keywords || [], the reason chain, the recommended_action_clusters log branch) has a matching test.
Non-blocking (6): doc placement, test parity, and small consistency notes
- suggestion (docs): the repo
CLAUDE.mdliterally says significant new functionality should add a spec underdocs/specs/; this PR ships a plan indocs/plans/and the controlling spec lives cross-repo atadobe/mysticat-architecture#149. That split matches the team information hierarchy (cross-repo platform specs in mysticat-architecture, repo-specific plans in the repo) and how prior same-area PRs shipped, so it is not a blocker - but consider either a thindocs/specs/pointer or reconciling theCLAUDE.mdwording so the rule and practice agree.docs/plans/2026-06-16-ad-intent-mismatch-ui-v4-audit-worker.md - suggestion (docs): the
EXCLUDE_ALIGNMENT_SCORES({poor}) vsMISALIGNED_SCORES({poor, fair}) divergence is a real product trade-off. Reviewers split on whether it needs an ADR underdocs/decisions/or whether the inline comment plus the cross-repo spec are sufficient. Your call - flagging so the decision is explicit.src/paid-keyword-optimizer/guidance-opportunity-mapper.js(EXCLUDE_ALIGNMENT_SCORES) - nit: the
normalizeUrlrelocation intoutils.test.jsdropped the old explicit empty-string assertion (normalizeUrl('') === ''). c8 coverage is unaffected (the empty string hits the samecatcharm as'not a url'), but the behavioral case is no longer asserted - re-add one line for parity.test/audits/paid-keyword-optimizer/utils.test.js - nit:
landingPageMetricsguardspaidTrafficShare ?? nullbut copiesbounceRate/engagedScrollRateraw, so a row missing those two would persistundefinedwhile the third normalizes tonull. Benign today (the runner always computes all three); either guard all three for a uniform wire contract or add a one-line comment that onlypaidTrafficShareis back-compat-optional.src/paid-keyword-optimizer/guidance-opportunity-mapper.js(landingPageMetricsblock) - suggestion: the exported
mapToKeywordOptimizerOpportunitynow throwsTypeErrorif a future caller passesundefinedas the 4th arg. Safe at the only current caller, so optional; note that switching toauditResult?.predominantlyPaidPages || []would add a new branch the strict 100% gate then needs a test for, which is presumably why it was written this way.src/paid-keyword-optimizer/guidance-opportunity-mapper.js(signature) - suggestion: hoist
normalizeUrl(url)out of the.find()predicate (it is loop-invariant); minor readability/clarity, not a real perf concern given the small list.src/paid-keyword-optimizer/guidance-opportunity-mapper.js(matchedPagefind)
Out of scope, worth tracking
- The downstream UI should escape
whatsLikelyHappeningandrecommendedAction.clusters[].reasonon render - they are upstream-controlled (Mystique) free text persisted verbatim. Producer-side sanitization here is not warranted (non-rendering JSON layer), so this belongs in the consuming UI repo, not this PR. - The cross-repo wire contract for these three fields now lives implicitly in this mapper plus
adobe/mysticat-architecture#149. As Mystique and the UI adopt the fields, a shared schema (or a schema-validation test) would prevent producer/consumer drift.
|
This PR will trigger a minor release when merged. |
There was a problem hiding this comment.
Hey @danieljchuser,
Verdict: Approve - clean, additive, well-tested change with no blocking issues.
Changes: adds whatsLikelyHappening, recommendedAction, and landingPageMetrics to the ad-intent-mismatch opportunity.data, persists paidTrafficShare per predominantly-paid page, and extracts normalizeUrl to a shared utils.js (9 files).
Note: CI checks are currently pending - verify before merge.
Non-blocking (5): minor consistency and edge-case notes
- nit:
normalizeUrl(url)is loop-invariant inside the.find()predicate - hoist to a localconst normalizedUrl = normalizeUrl(url)for clarity -src/paid-keyword-optimizer/guidance-opportunity-mapper.js:183 - nit:
bounceRateandengagedScrollRateare copied raw whilepaidTrafficSharegets?? null- applying the same guard to all three would make the wire contract uniform for pre-existing rows -src/paid-keyword-optimizer/guidance-opportunity-mapper.js:185 - nit:
k.traffic ?? 0handles null/undefined but NaN or negative values from the SEO provider would propagate intototalSearchVolume- considerNumber(k.traffic) || 0or a floor at 0 -src/paid-keyword-optimizer/guidance-opportunity-mapper.js:43 - nit: the old
handler.test.jsassertednormalizeUrl('') === ''explicitly; the newutils.test.jsexercises the catch path via'not a url'but dropped the empty-string boundary assertion -test/audits/paid-keyword-optimizer/utils.test.js - suggestion: CLAUDE.md says "Add a spec to
/docs/specs/" but team practice places platform specs cross-repo and local plans indocs/plans/- consider reconciling the wording or adding a thin pointer underdocs/specs/so rule and practice agree -CLAUDE.md(Documentation Requirements)
Skill: pr-review | Model: us.anthropic.claude-opus-4-6-v1[1m] | Duration: 1m 6s | Cost: $2.76 | Commit: 95ed0256416b33907ab394b21044ee91396513fe
If this code review was useful, please react with 👍. Otherwise, react with 👎.
Summary
Part B (audit-worker) of the Ad Intent Mismatch UI v4 spec (
adobe/mysticat-architecture#149). Adds three new fields to thead-intent-mismatchopportunity.dataand persists one supporting metric — all additive, no Mystique or UI dependency to start populatingrecommendedAction+landingPageMetrics.recommendedAction— derived in the mapper from poorly-aligned (overallAlignmentScore === 'poor', non-failed) clusters: anexcludeaction with dedupedtotalKeywords/totalSearchVolumeand a per-cluster "why excluded" reason (keywordToPageGap.explanation → gapDescription → null).nullwhen no poor clusters.landingPageMetrics—{ bounceRate, engagedScrollRate, paidTrafficShare }matched from the audit'spredominantlyPaidPagesby normalized URL (mapper gains a 4thauditResultarg, passed by the guidance handler).nullwhen no match;paidTrafficSharenull for pre-change rows.paidTrafficSharepersisted per page inpaidKeywordOptimizerRunner(previously computed then discarded).whatsLikelyHappening— verbatim pass-through of the Mystique SQS field (?? null); a no-op until Mystique emits it, then carried through with no further change.normalizeUrlextracted to a sharedutils.js; field-population log (snake_case booleans) after opportunity creation for post-deploy Coralogix verification (spec M9).Strictly additive: no existing
opportunity.dataor top-level field renamed/removed/retyped.Test Plan
npm test)npm run lint)keywordToPageGap,traffic ?? 0,keywords || []fallback, URL www-normalization, paidTrafficShare-absent)Plan:
docs/plans/2026-06-16-ad-intent-mismatch-ui-v4-audit-worker.mdSpec:
adobe/mysticat-architecture#149🤖 Generated with Claude Code