Skip to content

fix(studio): stop offering the dead field.externalId toggle, and keep a stored value through every save - #27315

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-1898jl
Oct 29, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-1898jl

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread
Fixes #26674

Claim: 6471969865
Clause-②: no
Metadata-protocol: none

Studio stops offering the key; the protocol keeps parsing it. This is not a retirement: no tombstone, no narrowing, no schema, type or export change. FieldSchema accepts externalId exactly as before, and a field that already carries externalId: true keeps it through every Studio save.

维护者速读

  • 改了什么: Studio 的字段元数据表单、字段设计器抽屉、字段检查器三处不再显示「External ID」开关和「用于 upsert」的帮助文字;已经存了 externalId: true 的字段,在 Studio 里编辑保存后原样保留。
  • 为什么改: field.externalId 已判为 dead,任何 upsert、导入、种子都不按它匹配。开关让人以为重新导入会按它更新而不是新增,结果静默产生重复记录,os lint 还会对表单自己给出的值报警告。
  • 风险与回滚: 低。协议照旧解析该键,只删 UI 控件和只被它用到的文案;设计器更新路径改为靠展开保留旧值,三个保存面各有往返测试钉住。回滚 = revert 本 PR。
  • 先看: ui/packages/plugin-designer/src/FieldDesigner.tsx(更新路径不再写 externalId)、ui/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx(删复选框)、packages/spec/src/data/field.form.ts(删表单行)。
  • 两分钟验证: cd ui && pnpm exec vitest run packages/plugin-designer/src/MetadataFieldsPage.externalIdRoundTrip-26674.test.tsx,预期 2 个用例全绿:编辑保存后 PUT 里两个字段的 externalId 仍是 true。
  • 需要你做的: 无,席位落地。

What changes

Measured first on origin/main 2302b92b: three Studio surfaces offered the toggle.

Surface Before After
Spec field metadata form (packages/spec/src/data/field.form.ts, Advanced section) row externalId, help text "Mark as external ID for upsert operations", repeated in all four *.metadata-forms.generated.ts bundles (en "External Id" / zh-CN "外部 ID" / ja-JP / es-ES) row removed; bundles regenerated with node scripts/check-i18n-bundles.mjs --write --filter=platform-objects (16 lines removed, nothing else moved)
Field Designer drawer (ui/packages/plugin-designer/src/FieldDesigner.tsx, Advanced section; used by MetadataFieldsPage.tsx) externalId boolean control; update path wrote externalId: data.externalId === true control and its initial value removed; the update path no longer names the key, so ...editingField carries a stored value through; a created field no longer carries externalId: false
Field inspector (ui/packages/app-shell/.../inspectors/ObjectFieldInspector.tsx, Advanced section) External ID checkbox removed; patchDef spreads the whole stored definition, so the key survives every edit
  • i18n strings only the toggle used: appDesigner.fieldDesigner.externalId removed from all ten ui/packages/i18n/src/locales/*.ts packs and from the plugin-designer fallback table (useDesignerTranslation.ts); designer.field.externalId removed from the Studio namespace (en, zh).
  • MetadataFieldsPage.tsx is unchanged. Its toDesignerField / fromDesignerField pair already carries externalId both ways; with the drawer now preserving it, the round-trip holds, and a test pins it end to end.
  • Form ↔ Zod reconciliation: externalId is recorded as a deliberate root omission of the field form, with its reason (graded dead, the schema still parses it, the seed dataset's externalId and an import mapping's upsertKey are the real match keys).
  • Changesets (patch, one per touched published package): @objectstack/spec, @objectstack/platform-objects, @object-ui/plugin-designer, @object-ui/i18n. @object-ui/app-shell is private, so it gets none.
  • Liveness evidence (packages/spec/liveness/field.json, row externalId, claim amendment 6474597348): the evidence said Studio's inspector and designer edit the key, which this PR makes false. It now says the protocol still parses the key, no Studio surface offers it (form row, designer drawer, inspector), and the ui/ mentions only carry a stored value through a save. Prose only: the grade stays dead, verifiedAt and authorHint are unchanged.
  • Not governed: no skills/**, content/docs/**, .claude/** or other register path is touched.

Tests

  • packages/spec/src/data/field-form-external-id-not-offered.test.ts (new, 4 cases): the field form declares no externalId row at any depth (with a positive control that the walk reaches storage); no help text still calls a field flag an upsert key; FieldSchema still parses an authored externalId: true; the liveness row field.externalId still reads dead.
  • ui/packages/plugin-designer/src/__tests__/FieldDesigner.externalIdNotOffered-26674.test.tsx (new, 4 cases): no externalId control in the create or edit drawer (each paired with the trackHistory sibling as positive control); a field with externalId: true keeps it through the drawer's edit-and-save; a created field carries no externalId key.
  • ui/packages/plugin-designer/src/MetadataFieldsPage.externalIdRoundTrip-26674.test.tsx (new, 2 cases): the REAL FieldDesigner over a real MetadataClient and a fetch double; after an edit-and-save the PUT keeps externalId: true on the edited field and on an untouched one, a field without the flag gains none, and every PUT field parses through FieldSchema.
  • ui/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.externalIdNotOffered-26674.test.tsx (new, 3 cases): no External ID control (with its Advanced siblings rendering), a stored value does not resurrect it, and a label edit keeps externalId: true in the saved fields map.
  • Moved pins: metadata-form-zod-reconciliation.test.ts gains the field root omission row; object-lifecycle-panel-echo-decisions.test.ts translated-leaf positive control 695 → 694 (one row label fewer, per locale); ObjectFieldInspector.retiredIndexed.test.tsx drops External ID from its list of Advanced siblings.

Acceptance notes

  • DesignerFieldDefinition.externalId (@object-ui/types), its Zod mirror and MetadataService's field summary are unchanged on purpose: they carry the stored value, which is what keeps it from being stripped.
  • content/docs/** and skills/** already describe the field flag as "Not read" and teach the seed dataset's externalId, so nothing there became false.

Verification

All readings at head b357ce35 unless stated; local runs under scripts/pm/os-verify-lock.sh.

Round r1 (head 0b08d306, the liveness evidence prose only): pnpm --filter @objectstack/spec check:liveness exit 0 (80 dead, 1219 classified); pnpm --filter @objectstack/spec test 749 passed + 1 skipped of 750 files, 22379 passed, 0 failed.

Package tests and type-checks
  • pnpm --filter @objectstack/spec test at 5570dc7a: 749/750 files, 22380 passed, 1 failed (metadata-form-zod-reconciliation, the expected root-omission ripple). Fixed in b357ce35; that file, the new spec test and metadata-form-declared-rows.pin.test.ts then 3/3 files, 91/91 tests.
  • pnpm --filter @objectstack/spec typecheck: exit 0.
  • pnpm --filter @objectstack/platform-objects test at 5570dc7a: 95/96 files, 1360 passed, 1 failed (the 695 leaf-count pin). Fixed in b357ce35; that file 35/35.
  • pnpm --filter @objectstack/platform-objects typecheck: exit 0.
  • cd ui && pnpm exec vitest run packages/plugin-designer/ packages/i18n/: 139/139 files, 1833 passed, 13 skipped.
  • app-shell, narrowed to the 40 test files that import ObjectFieldInspector (the full app-shell suite is CI's Test UI): 40/40 files, 459/459 tests.
  • pnpm exec turbo run type-check --filter=@object-ui/plugin-designer --filter=@object-ui/i18n --filter=@object-ui/app-shell: 66/66 tasks, each of the three type-checks executed (cache miss).
  • pnpm --dir ui run check:designer-field-key-parity, check:i18n-keys, check:i18n-drift: exit 0 each (drift: 1 key removed, 0 values changed).
  • node scripts/check-i18n-bundles.mjs (pnpm check:i18n): OK, 10 packages in sync.
Ablation (the update-path change)

node scripts/ablation-replace.mjs put externalId: data.externalId === true back into FieldDesigner.handleFormSuccess's update path (anchor 1 → 0, blob a0985e17 → 6a7c1f3e). The two round-trip cases went red, exactly those two: FieldDesigner … keeps externalId: true through the update path and MetadataFieldsPage … keeps externalId: true on the edited field and on an untouched one (expected false to be true); 4 other cases stayed green. Restored: blob equals HEAD, git diff HEAD empty.

Derived gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 85 families. Reconciliation with --ran: 85 derived, 85 run, 0 NOT-MEASURED, 0 UNRUN; every recorded exit is 0. On the first pass nine families ran while a turbo build was rewriting packages/spec/dist (exit 3 / timeout) and were re-run on a settled tree, exit 0 each.

Then all 85 re-run once more, serially, on the final head b357ce35 with nothing else running: 85/85 exit 0; --ran again 85 derived, 85 run, 0 NOT-MEASURED, 0 UNRUN (dispatch-gates --ran exit 0).


Generated by Claude Code

The field form row, the designer drawer switch and the field inspector
checkbox are gone; a stored externalId: true is carried through every
Studio save untouched. Drops the i18n strings only the toggle used.

Claude-Session: https://claude.ai/code/session_01WGufzg4R646jZ5Ky7hqq9L
Co-authored-by: Claude <noreply@anthropic.com>
…ut field.externalId; changesets

Claude-Session: https://claude.ai/code/session_01WGufzg4R646jZ5Ky7hqq9L
Co-authored-by: Claude <noreply@anthropic.com>
…eaf count 695 -> 694

Claude-Session: https://claude.ai/code/session_01WGufzg4R646jZ5Ky7hqq9L
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/platform-objects, @objectstack/spec, touching 5 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/liveness/field.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/field-types.mdx (via externalId (literal, a string literal in sections))
  • content/docs/data-modeling/fields.mdx (via externalId (literal, a string literal in sections))
  • content/docs/data-modeling/seed-data.mdx (via externalId (literal, a string literal in sections))
  • content/docs/data-modeling/validation-rules.mdx (via externalId (literal, a string literal in sections))
  • content/docs/getting-started/examples.mdx (via externalId (literal, a string literal in sections))
What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/liveness/field.json) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 165 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 ff26bc4c2c4b4299c1e7acbf822774eb61b248b5 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from f2811273f2af8bc76fefa8465c140f615729f2d2 — the merge of head 0b08d30643b4630798a30d0142a5bb5b563db300 into base ff26bc4c2c4b4299c1e7acbf822774eb61b248b5, 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 f2811273f2af8bc76fefa8465c140f615729f2d2 && git checkout f2811273f2af8bc76fefa8465c140f615729f2d2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ff26bc4c2c4b4299c1e7acbf822774eb61b248b5 0b08d30643b4630798a30d0142a5bb5b563db300 && git checkout -B drift-repro ff26bc4c2c4b4299c1e7acbf822774eb61b248b5 && git merge --no-ff 0b08d30643b4630798a30d0142a5bb5b563db300

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

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs ff26bc4c2c4b4299c1e7acbf822774eb61b248b5 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…surface offers the key

Claude-Session: https://claude.ai/code/session_01WGufzg4R646jZ5Ky7hqq9L
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

Contract-review-claim: session_01AavokzJ5DndAwitDXvKy4U · head 0b08d30643b4630798a30d0142a5bb5b563db300 · 2026-10-29T00:48Z

Triage seat, taking needs:contract-review on this head (#27062 (a)). The review runs at CONTRACT_REVIEW_TIER. If the lane claims or posts a record on this head first, triage withdraws.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 0b08d30643b4630798a30d0142a5bb5b563db300
Local-runs: probe — git merge-tree --write-tree origin/main prr-27315 (no checkout, no build, no test run) to confirm the head still merges clean with origin/main 6abcb72b, which has moved past the PR base ff26bc4c; it does
Scope: packages/spec/src/data/field.form.ts + packages/spec/src/data/field.zod.ts, packages/spec/src/system/metadata-form-zod-reconciliation.test.ts, packages/spec/src/data/field-form-external-id-not-offered.test.ts, packages/spec/liveness/field.json, packages/platform-objects/src/apps/translations/{en,zh-CN,ja-JP,es-ES}.metadata-forms.generated.ts, packages/metadata-protocol/src/protocol.ts (TYPE_TO_FORM), ui/packages/app-shell/src/views/metadata-admin/SchemaForm.tsx, ui/packages/plugin-designer/src/FieldDesigner.tsx, ui/packages/plugin-designer/src/MetadataFieldsPage.tsx, ui/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx, ui/packages/app-shell/src/services/MetadataService.ts, ui/packages/types/src/zod/designer.zod.ts

Inputs read: card #26674 body and all 4 comments (triage 6442569651, claim 6471969865, os-dev-report 6474555034, claim amendment 6474597348); the ruling the triage comment cites, the #26598 contract-review records (should-fix 2 = this card); PR #27315 body, 32-file list, net diff vs main (32 files, matches the file list); 68 check-runs on the head: 0 pending, 0 failure — every non-skipped run success (Test Core 1-8/8, Test UI 1-16/16, Type Check UI, Lint UI, Build & E2E UI, Live E2E UI, Lint & Repo Gates, Lint · PM dispatch-gates, Spec property liveness, Check Changeset x2, PR-body guards x2, Governed Surface Queue Guard, Flag docs affected); the 12 skipped runs are path-filtered or opt-in (Dogfood matrix / Verify CLI, UI Published Artifacts, Build Docs, Temporal Conformance, Packed-tarball smoke, 18.x harness, Schedule gate, Auto Label, Check PR Size duplicates).

① Derived judgments

  1. packages/spec/src/data/field.form.ts:393 (main) — the Advanced-section row { field: 'externalId', helpText: 'Mark as external ID for upsert operations' } is removed; a comment names the liveness grade and the two real match keys. fieldForm is a published @objectstack/spec export and METADATA_FORM_REGISTRY['field'], served to Studio through packages/metadata-protocol/src/protocol.ts:451 (TYPE_TO_FORM), so this is a public-surface change: the served field form no longer offers the key. RIGHT against the ruling (triage 6442569651: "Studio must stop offering it as an upsert key"; claim 6471969865 seat ruling: "Studio stops offering the toggle, the protocol keeps parsing the key") and against the key's own .describe() at field.zod.ts:2348 ("Not read: no upsert, import or seed matches on this flag"), which the row contradicted.
  2. Accept set — FieldSchema.externalId stays z.boolean().default(false) at field.zod.ts:2348: no tombstone, no narrowing, no D2 conversion. Not a retirement, exactly as the card's protocol-first clause allows ("stop offering the toggle ... the protocol keeps parsing the key"). RIGHT. The new spec test pins it (FieldSchema.safeParse({ ..., externalId: true }).success).
  3. metadata-form-zod-reconciliation.test.ts:588-594 — a root omit ledger row field.(root).externalId. REQUIRED, not optional: reconcileRoot (:176-181) fails by name any authorable top-level key no form offers and no root row excuses (:235-239), so removing the row without the ledger entry reds the gate (the dev's 5570dc7a run shows exactly that failure, fixed in b357ce35). The why is non-vacuous and read off the liveness verdict, as the ledger's header demands. RIGHT.
  4. packages/platform-objects/src/apps/translations/{en,zh-CN,ja-JP,es-ES}.metadata-forms.generated.ts — exactly the 4-line externalId: { label, helpText } block removed per locale, nothing else moved. Pure regeneration from the form; check:i18n runs inside the green Lint & Repo Gates. No hand-authored catalog still carries the strings: git grep on the head for the en/zh/ja/es help texts hits only the changesets and the new spec test. RIGHT.
  5. object-lifecycle-panel-echo-decisions.test.ts:1241 — translated-leaf positive control 695 to 694: one row label fewer in all three locales. Consistent with 4; Test Core green. RIGHT.
  6. packages/spec/liveness/field.json externalId row — prose only: grade dead, verifiedAt 2026-10-27, authorHint unchanged; evidence now says no Studio surface offers the key and the ui/ mentions only carry it. Verified on the head tree: FieldDesigner.tsx has no externalId control or write (:343,477 are comments); ObjectFieldInspector.tsx has no checkbox (:30-33 comments); MetadataFieldsPage.tsx:251,466 and MetadataService.ts:629 copy the stored value. Content RIGHT. The citation pin is stale, see ④ should-fix 1.
  7. ui/packages/plugin-designer/src/FieldDesigner.tsx — the Advanced control at :476 and the edit seed externalId: editingField.externalId || false are removed; the update path no longer writes externalId: data.externalId === true, so ...editingField carries a stored value; the create path no longer emits externalId: false. A created field now reaches FieldSchema without the key and takes default(false), so stored semantics are unchanged. RIGHT, and the one behaviour change in the PR that could strip data is the one the dev ablated (both round-trip tests red with the line restored).
  8. ObjectFieldInspector.tsx:1359-1364 — checkbox removed; patchDef (:754-758) writes { ...def, ...patch }, so a stored externalId: true survives every edit; retiredIndexed sibling list loses External ID. RIGHT.
  9. i18n — appDesigner.fieldDesigner.externalId removed from 10 locale packs and the plugin-designer fallback table; designer.field.externalId from the Studio namespace (en, zh). No reader of either key remains on the head (git grep of both keys: changesets only). RIGHT; check:i18n-keys / check:i18n-drift run inside the green Lint UI.
  10. DesignerFieldDefinition.externalId (ui/packages/types/src/designer.ts:1266), its Zod mirror (designer.zod.ts:644) and MetadataService's field summary kept — they are the carriers that keep a stored value from being stripped. RIGHT to keep.

② Semver level

Four changesets, each patch: @objectstack/spec, @objectstack/platform-objects, @object-ui/plugin-designer, @object-ui/i18n. @object-ui/app-shell is private (no changeset, correctly). MATCHES what the diff publishes: no schema key added, removed, narrowed or widened; FieldSchema parses externalId exactly as before; the published changes are a served form layout losing one row, regenerated translation bundles, a drawer losing one control, and translation keys only that control read. Precedent for a form-offer change at patch: the agent form's lifecycle row ("the agent metadata form no longer offers it", packages/spec/CHANGELOG.md:14734, a Patch Changes section). Check Changeset is green twice on the head.

Clause-②: no, on the PR body and all four changesets, and no (narrowing) arm. RIGHT: no accept set is narrowed anywhere — FieldSchema still accepts externalId: true, the designer's DesignerFieldDefinition still carries it, and a served form layout is an offer, not an accept set. Metadata-protocol: none holds: no conversion, no tombstone.

③ Boundary flags

  • os-dev-report open_questions: [] — nothing to answer.
  • out_of_scope_findings (1): the liveness evidence prose made false by this PR. Not left out of scope: the claim amendment 6474597348 added packages/spec/liveness/field.json to the surface, and 0b08d306 fixes it (judgment 6). Answered.
  • Dev-declared narrowing: app-shell tests run locally on the 40 files importing ObjectFieldInspector only. Covered on the head by CI Test UI (16/16 shards green), which runs the whole app-shell suite. Answered.
  • Dev flag: "CI check-runs in_progress at report time" — now 68 completed, none pending, none failed. Answered.
  • Dev flag: branch is claude/project-thread-1898jl, not claude/issue-26674-slug — declared in the claim as platform-assigned; PR-body guards (claim · single-writer path) green. Answered.
  • Docs Drift Check (PR comment 6474580130) flags 5 pages by the literal externalId. Read on the head: field-types.mdx:1012, fields.mdx:394, validation-rules.mdx:31 carry the key's "Not read" row (true); seed-data.mdx and examples.mdx:215 teach the seed dataset's own externalId (unrelated key, true). No page describes a Studio "External ID" switch (git grep 'External ID' content/docs hits only batch/seed-loader/scim). Nothing became false. Answered.
  • Dogfood battery n/a (no !, no narrowing arm) — consistent with ②.
  • Main has moved past the PR base (ff26bc4c to 6abcb72b); the head merges clean (Local-runs probe). No escalation.

④ Design judgment

packages/spec/src/data/field.form.ts — HOLDS, judged against the ruling it implements (triage 6442569651 / claim 6471969865: stop offering, keep parsing), the key it describes (field.zod.ts:2348), the gate that enforces the omission (reconciliation root ledger, judgment 3), and its first consumer (the served form in Studio).

Break attempts, each read in the live tree at the head:

  • Does the generic Studio metadata editor — the fourth surface, the one the PR body does not name — strip a stored externalId: true once the form has no row for it? No: SchemaForm.tsx:1219-1225 setField writes { ...v, [key]: fieldValue } over the whole current value, and hiddenFields are documented "still preserved on save" (:1112), so a key with no row rides through untouched. The row removal cannot lose data on the served form either.
  • Is the key still offered anywhere else in packages/spec? No: git grep "field: 'externalId'" packages/spec/src is empty; the object designer's per-field quick-add repeater is a ledgered object.fields subset that never offered it.
  • Does the form now contradict os lint? No — the opposite: lint warns on an authored externalId: true (liveness-dead-property, booleans only when true), and no Studio surface produces one any more. The card's "the product contradicts its own lint" is closed.
  • Is the comment's claim true? "upsert matching lives on a seed dataset's externalId and an import mapping's upsertKey" matches field.zod.ts:2348's describe and the liveness row's evidence (seed-loader.ts, import-prepare.ts). Yes.
  • Does the new spec test fail if a row returns at any depth, and does its positive control reach the Advanced section? Yes: the walk recurses fields, the control asserts storage is found (an Advanced row), and a second case greps every helpText for "external id for upsert".
  • Does a stored externalId: true survive each of the three Studio writers the card names? Designer drawer: ...editingField spread with no overwrite; MetadataFieldsPage: toDesignerField :251 / fromDesignerField :466 copy it both ways (the PUT-bytes test asserts it on an edited and an untouched field, and that a field without the flag gains none); inspector: patchDef spread. Yes, each pinned by a test the ablation showed is live.

Must-fix: none.

Should-fix (do not decide the verdict):

  1. packages/spec/liveness/field.json externalId row — the evidence still opens "at 1c63231" and keeps verifiedAt: 2026-10-27 while describing the tree as it stands after this PR (no Studio control). The README's rule (liveness/README.md:340-342) is that a re-reading re-pins the commit and stamps the reading's date, and the dev did re-read the tree at this head; re-pin the citation to the head and stamp the date of that reading, so the row does not cite a commit in which FieldDesigner.tsx:476 still offered the key. The gate stays green either way (age never fails CI; every citation resolves).
  2. ui/packages/types/src/zod/designer.zod.ts:644 — the carrier's .describe('External ID flag') is kept on purpose, but says nothing about the key being unread, so a reader of @object-ui/types still meets the old meaning. Outside the claimed surface; note for the row's next re-grade.

Implemented-by: claude/project-thread-1898jl
Reviewed-by: session_01AavokzJ5DndAwitDXvKy4U

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读 (席位终稿)

  • 改了什么:Studio 的字段元数据表单、字段设计器抽屉、字段检查器三处不再显示「External ID」开关和「用于 upsert」的帮助文字。已经存了 externalId: true 的字段,在 Studio 里编辑保存后原样保留。
  • 为什么改:field.externalId 已判为 dead,没有任何 upsert、导入或种子按它匹配。开关让人以为重新导入会按它更新,结果静默产生重复记录,os lint 还会对表单自己给出的值报警告。
  • 风险与回滚:协议照旧解析这个键,只删 UI 控件和只被它用到的文案(四个 patch changeset)。设计器更新路径改为靠展开保留旧值,三个保存面各有往返测试。回滚 = revert 本 PR。
  • 先看:ui/packages/plugin-designer/src/FieldDesigner.tsx(更新路径不再写 externalId)、packages/spec/src/data/field.form.ts(删表单行)。
  • 两分钟验证:cd ui && pnpm exec vitest run packages/plugin-designer/src/MetadataFieldsPage.externalIdRoundTrip-26674.test.tsx,2 个用例全绿。
  • 需要你做的:无,席位落地。

席位意见:接受。分诊契约复核 PASS(6485032650,就在当前 head 上):接受集不变,表单与 Zod 对账的省略行是必需的,三个 Studio 保存面和通用元数据编辑器都会保留已存的值。

席位核对 · ACCEPT · domain:spec seat 3 · os-support-ai · session session_01WGufzgbJhFoXYsLSe5WSPP · 2026-10-29T01:24Z

Head: 0b08d30643b4630798a30d0142a5bb5b563db300 · card #26674 · claim 6471969865 (amended 6474597348)

  1. 形态: a draft PR to main carrying Fixes #26674: 32 paths (+461 / −44), inside the amended claim's surface. It removes the externalId row from the spec field form and its four generated bundles, the Field Designer control and its update-path write, and the inspector checkbox; it adds the reconciliation root-omission row, rewrites the liveness evidence prose, removes the now-unread i18n keys, adds four test files and four patch changesets. check-governed-merges --pr 27315 reports NOT governed (0 of 32 paths), so the ordinary queue landing applies. The assignee is os-support-ai.
  2. 前提实测: the premise holds. The triage contract review (6485032650, PASS, on this head) confirmed FieldSchema.externalId still parses (no tombstone, no narrowing), so Clause-②: no / Metadata-protocol: none hold. It traced a stored externalId: true through every writer: the designer drawer's spread, MetadataFieldsPage's toDesignerField / fromDesignerField, the inspector's patchDef, and the generic SchemaForm setField, which the PR body does not name. The ablation shows the round-trip tests are live.
  3. 门禁 check-runs (on this head): 71 runs read at 2026-10-29T01:24Z: 59 success, 12 skipped, none red or pending. Every required context is success, Test UI and Live E2E UI included. The review's merge-tree probe shows the head merges clean with origin/main 6abcb72b.
  4. 接受的偏差:
    • (a) Review should-fix 1: the field.json externalId row keeps its 1c632311 citation and verifiedAt: 2026-10-27 while its evidence now describes the tree after this PR. The gate stays green, and re-pinning to this PR head would cite a commit that the queue does not put on main. The row is re-pinned to the merge commit at its next re-grade.
    • (b) Review should-fix 2: designer.zod.ts:644's .describe('External ID flag') does not say the key is unread. It is outside the claimed surface, and it goes with (a).
  5. 范围外去向: (a) and (b) above, at the row's next re-grade. No card is owed.
  6. 落地条款: the seat checked the prose faces against the diff on this head: the PR body, the four changesets and the liveness prose. The seat readies the PR and arms auto-merge in this act, follows it to MERGED, and lands Studio still offers field externalId as an upsert key after it was graded dead #26674.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 29, 2026 01:25
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 29, 2026 01:25
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 29, 2026
Merged via the queue into main with commit 74801d1 Oct 29, 2026
72 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-1898jl branch October 29, 2026 01:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Studio still offers field externalId as an upsert key after it was graded dead

2 participants