Skip to content

docs(marketing): name both Add to Campaign paths on campaign-members - #1855

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-1845-enrollment-paths-docs
Sep 10, 2026
Merged

os-steve merged 1 commit into
mainfrom
claude/issue-1845-enrollment-paths-docs

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes #1845

Item 2 of "How members get enrolled" on content/docs/marketing/campaign-members.mdx (and both Chinese siblings, same line) said this:

From a lead — open a lead (or tick several in a lead list view) and run Add to Campaign … This is the only picker-driven path that ships: there is no Add Members button on the campaign that picks people from lead and contact list views.

Both halves in front of the colon were false, and neither had gone stale — they were false the day #961 wrote them, because #597 had already shipped the contact-side mirror.

What each new claim is sourced from

claim source
Add to Campaign ships on a lead list and a contact list create_campaign (src/actions/lead.actions.ts:141-143) and add_contact_to_campaign (src/actions/contact.actions.ts:70-72) carry the same label: 'Add to Campaign', wired bulkActions: ['create_campaign'] at src/views/lead.view.ts:204 and bulkActions: ['add_contact_to_campaign'] at src/views/contact.view.ts:36
selection toolbar or a single row locations: ['list_toolbar', 'list_item'] on both actions; the bare-string bulkActions form fans the action out once per selected row with that row's recordId (contact.actions.ts:50-62), and list_item is the per-row entry point. "selection toolbar" is the wording sales/leads.mdx:161 already uses for this same action; the ⋮ row menu is sales/meetings-and-calls.mdx:31's
a record picker, not a paste-the-ID box the campaign param is field-backed — field: 'crm_campaign' + objectOverride: 'crm_campaign_member' — which is what makes the console resolve the widget from the lookup (both actions, same param block)
opening a lead or a contact does not offer it grep -rn "add_contact_to_campaign|create_campaign" src/pages/ returns nothing (exit 1). Both actions are list_toolbar + list_item only
the dedupe difference verified in both bodies: the lead action reads fields: ['crm_lead'] and builds its skip set from r.crm_lead; the contact action reads fields: ['crm_contact'] and skips on r.crm_contact. The contact action's header states it as deliberate — "the dedupe read scopes on crm_contact, so a person enrolled as a LEAD and again as a CONTACT is not treated as a duplicate"
the half that stays still true: nothing in src/ declares an Add Members action, and grep -rn "add_member|Add Members" src/ returns nothing

content/docs/marketing/campaigns.mdx:76 already carried the truthful version — "the Add to Campaign action on any lead/contact" — so the two marketing pages agreed on nothing before this and agree now. (The card cites that line as :74; #1846 moved it two lines down.)

zh-Hans names the action as the zh-CN pack does — 加入营销活动, which is the label both actions ship (src/translations/zh-CN/objects.pipeline.ts:192 for the lead, objects.customer.ts:198 for the contact) — glossed after the English label, the pattern this page already uses for Related(相关)and New(新建). zh-Hant mirrors it in that page's own Traditional conventions (加入行銷活動, as sales/leads.zh-Hant.mdx:149 writes it), never a mechanical conversion.

Docs guards that read this page

Which guards read the page was measured, not guessed: the whole suite was re-run through a scratchpad-only vitest config that aliases node:fs to a facade recording every read of content/docs/marketing/campaign-members*.mdx, attributed per test file. Nothing in the repo was touched by it (the alias breaks 7 unrelated fs-tooling suites, which is why it is a probe and not a commit). 15 guards read the page:

guard why it stays green
docs-anchor-links anchored links must resolve to a real heading. This change adds no link and no heading; the page's one self-link (#how-the-metrics-roll-up) is untouched
docs-locale-callouts callout parity across locales, held at 1/1/1 after #1846. No > block is added or removed on any of the three
docs-object-coverage its ledger row wants the page to exist in three locales, sit in each meta*.json, and name the object — all unchanged (it reads the English page and stats the siblings)
docs-object-term-consistency pins the crm_case / crm_quote terms across Chinese pages. Neither object is named here, and no retired spelling is introduced
docs-conversion-rate-spelling sweeps the Chinese pages for 转换率/轉換率. The new Chinese text contains neither
docs-zh-hant-justification the zh-Hant convention note must sit on exactly the declared pages and state the sanctioned reason. This page carries none, none is added, and no retired reason is asserted
docs-setup-navigation-names parses bold **App → child** paths. The new prose has no arrow path at all
docs-view-rosters keys on pages carrying a list-view roster table; campaign-members has none and gains none
docs-metadata-counts every metadata count a page states must equal what the stack registers. No count is stated
docs-drift its two families are flow-vs-source prose and the large-deal CEL threshold. Neither is named
docs-retired-personas no retired copilot persona is named
docs-role-hierarchy no role name is named
docs-runnable-samples no code fence added
docs-analytics-vocabulary no dataset / cube / report vocabulary added
docs-dashboard-tiles no tile name added

Two more touch the page without reading its body: docs-app-workflow-paths (path globs only) and scripts/check-source-hygiene.mjs (control-byte scan over content/, green). scripts/check-source-token-ratchet.mjs measures src/**/*.ts only, so a docs edit is outside its surface — the readings are byte-identical before and after (business semantics ~85,285 · interaction layer ~38,335 · authored total ~138,265).

Verification

pnpm verify run stage by stage on the committed tree, each exit code captured before any pipe (cmd file 2 1; EXIT=$?):

stage exit evidence
pnpm validate 0 pre-existing advisories only (carrier-only field warnings)
pnpm typecheck 0
pnpm lint 0 4 warnings, 12 suggestions — all pre-existing
pnpm lint:i18n-gate 0 ✓ i18n lint gate: 0 i18n/missing-* issues
pnpm hygiene 0 ✓ source hygiene clean
pnpm hygiene:tokens 0 ✓ source token ratchet clean, readings unchanged
pnpm build 0 Artifact: dist/objectstack.json (1916.9 KB)
pnpm test 0 165 files, 3455 passed / 1 skipped (3456)

Test-file and test counts before the change on the same worktree: 165 files, 3455 passed / 1 skipped (3456) — identical. No test file is added or removed (#1579's fence: nothing under test/** is touched).

The changeset is written, not skipped: this ships to users as product documentation.

Scope

Three docs files plus the changeset. ⛔ No metadata change, ⛔ nothing under test/**, ⛔ no content/docs/releases/ edit, and the sections #1846 rewrote are not re-opened — the diff is one line per locale.

Out of scope — filed, not fixed here

🤖 Generated with Claude Code

https://claude.ai/code/session_01DuzfS5chho38Yx1jxx9DEj


Generated by Claude Code

Item 2 of "How members get enrolled" called the lead-side action "the only
picker-driven path that ships", and told the reader to open a lead and run it.
`add_contact_to_campaign` has shipped the contact-side mirror since #597 — same
label, same locations, same field-backed campaign param — and neither action is
registered on a record page: both are `list_toolbar` + `list_item` only, so the
gesture the sentence described is not one the console offers.

Rewritten in all three locales: both entry points and the surface each lives on
(a lead list or a contact list, from the selection toolbar or a single row's ⋮
menu), the dedupe difference (each action scopes its skip-check on its own
relationship, so a person enrolled once as a lead and once as a contact is two
memberships by design), and the half that is still true — there is no *Add
Members* picker on the campaign that reaches lead and contact list views.

zh-Hans names the action as the zh-CN pack does (加入营销活动 — the one label
both actions ship); zh-Hant mirrors it in that page's own Traditional
conventions (加入行銷活動).

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:56am 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 7c09fb8 Sep 10, 2026
10 checks passed

Copy link
Copy Markdown
Collaborator Author

PM review → accepted, auto-merge armed.

Three files, one line each, nothing else in them touched. Exactly the scope.

⭐ You replaced reasoning with an instrument, and that is the advance

Every docs PR in this series has answered "which guards read this page?" by grepping and arguing. You measured it: re-ran the suite under a scratchpad-only vitest config aliasing node:fs to a facade that records every read of campaign-members*.mdx, attributed per test file. Answer: 15 guards read the page, plus two that touch it without reading its body.

That is a strictly better answer than the four or five each prior PR reasoned its way to, and it converts the load-bearing claim from "I could not think of a guard that pins this" into a read-set. ⭐ Keeping it as a probe — nothing in the repo touched, and stating plainly that the alias breaks 7 unrelated fs-tooling suites so it could never be committed — is the judgement that makes it usable rather than a mess. ⛔ It stays a probe; it is not a thing to land.

On the missing ablation leg: accepted, with the boundary stated. Your reason is right for the leg the prior PRs ran (a prose-only diff adds no assertion to mutate), and the fs probe answers the harder question — which guards could possibly have caught me — rather than the narrower one. ⚠️ It is not a general licence: a docs change that adds or removes something a guard counts (a callout, an anchor, a bold nav path) still owes the parity ablation, as PR #1852's carrier leg showed.

Both premises re-verified, and one citation corrected

You re-derived the two claims in the bodies rather than taking my comment's word, which is the standing rule working in the right direction. ⭐ And the dedupe clause was verified before being written, not inferred from the card: the lead action reads fields: ['crm_lead'] and skips on r.crm_lead, the contact action reads ['crm_contact'] and skips on r.crm_contact — so one person enrolled as a lead and again as a contact really is two memberships by construction, not by accident.

⭐ The citation correction matters more than it looks: the truthful sentence on campaigns.mdx is at :76, not :74 as the card says — PR #1846 moved it two lines down between the filing and the fix. A card's line numbers are stale from the moment they are written, and re-reading rather than trusting them is what kept the two marketing pages agreeing with each other instead of a third phrasing appearing.

The Chinese half is sourced, not translated. 加入营销活动 is the label both actions actually ship (objects.pipeline.ts:192, objects.customer.ts:198), glossed after the English in the pattern the page already uses for Related(相关)and New(新建); zh-Hant follows sales/leads.zh-Hant.mdx:149. That is documentation rule 6 done properly — the pack decides, not the translator.

#1854 is claimed and dispatched — and it is the sharpest one yet

I verified it myself before dispatching, and the metadata is unambiguous:

  • campaign_enrollment is type: 'screen' (campaign-enrollment.flow.ts:52), entered only through the console's flow-action trigger seeding recordId;
  • its own docblock at :13-15 records that it used to be a schedule flow on a Monday 9 AM cron, and why that was removed — "a cron firing seeds NO inputs";
  • the only entry point is the enroll_leads action, labelled Enroll Members, type: 'flow' on crm_campaign.

⇒ "when a campaign moves to In Progress, the Campaign Enrollment flow runs and adds everyone matching the audience criteria" is the fourth false-automation claim this shift, after marketing/index:35, the Engagement tracking section and the admin tip. Same shape every time: the page promises something runs by itself, and the truth is a human clicking a button and filling a screen. ⭐ That pattern is now the first thing the next docs dispatch looks for.

Auto-merge armed (squash). #1845 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.

campaign-members calls the lead-side Add to Campaign "the only picker-driven path that ships" — #597 shipped the contact-side mirror too

2 participants