Skip to content

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
mainfrom
claude/issue-5081-adr0120-17x-closeout
Draft

feat(types,cloud-connection,lint,cli): ADR-0120 17.x 收尾 —— isolated 安装期姿态硬门 + D5c advisory + 成文契约扫荡 + 三姿态 conformance (#5081)#5314
os-zhuang wants to merge 3 commits into
mainfrom
claude/issue-5081-adr0120-17x-closeout

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #5081

ADR-0120 17.x 波的第三块,也是最后一块。前两块已在 main:#5212(driver 侧 D3+D4)、#5208(spec 词汇 + D5a/D5b lint)。

前置核验(都对着 origin/main 实测,不是照抄 issue):#5212COALESCE(organization_id, '__global__') 物化在 driver-sql/src/schema-drift.tssql-driver.ts 里在;#5208UniqueScopeSchema = 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 写入之前

  • 逐索引列出并硬停;确认按 ADR-0104 attestation 风格留痕在 InstalledManifestEntry.globalUniqueAttestation(确认了什么 / 谁 / 何时 / 在哪个姿态下问的),之后不复问
  • 停下的安装什么都不留 —— 不注册、不写 ledger,作者改完元数据直接重试即可,不用先卸载。
  • 逐索引确认是有牙齿的:confirmGlobalUniquestrue 或明确 id 数组,只确认其中一条仍会在剩下那条上停住
  • 升级引入的约束会被问,老答案继续算数。
  • 另一个姿态下给的确认不算同意 —— isolated 那个问题在 single 下从没被问过,按未确认处理(唯一不会静默放行跨客户约束的方向)。
  • 永不做成启动期告警([schema-drift] Fresh DB boots "drifted": detector can't read COALESCE index columns, then tells the operator to --allow-destructive away a legitimate unique index #4884)。boot 期 rehydrate 不评估此门,并有一条专门的测试钉住这点。门够不到的两类存量(门禁上线前的安装、装后姿态变更)走 os doctor / os migrate plan 的 advisory。

判定里有三条是承重的,请 review 时特别看一眼:

拼写 算不算 finding 依据
声明索引 unique: true ✅ 算 D1 说它就是 'global' 的位置式拼写,物理形状完全一样。排除它 = 整个 17.x 可以靠拼写绕过这道门
字段级 unique: true ❌ 不算 它是 'organization',Resolved #2 说永久合法
sys_ / base_ 对象 ❌ 不算 S5 那批引擎幂等键天然平台级,每次安装都问一遍就是 #4884 的误报类

纯枚举函数放在 @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、「停下的安装什么都不留」、「只问新约束」、「部分确认仍然停住」、「另一姿态的确认不算同意」。那几条绿的诚实价值是防误报(门开太宽),不是防漏报 —— 写在这里免得被读成比实际更强的保证。已还原并复跑绿。

验证

pnpm --filter @objectstack/types test        →  7 files / 109 tests passed
pnpm --filter @objectstack/lint test         → 57 files / 1198 tests passed
pnpm --filter @objectstack/cloud-connection test → 14 files / 89 tests passed
pnpm --filter @objectstack/driver-sql test   → 61 files / 800 passed | 44 skipped
pnpm --filter @objectstack/cli test          → 70 files / 641 tests passed
pnpm --filter @objectstack/spec test         → 308 files / 7944 tests passed
typecheck(types/lint/spec/cli/driver-sql)   → 全部 Done
check:adr-anchors → OK(29 anchored)· check:nul-bytes → OK(5361 files)
check:doc-authoring → 362 files clean · check:skill-examples → 204 examples ✓
spec check:generated → All 9 generated artifacts up to date

生成物同步(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-)。#5289references/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.tsTenancySchema.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

claude added 2 commits August 4, 2026 17:57
…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
@vercel

vercel Bot commented Aug 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 4, 2026 6:55pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:data tests tooling size/xl labels Aug 4, 2026
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/cli, @objectstack/cloud-connection, @objectstack/lint, @objectstack/spec, @objectstack/types.

114 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via packages/cli, @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/api/data-flow.mdx (via @objectstack/cli)
  • content/docs/api/environment-routing.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/cli, @objectstack/lint, packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/backup-restore.mdx (via @objectstack/cli)
  • content/docs/deployment/cli.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/cli)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via packages/cli, @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via packages/cli)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/cli, packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authentication.mdx (via @objectstack/cli)
  • content/docs/permissions/authorization.mdx (via @objectstack/lint, @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/cli, @objectstack/spec, @objectstack/types)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/metadata-service.mdx (via @objectstack/cloud-connection)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/cli)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/cli, @objectstack/cloud-connection, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/cli, @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/cli, @objectstack/lint, @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/xl tests tooling

Projects

None yet

2 participants