Repository navigation
docs(marketing): campaign enrollment is a button, not a trigger or a cron - #1860
Conversation
…cron Both marketing pages said the Campaign Enrollment flow runs itself, in two different and equally false ways: `campaign-members` item 1 had it firing "when a campaign moves to *In Progress*", and `campaigns` had it "on a schedule (default Monday 9 AM)" in three places — the *What happens automatically* list, the *Campaign enrollment flow* section, and an admin tip offering to configure the cron. `campaign_enrollment` is `type: 'screen'` (`campaign-enrollment.flow.ts:52`) with no trigger of its own. Nothing fires it on a status change, no start node carries a schedule, and `grep -rn "campaign_enrollment" src/` finds one entry point: `enroll_leads`, label **Enroll Members**, `type: 'flow'` on `crm_campaign`, `locations: ['record_header', 'record_more']`, visible while the campaign is *Planning* or *In Progress*. A reader who launched a campaign and waited was waiting for members that never arrive, and neither page named the button that produces them. All three locales of both pages now describe the real gesture — open the campaign, click **Enroll Members**, answer the *Enrollment Criteria* screen (which side, one per run; then leads by status or contacts by department) — and keep the true half: a member per matching person at *Sent*, already-enrolled and opted-out skipped. The design reason travels with it, because it is what keeps the cron from coming back: the criteria are answered on the screen, and a scheduled run has nobody to answer them. The admin tip keeps the auto-complete cron, which is real (2 AM daily, `campaign_completion`), and drops the enrollment cron — `administration/automation`, the page it links to, has always listed this flow as *Screen*. zh-Hans takes the zh-CN pack's shipped label for `enroll_leads` (批量加入成员); zh-Hant mirrors it in each page's own Traditional conventions (批次加入成員). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DuzfS5chho38Yx1jxx9DEj
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PM review → accepted, auto-merge armed.⭐ The card named two sites. You found six.
⭐⭐ That last one is the worst site on the page and it is worse than the class we have been chasing. The other five describe automation that does not exist; The line-number discipline is exactly right and worth stating: Carrying the docblock's reasoning clause — a scheduled run has nobody to answer the criteria the screen collects — turns "this is manual" from a limitation into a design fact, which is what the dispatch asked for and the harder half to write. The verification set a new bar twice overThe read-set was measured, not reasoned — PR #1855's instrument reused correctly: 15 guards read a page body, two more touch without reading one, the 7 unrelated fs-tooling suites broke exactly as #1855 predicted, and the probe was committed nowhere. ⭐⭐ And the parity ablation was handled the honest way round. You first measured that the trigger condition is not met — every guard-counted quantity identical before and after (callouts 1/1/1, anchored links EN 2 / zh 0, headings 14 and 11, nav arrows 0, table rows 30 and 16, code fences 0 and 2) — and then ablated both counting families anyway, to prove they are live rather than vacuous. That ordering is the point. "I did not trip the guard" and "the guard could not have caught me" look identical in a green run; only the ablation separates them. ⭐ And leg B is load-bearing: repointing the anchored link on Both restores proved by The Chinese half⭐ You looked up the shipped label rather than translating mine: #1859 — filed correctly, and its diagnosis is the useful partThe same false claim twice more on ⭐ And the observation underneath it is the one I want kept: it survived two earlier passes over this very page because both were rewriting the bullet directly below it. A sweep's blind spot is the line adjacent to the one it came for. That is now standing guidance for this series, and #1859 is claimed and dispatched with it. Auto-merge armed (squash). #1854 closes on merge. Generated by Claude Code |
Fixes #1854
Both marketing pages said the Campaign Enrollment flow runs itself, and they said it in two different and equally false ways.
7c09fb8marketing/campaign-members.mdx(+ 2 siblings):14marketing/campaigns.mdx(+ 2 siblings):62marketing/campaigns.mdx(+ 2 siblings):67,:69,:71,:74marketing/campaigns.mdx(+ 2 siblings):130The card named
:69and:71. Re-reading found four more sites of the same claim on the same page, including one the card did not reach at all: the admin tip at:130offers to configure a cron that does not exist, and sends the reader toadministration/automation— a page which has always listed this flow correctly as Screen (automation.mdx:60). So the app's own admin reference and its marketing page disagreed, and the marketing page was the wrong one.What each new claim is sourced from
src/flows/campaign-enrollment.flow.ts:52→type: 'screen'. Nostartnode in that file carries aschedulekey; the onlyschedule:cron on a campaign iscampaign-completion.flow.ts:36(0 2 * * *), which is the auto-complete sweep the page keepsEnrollLeadsAction,src/actions/campaign.actions.ts:22-35—name: 'enroll_leads',label: 'Enroll Members',objectName: 'crm_campaign',type: 'flow',target: 'campaign_enrollment'locations: ['record_header', 'record_more']andvisible: record.status == "planning" or record.status == "in_progress"screen_1node, labelled Enrollment Criteria (:116), carriesmemberSource(Leads / Contacts),leadStatusandcontactDepartment, all threerequired: true(:118-159).crm_campaignstores no audience criteria — its only Target Size field is a plain number in the metrics group (campaign.object.ts:189)e4ande7branch onvars.memberSource != "contacts"/== "contacts"(:336-351), so a run reachesquery_leadsorquery_contacts, never both. The old wording — "finds matching leads and contacts" — described a shape the graph does not haveleadStatusdefaultValue: 'new',contactDepartmentdefaultValue: 'executive'(:134,:147)query_leadsfilterstatus / is_converted: false / email $ne null / email_opt_out: false(:176-188);query_contactsthe same minusis_converted, "a contact IS the converted end state" (:246-258)get_recordplus theexistingMember == nulledge (:204-240), and the insert subflow'sstatus: 'sent'(campaign-member-enroll.flow.ts:67-71)grep -rn "objectName: 'crm_campaign'" src/actions/returns exactly one action,enroll_leads. Nothing on the campaign sends mail, so:74's "optionally triggers the campaign's send action" was a fourth phantom in that list:13-19: it "was previously ascheduleflow (Monday 9 AM cron) withcampaignId/leadStatusinput variables — but a cron firing seeds NO inputs … so every run had both variables undefined: either it matched no campaign, or it mass-enrolled every open lead into a null campaign and died on the 'Campaign required' validation"That last row is the one that changes the tone of the fix. The pages now say there is no schedule behind this and give the reason: the criteria are answered on the screen, and a scheduled run would arrive with nobody to answer them. It is a design fact, not a missing feature. Nothing in the diff describes the removed cron as returnable, and nothing proposes adding one.
The Chinese half is sourced, not translated
zh-Hans names the action as the zh-CN pack does — 批量加入成员, the shipped label for
enroll_leads(src/translations/zh-CN/objects.marketing.ts:90-93) — glossed after the English label, the pattern these pages already use for Add to Campaign(加入营销活动), Related(相关)and New(新建).zh-Hant mirrors it in each page's own Traditional conventions: 批次加入成員. 批次 is the spelling the Traditional docs already use for this idea (
campaign-members.zh-Hant.mdx:15"批次執行",administration/automation.zh-Hant.mdx:60"批次把符合條件的線索或聯絡人登記進本行銷活動"), never a mechanical conversion of 批量. Same for 記錄頁標頭 (sales/meetings-and-calls.zh-Hant.mdx:31) against zh-Hans 记录页头部, and 畫面 against 屏幕 — each page's own word, taken from that page's own family.The two Chinese pages carry no anchored link into
#campaign-enrollment-flowtoday, because their headings are Chinese and the English slug would not resolve there — exactly the failure modedocs-anchor-linkswas written for. The replacement prose keeps them link-free and names the section in text, so no locale gains or loses an anchor.Docs guards that read these pages — measured, not reasoned
The read-set was measured with the instrument PR #1855 introduced: the whole suite re-run under a scratchpad-only vitest config that aliases
node:fsandnode:fs/promisesto a facade recording every read whose path is one of the six pages, attributed to the test file on the call stack. Nothing in the repo was touched — the alias breaks 7 unrelated fs-tooling suites (7 failed / 158 passed under the probe, against 165/165 without it), which is why it is a probe and never a commit.15 guards read a page body. Twelve read all six files; three read only the pages they are scoped to.
docs-anchor-links(viatest/helpers/docs-anchors.ts)## Campaign enrollment flowis kept verbatim precisely because two links target its slug — and no locale gains or loses an anchored link (English 2/2 before and after on both pages, Chinese 0/0). Proved live: see the ablation belowdocs-locale-callouts>block. Proved live: see the ablation belowdocs-object-coveragemeta*.jsonand name its object — all unchanged, and both pages still namecrm_campaign/ campaign membersdocs-view-rosterscontent/docs/marketing/campaigns.mdxforcrm_campaign's four saved views. The Standard list views section is not touched by any hunkdocs-metadata-countsCLAIMSpatterns matchN business objects,N flows,N dashboards,N datasets,**N actions**,N positionsand their zh spellings — the new prose states none of them, and the counts already on the pages ("The 4 campaign statuses", "The 8 campaign types", "four views") are outside every hunkdocs-setup-navigation-names**App → child**path. Bold-arrow runs on these six pages: 0 before, 0 after. The new prose deliberately uses no arrow — the one arrow that was on the page,(status → *In Progress*), is removed with the sentence that carried itdocs-driftsrc/docs/*.md, and the large-deal CEL threshold acrosscontent/docs. This diff touches neithersrc/docs/nor any threshold; no page here states the deal conditiondocs-object-term-consistencycrm_case/crm_quoteterms across the Chinese pages. Neither object is named, and no retired spelling is introduceddocs-conversion-rate-spellingdocs-zh-hant-justificationdocs-retired-personasdocs-role-hierarchydocs-runnable-samplesdocs-analytics-vocabularydocs-dashboard-tilesTwo more touch the files without reading a body:
docs-app-workflow-paths(path globs only) andlabeler-config(a directory listing undercontent/docs/marketing).scripts/check-source-token-ratchet.mjsmeasuressrc/**/*.tsonly, 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).Parity ablation — measured, and it did not fire
The obligation is conditional: it is owed if the diff adds or removes something a guard counts. So the counted quantities were measured on all six pages, working tree against
HEAD:blobs, rather than argued:Identical in both columns of the comparison — every figure above is the before row and the after row. Nothing a guard counts moved, so the condition is not met.
That says nothing about whether those guards could have caught a change, so both counting families were ablated on the committed tree. Each leg mutates, proves the mutation reached disk by re-counting the exact token, runs the guard with its exit code captured before any pipe, then restores with
git checkout HEAD -- (absolute path)and proves the restore by blob hash againstHEAD:campaigns.zh-Hans.mdx—^>lines 1 → 0docs-locale-callouts5333e61=HEAD:blob;git diff HEADemptycampaign-members.mdx:14at#campaign-enrollment-cron— 1 hit for the new slugdocs-anchor-links14f1a80=HEAD:blob;git diff HEADemptyLeg B is the useful one: the line it names,
campaign-members.mdx:14, is the line this PR rewrote, so the anchor in the new sentence is demonstrably resolved by a live guard rather than merely present.Verification
pnpm verifyrun stage by stage on the committed tree at9add45d, every exit code captured before any pipe (pnpm stage file 2 to 1; EXIT=$?). The four heavy stages ran through this container's shared verify lock.pnpm validatepnpm typechecktsc --noEmit, no outputpnpm lint4 warning(s), 12 suggestion(s)— all pre-existingpnpm lint:i18n-gate✓ i18n lint gate: 0 i18n/missing-* issuespnpm hygiene✓ source hygiene cleanpnpm hygiene:tokens✓ source token ratchet clean— readings unchangedpnpm buildArtifact: dist/objectstack.json (1916.9 KB)pnpm testTest-file and test counts before the change, measured on this same worktree at
7c09fb8: 165 files, 3455 passed / 1 skipped (3456) — identical. No test file is added or removed, and nothing undertest/**is touched (#1579's fence).Also self-scanned for control bytes beyond
check-source-hygiene's pass:grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]'over the six pages returns nothing.Scope
Six docs files plus the changeset. ⛔ No metadata change, ⛔ nothing under
test/**, ⛔ nocontent/docs/releases/edit. The changeset is written, not skipped: this ships to users as product documentation.Two sentences were read for the same false-automation shape and deliberately kept, so the judgement is on the record rather than silent:
campaigns.mdx:15("In Progress | Live — members are enrolled, and the responses that come back are recorded on them") describes what characterises a live campaign rather than claiming a trigger, andcampaign-members.mdx:66("the enrollment flow skips opted-out people") is true —query_leads/query_contactsboth filteremail_opt_out: false, andcampaign_member_optout_syncis what populates that column.Out of scope — filed, not fixed here
marketing/indexstill promises "Automatic enrollment" — the fifth false-automation claim about the same screen flow #1859 —content/docs/marketing/index.mdx(+ both siblings) carries the same claim twice more::34is a bullet titled Automatic enrollment under the heading What the system does for you, and:27has "the system bulk-enrolls the target audience". The page's ASCII life-cycle diagram draws it as an arrow out of In Progress too, so the fix is a page-structure decision and not a line edit — which is why it is filed rather than folded into this diff. It survived the two earlier passes over this page (the opens/clicks sweeps) because both were rewriting the bullet directly below it.🤖 Generated with Claude Code
https://claude.ai/code/session_01DuzfS5chho38Yx1jxx9DEj
Generated by Claude Code