Repository navigation
Description
epic:#28130#1 · hotlong · session_040dfd80c4c9411b895c698e785a6848
Filing gate: ④ child of epic #28236 (P2, cut 2026-11-01 on the maintainer's 「四项提速措施全部按推荐执行」, schedule reviewed) · plan of record: ADR-0146 Rollout P2 "N-23a/b/c object extension … Studio add-field (D10)". ⛔ Not a claim.
Blocked-by: #28560, #28564, #28565, #28574, #28407
Part of #28236 (P2, child N-23c) · governs: ADR-0146 D10 (last paragraph: Studio's add field writes an extension in .custom, suffixed, the base untouched; the display-property patch), D3 (auto-create, no prompt) · lane: domain:ui · size: L.
Why it waits. This card writes the extension body N-23a declares, including its per-field patch slot. Four other cards come first:
- N-23e, the runtime write door for an extension into a writable package. No such door exists today.
- N-23b, the
__cgates. - N-23d, which applies the patch and checks its target.
- The ensure door (metadata-protocol: the <base>.custom ensure door — the one idempotent creation path for the paired package, explicit <base namespace>_custom, walled 403 #28407, PR feat(metadata-protocol): ensure door for a read-only base's paired customization package #28512:
POST /api/v1/packages/:id/ensure-customization), which creates the pair.
Business reason. An administrator wants a "Region" field on HotCRM's Account. Today Studio refuses because HotCRM is read-only. They should add it in Studio and get region__c in hotcrm.custom. HotCRM stays as shipped and the next HotCRM upgrade cannot collide with the field. Renaming a HotCRM field's label works the same way, within the whitelist.
Measured (35b178bd0a).
- Data pillar:
canAddFields = !readOnly && !objIsExternal(ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx:7485).addFieldpushes the field into the open object's own buffer (objDraft), and autosave writes that object whole (:7473–7484). On a read-only base, nothing is offered. AddFieldPopover.tsxderives the api name from the label (toFieldNameLoose) and refuses a taken name (fieldNameTakenRefusal).- Spec:
EXTENSION_FIELD_SUFFIX = '__c'andisExtensionFieldName(packages/spec/src/data/field-customization.ts:31, :40).BASE_FIELD_DISPLAY_WHITELISTislabel,description,inlineHelpText,placeholderand optionlabel(:74). The patch slot is not declared yet (:21–23, N-23a). ObjectExtensionSchema(packages/spec/src/data/object.zod.ts:4709) carriesextend,fields,label,pluralLabel,description,validations,indexesandpriority.- No runtime write path for an extension exists.
object_extensionhas no slot of its own (packages/objectql/src/plugin.ts:178–182), and only the manifest load registers one (objectql/src/engine.ts, the'extend'registration). N-23e builds the door.
Wanted.
- On a base object of a read-only base, Studio offers Add field (grid "+" and Form toolbar). Create ensures
<base>.customwithout a prompt and writes an extension of that object into the pair, not into the base object's buffer. - The popover shows the api name with
__cbefore Create (region→region__c). A typed name is suffixed the same way. The taken-name refusal covers the folded object's fields. - The new field appears in the grid and form at once, and its inspector edits the extension. The base object's other fields stay read-only.
- On a base field, the inspector edits only the whitelisted properties, read from the spec constant (no client copy). Edits are written as the per-field patch in the pair's extension. Every other property is shown read-only, with one sentence naming the whitelist.
- Server refusals are shown verbatim: suffix, structural key, walled
403from the ensure door. Under a walled posture, for a principal the served facts refuse, the actions draw disabled in the console's own words. - Unchanged: add-field on a writable package's own object (plain names), and the field inspector there.
Tests. Fixture read-only base + object:
- Add field "Region" ensures the pair once, writes an extension with
region__cinto it, and leaves the base object's body unsent; - a second add does not ensure again;
- a label patch on a base field writes only
labelinto the patch; requiredon a base field is not editable;- a refusal is shown verbatim;
- the walled tenant case is disabled;
- a writable package's add-field is unchanged.
Release safety. Safe on its own: the write door (N-23e), its gates (N-23b, N-23d) and the ensure door (#28407) are all in Blocked-by:. Without this card, base objects stay read-only in Studio, as today.
Not here. The patch slot (N-23a). __c at every door (N-23b). Patch merge and target check (N-23d). The extension write door (N-23e). The customized/added marks on rails (N-19). Directory marks (N-26b). No fields table (D6).
Dedupe. Searched add field __c and extension field studio base object: only #28236, #28411 (the read) and closed N-08 history (#28404, #28467).
Links back: #28236, #28407, #28404, ADR-0146 D10, D3, D6, ADR-0029 D9.2.
Generated by Claude Code
Activity
objectstack-fleet commentedon Nov 1, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Unblocked (R828). Every card in
Blocked-by:is closed as completed: #28560, #28564, #28565, #28574 and #28407. Triage read "Why it waits" before releasing. The card writes the extension body N-23a declares, and the cards it said must land first have landed. Its write door (N-23e), its gates (N-23b, N-23d) and its ensure door are all among those closed blockers, so nothing release-relevant still waits.pm:blocked→pm:queue, priority stays p1 (domain:ui).Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:ui#1 · zhuangjianguo · session_01QsR2jUJ19MstFvFhjgiYTu
Claim: PM loop round 7 · 2026-11-02T00:14Z
Session:
session_01QsR2jVPKQuoz3QHyQ76RnU(card thread, os-dev); dispatched by seat sessionsession_01QsR2jUJ19MstFvFhjgiYTuAccount:
zhuangjianguoBranch:
claude/project-thread-y2hv3c(platform-assigned to the card thread; it can push only this branch)Worktree:
objectstack-issue-28575Domain:
domain:uiSeat:
domain:ui#1File surface:
ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx(the Data pillar's add-field path),studio-design/AddFieldPopover.tsx,views/metadata-admin/inspectors/ObjectFieldInspector.tsx, the N-17 ensure path it reuses (studio-design/packages-io.ts,studio-design/StudioCustomizeControls.tsx),ui/packages/i18n/src/namespaces/studio/{en,zh}.ts, their tests, and a root.changeset; server andpackages/specread only (stop on breach; explain in the report)Container & model: L,
mode:thread,model: default tier (dispatch-gates --tier: no path-derived mandate),effort: highClause-②: no
Metadata-protocol: none
Responsibility:
n/a — not a defect cardThread-read: 6565810476
Premise: holds (at
origin/main6ea8342a:canAddFields = !readOnly && !objIsExternalisStudioDesignSurface.tsx:8287;EXTENSION_FIELD_SUFFIX/BASE_FIELD_DISPLAY_WHITELISTarepackages/spec/src/data/field-customization.ts:33/ :75;object_extensionis an authorable item through the/api/v1/metadoors since #28574 (PR #28811,0c1829a28a); thefieldPatchesslot is onObjectExtensionSchema(#28560))Serial constraints cleared: this seat's PR #28818 (#28578) also edits
StudioDesignSurface.tsxand the studio i18n namespace; whichever lands later mergesmain.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:ui#1 · zhuangjianguo · session_01QsR2jUJ19MstFvFhjgiYTu
Landed · 2026-11-02T03:02Z · PR #28860 merged through the merge queue as
7fa9873fat 2026-11-02T03:02Z (GitHub's clock). ACCEPT 6567788691 at headb48d97ff.7fa9873f~1)origin/main)export function foldObjectExtensioninstudio-design/StudioCustomizeControls.tsxstudio-design/DataPillar.baseObjectExtension-28575.test.tsxexistsFixes #28575closed this card.pm:dispatchedcomes off in the same stroke as this note.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:ui#1 · zhuangjianguo · session_01QsR2jVPKQuoz3QHyQ76RnU
os-dev-report
{ "issue": 28575, "status": "done", "branch": "claude/project-thread-y2hv3c", "pr": "https://github.com/objectstack-ai/objectstack/pull/28860", "session": "session_01QsR2jVPKQuoz3QHyQ76RnU", "premise_still_valid": true, "summary": "Studio's Data pillar offers Add field on a base object of a read-only base (Form toolbar leading slot and grid +). Create ensures <base>.custom once and writes an object_extension draft with a __c field into the pair; the base object's body is never sent. On a base field only BASE_FIELD_DISPLAY_WHITELIST keys are editable (read from the spec), written as fieldPatches; refusals verbatim; walled posture draws the action disabled. Merged as 7fa9873f.", "tests": "New DataPillar.baseObjectExtension-28575 (10 cases incl. the seat's dedupe case) + StudioCustomizeControls.objectExtension-28575 (9 unit pins), green. Sweep: studio-design + inspectors + zh census + studio-locale + packages/i18n, 615 files / 6493 tests, one red (gridColumns identity-churn liveness, caused by my names-key memo), fixed and re-run (36 files / 206 tests). After the merge of main: 113 files / 830 tests green. Ablations via ablation-replace.mjs, restored to HEAD: suffix removed -> 3 of 10 red; base fields routed whole -> 7 red across both files.", "gates": "dispatch-gates --commands: 34 commands, all exit 0 (check:type-check-debt first NOT MEASURED on a lock timeout, re-run OK). app-shell type-check green at each head; ui scripts/ 169 files green; check:i18n green after the merge; cross-file line citations 0. CI: Type Check UI red at fe34b830 on check:spec-symbols (local isPlainRecord), fixed at 10f4f254; merged main at b48d97ff; landed 7fa9873f.", "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm", "mcp_calls": "0 GitHub MCP calls", "api_writes": "1 relay write — pr_create (POST /repos/objectstack-ai/objectstack/pulls); plus 3 git pushes", "open_questions": [], "out_of_scope_findings": [], "deviations": [ "Wanted 1 grid half: a draft field joins the Records grid columns only after publish (gridColumns projects published names, cloud#1652); accepted by the seat and recorded in Acceptance notes.", "Form toolbar Add field sits in FormToolbar's leading slot and the walled disabled action in the object header, because ObjectFormDesigner.tsx and GridFieldAuthoringProvider are outside the surface.", "Base-field mode reaches the inspector by React context (inspector-registry.ts outside the surface).", "Added beyond the card: a cleared patch value is held as '' in the buffer and dropped on save." ] }