You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
/import: inline mapping entries' transform (uppercase / lowercase / trim …) is accepted and silently ignored #25491
ImportRequestSchema.mapping accepts the rich FieldMappingEntry[] form with transform: 'trim' | 'lowercase' | 'uppercase' | 'lookup' | 'date_format' | 'none', defaultValue and required. prepareImportRequest (packages/rest) flattens an inline array mapping to { sourceField: targetField } and drops everything else, so the request validates, the import runs, and no transform is applied.
The row fails the object's ^[0-9A-Z]{18}$ rule because the cell is still lowercase. Same for transform: 'lowercase' on an email column: ZHOU@X.EXAMPLE.COM and zhou@x.example.com are stored as two contacts, because matchFields: ['supplier', 'email'] compares them case-sensitively.
Expected
Either apply the entry's transform (and defaultValue / required) per cell before coercion, or refuse the request with a clear error that inline entries only support sourceField / targetField. Accepting it and doing nothing is the worst of the three.
Impact in hotsrm
Legacy supplier ledgers carry lowercase credit codes and mixed-case emails. Today the import guide tells operators to clean these columns by hand before importing (temporary measure).
Triage: lands in packages/rest/src/import-prepare.ts (prepareImportRequest, the inline-array mapping flattening); rationale: declared ≠ enforced: ImportRequestSchema.mapping accepts FieldMappingEntry.transform / defaultValue / required, and the inline path drops them silently. Apply them per cell before coercion (the named-mapping path already knows them), so the accepted contract is honoured rather than narrowed.
Path: records · 把客户的存量数据搬进来 | records-forms.import-transform-matrix (P2) | step ⑥
Grade: priority:p2 — inherits the P2 item; workaround (hand-cleaning) exists.
Type: Bug
Dedupe: checked against #25493 (cell coercion shapes, a different mechanism) and #25475/#25476 (import write modes) — distinct.
Session: session_01AkmasAfXt3cu83xGzkuKhe (objectstack triage, run on the maintainer's request in the project thread)
Claim: PM loop · domain:cli seat 1 · p2 · 2026-10-23T18:22Z
Session: session_01CxBKycaiUpA1yGx1XyoGqX
Account: os-sales (the seat's linked user; always the card's assignee)
Branch: claude/issue-25491-import-inline-transform
Worktree: objectstack-issue-25491
Domain: domain:server (this seat's side of the server lane under the coordinator's split: former cli and engine paths)
Seat: domain:cli#1 (seat post #6024; 由Sales账户的项目负责)
File surface (read on origin/maindc3e5b9d7b; stop on a breach): packages/rest/src/import-prepare.ts (prepareImportRequest, the inline-array mapping flattening), the shared transform it reuses if that lives in packages/rest, their tests, one changeset.
Container & model: S, mode:subagent, model: default tier
Clause-②: no
Basis for the Clause-② line: honouring fields the schema already accepts (FieldMappingEntry.transform / defaultValue / required); revised with an arm if the measured diff refuses a request that is accepted today.
Responsibility: n/a — reported from hotsrm#10, triaged 6370310580.
Thread-read: 6370310580
Serial constraints cleared: none of the 10 open PRs touches this surface (file lists read in this act).
{
"issue": 25491,
"status": "done",
"branch": "claude/issue-25491-import-inline-transform",
"pr": "https://github.com/objectstack-ai/objectstack/pull/25540",
"session": "session_01CxBKycaiUpA1yGx1XyoGqX",
"premise_still_valid": true,
"summary": "prepareImportRequest (packages/rest/src/import-prepare.ts) now reads each inline FieldMappingEntry's transform, defaultValue and required and applies them per cell after the rows are built and before coercion (csv, json rows, xlsx; sync import and async job alike). trim/uppercase/lowercase rewrite a string cell, none/lookup copy it (lookup still resolved downstream, the named path's semantics), defaultValue fills a blank cell (core's isBlank), a still-blank required cell refuses the request 400 MISSING_REQUIRED_FIELD before any row naming column and rows, and date_format (or any undeclared name) refuses 400 UNSUPPORTED_TRANSFORM naming it. Mechanism assumption measured false: the named path (core applyMappingToRows) implements the mapping artifact's none/constant/map/split/join/lookup vocabulary, and trim/uppercase/lowercase/date_format/defaultValue/required are implemented nowhere in the tree, so there was nothing to hand the entries to; the three string transforms live in one table in import-prepare.ts (no second implementation exists), no spec change, no file outside packages/rest. Card assignee was os-sales (not touched).",
"tests": "Head b5a3141e7f. New packages/rest/src/import-inline-mapping-entry.test.ts through the real POST /api/v1/data/:object/import route (ObjectQL + protocol + sqlite :memory:): card repro (uppercase credit code passes ^[0-9A-Z]{18}$ format rule, upsert on credit_code), lowercase email merge under matchFields [supplier, email] (created 1 then updated 1, one row stored), trim with trimWhitespace false, defaultValue (blank cell + missing column), required refusal on dry run and commit with 0 rows written, required satisfied by defaultValue, date_format 400 UNSUPPORTED_TRANSFORM, control plain {sourceField,targetField} unchanged: 'Tests 8 passed (8)', lock VERDICT command-exit 0. Whole package `pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2`: 'Test Files 322 passed (322) / Tests 6616 passed | 344 skipped', VERDICT command-exit 0 (source unchanged after; later commits are changeset-only). Ablation via scripts/ablation-replace.mjs (suite resolves ./rest-server from src, no dist leg): anchor 'if (entryRules.length > 0) {' x1 -> x0, blob c170dc3885 -> ed352b27ee, 5 of 8 red (uppercase, lowercase, trim, defaultValue, required), 3 green as expected (parse-time date_format refusal, required+default, control); restored blob == HEAD c170dc3885, git diff HEAD empty.",
"gates": "Head b5a3141e7f. `pnpm --filter '@objectstack/rest^...' build` VERDICT command-exit 0; `pnpm --filter @objectstack/rest build && pnpm --filter @objectstack/rest typecheck` (tsc + check:test-typecheck, 0 debt) VERDICT command-exit 0. `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran`: '67 derived famil(ies) accounted for — 65 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3)', exit 0; all 65 exit 0 (check-adr-0087-registration first red on the arrow in the changeset body, reworded, exit 0 at b5a3141e7f; check-changeset-no-major, check-empty-changeset, check-changeset-fixed rerun at b5a3141e7f exit 0). NOT MEASURED: pnpm check:dual-build-cjs-loads, reason: PREREQUISITE NOT MET exit 3 (whole-workspace build). NOT MEASURED: pnpm check:type-check-debt, reason: its closure build takes the verify lock and the wait budget expired behind a sibling's dogfood holder (exit 3). NOT MEASURED: pnpm lint (repo-wide), reason: three verify-lock queue-timeouts (VERDICT exit 99) behind pid 19029 (#25482's dogfood run, held 1673s+); declared narrowing: `pnpm exec eslint --no-inline-config packages/rest/src/import-prepare.ts packages/rest/src/import-inline-mapping-entry.test.ts` exit 0. Extra outside derivation: check-changeset-fixed exit 0, check:error-code-casing exit 0. CI check-runs on PR #25540: in_progress at report time (not awaited). origin/main moved 11 commits; none touches packages/rest, packages/core/src/utils or the spec import schema; merge-tree clean; no merge taken.",
"dogfood_battery": "b5a3141e7f · os-verify-lock: VERDICT command-exit 0 · held the lock 2008s · Test Files 290 passed | 3 skipped (293) · Tests 2494 passed | 18 skipped (2512) · no failures",
"line_budget": "+365 / -1 over 3 files (import-prepare.ts +119/-1, test +220, changeset +26): past the ~300-line S guide, almost all of it the route-level test file (8 cases with its own engine boot); implementation is ~120 lines.",
"files_changed": [
"packages/rest/src/import-prepare.ts",
"packages/rest/src/import-inline-mapping-entry.test.ts",
".changeset/25491-import-inline-mapping-entry-rules.md"
],
"deviations": [
"Reuse route not available: the named-mapping path implements none of trim/uppercase/lowercase/date_format/defaultValue/required (it serves the mapping artifact's other vocabulary), so the string transforms were implemented once in import-prepare.ts (none exists elsewhere) and none/lookup keep the named path's copy semantics; blank test reuses core isBlank.",
"date_format refused 400 UNSUPPORTED_TRANSFORM (no format exists on FieldMappingEntry); a transform name outside the declared set is refused the same way.",
"required miss refuses the whole request (400 MISSING_REQUIRED_FIELD, rows named) instead of failing rows: the core runner has no pre-failed-row seam and core is outside the file surface.",
"Clause-② moved from `no` to `no (narrowing)` (date_format and blank-required requests that ran are now refused); title carries `!`, changeset patch with BREAKING banner and adr-0087 not-required (no-migration-prescription) marker; Metadata-protocol: none.",
"Repo-wide pnpm lint not run (lock contention); narrowed to the changed files and declared.",
"PR body opens with the dispatch-mandated HTML attribution comment, conflicting with the role file's no-HTML-comment rule; followed the dispatch for that one platform marker, read back byte-identical (6825 sent / 6825 stored). `Fixes #25491` is line 4, after the two mandated lines."
],
"mcp_calls": "0",
"api_writes": "2 relay strokes via scripts/pm/fleet-write/dispatch.mjs (each POST /repos/objectstack-ai/objectstack/dispatches): (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls + POST /repos/objectstack-ai/objectstack/issues/25540/assignees; (2) comment = POST /repos/objectstack-ai/objectstack/issues/25491/comments (this report). Plus git push (not REST).",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none · noted, not filed — packages/rest/src/import-mapping.ts docblock still calls the inline mapping a plain rename (accurate for the compact record form and plain entries); listed in PR Acceptance notes."
]
}
merged: true, merge commit 50af6fa3ca, merged 2026-10-24T10:10:39Z through the merge queue
origin/main
50af6fa3ca is an ancestor of origin/main, single parent (squash)
queue branch
no gh-readonly-queue/main/* head for PR 25540 remains
State: card closed completed by the PR's Fixes #25491. pm:dispatched stripped in this act. The round-2 review's finding is filed as #25611 and moves from pm:blocked to pm:queue in this act, for another domain:server seat (this seat claims no new cards).
Activity
objectstack-fleet commentedon Oct 23, 2026
Triage: lands in
packages/rest/src/import-prepare.ts(prepareImportRequest, the inline-array mapping flattening); rationale: declared ≠ enforced:ImportRequestSchema.mappingacceptsFieldMappingEntry.transform/defaultValue/required, and the inline path drops them silently. Apply them per cell before coercion (the named-mapping path already knows them), so the accepted contract is honoured rather than narrowed.Path: records · 把客户的存量数据搬进来 | records-forms.import-transform-matrix (P2) | step ⑥
Grade:
priority:p2— inherits the P2 item; workaround (hand-cleaning) exists.Type: Bug
Dedupe: checked against #25493 (cell coercion shapes, a different mechanism) and #25475/#25476 (import write modes) — distinct.
Session:
session_01AkmasAfXt3cu83xGzkuKhe(objectstack triage, run on the maintainer's request in the project thread)Generated by Claude Code
objectstack-fleet commentedon Oct 23, 2026
Claim: PM loop ·
domain:cliseat 1 · p2 · 2026-10-23T18:22ZSession:
session_01CxBKycaiUpA1yGx1XyoGqXAccount:
os-sales(the seat's linked user; always the card's assignee)Branch:
claude/issue-25491-import-inline-transformWorktree:
objectstack-issue-25491Domain:
domain:server(this seat's side of the server lane under the coordinator's split: former cli and engine paths)Seat:
domain:cli#1(seat post #6024; 由Sales账户的项目负责)File surface (read on
origin/maindc3e5b9d7b; stop on a breach):packages/rest/src/import-prepare.ts(prepareImportRequest, the inline-array mapping flattening), the shared transform it reuses if that lives inpackages/rest, their tests, one changeset.Container & model:
S,mode:subagent,model: default tierClause-②: no
Basis for the Clause-② line: honouring fields the schema already accepts (
FieldMappingEntry.transform/defaultValue/required); revised with an arm if the measured diff refuses a request that is accepted today.Responsibility: n/a — reported from hotsrm#10, triaged
6370310580.Thread-read: 6370310580
Serial constraints cleared: none of the 10 open PRs touches this surface (file lists read in this act).
Generated by Claude Code
objectstack-fleet commentedon Oct 23, 2026
os-dev-report
{ "issue": 25491, "status": "done", "branch": "claude/issue-25491-import-inline-transform", "pr": "https://github.com/objectstack-ai/objectstack/pull/25540", "session": "session_01CxBKycaiUpA1yGx1XyoGqX", "premise_still_valid": true, "summary": "prepareImportRequest (packages/rest/src/import-prepare.ts) now reads each inline FieldMappingEntry's transform, defaultValue and required and applies them per cell after the rows are built and before coercion (csv, json rows, xlsx; sync import and async job alike). trim/uppercase/lowercase rewrite a string cell, none/lookup copy it (lookup still resolved downstream, the named path's semantics), defaultValue fills a blank cell (core's isBlank), a still-blank required cell refuses the request 400 MISSING_REQUIRED_FIELD before any row naming column and rows, and date_format (or any undeclared name) refuses 400 UNSUPPORTED_TRANSFORM naming it. Mechanism assumption measured false: the named path (core applyMappingToRows) implements the mapping artifact's none/constant/map/split/join/lookup vocabulary, and trim/uppercase/lowercase/date_format/defaultValue/required are implemented nowhere in the tree, so there was nothing to hand the entries to; the three string transforms live in one table in import-prepare.ts (no second implementation exists), no spec change, no file outside packages/rest. Card assignee was os-sales (not touched).", "tests": "Head b5a3141e7f. New packages/rest/src/import-inline-mapping-entry.test.ts through the real POST /api/v1/data/:object/import route (ObjectQL + protocol + sqlite :memory:): card repro (uppercase credit code passes ^[0-9A-Z]{18}$ format rule, upsert on credit_code), lowercase email merge under matchFields [supplier, email] (created 1 then updated 1, one row stored), trim with trimWhitespace false, defaultValue (blank cell + missing column), required refusal on dry run and commit with 0 rows written, required satisfied by defaultValue, date_format 400 UNSUPPORTED_TRANSFORM, control plain {sourceField,targetField} unchanged: 'Tests 8 passed (8)', lock VERDICT command-exit 0. Whole package `pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2`: 'Test Files 322 passed (322) / Tests 6616 passed | 344 skipped', VERDICT command-exit 0 (source unchanged after; later commits are changeset-only). Ablation via scripts/ablation-replace.mjs (suite resolves ./rest-server from src, no dist leg): anchor 'if (entryRules.length > 0) {' x1 -> x0, blob c170dc3885 -> ed352b27ee, 5 of 8 red (uppercase, lowercase, trim, defaultValue, required), 3 green as expected (parse-time date_format refusal, required+default, control); restored blob == HEAD c170dc3885, git diff HEAD empty.", "gates": "Head b5a3141e7f. `pnpm --filter '@objectstack/rest^...' build` VERDICT command-exit 0; `pnpm --filter @objectstack/rest build && pnpm --filter @objectstack/rest typecheck` (tsc + check:test-typecheck, 0 debt) VERDICT command-exit 0. `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran`: '67 derived famil(ies) accounted for — 65 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3)', exit 0; all 65 exit 0 (check-adr-0087-registration first red on the arrow in the changeset body, reworded, exit 0 at b5a3141e7f; check-changeset-no-major, check-empty-changeset, check-changeset-fixed rerun at b5a3141e7f exit 0). NOT MEASURED: pnpm check:dual-build-cjs-loads, reason: PREREQUISITE NOT MET exit 3 (whole-workspace build). NOT MEASURED: pnpm check:type-check-debt, reason: its closure build takes the verify lock and the wait budget expired behind a sibling's dogfood holder (exit 3). NOT MEASURED: pnpm lint (repo-wide), reason: three verify-lock queue-timeouts (VERDICT exit 99) behind pid 19029 (#25482's dogfood run, held 1673s+); declared narrowing: `pnpm exec eslint --no-inline-config packages/rest/src/import-prepare.ts packages/rest/src/import-inline-mapping-entry.test.ts` exit 0. Extra outside derivation: check-changeset-fixed exit 0, check:error-code-casing exit 0. CI check-runs on PR #25540: in_progress at report time (not awaited). origin/main moved 11 commits; none touches packages/rest, packages/core/src/utils or the spec import schema; merge-tree clean; no merge taken.", "dogfood_battery": "b5a3141e7f · os-verify-lock: VERDICT command-exit 0 · held the lock 2008s · Test Files 290 passed | 3 skipped (293) · Tests 2494 passed | 18 skipped (2512) · no failures", "line_budget": "+365 / -1 over 3 files (import-prepare.ts +119/-1, test +220, changeset +26): past the ~300-line S guide, almost all of it the route-level test file (8 cases with its own engine boot); implementation is ~120 lines.", "files_changed": [ "packages/rest/src/import-prepare.ts", "packages/rest/src/import-inline-mapping-entry.test.ts", ".changeset/25491-import-inline-mapping-entry-rules.md" ], "deviations": [ "Reuse route not available: the named-mapping path implements none of trim/uppercase/lowercase/date_format/defaultValue/required (it serves the mapping artifact's other vocabulary), so the string transforms were implemented once in import-prepare.ts (none exists elsewhere) and none/lookup keep the named path's copy semantics; blank test reuses core isBlank.", "date_format refused 400 UNSUPPORTED_TRANSFORM (no format exists on FieldMappingEntry); a transform name outside the declared set is refused the same way.", "required miss refuses the whole request (400 MISSING_REQUIRED_FIELD, rows named) instead of failing rows: the core runner has no pre-failed-row seam and core is outside the file surface.", "Clause-② moved from `no` to `no (narrowing)` (date_format and blank-required requests that ran are now refused); title carries `!`, changeset patch with BREAKING banner and adr-0087 not-required (no-migration-prescription) marker; Metadata-protocol: none.", "Repo-wide pnpm lint not run (lock contention); narrowed to the changed files and declared.", "PR body opens with the dispatch-mandated HTML attribution comment, conflicting with the role file's no-HTML-comment rule; followed the dispatch for that one platform marker, read back byte-identical (6825 sent / 6825 stored). `Fixes #25491` is line 4, after the two mandated lines." ], "mcp_calls": "0", "api_writes": "2 relay strokes via scripts/pm/fleet-write/dispatch.mjs (each POST /repos/objectstack-ai/objectstack/dispatches): (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls + POST /repos/objectstack-ai/objectstack/issues/25540/assignees; (2) comment = POST /repos/objectstack-ai/objectstack/issues/25491/comments (this report). Plus git push (not REST).", "open_questions": [], "out_of_scope_findings": [ "carrier: none · noted, not filed — packages/rest/src/import-mapping.ts docblock still calls the inline mapping a plain rename (accurate for the compact record form and plain entries); listed in PR Acceptance notes." ] }objectstack-fleet commentedon Oct 24, 2026
ACCEPT → PR #25540 comment 6382301638 ·
domain:cliseat 1 · sessionsession_01CxBKycaiUpA1yGx1XyoGqX· 2026-10-24T09:02Z · contract review round 2 PASS (6382284309) · landing held by the release freeze · review finding filed as #25611Generated by Claude Code
objectstack-fleet commentedon Oct 24, 2026
Landed ·
domain:cliseat 1 · sessionsession_01CxBKycaiUpA1yGx1XyoGqX· 2026-10-24T10:11Zmerged: true, merge commit50af6fa3ca, merged 2026-10-24T10:10:39Z through the merge queueorigin/main50af6fa3cais an ancestor oforigin/main, single parent (squash)gh-readonly-queue/main/*head for PR 25540 remainsState: card closed
completedby the PR'sFixes #25491.pm:dispatchedstripped in this act. The round-2 review's finding is filed as #25611 and moves frompm:blockedtopm:queuein this act, for anotherdomain:serverseat (this seat claims no new cards).Generated by Claude Code