Skip to content

fix(contact-import): the shipped contacts.csv imports all 50 rows ("HR" maps to the hr department) - #2001

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-1998-contacts-csv-hr-department
Oct 3, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-1998-contacts-csv-hr-department

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #1998
Clause-②: no. This is an app import mapping; it touches no published schema and no accept set.

What changed

  • src/sales/mappings/_shared.ts: a new DEPARTMENT_SYNONYMS = { 'HR': 'hr' } sits beside LEAD_SOURCE_SYNONYMS. The file header no longer claims that option values match case-insensitively. In @objectstack/core 17.6.0, matchOption compares the value exactly and only the label case-insensitively. That is why HR matched neither the value hr nor the label "Human Resources".
  • src/sales/mappings/contact_import.mapping.ts: the Department column now goes through transform: 'map' with params: { valueMap: DEPARTMENT_SYNONYMS }. The Lead Source column already uses this mechanism.
  • test/import-mappings.test.ts: one new it.each over the three shipped templates. Every data row goes through the import door's own steps from @objectstack/core: first applyMappingToRows with the object schema, then buildFieldMetaMap and coerceRow. The test expects no cell error. The header reader and the new row reader now share one quote-aware line splitter.
  • .changeset/1998-contacts-template-hr-department.md: 'hotcrm': patch.
  • assets/import-templates/contacts.csv is unchanged (git diff f44ab642..HEAD -- assets/ is empty). The template is the customer-facing contract that the ruling on crm_contact models a postal address as five flat text fields while crm_account and crm_lead use Field.address() — and AGENTS.md's own guidance names Field.address() for exactly this #1836 protects.

Which synonyms, and why only one

The template's Department column holds 8 distinct values: Executive ×7, Sales ×7, Marketing ×6, Engineering ×6, Support ×6, Finance ×6, HR ×6 and Operations ×6. crm_contact.department declares the values executive, sales, marketing, engineering, support, finance, hr and operations. Their labels are Executive, Sales, Marketing, Engineering, Support, Finance, Human Resources and Operations. Seven of the template values equal an option label, so the platform's case-insensitive label match lands them. Only HR matches neither a value (an exact match) nor a label. So the map has exactly one entry, and it adds no synonym that no template row uses.

Measured through the import door

The app ran under objectstack start -p 4818 on a fresh local database in the worktree. accounts.csv was imported first (50 of 50 created). Then the shipped contacts.csv was sent to POST /api/v1/data/crm_contact/import with mappingName: crm_contact_import:

run before (f44ab642) after (b66111ae)
dry run (runAutomations default) total 50 · ok 44 · errors 6 total 50 · ok 50 · errors 0
dry run (runAutomations: false) total 50 · ok 44 · errors 6 total 50 · ok 50 · errors 0
commit 44 created · 6 errors 50 created · 0 errors

Before the fix, all six errors were department invalid_option: Department: "HR" is not a known option, on rows 7, 15, 23, 31, 39 and 47. After the fix, each of those six contacts reads back by email with department: "hr". On the same query, control row 1 (ana.haddad@northwindtraders.example.com) reads back "executive". The served mapping (GET /api/v1/meta/mapping/crm_contact_import) carried {"source":"Department","target":"department","transform":"map","params":{"valueMap":{"HR":"hr"}}}, which equals dist/objectstack.json from the same build. Before the fix, the served entry was transform: "none" with no valueMap.

Ablation

The ablation ran from committed b66111ae with ablation-replace.mjs --file src/sales/mappings/_shared.ts --anchor " 'HR': 'hr',\n" --delete, wrapping the vitest run of test/import-mappings.test.ts under the shared lock:

  • The mutation landed: anchor count 1 → 0, blob b8616d6d2395 → 8ec71c8b8058.
  • Result: Tests 1 failed | 41 passed (42). The failing test is every cell of 'contacts.csv' coerces through 'crm_contact_import' with no error. It lists exactly the six rows the door reported, from row 7 department invalid_option: Department: "HR" is not a known option through row 47.
  • Restored: the blob equals HEAD (b8616d6d2395) and git diff HEAD is empty.

The test reads the mapping from source through objectstack.config.ts (vitest), not from a build. No dist step sits between the mutation and the run.

Verification

pnpm verify under the shared lock (slot hotcrm-issue-1998) at HEAD f10cae68:

  • os-verify-lock: VERDICT command-exit 0
  • validate ✓ · typecheck ✓ · lint reports 1 warning and 18 suggestions. None is on a mapping. The warning is the existing sales_home_page page:card warning. The i18n gate reports 0 issues, hygiene is clean, the token ratchet is clean and the build passes. Tests: Test Files 176 passed (176), Tests 3790 passed | 1 skipped (3791).

Token ratchet, src/sales rows:

  • before: other authored metadata 45 1,296 61,985 15,496 — and authored total 117 10,409 399,528 99,882 107,000
  • after: other authored metadata 45 1,304 62,174 15,544 — and authored total 117 10,417 399,717 99,929 107,000
  • The business semantics and interaction layer rows are unchanged (56,418 and 27,968). No ceiling was touched.

Acceptance notes

  • The customer guides still say picklists match labels "or codes, case-insensitively". This is in content/docs/guides/importing-your-data.mdx (How values are matched) and content/docs/guides/import-and-export.mdx (Picklists), and in the zh-Hans and zh-Hant copies of each. By matchOption's source in 17.6.0, a code is compared exactly, so an upper-cased code fails its row. This was not measured at the door beyond HR. It is not changed here because it is outside this card's file surface. It is noted for whoever next edits those guides.
  • _shared.ts points INDUSTRY_SYNONYMS at src/objects/_picklists.ts, but the file is src/sales/objects/_picklists.ts. It is left as is.
  • The token ratchet suggests re-anchoring the src/sales interaction-layer ceiling to about 30,000. That is not done here, and no ceiling is touched.

Generated by Claude Code

claude added 3 commits October 3, 2026 09:37
…ption

The shipped contacts.csv writes "HR" in its Department column. The import
path matches a picklist cell against the option VALUE exactly and the LABEL
case-insensitively, so "HR" matched neither `hr` nor "Human Resources" and
six of the template's fifty rows failed with invalid_option. The contact
mapping now runs Department through a DEPARTMENT_SYNONYMS value map, the
mechanism LEAD_SOURCE_SYNONYMS already uses; the template file is unchanged.
The _shared.ts header no longer claims values match case-insensitively.

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

The import-mappings suite compared each template's headers with its mapping
but never ran a row through it, so six "HR" rows of contacts.csv failed at the
import door while the suite stayed green. Each template's rows now go through
the import door's own mapping pipeline and cell coercion (@objectstack/core
applyMappingToRows + coerceRow) and the test expects no cell error. The
header reader shares one quote-aware line splitter with the new row reader.

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:46am 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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The shipped contacts.csv import template fails 6 of its 50 rows: Department: "HR" is not a known option (the option is hr, labelled "Human Resources")

2 participants