Repository navigation
Description
epic:#26676#1 · zhuangjianguo · session_cdb7a717def74e719988f089fbaa18ff
The maintainer: 「按「覆盖」立卡」 (2026-11-04), after 「其他元数据是怎么处理的。很多是叠加覆盖对吧。」 The governing ruling is ADR-0146 D2: 「方向定为包级覆盖:代码和 Studio 用同一机制,定制都进 .custom。」 Measured by the seat on main 63b86b385d, on the example CRM. Code re-read on c23501b2b9. No domain:* label is set: triage routes this card.
Business reason
An operations person adds a contract object to an installed CRM and publishes it. The object works, but users can reach it only by a typed URL, because Studio offers no way to put it on the app's menu. This is the normal case: real apps are installed, so they are read-only. The customization is finished in every way except the one step that makes it usable.
What already works, with no code change
An override of the app in the paired customization package, written through the existing metadata door:
- Item:
appnamedcrm_app, incom.example.crm.custom, withoverrides: { package: 'com.example.crm', basedOn: '4.0.0' }, a whole copy of the app, and one extra entry undergroup_sales:{ id: 'nav_eval_hetong', type: 'object', objectName: 'crm_custom_eval_1104_hetong', label: '合同' }.
| Step | Measured result |
|---|---|
Save as draft, then POST /api/v1/packages/com.example.crm.custom/publish-drafts |
published, 1 item |
GET /api/v1/meta/app/crm_app |
The entry is served under group_sales, with _packageId still com.example.crm |
| Running app | The sidebar shows the entry and opens the list |
| Studio Interfaces rail | 合同 appears under 销售管理 |
| Restart the backend | Still there |
DELETE /api/v1/meta/app/crm_app?package=com.example.crm.custom |
The entry leaves, and the app reads as shipped |
GET /api/v1/packages/com.example.crm/customizations lists the override.
What is missing: the Studio door
ui/packages/app-shell/src/views/studio-design/StudioCustomizeControls.tsx:73:STUDIO_CUSTOMIZE_TYPESis view, dashboard and report.appis overridable (packages/spec/src/kernel/customization-package.ts:325) but Studio does not offer it.ui/packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx:4651onc23501b2b9: on a read-only base the rail's 「+」 offers only what it can create (page, dashboard, report), and the 编辑 navigation button draws only when the package is writable.- On the measured override: the 合同 row carries no mark, 仅看定制 did not count the app override, and there is no way back from the rail.
Scope
- On a read-only base with a pair, the navigation can be customized. The first save takes a whole copy of the app into the pair with the
overridesdeclaration, as a view's 定制 already does.basedOnis the base'smanifest.version. - It reuses the writable package's navigation editor (
addNavEntry,StudioNavItemInspector): add object, page, dashboard, report and the other entry kinds, plus rename, reorder, group and remove, including entries the base shipped. - The target pickers offer the pair's own items as well as the base's. A person can add the object they just made in the pair, and an already existing page, dashboard or report listed under 不在导航中.
- A navigation change is a draft in 待发布的变更 and publishes in one batch with the object it opens.
- The rail shows that the navigation is customized, 仅看定制 counts it, and 恢复原版 removes the override after a confirmation.
Acceptance
- The job end to end in a browser: new object in the pair, add it to the menu, publish both together, see it as a user. Publishing the object and the app in the same batch was not measured here, so measure it.
- 恢复原版 returns the shipped navigation. The base package is unchanged throughout.
- A member who cannot read the object does not get a working entry. This was not measured. Say on the PR what the member sees.
- An entry whose object is later removed does not leave a broken link. Use the existing
requiresObjectif that is what the measurement calls for. - No new metadata type and no spec change. If the server turns out to need a change, report it on the card with the reason.
- Tests pin the first-save copy, the pair target pickers and the rail mark and count. Screenshots on the PR, from latest
main.
The accepted cost
An override is a whole copy. A navigation entry the vendor adds in a later version does not appear in a customized app until the override is restored and redone. ADR-0146 D2 records basedOn and does not compare it, and upgrade comparison is out of that epic. This cost follows from the rule and was not measured with a real upgrade. The maintainer chose this over an additive new metadata type, which would have kept vendor entries but added a second rule and server work. Do not add a comparison or a merge in this card.
Not in this card
- A new
app_extensiontype: considered by the seat and dropped. - The separate gap that
navigationContributionson a runtime-created package is accepted and never applied. - Text labels for the rail's customization marks.
- Studio Interfaces: choose an object's view from a dropdown at the top of the preview, in the object's title, the way the running list does #30046, the view dropdown in the same header. Coordinate the file surface if both are in flight.
Re-check
git grep -n "STUDIO_CUSTOMIZE_TYPES: readonly" origin/main -- ui/packages/app-shell/src/views/studio-design/StudioCustomizeControls.tsx
Activity
objectstack-fleet commentedon Nov 4, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: first-touch audit · p1 kept ·
domain:ui· 2026-11-04T13:00Z⛔ Not a claim.
StudioCustomizeControls.tsx(STUDIO_CUSTOMIZE_TYPES) and the Interfaces rail. [Decision] Can an organization author its own assignment rules, or do they stay package-declared only? #30066 is ordered after this card.Generated by Claude Code
objectstack-fleet commentedon Nov 4, 2026
domain:ui#1 · os-support-ai · session_01WGufzjDyeFwq6AuFu3vwTd
Claim: PM loop round 1 · 2026-11-04T14:49Z
Session:
session_01WGufzerTiGyoUPgrA6K1SY(card thread; dispatching seatsession_01WGufzjDyeFwq6AuFu3vwTd)Account:
os-support-aiBranch:
claude/project-thread-pn7lsr(platform-assigned to the card thread; it can push only this branch)Worktree:
objectstack-issue-30059Domain:
domain:uiSeat:
domain:ui#1File surface:
ui/packages/app-shell/src/views/studio-design/StudioCustomizeControls.tsx(types list + app door only), the Interfaces rail / navigation editor inStudioDesignSurface.tsx,InterfacesNavRail.tsx,StudioNavItemInspector.tsx; their tests, browser screenshots under/mnt/project-files/ui-orders/30059-shots/, one root.changeset/file; ⛔ no spec change, no new metadata type, no upgrade comparison/merge, noapp_extension(stop on breach; explain in the report)Container & model:
L(Scope 1–4 first asRefsif the diff passes ~1,500 lines),mode:thread,model: TIER_DEFAULT(dispatch-gates --tier: no path-derived mandate),effort: highClause-②: no
Metadata-protocol: none
Responsibility: ui code produces it (
domain:ui) | no platform path covers it | every Studio author customizing an installed appThread-read: 6631291035
Premise: holds (re-read on
origin/mainf68b47cac:STUDIO_CUSTOMIZE_TYPESis['view','dashboard','report']; the rail's 「+」 on a read-only base offers only creatable kinds)Serial constraints cleared: #30057 (same seat, PR #30113 ACCEPTed) touches
ObjectFormDesigner.tsxand the Data pillar inStudioDesignSurface.tsx; this card keeps to the Interfaces rail and mergesmainbefore its PR if #30113 lands first. #30046 unclaimed.Order:
/mnt/project-files/ui-orders/30059.md· maintainer scope 「总并发上线 10」 (2026-11-04T14:01Z)feat(app-shell): gate a nav entry bound to the pair's object; keep un…
objectstack-fleet commentedon Nov 4, 2026
domain:ui#1 · os-support-ai · session_01WGufzerTiGyoUPgrA6K1SY
os-dev-report
Stamp: 2026-11-04T16:48Z
{ "issue": 30059, "status": "done", "branch": "claude/project-thread-pn7lsr", "pr": "https://github.com/objectstack-ai/objectstack/pull/30153", "session": "session_01WGufzerTiGyoUPgrA6K1SY", "premise_still_valid": true, "summary": "Draft PR #30153, Fixes #30059, head 6e106af3437be591f947a0ffd323adb05dd2fdcc, no assignee and no reviewer. Scope 1 to 5 ship in one PR: 677 changed lines, under the 1,500 split line. On a read-only base with a pair (or one the ensure door can create), the Interfaces rail's Edit and its \"+\" open the existing nav editor (addNavEntry and StudioNavItemInspector, reused). Opening Edit writes nothing. The first saved edit copies the whole app into the pair as a draft with overrides {package, basedOn: base manifest.version}. The pickers list the pair's items after the base's. The rail header carries the compact customized mark (no text label); only-customizations counts it; the mark's popover offers Restore package original behind a confirmation. Added from the browser run: an entry bound to an object only the pair holds carries requiresObject, because deleting such an object otherwise left a dead runtime link. Nav customizing is also offered only when the base manifest has a version, and one existing pin (oneSurface-28572: the \"+\" on a read-only base offers no nav entry) was updated on purpose. No spec, server or metadata-type change. StudioCustomizeControls.tsx changes only its types list and docblock. #30113 (#30057) landed first and main is merged; it merged without conflicts.", "tests": "New describe in InterfacesPillar.customize-28562.test.tsx, 6 tests: the first-save whole copy with the declaration (body toStrictEqual, ifNoneMatch *, base untouched); the pickers offer the pair's object and dashboard, the pair-object entry is saved with requiresObject, and a rebind to a base object leaves no gate; the mark plus a count of 1 under only-customizations; Restore cancel and confirm; walled caller sees no Edit and no \"+\"; CONTROL: a writable package saves at the package with no door and no declaration. Ablations through scripts/ablation-replace.mjs, each restored to the pre-mutation blob: drop 'app' from STUDIO_CUSTOMIZE_TYPES gave 4 failed and 2 passed (CONTROL green); skip the declaration (saved = declared to saved = { ...saved }) gave 2 failed and 4 passed; no gate for a pair object gave 1 failed and 5 passed. Sweep of every app-shell test file importing StudioDesignSurface, StudioNavItemInspector, InterfacesNavRail or StudioCustomizeControls under the lock: the first pass had 197 files with 3 red (sharedSelect-11865 keys, navEditClosesOnReadOnly-11167, oneSurface-28572), all fixed in bee40cbe; the rerun gave Test Files 197 passed and Tests 1359 passed, VERDICT command-exit 0. After merging main, the 8 shared files gave 180 passed. app-shell type-check exited 0 before and after the merge. eslint on the touched files: 0 errors. Browser on a dev boot of examples/app-crm (com.example.crm 4.0.0 read-only), 16 screenshots in /mnt/project-files/ui-orders/30059-shots/. Acceptance 1: the ensure door answered 201, the object draft was created in the pair, the picker listed it, and publish-drafts on the pair answered publishedCount 2 (object plus app) with one commitId, measured twice; the runtime sidebar showed the entry. Acceptance 2: Restore sent DELETE of the draft and the row at the pair; layers=true read overlay null and effective == code, with the base code hash unchanged; the runtime showed the shipped nav. Acceptance 3: an invited member got 403 on the object; the entry was absent from their sidebar and the typed URL showed no access. Acceptance 4: deleting the object left a dead link before the gate; with the gate the entry showed while the object existed and was hidden after deletion. The browser run was on 6f468077; the last commit changes nothing it exercised.", "gates": "dispatch-gates.mjs --commands after fetching origin/main: 35 commands, run from a runner script, each exit recorded. 34 exited 0 the first time. pnpm --dir ui exec vitest run scripts/ exited 1 with 2 tests timing out at 15 s while the 197-file sweep shared the box; both files passed alone under the lock, and the whole command reran to exit 0 (170 files). The recorded line is that rerun. --ran: 35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. The gate sweep ran before the last app-shell fixes and the merge of main. Those changes are app-shell source and tests only, and they were re-tested above. check-sdui-manifest exited 0. CI check-runs on #30153 have not been read yet.", "dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm.", "mcp_calls": "0 MCP GitHub calls; reads went through gh api REST. Other MCP: claude-code-remote subscribe_pr_activity on #30153 (not a GitHub write).", "api_writes": "4: git push of 284ece45 (first slice); git push of 6e106af3 (follow-up commit plus a merge of main); pr_create through the relay (fleet-write run 41870856456, #30153, read-back identical at 10678 bytes, assignees none); this comment via post-stamped.mjs. One push was refused locally by check:commit-card-trailers (Refs #30059 in two unpublished messages, reworded before pushing), so nothing was sent on that one.", "open_questions": [], "security_findings": "none", "out_of_scope_findings": [ "carrier: none · noted, not filed · The pending-changes row and the Restore dialog name the app by its raw label CRM, while the Interfaces rail shows the translated label 客户管理 (zh-CN, app-crm dev boot). Noted in the PR's Acceptance notes." ] }Generated by Claude Code
objectstack-fleet commentedon Nov 4, 2026
domain:ui#1 · os-support-ai · session_01WGufzjDyeFwq6AuFu3vwTd
Landed · PR #30153 ·
6cc0b818c2· 2026-11-04T17:53Z6cc0b818c26e106af3, CI green (44 success, 11 expected skips)The seat's ACCEPT is comment 6636579502. The card is closed by
Fixes #30059(state: closed completed).pm:dispatchedis removed in this stroke..custompair, Studio's Interfaces rail now edits the app's navigation with the writable package's editor.overrides: { package, basedOn }.requiresObject.publishedCount 2, one commit./mnt/project-files/ui-orders/30059-shots/(06–10, 15–18, plus 09b for the dead link before the gate).CRM, while the rail shows 客户管理.