Repository navigation
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
Activity
- addedpm:epicParent delegated to a dedicated epic PM — other PMs never dispatch into its subtreeParent delegated to a dedicated epic PM — other PMs never dispatch into its subtree
on Sep 9, 2026 Claim:session_01DuzfS5chho38Yx1jxx9DEj→ branchclaude/issue-1827-contact-mailing-addressDispatched 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
mainat965933bor later. ⛔ Do not branch fromclaude/issue-1581-lint-strict-flip(polluted by #1806's superseded bump commits, stale base).⚠️ claude/issue-1826-field-consumers-sixis live and unmerged (draft PR #1831, headc7a7f47f). It touchessrc/views/account.view.ts·campaign.view.ts·contract.view.ts·src/objects/campaign_member.object.tsand the accounts / campaign-members docs. This card touchessrc/views/contact.view.tsand the contacts docs — no overlap, but ⛔ do not measure your lint delta against a tree that contains #1826's work: base offmain, 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: base965933b=errors 0 / warnings 13 / suggestions 12; after a single form-section binding oncrm_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: oncrm_quote_line_item.line_numberthe 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 amailing_*field, confirm something actually puts data in it — for these five,contact_import.mapping.tsdoes, which is exactly why they are different fromline_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
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
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
mappingis 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.0artifact (npm pack, then grep the shippeddist):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."mappingsis a named carrier root, and the finding message names "an import mapping" in so many words. The dev also reports the hint'sRoots scannedline printing it on the carriers side and each finding'scarriers[]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
objectstackfor 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
Filed by the epic PM (
session_01DuzfS5chho38Yx1jxx9DEj) on the maintainer's instruction, 2026-09-09, verbatim: 「继续处理所有相关任务」.pm:epicreserves it for the epic PM. Companion to #1826 — together the two cards account for 11 of the 12field-no-consumerswarnings; the twelfth (crm_forecast.seed_key) is correct by design.#1581 is gated on this. Its acceptance needs
errors: 0andwarnings: 0. Errors have been 0 since the 17.4.0 migration (#1807 / PR #1814,main965933b). 13 warnings stand: 12 ×field-no-consumers+ 1 ×component-props-unknown-key(#1216's, ⛔ never fixed here). Until they reach 0 theos lint --strictflip 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 declaredmailing_addressfieldGroup (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–49maps all five astarget, andassets/import-templates/contacts.csvcarries the columns. The mapping's own header comment is explicit: "Address lands in the flatmailing_*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_datefire in #1826 despite two flow nodes stamping it.field-no-consumersasks who reads the field — its own wording lists "view column, form section, page binding, flow node, dataset, widget, formula, validation, hook or action".defineMappingis 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_contactis different: it has an authored form.src/views/contact.view.tsdeclaresform: { type: 'tabbed', sections: [...] }with exactly three sections —identity,contact_details,comm_preferences— and not one of them names anymailing_*field, nor themailing_addressgroup.form.sectionslist is exhaustive, then these five are not on screen at all: the CSV importer writes address data into columns no user can see or edit. That is a real product defect, and the lint warning is correctly reporting it.fieldGroupsinto an authored form — the group carriesdefaultExpanded: false, phrasing that only makes sense for something that renders, and thees-EStranslation file even annotates the group heading as accompanying these fields — then they do render, and this is the Give the six declared-but-unnamed fields the consumer they should already have — clear 6 of the 12field-no-consumerswarnings blocking #1581 (epic #1579) #1826 situation after all.⛔ Do not assume either. Determine which it is against
@objectstack/*@17.4.0and 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)
mailing_addresssection to the contact form naming all five. One change clears all five warnings and fixes the product defect. The object already declares the group with an icon anddefaultExpanded: false— the authored intent for a collapsible address block is on the record; the form just never picked it up.field-no-consumerswarnings blocking #1581 (epic #1579) #1826 one: name them in metadata (a section, a binding, or address columns on a view). Coordinate with Give the six declared-but-unnamed fields the consumer they should already have — clear 6 of the 12field-no-consumerswarnings blocking #1581 (epic #1579) #1826 so the two PRs do not fight overfield-no-consumersmeasurements.⛔ How NOT to do it
os lint --strictfirst, then retire the local re-implementations by family #1579 exists to remove, and it would make Turn onos lint --strictin this repo's verify chain and prove the gate reds (epic #1579, step 2b — the half that needs a release) #1581's flip meaningless.assets/import-templates/contacts.csv.en,es-ES,ja-JP,zh-CN) — the i18n gate will catch a miss. Note the existing comments incontact.view.tsabout section names colliding withfieldGroupkeys (contact_detailsvscontact_info,comm_preferencesvspreferences): a section namedmailing_addresswould collide with the group key the same way. Read those comments before naming yours.Acceptance
pnpm lint --jsonbefore and after on your own base SHA and report botherrors / warnings / suggestionstriples. Parse withraw_decodefrom the first{— pnpm appendsELIFECYCLEnoise.pnpm verifygreen end to end.⛔ Not in this card
field-no-consumerswarnings blocking #1581 (epic #1579) #1826 (crm_account.logo,crm_campaign.description,crm_campaign_member.added_date,crm_contract.description,crm_quote_line_item.line_number,crm_article_feedback.comment).crm_forecast.seed_key— correctly unconsumed by design (hidden: true, seeder-only upsert identity); raised upstream as objectstack#17135.component-props-unknown-key×1 — The Sales Home AI card's paragraph never reaches the screen:descriptionis not a proppage:carddeclares, and #1002's guard pins the copy there #1216's, ⛔ never fixed here.--strictflip itself — Turn onos lint --strictin this repo's verify chain and prove the gate reds (epic #1579, step 2b — the half that needs a release) #1581.⭐ Worth raising upstream either way: should a
defineMappingfieldMapping.targetcount as a consumer for this rule? The PM's reading is no — a writer is a carrier — and #1826'sadded_datecase says the rule is deliberate about this. But the rule's message names ten metadata kinds andmappingis 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 onobjectstack, ⛔ 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.