Skip to content

fix(data-objectstack): layered 读迁到声明路径 GET /meta/:type/:name/layers (#4016) - #4988

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4016-meta-layers-route
Aug 17, 2026
Merged

fix(data-objectstack): layered 读迁到声明路径 GET /meta/:type/:name/layers (#4016)#4988
yinlianghui merged 1 commit into
mainfrom
claude/issue-4016-meta-layers-route

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#4016

os#5882 B 案的消费半边:Studio 元数据编辑页的三层对比读,从「给普通条目读挂一个查询标记」迁到声明路径 GET /meta/:type/:name/layers。服务端半边是 os#6596,已随 @objectstack/spec@17.0.0 GA 落到本仓 pin(dependabot 7484f7f0a / PR #4953)。

一、前提测量(动工前先量,三项)

1. GA 契约在位。 本仓解析到的 @objectstack/spec@17.0.0 里,DEFAULT_METADATA_ROUTES 声明了 GET /:type/:name/layers(prefix /api/v1/meta,handler getMetaItemLayered,responseSchema GetMetaItemLayeredResponseSchema),且该 schema 确实从 @objectstack/spec/api 导出(运行时 typeof 为 function)。spec 自己的 route 描述也写明老拼法「still works during its deprecation window but is answered with Deprecation / Link headers pointing here」。

2. GA 客户端没有 layered 方法 —— 但这不是「缺口」。@objectstack/client@17.0.0 整包 grep layered 零命中,两种拼法都没有。framework origin/main(e4e5c6e3c)的 packages/rest/src/rest-route-ledger.ts 把该路由记作 disposition: 'server-only',note 原文:「consumed by objectui over plain HTTP, and the SDK expressed no layered read under the ?layers= spelling either」,并明确把「SDK 是否应当表达它」留作独立的产品决定。所以本仓继续手工构造该请求,是台账声明的消费方式,不是绕过 SDK;本 PR 只搬路径,不新增任何手拼 URL(那一处手拼在改动前就已经存在)。

3. 调用点清单。 构造该查询串的地方全仓只有一处:packages/data-objectstack/src/metadata-client.tsMetadataClient.layered()。app-shell 共 15client.layered() 调用点全部经由它,零个自己拼 URL —— studio-design/ 6 处(PackageOwdOverviewPanel 2、StudioDesignSurface 4),metadata-admin/ 9 处(ResourceEditPage 5、PermissionMatrixEditor 3、EmbeddedItemEditor 1)。除此之外还有:1 个按老 URL 匹配的 fetch 测试替身、3 处注释、1 处技能指南正文提到老拼法。

前提成立,premise_still_valid: true

二、改了什么

  • MetadataClient.layered() 改读 …/:type/:name/layers
  • 迁移里唯一的活陷阱是 package 参数的连接符:它此前以 & 挂在标记后面。留着 & 会把它焊到最后一个 PATH 段上(…/layers&package=crm),不匹配任何路由 —— 404,而 layered() 把 404 报成「空但成功」的层视图,静默。已改成 ?package= 并单独钉了一条。
  • 老拼法归零:注释、技能指南、测试替身一并改到新路径,并加了一条 ratchet 扫描 packages/*/srcapps/*/srcskills/**/*.md,阻止任何出货源文件或技能指南再用查询标记去取该投影。

信封不变(行为等价):code / overlay / overlayScope / effective、载入期 _diagnostics、ADR-0010 完整保护信封仍在同一次往返里回来,?package=(ADR-0048)照旧透传 —— 上游两个入口由同一个 handler 服务,正是为了让弃用窗口的承诺成立。

一条行为差异,是服务端的设计而不是本仓的选择:被弃用的标记在后端 protocol 实现缺 getMetaItemLayered 时会下坠到普通条目读、答 { type, name, item } 信封;专用路径拒绝用本路径声明的形状去答另一个资源,改答 501 NOT_IMPLEMENTED(rest-server.ts 该挂载点注释原话:「A dedicated path cannot fall through to the plain read the way the flag did」)。于是这里表现为抛错,而不是 code / overlay 无故空白的对比视图。这条已单独钉住。

新测试的路径期望派生自装到本地的 @objectstack/spec 路由表,而不是抄一份字符串 —— 抄的那份在生产者搬路由之后仍然会绿,而那正是这张卡要消除的失效模式。信封等价那条先用生产者自己的 GetMetaItemLayeredResponseSchema 校验 fixture(断的是的判定,所以要求 full parse 绿,不是只看没有 unrecognized_keys),再断言解析结果逐键相等。

三、反向验证(先预判,后跑;git checkout 还原,未用 stash)

预判(跑之前写下):把 layered() 的 URL 改回老拼法(含 &package=)后 ——

  1. ratchet 红,且是多出一条发现而红(offenders 从 [] 变成 1 个文件),不是「什么都没产出」的空绿;
  2. metadata-client.layeredRoute.test.ts 7 条里 3 红 4 绿:红的是 3 条 URL 钉,绿的 4 条(spec 声明、信封等价、404、501)必须保持绿 —— 它们量的是「回来的东西没变」,而本 PR 改的是「去哪里取」;它们若也红,说明我顺手改了行为;
  3. app-shell PermissionMatrixEditor.fieldEnvelope.test.tsx 红(替身按新路径精确匹配,老拼法会掉到 catch-all)。

实测:1 与 2 与预判逐条一致 ——

× scripts/__tests__/layered-read-declared-path-4016.test.ts > ... names the retired flag
→ expected [ Array(1) ] to deeply equal []
+ "packages/data-objectstack/src/metadata-client.ts",
✓ ... > is declared by the installed spec as a GET on its own path with its own schema
× ... > requests the spec-declared path and carries no `layers` query flag
× ... > leads the query string with `?package=` instead of trailing it (ADR-0048)
× ... > percent-encodes the type and name into the path
✓ ... > hands back every layer and protection carrier the declared body sends
✓ ... > reports an unknown item as an empty layered view (404)
✓ ... > throws when the backend cannot serve the projection (501)
Test Files 2 failed | 1 passed (3) Tests 4 failed | 7 passed (11)

第 3 条预判错了,如实报告:PermissionMatrixEditor.fieldEnvelope.test.tsx 三条全绿。原因是那个替身的 layered 分支对该文件的断言没有任何压力:layered body 只喂「是否 artifact-backed」的判定与 permission 草稿,而这三条断言的字段子表是由下面那条 get() 读喂的;掉到 catch-all 的 { items: [] } 也不影响它们(client.layered(...).catch(() => null)PermissionMatrixEditor.tsx:433 有意的 fail-open,注释已论证过「a transient read failure must not invent a lock」)。

我原本在那处替身上写了一句「老拼法会掉到 catch-all 并把断言一起带红」的注释 —— 实测证明那是假的,已把注释改成实测结论,并注明 URL 的钉在 data-objectstack 那份测试里。替身仍改到新路径:留着老拼法会让这个「用真 client 答真服务端形状」的替身对着线上撒谎,下一个读者会从这里抄走已弃用的拼法。

四、测试

仓根跑法(AGENTS.md §9 唯一正确写法),全部在共享验证锁内串行:

  • pnpm exec vitest run packages/data-objectstack/ scripts/__tests__/layered-read-declared-path-4016.test.ts packages/app-shell/src/views/metadata-admin/Test Files 213 passed (213) / Tests 2267 passed | 1 skipped (2268)
  • turbo run type-check --filter=@object-ui/data-objectstack --filter=@object-ui/app-shellTasks: 31 successful, 31 total(首轮曾红:ratchet 用 node 内建但 data-objectstack 的 tsconfig 不带 node types —— 这也是把 ratchet 放进 scripts/__tests__/ 的原因,那里 types: ["node"] 且由 pnpm type-check:scripts 与 ci.yml 覆盖)
  • pnpm type-check:scripts → exit 0
  • check:control-bytes / check:changeset-presence / check:changeset-no-major / check:spec-symbols / check:phantom-deps / check:self-import / check:skills-paths / check:type-check-coverage / check:lint-coverage → 全绿
  • 改动文件 eslint → 0 error(只剩本文件既有的 no-explicit-any warning,均在未改动行)

五、相邻发现(未在本 PR 修)


Generated by Claude Code

`packages/cli` 的 `sources every range from this repo instead of inventing one` 与
`packages/create-plugin` 的 `sources every devDependency range from this repo instead of
inventing them` 都在 `for` 循环里逐名 `expect`。`expect` 失败即抛,所以一次只报第一个
不匹配的名字,而表是插入序遍历的 —— 被报的是「谁排在前面」,不是「谁更严重」。
代价兑现过两次:#4098 有五个同窗 bump 藏在第一个后面;#4968 里 `lucide-react` 排在
`postcss`/`react`/`tailwindcss`/`typescript`/`vite` 之前,遮住后面六个名字的判定,
只能另写脚本才敢说这批到底漂了几处。跨包那半更贵:create-plugin 那处当时在 `main`
上也是红的,但按包边界扫 cli 时没有任何东西报它,落地后仍要第二轮。
两处都改成两趟:
- 前置断言(锚必须能解析:名字至少被一个生成器声明、`root` 锚在根清单里、仓内区间
必须一致)保持首败即抛,并且整趟跑在任何区间比对之前 —— 前置败时不产出任何漂移行,
不与漂移串扰。锚不可解析时也没有可累积的期望值。
- 漂移断言累积后一次报空集,逐条带生成器、名字、实得区间与应得区间。
顺带把两张锚表的文档块补上「范围规则只判本表内的名字、完备性规则要求生成清单的键集
恰好等于本表」这一对互补关系 —— 两条规则分居两个 `it`,读任一处都看不到另一处。
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)24.7 KB350 KB
Entry fileindex-DFCRZQdN.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)9.56KB3.59KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)8.92KB3.41KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)25.13KB5.40KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.79KB
auth (createAuthenticatedFetch.js)6.34KB2.43KB
auth (index.js)2.71KB1.22KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.02KB0.88KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)498.79KB111.24KB
core (index.js)4.11KB1.62KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)159.03KB44.08KB
fields (index.js)233.27KB58.22KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.35KB1.38KB
i18n (pickLocalized.js)3.69KB1.73KB
i18n (provider.js)23.12KB7.62KB
i18n (useDisplayLocale.js)2.84KB1.45KB
i18n (useObjectLabel.js)27.59KB6.63KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)39.16KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.35KB3.31KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.42KB1.42KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.81KB0.83KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.75KB18.37KB
plugin-chatbot (index.js)181.21KB43.14KB
plugin-dashboard (index.js)127.85KB32.73KB
plugin-designer (index.js)212.39KB42.83KB
plugin-detail (index.js)240.03KB60.04KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)120.42KB29.03KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)197.61KB53.03KB
plugin-kanban (index.js)52.72KB14.54KB
plugin-list (index.js)111.22KB26.98KB
plugin-map (index.js)17.91KB5.72KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)41.97KB11.33KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)83.81KB20.49KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)27.53KB9.41KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.28KB0.68KB
react (schema-input.js)1.45KB0.83KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)4.09KB1.74KB
sdui-parser (index.js)4.55KB2.07KB
sdui-parser (parse.js)10.76KB3.17KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)4.69KB1.48KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.05KB1.52KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM 验收 ✅ ACCEPT(#4016,批次 20)

实物核验(merge-base 7d1017790 与报告基线一致):8 files,+436/−12 逐字对上 —— MetadataClient.layered() 迁到 spec 台账声明的 GET /:type/:name/layers(全仓唯一构造点,15 个调用点零手拼);知道老拼法的 5 处一并清零;ratchet(117 行)放 scripts/__tests__/(扫描面本就是全仓,且 data-objectstack 的 tsconfig 无 node types —— 首轮 type-check 真红后的迁移判断正确)。模型标识 msg/diff 双 0;releases/ 零触碰;changeset patch。

skills/ 触碰的定性(写明留否决窗):skills/objectui/guides/console-development.md 7 行是把指南里的老拼法迁到声明路径 —— 本 PR 自己的 ratchet 扫描面含 skills/**/*.md,不改则新门自红,属机械必需的事实跟随,不是 agent 指引政策变更,故不走 skills/** 的 ADR 亲合路径。维护者如认为应从严,合并后可revert 该文件单独走流程。

CI 亲读:20/20 check runs completed,零失败(两项 path-filter skipped 计绿)。

GA 客户端问题的回答:实测三点闭合 —— spec 台账声明该路由、@objectstack/client@17.0.0 整包无 layered 方法、framework 台账记 server-only 且明文把 SDK 表达留作独立产品决定 —— 手工构造正是台账声明的消费方式,不属「客户端缺方法就别手拼」要拦的情形。判据全部给了原文坐标,验收采信。

反向验证读数:①ratchet「多一条发现」方向命中(非空绿);②行为钉 3 红 4 绿按预判分池(4 绿量「回来的东西没变」);③fieldEnvelope 替身预判错、如实报告 —— 替身 layered 分支对断言零压力 + .catch(() => null) 是有注释论证的有意 fail-open,dev 把自己被证伪的注释改成实测结论而不是让它继续撒谎,正确。

findings:#4982(overlayScope 词表漂移 + LayeredDiff 裸英文徽标,今天用户可见)已由 PM 定级 pm:queue;#4983(不可达兜底,待上游关弃用窗)入池。undraft + auto-merge(squash)。


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 17, 2026 12:53
@yinlianghui
yinlianghui added this pull request to the merge queueAug 17, 2026
Merged via the queue into main with commit cf4f8a6Aug 17, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4016-meta-layers-route branch August 17, 2026 12:53
@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

⚠️给后来的读者:这次 squash 落到 main 的提交信息不属于本 PR。

maincf4f8a6 携带的正文是 #4974(两处生成器锚规则改累积报全部漂移)的提交信息,而 diff 是本 PR 的 8 个文件(#4016 的 layered 声明路径迁移)。本 PR 的正文本身是正确的 —— 只有那条 commit message 串了台。

成因与实测记在 #4994:并行会话的 scratchpad 目录事实上是共享的,通用文件名(commitmsg.txt / pr-body.md)会被后写者顶掉;同一现象在 20 分钟内撞了两次(另一次是 #4974 席的 pr-body.md#4190 席的正文整份覆盖)。

⛔ 没有改写 main 的历史。留此评论只为让 git log --grep 找到 #4974 的人不至于据此认为那张卡已经实施 —— 它当时未实施,现在的 PR 是 #4993#4016 的实施与验证不受影响。


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

objectui: Studio 元数据编辑页从 ?layers=true 迁到 GET /meta/:type/:name/layers(#5882 B 案的消费半边)

2 participants

@yinlianghui@claude