aa-top-movers-watchlist

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Top Movers Watchlist (Adobe Analytics)

异动指标监控清单(Adobe Analytics)

Identify which dimension items had the biggest increases and decreases for a key metric between two time periods. Surface the top gainers and losers across pages, channels, campaigns, products, or any other breakout dimension.

识别两个时间段内,哪些维度项的关键指标出现了最大幅度的增长或下降。展示页面、渠道、营销活动、产品或其他拆分维度下的Top涨幅项与跌幅项。

AA MCP Tools Used

使用的AA MCP工具

  • findReportSuites
    — select report suite
  • setSessionDefaults
    — set session context (reportSuiteId + globalCompanyId)
  • describeAa(REPORT_SUITE_CONTEXT_GUIDE)
    — load calendar/timezone context
  • findMetrics
    — resolve the key metric ID
  • findDimensions
    — discover available breakdown dimensions
  • runReport
    — period A and period B for each dimension
  • searchDimensionItems
    — validate specific items if needed

  • findReportSuites
    — 选择报表套件
  • setSessionDefaults
    — 设置会话上下文(reportSuiteId + globalCompanyId)
  • describeAa(REPORT_SUITE_CONTEXT_GUIDE)
    — 加载日历/时区上下文
  • findMetrics
    — 解析关键指标ID
  • findDimensions
    — 发现可用的拆分维度
  • runReport
    — 获取每个维度在时间段A和时间段B的数据
  • searchDimensionItems
    — 按需验证特定维度项

Phase 0 — Setup

阶段0 — 准备工作

  1. Confirm report suite with
    findReportSuites
    /
    setSessionDefaults
    .
  2. Call
    describeAa(REPORT_SUITE_CONTEXT_GUIDE)
    to load report suite context. Record:
    • WEEK_START_DOW
      — first-day-of-week from the context guide. If the context guide returns no value, use Monday (ISO 8601) as the explicit deterministic default. This is not a fallback that drifts between runs: every run on the same report suite resolves to the same
      WEEK_START_DOW
      , either from the context guide or from the fixed Monday default.
    • TIMEZONE
      — report suite timezone.
    • WEEK_START_DOW_SOURCE
      "context guide"
      if the value came from
      describeAa
      ,
      "default"
      if the context guide was silent and Monday was used. Surface this in the artifact footer so the source is auditable.
    You will use these values in Phase 1 when defining Period A and Period B.
findReportSuites(globalCompanyId: "<gcid>")
setSessionDefaults(globalCompanyId: "<gcid>", reportSuiteId: "<rsid>")
describeAa(guideType: "REPORT_SUITE_CONTEXT_GUIDE")

  1. 使用
    findReportSuites
    /
    setSessionDefaults
    确认报表套件。
  2. 调用
    describeAa(REPORT_SUITE_CONTEXT_GUIDE)
    加载报表套件上下文,并记录:
    • WEEK_START_DOW
      — 上下文指南中的一周起始日。如果上下文指南未返回值,则明确使用周一(ISO 8601标准)作为确定性默认值。此默认值不会随运行变化:同一报表套件的每次运行都会解析为相同的
      WEEK_START_DOW
      ,要么来自上下文指南,要么使用固定的周一默认值。
    • TIMEZONE
      — 报表套件时区。
    • WEEK_START_DOW_SOURCE
      — 如果值来自
      describeAa
      则为
      "context guide"
      ,如果上下文指南未提供值而使用了周一默认值则为
      "default"
      。将此信息显示在报告页脚,以便追溯来源。
    这些值将在阶段1定义时间段A和时间段B时使用。
findReportSuites(globalCompanyId: "<gcid>")
setSessionDefaults(globalCompanyId: "<gcid>", reportSuiteId: "<rsid>")
describeAa(guideType: "REPORT_SUITE_CONTEXT_GUIDE")

Phase 1 — Confirm Parameters

阶段1 — 确认参数

Ask the user:
  1. Metric — "Which metric should I rank movers by? (visits, revenue, conversions, page views, etc.)" If not specified, default to
    metrics/visits
    .
  2. Dimension — "Which dimension should I break down?
    • Pages
    • Marketing Channel
    • Campaigns
    • Products
    • Traffic Sources
    • Geographic Regions
    • Entry Pages" If not specified, offer the above list.
  3. Time periods — "What two periods should I compare?" Common defaults:
    • This week vs. last week
    • This month vs. last month
    • Last 7 days vs. prior 7 days
    • Last 30 days vs. prior 30 days
    Calendar rule (mandatory): Period A and Period B MUST use the same
    WEEK_START_DOW
    from Phase 0 — both periods'
    startDate
    fall on the same day-of-week, both are exactly equal length, and Period B ends immediately before Period A starts. Never mix conventions (e.g., a Mon–Sun Period A with a Sun–Sat Period B) within the same run. For custom date ranges, compute Period B as the equal-length window ending immediately before Period A starts.
    Sanity check before calling
    runReport
    :
    confirm
    periodA.startDate
    and
    periodB.startDate
    are the same day-of-week and that
    periodA.startDate - periodB.endDate == 1 day
    . If not, recompute.
  4. Item limit — how many top gainers and losers to show (default: 10 each).
  5. Materiality threshold — minimum absolute value in Period A or B to be included (filters out noise from low-volume items). Default: exclude items with fewer than 100 visits (or equivalent) in both periods.
Confirm: "I'll show the top 10 gainers and losers for [metric] broken down by [dimension], comparing [Period A] vs [Period B]."

询问用户:
  1. 指标 — “我应该依据哪个指标对异动项进行排名?(访问量、收入、转化量、页面浏览量等)” 如果未指定,默认使用
    metrics/visits
  2. 维度 — “我应该按哪个维度拆分数据?
    • 页面
    • 营销渠道
    • 营销活动
    • 产品
    • 流量来源
    • 地理区域
    • 入口页面” 如果未指定,提供上述列表供选择。
  3. 时间段 — “我应该对比哪两个时间段?” 常见默认选项:
    • 本周 vs 上周
    • 本月 vs 上月
    • 最近7天 vs 之前7天
    • 最近30天 vs 之前30天
    日历规则(强制要求):时间段A和时间段B必须使用阶段0中确定的相同
    WEEK_START_DOW
    — 两个时间段的
    startDate
    必须是同一星期几,长度完全相等,且时间段B在时间段A开始前结束。同一运行中绝不能混合使用不同的日期约定(例如,时间段A为周一至周日,而时间段B为周日至周六)。对于自定义日期范围,将时间段B计算为与时间段A长度相等且在其开始前结束的时间窗口。
    调用
    runReport
    前的合理性检查
    :确认
    periodA.startDate
    periodB.startDate
    是同一星期几,且
    periodA.startDate - periodB.endDate == 1天
    。如果不满足,则重新计算。
  4. 项数限制 — 显示多少个Top涨幅项和跌幅项(默认:各10个)。
  5. 重要性阈值 — 时间段A或B中需达到的最小绝对值(过滤低流量项带来的噪音)。默认:排除两个时间段中访问量(或等效指标)均少于100的项。
确认话术:“我将展示[指标]按[维度]拆分后的Top 10涨幅项和跌幅项,对比[时间段A]与[时间段B]。”

Phase 2 — Resolve Components

阶段2 — 解析组件

findMetrics(expansions: "componentType")
findDimensions(page: 1, limit: 100)
Note:
findMetrics
requires the
expansions
parameter (use
"componentType"
or
"categories"
).
findDimensions
requires
page
and
limit
.
Record
id
for the metric and dimension.

findMetrics(expansions: "componentType")
findDimensions(page: 1, limit: 100)
注意
findMetrics
需要
expansions
参数(使用
"componentType"
"categories"
)。
findDimensions
需要
page
limit
参数。
记录指标和维度的
id

Phase 3 — Run Period Reports

阶段3 — 运行时间段报表

Run the metric for the selected dimension in both periods:
runReport(
  metricIds: "<metricId>",
  dimensionId: "<dimensionId>",
  startDate: "<period A start>T00:00",
  endDate: "<period A end>T23:59",
  limit: 200
)

runReport(
  metricIds: "<metricId>",
  dimensionId: "<dimensionId>",
  startDate: "<period B start>T00:00",
  endDate: "<period B end>T23:59",
  limit: 200
)
Note:
runReport
uses
metricIds
(not
metricId
) and
startDate
/
endDate
in ISO 8601 format (
YYYY-MM-DDTHH:mm
), not a
dateRange
parameter.
Use
limit: 200
to capture enough items for a meaningful mover analysis.

为选定维度在两个时间段内运行指标查询:
runReport(
  metricIds: "<metricId>",
  dimensionId: "<dimensionId>",
  startDate: "<period A start>T00:00",
  endDate: "<period A end>T23:59",
  limit: 200
)

runReport(
  metricIds: "<metricId>",
  dimensionId: "<dimensionId>",
  startDate: "<period B start>T00:00",
  endDate: "<period B end>T23:59",
  limit: 200
)
注意
runReport
使用
metricIds
(而非
metricId
),且
startDate
/
endDate
采用ISO 8601格式(
YYYY-MM-DDTHH:mm
),不使用
dateRange
参数。
使用
limit: 200
以捕获足够多的项,确保异动分析有意义。

Phase 4 — Compute Deltas and Rank

阶段4 — 计算差值并排名

For each dimension item present in either period:
  1. Period A value (or 0 if not in results)
  2. Period B value (or 0 if not in results)
  3. Absolute delta: Period A - Period B
  4. Percent change: delta / Period B × 100 (or "+100% new" if Period B = 0)
  5. Apply materiality filter: exclude items where max(Period A, Period B) < materiality threshold
Sort by absolute delta descending for gainers (positive delta). Sort by absolute delta ascending for losers (negative delta).
Take top N from each list.
对于任一时间段中存在的每个维度项:
  1. 时间段A的值(如果未在结果中则为0)
  2. 时间段B的值(如果未在结果中则为0)
  3. 绝对差值:时间段A - 时间段B
  4. 变化百分比:差值 / 时间段B × 100(如果时间段B=0则显示"+100% new")
  5. 应用重要性过滤:排除时间段A和时间段B的最大值均低于重要性阈值的项
按绝对差值降序排列涨幅项(正差值)。 按绝对差值升序排列跌幅项(负差值)。
从每个列表中选取前N项。

Special cases

特殊情况

  • New items (present in Period A only): flag as "New — no prior period data." Include if materiality threshold met.
  • Disappeared items (present in Period B only, 0 in Period A): flag as "Dropped — no current period data."
  • Near-zero items: items with <1% of total metric value — consider excluding from the watchlist unless they show very large percent changes.

  • 新增项(仅在时间段A中存在):标记为"新增 — 无前期数据"。如果满足重要性阈值则纳入。
  • 消失项(仅在时间段B中存在,时间段A中为0):标记为"已消失 — 无当前数据"。
  • 近零项:指标值占总指标值不足1%的项 — 除非其变化百分比极大,否则考虑从监控清单中排除。

Phase 5 — Optional Multi-Dimension Watchlist

阶段5 — 可选的多维度监控清单

If the user wants movers across multiple dimensions (e.g., both pages AND channels), repeat Phase 3–4 for each additional dimension. Each additional dimension adds 2 more
runReport
calls.
runReport(metricIds: "<metricId>", dimensionId: "variables/marketingchannel",
          startDate: "<A>T00:00", endDate: "<A>T23:59", ...)
runReport(metricIds: "<metricId>", dimensionId: "variables/page",
          startDate: "<A>T00:00", endDate: "<A>T23:59", ...)
Group results into separate tables by dimension in the HTML report.

如果用户希望查看跨多个维度的异动项(例如,同时查看页面和渠道),则为每个额外维度重复阶段3-4的操作。每个额外维度会增加2次
runReport
调用。
runReport(metricIds: "<metricId>", dimensionId: "variables/marketingchannel",
          startDate: "<A>T00:00", endDate: "<A>T23:59", ...)
runReport(metricIds: "<metricId>", dimensionId: "variables/page",
          startDate: "<A>T00:00", endDate: "<A>T23:59", ...)
在HTML报告中按维度将结果分组为单独的表格。

Phase 6 — Generate HTML Report

阶段6 — 生成HTML报告

Build the movers report inline and write to
/tmp/aa_top_movers_report_<YYYY-MM-DD_HHMMSS>.html
.
在线构建异动报告,并写入
/tmp/aa_top_movers_report_<YYYY-MM-DD_HHMMSS>.html

Rendering rules — apply consistently across runs

渲染规则 — 运行间保持一致

Two runs of this skill on the same report suite + metric/dimension + period must render identically (modulo the generation timestamp). The rules below pin the formatting choices that the AI would otherwise drift on.
同一报表套件、指标/维度、时间段下,此技能的两次运行必须生成完全相同的报告(仅生成时间戳可不同)。以下规则固定了AI可能会随意变动的格式选择。

Number formatting

数字格式

  • KPI values (the big number in each summary tile, and per-row mover values) — use full digits with thousands separators (
    8,160
    ,
    77,584
    ,
    1,250,000
    ). Do NOT use SI suffixes like
    K
    or
    M
    , even for large values. Stakeholders want exact numbers, not abbreviations.
  • Percent change (in pills and narrative bullets) — always one decimal place, rounded half-away-from-zero. For example,
    −23.55%
    displays as
    −23.6%
    , never
    −23.5%
    . Compute on full-precision values; round only at display time.
  • Percentage-point change (for already-percentage metrics like Conversion Rate or Bounce Rate) — same rounding, suffix
    pp
    . Example:
    +0.40 pp
    .
  • Currency
    $
    prefix with thousands separators and no decimals for values ≥ $100 (
    $1,240,000
    ); cents only when value < $100 (
    $45.20
    ).
  • KPI值(每个汇总卡片中的大数字,以及每行异动项的值) — 使用带千位分隔符的完整数字(
    8,160
    77,584
    1,250,000
    )。即使数值很大,也不要使用
    K
    M
    等国际单位制后缀。相关人员需要精确数字,而非缩写。
  • 变化百分比(在标签和叙述性项目符号中) — 始终保留一位小数,采用四舍五入到最近整数的方式。例如,
    −23.55%
    显示为
    −23.6%
    ,绝不能显示为
    −23.5%
    。基于全精度值计算;仅在显示时进行四舍五入。
  • 百分点变化(针对转化率或跳出率等本身为百分比的指标) — 采用相同的四舍五入规则,后缀为
    pp
    。示例:
    +0.40 pp
  • 货币 — 以
    $
    为前缀,带千位分隔符;数值≥$100时不保留小数(
    $1,240,000
    );数值<$100时保留分位(
    $45.20
    )。

Null / missing data handling

空值/缺失数据处理

A KPI tile or mover row must reflect what the report suite actually returned. The AI must not silently substitute a different metric or hide a tile to make the report look cleaner.
  • Both periods return 0 or NULL for the tracked metric in a summary tile: render the tile with
    kpi-value
    =
    Data unavailable
    , pill class
    flat
    , pill text
    ⚠ N/A
    , and
    prior
    text =
    Both periods returned no data — validate instrumentation
    . The tile stays in the grid; do not omit it.
  • One period returns valid data, the other 0 / NULL: render the tile with the valid value as
    kpi-value
    , pill class
    flat
    , pill text
    ⚠ N/A
    , and
    prior
    text =
    Prior {period_noun}: no data
    .
  • Never substitute a different metric (e.g., switching from Revenue to Orders because Revenue came back $0). The metric being analyzed MUST be the metric rendered.
KPI卡片或异动项行必须如实反映报表套件返回的数据。AI不得擅自替换为其他指标或隐藏卡片来美化报告。
  • 汇总卡片中两个时间段的跟踪指标均返回0或NULL:卡片的
    kpi-value
    显示为
    数据不可用
    ,标签样式为
    flat
    ,标签文本为
    ⚠ N/A
    prior
    文本为
    两个时间段均无数据 — 请验证数据采集
    。卡片保留在网格中;不得省略。
  • 一个时间段返回有效数据,另一个返回0/NULL:卡片的
    kpi-value
    显示有效数据,标签样式为
    flat
    ,标签文本为
    ⚠ N/A
    prior
    文本为
    前期{时间段名词}:无数据
  • 绝不能替换为其他指标(例如,因收入返回$0而切换为订单量)。分析的指标必须与报告中显示的指标一致。

HTML template

HTML模板

html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Top Movers &mdash; {ORG_NAME} &mdash; {METRIC_NAME}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700;900&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
  * { box-sizing: border-box; margin: 0; padding: 0; }
  :root {
    --bg: #f5f4f1;
    --surface: #ffffff;
    --ink: #1a1a1a;
    --ink-muted: #6b6b6b;
    --border: #e5e2dc;
    --header-bg: #0e0e10;
    --header-warm: #3a1010;
    --accent-red: #c8312f;
    --accent-red-bright: #ff6b68;
    --accent-red-soft: #fdecea;
    --accent-green: #1f7a4d;
    --accent-yellow: #d4a017;
  }
  body { font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
         background: var(--bg); color: var(--ink); line-height: 1.5;
         -webkit-font-smoothing: antialiased; }

  /* === Header === */
  header { background: linear-gradient(120deg, var(--header-bg) 0%, #1a0d0d 55%, var(--header-warm) 100%);
           color: #fff; padding: 56px 56px 44px; position: relative; overflow: hidden; }
  header::after { content: ""; position: absolute; right: -140px; top: -140px;
                  width: 460px; height: 460px;
                  background: radial-gradient(circle, rgba(200,49,47,.35) 0%, transparent 70%);
                  pointer-events: none; }
  .header-inner { max-width: 1080px; margin: 0 auto; position: relative; z-index: 1; }
  .eyebrow { display: inline-flex; align-items: center; gap: 8px;
             padding: 6px 14px; border: 1px solid rgba(255,107,104,.55);
             border-radius: 999px; color: var(--accent-red-bright);
             font-size: 11px; font-weight: 600; letter-spacing: 1.2px;
             text-transform: uppercase; margin-bottom: 24px;
             background: rgba(200,49,47,.10); }
  .eyebrow::before { content: ""; width: 6px; height: 6px;
                     background: var(--accent-red-bright); border-radius: 50%; }
  header h1 { font-family: "Playfair Display", Georgia, serif;
              font-size: 56px; font-weight: 700; letter-spacing: -1.5px;
              line-height: 1.05; margin-bottom: 14px; color: #fff; }
  header .lede { font-size: 16px; max-width: 600px;
                 color: rgba(255,255,255,.80); margin-bottom: 24px;
                 line-height: 1.55; }
  header .meta { display: flex; flex-wrap: wrap; gap: 22px;
                 font-size: 13px; color: rgba(255,255,255,.60); }
  header .meta span { display: inline-flex; align-items: center; gap: 6px; }
  header .meta .icon { opacity: .8; }

  /* === Tabs === */
  nav { background: var(--surface); border-bottom: 1px solid var(--border);
        padding: 0 56px; display: flex; gap: 28px;
        position: sticky; top: 0; z-index: 50; }
  nav a { display: block; padding: 16px 0; font-size: 14px;
          color: var(--ink); text-decoration: none;
          border-bottom: 2px solid transparent;
          transition: border-color .15s ease; }
  nav a:hover { border-bottom-color: var(--accent-red); }

  /* === Container === */
  .container { max-width: 1080px; margin: 0 auto; padding: 36px 56px 60px; }

  /* === Period strip === */
  .period-bar { background: var(--surface); border-radius: 8px;
                padding: 14px 22px; margin-bottom: 28px;
                font-size: 13px; color: var(--ink-muted);
                box-shadow: 0 1px 3px rgba(0,0,0,.04);
                border-left: 3px solid var(--accent-red); }
  .period-bar strong { color: var(--ink); font-weight: 600; }

  /* === Dimension section === */
  .dim-section { margin-top: 32px; }
  .dim-section > h2 { font-family: "Playfair Display", Georgia, serif;
                      font-size: 22px; font-weight: 700; color: var(--ink);
                      margin-bottom: 16px; padding-bottom: 10px;
                      border-bottom: 1px solid var(--border); }

  /* === Gainers / Losers dual panels === */
  .movers-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 22px;
                 margin-bottom: 32px; }
  .movers-panel { background: var(--surface); border-radius: 8px;
                  box-shadow: 0 1px 3px rgba(0,0,0,.05); overflow: hidden; }
  .movers-panel-header { padding: 18px 24px; border-bottom: 1px solid var(--border); }
  .movers-panel-header.gainers { border-top: 3px solid var(--accent-green); }
  .movers-panel-header.losers  { border-top: 3px solid var(--accent-red); }
  .movers-panel-header h2 { font-family: "Playfair Display", Georgia, serif;
                            font-size: 17px; font-weight: 700; }
  .movers-panel-header.gainers h2 { color: var(--accent-green); }
  .movers-panel-header.losers  h2 { color: var(--accent-red); }

  /* === Tables === */
  table { width: 100%; border-collapse: collapse; font-size: 13px; }
  thead th { background: #faf8f4; padding: 10px 18px;
             text-align: left; font-weight: 600;
             text-transform: uppercase; letter-spacing: .6px;
             font-size: 11px; color: var(--ink-muted);
             border-bottom: 1px solid var(--border); }
  tbody td { padding: 10px 18px; border-bottom: 1px solid #f4f1eb; }
  tbody tr:last-child td { border-bottom: none; }
  .delta-pos { color: var(--accent-green); font-weight: 700; }
  .delta-neg { color: var(--accent-red); font-weight: 700; }

  /* === Rank badges (circular) === */
  .rank-badge { display: inline-flex; align-items: center; justify-content: center;
                width: 24px; height: 24px;
                border-radius: 50%;
                background: var(--accent-red); color: #fff;
                font-size: 11px; font-weight: 700;
                margin-right: 8px; vertical-align: middle; }
  .rank-badge.gain { background: var(--accent-green); }

  .back-top { position: fixed; bottom: 24px; right: 24px; background: var(--accent-red);
              color: #fff; width: 44px; height: 44px; border-radius: 50%; border: none;
              font-size: 20px; cursor: pointer;
              box-shadow: 0 4px 12px rgba(200,49,47,.35); }
  footer { text-align: center; padding: 32px 24px;
           font-size: 12px; color: var(--ink-muted); }
  @media (max-width: 700px) { .movers-grid { grid-template-columns: 1fr; } }

  /* === Print === */
  @media print {
    nav { display: none; position: static; }
    header { padding: 36px 32px 28px; }
    header h1 { font-size: 42px; }
    .back-top { display: none; }
    .movers-grid { page-break-inside: avoid; }
    .movers-panel, .period-bar {
      box-shadow: none; border: 1px solid var(--border);
    }
  }
</style>
</head>
<body>
<header>
  <div class="header-inner">
    <div class="eyebrow">Top Movers Watchlist</div>
    <h1>{ORG_NAME} Top Movers</h1>
    <p class="lede">Biggest gainers and decliners for {METRIC_NAME} between {PERIOD_A} and {PERIOD_B}.</p>
    <div class="meta">
      <span><span class="icon">&#128197;</span> {PERIOD_A} vs {PERIOD_B}</span>
      <span><span class="icon">&#128202;</span> {REPORT_SUITE}</span>
      <span><span class="icon">&#128340;</span> Prepared {GENERATED_DATE}</span>
    </div>
  </div>
</header>
<nav>
  <a href="#summary">Summary</a>
  <!-- one nav link per dimension -->
</nav>
<div class="container">
  <div id="summary" class="period-bar">
    <strong>Metric:</strong> {METRIC_NAME} &nbsp;|&nbsp;
    <strong>Period A:</strong> {PERIOD_A} &nbsp;|&nbsp;
    <strong>Period B:</strong> {PERIOD_B} &nbsp;|&nbsp;
    <strong>Materiality:</strong> &ge;{THRESHOLD} {METRIC_UNIT}
  </div>
  <!-- One .dim-section per dimension -->
  <div id="dim-{DIM_ID}" class="dim-section">
    <h2>{DIMENSION_NAME}</h2>
    <div class="movers-grid">
      <div class="movers-panel">
        <div class="movers-panel-header gainers"><h2>&#9650; Top Gainers</h2></div>
        <table>
          <thead><tr>
            <th>#</th><th>Item</th><th>Period A</th><th>Period B</th>
            <th>Delta</th><th>% Change</th>
          </tr></thead>
          <tbody><!-- gainer rows: use <span class="rank-badge gain">N</span> --></tbody>
        </table>
      </div>
      <div class="movers-panel">
        <div class="movers-panel-header losers"><h2>&#9660; Top Losers</h2></div>
        <table>
          <thead><tr>
            <th>#</th><th>Item</th><th>Period A</th><th>Period B</th>
            <th>Delta</th><th>% Change</th>
          </tr></thead>
          <tbody><!-- loser rows: use <span class="rank-badge">N</span> --></tbody>
        </table>
      </div>
    </div>
  </div>
</div>
<button class="back-top" onclick="window.scrollTo({top:0,behavior:'smooth'})">
  &#8679;
</button>
<footer>Top Movers Watchlist &mdash; {ORG_NAME} &mdash; Generated {GENERATED_DATE}</footer>
</body>
</html>
Section titles — no phase prefix: Section headings in the HTML report must not include the phase number. Use the plain section name only (e.g., "Top Movers" not "Phase 3 — Top Movers", "Dimension Breakdown" not "Phase 4 — Dimension Breakdown").
Write to
/tmp/aa_top_movers_report_<YYYY-MM-DD_HHMMSS>.html
and open:
bash
open /tmp/aa_top_movers_report_<YYYY-MM-DD_HHMMSS>.html

html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Top Movers &mdash; {ORG_NAME} &mdash; {METRIC_NAME}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700;900&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
  * { box-sizing: border-box; margin: 0; padding: 0; }
  :root {
    --bg: #f5f4f1;
    --surface: #ffffff;
    --ink: #1a1a1a;
    --ink-muted: #6b6b6b;
    --border: #e5e2dc;
    --header-bg: #0e0e10;
    --header-warm: #3a1010;
    --accent-red: #c8312f;
    --accent-red-bright: #ff6b68;
    --accent-red-soft: #fdecea;
    --accent-green: #1f7a4d;
    --accent-yellow: #d4a017;
  }
  body { font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
         background: var(--bg); color: var(--ink); line-height: 1.5;
         -webkit-font-smoothing: antialiased; }

  /* === Header === */
  header { background: linear-gradient(120deg, var(--header-bg) 0%, #1a0d0d 55%, var(--header-warm) 100%);
           color: #fff; padding: 56px 56px 44px; position: relative; overflow: hidden; }
  header::after { content: ""; position: absolute; right: -140px; top: -140px;
                  width: 460px; height: 460px;
                  background: radial-gradient(circle, rgba(200,49,47,.35) 0%, transparent 70%);
                  pointer-events: none; }
  .header-inner { max-width: 1080px; margin: 0 auto; position: relative; z-index: 1; }
  .eyebrow { display: inline-flex; align-items: center; gap: 8px;
             padding: 6px 14px; border: 1px solid rgba(255,107,104,.55);
             border-radius: 999px; color: var(--accent-red-bright);
             font-size: 11px; font-weight: 600; letter-spacing: 1.2px;
             text-transform: uppercase; margin-bottom: 24px;
             background: rgba(200,49,47,.10); }
  .eyebrow::before { content: ""; width: 6px; height: 6px;
                     background: var(--accent-red-bright); border-radius: 50%; }
  header h1 { font-family: "Playfair Display", Georgia, serif;
              font-size: 56px; font-weight: 700; letter-spacing: -1.5px;
              line-height: 1.05; margin-bottom: 14px; color: #fff; }
  header .lede { font-size: 16px; max-width: 600px;
                 color: rgba(255,255,255,.80); margin-bottom: 24px;
                 line-height: 1.55; }
  header .meta { display: flex; flex-wrap: wrap; gap: 22px;
                 font-size: 13px; color: rgba(255,255,255,.60); }
  header .meta span { display: inline-flex; align-items: center; gap: 6px; }
  header .meta .icon { opacity: .8; }

  /* === Tabs === */
  nav { background: var(--surface); border-bottom: 1px solid var(--border);
        padding: 0 56px; display: flex; gap: 28px;
        position: sticky; top: 0; z-index: 50; }
  nav a { display: block; padding: 16px 0; font-size: 14px;
          color: var(--ink); text-decoration: none;
          border-bottom: 2px solid transparent;
          transition: border-color .15s ease; }
  nav a:hover { border-bottom-color: var(--accent-red); }

  /* === Container === */
  .container { max-width: 1080px; margin: 0 auto; padding: 36px 56px 60px; }

  /* === Period strip === */
  .period-bar { background: var(--surface); border-radius: 8px;
                padding: 14px 22px; margin-bottom: 28px;
                font-size: 13px; color: var(--ink-muted);
                box-shadow: 0 1px 3px rgba(0,0,0,.04);
                border-left: 3px solid var(--accent-red); }
  .period-bar strong { color: var(--ink); font-weight: 600; }

  /* === Dimension section === */
  .dim-section { margin-top: 32px; }
  .dim-section > h2 { font-family: "Playfair Display", Georgia, serif;
                      font-size: 22px; font-weight: 700; color: var(--ink);
                      margin-bottom: 16px; padding-bottom: 10px;
                      border-bottom: 1px solid var(--border); }

  /* === Gainers / Losers dual panels === */
  .movers-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 22px;
                 margin-bottom: 32px; }
  .movers-panel { background: var(--surface); border-radius: 8px;
                  box-shadow: 0 1px 3px rgba(0,0,0,.05); overflow: hidden; }
  .movers-panel-header { padding: 18px 24px; border-bottom: 1px solid var(--border); }
  .movers-panel-header.gainers { border-top: 3px solid var(--accent-green); }
  .movers-panel-header.losers  { border-top: 3px solid var(--accent-red); }
  .movers-panel-header h2 { font-family: "Playfair Display", Georgia, serif;
                            font-size: 17px; font-weight: 700; }
  .movers-panel-header.gainers h2 { color: var(--accent-green); }
  .movers-panel-header.losers  h2 { color: var(--accent-red); }

  /* === Tables === */
  table { width: 100%; border-collapse: collapse; font-size: 13px; }
  thead th { background: #faf8f4; padding: 10px 18px;
             text-align: left; font-weight: 600;
             text-transform: uppercase; letter-spacing: .6px;
             font-size: 11px; color: var(--ink-muted);
             border-bottom: 1px solid var(--border); }
  tbody td { padding: 10px 18px; border-bottom: 1px solid #f4f1eb; }
  tbody tr:last-child td { border-bottom: none; }
  .delta-pos { color: var(--accent-green); font-weight: 700; }
  .delta-neg { color: var(--accent-red); font-weight: 700; }

  /* === Rank badges (circular) === */
  .rank-badge { display: inline-flex; align-items: center; justify-content: center;
                width: 24px; height: 24px;
                border-radius: 50%;
                background: var(--accent-red); color: #fff;
                font-size: 11px; font-weight: 700;
                margin-right: 8px; vertical-align: middle; }
  .rank-badge.gain { background: var(--accent-green); }

  .back-top { position: fixed; bottom: 24px; right: 24px; background: var(--accent-red);
              color: #fff; width: 44px; height: 44px; border-radius: 50%; border: none;
              font-size: 20px; cursor: pointer;
              box-shadow: 0 4px 12px rgba(200,49,47,.35); }
  footer { text-align: center; padding: 32px 24px;
           font-size: 12px; color: var(--ink-muted); }
  @media (max-width: 700px) { .movers-grid { grid-template-columns: 1fr; } }

  /* === Print === */
  @media print {
    nav { display: none; position: static; }
    header { padding: 36px 32px 28px; }
    header h1 { font-size: 42px; }
    .back-top { display: none; }
    .movers-grid { page-break-inside: avoid; }
    .movers-panel, .period-bar {
      box-shadow: none; border: 1px solid var(--border);
    }
  }
</style>
</head>
<body>
<header>
  <div class="header-inner">
    <div class="eyebrow">Top Movers Watchlist</div>
    <h1>{ORG_NAME} Top Movers</h1>
    <p class="lede">Biggest gainers and decliners for {METRIC_NAME} between {PERIOD_A} and {PERIOD_B}.</p>
    <div class="meta">
      <span><span class="icon">&#128197;</span> {PERIOD_A} vs {PERIOD_B}</span>
      <span><span class="icon">&#128202;</span> {REPORT_SUITE}</span>
      <span><span class="icon">&#128340;</span> Prepared {GENERATED_DATE}</span>
    </div>
  </div>
</header>
<nav>
  <a href="#summary">Summary</a>
  <!-- one nav link per dimension -->
</nav>
<div class="container">
  <div id="summary" class="period-bar">
    <strong>Metric:</strong> {METRIC_NAME} &nbsp;|&nbsp;
    <strong>Period A:</strong> {PERIOD_A} &nbsp;|&nbsp;
    <strong>Period B:</strong> {PERIOD_B} &nbsp;|&nbsp;
    <strong>Materiality:</strong> &ge;{THRESHOLD} {METRIC_UNIT}
  </div>
  <!-- One .dim-section per dimension -->
  <div id="dim-{DIM_ID}" class="dim-section">
    <h2>{DIMENSION_NAME}</h2>
    <div class="movers-grid">
      <div class="movers-panel">
        <div class="movers-panel-header gainers"><h2>&#9650; Top Gainers</h2></div>
        <table>
          <thead><tr>
            <th>#</th><th>Item</th><th>Period A</th><th>Period B</th>
            <th>Delta</th><th>% Change</th>
          </tr></thead>
          <tbody><!-- gainer rows: use <span class="rank-badge gain">N</span> --></tbody>
        </table>
      </div>
      <div class="movers-panel">
        <div class="movers-panel-header losers"><h2>&#9660; Top Losers</h2></div>
        <table>
          <thead><tr>
            <th>#</th><th>Item</th><th>Period A</th><th>Period B</th>
            <th>Delta</th><th>% Change</th>
          </tr></thead>
          <tbody><!-- loser rows: use <span class="rank-badge">N</span> --></tbody>
        </table>
      </div>
    </div>
  </div>
</div>
<button class="back-top" onclick="window.scrollTo({top:0,behavior:'smooth'})">
  &#8679;
</button>
<footer>Top Movers Watchlist &mdash; {ORG_NAME} &mdash; Generated {GENERATED_DATE}</footer>
</body>
</html>
章节标题 — 无阶段前缀:HTML报告中的章节标题不得包含阶段编号。仅使用纯章节名称(例如,“异动指标Top榜单”而非“阶段3 — 异动指标Top榜单”,“维度拆分”而非“阶段4 — 维度拆分”)。
写入
/tmp/aa_top_movers_report_<YYYY-MM-DD_HHMMSS>.html
并打开:
bash
open /tmp/aa_top_movers_report_<YYYY-MM-DD_HHMMSS>.html

Inline Summary (Always Deliver)

在线摘要(必须提供)

Always follow the HTML report with a brief text callout:
Top Movers: [Metric] — [Period A] vs [Period B]

GAINERS (by [Dimension])
1. [Item A]  +12,400 visits  (+34%)
2. [Item B]  +8,200 visits   (+21%)
3. [Item C]  +5,100 visits   (+18%)

LOSERS
1. [Item X]  -9,800 visits   (-41%)
2. [Item Y]  -4,200 visits   (-18%)
3. [Item Z]  -2,900 visits   (-12%)

Key takeaway: [1 sentence explanation of the story]

HTML报告后必须附带简短的文本摘要:
异动指标Top榜单: [指标] — [时间段A] vs [时间段B]

涨幅项(按[维度])
1. [项A]  +12,400访问量  (+34%)
2. [项B]  +8,200访问量   (+21%)
3. [项C]  +5,100访问量   (+18%)

跌幅项
1. [项X]  -9,800访问量   (-41%)
2. [项Y]  -4,200访问量   (-18%)
3. [项Z]  -2,900访问量   (-12%)

关键结论: [1句话解释核心发现]

Guardrails

约束规则

  • Always apply a materiality threshold to suppress noise from items with negligible volume.
  • Flag new items (no prior period) and disappeared items (no current period) explicitly — they often tell a significant story.
  • For percent-change ranking, use absolute delta (not percent) as primary sort to avoid surfacing low-volume items with misleading large percentages.
  • Cap dimension list at 6 for call efficiency. Offer to run additional dimensions on request.

  • 始终应用重要性阈值,以抑制低流量项带来的噪音。
  • 明确标记新增项(无前期数据)和消失项(无当前数据) — 这些项往往能反映重要情况。
  • 对于变化百分比排名,以绝对差值(而非百分比)作为主要排序依据,避免因低流量项的误导性高百分比而使其被突出显示。
  • 为提高调用效率,维度列表最多保留6个。可根据用户请求运行额外维度。

Example Interaction

示例交互

"What pages were up and down the most last week?"
  1. Confirm report suite.
  2. Confirm metric: page views. Dimension: page name. Period: last 7 days vs prior 7 days. Limit: top 10 each.
  3. Run 2 reports for page × visits.
  4. Compute deltas, apply materiality (min 500 page views in either period).
  5. Generate HTML report and deliver inline summary.
  6. "Top gainer: /products/new-feature with +18,200 page views (+312%). Top loser: /promo/expired-campaign with -9,400 page views (-88%)."
“上周哪些页面的流量涨幅和跌幅最大?”
  1. 确认报表套件。
  2. 确认指标:页面浏览量。维度:页面名称。时间段:最近7天 vs 之前7天。限制:各Top 10。
  3. 运行2次页面×访问量的报表查询。
  4. 计算差值,应用重要性阈值(任一时间段页面浏览量≥500)。
  5. 生成HTML报告并提供在线摘要。
  6. “Top涨幅项:/products/new-feature,页面浏览量增加18,200次(+312%)。Top跌幅项:/promo/expired-campaign,页面浏览量减少9,400次(-88%)。”