feat(types,cloud-connection,lint,cli): ADR-0120 17.x 收尾 —— isolated 安装期姿态硬门 + D5c advisory + 成文契约扫荡 + 三姿态 conformance (#5081) - #5314
Draft
os-zhuang wants to merge 3 commits into
Draft
Conversation
…ted install gate, D5c advisory, truth sweep, three-posture conformance (#5081) ADR-0120's third and final 17.x block, on top of #5212 (driver D3+D4) and #5208 (spec vocabulary + D5a/D5b lint). D5e — the isolated-posture install gate. Installing an app that carries 'global' uniques on its own (non-sys) objects into an isolated environment now stops and lists each index; the installer confirms them as genuinely platform-wide or rewrites them to 'organization'. The confirmation is recorded in the install manifest ADR-0104 attestation style (what, who, when, under which posture) and is never re-asked. A stopped install registers nothing and writes no ledger entry. NEVER a boot-time warning (#4884): rehydrate does not evaluate the gate, and the two populations the gate cannot reach — pre-gate installs, post-install posture changes — are covered by the advisory form in `os doctor` / `os migrate plan`. The pure enumerator lives in @objectstack/types so the hard stop and both advisories read one classification. Declared-index bare `unique: true` counts (D1 makes it the positional spelling of 'global'); field-level `true` does not; sys_/base_ objects do not. D5c — new advisory rule `unique/legacy-organization-composite` for the S6 hand-written organization composite, pointing at the 'organization' respelling that closes its NULL hole (#5030). Advisory forever, never auto-fixed: opting in is a real D4 tightening. D6 — indexing.mdx §Two ways to say "unique" rewritten in the new vocabulary; schema.mdx §Uniqueness and tenancy rewritten as §Uniqueness and scope, replacing the "composite degenerates to the single-column one" claim #5030 falsified; cli.mdx drift-op table updated; references regenerated via gen:schema && gen:docs; skills/objectstack-data swept (and its non-existent `tenant_id` column corrected to organization_id). Conformance — one fixture app booted under single | group | isolated, each S-row's enforcement asserted with real violating inserts, the materialized unique key parts asserted byte-identical across all three, plus the one transition smoke the ADR asks for: a posture flip emits zero drift ops. Fixes #5081 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB
…0120-17x-closeout # Conflicts: # packages/cloud-connection/src/marketplace-install-local-plugin.ts
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 5 package(s): 114 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…ct, not `any` (#5081) The ADR-0120 D5e advisory's `SchemaStack.allObjects()` erased its service-lookup result to `any`, which `no-restricted-syntax` rejects (#4168/#4176/#4251): the lookup already returns the slot's contract, and the annotation switched that checking off while looking identical to code that has it. The `objectql` slot HAS a contract — `IObjectQLEngine`, whose `registry.getAllObjects()` is exactly what this reads — so this takes the prescription's first branch (pass the contract type) rather than the UNCONTRACTED_SLOTS registration. Concretely it buys what the rule is for: a rename of `registry` or `getAllObjects` now breaks this at compile time instead of silently returning zero objects and turning the advisory mute. `allObjects()` returns `unknown[]` accordingly; its only consumer, `collectGlobalUniques`, already accepts `unknown`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB
This was referenced Aug 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5081
ADR-0120 17.x 波的第三块,也是最后一块。前两块已在 main:#5212(driver 侧 D3+D4)、#5208(spec 词汇 + D5a/D5b lint)。
前置核验(都对着
origin/main实测,不是照抄 issue):#5212 的COALESCE(organization_id, '__global__')物化在driver-sql/src/schema-drift.ts与sql-driver.ts里在;#5208 的UniqueScopeSchema=boolean | 'global' | 'organization'与两条 lint 规则在;D5e 门不存在(反查零命中)。一个 issue 正文里的 D6 条目已经提前落地了:syncDeclaredIndexes文档块的「VERBATIM」表述已经被 #5212 收窄到'global'了,所以本 PR 没动它 —— 记在这里免得 reviewer 找不到对应 diff。D5e —— 装进
isolated环境的安装期硬门词汇本身是姿态无关的:作者说的是业务边界,没有任何索引形状读姿态。唯一的残留只在一个方向上 ——
isolated下组织就是不同客户,此时 app 业务对象上的'global'唯一既跨客户过度约束,又变成跨客户的存在性预言机(S10/S14)。落点(读代码定的,不是猜的):真正的安装链路是
MarketplaceInstallLocalPlugin.handleInstall(POST /api/v1/marketplace/install-local,os package install打的就是它),而「安装清单」就是LocalManifestSource那本 ledger。门放在冲突检查之后、hot-register 与任何 ledger 写入之前。InstalledManifestEntry.globalUniqueAttestation(确认了什么 / 谁 / 何时 / 在哪个姿态下问的),之后不复问。confirmGlobalUniques收true或明确 id 数组,只确认其中一条仍会在剩下那条上停住。isolated那个问题在single下从没被问过,按未确认处理(唯一不会静默放行跨客户约束的方向)。os doctor/os migrate plan的 advisory。判定里有三条是承重的,请 review 时特别看一眼:
unique: true'global'的位置式拼写,物理形状完全一样。排除它 = 整个 17.x 可以靠拼写绕过这道门unique: true'organization',Resolved #2 说永久合法sys_/base_对象纯枚举函数放在
@objectstack/types(只依赖 spec),因为硬门在cloud-connection、两个 advisory 在cli,而cloud-connection不依赖lint—— 放一处才能保证三个界面给出同一个判定。CLI:
os package install加--confirm-global-uniques,并把 409 渲染成可读的逐条清单。D5c —— 遗留手写组织复合索引的 advisory
新规则
unique/legacy-organization-composite。{ fields: ['name','organization_id'], unique: true }读起来像「每组织唯一」,物化出来却是普通复合索引,而 SQL UNIQUE 是 NULL-distinct 的:组织列为 NULL 的行上它什么都不约束(#5030)—— 单组织部署上那就是每一行。改写成unique: 'organization'(fields原样保留,driver 把已列出的组织列就地变 NULL-safe)正是补洞的动作。永远只是 advisory、永远不自动修:老拼写永久合法、零强制 drift,opt-in 是真实的物理收紧,要走 D4 的
recreate_index+ 重复预检。这条约束我写进了 adr-anchors 的 invariant 里。D6 —— 成文契约扫荡
content/docs/data-modeling/indexing.mdx:§Two ways to say "unique" 全节按新词汇重写(含os:check代码块),并新增遗留复合索引与isolated安装两小节。content/docs/protocol/objectql/schema.mdx:§Uniqueness and tenancy → §Uniqueness and scope。这里原本那句「单租户部署不受影响 —— 租户列是常量,复合索引退化为单列索引」正是 driver-sql: 单租户栈上 organization_id 恒为 NULL,#3696 的 (tenant, col) 复合 UNIQUE 因 NULL-distinct 而完全不生效 —— 字段级 unique: true 静默零约束 #5030 证伪过的原话(ADR D6.2 点名的那条),已替换为 D3 的 NULL-safe 事实。content/docs/deployment/cli.mdx:replace_unique_index/recreate_index两条补上 NULL-safe 形状与重复预检。content/docs/references/**:经gen:schema && gen:docs再生成,未手改。skills/objectstack-data/**:按 Resolved ✨ Set up Copilot instructions #2 的非规范性引导整体扫过,声明索引一律说清 scope,并新增一节讲'organization'的 NULL-safe 语义与「永远不写姿态」。顺带修掉那里长期在用的tenant_id—— 平台的列叫organization_id。thread 里那条 sweep 备注的核查结论:
examples/**、create-objectstack模板与os generate根本没有声明任何唯一约束,所以无可扫 —— 这是核查结论,不是漏掉了。真正的引导面是skills/,已扫。三姿态 conformance
同一个 fixture app 在
single | group | isolated三姿态下启动,逐 S 行用真实的违规插入断言 enforcement(S1/S2/S3/S4/S5/S6/S7/S8/S9/S11/S12),并逐姿态捕获物化出的索引键,断言三者逐字节相同。写在 driver 层而不是三次 kernel boot,是因为 driver 是唯一物化唯一约束的层 —— 姿态要是漏进形状里,只能漏在这里;三次 kernel boot 会用 100 倍的机器跑同一个
initObjects,却证明得更少(kernel boot 插不了那条违规记录)。相同性断言是那种「什么都没产出也会绿」的形状,所以配了一条对着期望键形状的正向断言 —— 三次都没建索引不会读成「一致」。这条不是纸面保险:第一次跑就是它红的(我把
'organization'声明索引的名字写错了,driver 用的是解析后的列名)。外加 ADR 只要的那一条 transition smoke:
single下建库、isolated下用一个全新 driver 重新打开(真实重启),drift op 为零。Reverse verification(先定方向再跑)
预测:把
if (gate.blocked)短路掉,断言 409 的那几条变红;而single/group断言 200 的那几条、以及 #4884 那条 rehydrate 测试仍然绿 —— 它们断言的是门的缺席,短路破坏不了。实测与预测一致:5 红 / 71 绿,红的正好是 HARD-STOPS、「停下的安装什么都不留」、「只问新约束」、「部分确认仍然停住」、「另一姿态的确认不算同意」。那几条绿的诚实价值是防误报(门开太宽),不是防漏报 —— 写在这里免得被读成比实际更强的保证。已还原并复跑绿。
验证
生成物同步(os-regen 四步)
中途 main 前进了 10 个 commit,按四步互保处理:
git merge origin/main(⛔ 无 rebase / 无 force-push)→git checkout origin/main -- <全部生成物路径>→ 全量重跑gen:schema / gen:docs / gen:api-surface / gen:spec-changes / gen:upgrade-guide→ 核对在飞单的条目还在。结果:重跑后生成物相对
origin/main的唯一 delta 就是我自己那处validation.zod.ts文档块(7+/3-)。#5289 的references/ui/theme.mdx、strictness counts ledger、api-surface.json全部与origin/main逐字节相同,即无一被吞。唯一的合并冲突在
marketplace-install-local-plugin.ts的 import 块,与 #5302(「八处把被降级的OS_MULTI_ORG_ENABLED当是不是多组织在读」)相撞。解法是丢掉resolveMultiOrgEnabled:#5302 删掉了本文件里它最后一处用法,并加了注释禁止再读那个被降级的布尔;我这边合并后保留resolveTenancyPosture+ 它新引入的postureEnforcesWall,再叠上 D5e 的 import。未随手修、已记录
packages/spec/src/data/object.zod.ts的TenancySchema.tenantField默认值是'tenant_id',而 driver 的computeTenantField隐式回落到organization_id—— 声明的默认值几乎没有消费方。与 ADR-0120 的契约不相干,按 Prime Directive #10 另开 issue,不在本 PR 修。🤖 Generated with Claude Code
https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB
Generated by Claude Code