Skip to content

console 的内嵌索引编辑器与 IndexSchema 漂移:where vs partial、枚举里的 brin —— 阻塞 #4001 对 IndexSchema 的收紧 #5247

Description

@xuyushun441-sys

Blocked-by: #5248

发现于 #4001 批 20(data/object.zod.ts 内层块收紧)的逐站点测量。本 issue 只记录,不在批 20 里修——批 20 因此有意保留 IndexSchema 未收紧,是该批 14 个站点里唯一没关的一个。

症状

objectui 的 console 为 object.indexes[] 里的条目自带了一份手抄的 JSON-Schema:

  • packages/app-shell/src/views/metadata-admin/EmbeddedItemEditor.tsx → FALLBACK_SCHEMAS.index

它存在的原因是合理的:index 是内嵌专用子类型,没有自己的元数据类型,框架没有槽位发布它的 schema(文件里的注释也这么写:"Once / if the framework grows an embedded-sub-type registry, this can move server-side too.")。注释同时声称这些 fallback "mirror the framework's Zod definitions" —— 而它已经不再 mirror 了:

console 表单提供 IndexSchema 声明 后果
where(标题 "Partial-index predicate") partial 键名不同,当前被静默丢弃
枚举含 brin ['btree','hash','gin','gist','fulltext'] 值非法,今天就会被拒

为什么它现在是静默失败

编辑器的保存路径(同文件,spliceEmbedded → client.save(parentType, …))把表单结果拼回父对象再 PUT 整个对象。服务端 saveMetaItem(packages/metadata-protocol/src/protocol.ts)对 body 做 safeParse 并在失败时 422,但逐字保留原 body。

IndexSchema 目前是 zod 默认的 .strip,所以管理员在 "Partial-index predicate" 里填的内容:

  1. 保存成功,没有任何提示;
  2. where 作为未声明键被 spec 在每次后续 parse 时丢掉;
  3. 即使它没被丢掉也没用 —— 见下。

两个方向都是死的

partial 和 type 都没有任何 DDL 消费者。driver-sql 的 syncDeclaredIndexes 只读 name / fields / unique(以及 ADR-0120 D3/D4 之后的 nullSafeColumns);DeclaredIndexInput(packages/plugins/driver-sql/src/schema-drift.ts)的字段列表就是这四个。

注意区分:sql-driver.ts 里确实有读 partial 的地方(introspectIndexes / parseIndexDdl),但那是从数据库自身的 DDL 解析出来做漂移检测的,和声明式元数据里的 IndexSchema.partial 无关。

为什么这会阻塞 #4001

关掉 IndexSchema 会把上面第 1 步变成 422,而 422 打在 console 自己渲染的控件上 —— 这正是 #5114 那一类(区别是这次在发布之前抓到)。

但"先修生产者再关"还不够。因为 partial 本身也是死的,收紧后把作者指向 partial,是一条声称超出平台实际交付的 guidance —— 台账 finding 18 记录的正是本战役已经发过的四条假 guidance。

所以关掉 IndexSchema 至少需要两件事先落地:

  1. 生产者侧(contract-first):objectui 把 where 改成 partial,并把枚举对齐 spec(去掉 brin,或在 spec 里声明它——后者需要有驱动读它)。
  2. ADR-0049 enforce-or-remove:给 IndexSchema.type 和 IndexSchema.partial 一个结论——要么接上驱动,要么退役。

顺带值得考虑的第三条:这类 fallback schema 本身就是漂移源。框架长一个内嵌子类型注册表(把 index 的 schema/form 发布出去,像 validation 已经通过 HAND_CRAFTED_SCHEMAS.validation 做的那样)能一次性消灭这个复制品。

现状记录在三个地方

  • packages/spec/src/data/object.zod.ts 的 IndexSchema JSDoc(明确写了"不要顺手补完");
  • packages/spec/src/data/object-strictness-batch20.test.ts §4 —— 把当前的 strip 行为钉住,使它改变的那天是有意改变;
  • docs/audits/2026-07-unknown-key-strictness-ledger.md 的 data/ 行。

复现(当前 main + 批 20 分支上行为一致):

ObjectSchema.parse({
  name: 'x', label: 'X', fields: { name: { type: 'text', label: 'N' } },
  indexes: [{ fields: ['name'], where: "status = 'open'" }],
}).indexes[0].where  // => undefined,静默丢弃

Activity

  1. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    Contributor

    分诊结论:pm:queue + repo:objectui

    已在 origin/main 复核两侧代码,漂移仍然存在,与 issue 描述完全一致:

    • objectstack packages/spec/src/data/object.zod.ts(第 309-341 行)IndexSchema 声明的是 partial(string,可选)与 type: z.enum(['btree','hash','gin','gist','fulltext'])。
    • objectui packages/app-shell/src/views/metadata-admin/EmbeddedItemEditor.tsx 的 FALLBACK_SCHEMAS.index 表单字段名是 where(标题 "Partial-index predicate"),type 枚举里是 brin 而非 fulltest。
    • 交叉核实了 packages/plugins/driver-sql/src/schema-drift.ts 的 DeclaredIndexInput(第 946-953 行):只有 name/fields/unique/nullSafeColumns 四个字段,partial 和 type 确实没有任何 DDL 消费者,与 issue 引用的结论一致。

    跨仓去重:搜索了 objectstack/objectui/cloud 三仓的 open issues 与 PRs(关键词 IndexSchema、FALLBACK_SCHEMAS、EmbeddedItemEditor),没有发现在途的竞争 PR 或未认领的重复 issue。PR #5250(已合并)是本 issue 的源头 PR(#4001 批 20),#5247/#5248/#5249 是它派生出的记录型 issue,不是重复。

    分类为 pm:queue:这是一个有明确文件位置和可复现的具体缺陷(字段名不一致 + 枚举值不一致),按 issue 自己的框定,生产者(objectui 的手抄 fallback schema)是漂移的一侧,spec 侧的 partial 命名是既有约定 —— 因此修复应落在 objectui,标 repo:objectui(而非 domain:spec)。

    需要指出:issue 正文里同时说明,仅改名还不足以关闭 #4001 对 IndexSchema 的收紧,因为 partial/type 本身在 driver-sql 侧仍是死键,真正解除阻塞需要本 issue(生产者对齐)与 #5248(ADR-0049 enforce-or-remove 结论)两者都落地。这一阻塞关系已经记录在 PR #5250 与两个 issue 的正文里,故未对 #4001 做任何编辑,仅在此评论中说明关系:Blocks: #4001(经由 #5248 联合阻塞)。

    「本评论来自分诊座位 Routine(#5474 试点),不构成认领」


    Generated by Claude Code

  2. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    Contributor

    前提更新 + 挂 pm:blocked(Blocked-by: #5248,已写入正文首行):维护者 2026-08-06 已裁 IndexSchema.type / partial 双双退役(见 #5248 裁决评论)。本单修向随之改变——不再是「where→partial 对齐拼写」,而是随退役同步 console 编辑器:FALLBACK_SCHEMAS.index 删除 where/"Partial-index predicate" 输入项与 brin 枚举值,只保留 name/fields/unique(含 ADR-0120 的 scope 语义)。#5248 的退役 PR 落地后解锁 sweep 回捡本单。正文第三条建议(内嵌子类型注册表消灭 fallback 漂移源)仍然成立,可在实施时评估顺手程度或另立单。会话:session_01N3uGFF8teXbpgtbEJ1aYXu


    Generated by Claude Code

  3. os-project-manager commented on Aug 8, 2026

    @os-project-manager
    Collaborator

    Unblocked — pm:blocked removed, back in the queue. Unlock sweep, 2026-08-07.

    Upstream #5248 (IndexSchema.type / partial enforce-or-remove) closed as completed 2026-08-06T09:06Z. ⚠️ Dispatchable since then — ~2 days unswept.

    Claim-time note: #5248's disposition decides what this card converges the console editor to, so read its outcome first — if partial was retired, the where vs partial drift is resolved by removal rather than by renaming.


    Generated by Claude Code

  4. self-assigned this
    on Aug 16, 2026
  5. os-zhuang commented on Aug 16, 2026

    @os-zhuang
    Contributor

    Claim: PM loop round 1 (spec seat shift 2026-08-16)
    Session: session_01SgModTWkJPeXMgbg7enL5Z
    Branch: claude/issue-5247-console-index-fallback
    Worktree: objectui-issue-5247
    Domain: cross-lane simple-blocker takeover — home lane of this card is repo:objectui; the claiming seat is domain:spec (+surface/tooling), which this card blocks (last real upstream of #4001's IndexSchema close, per the strictness ledger)
    File surface: packages/app-shell/src/views/metadata-admin/EmbeddedItemEditor.tsx + its tests (stop on breach; explain in the report)
    Container & model: S mechanical, mode:subagent, model: opus
    Serial constraints cleared: no open objectui PR touches EmbeddedItemEditor.tsx (searched 2026-08-16); objectui seat post #6025 names no hot-file queue for it; the seat's in-flight batches 29–33 (#3417/#3424/#3425/#3430/#3378+#3288) do not touch this file.

    Authorization context: maintainer in-session instruction this shift, verbatim, untranslated: 「4001 可以继续」— this card is the campaign's remaining upstream gate. Takeover is under the standing simple-blocker rule (mechanical, clearly specified, S-size, blocks the claiming lane); a completion note will be left for the repo:objectui seat.

    Scope as updated by this thread's 2026-08-06 premise comment (post-#5248 ruling, both keys retired via PR #5842): converge FALLBACK_SCHEMAS.index to the spec's current IndexSchema — delete the type/"Algorithm" control (retired key, and its brin value was never legal) and the where/"Partial-index predicate" control (edits a key that never existed; partial is also retired), keeping name/fields/unique with the spec's current unique semantics re-read from objectstack origin/main at implementation time. Drift re-verified present on objectui origin/main this morning (brin at :328, where at :336).


    Generated by Claude Code

  6. os-zhuang commented on Aug 16, 2026

    @os-zhuang
    Contributor
    {
      "issue": 5247,
      "status": "done",
      "branch": "claude/issue-5247-console-index-fallback",
      "pr": "https://github.com/objectstack-ai/objectui/pull/4772",
      "premise_still_valid": true,
      "summary": "Drift confirmed present on objectui origin/main exactly as the PM described (brin at :328, where at :336) and converged. Deleted the `type`/\"Algorithm\" control and the `where`/\"Partial-index predicate\" control from FALLBACK_SCHEMAS.index in EmbeddedItemEditor.tsx. One premise detail is now WORSE than the card recorded: measured against the installed @objectstack/spec@17.0.0-rc.6, `type` and `partial` are retiredKey tombstones that REJECT at any value, so the Algorithm select was not a silent-drop control — every one of its five options produced a 422 on the parent save (the #5114 class, live today). Only `where` was the silent strip the card describes. Also converged `unique` onto the ADR-0120 scope union per the claim comment's instruction to re-read the spec's current unique semantics: the console previously rendered a boolean switch, so the only spelling it could emit was the deprecated bare `true` that protocol 18 rejects (#5082); it now offers 'global'/'organization' for new indexes while the union's boolean branch keeps legacy indexes rendering and saving unchanged. Spec's live authorable surface re-read from objectstack origin/main and from the installed package is exactly name/fields/unique — no nullSafeColumns (that is driver-side DeclaredIndexInput, not spec).",
      "tests": "Union re-run AFTER the final commit at 2be19d6 (clean tree). `pnpm exec vitest run packages/app-shell/src/views/metadata-admin/` -> 'Test Files 168 passed (168) / Tests 1684 passed | 1 skipped (1685)'. New pin + sibling: `pnpm exec vitest run .../EmbeddedItemEditor.indexFallback.test.tsx .../EmbeddedItemEditor.preview.test.tsx` -> 'Test Files 2 passed (2) / Tests 21 passed (21)'. `pnpm --filter @object-ui/app-shell run type-check` exit 0 (tsc --noEmit && tsc -p tsconfig.test.json) — required `pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build` first, without which tsc reports 20+ 'Cannot find module @object-ui/*' false reds. eslint on the two changed files: 0 errors, 10 warnings. Repo gates all PASS: check:phantom-deps, check:control-bytes, lint:coverage (46/46 packages, 0 errors), check:i18n-keys, check:i18n-drift, check:i18n-dead-keys, check:spec-symbols, check-changeset-presence, check-changeset-no-major. REVERSE VERIFICATION: fix committed first, then ablated (export kept so failure is the drift, not a missing import); expected direction RED, observed RED with 8 failures each naming the real defect — `type` -> \"expected '`indexes[].type` was removed in @obje…' to be ''\" (the 422 half), `where` -> 'expected undefined to deeply equal sample' (the silent-strip half, which is why the pin asserts round-trip survival rather than unrecognized_keys — IndexSchema is deliberately not .strict()), completeness -> \"expected ['fields','name','type',…(2)] to deeply equal ['fields','name','unique']\", and the rendered 'Algorithm' label -> 'expected <label …> to be null'. No ablation-on-dist involved (source-resolved package, not a dogfood run). Repo-specific note: `pnpm --filter <pkg> test` and `-- <paths>` are both blocked by objectui's vitest-invocation-guard as silent-false-green traps; the correct form is `pnpm exec vitest run <path>` from the repo root, which is what every run above used.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as objectstack-ai/objectui#4765 (label `finding`, unassigned): `UniquenessValidation`'s @deprecated JSDoc in packages/types/src/data-protocol.ts prescribes `partial` (retired in spec 17.0.0) and `unique: true` (deprecated positional spelling) as the replacement for the removed uniqueness validation — the same false-guidance class this card cites from ledger finding 18, but in a different package, so filed rather than fixed under the declared file surface"
      ]
    }

    Notes the JSON does not carry well

    • File surface honoured, one deliberate side effect. Only EmbeddedItemEditor.tsx, its new test, and a .changeset/*.md (mandatory in objectui for any change to a released package's src/) are touched. FALLBACK_SCHEMAS is now exported so the pin reads the real object rather than regexing source text; that trips react-refresh/only-export-components as a warning (lint still exits 0). Moving the constant to its own module would clear it but adds a file outside the declared surface — flagged in the PR for the reviewer to call.
    • The pin is deliberately not a second hand copy. It runs the console's own properties through the installed @objectstack/spec, so it re-derives on every spec bump instead of re-stating expected key names in its own source. Completeness runs the other direction too (probe each spec shape key; a tombstone rejects every probe, a live key accepts one), so a spec key with no console control also fails — with no hand-listed key set to maintain.
    • The issue body's third suggestion (embedded sub-type registry) remains out of scope and remains valid — noted in the PR. It would delete this hand copy rather than re-align it; the pin is the cheap interim guard.
    • No CI wait per the standing dispatch contract (maintainer-decided 2026-08-10): draft PR is open, gates were run locally at the reported sha, CI conclusions are the PM's read.

    Generated by Claude Code

  7. os-zhuang commented on Aug 16, 2026

    @os-zhuang
    Contributor

    ACCEPT — PR objectstack-ai/objectui#4772 (draft, base main, Fixes objectstack-ai/objectstack#5247 first line verified).

    Reviewed against GitHub, not the report's self-description:

    • Changed files (from the PR's file list): exactly EmbeddedItemEditor.tsx + new EmbeddedItemEditor.indexFallback.test.tsx + one changeset — declared surface honoured. The FALLBACK_SCHEMAS export (react-refresh warning, lint still green) is a declared, deliberate trade so the pin reads the real object; follow-up module split left to a future card if anyone cares.
    • Both dead controls deleted; unique converged onto the ADR-0120 scope union with the scope branch first (new indexes can no longer author the deprecated bare true; legacy booleans keep rendering) — this was instructed at claim time, not scope creep.
    • Premise sharpened, not falsified: type was measured as a live 422 on every option (retiredKey tombstone), not the silent drop the card recorded; only where was the silent strip. The card's framing survives — both controls were false authoring surface.
    • Pin quality: asserts round-trip survival + rejection against the INSTALLED @objectstack/spec (no second hand copy; completeness probed in both directions). It stays green when 未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001's IndexSchema close later lands spec-side.
    • Reverse verification: 8 red, each naming the real defect. Out-of-scope finding UniquenessValidation's deprecation JSDoc points authors at the retired indexes[].partial key objectui#4765 verified filed (unassigned, finding).

    CI gate jobs still in_progress at review time — flip-to-ready + auto-merge happens only after the gate family concludes green (flip check armed). #4001's IndexSchema close slice dispatches once this PR is MERGED.


    Generated by Claude Code

  8. added 2 commits that reference this issue on Aug 17, 2026
    9a3d04e
    199a1db
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions