Skip to content

feat(contact): one structured mailing_address replaces five flat mailing_* fields (#1836) - #1997

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-1836-contact-mailing-address-r72
Oct 3, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-1836-contact-mailing-address-r72

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #1836
Clause-②: no. This is app metadata on crm_contact and converges on the shape crm_account and crm_lead already use; it touches no published schema and no accept set.

What changes

crm_contact now stores its mailing address as one structured mailing_address (Field.address()), the shape crm_account.billing_address and crm_lead.address already use. The five flat text fields mailing_street · mailing_city · mailing_state · mailing_postal_code · mailing_country retire in the same change. The shipped import template assets/import-templates/contacts.csv is unchanged: the mapping sends each of its five Mailing … columns to one part of the compound (mailing_address.street … mailing_address.country). Existing rows are converted once by scripts/backfill-contact-mailing-address.ts.

The blocker R64 measured on 17.4.0 is gone: on the 17.6.0 pin the importer writes compound parts from columns (objectstack 443b2f4f). The target spelling field.part is read from the installed @objectstack/spec 17.6.0 ImportFieldMappingSchema.target ("a declared part of a compound field is written field.part (e.g. mailing_address.street)").

Ruling 5731202390 → evidence

Ruling item Where Evidence (all on @objectstack/* 17.6.0)
1. Precondition: a Field.address() value renders as one unit on the contact detail page (#664) fresh boot of this branch's artifact, headless Chromium, port 4817 Contact detail: section Mailing Address shows one field MAILING ADDRESS = 12 Harbour Way, Austin, TX 78701, USA. Probe: no "street": and no [object Object] in the page text.
2. mailing_address: Field.address(); the five mailing_* fields retire in the same change src/sales/objects/contact.object.ts Build summary 363 Fields → 359 Fields. Served GET /api/v1/meta/object/crm_contact: the only mailing field is {"mailing_address":"address"}.
3. contacts.csv keeps its five columns, mapped into street / city / state / postalCode / country src/sales/mappings/contact_import.mapping.ts. contacts.csv is untouched: git diff 4807ed9e -- assets/import-templates/contacts.csv is empty Unchanged CSV through POST /api/v1/data/crm_contact/import (mappingName: crm_contact_import), after accounts.csv (50/50). main 4807ed9e: dryRun 50 total / 44 ok / 6 errors; commit 44 ok / 6 errors. This branch: dryRun 44 ok / 6 errors; commit 44 ok / 6 errors. The same six rows fail on both (Department: "HR" is not a known option, see notes). REST read-back: ana.haddad@… → {"street":"12 Harbour Way","city":"Austin","state":"TX","postalCode":"78701","country":"USA"}; ben.ibarra@… → {"street":"480 Sequoia Ave","city":"Portland","state":"OR","postalCode":"97205","country":"USA"}. Edge rows (scratch CSV): all five cells blank → mailing_address: null; only city and country → {"city":"Lyon","country":"France"}.
4a. The #1827 form section renders the one field src/sales/views/contact.view.ts Edit Contact dialog → Mailing Address tab shows one address widget (Street Address · City · State / Province · ZIP / Postal Code · Country) prefilled from the record. Changed postal code 78701 → 78702, Update, dialog closed; REST read-back postalCode: "78702".
4b. Labels collapse to one row per locale src/sales/translations/{en,zh-CN,es-ES,ja-JP}/objects.customer.ts Five field rows → one mailing_address row in each pack. pnpm lint:i18n-gate: 0 i18n/missing-*.
4c. Contacts docs in 3 locales say one structured field content/docs/sales/contacts{,.zh-Hans,.zh-Hant}.mdx The Mailing Address row reads one structured field (was "five separate fields").
4d. Existing rows converted once by a one-off system write scripts/backfill-contact-mailing-address.ts See Conversion below.
5. The reason in contact.object.ts in one sentence src/sales/objects/contact.object.ts The comment above mailing_address.

Conversion (scripts/backfill-contact-mailing-address.ts)

Same shape as backfill-line-number.ts: REST only, report-only unless --apply, read-back after writing, rerunnable.

  • Where it reads the old values. Upgrading leaves the five columns in the database as orphaned columns (os migrate plan calls them unmapped_column; only os migrate apply --allow-destructive drops them). Measured on 17.6.0 + SQLite: an unprojected POST …/crm_contact/query still returns mailing_street … mailing_country, while naming one in fields is refused (400 INVALID_FIELD: Unknown field 'mailing_street'). The script reads the unprojected row. The changeset tells operators to run it before the destructive migrate.
  • Fixture. Booted main's artifact (4807ed9e) on a fresh SQLite DB and imported the shipped CSVs: 53 contacts, 44 with the five columns filled. Then booted this branch's artifact on a copy of that DB. Before running the script, one contact was changed to have only city and country (blank street, state and postal code), and one had mailing_address set over REST to stand for an address someone entered after the upgrade.
  • Report-only: 9 with no old address (left empty) · 0 already converted · 1 whose mailing_address already holds a different value (kept) · 0 unmappable · 43 to convert. DB afterwards: mailing_address set only on the one pre-set row (nothing written).
  • --apply: Converted 43/43; 0 still without their address., exit 0. Read-back: full row → all five parts; the partial row → {"city":"Portland","country":"USA"}; the pre-set row kept its own value; an empty row stays null. The old columns are untouched (44 still filled).
  • Second --apply: 43 already converted · 0 to convert · Nothing to convert., exit 0.
  • A DB that never had the columns (fresh boot of this branch): the script says no contact has any of the five old columns, names the three possible reasons, and writes nothing (exit 0).

Ablation: a mapping target that names a part that does not exist

contact_import.mapping.ts target mailing_address.postalCode → mailing_address.planet, done through objectstack/scripts/ablation-replace.mjs (WRAP mode, restore on EXIT/INT/TERM) on the committed tree.

  • pnpm build exit 1: ✗ Author-time rules failed (1 issue) — Import mapping "crm_contact_import" writes target "mailing_address.planet", which names no field … "planet" is not a part of the address field "mailing_address": the parts a target may name on it are street, city, state, postalCode, country, countryCode, formatted.
  • pnpm validate exit 1, same diagnostic.
  • test/import-mappings.test.ts red: 1 failed / 38 passed, offender fieldMapping[12].target "mailing_address.planet" (unknown).
  • The first runtime leg did nothing. The build refused, so dist/ never got the mutation (marker count 0), and the boot inside that run served the earlier good artifact (44/6). That leg does not count. The import dry run was then measured on a copy of the built artifact with the same one-target edit (marker count 1 in the copy, 0 in the repo dist/), on a fresh boot: dryRun 400 INVALID_FIELD Unknown field 'mailing_address.planet' on object 'crm_contact' … the import is refused before any row, on the dry run and the commit alike. The commit gave the same 400. Contacts after: 9 (seed only, nothing imported).
  • Restore: blob after restore ce2e9ea3274d == blob at HEAD ce2e9ea3274d, git diff HEAD empty (tool verdict). git status clean afterwards.

Verification

  • OS_VERIFY_LOCK_SLOT=hotcrm-issue-1836 bash …/os-verify-lock.sh -c 'pnpm verify' at 85362cf0: os-verify-lock: VERDICT command-exit 0. Inside it: ✓ Validation passed; tsc --noEmit clean (--listFiles includes the new script and the edited test); lint 1 warning(s), 18 suggestion(s) (none on crm_contact); i18n gate 0; ✓ source hygiene clean; ✓ source token ratchet clean; ✓ Build complete; Test Files 176 passed (176) · Tests 3787 passed | 1 skipped (3788).
  • Token ratchet, src/sales, before (4807ed9e) → after: business semantics ~56,505 → ~56,418; interaction layer ~27,996 → ~27,968; authored total ~99,987 → ~99,882. No ceiling changed.
  • The served metadata matches the build (#21501): every boot ran from a cwd with no objectstack.config.ts and an explicit --artifact, and GET /api/v1/meta/object/crm_contact and …/mapping/crm_contact_import returned the converted field and the part targets. The browser-checked artifact is the 85362cf0 build (md5 ab0dc148…).
  • Not measured: the import through the console's import wizard. The 44/50 commit was measured on the platform import endpoint the wizard calls, on the same fresh boot.

Files outside the claim's file surface, and why

  • docs/STATUS.md — the pnpm validate summary figure (363 Fields → 359 Fields). test/docs-declared-versions.test.ts pins it.
  • content/docs/guides/importing-your-data{,.zh-Hans,.zh-Hant}.mdx and import-and-export{,.zh-Hans,.zh-Hant}.mdx — said contacts have separate address fields and that a structured address "cannot be assembled from separate spreadsheet columns". This change makes both false.
  • src/sales/mappings/account_import.mapping.ts (comment only) — its last sentence relied on the contact's mailing_* text fields, and its "cannot compose" claim is contradicted by the contact mapping now.
  • docs/requirements/0004-contact-buying-centre-map.md — named "the structured mailing_* block" (the R64 report's carrier note).
  • test/import-mappings.test.ts — the affected test. It now asks the platform's unknownImportMappingTargets (@objectstack/spec/data) instead of target in fields.

Acceptance notes

  • 6 of 50 shipped contact rows are rejected on main and on this branch alike: Department: "HR" is not a known option (rows 7, 15, 23, 31, 39, 47; the option is value hr, label Human Resources). Not touched here. Reported to the seat.
  • scripts/backfill-line-number.ts and scripts/backfill-owner-id.ts fail on the 17.6.0 pin at their first query. Measured on a fresh boot: Backfill failed: query crm_opportunity_line_item → 400: Invalid query request and Backfill failed: cannot read sys_user (400). 17.6.0's query schema refuses filters: [] (query.filters min_items) and a string sort: 'id asc' (query.sort invalid_shape). The new script uses neither. Reported to the seat, not fixed here.
  • The conversion depends on orphaned columns still coming back from an unprojected REST read. If the platform stops returning undeclared columns, a post-upgrade run can no longer see the old values. The script then says so and writes nothing. Reported to the seat.
  • The header of test/import-mappings.test.ts (point 1) still says a target naming no field is "not an error at import time". The ablation above measures the opposite on 17.6.0 (build, validate and the endpoint all refuse). Left as is because it is outside this card. Will be picked up by whoever next edits it.
  • Account and lead templates could now carry address columns mapped to parts, since the capability exists. Nobody has asked for that, so it is not done.

Generated by Claude Code

claude added 5 commits October 3, 2026 08:52
…ing_* fields

crm_contact now declares `mailing_address: Field.address()`, the shape
crm_account.billing_address and crm_lead.address already use. The import
mapping keeps the template's five Mailing columns and points each at a
declared part of the compound (`mailing_address.street` ... `.country`),
the target spelling @objectstack/spec 17.6.0's ImportFieldMappingSchema
documents. The contact form section renders the one field, and each locale
pack carries one label row instead of five.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ER8ntXZhYebyQ66aXWdjfT
`import-mappings` asserted `target in fields`, which a compound-part target
(`mailing_address.street`) can never satisfy. It now asks
`unknownImportMappingTargets` from @objectstack/spec/data, the verdict the
import endpoint applies before any row, so a declared part passes and an
undeclared one still fails. docs/STATUS.md carries the new field count the
`pnpm validate` summary prints (363 -> 359: five fields became one).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ER8ntXZhYebyQ66aXWdjfT
The contacts page in all three locales now describes one structured
Mailing Address instead of five separate fields. The import guides say the
contact template's five Mailing columns fill the parts of that one field,
and drop the claim that a structured address cannot be assembled from
columns, which the contact mapping now does. The account mapping's prose and
REQ-0004 no longer point at the retired `mailing_*` fields.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ER8ntXZhYebyQ66aXWdjfT
… columns

`scripts/backfill-contact-mailing-address.ts` reads the five retired
`mailing_*` columns, which an upgrade leaves in the database as orphaned
columns that an unprojected REST read still returns, and writes their
non-blank values as the parts of `mailing_address`. Report-only unless
`--apply`; writes only an empty `mailing_address`, never overwrites one a
person set after the upgrade, never touches the old columns, and a second
pass reports nothing to do. It must run before
`os migrate apply --allow-destructive`, which drops orphaned columns.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ER8ntXZhYebyQ66aXWdjfT
@vercel

vercel Bot commented Oct 3, 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 Oct 3, 2026 9:17am UTC

Request Review

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

Labels

ci/cd CI plumbing and the verification pipeline documentation Improvements or additions to documentation metadata Declarative metadata — schema, security posture, UI surfaces

Projects

None yet

2 participants