Skip to content

fix(service-datasource): fields of an object drafted from a datasource table carry a readable label - #30530

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-739vbl
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-739vbl

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Bill · project thread

domain:server#6 · os-bill · session_01Utp6CXCS98uhsFQYk64epE

Refs #30469 (item 1)

Clause-②: no
Metadata-protocol: none
Skills-docs: none — git grep -n -i -E "datasource (table )?(sync|import)|external object|import_object|generateObjectDraft|object draft" -- skills on aad627fd answers 1 line, skills/objectstack-data/SKILL.md:47 (the catalog entry for the datasources rule), and skills/objectstack-data/rules/datasources.md names no introspect, draft, import or field label; no published sentence describes the draft's field shape.

维护者速读

  • 改了什么:从外部数据源同步(导入)表生成的对象,每个字段现在都带可读的显示名,例如列 part_no → Part No、part_name → Part Name;对象自己的名称不变。
  • 为什么改:同步后新建记录表单直接显示数据库列名(part_no),而列表里读作人话,每个打开表单的成员都会看到原始列名。
  • 风险与回滚:只改生成草稿这一处,复用对象名称已在用的同一个转换函数;已导入的对象不回写,重新导入或手改才变。回滚即 revert 本 PR。
  • 先看:packages/services/service-datasource/src/external-datasource-service.ts(列循环里的 label: toLabel(fieldName)、lookup 保留原 label、源码渲染行),再看 src/__tests__/external-datasource-service.test.ts 新增的 generated field labels。
  • 两分钟验证:pnpm --filter @objectstack/service-datasource exec vitest run src/__tests__/external-datasource-service.test.ts -t "generated field labels",预期 3 条通过。
  • 需要你做的:无,席位落地。第 2 项(同步对象该住哪个包、是否随导出走)实测结果见下,ADR-0146 没有直接定论,由席位另立决策卡。

What changes

  • draftObject gives every generated field a label, derived by toLabel from the field name it is saved under, the same snake_case → Title Case function that already labels the object (toLabel(shortName)). No second derivation. The label follows the field name after rename and after the numbering of a taken name (PartNo beside part_no → part_no_2 → Part No 2), never the raw remote spelling.
  • A navigation that becomes a lookup keeps the label of the scalar column it replaces.
  • The rendered *.object.ts source writes the label on each field line (part_no: { type: 'text', label: 'Part No' },), quoted with the file's existing quoteTs.
  • importObject reuses the draft, so the persisted object carries the same labels. The object label is unchanged (control pin).
  • DraftField gains label: string.

Tests

  • New top-level describe('generated field labels') in the package's shared harness file external-datasource-service.test.ts: a spare_parts table (id, part_no, part_name, PartNo) yields Id, Part No, Part Name, Part No 2; the object label stays Spare Parts; a renamed column is labelled from its new name; the import persists the same labels.
  • Red on the merge base 0c7e95aff: 3 failed (expected { type: 'number' } to deeply equal { type: 'number', label: 'Id' }, expected undefined to be 'Catalogue Number', expected undefined to be 'Part No').
  • Eight existing pins asserted the old field shape exactly (toEqual({ type }), Object.keys(record) == ['type'], rendered field lines). They now name the label too; the primary-key pins still fail on any extra key, so the unauthorable primaryKey stays out.
  • Ablation (once, scripts/ablation-replace.mjs, anchor hit ×1, blob restored to HEAD, git diff HEAD empty): with the field written as { type } again, the 3 new pins go red.
  • On a booted showcase (scratch dogfood file, not committed): POST /datasources/showcase_external/external/tables/customers/import → 201, and GET /api/v1/meta/object/showcase_customers serves name: Name, email: Email, region: Region, lifetime_value: Lifetime Value.

Acceptance notes

Item 2: where a synced object lives, and whether it exports (measured, not built)

Reading on a booted showcase at fc8cea50 (this branch before the main merge; the incoming commits touch ui/** and one script only), after importing customers from the showcase's code-declared external datasource:

  • GET /api/v1/meta/object lists showcase_customers with _packageId: "sys_metadata", no _packageVersion. Control: showcase_task lists _packageId: "com.example.showcase", _packageVersion: "0.1.0". This matches the #30328 reading for a datasource added in Setup.
  • GET /api/v1/packages/export (what os package export reads) answers {"success":true,"data":{"packages":[]}}. The object is not in it. GET /api/v1/packages/com.example.showcase/export answers 200 and does not contain the object either.
  • Why: the import saves through the metadata door with no packageId (plugin.ts persistObject, writeFace: 'external-import'), so the row is a package-less sys_metadata row.

What ADR-0146 says about it:

  • D11 (Migrate): "A package-less row for an item no package ships is environment metadata, not an overlay, and is not touched." A synced object is an item no package ships, so today's placement is that class. Context item 3 states such a row "does not travel": no package export carries it.
  • D2 / D3 (ADR-0070 D1): "an item the customer adds 'to HotCRM' lives in a writable package", the base's BASE.custom. An import over a datasource a package declares takes that package's namespace prefix (showcase_customers), which reads as an item added to that package. The ADR does not say whether such an import is a customization of the base.

So the ADR does not settle it. For a datasource added in Setup (no package, no namespace), today's package-less placement matches D11 and the object does not export. For a datasource a package declares, D2 can be read as asking for BASE.custom. Nothing is moved here. This is recorded as an out-of-scope finding for the seat to take to a decision. That is why this PR says Refs, not a closing keyword.

Verification

Package suite and typecheck (final head)

pnpm --filter @objectstack/service-datasource run typecheck (tsc --noEmit, lists all 12 external-* test files) and vitest run --maxWorkers=2: Test Files 71 passed (71), Tests 1172 passed (1172).

Derived gates

Head aad627fdb (after the origin/main merge). node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 61 commands; each was run with its exit code recorded before any pipe, and --ran reconciles: 61 derived, 60 run, 1 NOT-MEASURED, 0 UNRUN.

  • 59 exit 0 on the first run.
  • node scripts/check-plugin-teardown-shape.mjs --self-test: exit 3 on the shallow checkout (it asks for history), exit 0 after git fetch origin 621a4876… (48 cases pass).
  • pnpm check:dual-build-cjs-loads: exit 3, PREREQUISITE NOT MET (it needs every package's dist, and only this card's closure was built). NOT MEASURED here; CI's Lint & Repo Gates runs it.
  • pnpm lint (outside the derivation): exit 0.
  • Per-file eslint --no-inline-config on the six changed TypeScript files: exit 0.

Generated by Claude Code

Each field of an object draft generated from a datasource table now gets a
label derived by toLabel (the same snake_case to Title Case derivation the
object's own label uses), from the field name it is saved under. A lookup
keeps the label of the column it replaces, and the rendered source carries it.

Claude-Session: https://claude.ai/code/session_01Utp6CZyxdo9F9Zo2bX81CM
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

4 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 103 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 3 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json fa9c0aa2a0a8c2752f83b57765f3aae978ae196a → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 8eff6617a8e403e98ced889a65561ff716ff2920 — the merge of head aad627fdb436933ad9815a003f7a969e66f973ef into base fa9c0aa2a0a8c2752f83b57765f3aae978ae196a, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 8eff6617a8e403e98ced889a65561ff716ff2920 && git checkout 8eff6617a8e403e98ced889a65561ff716ff2920
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fa9c0aa2a0a8c2752f83b57765f3aae978ae196a aad627fdb436933ad9815a003f7a969e66f973ef && git checkout -B drift-repro fa9c0aa2a0a8c2752f83b57765f3aae978ae196a && git merge --no-ff aad627fdb436933ad9815a003f7a969e66f973ef

node scripts/docs-audit/affected-docs.mjs --json fa9c0aa2a0a8c2752f83b57765f3aae978ae196a

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#6 · os-bill · session_01Utp6CXCS98uhsFQYk64epE

席位记录 · 2026-11-05T14:38Z · Head-sha aad627fd · ACCEPT

维护者速读(席位按 diff 校正)

  • 改了什么:从外部数据源导入(同步)表生成的对象,每个字段都带可读显示名,例如列 part_no 显示为 Part No;对象自己的名称不变。
  • 为什么改:同步后,新建记录表单直接显示数据库列名,成员每次打开表单都会看到原始列名(Datasource table sync: the generated object draft gives its fields no label, so forms show raw column names; where the synced object lives (package, export) is unstated #30469 第 1 项)。
  • 风险与回滚:只改生成草稿这一处,复用对象名称已在用的同一个 toLabel;已导入的对象不回写。回滚即 revert 本 PR。
  • 先看:external-datasource-service.ts 的三处改动:列循环里的 label: toLabel(fieldName)、lookup 沿用原字段 label、源码渲染行带 label。
  • 两分钟验证:pnpm --filter @objectstack/service-datasource exec vitest run src/__tests__/external-datasource-service.test.ts -t "generated field labels",3 条通过。
  • 需要你做的:无。非受管面、非 BREAKING,技能不要求你审,席位落地。第 2 项(导入对象该放哪个包、要不要随导出)已另开决策线程,呈你裁定。

席位意见:同意落地。label 从改名、编号之后的字段名推出,与对象 label 用同一个函数,没有第二套推导;lookup 替换时 hasOwnProperty 已守住原字段存在,fields[field]!.label 安全。

席位核对

  1. 形态:draft→ready,目标 main,Refs #30469 (item 1),没有闭合关键词(第 2 项待决,卡保持开);作者 objectstack-fleet[bot](中继);报告 mcp_calls 4 条全是读。
  2. 前提实测:origin/main 的 external-datasource-service.ts:918 仍是 fields[fieldName] = { type: fieldType }(席位 grep),字段没有 label;PR 在合并基 0c7e95aff 上新钉 3 条红,消融一次转红后已还原。
  3. 门禁 check-runs(钉 head aad627fd):48 条,35 success、13 skipped、0 失败;Lint & Repo Gates、TypeScript Type Check、Test Core 均 success(含报告里 NOT MEASURED 的 dual-build-cjs-loads,由 Lint & Repo Gates 跑过)。PR mergeable_state clean。
  4. 接受的偏差:PR 正文的声明行放在速读前面(按 os-dev.md 的顺序);没有设 assignee 或 reviewer,按派单交席位处理。都接受。
  5. 范围外去向:第 2 项实测(导入对象是 sys_metadata 中不属于任何包的行,os package export 不带它;ADR-0146 D11 与 D2/D3 的读法冲突)已转决策,席位倾向 B,决策线程已开。Skills-docs: none(grep 1 行,是目录条目,没有句子描述草稿的字段形状)。changeset patch,Clause-②: no,Metadata-protocol: none。
  6. 落地条款:check-governed-merges --pr 30530:NOT governed,146 行 ≤ 3000。全部 check 绿后 ready + auto-merge。合并后席位在卡上写落地,卡保持开等第 2 项裁定。

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 5, 2026 14:39
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 5, 2026 14:39
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 5, 2026
Merged via the queue into main with commit 898145b Nov 5, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-739vbl branch November 5, 2026 16:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant