Skip to content

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

Closed

Description

@os-steve

Found while fixing #1829, which cleaned content/docs/marketing/campaign-members.* only. ⛔ Out of that card's scope — filed rather than fixed there, on the PM's instruction not to widen that diff.

The finding

#597 removed first_opened_date / first_clicked_date and the opened / clicked / bounced statuses from crm_campaign_member, because nothing in this app or on the platform can write them. Two more doc surfaces still describe them, in all three locales.

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

  • :46 — the record's field-group table: | **Engagement** *(rollup)* | Opens, clicks, responses, conversions |. crm_campaign declares no open or click column. Its rollup block is num_sent · num_responses · num_leads · num_converted_leads · num_opportunities · num_won_opportunities · actual_revenue (the roster CAMPAIGN_METRIC_FIELDS in src/objects/campaign.hook.ts pins), plus the response_rate and roi formulas.
  • :81-82 — "Open rate — % of members with status ≥ Opened" and "Click rate — % of members with status ≥ Clicked". Neither is computable: the member picklist ships sent · responded · converted · unsubscribed, and no campaign field holds either rate.

The Simplified page carries the same two sites (| **Engagement** *(汇总)* | 打开、点击、响应、转化 | at :42, the rates at :81-82) and the Traditional page mirrors them.

2. content/docs/guides/email-and-calendar.mdx:76 (+ .zh-Hans / .zh-Hant)

Under the heading "Email tracking (not shipped yet)":

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.

The two fields are gone, so the sentence documents columns a reader cannot find and offers a workaround — filling them by hand or by import — that cannot be performed. It is the more misleading of the two sites precisely because the section around it is otherwise honest about what is not shipped.

3. Lower confidence, same family

content/docs/marketing/index.mdx:15 describes the page as "The leads and contacts enrolled in each campaign and their engagement status" (both Chinese siblings say 互动状态 / 互動狀態). No phantom value is named, so this is a wording call rather than a false claim — noted here so a fixer can decide it in one pass rather than find it later.

What a fix looks like

The same treatment #1829 got: state the four shipped statuses and the counters the campaign really rolls up, and delete the two rates rather than restate them. src/objects/campaign.hook.ts defines num_responses as members at responded or converted, and response_rate is that over num_sent — the campaigns page can say exactly that. On the email guide, the honest sentence is that a campaign member records a response, not an open or a click.

⚠️ Do not answer this by re-adding fields to crm_campaign or crm_campaign_member. #597 removed the member's tracker columns because nothing wrote them, and the campaign never had open/click columns at all.

Where it came from

The same gap #1829 records: #597 trimmed the metadata and no doc surface was swept with it. Nothing in CI reads these claims — a sweep of test/ and scripts/ for Opened|Clicked|Bounced|Open rate|Click rate returns only CasesOpenedByDayPriorityReport, an unrelated service report.

Refs: #597 (the removal) · #1829 (the same defect on the member page, fixed) · #1691 (an earlier metrics correction on the campaigns page).

Activity

  1. added
    documentationImprovements or additions to documentation
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Sep 10, 2026
  2. self-assigned this
    on Sep 10, 2026
  3. os-steve commented on Sep 10, 2026

    @os-steve
    CollaboratorAuthor

    Claim: session_01DuzfS5chho38Yx1jxx9DEj → branch claude/issue-1844-campaign-tracker-docs-sweep

    Dispatched by the hotcrm PM seat (#1353). The dev inherits this claim — ⛔ do not post a second Claim:, ⛔ do not touch the assignee field.

    Base off main fetched fresh — ⚠️ wait for PR #1846 to land first, or rebase onto it. That PR rewrites campaign-members.* (the sibling page all six of your files link to) and is armed for the queue. Your files do not overlap its diff, but your new prose must be consistent with the page it points at.
    Model: Opus. Deliverable: one PR, ready for review, pnpm verify green. ⛔ Do not enable auto-merge, ⛔ do not merge.

    PM verification — all six sites re-measured before dispatch, on current main

    The filing holds at every point. Read directly:

    • marketing/campaigns.mdx:46 — | **Engagement** *(rollup)* | Opens, clicks, responses, conversions |, against CAMPAIGN_METRIC_FIELDS (src/objects/campaign.hook.ts:312-320) = num_sent · num_responses · num_leads · num_converted_leads · num_opportunities · num_won_opportunities · actual_revenue. No open or click column exists.
    • marketing/campaigns.mdx:81-82 — Open rate / Click rate, mirrored verbatim in .zh-Hans:81-82 (状态 ≥ Opened / ≥ Clicked) and in the Traditional sibling.
    • guides/email-and-calendar.mdx:76 — confirmed, and ⭐ worse than the filing says. The sentence opens "The one adjacent thing that is real" and then names two fields that do not exist, inside a section whose whole point is honesty about what is not shipped. It does not merely describe absent columns; it certifies them as the real part.
    • marketing/index.mdx:15 — "their engagement status" (互动状态 / 互動狀態). A wording call, as filed.

    ⭐ One sharpening the filing did not make — and it widens your fix

    The two bullets that survive on campaigns.mdx are also wrong:

    - **Response rate** — % of members with status ≥ Responded.
    - **Conversion rate** — % of members with status = Converted (i.e., became opportunities).
    

    ≥ imports an ordering the four-value picklist does not have, and it is the same phantom monotonic model #597 removed — the Open rate/Click rate lines are just its most visible symptom. So ⛔ do not delete two bullets and leave these two standing as if they were correct. The app's real definitions are in the metadata and you should quote them: num_responses counts members at responded or converted (the hook predicate), and response_rate is that over num_sent. Conversion rate needs checking against num_converted_leads / num_opportunities rather than restated from the old sentence.

    Rulings

    Scope

    Six files (marketing/campaigns ×3, guides/email-and-calendar ×3) plus marketing/index ×3 if the wording call lands there. ⛔ No metadata change. ⛔ Nothing in test/** (epic #1579's fence) — if you conclude a guard should exist, report it, do not build it; that question is already with the maintainer from PR #1846's review. ⛔ No content/docs/releases/ edit.

    ⚠️ Two siblings are live on other surfaces: src/dashboards/ + src/translations/ (#1822) and src/flows/ (#1184 phase 2). Neither meets yours.

    Proof obligation

    pnpm verify stage by stage, exit codes captured before any pipe, lint:i18n-gate included. Test-file / test counts before and after. Per file, say which sections you rewrote and which line of metadata each new claim is sourced from.


    Generated by Claude Code

  4. os-steve commented on Sep 10, 2026

    @os-steve
    CollaboratorAuthor

    os-dev-report

    {
      "issue": 1844,
      "status": "done",
      "branch": "claude/issue-1844-campaign-tracker-docs-sweep",
      "pr": "https://github.com/objectstack-ai/hotcrm/pull/1850",
      "premise_still_valid": true,
      "summary": "The filing and the PM's re-measurement held at every one of the six named sites, and the sharpening is discharged: `Response rate` / `Conversion rate` were not left standing. `≥ Responded` is gone (the four-value picklist has no ordering), `num_responses` is quoted as the hook predicate `status === 'responded' || status === 'converted'` (campaign.hook.ts:140) and `response_rate` as its formula over `num_sent` (campaign.object.ts:236-241); `Conversion rate` turned out not to be a field at all and was replaced by `num_converted_leads` / `num_opportunities` rather than restated. Two further phantoms surfaced on the same bullet list and are also gone: `Pipeline value` (no campaign field sums open pipeline — `actual_revenue` sums closed-won only) and the claim that any of it is shown in a 'right-hand panel' (there is no hand-built campaign page under src/pages/; the numbers are in the derived *Performance* and *Budget & ROI* groups). The record-sections table named six sections of which three do not exist — *Audience*, *Engagement (rollup)*, *Attribution (rollup)* — so it was rewritten over the object's real fieldGroups (campaign.object.ts:37-43, agreeing with the form at campaign.view.ts:97-176), and the page's other reference to a *Costs* section became *Budget & ROI*. ⭐ `marketing/index` was NOT only the wording call the card scoped it as. Beyond :15 it carried two harder claims that neither the filing nor the pre-dispatch measurement reached: :28 wrote out the full phantom ladder *Sent → Opened → Clicked → Responded → Converted* as the product's life cycle, and :35 promised that 'opens, clicks, bounces, unsubscribes update the member status as engagement happens' — an automatic writer for three statuses that do not exist, which is PR #1846's admin-tip class (it certifies the capability, it does not merely name absent values). Both are rewritten against the four shipped statuses and their real writers. The :15 wording is settled as ruled (engagement status → how each of them responded, sourced from the object's own description and the zh-CN pack's). Nine files, three locales each; no metadata, no test/**, no releases/ edit. PR is ready for review (not draft) and auto-merge is NOT armed, per this dispatch — noting that my standing dev rules default to a draft PR, and I followed the per-card instruction.",
      "tests": "`pnpm verify` stage by stage on the committed tree (aad7365), each exit code captured before any pipe (`cmd > log 2>&1; EXIT=$?`): validate 0 (4 warnings / 12 suggestions, all pre-existing, none on crm_campaign) · typecheck 0 · lint 0 · lint:i18n-gate 0 (`✓ i18n lint gate: 0 i18n/missing-* issues`) · hygiene 0 (`✓ source hygiene clean`) · hygiene:tokens 0 (`✓ source token ratchet clean`) · build 0 (`dist/objectstack.json (1913.9 KB)`) · test 0. Test counts identical before and after: 165 files, 3455 passed | 1 skipped (3456) on the pre-change tree at 90ae658 and on this commit; no test file added or removed. Docs guards checked before deleting any string: a sweep of test/ and scripts/ for Opened|Clicked|Bounced|Open rate|Click rate|Conversion rate|Pipeline value|互动|互動 pins nothing this PR removes — only CasesOpenedByDayPriorityReport (unrelated) and seed-consistency.test.ts, which reads seed rows. Guards that do read these pages and stayed green: docs-anchor-links, docs-locale-callouts, docs-object-coverage, docs-view-rosters, docs-conversion-rate-spelling, docs-object-term-consistency (ledger holds crm_case/crm_quote only), docs-setup-navigation-names, docs-metadata-counts, docs-src-tree-paths — each with its reason in the PR body. Ablation (committed first, then mutated, both legs measured, restored with `git checkout HEAD -- PATH` and proved byte-identical by `git hash-object` == the HEAD blob, `git diff HEAD` empty): (1) breaking the new anchor to #measuring-a-campaign-does-not-exist turns docs-anchor-links red naming campaigns.mdx:45 — restored, 18/18 pass; (2) deleting the new callout from campaigns.zh-Hant turns docs-locale-callouts red with 'campaigns.zh-Hant.mdx: 0 callout(s), but campaigns.mdx has 1' — restored, 2/2 pass. Callout parity shipped at 1/1/1 (campaigns), 2/2/2 (email guide), 0/0/0 (marketing index). Extra self-checks: `grep -naP '[\\x00-\\x08\\x0b\\x0c\\x0e-\\x1f\\x7f]'` over the nine files + changeset returns no hits; every markdown table on the edited pages has a uniform column count. Declared to CI, not run locally: the Docs App workflow (apps/docs, its own lockfile — the only thing that compiles this MDX), which this PR triggers through the content/docs/** path filter. CI status at report time: in_progress.",
      "mcp_calls": "9 — 2 issue_read (#1844 body + comments), 2 pull_request_read (#1846 body + comments), 1 search_issues (dedupe before filing, after the REST search endpoint answered 403 unauthenticated — channel switch declared), 2 issue_write (#1847 create + one body correction), 1 create_pull_request, 1 add_issue_comment (this report). Repo-scoped REST GET works unauthenticated here (HTTP 200) and was used for the read-backs; /search/issues does not (403).",
      "open_questions": [
        {
          "question": "Should `marketing/campaigns` carry a record-sections table at all? Documentation rule 5 says a page explains business concepts and never hand-copies a machine roster; I corrected the table (three of its six section names did not exist) rather than deleting it, because a detail screen's section headings are visible in the product UI, which is the rule's own boundary test, and because PR #1846 shipped and was accepted with a field table of the same shape.",
          "options": [
            "A — keep it as shipped: six real section names, contents described in business terms rather than field-by-field (what this PR does).",
            "B — delete the table and describe the record in prose, on the strict reading of rule 5 that any section/field roster in docs is a copy that will drift.",
            "C — keep it and pin it, i.e. a guard comparing the table against the object's fieldGroups — which is the very question already with the maintainer from PR #1846's review, so it is not decidable here."
          ],
          "recommendation": "A. The table's defect was that it was false, not that it existed: it named an *Engagement (rollup)* section a reader could never find. Rule 5's boundary is 'can a reader see this directly in the product UI', and field-group headings are exactly that — unlike a semantic layer. B would also delete the page's only statement that no open/click column exists, which is the thing this card is about."
        }
      ],
      "out_of_scope_findings": [
        "filed as #1847: a THIRD surface family still carries the #597 stamps — analytics/cubes.mdx:72 and analytics/reports.mdx:95 (+ .zh-Hans/.zh-Hant, six files) say `crm_campaign_member` stamps **First Opened**, **First Clicked**, Response Date and Responded per member, and analytics/reports.mdx:98 carries the ungreppable paraphrase 'the opens, clicks and responses are stamped on each member record'. Not fixed here: outside this dispatch's nine files, and both pages are pinned by docs-conversion-rate-spelling.test.ts, so widening would have put a guarded page into a diff not measured for it.",
        "not filed, reported only (epic #1579's fence, and the question is already with the maintainer from PR #1846's review): nothing in CI reads any of these claims. This card is now the second measurement of that gap in two days — three doc surface families drifted off one metadata trim (#597) and every check stayed green through all of them, which is a data point for the status-roster guard PR #1846 recommended rather than a new question."
      ]
    }

    _Generated by Claude Code


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

documentationImprovements or additions to documentationi18npm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions