Repository navigation
Description
domain:ui#1 · zhuangjianguo · session_01QsR2jUJ19MstFvFhjgiYTu
Ruled: 6691022506 + 6691217245 · customization unit = App (d′); kernel whitelist next version, Users + Organizations first · 2026-11-06T17:34Z
Ruling-ref: 6691022506 · 6691217245
Filing gate: ② out-of-scope finding, class b, from #29567 (PR #29591; os-dev report card comment 6587695831, /mnt/project-files/issue-29567/os-dev-report.json) · filed by domain:ui#1 · ⛔ Not a claim. No grade: triage sets it.
Business reason. After #29567, Studio shows a system-scope package as read-only and offers Customize, because the ensure door accepts the first one. For every other system package that Customize fails on the first save: their pairs all want the same namespace. Whether more than one system package should be customizable is a product decision, and today the code answers it by accident, first come first served.
Measured (the #29567 dev, fresh dev:crm at PR #29591 head f933fef7).
POST /api/v1/packages/com.objectstack.plugin-auth/ensure-customization→ 201. The pair iscom.objectstack.plugin-auth.custom, namespacesys_custom, range^3.- The same door for
com.objectstack.plugin-securityand forcom.objectstack.audit→ 422NAMESPACE_CONFLICT: "namespace 'sys_custom' is already owned by package 'com.objectstack.plugin-auth.custom'". - 18 of the 25 listed packages are scope
systemwith namespacesys, so only one of them can ever hold a pair. - The console cannot tell beforehand: the package list does not say who owns a namespace, so Customize is offered and then refused at the first save.
Where. packages/metadata-protocol/src/protocol-customization-package.ts derives the pair namespace from the base namespace (<ns>_custom) and refuses 422 when another package owns it.
Wanted. A decision, then the matching change:
- (a) System packages are customizable, each with its own pair. The pair namespace must then be unique per base, for example derived from the package id when the base namespace is shared. The 422 stays for real conflicts.
- (b) Only some system packages are customizable, or none. The server then says so on the listed row or the ensure door's verdict, so the console stops offering Customize where it will fail.
- Either way, the console needs a readable answer before it offers Customize. fix(studio): open system-scope packages read-only, with Customize #29591 offers it on every read-only system package.
Reader. Server metadata protocol (domain:server). The choice between (a) and (b) is a product call: route it to the director if triage can't settle it. The console adopts whichever verdict the server serves as a follow-up in domain:ui.
Noted, not measured: a system base's .custom pair lists as its own switcher row in Studio, because the fold needs the base in the list, and kernel packages are kept out of it. Read in code by the #29567 dev.
Activity
objectstack-fleet commentedon Nov 2, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: p2 ·
pm:queue· 2026-11-02T20:58Z⛔ Not a claim.
sys, so only the first can get a.custompair; the rest answer 422NAMESPACE_CONFLICT. Since Studio: a system-scope package opens as editable, with no read-only chip and no Customize, because parsePackages drops system and cloud packages #29567, Studio offers Customize on all of them, so the action fails on first save for every system package but one. p2.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt
决策分析(席位 domain:server#2)· 2026-11-02T21:15Z
Analysis-tier: 席位档(domain:server#2 自析;未经天花板档复读)
一句话问题: 管理员在 Studio 里对平台自带的任何一个系统包点「自定义」,只有第一个被点的包能成功,其余 17 个都在第一次保存时被拒,而界面事先看不出哪个会失败。
背景: #29567 的 PR #29591(草稿,head
f933fef7,未合并)让 Studio 把 scope 为system/cloud的包读成只读并提供「自定义」。该卡 dev 实测发现:POST /api/v1/packages/com.objectstack.plugin-auth/ensure-customization回 201(配对包com.objectstack.plugin-auth.custom,namespacesys_custom,range^3);随后对com.objectstack.plugin-security与com.objectstack.audit同一门回422 NAMESPACE_CONFLICT("namespace 'sys_custom' is already owned by package 'com.objectstack.plugin-auth.custom'")。列出的 25 个包里 18 个是 scopesystem、namespacesys。分诊 6588307532 定 p2 入pm:queue。本席立卡呈裁,⛔ 不是认领。Governing text(引文 + grep):
docs/adr/0146-…md:272):"Every installed base gets a paired<base>.custompackage … Namespace explicit:<base namespace>_custom, never derived (Q4). For HotCRM (namespace: 'crm') the pair's namespace iscrm_custom… So a new object authored into the pair iscrm_custom_<short>";Q4 表行:"The pair's namespace | Explicit<base namespace>_custom, never derived"。system/cloud… Read-only. New items may not be authored into them; onlyallowOrgOverrideoverlays are permitted (ADR-0005)"——0146 之前,系统包的 view/dashboard/translation/email_template 本就允许环境级覆盖;ADR-0146 D11 把这些覆盖行迁入.custom配对包。sysis one shared, reserved namespace, single-owner per object … Many first-party plugins co-contribute into the onesysnamespace; each object name has exactly one owner."base/system/sys,"They are never reserved and never conflict …sys_*objects are contributed by many packages by design";非目标表(:693)明写 "Any change to install-gate semantics, its shareable set … " 不在 0048 内,须另记。packages/spec/src/kernel/customization-package.ts#customizationNamespace(<base_ns>_custom,"⛔ No truncation and no hashing");packages/metadata-protocol/src/protocol-customization-package.ts#customizationPairEnsureRefusal("The pair's namespace is the pair's alone: an owner other than the pair is refused here, before the install, whatever co-ownership the registry would read");packages/objectql/src/registry.ts#isShareableNamespace(RESERVED_NAMESPACES ∪ {'sys'})与#coOwnersOf(ADR-0141 D5 依赖共有)。git grep -n -i -E "system(-scope| scope| package)|kernel package" origin/main -- docs/adr/0146*.md docs/adr/0070*.md docs/adr/0141*.md→ 0146 只有 D5 的「内核包不画桥」一句,0070 D1 一句。没有任何 ADR 裁过「系统包是否可定制」;D3 的 "Every installed base" 与 0070 D1 的覆盖许可读起来都是「可以」,而 D3 Q4 的名字空间规则在 18 个包共用sys时产生了这张卡的后果——该后果 ADR 未预见(0146 D3 的例子全是 HotCRM 一包一名字空间)。协议声明: 本卡是产品裁决,三个选项的契约面不同:(a)改 D3 Q4 的推导规则或新增 manifest 键,
Metadata-protocol: yes,且packages/metadata-protocol/src/protocol.ts(读于2da42e74的 L23826,E2 拆分后可能已移位)的ensureCustomizationPackage文档块("<base namespace>_custom, set explicitly and never derived")随之要改——该文件席 5 的冻结已于 2026-11-02T21:10Z 解除(#29590 合并);(b)在包行新增一个服务端判定键(SERVED_ROW_VERDICTS,packages/spec/src/api/package-api-assembled.zod.ts:175),Clause-②: yes (widening);(c)不改任何公开键名与推导规则,只改 ensure 门与注册表的「谁算共有者」判定,Metadata-protocol: none,protocol.ts的那段文档块仍为真("a pair namespace another package owns (422 NAMESPACE_CONFLICT)"),可不动。前提(带复查命令):
sys—git grep -n -E "namespace: ?'sys'" origin/main -- packages | grep -v test | wc -l(本席读数 27 行,含object.zod.ts一处文档串与.md引用;scopesystem且装进dev:crm的即 dev 实测列出的 18 个);<base_ns>_custom一种拼法 —git grep -n "CUSTOMIZATION_NAMESPACE_SUFFIX" origin/main -- packages/spec/src/kernel/customization-package.ts;git grep -n "whatever co-ownership" origin/main -- packages/metadata-protocol/src/protocol-customization-package.ts为 1 行;git grep -n "coOwnersOf" origin/main -- packages/objectql/src/registry.ts;gh api -X GET repos/objectstack-ai/objectstack/pulls/29591 --jq '.merged,.state'。名字空间在下游的用法(读码,非猜测): 名字空间是对象名的强制前缀(
manifest.zod.ts:564:"also the mandatory prefix of every object name"),也是物理表名前缀;D3 下客户在配对包里新建对象名为<ns>_custom_<short>。扩展字段走__c后缀(D10),不带名字空间。两点与本卡直接相关:validateObjectNamespacePrefix(packages/spec/src/kernel/namespace-prefix.ts:33)对任何sys_开头的对象名直接放行。于是今天唯一能建出的sys_custom配对包,其对象无论叫sys_custom_x还是sys_whatever都过前缀门;ADR-0029 D4 的「app 不得定义sys_*」在代码里仍无执行点(RESERVED_NAMESPACES仍是{'base','system'})。这是现状缺口,不随本卡字母变化而消失,只有 (a) 把名字空间推离sys_前缀才顺带避开。NAMESPACE_CONFLICT在两处:注册表installPackage(ADR-0048 Phase 1 门,registry.ts:7729,对sys豁免、对声明依赖的共有者豁免)和 ensure 门(protocol-customization-package.ts:352,比注册表更严:不认任何共有者)。所以修 (c) 要改两处,只改一处不够:ensure 门放行后installPackage仍会因sys_custom非共享而抛NamespaceConflictError(配对包声明的依赖是持sys的 base,不是持sys_custom的包,D5 的 parent/sibling 两臂都不命中)。系统包装了什么、定制对客户有没有意义: 读码:
com.objectstack.plugin-auth带pages、dashboards(system_overview,#29567 的实测路径)、导航贡献;com.objectstack.audit带 4 个对象及其 listViews、翻译;plugin-email带邮件模板;三个 cloud-connection UI、mcp.connect-agent-ui、connector-credentials-ui、settings.setup-nav只带pages+ 导航。按OVERRIDE_TYPE_CLASSIFICATION(customization-package.ts),可整体覆盖的是 view/dashboard/report/translation/email_template/page/app 等;对象只能扩展(__c字段 + 显示属性白名单,D10);权限集/职位/能力excluded(D6)。所以「定制 plugin-security」能做的是:给sys_permission_set等加__c字段、改标签、覆盖它的页面;⛔ 不能改权限集本身,不能改字段类型/必填/引用。安全面:ensure 门要manage_metadata,walled 姿态下租户管理员 403、只有运营方能建(D3「Walled postures」);覆盖平台 Setup 页面会换掉管理员看到的页面但不改数据面权限。读码判断:风险由既有闭合清单与姿态门兜住,与定制 HotCRM 同级;真正的额外风险是上面那条sys_前缀放行,它让配对包能造出看起来像平台对象的sys_x,这需要独立修,不是本卡的字母能决定的。控制台两边都需要什么: PR #29591 从列出行的
writable: false推出「提供自定义」,并已为「manifest 无版本」这一 409 原因在客户端禁用按钮;它读不到注册表的名字空间占有关系。(a)/(c) 落地后系统包不再撞 422,控制台不需新判定;剩余的 409(名字空间过长、非 SemVer)极少、且门的句子本身可读。(b) 则必须新增服务端判定键(如customizable),由已存在的纯函数customizationPairEnsureRefusal(ADR-0146 D11,#28576)按行计算,控制台改读它——这是 (b) 的必做项,不是可选项。customizationNamespace);已建的sys_custom配对包要迁移plugin_auth_custom_x);实测按 id 推导不可行:名字空间上限 20 字符、base 侧上限 13(MAX_CUSTOMIZATION_BASE_NAMESPACE_LENGTH),18 个 id 的末段里activation-ledger(17)、plugin-security(15)、service-automation(18)、connect-agent-ui(16)、connector-credentials-ui(24)超限,三个 cloud-connection UI 末段同为ui又互撞;spec 明写「不截断不哈希」,所以 (a) 实际 = 每个系统包手写一个新键protocol.ts文档块(席 5 冻结中)customizable(specSERVED_ROW_VERDICTS+ runtimedomains/packages.ts),由customizationPairEnsureRefusal计算;若「都不可」则 ensure 门对 scopesystem/cloud回 409customizationPairEnsureRefusal文档块:"its step 1 fails")disableable行判定,+774/−7,8 文件);不触protocol.ts主体<ns>_custom:sys的 18 个包各有自己的.custom,全部落在sys_custom,对象名按 ADR-0029 D3 单一所有者coOwnersOf增一臂:base 名字空间可共享时,其各配对包互为共有者;其余冲突(非配对包占了crm_custom)照旧 422sys_custom_<short>,和平台对象统一叫sys_<short>一个道理;两个配对包起同名对象 → 响亮422 OBJECT_OWNERSHIP_CONFLICT。D3 Q4 规则、manifest 键、名字空间语法都不动protocol.ts,文档块句子仍为真业务含义直译:
sys),它的定制层也是一个东西(sys_custom),每个系统包仍各有自己的定制包与导出——类似 Salesforce 的标准对象:客户定制都落在同一客户层,不按平台内部模块分名字空间。四维(业务立场):
sys定下的「共享名字空间、单一对象所有者」规则,把它对称地用到sys_custom,不新增拼法;(a) 新增一条推导规则或一个 manifest 键,还把内核拆包暴露进客户对象名,而且按 id 推导已实测撞 20 字符上限;(b) 把一次偶然的「先到先得」或一次关门写进契约。sys_custom即对,同名对象被OBJECT_OWNERSHIP_CONFLICT响亮拒绝;(a) 下 AI 要在新键与推导之间猜拼法;(b-部分) 下失败与否取决于点击顺序,AI 无法从声明推出结果。各选项共同的静默面是namespace-prefix.ts:33对sys_的放行——配对包能造出像平台对象的sys_x而不被拒,须另立卡堵上。os-decision-facets
① 项目长远合理性: (c) 把 ADR-0029 D3 为
sys定的「共享名字空间、单一对象所有者」对称用到sys_custom,不加拼法;(a) 加推导/键并暴露内核拆包;(b) 把先到先得或关门写进契约。② 实际业务拉动: #29591 之后 18 个系统包里 17 个的「自定义」必在首次保存失败;D11 迁移对系统包条目只能迁一个包的覆盖行。
③ 防 AI 犯错: (c) 照 D3 Q4 写即对,同名对象响亮 422;(b-部分) 结果取决于点击顺序;各选项共有的静默面是
sys_前缀放行,另立卡。④ 创业阶段不扩散: (c) 零新键零新语法;(b) 加一个永久行判定键;(a) 加 manifest 键或改语法并迁移已建配对包。
Prior rulings read: "customization pair system package namespace" → 21 hits(均为本卡、#29567、QA 运行与 0146 落地卡,无裁决);"NAMESPACE_CONFLICT sys_custom" → 2;"ensure-customization system package" → 6;"sys_custom" → 3;"customizationNamespace shared namespace" → 2;"system package customizable .custom" → 8;"shareable namespace sys custom pair" → 0;is:pr "customization pair namespace sys" → 19(#28512、#28850、#29587、#29103 皆未裁系统包);ADR-0146 D3/Q4、ADR-0029 D3、ADR-0048 D3、ADR-0070 D1 均未裁「系统包是否可定制」→ none;thread: none
推荐:(c)。 只看①选 (c);②③④ 是否翻转:否。不取 (a):按包 id 推导已实测撞名字空间上限与末段互撞,实际要为每个系统包手写新键、改协议、迁移已建配对包,并把内核拆包暴露进客户对象名。不取 (b):「部分可」是把点击顺序包装成规则,「都不可」与 ADR-0070 D1 的覆盖许可和 D11 迁移相抵,且为此要加一个永久判定键。置信缺口:(1) 本席未在跑起来的栈上验证 (c) 两处判定改完后第二个配对包能装入且
sys_custom_<short>对象注册、导出、再安装都通——coOwnersOf新增一臂是读码估计;(2) ADR-0048 D3 把改动「shareable set / install-gate semantics」列为自己的非目标,(c) 不改isShareableNamespace的集合而是改共有者来源,是否仍需在 0048 记一条修订注由 spec 席判;(3) 主流平台的类比(Salesforce 标准对象定制落同一客户层)是本席的记忆,未查文档。裁后执行: (c) ⇒ 席 2 认领:一个 server 车道 PR,改
protocol-customization-package.ts#customizationPairEnsureRefusal的占有判定与objectql/src/registry.ts#coOwnersOf(新增「共享 base 名字空间的配对包互为共有者」一臂),测试:同一dev:crm上三个系统包 ensure 均 201、两个配对包起同名对象 422、非配对包占crm_custom仍 422;ADR-0146 D3 加修订注(Q4 规则不变,补「共享 base 名字空间的配对包共享其_custom」),ADR-0048 D3 是否加注由 spec 席定;不动protocol.ts;落地后在 #29567/#29591 留注,控制台无需跟进;另立卡:namespace-prefix.ts对sys_的放行应只对sys名字空间的包生效。(a) ⇒ 先立 spec 卡定新键拼法与迁移,再派 server;#29591 在此之前不宜合并。(b) ⇒ 席 2 派 server PR 新增customizable行判定(由customizationPairEnsureRefusal计算),ui 车道跟进读它;若裁「都不可」,ensure 门对 scopesystem/cloud回 409 并在 ADR-0146 D3 记例外、ADR-0070 D1 的覆盖句同步改。相关: #29567 · PR #29591 · PR #29587(已合并,只读包拒绝语指向
.custom路线)· #28512 · #28467 · #28850 · #28308 · ADR-0146 D3/D5/D10/D11 · ADR-0029 D3/D4 · ADR-0048 D3 · ADR-0070 D1。State:
pm:queue→needs-user-decision, recorded by this comment.Generated by Claude Code
objectstack-fleet commentedon Nov 6, 2026
director#2 · os-tesla · session_01L9qibxdiybU8ehPKBroYE3
Brief-claimed-by: session_01L9qibxdiybU8ehPKBroYE3 · batch #437 · 2026-11-06T04:58Z
Director seat (the maintainer's decisions thread). Presented to the maintainer in batch #437 (two items: #29594 customization-pair namespace for shared
sys, and #29484 drained connector keys at rest in history snapshots) with the latest analysis on this card as the seat's reading; the maintainer's letter follows in a ruling comment. Rule level only on the security item.objectstack-fleet commentedon Nov 6, 2026
director#2 · os-tesla · session_01L9qibxdiybU8ehPKBroYE3
Ruling: batch #437 item 1 · customization unit = App (option d′ of the analysis) · maintainer 「① 定制单位 = App;同意」 2026-11-06T17:23Z
Director seat (the maintainer's decisions thread). Presented in batch #437 (brief-claimed here at 6677317101). The maintainer asked for a multi-scenario analysis (decision doc "#29594 定制包命名空间冲突 · 多场景分析"; summary
/mnt/project-files/decisions/2026-11-06-29594-scenario-analysis.md), set the premise at 2026-11-06T11:04Z 「hotcrm 四个包一个命名空间,但是作为套件一次性安装,也是一个版本。内核包也是。」, and ruled point ① at 2026-11-06T17:17Z. Points ② and ③ below are recorded as verified fact and as a pending follow-up; neither changes ①.What the ruling binds (①, ruled):
type: apppackage and thetype: module/pluginpackages its artifact brings; ADR-0130 D1/D6: one artifact, one version). One pair per App:<app id>.custom(HotCRM →app.objectstack.hotcrm.custom), namespace = the App package's own namespace +_custom, dependency = the App's^major. A module never owns a pair. Customize on any object of any member lands in the App's pair; the ensure door returns the App's existing pair (200) instead of 422. 422NAMESPACE_CONFLICTstays for a namespace held by a package outside the App's artifact (registrycoOwnersOf()arms unchanged).^major(18.x today: the changesetfixedgroup of 109 packages ships one version;resolveRuntimeVersion()), never the kernel manifests' ownversionfields (0.1.0–3.x, unrelated to release).<ns>_custom; (e) an explicit split key — deleted, a vendor who wants separate customization ships a separate App (ADR-0130 D6). ADR-0019 and ADR-0130 are not amended; only ADR-0146 D3 moves from "one pair per read-only package" to "one pair per App / platform suite".customizableverdict (and the pair a Customize lands in); the console stops offering Customize where it would fail (fix(studio): open system-scope packages read-only, with Customize #29591's console half follows indomain:ui).sys_pass-through closed:validateObjectNamespacePrefixaccepts asys_object name only from a package whose namespace issys.② verified (fact vs goal): one platform version = fact. "Kernel always installed in full" = goal, not today's behaviour:
servemounts by tier preset (minimal= core;default/full= core/i18n/ui/ai/auth), by the stack'srequirescapabilities, by the always-on slate (PLATFORM_ALWAYS_ON_CAPABILITIES), with Security/Audit intry { } catch { // optional }and no Auth on a cloud host kernel. Design constraint recorded: the platform pair tolerates an absent kernel package (its overrides are kept and marked, never refuse the install — the same orphan handling as a module removed in a later App version), and thecustomizableverdict is served for the packages present. Converging the tiers is a separate card, not this one.③ pending (kernel whitelist in step 1): proposal before the maintainer — open a short kernel whitelist in the first batch (Users, Organizations, approval requests/forms, email templates; fields
__c, forms/views/pages, templates; never security, credentials, audit, queue/job/mailbox/realtime/datasource internals, settings), declared by a speccustomizablekey, judged at every write into the pair per overridden object's package, with the ~20startsWith('sys_')"system object" heuristics replaced byisSystem/hasPlatformObjectPrefix()first. Until that answer, kernel objects stay NOT customizable; a second ruling comment here records it.Workload:
domain:server(ensure door by App;pairOf(base) = customizationPackageId(appOf(base))replacing the purecustomizationPackageId(base)at ~20 call sites with behaviour unchanged first;customizableverdict; orphan-tolerant platform pair) ·domain:spec(ADR-0146 D3 amendment, Tier H; platform pair id + platform-version dependency;sys_gap) ·domain:uifollow-up (#29591 reads the verdict; an App's pair folds under the App) · 5–7 PRs · M–L · contract review on the ensure door and the lookup refactor · rehearse on HotCRMdev:crm(sales + 3 modules → oneapp.objectstack.hotcrm.custom) before the ensure-door change lands.Execution: the server lane dispatches, first PR = the lookup refactor (no behaviour change), then ensure door + verdict with
Ruling-ref:naming this comment; the spec seat files the ADR-0146 D3 amendment as its own Tier H card (os-zhuang reviewer); ③ lands as a second ruling comment here when the maintainer answers. Label strokes are the dispatching seat's.Prior rulings read: ADR-0019 D1–D4 · ADR-0130 D1/D2/D6 · ADR-0146 D3/Q4/D10/D11 · ADR-0029 D3 · ADR-0070 D1 · brief 6677317101 · server seat 2 analysis 6588653995.
objectstack-fleet commentedon Nov 6, 2026
domain:spec#1 · os-tesla · session_01L9qibzzqHZdAVqgBAvxXKB
Pointer · 2026-11-06T17:28Z · ⛔ not a claim
The ADR-0146 D3 amendment that ruling 6691022506 gives the spec seat is filed as #31014 (Tier H,
domain:spec, revieweros-zhuang). It covers the App as the customization unit, the platform pair id, the platform-version^18dependency, the orphan-override tolerance, and kernel objects staying not customizable until ③. Spec seat 1 dispatches it now. The server lane's ensure-door PR cites it once merged.23 remaining items
objectstack-fleet commentedon Nov 7, 2026
domain:server#2 · marchtian · session_017ihL2fBJ6xsLWJsnNJGMMx
Claim: PM loop round 1 · #29594 PR 3 of 3 (appOf resolves App membership; the ensure door answers the App pair) · 2026-11-07T12:05Z · resumes release 6704508301 (
domain:server#2, previous holder)Session:
session_017ihL2fBJ6xsLWJsnNJGMMxAccount:
marchtian(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-29594-app-custom-pair(the card thread's designated branch)Worktree:
objectstack-issue-29594Domain:
domain:serverSeat:
domain:server#2File surface:
packages/metadata-core/src/customization-pair-lookup.ts;packages/metadata-protocol/src/protocol-customization-package.tsand the pair call sites PR 2 threaded;packages/runtime/src/domains/packages.ts;packages/lint/src/validate-customization-namespace.ts;packages/cli/src/utils/override-declarations.ts;packages/cloud-connection/src/marketplace-install-local-plugin.ts;packages/objectql/src/plugin.ts; the dogfood helpercustomization-pair.ts; their tests; docs where a sentence becomes false; one changeset. ⛔packages/spec/**(thecustomizablelist-row flag needs a spec sub-card first), ⛔ui/**(the sixui/packages/app-shellcall sites are adomain:uifollow-up). Stop on breach; explain in the report.Container & model:
L,mode:thread,model: TIER_DEFAULT(dispatch-gates --tier: no path-derived mandate),effort: default(the thread runs at the project's default effort); the seat runs a CONTRACT_REVIEW_TIER review on the PR because served answers change.Clause-②: no
Metadata-protocol: semantic
Responsibility: n/a — not a defect card (ruling-implementation card; ruling 6691022506 gives the server lane the execution)
Thread-read: 6704508301
Premise: holds. On
origin/mainaa3e21cc,customization-pair-lookup.tsstill hasvoid installed;(appOf is the identity), and the gate named in release 6704508301, ADR-0146 amendment PR #31027, is merged (8d13c0b3bd, 2026-11-07T08:08Z).Serial constraints cleared: #30884 PR 2 would share
marketplace-install-local-plugin.tsandobjectql/src/plugin.ts; it is not claimed (waits on maintainer consent), so it queues behind this PR. No open PR edits this surface (open PRs read at claim time: #31093, #31013, #31111, #31088, #29963 and mailboxes).Clause-②: nocites the contract text that negates the refusal this PR removes: ADR-0146 D3 as amended by #31027 and ruling 6691022506 ("the ensure door returns the App's existing pair (200) instead of 422"). Order: project fileserver-seat-2/orders/29594-pr3.md, delivered as the card thread's opening message.Refs #29594(the PR carriesFixes).objectstack-fleet commentedon Nov 7, 2026
domain:server#2 · marchtian · session_017ihL2fBJ6xsLWJsnNJGMMx
Claim amendment · #29594 · 2026-11-07T12:06Z · amends claim 6706721940
Branch:
claude/project-thread-oj61dz(the card thread's harness-pinned push branch; platform exception toclaude/issue-<n>-<slug>; replacesclaude/issue-29594-app-custom-pair, which carries no commits)Everything else in claim 6706721940 stands.
objectstack-fleet commentedon Nov 7, 2026
domain:server#2 · marchtian · session_017ihL2dM9PvPPB9BMVdhf2R
os-dev-report
{ "issue": 29594, "status": "done", "branch": "claude/project-thread-oj61dz", "pr": "https://github.com/objectstack-ai/objectstack/pull/31134", "head": "016eb4599513eaa35897eb1a09a9aeafa759e6bb", "session": "session_017ihL2dM9PvPPB9BMVdhf2R", "premise_still_valid": true, "summary": "PR 3 of 3: appOf reads the installed manifests (type app = its own App; else the declared App, directly or through declared parents; several -> the one holding the namespace, else no_single_app; kernel and App-less packages keep their own answer), so the ensure door answers the App's one pair <app id>.custom for every member (201 once, 200 after; basePackageId = the App). Reads serve legacy per-package pairs beside the App's pair through pairsOf/isPairOf (Q12); a legacy member pair holding the App namespace gets 409 naming it, an outsider still 422 NAMESPACE_CONFLICT; the App pair's write closure reaches the App's members; census reads the declared base; uninstall refuses for the pair named for the package; lint's namespace advisory skips App members. packages/objectql/src/plugin.ts is NOT edited (no overlap with #31093).", "n1_rule": "Declared exception, no move in this PR: a legacy per-package pair (e.g. namespace crm_svc_custom) stays its own installed package, served beside the App's pair via pairsOf. When a move ceremony is built, its objects keep their crm_svc_custom_<short> names as a declared exception; the App's pair is NOT given a namespace set. Why: no live instance to move; renaming a data-holding table is refused; a namespace set would permanently widen what the App pair owns, a declared exception stays visible and bounded. N2 holds (a package declaring no App keeps its own pair; pinned).", "rehearsal": { "setup": "HotCRM dev:crm not reachable from this checkout -> in-repo fixture of the same shape, nothing stubbed: App app.objectstack.hotcrm (ns crm) + modules .service/.revenue/.marketing (type module, ns crm, each declaring the App ^6), engine.registerApp on SqlDriver :memory:, real HttpDispatcher.handlePackages and real appOf (packages/runtime/src/domains/meta-object-extension-item.test.ts).", "ensure_answers": "[[app.objectstack.hotcrm,201,app.objectstack.hotcrm.custom],[.service,200,same],[.revenue,200,same],[.marketing,200,same]]; one pair installed", "pair_id": "app.objectstack.hotcrm.custom (namespace crm_custom, dependency app.objectstack.hotcrm ^6)", "read_back": "object_extension sla_tier__c on module object crm_service_case saved+published into the App pair; getMetaItem, getMetaItemLayered and the registry read it back; extenders [app.objectstack.hotcrm.custom]; a record insert stores sla_tier__c 'gold'" }, "ablations": [ "422 kept outside the App (protocol-customization-package.ts same-App guard forced true): anchor x1->x0, blob 8f868ff7c6ba->1ad2eed1b3de, dist preflight marker in 2 files -> Tests 3 failed | 40 passed (incl. 'the namespace held by a package outside the App: 422 NAMESPACE_CONFLICT still, asked from a module'); restored blob==HEAD, dist --absent ok -> 3 files 87/87 green", "kernel 409 (kernelScope check disabled): blob 8f868ff7c6ba->2f0051e03486 -> Tests 5 failed | 59 passed (system/cloud ensure, kernel legacy pair, flow clone both scopes); restored blob==HEAD -> green", "App resolution (appOf single-App answer -> the package itself): blob 16a7bd55e08b->ee33f4027082 -> Tests 3 failed | 63 passed (module-first ensure, legacy-pair 409, rehearsal); restored blob==HEAD, dist --absent ok" ], "tests": "metadata-core 25 files/989; metadata-protocol 320 files/22524 (+26 skipped) + kernel-scopes pin 2/2; objectql 564 files/11422, registry 49/698 after the uninstall fix, uninstall file 11/11; runtime 554 files/8914 (+19 skipped), App-unit files 3/87; lint 197 files/8331 (re-run after the alias: one file raced a concurrent lint/dist rebuild, re-run alone 6/6); cli unit 396/5576; cloud-connection 935/935 (full re-run after the uninstall fix); service-automation 264/3431; service-datasource 72/1279; typecheck metadata-core, metadata-protocol, objectql, lint, runtime, qa/dogfood :: exit 0", "gates": "dispatch-gates --commands: 111 derived at 5ac6ed0f, all 111 :: exit 0 (first pass: 3 exit 3 for missing builds / the pinned teardown fixture commit, check:test-source-alias exit 1 -> fixed in 5ac6ed0f with an anchored metadata-core alias in lint's vitest config and a module-top import '@objectstack/metadata' in the rehearsal file; prerequisites built; those four plus every changeset/alias/test-input gate re-run, exit 0). dispatch-gates --ran: 111 derived, 111 run, 0 NOT-MEASURED, 0 UNRUN :: exit 0. pnpm lint :: exit 0. check-platform-checklist --changed-from origin/main :: exit 0. origin/main merged twice (latest 98583cd6 -> merge 016eb459, no overlapping file, same derived list). CI check-runs on #31134: pending at report time.", "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm declared (see open_questions 1)", "mcp_calls": "0", "api_writes": "0 direct REST writes. Relay writes as objectstack-fleet[bot]: pr_create #31134 (run 42379171667, 20001 bytes sent = stored, draft, no assignees/labels/reviewers) + this comment via post-stamped.mjs. git push x3 to the branch. Reads via gh api GET (issue, comments, PR read-back).", "open_questions": [ { "question": "The claim says Clause-②: no, but three things move a public face: (a) a package that reaches two Apps and whose namespace picks neither got its own pair 201 before and now gets 409; (b) a legacy member pair holding the App namespace now answers 409 RESOURCE_CONFLICT where a member's ensure got 201/its own pair before; (c) @objectstack/metadata-core gains exports (pairsOf, AppOfRefusal, PairOfRefusal) and appOf's return widens to string | undefined. PR body and changeset carry the claim's line verbatim.", "options": ["A keep Clause-②: no (a and b have no live instance; c is the same widening PR 2 declared)", "B amend to Clause-②: yes (narrowing) for (a)/(b): BREAKING banner in the changeset, dogfood battery run"], "recommendation": "B, because a 201 that becomes a 409 is a narrowing by the fleet's own criterion even without a live instance; the seat writes the claim amendment and I add the banner on request" } ], "security_findings": "none", "out_of_scope_findings": [ "class: b · reach: domain:ui · the six ui/packages/app-shell sites that derive a pair as customizationPackageId(base) and must read the ensure door's / server's answer for a module: src/views/metadata-admin/ResourceListPage.tsx:211, src/views/studio-design/AutomationsFlowRail.tsx:367, src/views/studio-design/BuilderLanding.tsx:163, src/views/studio-design/CustomizationMark.tsx:143, src/views/studio-design/customizationMarks.ts:58, src/views/studio-design/packages-io.ts:239 · carrier: seat · noted, not filed", "class: b · reach: named producer · packages/metadata-protocol/src/protocol-write-gates.ts pairOfAddedMember/findPairAddedMemberRow read only the App's pair, so a legacy member pair's added bundle members are not served (Q12 gap) · carrier: seat · noted, not filed", "class: c · Studio picker closure (lint resolvePackageClosure) is not widened to App members; no ceremony re-identifies a legacy pair as the App's pair (Q12 leaves it open) · noted, not filed", "class: c · Skills-docs follow-up owed: skills/objectstack-platform/SKILL.md 'Customizing an installed package' says one pair per base · governed path · noted, not filed", "class: c · the list-row customizable flag needs a spec sub-card (packages/spec out of scope) · noted, not filed" ] }objectstack-fleet commentedon Nov 7, 2026
domain:server#2 · marchtian · session_017ihL2fBJ6xsLWJsnNJGMMx
Claim amendment · #29594 · 2026-11-07T13:58Z · amends claim 6706721940 (as amended by 6706733388)
Clause-②: yes (narrowing)
Metadata-protocol: semantic
File surface: adds
packages/objectql/src/registry.ts(the uninstall refusal reads the pair named for the package), the remainingpackages/metadata-protocol/src/protocol-*.tspair readers, andpackages/lint/package.json/packages/lint/vitest.config.ts/pnpm-lock.yaml(test wiring). Nothing inpackages/spec/**orui/**.Why the line changes (measured on draft PR #31134, head
016eb4599513eaa35897eb1a09a9aeafa759e6bb, report 6707849840):@objectstack/metadata-coregrows (export *fromcustomization-pair-lookup.jsnow carriespairsOf,AppOfRefusal,PairOfRefusal). The 422 → 200 change for a member of the same App stays the cited-contractnoit was (ADR-0146 D3 as amended by docs(adr): ADR-0146 D3 — one.custompair per App, and one platform pair for the kernel suite #31027; ruling 6691022506), but the new exports are not covered by that text.201with its own pair and is now409(merge question 11); a module whose earlier per-package pair holds the App's pair namespace was201/200and is now409 RESOURCE_CONFLICTnaming that pair (merge question 12). In TypeScript,appOfnow returnsstring | undefinedandpairOf's refusal union widens, which breaks a caller that read the old return type.Consequences for this PR (the dev writes them from the diff): PR body and changeset carry
Clause-②: yes (narrowing)at line start, a BREAKING banner and the migration lines (the two 409 answers, theappOfreturn type), and exactly one ADR-0087 disposition marker; the full dogfood battery runs once on the final head (os-dev rule for a(narrowing)arm). The seat owes a CONTRACT_REVIEW_TIER review of the whole non-prose diff before ACCEPT. The card stays in the server lane: ruling 6691022506 assigns its execution here, and nothing in the diff touchespackages/spec/**.Everything else in claim 6706721940 stands.
objectstack-fleet commentedon Nov 7, 2026
domain:server#2 · marchtian · session_017ihL2dM9PvPPB9BMVdhf2R
os-dev-report
Supplement to 6707849840, on claim amendment 6707871792. Fields not listed stand as reported there.
{ "issue": 29594, "status": "done", "branch": "claude/project-thread-oj61dz", "pr": "https://github.com/objectstack-ai/objectstack/pull/31134", "head": "775c97b55e9e5c06f3c29fc6c0c7d4e96a790637", "session": "session_017ihL2dM9PvPPB9BMVdhf2R", "premise_still_valid": true, "summary": "Applied claim amendment 6707871792: the changeset and PR body carry `Clause-②: yes (narrowing)` at line start (Metadata-protocol: semantic unchanged), a BREAKING banner with one remedy line each for the two new 409 answers (merge questions 11 and 12) and for appOf's string | undefined return, and exactly one ADR-0087 disposition in the changeset: not-required (no-migration-prescription). PR title now carries `!`. No code changed since 5ac6ed0f; the only new commit is the changeset (775c97b5).", "gates": "check-adr-0087-registration --base origin/main :: exit 0 (exemption noticed); --self-test :: exit 0; check-changeset-no-major :: exit 0; check-empty-changeset :: exit 0; pnpm check:changeset-fixed :: exit 0; pnpm check:changeset-gate-self-tests :: exit 0. The 111-command derivation is unchanged (same file set); its run at 5ac6ed0f stands for the code.", "dogfood_battery": "775c97b55e9e5c06f3c29fc6c0c7d4e96a790637 · os-verify-lock: VERDICT command-exit 0 · held the lock 1971s · Test Files 435 passed | 3 skipped (438) · Tests 3609 passed | 41 skipped (3650) · no failure", "mcp_calls": "0", "api_writes": "0 direct REST writes. Relay as objectstack-fleet[bot]: issue_patch #31134 title+body (run 42382371991, 23006 bytes sent = stored) + this comment via post-stamped.mjs. git push 1 (775c97b5).", "open_questions": [], "security_findings": "none", "out_of_scope_findings": [ "The relay refuses a PR-body edit without a `<!-- signed: … -->` trailer as its last line, so #31134's body now ends with that trailer after the Generated-with line; the order's 'no HTML comment other than the attribution line' could not hold for an edited body · noted, not filed", "origin/main moved to af3451b08bc3 after the battery; not merged, so the battery's head stays the PR head — the seat's pre-landing merge covers it" ] }objectstack-fleet commentedon Nov 7, 2026
domain:server#2 · marchtian · session_017ihL2fBJ6xsLWJsnNJGMMx
Claim: PM loop round 1 · #29594 PR 3 of 3 (appOf resolves App membership; the ensure door answers the App pair) · 2026-11-07T14:38Z · consolidates this session's claim 6706721940 and amendments 6706733388 and 6707871792 into one
Claim:(the closing-target guard reads only aClaim:comment'sBranch:line); resumes release 6704508301Session:
session_017ihL2fBJ6xsLWJsnNJGMMxAccount:
marchtian(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/project-thread-oj61dz(the card thread's harness-pinned push branch; head of PR #31134)Worktree:
objectstack-issue-29594Domain:
domain:serverSeat:
domain:server#2File surface:
packages/metadata-core/src/customization-pair-lookup.ts;packages/metadata-protocol/src/protocol-customization-package.tsand the pair call sites PR 2 threaded;packages/runtime/src/domains/packages.ts;packages/lint/src/validate-customization-namespace.ts;packages/cli/src/utils/override-declarations.ts;packages/cloud-connection/src/marketplace-install-local-plugin.ts;packages/objectql/src/plugin.ts;packages/objectql/src/registry.ts;packages/lint/package.json,packages/lint/vitest.config.ts,pnpm-lock.yaml(test wiring); the dogfood helpercustomization-pair.ts; their tests; docs where a sentence becomes false; one changeset. ⛔packages/spec/**(thecustomizablelist-row flag needs a spec sub-card first), ⛔ui/**(the sixui/packages/app-shellcall sites are adomain:uifollow-up). Stop on breach; explain in the report.Container & model:
L,mode:thread,model: TIER_DEFAULT(dispatch-gates --tier: no path-derived mandate),effort: default(the thread runs at the project's default effort); the seat runs a CONTRACT_REVIEW_TIER review on the PR because served answers change.Clause-②: yes (narrowing)
Metadata-protocol: semantic
Responsibility: n/a — not a defect card (ruling-implementation card; ruling 6691022506 gives the server lane the execution)
Thread-read: 6704508301
Premise: holds. On
origin/mainaa3e21cc,customization-pair-lookup.tsstill hasvoid installed;(appOf is the identity), and the gate named in release 6704508301, ADR-0146 amendment PR #31027, is merged (8d13c0b3bd, 2026-11-07T08:08Z).Serial constraints cleared: #30884 PR 2 would share
marketplace-install-local-plugin.tsandobjectql/src/plugin.ts; it is not claimed (waits on maintainer consent), so it queues behind this PR. No open PR edits this surface (open PRs read at claim time: #31093, #31013, #31111, #31088, #29963 and mailboxes).Clause-②: yes (narrowing)for the reasons in amendment 6707871792 (new@objectstack/metadata-coreexports; two ensure-door answers move from accepted to409, merge questions 11 and 12;appOfreturnsstring | undefined). The 422 → 200 change for a member of the same App is covered by ADR-0146 D3 as amended by #31027 and ruling 6691022506. Order: project fileserver-seat-2/orders/29594-pr3.md.Refs #29594(the PR carriesFixes).objectstack-fleet commentedon Nov 7, 2026
domain:server#2 · marchtian · session_017ihL2dM9PvPPB9BMVdhf2R
os-dev-report
Patch round for the contract review on #31134 (should-fix 1 and 2). Fields not listed stand as reported in 6707849840 / 6708291325.
{ "issue": 29594, "status": "done", "branch": "claude/project-thread-oj61dz", "pr": "https://github.com/objectstack-ai/objectstack/pull/31134", "head": "fb2fd21eb9d05368b49eb24320d1586b40dac847", "session": "session_017ihL2dM9PvPPB9BMVdhf2R", "premise_still_valid": true, "summary": "Should-fix 1: isPairRowRemoval asks isPairOf, so a legacy <module>.custom removes its own declared override past the sealed-removal refusal again; pairOfAddedMember became pairsOfAddedMember (private protocol method + host key) and findPairAddedMemberRow reads every pair in pairsOf order; the customizations read in runtime packages.ts lists extensions from every pair and readBaseContributions excludes every pair; the sealed declare-or-rename sentences and the undeclared-same-name refusal treat a legacy pair as a pair. No reader is App-pair-only, so the BREAKING list is unchanged; the changeset's Q12 bullet says so. Should-fix 2: packages/runtime/src/domains/meta-legacy-pair-served.test.ts, three cases, the legacy override written straight to the store as an earlier version left it.", "tests": "new file 3/3; metadata-protocol 322 files / 22527 (+26 skipped); objectql 564 / 11425; runtime 555 / 8923 (+19 skipped); typecheck metadata-core, metadata-protocol, objectql, runtime, lint :: exit 0", "ablations": [ "reads: pairsOf legacy arm disabled (metadata-core), blob 16a7bd55e08b->44efe1c0117c, dist marker present -> Tests 3 failed (3): by name / list / layered answered the shipped dashboard, the doc read answered the shipped doc, the save was refused declared_outside_pair; restored blob==HEAD, rebuilt, dist --absent ok -> 3/3", "removal: isPairRowRemoval back to the single pairOf, blob cca50ea0d34b->dad267a4800c, dist marker present -> Tests 1 failed | 2 passed: the legacy pair's doc removal answered 403 NOT_OVERRIDABLE 'sealed against removal'; restored blob==HEAD, rebuilt, dist --absent ok -> 3/3. (The dashboard removal stays green under this ablation: it passes through the dashboard's own overlay channel, so the doc case is the pin.)" ], "gates": "origin/main merged at fb2fd21e. dispatch-gates --commands: 111 derived, all 111 :: exit 0; --ran: 111 run, 0 NOT-MEASURED, 0 UNRUN :: exit 0; pnpm lint :: exit 0.", "dogfood_battery": "fb2fd21eb9d05368b49eb24320d1586b40dac847 · os-verify-lock: VERDICT command-exit 0 · held the lock 1684s · Test Files 435 passed | 3 skipped (438) · Tests 3609 passed | 41 skipped (3650) · no failure", "mcp_calls": "0", "api_writes": "0 direct REST writes. Relay as objectstack-fleet[bot]: issue_patch #31134 body + this comment via post-stamped.mjs. git push 1 (fb2fd21e).", "open_questions": [], "security_findings": "none", "out_of_scope_findings": [ "nits not taken (optional): a legacy pair of a module the newer App version no longer delivers resolves to itself and answers 422, not Q12's 409; the lint advisory stays silent for a no-single-App package (it judges namespace length, and such a package has no pair at all); the lint -> metadata-core workspace dependency is named in the PR body and changeset only", "origin/main moved 4 commits after the fb2fd21e merge; not merged again so the battery's head stays the PR head" ] }objectstack-fleet commentedon Nov 7, 2026
domain:server#2 · marchtian · session_017ihL2dM9PvPPB9BMVdhf2R
os-dev-report
Incremental-review round on #31134 (review 6709902226, should-fix A and B). Fields not listed stand as reported in 6709801932.
{ "issue": 29594, "status": "done", "branch": "claude/project-thread-oj61dz", "pr": "https://github.com/objectstack-ai/objectstack/pull/31134", "head": "481d40492ddfc7af48d6362f844571efc6f44d32", "session": "session_017ihL2dM9PvPPB9BMVdhf2R", "premise_still_valid": true, "summary": "A: the changeset bumps '@objectstack/runtime': minor beside the other four (same feat), and its title scope names runtime; the PR title does too. B: meta-legacy-pair-served.test.ts runs every case on both topologies (host-config kernel, environment kernel 'env_hotcrm'), so the environment kernel's removal goes through refusePackagedBaseRemoval and the `!pairRowRemoval` pass at protocol-diff-delete-load.ts:498. Nit taken: the packages.ts pair loop is no longer an if…for on one line. Nits not taken: extra pins for a legacy pair's added bundle member and its extensions in the customizations census.", "ablations": [ "protocol-diff-delete-load.ts:498 `!pairRowRemoval` removed (`'ablation-pair-row-guard'.length > 0` in its place): blob 9df68e01d38f->bee509989cbd, dist marker present in 2 files -> Tests 1 failed | 5 passed (6): the environment kernel's doc removal answered refusePackagedBaseRemoval's 'Metadata item 'doc/crm_service_guide' is provided by a managed package and is sealed against removal' (the host-config kernel's case stays green: it never reaches that block); restored blob==HEAD, git diff HEAD empty, rebuilt, dist --absent ok -> Tests 6 passed (6)", "isPairRowRemoval back to the single pairOf (the previous round's reading, 6709801932): the host-config doc removal red via the repository's intent gate; unchanged code since" ], "tests": "meta-legacy-pair-served 6/6; App-unit + legacy-pair files 3 files / 72; runtime src/domains/packages* 58 files / 1008; runtime typecheck :: exit 0", "gates": "origin/main merged at 481d4049. dispatch-gates --commands: 111 derived, all 111 :: exit 0; --ran: 111 run, 0 NOT-MEASURED, 0 UNRUN :: exit 0.", "dogfood_battery": "481d40492ddfc7af48d6362f844571efc6f44d32 · os-verify-lock: VERDICT command-exit 0 · held the lock 2423s · Test Files 435 passed | 3 skipped (438) · Tests 3609 passed | 41 skipped (3650) · no failure", "mcp_calls": "0", "api_writes": "0 direct REST writes. Relay as objectstack-fleet[bot]: issue_patch #31134 title+body + this comment via post-stamped.mjs. git push 1 (481d4049).", "open_questions": [], "security_findings": "none", "out_of_scope_findings": [] }objectstack-fleet commentedon Nov 7, 2026
domain:server#2 · marchtian · session_017ihL2fBJ6xsLWJsnNJGMMx
Landed — PR 3 · 2026-11-07T17:46Z
PR #31134 merged 2026-11-07T17:45Z as
486b0a9c94. An App and the modules that declare it now share one customization pair, merge questions 11 and 12 answer409, and a legacy per-package pair keeps being served and can remove its own override. The review record is the full Fable review at775c97b55e, the Fable delta atfb2fd21eb9, and the in-seat ACCEPT at481d40492d.Fixes #29594closed the card. The seat removespm:dispatched, and this seat's slot for the card is free.