Skip to content

The five crm_contact.mailing_* fields: the CSV importer writes them and the authored contact form may show none of them — clear the last 5 field-no-consumers warnings blocking #1581 (epic #1579) #1827

Description

@os-steve

Filed by the epic PM (session_01DuzfS5chho38Yx1jxx9DEj) on the maintainer's instruction, 2026-09-09, verbatim: 「继续处理所有相关任务」. pm:epic reserves it for the epic PM. Companion to #1826 — together the two cards account for 11 of the 12 field-no-consumers warnings; the twelfth (crm_forecast.seed_key) is correct by design.

#1581 is gated on this. Its acceptance needs errors: 0 and warnings: 0. Errors have been 0 since the 17.4.0 migration (#1807 / PR #1814, main 965933b). 13 warnings stand: 12 × field-no-consumers + 1 × component-props-unknown-key (#1216's, ⛔ never fixed here). Until they reach 0 the os lint --strict flip cannot happen, which blocks six family cards (#1582–#1587).

The five

crm_contact.mailing_street · mailing_city · mailing_state · mailing_postal_code · mailing_country — all five in the declared mailing_address fieldGroup (src/objects/contact.object.ts:21, 136–140).

Why the rule fires even though something clearly names them

src/mappings/contact_import.mapping.ts:45–49 maps all five as target, and assets/import-templates/contacts.csv carries the columns. The mapping's own header comment is explicit: "Address lands in the flat mailing_* text fields, which is why contacts (unlike accounts and leads) carry address columns in their template."

⇒ A mapping target is a writer. A writer is a carrier, not a consumer. Same semantics that made crm_campaign_member.added_date fire in #1826 despite two flow nodes stamping it. field-no-consumers asks who reads the field — its own wording lists "view column, form section, page binding, flow node, dataset, widget, formula, validation, hook or action". defineMapping is on neither side of that list by name.

⭐ The question that decides this card — settle it FIRST, empirically

This is not the same situation as the six in #1826, and the difference is the whole card.

In #1826 every object renders through a synthesized detail/form layout, so "no consumer" means "not named in metadata" while the field is still on screen. crm_contact is different: it has an authored form. src/views/contact.view.ts declares form: { type: 'tabbed', sections: [...] } with exactly three sections — identity, contact_details, comm_preferences — and not one of them names any mailing_* field, nor the mailing_address group.

⛔ Do not assume either. Determine which it is against @objectstack/*@17.4.0 and state how you determined it. The remedy differs completely, and the PM has been wrong before by reasoning from wording instead of measuring.

Candidate remedies (the PM's reading, ⛔ not a specification)

⛔ How NOT to do it

Acceptance

  • Measure pnpm lint --json before and after on your own base SHA and report both errors / warnings / suggestions triples. Parse with raw_decode from the first { — pnpm appends ELIFECYCLE noise.
  • The render question above answered, with the evidence.
  • All five either cleared (naming what now consumes them) or standing (with the reason and the decision needed).
  • pnpm verify green end to end.
  • Changeset: ⛔ not empty frontmatter — if this turns out to be the product defect, the release-notes reader needs to know the contact form gained its address block.

⛔ Not in this card

⭐ Worth raising upstream either way: should a defineMapping fieldMapping.target count as a consumer for this rule? The PM's reading is no — a writer is a carrier — and #1826's added_date case says the rule is deliberate about this. But the rule's message names ten metadata kinds and mapping is not among them in either direction, so a hotcrm reader cannot tell "deliberately excluded" from "not considered". If you reach a view on it, file it on objectstack, ⛔ not here.

Refs: #1579 (epic) · #1581 (the gate this unblocks) · #1826 (companion, the other six) · #1807 / PR #1814 (where the 12 were measured and dispositioned) · objectstack#17135.

Activity

  1. added
    pm:epicParent delegated to a dedicated epic PM — other PMs never dispatch into its subtree
    on Sep 9, 2026
  2. self-assigned this
    on Sep 9, 2026
  3. os-steve commented on Sep 9, 2026

    @os-steve
    CollaboratorAuthor

    Claim: session_01DuzfS5chho38Yx1jxx9DEj → branch claude/issue-1827-contact-mailing-address

    Dispatched by the epic PM for #1579. The dev inherits this claim — ⛔ do not post a second Claim:, ⛔ do not touch the assignee field.

    Base off main at 965933b or later. ⛔ Do not branch from claude/issue-1581-lint-strict-flip (polluted by #1806's superseded bump commits, stale base).

    ⚠️ claude/issue-1826-field-consumers-six is live and unmerged (draft PR #1831, head c7a7f47f). It touches src/views/account.view.ts · campaign.view.ts · contract.view.ts · src/objects/campaign_member.object.ts and the accounts / campaign-members docs. This card touches src/views/contact.view.ts and the contacts docs — no overlap, but ⛔ do not measure your lint delta against a tree that contains #1826's work: base off main, and expect 9 warnings at base once #1831 merges, 13 before it does. Report which base you actually measured.


    ⭐ One finding from #1826 that changes what this card has to prove. The sibling card's gating premise — "adding a metadata binding clears field-no-consumers" — was measured and it HELD: base 965933b = errors 0 / warnings 13 / suggestions 12; after a single form-section binding on crm_campaign.description = warnings 12, and that field left the issue list. So ⛔ you do not need to re-litigate whether the remedy works. Your card's open question is the different one: whether these five fields render at all.

    ⚠️ And a caution from the same report, because it is the shape of mistake this card can repeat: on crm_quote_line_item.line_number the PM's suggested consumer was falsified — that field has no writer anywhere, so a column would have rendered blank on every row a user creates. A binding that satisfies the linter but shows a user nothing is the fabricated surface this card ranks below an honest standing warning. Before you bind a mailing_* field, confirm something actually puts data in it — for these five, contact_import.mapping.ts does, which is exactly why they are different from line_number.

    Model: Opus, per 「没有 fable 派 opus」.
    Deliverable: a draft PR. 「等总监复审」 — ⛔ not ready-for-review, ⛔ no auto-merge, ⛔ no merge, whatever CI reports.


    Generated by Claude Code

  4. claude commented on Sep 9, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 1827,
      "status": "done",
      "branch": "claude/issue-1827-contact-mailing-address",
      "pr": "https://github.com/objectstack-ai/hotcrm/pull/1837",
      "premise_still_valid": true,
      "summary": "THE RENDER QUESTION, ANSWERED: an authored `form.sections` list IS exhaustive — the platform does NOT merge declared `fieldGroups` into it. But the card's binary framing was incomplete in a way that matters: the five fields were NOT invisible. `crm_contact` authors no detail page, so the Console SYNTHESIZES its record page from `fieldGroups`, and the detail screen renders all six groups including a collapsed `Mailing Address` holding all five fields, each inline-editable. The gap was the FORM: create and edit (one authored form serves both) rendered 13 inputs, none `mailing_*`, with the string `Mailing` absent from the dialog entirely. So the CSV importer could write an address and a user could read it, but no form could enter or correct one — address entry existed only on the import path. That is the real product defect, narrower and more precise than 'not on screen at all'. HOW I DETERMINED IT (three independent sources, all agreeing): (1) BROWSER, the primary evidence — booted `pnpm dev` on base 965933b, signed into the Console as the seeded dev admin, drove headless Chromium: detail screen shows the six groups and expanding `Mailing Address` shows all five labels; the New dialog shows tabs Identity/Contact Info/Preferences with 13 inputs and no `mailing_*` name; the Edit dialog is identical. After the change, both dialogs show a fourth tab `Mailing Address` with all five inputs, and a real round trip (typed street/city/country, pressed Update, read the record back over REST) persisted `1 Broadway` / `Cambridge` / `USA`. (2) PINNED DIST — `@objectstack/console@17.4.0` `dist/assets/plugin-form-*.js`: all three form containers compute `sections?.length ? sections : deriveFieldGroupSections(...)`, and the derivation is itself guarded to return null when authored sections exist. No merge branch anywhere. (3) THIS REPO ALREADY KNEW — `src/views/case.view.ts` states it verbatim at console 17.1.0 ('an authored `sections` array wins outright'), and `test/field-groups-coverage.test.ts` states the synthesized-detail half. I re-measured both at 17.4.0 rather than quoting them. THE FIX: one form section named `mailing_address` in `src/views/contact.view.ts`, placed between `contact_details` and `comm_preferences` so form order mirrors the detail screen; `mailing_street` (a textarea) takes `span: 'full'`. NAMING TRAP HANDLED, and the answer is the opposite of the two neighbours: reusing the `mailing_address` group key makes the section heading follow the group's wording, which here is exactly what is wanted, and all four locales ALREADY carry `objects.crm_contact._sections.mailing_address` — so zero new translation rows and no i18n gate exposure. The reasoning is written into the file beside the two existing notes. NOT THE line_number TRAP: all five are filled — `contact_import.mapping.ts` maps every one as an import target and `assets/import-templates/contacts.csv` ships the columns — and they are plain text/textarea, not readonly/hidden/derived, so a user can type into them, which the round trip above proves rather than asserts. NO TEST ADDED (AGENTS.md scope rule 3, same call as #1826) and none needed updating — nothing in test/ pins the contact form's section roster. NO DOCS CHANGE: `content/docs/sales/contacts.mdx` documents the DETAIL screen's six sections, which this PR does not touch, and its promise that the record stores a mailing address is now true on the form as well. UPSTREAM QUESTION — the card's premise is FALSIFIED, so nothing was filed on objectstack: my view is that a `defineMapping` `fieldMapping.target` should NOT count as a consumer (a writer is a carrier), and the rule ALREADY says so by name. Read from `@objectstack/lint@17.4.0`: `CARRIER_ROOTS = ['translations','data','mappings','permissions']` names `mappings` explicitly; the finding message reads 'A translation label, a seed value, AN IMPORT MAPPING, a permission grant or a flow that only WRITES it is a carrier, not a consumer'; the hint's 'Roots scanned' line prints `mappings` on the carriers side; and each finding's `carriers[]` array lists the exact site, e.g. `mappings[1].fieldMapping[10].target`. So the card's claim that 'mapping is not among them in either direction, so a hotcrm reader cannot tell deliberately-excluded from not-considered' is not true of 17.4.0 — the reader can tell, four ways. There is nothing to raise. ASSIGNEE was already set by the PM (os-steve) and the newest Claim: comment named this session and this branch; I posted no second claim and never wrote the field.",
      "tests": "BASE SHA MEASURED: 965933b (origin/main), branched directly — not from claude/issue-1581-lint-strict-flip, and no tree containing #1826's work. `pnpm lint --json` parsed with raw_decode from the first `{`: BEFORE errors 0 / warnings 13 / suggestions 12 (total 25, passed true) — AFTER errors 0 / warnings 8 / suggestions 12 (total 20, passed true). Exactly -5 warnings, all five being the `crm_contact.mailing_*` entries; suggestions unchanged; no new finding of any rule. The 7 remaining `field-no-consumers` are objects[0].logo, objects[1].comment, objects[2].description, objects[3].added_date, objects[6].description (the five of #1826's six that are not already cleared on its own branch) plus objects[9].seed_key and objects[16].line_number. `pnpm verify` GREEN end to end under the shared verify lock: `os-verify-lock: VERDICT command-exit 0 · held the lock 168s · waited 131s`. Step by step from that run: `✓ Validation passed (1509ms)` · typecheck clean · lint clean · `✓ i18n lint gate: 0 i18n/missing-* issues (20 total lint issue(s) reported)` · `✓ source hygiene clean` · `✓ source token ratchet clean` (interaction layer ~37,921 of ceiling ~40,000; authored total ~137,833 of ~140,000 — the added code is ~10 lines and comments are stripped from that measure) · `✓ Build complete (1765ms)` with `⚠ 12 author-time warning(s)`, down from 17 at base · `Test Files 165 passed (165)` / `Tests 3454 passed | 1 skipped (3455)`. BROWSER VERIFICATION (the render question, and the change itself): headless Chromium at /opt/pw-browsers/chromium against `pnpm dev` on 4001, signed in as admin@objectos.ai. Before: create dialog input names = [salutation, first_name, last_name, title, department, email, phone, mobile, avatar, lead_source, is_primary, do_not_call, email_opt_out] — no `mailing_*`, and `Mailing` absent from the dialog text; edit dialog identical; detail screen renders `Mailing Address` collapsed and, expanded, all five labels with an inline-edit pencil. After: both dialogs carry a fourth tab `Mailing Address` and all five inputs; filled mailing_street/city/country in the edit dialog, pressed Update, re-read GET /api/v1/data/crm_contact/{id} — mailing_street \"1 Broadway\", mailing_city \"Cambridge\", mailing_country \"USA\". Screenshots in the session scratchpad. No ablation applies: this card adds a surface, it does not add or weaken a gate — the negative control is the before-measurement above, taken on the same tree in the same browser session. NOT MEASURED / declared to CI: nothing — the full local chain is the same chain CI runs (package.json `verify`), and it ran here.",
      "mcp_calls": "4 — two `search_issues` (one dedup query per finding, one control query to prove the empty result was a real reading) and two `issue_write` (the two findings). Everything else went through zero-quota channels: git for the branch and diff, container REST for the issue body, the comment thread, the PR creation and the record round trip. REST `/search/issues` returned 403 as the channel table predicts, which is why the two dedup searches are MCP — declared channel switch.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #1835: every `fieldGroups` collapse hint in this repo is authored with the deprecated `defaultExpanded` alias — 25 occurrences across 13 object files, zero use of the canonical `collapse: 'none'|'expanded'|'collapsed'`. Honoured today (verified in the browser: the contact's `defaultExpanded: false` group does render collapsed), so it is drift, not a defect — but the boolean alias cannot express `collapse: 'none'`, and the platform describes the aliases as a compatibility path for un-normalized metadata, which hand-authored .object.ts files are not. Labelled `finding` + `metadata`, no assignee, no pm:queue.",
        "filed as #1836: `crm_contact` models a postal address as five flat text fields while `crm_account.billing_address` and `crm_lead.address` both use `Field.address()`, and AGENTS.md's own Field Type Guidance table names `Field.address()` for 'Mailing address'. The flat shape is asserted as deliberate in `contact_import.mapping.ts`'s header but with no reason and no ruling cited; #664 (closed, `address` values rendering as raw JSON) is a plausible expired premise worth re-measuring. Not touched here — converting moves the schema, the shipped CSV importer contract, four locales, the docs and existing data, so it wants a decision first. Labelled `finding` + `metadata`, no assignee, no pm:queue.",
        "not filed, reported here instead — the card's own upstream question is answered NO and needs no objectstack card: `@objectstack/lint@17.4.0` already names `mappings` as a carrier root, names 'an import mapping' in the finding message, prints it in the hint's roots line, and lists the exact mapping site in `carriers[]`. Filing would duplicate an explicit, four-way-documented decision."
      ]
    }

    Generated by Claude Code


    Generated by Claude Code

  5. os-steve commented on Sep 9, 2026

    @os-steve
    CollaboratorAuthor

    Correction to this card's own text, from the PM who wrote it

    The card closes with an upstream question and a claim underneath it. The claim is false, and I have verified that myself rather than taking the dev's word for it.

    What I wrote:

    the rule's message names ten metadata kinds and mapping is not among them in either direction, so a hotcrm reader cannot tell "deliberately excluded" from "not considered".

    Measured against the published @objectstack/lint@17.4.0 artifact (npm pack, then grep the shipped dist):

    CARRIER_ROOTS = ["translations", "data", "mappings", "permissions"]
    
    "A translation label, a seed value, an import mapping, a permission grant
     or a flow that only WRITES it is a carrier, not a consumer."
    

    mappings is a named carrier root, and the finding message names "an import mapping" in so many words. The dev also reports the hint's Roots scanned line printing it on the carriers side and each finding's carriers[] array naming the exact site (e.g. mappings[1].fieldMapping[10].target). A reader can tell, four ways over.

    ⇒ The rule's answer to my question was already "no, a writer is a carrier", stated explicitly. ⛔ Nothing is to be filed on objectstack for it; filing would duplicate an explicit decision. The dev was right to answer the question here and file nothing, and right that the premise was falsified rather than the question merely answered.

    ⚠️ The failure mode, recorded because it is the second time today. I asserted an absence — "X is not mentioned" — without measuring it, exactly as I asserted that no objectui card existed for #8820's work when #7783 had been open four days. An absence is a claim like any other and needs the same measurement as a presence. Both cost real work: this one nearly sent a card upstream against a documented decision.

    The rest of the card stands, and its central question was answered correctly — see PR #1837.


    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

pm:epicParent delegated to a dedicated epic PM — other PMs never dispatch into its subtree

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions