Skip to content

docs(console): name the app-list route the console actually calls, and where per-session filtering really happens - #4937

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-4887-phantom-route-comments
Aug 17, 2026
Merged

docs(console): name the app-list route the console actually calls, and where per-session filtering really happens#4937
yinlianghui merged 1 commit into
mainfrom
claude/issue-4887-phantom-route-comments

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes #4887

溯源结论(先做这一步,再动注释)

卡面要求「先确认 console 到底用哪个端点列 apps、per-session 过滤真正发生在哪」。两个问题分开答:

1. 端点:通用元数据列表路由 GET /api/v1/meta/:type,type 段用单数 app

环节 证据指针
console 的取数点 packages/app-shell/src/providers/MetadataProvider.tsx:466-468preview ? preview.list(type) : adapterRef.current.getClient().meta.getItems(type)
type 的值是单数 app 同文件 :72 TYPE_BY_STATE_KEY = { apps: 'app', … };:50 EAGER_TYPES = ['app', 'view'](挂载即取)
SDK 拼出的 URL @objectstack/client meta.getItems:`${baseUrl}${route}/${type}`,route = getRoute('metadata'),默认表 metadata: '/api/v1/meta' → 线上路径 /api/v1/meta/app
ADR-0037 draft 预览路径拼的是同一个地址 packages/data-objectstack/src/metadata-client.ts:586 const url = `${this.base}/${encodeURIComponent(type)}${qs}`,base 由 :405-406API_PREFIX='/api/v1' + META_PREFIX='/meta' 组成
by-name 探针用的是同一族地址 packages/data-objectstack/src/index.ts probeAppAccessthis.client.meta.getItem('app', appName);appAccessProbe.test.ts:94 断言 toContain('/api/v1/meta/app/finance')
framework 路由台账 packages/rest/src/rest-route-ledger.ts:150GET /api/v1/meta/:typemeta.getItems;packages/runtime/src/route-ledger.ts:343 同一行

台账里没有以集合复数拼写单独立行的 app 列表路由 —— 这正是卡面测量到的「零命中」。

补一条诚实的限定,免得下个读者按「404」去理解:那个被退役的拼写并不会 404。列表路由本身是参数化的 :type,而 RestServer.metaTypeSingular(rest-server.ts:1500-1503)用 @objectstack/spec/sharedPLURAL_TO_SINGULAR 把复数折叠成单数(spec 侧钉死:packages/spec/src/shared/metadata-collection.test.ts:283 expect(PLURAL_TO_SINGULAR['apps']).toBe('app'))。所以问题不是「那个 URL 不存在」,而是:本仓没有任何一处 fetch 构造那个拼写,台账也没有它自己的行 —— 六处注释按名字指了一个不是调用地址的东西。

2. per-session 过滤:真实存在,且确实在服务端 —— 这半句注释原本就是对的。

@objectstack/restRestServer:

  • filterAppForUser(packages/rest/src/rest-server.ts:1814)/ filterAppForUserWithReason(:1867)。
  • 列表侧:路由挂载在 :3700-3704(path: ${metaPath}/:type);app 分支入口 :3818(metaTypeSingular(req.params.type) === 'app');过滤调用 :3838;整段包在 if (ctx?.userId)(:3827)里 —— 这就是「per session」的机械依据,没有解析出会话用户就不过滤。
  • by-name 侧::4682filterAppForUserWithReason 得出 objectstack#8013 的 403 PERMISSION_DENIED 判决。

所以本卡的两个候选原因里,命中的是前者:路由名错,过滤位置不错。六处注释因此保留「服务端 per-session 过滤」这句声称,并把它从一个泛泛的函数名升级成带文件路径的定位(filterAppForUser, packages/rest/src/rest-server.ts)。

⛔ 卡面边界照办:没做全仓幽灵路由 sweep,没有声称也没有修任何访问控制缺陷。

六处前后对照

每处的新声称都配了上表里的一条可核证据指针。

# 位置 改前(节选) 改后(节选)
1 packages/app-shell/src/console/AppContent.tsx:246 「(退役拼写)is filtered per session server-side (filterAppForUser)」 「The list this reads is the generic metadata list route GET /api/v1/meta/:type, requested with the SINGULAR type segment app (MetadataProviderclient.meta.getItems('app')), and the server filters it per session (filterAppForUser, packages/rest/src/rest-server.ts)」
2 packages/app-shell/src/console/AppContent.tsx:676 「(退役拼写)is filtered PER SESSION server-side (filterAppForUser)」 「The app list arrives from GET /api/v1/meta/:type with the SINGULAR type segment app, and the server filters that list PER SESSION (filterAppForUser, packages/rest/src/rest-server.ts)」
3 packages/data-objectstack/src/index.ts:3800(probeAppAccess docblock) 「(退役拼写)is filtered per session server-side (filterAppForUser)」 「The app LIST is the generic metadata list route GET /api/v1/meta/:type, requested with the singular type segment app — the same address this method appends a name to below … applied inside the :type list handler once the type segment resolves to app
4 packages/data-objectstack/src/appAccessProbe.test.ts:13 同上句式,作为「两种条件在列表里不可区分」的 rationale 同一 rationale,路由名与过滤位置改成真实的;断言未动
5 …/console/__tests__/AppContent.deniedVsUnpublished.test.tsx:9 同上句式,作为 measured defect 的前提 「The console reads its app list from the generic metadata list route GET /api/v1/meta/:type with the singular type segment app, and the server filters that list per session in filterAppForUser (packages/rest/src/rest-server.ts)」;断言未动
6 …/console/__tests__/AppContent.inaccessibleAppStrand.test.tsx:17 「(退役拼写)is already filtered PER SESSION server-side (filterAppForUser, packages/rest/src/rest-server.ts)」 「That list is the generic metadata list route GET /api/v1/meta/:type requested with the singular type segment app — the same one the sessionStorage key above mirrors — and the server already filters it PER SESSION (…)」;断言未动

第 6 处原本已经写对了过滤位置(它是六处里唯一带 rest-server.ts 路径的),所以那处只订正了路由名。

三个测试文件的断言本体一行未改,rationale 修正后也不需要跟着改语义 —— 因为修正的方向是「注释向断言靠拢」,不是反过来:appAccessProbe.test.ts:94expect(String(fetchImpl.mock.calls[0][0])).toContain('/api/v1/meta/app/finance') 本来就钉着单数地址。文件自己的断言与自己的文件头互相矛盾,这是本卡结论最硬的一条内证。

CHANGELOG.md 里还有三处同样的拼写(app-shell / data-objectstack / i18n),故意未动:那是已发布的 release 记录,改它等于重写历史发布说明。

验证

注释-only 改动没有行为门可反向验证,所以用机械等价证明 + 正向判据,不编造一个「改前红/改后绿」的形状。

A. 机械等价(照 PR #4888 六文件 removeComments 先例) —— 五个被改文件在 removeComments: true 下转译,改前(git show HEAD:)与改后逐字节比对:

IDENTICAL  emit 33067B/33067B  packages/app-shell/src/console/AppContent.tsx
IDENTICAL  emit 10560B/10560B  .../__tests__/AppContent.deniedVsUnpublished.test.tsx
IDENTICAL  emit  6217B/ 6217B  .../__tests__/AppContent.inaccessibleAppStrand.test.tsx
IDENTICAL  emit  3992B/ 3992B  packages/data-objectstack/src/appAccessProbe.test.ts
IDENTICAL  emit 81706B/81706B  packages/data-objectstack/src/index.ts

5 files; comment-stripped emit identical for all: true

git diff -U0 过滤掉注释行后剩零行,与上面互为独立佐证。

B. 受影响测试(flock 串行)

pnpm exec vitest run packages/app-shell/src/console/__tests__/ packages/data-objectstack/ --maxWorkers=2
 Test Files  48 passed (48)
      Tests  630 passed (630)

单独跑三个被改的测试文件:Test Files 3 passed (3) / Tests 25 passed (25)

C. 仓根 type-check + 控制字节门

pnpm exec turbo run type-check --concurrency=2
 Tasks:    81 successful, 81 total   [exited with code 0]
node scripts/check-control-bytes.mjs
✅  check-control-bytes: OK (scanned 4417 tracked text file(s); skipped 85 binary).

排版微调发生在那次全仓 type-check 之后,所以又对两个受影响包重跑了一遍最终树:Tasks: 31 successful, 31 total。另按控制字节纪律做了门外自查(grep -naP\x00-\x08\x0b\x0c\x0e-\x1f,六个文件 + changeset):零命中。

D. 「六处全改、零处遗漏」的正向判据

  • 退役拼写在 packages/** / apps/**.ts/.tsx 里命中数:0
  • 扫描器活着(邻近词正查,同一条 grep 管道):meta/:type 在两个受影响包里 37 命中;filterAppForUser 全仓 8 命中 —— 其中 6 处是本 PR 改过的句子,另 2 处(deniedVsUnpublished.test.tsx:187inaccessibleAppStrand.test.tsx:127)只提函数名、不带路由名,本来就没有错误声称,不在六处之内。

changeset:.changeset/console-app-list-route-comment-truth.md(注释-only,照 combo-drill-doc-truth.md / app-shell-props-block-4808.md 先例用空 frontmatter,不推任何包版本)。


Generated by Claude Code

Six comments in the console app surface named the app LIST endpoint with a
plural-collection spelling that no request in this repo constructs and that no
framework route ledger declares as a row of its own.

Traced first (the issue asked for this before any edit):

  - `MetadataProvider` fetches the list as
    `client.meta.getItems('app')` (providers/MetadataProvider.tsx:466-468,
    type key `apps: 'app'` at :72, eager at :50); the SDK builds
    `${baseUrl}${getRoute('metadata')}/${type}` with `metadata` defaulting to
    `/api/v1/meta` — so the wire path is `/api/v1/meta/app`.
  - The ADR-0037 preview path builds the same address:
    `MetadataClient.list()` at metadata-client.ts:586 over the
    `/api/v1` + `/meta` base at :405-406.
  - Framework ledgers declare `GET /api/v1/meta/:type` -> `meta.getItems`
    (rest/src/rest-route-ledger.ts:150, runtime/src/route-ledger.ts:343).
    No collection-spelled row exists in either.
  - Per-session filtering is real and server-side: `filterAppForUser` /
    `filterAppForUserWithReason` (rest/src/rest-server.ts:1814 / :1867) run
    inside that `:type` list handler (mounted :3700-3704) at :3838, guarded by
    `metaTypeSingular(req.params.type) === 'app'` (:3818) and by
    `ctx?.userId` (:3827) — which is what makes it per session.

All six now name the real route and the real filtering locus. Test assertions
are untouched; `appAccessProbe.test.ts:94` already pinned the singular address
in its own expectation, which is what made its header's spelling demonstrably
wrong.

No behaviour change: each touched file transpiles byte-identically with
comments stripped (five of five).

Fixes #4887

Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Main entry (gzip) 24.7 KB 350 KB
Entry file index-CTYUWeyJ.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 9.56KB 3.59KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 8.92KB 3.41KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 25.13KB 5.40KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 38.46KB 10.17KB
auth (createAuthenticatedFetch.js) 6.34KB 2.43KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.02KB 0.88KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.65KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 498.61KB 111.16KB
core (index.js) 4.06KB 1.61KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 157.40KB 43.42KB
fields (index.js) 232.86KB 58.11KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.35KB 1.38KB
i18n (pickLocalized.js) 3.69KB 1.73KB
i18n (provider.js) 23.12KB 7.62KB
i18n (useDisplayLocale.js) 2.84KB 1.45KB
i18n (useObjectLabel.js) 27.59KB 6.63KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 39.16KB 10.97KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 9.35KB 3.31KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 4.42KB 1.42KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.81KB 0.83KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.62KB 12.83KB
plugin-charts (index.js) 64.75KB 18.37KB
plugin-chatbot (index.js) 181.21KB 43.14KB
plugin-dashboard (index.js) 127.85KB 32.73KB
plugin-designer (index.js) 212.39KB 42.83KB
plugin-detail (index.js) 239.90KB 60.01KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 120.42KB 29.03KB
plugin-gantt (index.js) 164.10KB 39.87KB
plugin-grid (index.js) 197.58KB 53.00KB
plugin-kanban (index.js) 52.72KB 14.54KB
plugin-list (index.js) 111.23KB 26.97KB
plugin-map (index.js) 17.91KB 5.72KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 41.97KB 11.33KB
plugin-timeline (index.js) 26.68KB 7.66KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 83.81KB 20.49KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 27.53KB 9.41KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.28KB 0.68KB
react (schema-input.js) 1.45KB 0.83KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.55KB 2.07KB
sdui-parser (parse.js) 10.76KB 3.17KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 3.05KB 1.52KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

Copy link
Copy Markdown
Collaborator Author

PM 验收:ACCEPT(#4887,批次 18,PM 会话 session_01GTRjn8xBqp75dk7kFupVRt)

溯源答案(卡面 scope ①,证据链齐):真实端点是通用元数据列表路由 GET /api/v1/meta/:type(type 段单数 app),证据从 console 取数点 → TYPE_BY_STATE_KEY → SDK 拼接 → framework 两处台账逐环带行号;重要限定如实记录 —— 退役拼写并不 404(服务端 PLURAL_TO_SINGULAR 折叠),诚实事实是「零 fetch 构造 + 台账无本行」。过滤位置声称原本就对(filterAppForUser 在列表 handler 内、ctx.userId 门控),六处注释保留该安全声称并升级为带文件路径的定位 —— 命中卡面两候选里的「路由名错、位置不错」。

实物核验(已过):6 文件 +50/−16;PM 独立过滤非注释行变更 = 零输出,与 dev 的 removeComments 五文件逐字节等价证明互相印证;三个测试文件断言全部未动(其中 appAccessProbe.test.ts:94 的断言本就钉着单数地址 —— 「注释向断言靠拢」的修正方向由文件自身矛盾内证);标识 0;releases 0。

正向判据:退役拼写在 packages/+apps/ 归零 + 同管道邻近词正查(meta/:type 37 命中、filterAppForUser 8 命中,其中 2 处不带路由名的合法提及如实排除在六处外)。

有据偏离(接受):三处 CHANGELOG 的同拼写刻意不动 —— 已发布 release 记录,改它等于重写历史(与 releases 禁令同一精神)。

CI(亲读终态):20 项全 completed,18 success + 2 skipped,零失败。

附带产出:新 finding #4940(同文件 14 个调用点 2 个复数拼写,与本卡同源的拼写漂移观察卡)。

→ undraft + auto-merge (SQUASH)。


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 17, 2026 07:08
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 17, 2026
Merged via the queue into main with commit b9f2c39 Aug 17, 2026
21 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-4887-phantom-route-comments branch August 17, 2026 07:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants