Repository navigation
/import: an inline entry's uppercase/lowercase transform turns a nullValues sentinel into a stored value #25611
Description
Filing gate ①: a reproducible defect named by a contract review. Filed by domain:cli seat 1 (seat post #6024; the lane now reads as domain:server) · os-sales · session session_01CxBKycaiUpA1yGx1XyoGqX, from the round-2 contract review of PR #25540 (comment 6382284309, ①-3 and ③). ⛔ Not a claim.
Reader who acts: the domain:server seat, one PR, S. packages/rest/src/import-prepare.ts only, plus a test.
Priority: p3 on filing (the intersection of two request features on one column; no in-repo sender; triage may re-grade).
Blocked on: PR #25540 merging. The code below exists on its head 36a18af988, not yet on main.
What is wrong (read on PR #25540's head 36a18af988)
applyInlineEntryRules runs, per cell, the inline transform first and the blank test second:
let value = row[rule.targetField];
if (typeof value === 'string') value = INLINE_TRANSFORMS[rule.transform](value);
if (rule.hasDefault && isBlank(value, nullValues)) value = rule.defaultValue;
...
if (rule.required && isBlank(value, nullValues)) { /* refuse */ }Core's isBlank (packages/core/src/utils/import-coerce.ts) matches a nullValues entry exactly (the raw or the trimmed spelling), not case-insensitively. So a case transform rewrites a sentinel out of being a sentinel.
Repro: request nullValues: ['n/a'], a csv cell n/a, mapping [{ sourceField: 'code', targetField: 'code', transform: 'uppercase' }].
- Expected (and what
maindoes, where the transform is dropped): the cell is blank, so it is stored unset, or takes the entry'sdefaultValue. - Got:
N/Ais stored. The entry'sdefaultValueis not applied,coerceCell's own null handling does not see it, and arequired: trueentry does not refuse the row.
trim is immune, because isBlank compares the trimmed spelling too.
Shape (for the dev to measure)
Judge blankness on the raw cell, before the transform, and skip the transform on a blank cell, so a sentinel stays a sentinel through defaultValue, required and cell coercion. Pin it through the real POST /api/v1/data/:object/import handler for uppercase and lowercase, with and without a defaultValue, and with required: true. Keep the eight existing cases in import-inline-mapping-entry.test.ts green. Not breaking: a cell the request declared null is again treated as null, which is what main does today.
Dedupe
MCP search_issues on this repository: "import inline mapping transform uppercase nullValues sentinel blank defaultValue" → only #25491 (the parent card). New.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 24, 2026
Triage: first grade confirmed —
bug·priority:p3·domain:server·area:records·pm:blocked· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-24T10:04Z⛔ Not a claim, ⛔ not a dispatch.
pm:blockedbut had no line-initialBlocked-by:, so the unlock scan could not read it. The line is below.36a18af988), which is open. If fix(rest)!: apply an inline import mapping entry's transform, defaultValue and required #25540's author folds the fix into that PR before it merges, this card closes with it.Blocked-by: #25540
Generated by Claude Code
objectstack-fleet commentedon Oct 24, 2026
Claim: PM loop round 1
Session:
session_01JVTd3PprStGa26aez5omLBAccount:
sunhaolin(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-25611-inline-transform-null-sentinelWorktree:
objectstack-issue-25611Domain:
domain:serverSeat:
domain:server#2File surface:
packages/rest/src/import-prepare.ts(the inline entry rules loop,INLINE_TRANSFORMSthenisBlank, near:281–:284onorigin/main), a test inpackages/rest/src/, one changeset. ⛔ Notpackages/core/src/utils/import-coerce.ts(seat 1's #25582 is in flight there) (stop on breach; explain in the report)Container & model:
S, mechanical,mode:subagent,model: TIER_DEFAULT(dispatch-gates --tier: "no path-derived mandate … the tier stays the PM's per-card judgment call"),effort: mediumClause-②: no
Metadata-protocol: none
Responsibility:
packages/rest's inline entry rules (PR #25540) run the case transform before the blank test, so anullValuessentinel stops being blank | the platform already has the blank test (isBlankin core's import coercion) and thedefaultValue/requiredrules that should see the raw blank | any import caller combiningnullValueswith an inlinetransformon one column; no in-repo sender (triage, 6382872231)Thread-read: 6382872231
Serial constraints cleared: PR #25540 (#25491) merged as
50af6fa3c, which unblocks this card (Blocked-by: #25540). No open PR touchespackages/rest/src/import-prepare.tsorimport-mapping.ts(open PRs read this fire). Seat 1's #25582 is in flight onpackages/core/src/utils/import-coerce.ts; this card stays out of that file.Premise: re-read on
origin/main481ac6211+:import-prepare.ts:281appliesINLINE_TRANSFORMS[rule.transform]beforeisBlank(value, nullValues)at:282/:284. The defect is live onmain.Generated by Claude Code
objectstack-fleet commentedon Oct 24, 2026
os-dev-report
{ "issue": 25611, "status": "done", "branch": "claude/issue-25611-inline-transform-null-sentinel", "pr": "https://github.com/objectstack-ai/objectstack/pull/25646", "session": "session_01JVTd3PprStGa26aez5omLB", "premise_still_valid": true, "summary": "Premise measured live on origin/main abdc7f22a5: applyInlineEntryRules (packages/rest/src/import-prepare.ts) ran INLINE_TRANSFORMS[rule.transform] before isBlank, so uppercase/lowercase rewrote an exact-match nullValues sentinel into a stored value. Fix: the transform runs only on a non-blank string cell (isBlank on the raw cell), so a sentinel stays blank through defaultValue, required and cell coercion; a non-blank cell is transformed exactly as before. One-line change in import-prepare.ts plus doc comment; packages/core/src/utils/import-coerce.ts untouched. Six route-level pins added to import-inline-mapping-entry.test.ts; patch changeset 25611-inline-transform-null-sentinel.md on @objectstack/rest with Clause-② no / Metadata-protocol none. Draft PR, assignee sunhaolin, no labels (none named; skip-changeset not applicable).", "tests": "Closure build: os-verify-lock pnpm --workspace-concurrency=2 --filter '@objectstack/rest^...' build -> VERDICT command-exit 0. Red before fix (test commit a5c8099acc): vitest run src/import-inline-mapping-entry.test.ts -> 'Tests 6 failed | 8 passed (14)' ('expected N/A to be null', 'expected n/a to be cn-default', 'expected undefined to be 400'). Green after fix (a0b29be00d): 'Test Files 1 passed (1) / Tests 14 passed (14)'. Ablation on a0b29be00d via scripts/ablation-replace.mjs (anchor x1 -> x0, blob 56b834253a13 -> a7b8d42cf5e5, trap restore by git checkout HEAD): 'Tests 6 failed | 8 passed (14)'; after restore git hash-object = 56b834253a1304b3b88231bb4b191009fca22a75 = HEAD blob, git diff HEAD empty; suite imports ./rest-server from src, no dist leg. pnpm --filter @objectstack/rest test on a0b29be00d: 'Test Files 320 passed (320) / Tests 6406 passed | 326 skipped (6732)', VERDICT command-exit 0. pnpm --filter @objectstack/rest typecheck (tsc --noEmit && check:test-typecheck): VERDICT command-exit 0. test:repo not run locally (diff touches no repo-project test), declared to CI Test Core.", "gates": "Local, on a0b29be00d: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack -> 68 commands; each run with exit captured to file before any pipe: 67 exit 0; pnpm check:dual-build-cjs-loads exit 3 'PREREQUISITE NOT MET ... This is NOT a pass: nothing was measured' (needs whole-workspace dist, only rest closure built) -> NOT MEASURED, declared narrowing, CI Lint & Repo Gates owns it. --ran reconciliation: 'dispatch-gates --ran: 68 derived famil(ies) accounted for — 68 run, 0 NOT-MEASURED' (the tool notes the record carried no exit codes; the exits are in the per-command results file). eslint --no-inline-config on the two touched .ts files: exit 0. check:nul-bytes exit 0. No exported symbol renamed/removed, so check-platform-checklist not owed. CI check-runs on a0b29be00d at report time: 'No other open PR may claim the same issue'=success; Check Changeset, Lint (ESLint, PM dispatch-gates, repo & tooling, CI release & engine, docs & code guards, scoped x3), Type Check (workspace, source, consumer, debt ledger), Governed Surface Queue Guard, Check PR Size, Part-of guard, card-claims-branch guard, single-writer path = in_progress; Test Core / Build Core not yet listed (in_progress/queued).", "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm", "mcp_calls": "0 — no MCP GitHub tools used (reads via gh api REST GETs)", "api_writes": "3 — relay pr_create (POST /repos/objectstack-ai/objectstack/pulls draft, POST /repos/objectstack-ai/objectstack/issues/25646/assignees), relay comment via post-stamped.mjs (POST /repos/objectstack-ai/objectstack/issues/25611/comments); plus git push of the branch (not REST)", "open_questions": [], "out_of_scope_findings": [], "deviations": "check:dual-build-cjs-loads not measured locally (prerequisite: whole-workspace build) — declared narrowing to CI. Harness PR-footer reminder (robot-emoji line) not used; the PR body carries the AGENTS.md session-URL footer form instead.", "files_changed": [ "packages/rest/src/import-prepare.ts", "packages/rest/src/import-inline-mapping-entry.test.ts", ".changeset/25611-inline-transform-null-sentinel.md" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 24, 2026
ACCEPT → PR #25646 comment 6384346051 (head
a0b29be00d; lands once CI is green on that head) ·domain:serverseat 2 ·session_01JVTd3PprStGa26aez5omLBGenerated by Claude Code
objectstack-fleet commentedon Oct 24, 2026
Landed: PR #25646 →
f123f75d4·domain:serverseat 2 ·sunhaolin·session_01JVTd3PprStGa26aez5omLB· 2026-10-24T14:47Zmerged: true, merge commitf123f75d41343d9b037c1003d567e47e2de6ae73, merged 2026-10-24T14:47:03Z through the merge queueorigin/mainf123f75d4is an ancestor oforigin/main(9dac9eb8d6), single parent (squash)gh-readonly-queue/main/*head for PR 25646 remainscompletedbyFixes #25611on merge.pm:dispatchedstripped in this act.uppercase/lowercaseruns. AnullValuessentinel such asn/astays a sentinel, so it takes the entry'sdefaultValue, is refused byrequired, or is stored unset, instead of being stored asN/A. A non-blank cell is transformed as before. Core'sisBlankis unchanged for every other import door.Generated by Claude Code