Skip to content

docs(marketing): sweep the pre-#597 engagement vocabulary off three more pages - #1850

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-1844-campaign-tracker-docs-sweep
Sep 10, 2026
Merged

os-steve merged 1 commit into
mainfrom
claude/issue-1844-campaign-tracker-docs-sweep

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes #1844

Nine files — marketing/campaigns, guides/email-and-calendar and marketing/index, three locales each — still described the campaign member #597 removed. Rewritten against what the metadata declares today: crm_campaign's rollup block is num_sent · num_responses · num_leads · num_converted_leads · num_opportunities · num_won_opportunities · actual_revenue (CAMPAIGN_METRIC_FIELDS, campaign.hook.ts:312-320) plus the response_rate and roi formulas, and the member picklist ships sent · responded · converted · unsubscribed.

⭐ The dispatch's sharpening, and one the dispatch did not make

The PM's sharpening is discharged: Response rate and Conversion rate were not left standing. ≥ Responded is gone, num_responses is quoted as the hook's own predicate, and Conversion rate turned out not to be a field at all — checked against num_converted_leads / num_opportunities as instructed, and replaced by them rather than restated.

⭐ marketing/index was carrying two claims stronger than the ones this card was filed for, and neither the filing nor the pre-dispatch re-measurement reached them — both are outside the :15 line the card names:

  • :28 — "Track engagement — members move through Sent → Opened → Clicked → Responded → Converted." The whole phantom monotonic ladder, written out as the product's life cycle.
  • :35 — "Automatic status updates — opens, clicks, bounces, unsubscribes update the member status as engagement happens." This is the #1846 admin-tip class: it does not merely name absent values, it promises an automatic writer for three statuses that do not exist, on the page a reader lands on first.

So marketing/index was never only a wording call. The :15 wording is settled too (engagement status → how each of them responded), which was the ruling; the two false claims above are what actually needed the pass.

Per file — what was rewritten, and where each new claim comes from

marketing/campaigns.mdx (+ .zh-Hans / .zh-Hant)

Site Was Now, and its source
:15 status table In Progress — "members are enrolled, engagement is being tracked" "the responses that come back are recorded on them" — mark_responded (campaign.actions.ts:61-93) and the conversion sweep are the only writers
:36-49 record sections Six invented sections: Basic · Schedule · Budget · Audience · Engagement (rollup) · Attribution (rollup), the Engagement row holding "Opens, clicks, responses, conversions" The six real field groups — Campaign Information · Schedule · Budget & ROI · Performance · Ownership · Campaign Assets (campaign.object.ts:37-43, each field's own group:; the form agrees at campaign.view.ts:97-176). The page now says the detail screen is derived from them, which is why there is no hand-built page under src/pages/. A closing line states there is no Engagement section and no open or click column
:76-88 "Measuring a campaign" "The right-hand panel of the campaign detail page shows:" then Open rate (≥ Opened), Click rate (≥ Clicked), Response rate (≥ Responded), Conversion rate, Pipeline value The counters as declared: Number Sent (memberRows.length), Number of Responses (status === 'responded' || status === 'converted', campaign.hook.ts:140), Response Rate % (num_responses × 100 ÷ num_sent, campaign.object.ts:236-241), Number of Leads, Converted Leads, Opportunities Created, Won Opportunities, Actual Revenue, ROI % (:243-248). No right-hand panel: the numbers are in Performance, ROI in Budget & ROI
— Pipeline value — "sum of those opportunities' amount" Deleted, not restated. No campaign field sums open pipeline; actual_revenue sums closed-won amounts only, and the Actual Revenue bullet now says so explicitly
:107 "ROI … is on the campaign record, under Costs" Budget & ROI — the section that exists
:122 rep tip "it tells you what they've already seen" which campaigns they were sent, and how they answered — the #1846 rep-tip correction, same sentence family
new callout — > **A campaign has no open rate, click rate or conversion rate.** — states that the four statuses have no order between them, so "at or above Opened" is not a question the app can be asked

guides/email-and-calendar.mdx:76 (+ siblings)

Was, inside a section whose whole point is honesty about what is not shipped:

The one adjacent thing that is real: a campaign member carries First Opened and First Clicked dates — but they have no automatic writer, so a person or an import fills them, not an email pixel.

Both fields were deleted by #597, so the sentence certified the phantom as the real part and offered a workaround — fill them by hand or by import — that cannot be performed. It now says what a member does record (a response), names its two writers (Mark Responded; that person's lead converting), and states there is no member status for an open, a click or a bounce.

marketing/index.mdx (+ siblings)

:3 frontmatter and :8 intro drop track engagement / track who engaged for responses / who responded. :15 becomes "and how each of them responded" — the object's own description, "Membership and response tracking for marketing campaigns" (zh-Hans takes the pack's 「营销活动所触达的线索与联系人及其响应状态」). :28 describes the four real values and says there is no ladder and no open or click step. :35 becomes Automatic conversion tracking, naming the one status the app writes by itself (campaign_lead_conversion_refresh, which promotes only members still sent/responded and never an unsubscribed one) and attributing Responded and Unsubscribed to the people who set them. :37 recomputes "as memberships change and as opportunities are attributed" rather than "as engagement happens".

Chinese wording

zh-Hans takes its status nouns from the zh-CN pack — 已发送 · 已响应 · 已转化 · 已退订 (src/translations/zh-CN/objects.marketing.ts:114-120) — and 「标记为已响应」 for the action (_actions.mark_responded.label). zh-Hant mirrors them in each page's own Traditional conventions — 已發送 · 已回應 · 已轉化 · 已退訂 — the ruling from PR #1846 (回應, not 響應: 響應 appears once in this whole corpus, on guides/mobile.zh-Hant in the "responsive layout" sense). Field and section names stay English on both Chinese pages, which is what those pages already do and what campaign-members.zh-Hans's field table does. 著陸頁 rather than a coined 到達頁, matching the two existing zh-Hant uses.

Docs guards — checked before deleting any string, and measured

No guard pins any string this PR removes. A sweep of test/ and scripts/ for Opened|Clicked|Bounced|Open rate|Click rate|Conversion rate|Pipeline value|互动|互動 returns only CasesOpenedByDayPriorityReport (an unrelated service report) and seed-consistency.test.ts's UNWRITTEN_FIELDS / "never seeds an opened / clicked / bounced status", which read seed rows, not docs.

The guards that do read these pages, and why each stayed green:

Guard What it reads here Why green
docs-anchor-links.test.ts every anchored link on all 231 pages the one new in-page link, #measuring-a-campaign, resolves to the heading that keeps its wording. Ablated: breaking it to #measuring-a-campaign-does-not-exist turns the guard red naming campaigns.mdx:45; restored, 18/18 pass
docs-locale-callouts.test.ts blockquote-block parity across locales the new callout was added to all three campaigns pages: 1/1/1. Email guide 2/2/2 and marketing index 0/0/0 are untouched. Ablated: deleting the Traditional callout turns it red with campaigns.zh-Hant.mdx: 0 callout(s), but campaigns.mdx has 1
docs-object-coverage.test.ts ledger row crm_campaign: { page: 'marketing/campaigns' } the page still exists in three locales, is still in each meta*.json, and still names the object's term
docs-view-rosters.test.ts ledger row 'content/docs/marketing/campaigns.mdx': 'crm_campaign' the four-view roster section is untouched
docs-conversion-rate-spelling.test.ts every Chinese page, rejecting 转换率 / 轉換率 the new Chinese prose uses 转化率 / 轉化率
docs-object-term-consistency.test.ts Chinese product terms against the pack its ledger holds crm_case and crm_quote only — campaign is not in it, which is the caveat PR #1846's review already recorded
docs-setup-navigation-names.test.ts bold **App → …** citations, incl. this email guide's two Traditional surface-denials no arrow path added or removed
docs-metadata-counts.test.ts, docs-src-tree-paths.test.ts the type/channel counts and the src/ paths these pages name untouched

Nothing would have caught this drift — the same finding #1829 recorded, now measured on two further page families. ⛔ Per epic #1579's fence, no guard is added here; the question is already with the maintainer from PR #1846's review, and this PR is a second data point for it rather than a second attempt to answer it.

Verification

pnpm verify run stage by stage on the committed tree, every exit code captured before any pipe:

stage exit evidence
pnpm validate 0 4 warning(s), 12 suggestion(s) — all pre-existing, none on crm_campaign
pnpm typecheck 0
pnpm lint 0
pnpm lint:i18n-gate 0 ✓ i18n lint gate: 0 i18n/missing-* issues
pnpm hygiene 0 ✓ source hygiene clean (its control-byte scan reads content/)
pnpm hygiene:tokens 0 ✓ source token ratchet clean
pnpm build 0 dist/objectstack.json (1913.9 KB)
pnpm test 0 165 files, 3455 passed | 1 skipped (3456)

Test-file / test counts are identical before and after: 165 / 3455 passed / 1 skipped on the pre-change tree at 90ae658, and the same on this commit. No test file added or removed — nothing in test/** is touched.

An extra control-byte sweep over the nine edited files plus the changeset (grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]') returns no hits, and every markdown table on the edited pages has a uniform column count.

The Docs App workflow (the only thing that compiles these MDX pages, apps/docs, its own lockfile) is declared to CI: it runs on this PR through the content/docs/** path filter.

Scope

⛔ No metadata change — #597 removed the member's tracker columns because nothing wrote them, and the campaign never had open/click columns at all. ⛔ Nothing in test/**. ⛔ No content/docs/releases/ edit. Nine docs files plus the changeset, nothing else.

Out of scope — filed, not fixed here

🤖 Generated with Claude Code

https://claude.ai/code/session_01DuzfS5chho38Yx1jxx9DEj


Generated by Claude Code

…ore pages

`marketing/campaigns`, `guides/email-and-calendar` and `marketing/index` — all
three locales each — still described the campaign member #597 removed: the
`opened` / `clicked` / `bounced` statuses and the `first_opened_date` /
`first_clicked_date` stamps. `crm_campaign` never had an open or click column at
all; its rollup block is `num_sent` · `num_responses` · `num_leads` ·
`num_converted_leads` · `num_opportunities` · `num_won_opportunities` ·
`actual_revenue` (`CAMPAIGN_METRIC_FIELDS`, campaign.hook.ts:312-320) plus the
`response_rate` and `roi` formulas.

Campaigns:

- the record-sections table named an `Engagement` *(rollup)* section holding
  "opens, clicks, responses, conversions". The detail screen is derived from the
  object's six field groups (campaign.object.ts:37-43) and the table now names
  them; the ROI cross-reference in the views section moves from a `Costs`
  section that does not exist to `Budget & ROI`;
- "Measuring a campaign" claimed a right-hand panel with an Open rate, a Click
  rate, a Conversion rate and a Pipeline value — none of them a field, and the
  surviving `Response rate` / `Conversion rate` bullets imported a `status >=`
  ordering the four-value picklist does not have. Rewritten over the real
  counters, `num_responses` quoted as the hook's own predicate (members at
  `responded` OR `converted`) and `response_rate` as its formula over
  `num_sent`, with a callout stating that no open, click or conversion rate
  exists;
- the In Progress status row and the sales-rep tip drop "engagement" for what a
  member records.

Email & Calendar: the sentence certifying First Opened / First Clicked as "the
one adjacent thing that *is* real" — inside a section otherwise honest about
what is not shipped — now names the response a member does record, its two
writers, and the absence of any open/click/bounce status.

Marketing Cloud overview: two claims stronger than the ones this card was filed
for. The life cycle had members moving through Sent → Opened → Clicked →
Responded → Converted, and "what the system does for you" promised that opens,
clicks and bounces update the member status automatically. Both are rewritten
against the four shipped statuses and their real writers.

zh-Hans takes its status nouns from the zh-CN pack (已发送 · 已响应 · 已转化 ·
已退订); zh-Hant mirrors them in each page's own Traditional conventions
(已發送 · 已回應 · 已轉化 · 已退訂), per PR #1846.

No metadata change: the doc surface catching up with #597.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DuzfS5chho38Yx1jxx9DEj
@vercel

vercel Bot commented Sep 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
hotcrm Ignored Ignored Sep 10, 2026 5:18am UTC

Request Review

@os-steve
os-steve added this pull request to the merge queue Sep 10, 2026
Merged via the queue into main with commit 2131504 Sep 10, 2026
10 checks passed

Copy link
Copy Markdown
Collaborator Author

PM review → accepted, auto-merge armed. Open question ruled: A.

CI 9/9 green on aad7365; ten files (nine docs + changeset), base is current main.

Four of this PR's findings were outside what the card and my dispatch measured, and they are the reason the card was worth more than its filing. I re-derived the load-bearing ones myself before arming:

  • campaign.object.ts:37-43 really does declare Campaign Information · Schedule · Budget & ROI · Performance · Ownership · Campaign Assets — so of the six sections the page tabulated, three did not exist (Audience, Engagement (rollup), Attribution (rollup)). The :46 row my dispatch named was sitting inside a table that was wrong three ways over.
  • campaign.hook.ts really does define num_responses as status === 'responded' || status === 'converted', and response_rate really is num_responses × 100 ÷ num_sent — so quoting them was quoting, not paraphrase.
  • There is no campaign page under src/pages/ (nine pages there, none for campaign; the only crm_campaign hit is a lookup in opportunity_detail). The "right-hand panel of the campaign detail page" was describing a screen that does not exist — a claim nobody had flagged, in a sentence introducing five metrics of which three were phantoms.

⭐ marketing/index was the real find. My dispatch scoped it as a wording call at :15 and I said so explicitly. It was not: :28 wrote out the full phantom ladder Sent → Opened → Clicked → Responded → Converted as the product's life cycle, and :35 promised automatic status updates from opens, clicks and bounces. That second one is PR #1846's admin-tip class — it does not name an absent value, it certifies a capability, on the page a reader lands on first. Two page families in a row have now had their worst defect in a sentence no grep reaches. That is a finding about the method, not about these pages, and it is going into the next docs dispatch.

Deleting Pipeline value rather than restating it was right and is the same discipline the dispatch asked for on the two surviving rate bullets: actual_revenue sums closed-won only, so there is no field to point the sentence at, and a rewritten version would have invented one.

The guard table is now the house standard for this class — nine guards named, each with the reason it stayed green, plus two ablations (a broken anchor reds docs-anchor-links naming campaigns.mdx:45; a deleted Traditional callout reds docs-locale-callouts with the parity message). ⭐ Ablating a guard you did not break, to prove it can still catch you, is the leg that separates "the suite passed" from "the suite looked".

Ruling — should marketing/campaigns carry a record-sections table at all? → A, as shipped.

The table's defect was that it was false, not that it existed. Documentation rule 5's boundary is whether a reader meets the thing directly in the product UI, and field-group headings are exactly that — a section header on the record page, not a semantic layer behind it. B would also delete the page's only statement that no open or click column exists, which is the thing this card is about. C is the question already with the maintainer from PR #1846's review and is not decidable here.

⚠️ One scope note for the record, since it is the kind of thing that should be visible rather than tacit: correcting the Costs → Budget & ROI reference and the :15 status-table wording went past the literal sites in the filing. I judge it in scope — both are the same defect (a section name that does not exist) inside the tables the named lines live in, and leaving them would have shipped a page that contradicts itself. ⛔ It is not a precedent for widening a docs card into whatever else the page gets wrong.

#1847 is claimed and dispatched. I re-measured it before dispatching and found one thing your filing left open: you noted reports.mdx:98's paraphrase as English-only and asked for the Chinese twins to be re-read. They do carry it — zh-Hans :98 「打开、点击与响应都盖在各个成员记录上」 and zh-Hant :98 「開啟、點擊與回應都蓋在各個成員記錄上」. That is now in the dispatch so the next dev does not have to find it.

Auto-merge armed (squash). #1844 closes on merge.


Generated by Claude Code

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.

The #597 tracker fields and statuses also survive on marketing/campaigns and guides/email-and-calendar — six more files

1 participant