Skip to content

/import: an inline entry's uppercase/lowercase transform turns a nullValues sentinel into a stored value #25611

Closed

Description

@objectstack-fleet

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 main does, where the transform is dropped): the cell is blank, so it is stored unset, or takes the entry's defaultValue.
  • Got: N/A is stored. The entry's defaultValue is not applied, coerceCell's own null handling does not see it, and a required: true entry 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

  1. added
    bugSomething isn't working
    area:recordsBusiness objects, records, the views that show data, usable forms, search
    domain:serverDomain lane — seat card indexed by label:pm:seat
    on Oct 24, 2026
  2. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    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.

    Blocked-by: #25540


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01JVTd3PprStGa26aez5omLB
    Account: sunhaolin (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-25611-inline-transform-null-sentinel
    Worktree: objectstack-issue-25611
    Domain: domain:server
    Seat: domain:server#2
    File surface: packages/rest/src/import-prepare.ts (the inline entry rules loop, INLINE_TRANSFORMS then isBlank, near :281–:284 on origin/main), a test in packages/rest/src/, one changeset. ⛔ Not packages/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: medium
    Clause-②: no
    Metadata-protocol: none
    Responsibility: packages/rest's inline entry rules (PR #25540) run the case transform before the blank test, so a nullValues sentinel stops being blank | the platform already has the blank test (isBlank in core's import coercion) and the defaultValue / required rules that should see the raw blank | any import caller combining nullValues with an inline transform on 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 touches packages/rest/src/import-prepare.ts or import-mapping.ts (open PRs read this fire). Seat 1's #25582 is in flight on packages/core/src/utils/import-coerce.ts; this card stays out of that file.
    Premise: re-read on origin/main 481ac6211+: import-prepare.ts:281 applies INLINE_TRANSFORMS[rule.transform] before isBlank(value, nullValues) at :282 / :284. The defect is live on main.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  5. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #25646 comment 6384346051 (head a0b29be00d; lands once CI is green on that head) · domain:server seat 2 · session_01JVTd3PprStGa26aez5omLB


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #25646 → f123f75d4 · domain:server seat 2 · sunhaolin · session_01JVTd3PprStGa26aez5omLB · 2026-10-24T14:47Z

    reading result
    PR #25646 object merged: true, merge commit f123f75d41343d9b037c1003d567e47e2de6ae73, merged 2026-10-24T14:47:03Z through the merge queue
    origin/main f123f75d4 is an ancestor of origin/main (9dac9eb8d6), single parent (squash)
    queue branch no gh-readonly-queue/main/* head for PR 25646 remains
    • State: closed completed by Fixes #25611 on merge. pm:dispatched stripped in this act.
    • Delivered: an inline import entry now judges a cell blank on its raw spelling, before uppercase / lowercase runs. A nullValues sentinel such as n/a stays a sentinel, so it takes the entry's defaultValue, is refused by required, or is stored unset, instead of being stored as N/A. A non-blank cell is transformed as before. Core's isBlank is unchanged for every other import door.
    • Dev: a local subagent; no cloud session to archive.

    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions