Skip to content

content(blog): how do you import a spreadsheet from an old system? import mappings, dry runs, three non-match outcomes (slot 2026-10-25) - #283

Open
objectstack-fleet[bot] wants to merge 2 commits into
mainfrom
claude/pm-dispatch-objectos-ju9td1-issue-273
Open

objectstack-fleet[bot] wants to merge 2 commits into
mainfrom
claude/pm-dispatch-objectos-ju9td1-issue-273

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #273

Slot: Sun 2026-10-25, on the shared calendar on #267. Merge on the slot day only. Seat review. The seat found one visual defect: two cells in the three-ways diagram ran flush to their card edge. It was fixed in 3a2a799, which also fixed three more tight boxes found by a clearance scan. The seat re-viewed the diagram at native size. The measured "dry run of an update" limit stays in the post (writing rule 7). The upstream defect is filed separately in objectstack, and the post does not cite it. ACCEPT.

Slot Sun 2026-10-25 (date: 2026-10-25 in every locale, per the coordinator's calendar change during the run; the card said Tue 2026-10-27). Merge on the slot day only.

What this adds

  • content/blog/how-to-import-a-spreadsheet-from-an-old-system/ — the post in all eight locales (index.mdx, index.zh-Hans.mdx, index.zh-Hant.mdx generated by pnpm gen:zh-hant, index.ja.mdx, index.de.mdx, index.es.mdx, index.fr.mdx, index.ko.mdx), a cover and two diagrams, each as an English-text asset (cover-en.svg, mapping-is-a-projection-en.svg, three-ways-a-row-fails-to-match-en.svg) and a Chinese-text pair (cover.svg, mapping-is-a-projection.svg, three-ways-a-row-fails-to-match.svg). Topic integration-data, audience business, solution crm.
  • Ownable concept: the dry run is the review — a before/after count signed off before any row lands. The one number: a 3-row file reports created 3 from the dry run while the table holds 0 rows, 3 after the real run, updated 3 on the second run.
  • Back-links, one sentence plus updated: 2026-10-25T10:00:00+08:00 each: how-to-move-an-internal-system-to-metadata (English only, as published), extend-existing-systems-with-ai and manufacturing-legacy-systems-ai (all seven authored locales; zh-Hant regenerated).

Proof artifact — measured on the published @objectstack/cli 17.7.0

A fresh npm create objectstack@latest crm scaffold (Create ObjectStack v17.7.0; @objectstack/cli@17.7.0, @objectstack/spec@17.7.0, @objectstack/runtime@17.7.0; npm latest = 17.7.0) carrying the CRM example's Lead object from examples/app-crm/src/objects/lead.object.ts at tag @objectstack/cli@17.7.0 (its two lookups to crm_account / crm_opportunity left out, said in the post) and a defineMapping for crm_lead registered through mappings:. Server objectstack dev --no-watch -p 49180, dev admin session, every call POST /api/v1/data/crm_lead/import. Logs under scratchpad/issue-273/proof/ (run.log, run-dryrun-update.log, run-projection.log, run-413.log, run-saved.log, build-javascript.log); the app itself under scratchpad/issue-273/app/crm/.

Step Request Report (quoted) Rows in table after
Dry run mappingName: "crm_lead_feed", dryRun: true, 3 rows total 3, ok 3, errors 0, created 3, updated 0, skipped 0 0
Real run same body without dryRun created 3, an id on every row; source stored as web, partner, web 3
Same file again same body created 0, updated 3 3
Update mode writeMode: "update", 2 rows (one known, one new) updated 1, skipped 1; row 2 skipped, code: "NO_MATCH" 3
Edge rows, upsert unknown code Trade Show / label Event / not-an-email / blank email row 1 failed, field: "source", code: "invalid_option", "Lead Source: "Trade Show" is not a known option"; row 2 created (source event); row 3 failed, field: "email", code: "invalid_email"; row 4 created; ok 2, errors 2 5
Blank key, skipBlankMatchKey: true (dry run) same edge file row 4 skipped, code: "BLANK_MATCH_KEY" 5
Ambiguous one row loaded twice with writeMode: "insert", then upsert two records with one email; upsert row failed, code: "AMBIGUOUS_MATCH", "matchFields matched more than one crm_lead record" 7
Strict projection a second 3-row file (Channel cells are our own labels, plus a Legacy ID column): inline mapping dry run / real run; mappingName dry run inline: 0 of 3, each failed, code: "INVALID_FIELD", field: "Legacy ID", "Unknown field 'Legacy ID' on object 'crm_lead'" (dry run and real run alike); artifact: 3 of 3 created 7
First file through the inline rename (dry run) the Webform / Partner Referral file, inline mapping 3 of 3 failed, code: "invalid_option", field: "source", "Lead Source: "Webform" is not a known option" 7
Ceiling 5,001 rows, dry run / 5,000 rows, dry run 413 PAYLOAD_TOO_LARGE, "Import limit is 5000 rows per request (got 5001). Use an async import job for larger files." / 200, total 5000, ok 5000, created 5000 7
Runtime-saved mapping PUT /api/v1/meta/mapping/crm_lead_feed_saved (same shape, new name); dry run by that name; PUT to the packaged name 200, "Saved mapping 'crm_lead_feed_saved' (env-wide, state=active)"; created 3; 403 NOT_OVERRIDABLE 7
javascript transform, build time one entry switched to transform: 'javascript', objectstack build exit 2: "Mapping 'crm_lead_feed' uses transform 'javascript', which the import path does not execute (no server-side sandbox). Use none/constant/map/split/join/lookup, or model the logic as a flow." (restored; artifact hash identical before/after) —
javascript transform, runtime-saved PUT .../crm_lead_feed_js with a javascript entry, then import by name save 200; import 400 UNSUPPORTED_TRANSFORM 7
Whole-request refusals both mappingName and inline mapping / unknown name 400 CONFLICTING_MAPPING, "Provide either mappingName or an inline mapping, not both" / 404 MAPPING_NOT_FOUND, "No mapping artifact named "crm_lead_feed_nope" is registered" 7

Two things the card predicted came out differently and the post says what was measured: (1) a row with an unknown Channel code passes through the map unchanged, as documented, and then fails at select coercion with invalid_option naming source — it is not a row that lands unchanged; (2) the strict-projection contrast could not be shown on the card's 3-row file, because the inline route fails that file one step earlier (untranslated codes), so a second file was built whose only difference from a clean one is the junk column.

Every factual claim and its source line

objectstack at tag @objectstack/cli@17.7.0 (ba459d8d5); content/docs/data-modeling/import-mappings.mdx is byte-identical at the tag and at local origin/main (diff empty).

Claim in the post Source
"An import mapping is a named, reusable projection from the columns of a source file onto the fields of one object." import-mappings.mdx:7-8
first-class metadata kind; ships in a package or is saved at runtime; POST /api/v1/data/:object/import applies it by name with mappingName :8-10
for a single ad-hoc file the wizard's inline rename is enough; two mechanisms with different semantics :12-15
the defineMapping shape (name, label, sourceFormat, targetObject, fieldMapping[], transform: 'map' + valueMap, mode: 'upsert', upsertKey: ['email']); registered with mappings: [...] :22-43, :48-53; the post's own mapping, compiled and served (dist/objectstack.json, GET /api/v1/meta/mapping/crm_lead_feed answers it with _provenance: "package")
defineStack() validates while you build: an undefined targetObject fails the build; a javascript transform fails the build :56-61; measured exit 2 (above)
defineMapping() parses at import time; a misspelled key is rejected with the canonical spelling named :63-68
two origins (package / PUT /api/v1/meta/mapping/NAME); saved rows read first, packaged registry as fallback; packaged mappings locked against tenant edits; no per-organization overlay :74-86; measured 200 saved under a new name and 403 NOT_OVERRIDABLE under the packaged name
mode supplies writeMode when omitted; upsertKey supplies matchFields :96-100, :181-184
the seven TransformType values; "A value with no entry in the table passes through unchanged"; lookup pass-through; javascript "Refused. There is no server-side sandbox, and silently skipping a declared transform would corrupt data" :118-129; packages/core/src/utils/import-mapping.ts:221-226 (raw in valueMap ? valueMap[raw] : raw)
"For logic beyond these, transform the data before you post it, or model it as a flow" :131-132
the request and the report shape (object, dryRun, writeMode, total, ok, errors, created, updated, skipped, results[]) :136-169; packages/rest/src/rest-server.ts:9347-9358
"dryRun": true returns "the same report with nothing persisted; the verdict comes from the engine's own write-path validation" :176-177; packages/spec/src/api/export.zod.ts:274-282
runAutomations is a request key (named only, not described) :186-187
inline mapping vs mappingName mutually exclusive, 400 CONFLICTING_MAPPING; unmapped columns "pass through to the write path under their own header" vs "Dropped. The artifact is a strict projection: only mapped targets survive" :195-203; packages/core/src/utils/import-mapping.ts:151-152 ("Returns NEW rows containing only mapped targets")
whole-request refusals table (404 MAPPING_NOT_FOUND, 400 MAPPING_TARGET_MISMATCH, 400 UNSUPPORTED_TRANSFORM, 400 CONFLICTING_MAPPING, 413 PAYLOAD_TOO_LARGE over 5,000 rows) :215-224; packages/rest/src/import-mapping.ts:63-96; measured 404 / 400 / 413
per-row outcomes (created, updated, skipped NO_MATCH, skipped BLANK_MATCH_KEY, failed AMBIGUOUS_MATCH, failed with a field code); "A row that fails does not stop the import"; "a row that does not match is not one behaviour but three" :228-244; packages/core/src/utils/import-runner.ts:962-986
select labels resolve to stored values without a mapping, translated labels too; "A cell that cannot be coerced fails its own row with the offending column named" :254-265
ceilings 5,000 synchronous / 50,000 via POST /api/v1/data/:object/import/jobs, job id, poll, cancel/undo :271-274; measured 5,001 → 413, 5,000 → 200
the Lead object's source options web, referral, event, social, cold_outreach, partner; name, email, company fields examples/app-crm/src/objects/lead.object.ts:19-40, 57-67 at the tag
the CRM example declares no mappings: today (the post adds one) examples/app-crm/objectstack.config.ts:37-120 at the tag
npm create objectstack@latest installs 17.7.0 npm view @objectstack/cli dist-tags → latest: 17.7.0; scaffold banner "Create ObjectStack v17.7.0"
T4 standalone and E1 in the closing section, verbatim per locale src/lib/positioning.ts (T4_STANDALONE, CANONICAL.e1, and the zh-Hans / ja / de / es / fr / ko strings)
the installed skills bundle does not mention defineMapping (so the CTA points at the docs page) git grep defineMapping '@objectstack/cli@17.7.0' -- 'skills/**' → no hits

A measured limit the post states (and a finding for the ObjectStack team)

On 17.7.0, a dry run of rows that resolve to an update failed every row with code: "rule_violation" on the Lead object's lead_score_range script rule — "Validation rule 'lead_score_range' could not be evaluated (runtime: No such key: lead_score) — write rejected. The predicate reads 'lead_score', which this object does not declare" — while the real run of the same body answered updated 3, and a direct PATCH of one record with the same four fields answered 200. The object does declare lead_score; the preview evaluated the rule against the incoming cells alone. Previews of rows that are created agreed with the write in every run. The post keeps this as a version-named paragraph in "What this does not solve" (seat ruling A); the post does not cite the filing. Evidence: proof/run-dryrun-update.log A–D; server log lines "Validation rule 'lead_score_range' predicate failed to evaluate (runtime: No such key: lead_score". Contract it breaches: packages/core/src/utils/import-runner.ts:28-40 at the tag ("A dry run's contract is that it reports the verdict the real write produces"). Filed through the seat, not by this PR.

Gates (final tree, tip 3a2a799; two commits on lin/issue-273: bfc0687 the post, 3a2a799 the seat-review fix)

Gate Result Log
pnpm content:lint pass, 355 files; warnings only (title/description width, as the reference post; "Date is in the future" / "Updated is in the future" for the slot) scratchpad/issue-273/clint-5.log
pnpm content:lint --published pass (inside the build) build-5.log line 71
pnpm svg:lint OK — 124 SVGs, 2,703 text boxes, no crossing; a verbose clearance scan of the six new SVGs finds no text box under 12px from its card edge on any side (smallest right clearance in the three-ways cards: 20px) svglint-final-2.log, build-5.log line 72
pnpm check (via os-verify-lock.sh) VERDICT command-exit 0 — 160 files, 0 errors / 0 warnings / 0 hints check-final-3.log
pnpm build (via os-verify-lock.sh) VERDICT command-exit 0; content lint --dist passed (494 built blog pages); CJK line breaks, quotation marks and links passed; git status clean afterwards build-5.log
pnpm seo:smoke passed — 910 HTML pages, 17,517 prose blocks language-checked, 99,477 internal links seo-4.log

All descriptions ≤ 230 characters (en 224, zh-Hans 97, ja 121, de 228, es 221, fr 227, ko 133).

Browser check (actually looked at)

astro preview on 127.0.0.1:49181, Chromium via Playwright, script scratchpad/issue-273/shots.mjs, screenshots under scratchpad/www/shots-273/ (123 files, results.json). 265/265 assertions pass (shots-4.log, on the dist/ of 3a2a799): the article in en and zh-Hans at 1366×625, 1440×750, 1512×830, 1920×945, 390×844 and 360×800, light and dark, lazy images scrolled into view first — HTTP 200, no horizontal overflow outside a scroll container, no repeated image, the cover renders, every image loads; the blog list in en and zh-Hans at 1440 and 390, light and dark, links the new post; the three back-linked posts show the new link and an Updated line; every new SVG at native size on a light and a dark page, lowest text contrast 4.59:1 (first pass found 4.3:1 on the muted grey over the cream cards; lifted to 5.5:1+). Looked at: both covers light/dark, both diagrams light/dark, the article at 1440 light, 1920 dark, 390 dark, 360 light, the zh-Hans article at 1366, phone-width crops of a code block and the tables (horizontal scroll containers), the list page.

Deviations and notes for the seat


Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr

…mport mappings, dry runs, three non-match outcomes (slot 2026-10-25)

Post for #273, slate 1 of the daily cadence (#267), slot Sun 2026-10-25, in
all eight locales (zh-Hant generated by `pnpm gen:zh-hant`), with an English
and a Chinese cover and two diagrams in each script.

Every count in the post was measured on the published @objectstack/cli
17.7.0, on a fresh `npm create objectstack@latest` scaffold carrying the CRM
example's Lead object and a `defineMapping` for crm_lead: the dry run
reported created 3 with 0 rows stored, the real run stored 3, the same file
again updated 3; the same file through an inline rename landed 0 of 3
(INVALID_FIELD on the unmapped Legacy ID column); update mode gave NO_MATCH,
a duplicate gave AMBIGUOUS_MATCH, a blank key was created and, under
skipBlankMatchKey, BLANK_MATCH_KEY; 5,001 rows answered 413 and 5,000 rows
200; a javascript transform was refused by the build and, saved at runtime,
by the import request.

Back-links: how-to-move-an-internal-system-to-metadata (en only),
extend-existing-systems-with-ai and manufacturing-legacy-systems-ai (all
locales) each gain one link sentence and updated: 2026-10-25T10:00:00+08:00.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…dry-run limit without citing the filing

Seat review of #273. In three-ways-a-row-fails-to-match (en and zh) two cell
texts ran flush to the upsert column's right border — "skipBlankMatchKey:
true → skipped" and, in en, "AMBIGUOUS_MATCH, nothing written" — passing the
svg-text-fit gate only inside its 2px tolerance. Both are shortened so every
cell in both variants keeps at least 20px of clearance. The same scan found
the last line of each bottom box in mapping-is-a-projection (en and zh) 7px
above its card edge, and one zh record-card line 3px from the right edge: the
boxes grow to 178px and the line is shortened.

The "dry run of an update" paragraph stays, version-named, as the measured
limit writing rule 7 asks for. The sentence that referred to the measurement
being with the ObjectStack team is removed from every locale: the seat files
the upstream finding, and the post does not cite it. zh-Hant regenerated with
pnpm gen:zh-hant.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants