fix(app-shell): Studio package switcher reads the server's writable verdict, heuristic only as fallback (#7177) - #7331

Merged
hotlong merged 2 commits into
mainfrom
claude/issue-7177-studio-picker-module-packages
Sep 2, 2026
Merged

fix(app-shell): Studio package switcher reads the server's writable verdict, heuristic only as fallback (#7177)#7331
hotlong merged 2 commits into
mainfrom
claude/issue-7177-studio-picker-module-packages

Conversation

@hotlong

Copy link
Copy Markdown
Contributor

Fixes#7177
Part of objectstack-ai/objectstack#14122 — ADR-0130 Consequences row 6, the client half. Server half: objectstack-ai/objectstack#14375 (PR objectstack-ai/objectstack#14430).

Why

parsePackages derived "writable" from manifest.scope alone (scope !== 'project'). That is not the rule the server enforces. isWritablePackage (ADR-0070 D2) reads engine.manifests first — a package booted from an artifact through registerApp is read-only whatever its scope says — and only then the system / cloud scopes. The two rules split on exactly the row ADR-0130 introduces:

rowmanifest.scopein engine.manifestsserverold client heuristic
type: module sub-package of a multi-package artifact (D4/D7 raw body)absentyesread-onlywritable — wrong
Studio-created database baseabsentnowritablewritable

Nothing in the raw row separates those two; only the server's engine.manifests does. A client-side "missing scope means read-only" rule (the first fix proposed on the card, since withdrawn) would have flipped every Studio base read-only. So the verdict moved server-side and this PR consumes it.

Measured on a live server (see Verification): the scope default is applied at PARSE time, while the artifact load path hands the RAW manifest body to registerApp — so the served row has no scope key at all, and the heuristic reads it as a writable database base.

What changed

packages/app-shell/src/views/studio-design/packages-io.ts:

  • parsePackages uses the row's own writable when the server states one (typeof p.writable === 'boolean'), and falls back to the unchanged scope !== 'project' expression when the key is absent (older servers). A non-boolean value is not a verdict and falls back too.
  • The module doc comment is rewritten: it used to state the heuristic as the rule. It now says the server is the authority, the heuristic is only the pre-objectstack-14375 fallback, and it is wrong for the scope-less booted module row — with the mechanism (parse-time default vs raw body) written down.
  • The system / cloud hide filter is untouched, and now says in writing that it is about visibility, not writability.

Changeset: .changeset/7177-studio-switcher-server-writable-verdict.md (@object-ui/app-shell: patch).

Pins

packages/app-shell/src/views/studio-design/packages-io.writableVerdict.test.ts — 5 tests:

  1. writable: false on a scope-less row (the ADR-0130 module) is honoured, and the test asserts the row really carries no scope so the verdict cannot be leaking out of one.
  2. writable: true on a scope-less row (a Studio base) is honoured.
  3. scope: 'project' is overridden in BOTH directions — server false stays false, server true wins over the heuristic.
  4. A non-boolean writable (the string "false") is ignored. Boolean('false') is true, so a coercing read would have agreed with the fallback by accident on the scope-less row; the scope: 'project' row is what tells the two apart.
  5. Kernel packages stay hidden whatever verdict they carry.

Negative pin (same file): a payload with no writable key anywhere produces output deep-equal to the pre-change capture. The expected value was captured by running parsePackages against that payload on the UNTOUCHED tree at ad3d4029abb949cb41815b6ce38d5e0ecad1486a and pasted in, never re-derived.

Reverse verification — ablation, prediction stated first

Subject: packages-io.ts restored to its pre-change bytes from the pinned base commit, with the fix committed first so the restore leg has a real restore point.

  • Mutation proved on disk before the run: removed text count 0, injected text count 1, git hash-object differs from the HEAD blob.
  • Predicted: only the two pins that DISCRIMINATE go red — pin 1 and pin 3 — while pins 2, 4, 5 and the negative stay green, because those agree with the heuristic by construction.
  • Observed: Tests 2 failed | 13 passed (15), failing exactly honours writable:false on a scope-less row and lets the server win over the heuristic in BOTH directions on scope:project.
  • Restore leg: on-disk blob c94002e170b25bc7a2d40500e7adfef3a7328393 equal to the HEAD blob, git diff HEAD empty, 15/15 green again.

No rebuild step applies: the pins import the subject by a relative specifier inside the same package, so nothing resolves through dist.

Verification at dcef834

  • pnpm exec vitest run packages/app-shell/src/views/studio-design/packages-io.writableVerdict.test.ts packages/app-shell/src/views/studio-design/packages-io.test.ts packages/app-shell/src/views/studio-design/packages-io.duplicateEnvelope.test.tsTest Files 3 passed (3), Tests 23 passed (23), exit 0.
  • Whole directory: pnpm exec vitest run packages/app-shell/src/views/studio-design/Test Files 44 passed (44), Tests 236 passed (236), exit 0.
  • pnpm --filter @object-ui/app-shell run type-check — exit 0 (dependency closure built first; the chained tsconfig.test.json project includes src/**/*.test.ts, so the new pins are type-checked).
  • eslint on the two changed files with --no-inline-config — 0 errors, 0 warnings. Narrowing declared: the repo-wide population is eslint .; the file count (2) is read from --format json; eslint.config.js configures no type-aware linting (0 occurrences of projectService / parserOptions / project: / TypeChecked), so this diff cannot move any untouched file's verdict. CI runs the full farm regardless.
  • node scripts/check-changeset-presence.mjs — exit 0, 1 changeset declared for 1 released package.
  • pnpm check:control-bytes, check:vi-mock-specifiers, check:vi-mock-inherit, check:i18n-keys — all green.
  • node scripts/check-governed-queue-guard.mjs --test on the three changed paths — NOT GOVERNED.

Live server verification

Booted from objectstack commit bd0ee2fbb634e7cdcd3c2afffb6258fd8f3a0942 — the head of PR objectstack-ai/objectstack#14430, which is not yet in objectstack main (checked: withWritableVerdict has 0 occurrences in origin/main's packages/runtime/src/domains/packages.ts). The squash content is identical.

Single-package boot — the negative check

examples/app-todo on its own port with a fresh file: DB (the showcase app was tried first and is unusable for this: its GET /api/v1/packages answers HTTP 500 Converting circular structure to JSON … '_ObjectQL' … property 'engine', a pre-existing platform defect the PM is filing, unrelated to this change).

GET /api/v1/packages: 23 rows, all 23 carrying writable. The one row the switcher keeps:

com.example.todo | type=app | scope="project" | writable=false

Identical to what the heuristic said, so the switcher is unchanged for existing single-package apps.

Then POST /api/v1/packages/com.example.todo/duplicate created a Studio base, giving the pair that matters:

com.example.todo | type=app | scope="project" | writable=false
com.example.todo_copy | type=app | scope=ABSENT | writable=true

The Studio switcher, driven through the objectui HMR console pointed at that server (VITE_SERVER_URL / DEV_PROXY_TARGET, so this branch's code is what rendered — the vendored /_console bundle is stale by construction), lists Todo Manager as Read-only and Todo Copy (writable base) as Writable. That second row is the one a client-side "missing scope means read-only" rule would have broken, and it is scope-less on the wire.

Multi-package boot — BLOCKED

BLOCKED-BY objectstack-ai/objectstack#14439. The card's second acceptance line — switcher shows both packages, the module marked read-only, and the three Studio sections filtering by package — cannot be verified end to end yet, because no producer emits a packages[] artifact. One honest attempt, a two-package packages[] config booted through os dev from bd0ee2fb, refused at the producer door:

◆ Compile
────────────────────────────────────────
→ Loading configuration...
✗ defineStack validation failed (2 issues):
✗ packages.0.manifest.objects.0: Expected string but received object.
✗ packages.1.manifest.objects.0: Expected string but received object.
› Error: defineStack validation failed (2 issues):
✗ Compile failed — fix errors above before starting dev server

ArtifactPackageEntrySchema.manifest is the AUTHORING ManifestSchema, whose objects is z.array(z.string()) — glob patterns — so object DEFINITIONS in a sub-package body are refused before compile finishes.

A second, independent door refuses the same shape after compile, recorded here because it is a separate seam and objectstack-ai/objectstack#14439 will meet both: MetadataPlugin._parseAndRegisterArtifact hard-parses the whole artifact with ObjectStackDefinitionSchema before ADR-0130 D4's load path (which deliberately does not judge bodies) is ever reached, so an assembled packages[i].manifest.objects fails with expected string, received object and packages[i].manifest.permissions fails its union. Measured by parsing artifact variants directly: bodies with objects + permissions fail 4 ways; with permissions removed, 2 ways; with both removed, the parse passes. flows, apps and the other collections are simply undeclared on ManifestSchema, so they are stripped from the parsed copy and survive in the raw body — which is why objects (Data pillar) and permissions (Access pillar) are precisely the two the D4 seam cannot carry today.

This verification will be re-run against that card's examples/app-multi-package fixture once it lands. Nothing was worked around in objectui.

Boundaries

No new authorable key, no spec change, no lenient alias. The fallback is version compatibility with servers that predate the field, not tolerance of a second dialect: the field is either a boolean or it is absent.


🤖 Generated with Claude Code

https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m


Generated by Claude Code

…erdict
`GET /api/v1/packages` rows now carry a top-level `writable: boolean` computed
server-side by `isWritablePackage` (ADR-0070 D2, objectstack#14375) — the same
predicate the authoring and lifecycle gates enforce. `parsePackages` reads it
when present and keeps `scope !== 'project'` only as the fallback for servers
that predate the field.
The heuristic is wrong for exactly one row: a `type: module` sub-package of a
multi-package artifact (ADR-0130 D4) is served with no `scope` key, because the
schema default is applied at parse time while the artifact load path hands the
raw manifest body to `registerApp`. The heuristic reads it as a writable
database base while the server refuses every write to it, and nothing in the raw
row separates it from a scope-less Studio-created base that really is writable.
Pins: scope-less rows in both verdict directions, `scope: 'project'` overridden
in both directions, a non-boolean `writable` ignored, kernel packages hidden
whatever verdict they carry, and a payload with no `writable` key anywhere
producing output byte-identical to the pre-change capture.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.3 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-CljwiCD4.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
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.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.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)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.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.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
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.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.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-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
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 (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
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

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.3 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-CljwiCD4.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
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.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.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)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.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.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
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.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.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-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
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 (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
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

@hotlong
hotlong marked this pull request as ready for review September 2, 2026 07:45
@hotlongClaude

Copy link
Copy Markdown
ContributorAuthor

PM 复审:PASS —— head ac9555bdcef834 + merge origin/main

独立性声明

  • 复审会话 session_01UHvF5hyiZjnCyExFnfQB8m(Fable),与派发的 dev agent 同一会话身份、不同 agent 实例;维护者明示授权 PM 自审。复审对象是全 diff(3 文件 +186/−6)与真实服务器验证结果。
  • 为符合本仓「git merge origin/main、不 rebase、不 force」的同步规矩,PR 当时 behind,由我合入 origin/main(merge commit ac9555b)并推送;合并后三个 packages-io 测试文件 23/23 绿。

对树核实

断言实测
服务端优先、缺失回落typeof p.writable === 'boolean' ? p.writable : scope !== 'project';回落表达式一字未改
非布尔不当裁定✅ pin 4 用 'false' 字串在 scope:'project' 行上把"回落"与"强转"区分开(Boolean('false') 为 true 会在无 scope 行上巧合一致)
system/cloud 只管可见性✅ 过滤保持在 writable 读取之前,注释改写为"visibility, not writability"
逐位负向 pin✅ 期望值在未改动的树 ad3d4029 上采集后粘贴,不是重推
文档注释✅ 把"缺 scope = 可写的 DB base"这条错误规则改写为"服务端是权威,启发式只是旧服务端回落,且对无 scope 的启动 module 行是错的",并写明机制(parse 时默认 vs 原始体)
消融✅ 预测先说:只有两条有区分度的 pin 红(1、3),其余与启发式同向的绿;实测 2 红 13 绿恰如预测;复原 blob = HEAD;无 dist 腿(相对路径导入)——声明正确
真实服务器✅ app-todo:23 行全带 writablecom.example.todo 只读、duplicate 出的无 scope 键 base 可写;HMR console 指向该服务器渲染的是本分支代码
多包验证⏸ 诚实尝试一次,defineStack 在作者态就拒(原文在正文);挂到 objectstack#14439 落地后由 PM 复跑(已记在该卡)。dev 顺带挖出第三道 parse 缝(metadata/plugin.ts:912),已并入 #14439

changeset @object-ui/app-shell: patch ✓;三个文件对受管面零命中(dev 跑过 check-governed-queue-guard --test,CI Governed Surface Queue Guard 亦绿)。

CI(ac9555b

30 项全绿(含 Test 4 分片、Type Check、Lint、Build & E2E、Bundle Analysis、Governed Surface Queue Guard、Changeset 三项)。

收口

ready → squash auto-merge。落地即 Fixes #7177。多包 Studio 复跑与 .objectui-sha 提针由 PM 在 #14439 落地后跟进。


Generated by Claude Code

@hotlong
hotlong added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit ebc05b4Sep 2, 2026
32 checks passed
@hotlong
hotlong deleted the claude/issue-7177-studio-picker-module-packages branch September 2, 2026 08:00
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ADR-0130(objectstack#14122):Studio 包选择器列出 project 域的 module 包

2 participants

@hotlong@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(app-shell): Studio package switcher reads the server's writable verdict, heuristic only as fallback (#7177) - #7331

Merged
hotlong merged 2 commits into
mainfrom
claude/issue-7177-studio-picker-module-packages
Sep 2, 2026
Merged

fix(app-shell): Studio package switcher reads the server's writable verdict, heuristic only as fallback (#7177)#7331
hotlong merged 2 commits into
mainfrom
claude/issue-7177-studio-picker-module-packages

Conversation

@hotlong

Copy link
Copy Markdown
Contributor

Fixes#7177
Part of objectstack-ai/objectstack#14122 — ADR-0130 Consequences row 6, the client half. Server half: objectstack-ai/objectstack#14375 (PR objectstack-ai/objectstack#14430).

Why

parsePackages derived "writable" from manifest.scope alone (scope !== 'project'). That is not the rule the server enforces. isWritablePackage (ADR-0070 D2) reads engine.manifests first — a package booted from an artifact through registerApp is read-only whatever its scope says — and only then the system / cloud scopes. The two rules split on exactly the row ADR-0130 introduces:

rowmanifest.scopein engine.manifestsserverold client heuristic
type: module sub-package of a multi-package artifact (D4/D7 raw body)absentyesread-onlywritable — wrong
Studio-created database baseabsentnowritablewritable

Nothing in the raw row separates those two; only the server's engine.manifests does. A client-side "missing scope means read-only" rule (the first fix proposed on the card, since withdrawn) would have flipped every Studio base read-only. So the verdict moved server-side and this PR consumes it.

Measured on a live server (see Verification): the scope default is applied at PARSE time, while the artifact load path hands the RAW manifest body to registerApp — so the served row has no scope key at all, and the heuristic reads it as a writable database base.

What changed

packages/app-shell/src/views/studio-design/packages-io.ts:

  • parsePackages uses the row's own writable when the server states one (typeof p.writable === 'boolean'), and falls back to the unchanged scope !== 'project' expression when the key is absent (older servers). A non-boolean value is not a verdict and falls back too.
  • The module doc comment is rewritten: it used to state the heuristic as the rule. It now says the server is the authority, the heuristic is only the pre-objectstack-14375 fallback, and it is wrong for the scope-less booted module row — with the mechanism (parse-time default vs raw body) written down.
  • The system / cloud hide filter is untouched, and now says in writing that it is about visibility, not writability.

Changeset: .changeset/7177-studio-switcher-server-writable-verdict.md (@object-ui/app-shell: patch).

Pins

packages/app-shell/src/views/studio-design/packages-io.writableVerdict.test.ts — 5 tests:

  1. writable: false on a scope-less row (the ADR-0130 module) is honoured, and the test asserts the row really carries no scope so the verdict cannot be leaking out of one.
  2. writable: true on a scope-less row (a Studio base) is honoured.
  3. scope: 'project' is overridden in BOTH directions — server false stays false, server true wins over the heuristic.
  4. A non-boolean writable (the string "false") is ignored. Boolean('false') is true, so a coercing read would have agreed with the fallback by accident on the scope-less row; the scope: 'project' row is what tells the two apart.
  5. Kernel packages stay hidden whatever verdict they carry.

Negative pin (same file): a payload with no writable key anywhere produces output deep-equal to the pre-change capture. The expected value was captured by running parsePackages against that payload on the UNTOUCHED tree at ad3d4029abb949cb41815b6ce38d5e0ecad1486a and pasted in, never re-derived.

Reverse verification — ablation, prediction stated first

Subject: packages-io.ts restored to its pre-change bytes from the pinned base commit, with the fix committed first so the restore leg has a real restore point.

  • Mutation proved on disk before the run: removed text count 0, injected text count 1, git hash-object differs from the HEAD blob.
  • Predicted: only the two pins that DISCRIMINATE go red — pin 1 and pin 3 — while pins 2, 4, 5 and the negative stay green, because those agree with the heuristic by construction.
  • Observed: Tests 2 failed | 13 passed (15), failing exactly honours writable:false on a scope-less row and lets the server win over the heuristic in BOTH directions on scope:project.
  • Restore leg: on-disk blob c94002e170b25bc7a2d40500e7adfef3a7328393 equal to the HEAD blob, git diff HEAD empty, 15/15 green again.

No rebuild step applies: the pins import the subject by a relative specifier inside the same package, so nothing resolves through dist.

Verification at dcef834

  • pnpm exec vitest run packages/app-shell/src/views/studio-design/packages-io.writableVerdict.test.ts packages/app-shell/src/views/studio-design/packages-io.test.ts packages/app-shell/src/views/studio-design/packages-io.duplicateEnvelope.test.tsTest Files 3 passed (3), Tests 23 passed (23), exit 0.
  • Whole directory: pnpm exec vitest run packages/app-shell/src/views/studio-design/Test Files 44 passed (44), Tests 236 passed (236), exit 0.
  • pnpm --filter @object-ui/app-shell run type-check — exit 0 (dependency closure built first; the chained tsconfig.test.json project includes src/**/*.test.ts, so the new pins are type-checked).
  • eslint on the two changed files with --no-inline-config — 0 errors, 0 warnings. Narrowing declared: the repo-wide population is eslint .; the file count (2) is read from --format json; eslint.config.js configures no type-aware linting (0 occurrences of projectService / parserOptions / project: / TypeChecked), so this diff cannot move any untouched file's verdict. CI runs the full farm regardless.
  • node scripts/check-changeset-presence.mjs — exit 0, 1 changeset declared for 1 released package.
  • pnpm check:control-bytes, check:vi-mock-specifiers, check:vi-mock-inherit, check:i18n-keys — all green.
  • node scripts/check-governed-queue-guard.mjs --test on the three changed paths — NOT GOVERNED.

Live server verification

Booted from objectstack commit bd0ee2fbb634e7cdcd3c2afffb6258fd8f3a0942 — the head of PR objectstack-ai/objectstack#14430, which is not yet in objectstack main (checked: withWritableVerdict has 0 occurrences in origin/main's packages/runtime/src/domains/packages.ts). The squash content is identical.

Single-package boot — the negative check

examples/app-todo on its own port with a fresh file: DB (the showcase app was tried first and is unusable for this: its GET /api/v1/packages answers HTTP 500 Converting circular structure to JSON … '_ObjectQL' … property 'engine', a pre-existing platform defect the PM is filing, unrelated to this change).

GET /api/v1/packages: 23 rows, all 23 carrying writable. The one row the switcher keeps:

com.example.todo | type=app | scope="project" | writable=false

Identical to what the heuristic said, so the switcher is unchanged for existing single-package apps.

Then POST /api/v1/packages/com.example.todo/duplicate created a Studio base, giving the pair that matters:

com.example.todo | type=app | scope="project" | writable=false
com.example.todo_copy | type=app | scope=ABSENT | writable=true

The Studio switcher, driven through the objectui HMR console pointed at that server (VITE_SERVER_URL / DEV_PROXY_TARGET, so this branch's code is what rendered — the vendored /_console bundle is stale by construction), lists Todo Manager as Read-only and Todo Copy (writable base) as Writable. That second row is the one a client-side "missing scope means read-only" rule would have broken, and it is scope-less on the wire.

Multi-package boot — BLOCKED

BLOCKED-BY objectstack-ai/objectstack#14439. The card's second acceptance line — switcher shows both packages, the module marked read-only, and the three Studio sections filtering by package — cannot be verified end to end yet, because no producer emits a packages[] artifact. One honest attempt, a two-package packages[] config booted through os dev from bd0ee2fb, refused at the producer door:

◆ Compile
────────────────────────────────────────
→ Loading configuration...
✗ defineStack validation failed (2 issues):
✗ packages.0.manifest.objects.0: Expected string but received object.
✗ packages.1.manifest.objects.0: Expected string but received object.
› Error: defineStack validation failed (2 issues):
✗ Compile failed — fix errors above before starting dev server

ArtifactPackageEntrySchema.manifest is the AUTHORING ManifestSchema, whose objects is z.array(z.string()) — glob patterns — so object DEFINITIONS in a sub-package body are refused before compile finishes.

A second, independent door refuses the same shape after compile, recorded here because it is a separate seam and objectstack-ai/objectstack#14439 will meet both: MetadataPlugin._parseAndRegisterArtifact hard-parses the whole artifact with ObjectStackDefinitionSchema before ADR-0130 D4's load path (which deliberately does not judge bodies) is ever reached, so an assembled packages[i].manifest.objects fails with expected string, received object and packages[i].manifest.permissions fails its union. Measured by parsing artifact variants directly: bodies with objects + permissions fail 4 ways; with permissions removed, 2 ways; with both removed, the parse passes. flows, apps and the other collections are simply undeclared on ManifestSchema, so they are stripped from the parsed copy and survive in the raw body — which is why objects (Data pillar) and permissions (Access pillar) are precisely the two the D4 seam cannot carry today.

This verification will be re-run against that card's examples/app-multi-package fixture once it lands. Nothing was worked around in objectui.

Boundaries

No new authorable key, no spec change, no lenient alias. The fallback is version compatibility with servers that predate the field, not tolerance of a second dialect: the field is either a boolean or it is absent.


🤖 Generated with Claude Code

https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m


Generated by Claude Code

…erdict
`GET /api/v1/packages` rows now carry a top-level `writable: boolean` computed
server-side by `isWritablePackage` (ADR-0070 D2, objectstack#14375) — the same
predicate the authoring and lifecycle gates enforce. `parsePackages` reads it
when present and keeps `scope !== 'project'` only as the fallback for servers
that predate the field.
The heuristic is wrong for exactly one row: a `type: module` sub-package of a
multi-package artifact (ADR-0130 D4) is served with no `scope` key, because the
schema default is applied at parse time while the artifact load path hands the
raw manifest body to `registerApp`. The heuristic reads it as a writable
database base while the server refuses every write to it, and nothing in the raw
row separates it from a scope-less Studio-created base that really is writable.
Pins: scope-less rows in both verdict directions, `scope: 'project'` overridden
in both directions, a non-boolean `writable` ignored, kernel packages hidden
whatever verdict they carry, and a payload with no `writable` key anywhere
producing output byte-identical to the pre-change capture.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.3 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-CljwiCD4.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
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.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.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)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.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.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
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.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.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-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
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 (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
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

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.3 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-CljwiCD4.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
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.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.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)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.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.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
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.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.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-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
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 (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
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

@hotlong
hotlong marked this pull request as ready for review September 2, 2026 07:45
@hotlongClaude

Copy link
Copy Markdown
ContributorAuthor

PM 复审:PASS —— head ac9555bdcef834 + merge origin/main

独立性声明

  • 复审会话 session_01UHvF5hyiZjnCyExFnfQB8m(Fable),与派发的 dev agent 同一会话身份、不同 agent 实例;维护者明示授权 PM 自审。复审对象是全 diff(3 文件 +186/−6)与真实服务器验证结果。
  • 为符合本仓「git merge origin/main、不 rebase、不 force」的同步规矩,PR 当时 behind,由我合入 origin/main(merge commit ac9555b)并推送;合并后三个 packages-io 测试文件 23/23 绿。

对树核实

断言实测
服务端优先、缺失回落typeof p.writable === 'boolean' ? p.writable : scope !== 'project';回落表达式一字未改
非布尔不当裁定✅ pin 4 用 'false' 字串在 scope:'project' 行上把"回落"与"强转"区分开(Boolean('false') 为 true 会在无 scope 行上巧合一致)
system/cloud 只管可见性✅ 过滤保持在 writable 读取之前,注释改写为"visibility, not writability"
逐位负向 pin✅ 期望值在未改动的树 ad3d4029 上采集后粘贴,不是重推
文档注释✅ 把"缺 scope = 可写的 DB base"这条错误规则改写为"服务端是权威,启发式只是旧服务端回落,且对无 scope 的启动 module 行是错的",并写明机制(parse 时默认 vs 原始体)
消融✅ 预测先说:只有两条有区分度的 pin 红(1、3),其余与启发式同向的绿;实测 2 红 13 绿恰如预测;复原 blob = HEAD;无 dist 腿(相对路径导入)——声明正确
真实服务器✅ app-todo:23 行全带 writablecom.example.todo 只读、duplicate 出的无 scope 键 base 可写;HMR console 指向该服务器渲染的是本分支代码
多包验证⏸ 诚实尝试一次,defineStack 在作者态就拒(原文在正文);挂到 objectstack#14439 落地后由 PM 复跑(已记在该卡)。dev 顺带挖出第三道 parse 缝(metadata/plugin.ts:912),已并入 #14439

changeset @object-ui/app-shell: patch ✓;三个文件对受管面零命中(dev 跑过 check-governed-queue-guard --test,CI Governed Surface Queue Guard 亦绿)。

CI(ac9555b

30 项全绿(含 Test 4 分片、Type Check、Lint、Build & E2E、Bundle Analysis、Governed Surface Queue Guard、Changeset 三项)。

收口

ready → squash auto-merge。落地即 Fixes #7177。多包 Studio 复跑与 .objectui-sha 提针由 PM 在 #14439 落地后跟进。


Generated by Claude Code

@hotlong
hotlong added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit ebc05b4Sep 2, 2026
32 checks passed
@hotlong
hotlong deleted the claude/issue-7177-studio-picker-module-packages branch September 2, 2026 08:00
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ADR-0130(objectstack#14122):Studio 包选择器列出 project 域的 module 包

2 participants

@hotlong@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(app-shell): Studio package switcher reads the server's writable verdict, heuristic only as fallback (#7177) - #7331

Merged
hotlong merged 2 commits into
mainfrom
claude/issue-7177-studio-picker-module-packages
Sep 2, 2026
Merged

fix(app-shell): Studio package switcher reads the server's writable verdict, heuristic only as fallback (#7177)#7331
hotlong merged 2 commits into
mainfrom
claude/issue-7177-studio-picker-module-packages

Conversation

@hotlong

Copy link
Copy Markdown
Contributor

Fixes#7177
Part of objectstack-ai/objectstack#14122 — ADR-0130 Consequences row 6, the client half. Server half: objectstack-ai/objectstack#14375 (PR objectstack-ai/objectstack#14430).

Why

parsePackages derived "writable" from manifest.scope alone (scope !== 'project'). That is not the rule the server enforces. isWritablePackage (ADR-0070 D2) reads engine.manifests first — a package booted from an artifact through registerApp is read-only whatever its scope says — and only then the system / cloud scopes. The two rules split on exactly the row ADR-0130 introduces:

rowmanifest.scopein engine.manifestsserverold client heuristic
type: module sub-package of a multi-package artifact (D4/D7 raw body)absentyesread-onlywritable — wrong
Studio-created database baseabsentnowritablewritable

Nothing in the raw row separates those two; only the server's engine.manifests does. A client-side "missing scope means read-only" rule (the first fix proposed on the card, since withdrawn) would have flipped every Studio base read-only. So the verdict moved server-side and this PR consumes it.

Measured on a live server (see Verification): the scope default is applied at PARSE time, while the artifact load path hands the RAW manifest body to registerApp — so the served row has no scope key at all, and the heuristic reads it as a writable database base.

What changed

packages/app-shell/src/views/studio-design/packages-io.ts:

  • parsePackages uses the row's own writable when the server states one (typeof p.writable === 'boolean'), and falls back to the unchanged scope !== 'project' expression when the key is absent (older servers). A non-boolean value is not a verdict and falls back too.
  • The module doc comment is rewritten: it used to state the heuristic as the rule. It now says the server is the authority, the heuristic is only the pre-objectstack-14375 fallback, and it is wrong for the scope-less booted module row — with the mechanism (parse-time default vs raw body) written down.
  • The system / cloud hide filter is untouched, and now says in writing that it is about visibility, not writability.

Changeset: .changeset/7177-studio-switcher-server-writable-verdict.md (@object-ui/app-shell: patch).

Pins

packages/app-shell/src/views/studio-design/packages-io.writableVerdict.test.ts — 5 tests:

  1. writable: false on a scope-less row (the ADR-0130 module) is honoured, and the test asserts the row really carries no scope so the verdict cannot be leaking out of one.
  2. writable: true on a scope-less row (a Studio base) is honoured.
  3. scope: 'project' is overridden in BOTH directions — server false stays false, server true wins over the heuristic.
  4. A non-boolean writable (the string "false") is ignored. Boolean('false') is true, so a coercing read would have agreed with the fallback by accident on the scope-less row; the scope: 'project' row is what tells the two apart.
  5. Kernel packages stay hidden whatever verdict they carry.

Negative pin (same file): a payload with no writable key anywhere produces output deep-equal to the pre-change capture. The expected value was captured by running parsePackages against that payload on the UNTOUCHED tree at ad3d4029abb949cb41815b6ce38d5e0ecad1486a and pasted in, never re-derived.

Reverse verification — ablation, prediction stated first

Subject: packages-io.ts restored to its pre-change bytes from the pinned base commit, with the fix committed first so the restore leg has a real restore point.

  • Mutation proved on disk before the run: removed text count 0, injected text count 1, git hash-object differs from the HEAD blob.
  • Predicted: only the two pins that DISCRIMINATE go red — pin 1 and pin 3 — while pins 2, 4, 5 and the negative stay green, because those agree with the heuristic by construction.
  • Observed: Tests 2 failed | 13 passed (15), failing exactly honours writable:false on a scope-less row and lets the server win over the heuristic in BOTH directions on scope:project.
  • Restore leg: on-disk blob c94002e170b25bc7a2d40500e7adfef3a7328393 equal to the HEAD blob, git diff HEAD empty, 15/15 green again.

No rebuild step applies: the pins import the subject by a relative specifier inside the same package, so nothing resolves through dist.

Verification at dcef834

  • pnpm exec vitest run packages/app-shell/src/views/studio-design/packages-io.writableVerdict.test.ts packages/app-shell/src/views/studio-design/packages-io.test.ts packages/app-shell/src/views/studio-design/packages-io.duplicateEnvelope.test.tsTest Files 3 passed (3), Tests 23 passed (23), exit 0.
  • Whole directory: pnpm exec vitest run packages/app-shell/src/views/studio-design/Test Files 44 passed (44), Tests 236 passed (236), exit 0.
  • pnpm --filter @object-ui/app-shell run type-check — exit 0 (dependency closure built first; the chained tsconfig.test.json project includes src/**/*.test.ts, so the new pins are type-checked).
  • eslint on the two changed files with --no-inline-config — 0 errors, 0 warnings. Narrowing declared: the repo-wide population is eslint .; the file count (2) is read from --format json; eslint.config.js configures no type-aware linting (0 occurrences of projectService / parserOptions / project: / TypeChecked), so this diff cannot move any untouched file's verdict. CI runs the full farm regardless.
  • node scripts/check-changeset-presence.mjs — exit 0, 1 changeset declared for 1 released package.
  • pnpm check:control-bytes, check:vi-mock-specifiers, check:vi-mock-inherit, check:i18n-keys — all green.
  • node scripts/check-governed-queue-guard.mjs --test on the three changed paths — NOT GOVERNED.

Live server verification

Booted from objectstack commit bd0ee2fbb634e7cdcd3c2afffb6258fd8f3a0942 — the head of PR objectstack-ai/objectstack#14430, which is not yet in objectstack main (checked: withWritableVerdict has 0 occurrences in origin/main's packages/runtime/src/domains/packages.ts). The squash content is identical.

Single-package boot — the negative check

examples/app-todo on its own port with a fresh file: DB (the showcase app was tried first and is unusable for this: its GET /api/v1/packages answers HTTP 500 Converting circular structure to JSON … '_ObjectQL' … property 'engine', a pre-existing platform defect the PM is filing, unrelated to this change).

GET /api/v1/packages: 23 rows, all 23 carrying writable. The one row the switcher keeps:

com.example.todo | type=app | scope="project" | writable=false

Identical to what the heuristic said, so the switcher is unchanged for existing single-package apps.

Then POST /api/v1/packages/com.example.todo/duplicate created a Studio base, giving the pair that matters:

com.example.todo | type=app | scope="project" | writable=false
com.example.todo_copy | type=app | scope=ABSENT | writable=true

The Studio switcher, driven through the objectui HMR console pointed at that server (VITE_SERVER_URL / DEV_PROXY_TARGET, so this branch's code is what rendered — the vendored /_console bundle is stale by construction), lists Todo Manager as Read-only and Todo Copy (writable base) as Writable. That second row is the one a client-side "missing scope means read-only" rule would have broken, and it is scope-less on the wire.

Multi-package boot — BLOCKED

BLOCKED-BY objectstack-ai/objectstack#14439. The card's second acceptance line — switcher shows both packages, the module marked read-only, and the three Studio sections filtering by package — cannot be verified end to end yet, because no producer emits a packages[] artifact. One honest attempt, a two-package packages[] config booted through os dev from bd0ee2fb, refused at the producer door:

◆ Compile
────────────────────────────────────────
→ Loading configuration...
✗ defineStack validation failed (2 issues):
✗ packages.0.manifest.objects.0: Expected string but received object.
✗ packages.1.manifest.objects.0: Expected string but received object.
› Error: defineStack validation failed (2 issues):
✗ Compile failed — fix errors above before starting dev server

ArtifactPackageEntrySchema.manifest is the AUTHORING ManifestSchema, whose objects is z.array(z.string()) — glob patterns — so object DEFINITIONS in a sub-package body are refused before compile finishes.

A second, independent door refuses the same shape after compile, recorded here because it is a separate seam and objectstack-ai/objectstack#14439 will meet both: MetadataPlugin._parseAndRegisterArtifact hard-parses the whole artifact with ObjectStackDefinitionSchema before ADR-0130 D4's load path (which deliberately does not judge bodies) is ever reached, so an assembled packages[i].manifest.objects fails with expected string, received object and packages[i].manifest.permissions fails its union. Measured by parsing artifact variants directly: bodies with objects + permissions fail 4 ways; with permissions removed, 2 ways; with both removed, the parse passes. flows, apps and the other collections are simply undeclared on ManifestSchema, so they are stripped from the parsed copy and survive in the raw body — which is why objects (Data pillar) and permissions (Access pillar) are precisely the two the D4 seam cannot carry today.

This verification will be re-run against that card's examples/app-multi-package fixture once it lands. Nothing was worked around in objectui.

Boundaries

No new authorable key, no spec change, no lenient alias. The fallback is version compatibility with servers that predate the field, not tolerance of a second dialect: the field is either a boolean or it is absent.


🤖 Generated with Claude Code

https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m


Generated by Claude Code

…erdict
`GET /api/v1/packages` rows now carry a top-level `writable: boolean` computed
server-side by `isWritablePackage` (ADR-0070 D2, objectstack#14375) — the same
predicate the authoring and lifecycle gates enforce. `parsePackages` reads it
when present and keeps `scope !== 'project'` only as the fallback for servers
that predate the field.
The heuristic is wrong for exactly one row: a `type: module` sub-package of a
multi-package artifact (ADR-0130 D4) is served with no `scope` key, because the
schema default is applied at parse time while the artifact load path hands the
raw manifest body to `registerApp`. The heuristic reads it as a writable
database base while the server refuses every write to it, and nothing in the raw
row separates it from a scope-less Studio-created base that really is writable.
Pins: scope-less rows in both verdict directions, `scope: 'project'` overridden
in both directions, a non-boolean `writable` ignored, kernel packages hidden
whatever verdict they carry, and a payload with no `writable` key anywhere
producing output byte-identical to the pre-change capture.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.3 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-CljwiCD4.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
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.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.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)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.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.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
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.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.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-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
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 (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
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

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.3 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-CljwiCD4.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
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.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.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)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.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.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
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.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.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-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
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 (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
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

@hotlong
hotlong marked this pull request as ready for review September 2, 2026 07:45
@hotlongClaude

Copy link
Copy Markdown
ContributorAuthor

PM 复审:PASS —— head ac9555bdcef834 + merge origin/main

独立性声明

  • 复审会话 session_01UHvF5hyiZjnCyExFnfQB8m(Fable),与派发的 dev agent 同一会话身份、不同 agent 实例;维护者明示授权 PM 自审。复审对象是全 diff(3 文件 +186/−6)与真实服务器验证结果。
  • 为符合本仓「git merge origin/main、不 rebase、不 force」的同步规矩,PR 当时 behind,由我合入 origin/main(merge commit ac9555b)并推送;合并后三个 packages-io 测试文件 23/23 绿。

对树核实

断言实测
服务端优先、缺失回落typeof p.writable === 'boolean' ? p.writable : scope !== 'project';回落表达式一字未改
非布尔不当裁定✅ pin 4 用 'false' 字串在 scope:'project' 行上把"回落"与"强转"区分开(Boolean('false') 为 true 会在无 scope 行上巧合一致)
system/cloud 只管可见性✅ 过滤保持在 writable 读取之前,注释改写为"visibility, not writability"
逐位负向 pin✅ 期望值在未改动的树 ad3d4029 上采集后粘贴,不是重推
文档注释✅ 把"缺 scope = 可写的 DB base"这条错误规则改写为"服务端是权威,启发式只是旧服务端回落,且对无 scope 的启动 module 行是错的",并写明机制(parse 时默认 vs 原始体)
消融✅ 预测先说:只有两条有区分度的 pin 红(1、3),其余与启发式同向的绿;实测 2 红 13 绿恰如预测;复原 blob = HEAD;无 dist 腿(相对路径导入)——声明正确
真实服务器✅ app-todo:23 行全带 writablecom.example.todo 只读、duplicate 出的无 scope 键 base 可写;HMR console 指向该服务器渲染的是本分支代码
多包验证⏸ 诚实尝试一次,defineStack 在作者态就拒(原文在正文);挂到 objectstack#14439 落地后由 PM 复跑(已记在该卡)。dev 顺带挖出第三道 parse 缝(metadata/plugin.ts:912),已并入 #14439

changeset @object-ui/app-shell: patch ✓;三个文件对受管面零命中(dev 跑过 check-governed-queue-guard --test,CI Governed Surface Queue Guard 亦绿)。

CI(ac9555b

30 项全绿(含 Test 4 分片、Type Check、Lint、Build & E2E、Bundle Analysis、Governed Surface Queue Guard、Changeset 三项)。

收口

ready → squash auto-merge。落地即 Fixes #7177。多包 Studio 复跑与 .objectui-sha 提针由 PM 在 #14439 落地后跟进。


Generated by Claude Code

@hotlong
hotlong added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit ebc05b4Sep 2, 2026
32 checks passed
@hotlong
hotlong deleted the claude/issue-7177-studio-picker-module-packages branch September 2, 2026 08:00
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ADR-0130(objectstack#14122):Studio 包选择器列出 project 域的 module 包

2 participants

@hotlong@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(app-shell): Studio package switcher reads the server's writable verdict, heuristic only as fallback (#7177) - #7331

Merged
hotlong merged 2 commits into
mainfrom
claude/issue-7177-studio-picker-module-packages
Sep 2, 2026
Merged

fix(app-shell): Studio package switcher reads the server's writable verdict, heuristic only as fallback (#7177)#7331
hotlong merged 2 commits into
mainfrom
claude/issue-7177-studio-picker-module-packages

Conversation

@hotlong

Copy link
Copy Markdown
Contributor

Fixes#7177
Part of objectstack-ai/objectstack#14122 — ADR-0130 Consequences row 6, the client half. Server half: objectstack-ai/objectstack#14375 (PR objectstack-ai/objectstack#14430).

Why

parsePackages derived "writable" from manifest.scope alone (scope !== 'project'). That is not the rule the server enforces. isWritablePackage (ADR-0070 D2) reads engine.manifests first — a package booted from an artifact through registerApp is read-only whatever its scope says — and only then the system / cloud scopes. The two rules split on exactly the row ADR-0130 introduces:

rowmanifest.scopein engine.manifestsserverold client heuristic
type: module sub-package of a multi-package artifact (D4/D7 raw body)absentyesread-onlywritable — wrong
Studio-created database baseabsentnowritablewritable

Nothing in the raw row separates those two; only the server's engine.manifests does. A client-side "missing scope means read-only" rule (the first fix proposed on the card, since withdrawn) would have flipped every Studio base read-only. So the verdict moved server-side and this PR consumes it.

Measured on a live server (see Verification): the scope default is applied at PARSE time, while the artifact load path hands the RAW manifest body to registerApp — so the served row has no scope key at all, and the heuristic reads it as a writable database base.

What changed

packages/app-shell/src/views/studio-design/packages-io.ts:

  • parsePackages uses the row's own writable when the server states one (typeof p.writable === 'boolean'), and falls back to the unchanged scope !== 'project' expression when the key is absent (older servers). A non-boolean value is not a verdict and falls back too.
  • The module doc comment is rewritten: it used to state the heuristic as the rule. It now says the server is the authority, the heuristic is only the pre-objectstack-14375 fallback, and it is wrong for the scope-less booted module row — with the mechanism (parse-time default vs raw body) written down.
  • The system / cloud hide filter is untouched, and now says in writing that it is about visibility, not writability.

Changeset: .changeset/7177-studio-switcher-server-writable-verdict.md (@object-ui/app-shell: patch).

Pins

packages/app-shell/src/views/studio-design/packages-io.writableVerdict.test.ts — 5 tests:

  1. writable: false on a scope-less row (the ADR-0130 module) is honoured, and the test asserts the row really carries no scope so the verdict cannot be leaking out of one.
  2. writable: true on a scope-less row (a Studio base) is honoured.
  3. scope: 'project' is overridden in BOTH directions — server false stays false, server true wins over the heuristic.
  4. A non-boolean writable (the string "false") is ignored. Boolean('false') is true, so a coercing read would have agreed with the fallback by accident on the scope-less row; the scope: 'project' row is what tells the two apart.
  5. Kernel packages stay hidden whatever verdict they carry.

Negative pin (same file): a payload with no writable key anywhere produces output deep-equal to the pre-change capture. The expected value was captured by running parsePackages against that payload on the UNTOUCHED tree at ad3d4029abb949cb41815b6ce38d5e0ecad1486a and pasted in, never re-derived.

Reverse verification — ablation, prediction stated first

Subject: packages-io.ts restored to its pre-change bytes from the pinned base commit, with the fix committed first so the restore leg has a real restore point.

  • Mutation proved on disk before the run: removed text count 0, injected text count 1, git hash-object differs from the HEAD blob.
  • Predicted: only the two pins that DISCRIMINATE go red — pin 1 and pin 3 — while pins 2, 4, 5 and the negative stay green, because those agree with the heuristic by construction.
  • Observed: Tests 2 failed | 13 passed (15), failing exactly honours writable:false on a scope-less row and lets the server win over the heuristic in BOTH directions on scope:project.
  • Restore leg: on-disk blob c94002e170b25bc7a2d40500e7adfef3a7328393 equal to the HEAD blob, git diff HEAD empty, 15/15 green again.

No rebuild step applies: the pins import the subject by a relative specifier inside the same package, so nothing resolves through dist.

Verification at dcef834

  • pnpm exec vitest run packages/app-shell/src/views/studio-design/packages-io.writableVerdict.test.ts packages/app-shell/src/views/studio-design/packages-io.test.ts packages/app-shell/src/views/studio-design/packages-io.duplicateEnvelope.test.tsTest Files 3 passed (3), Tests 23 passed (23), exit 0.
  • Whole directory: pnpm exec vitest run packages/app-shell/src/views/studio-design/Test Files 44 passed (44), Tests 236 passed (236), exit 0.
  • pnpm --filter @object-ui/app-shell run type-check — exit 0 (dependency closure built first; the chained tsconfig.test.json project includes src/**/*.test.ts, so the new pins are type-checked).
  • eslint on the two changed files with --no-inline-config — 0 errors, 0 warnings. Narrowing declared: the repo-wide population is eslint .; the file count (2) is read from --format json; eslint.config.js configures no type-aware linting (0 occurrences of projectService / parserOptions / project: / TypeChecked), so this diff cannot move any untouched file's verdict. CI runs the full farm regardless.
  • node scripts/check-changeset-presence.mjs — exit 0, 1 changeset declared for 1 released package.
  • pnpm check:control-bytes, check:vi-mock-specifiers, check:vi-mock-inherit, check:i18n-keys — all green.
  • node scripts/check-governed-queue-guard.mjs --test on the three changed paths — NOT GOVERNED.

Live server verification

Booted from objectstack commit bd0ee2fbb634e7cdcd3c2afffb6258fd8f3a0942 — the head of PR objectstack-ai/objectstack#14430, which is not yet in objectstack main (checked: withWritableVerdict has 0 occurrences in origin/main's packages/runtime/src/domains/packages.ts). The squash content is identical.

Single-package boot — the negative check

examples/app-todo on its own port with a fresh file: DB (the showcase app was tried first and is unusable for this: its GET /api/v1/packages answers HTTP 500 Converting circular structure to JSON … '_ObjectQL' … property 'engine', a pre-existing platform defect the PM is filing, unrelated to this change).

GET /api/v1/packages: 23 rows, all 23 carrying writable. The one row the switcher keeps:

com.example.todo | type=app | scope="project" | writable=false

Identical to what the heuristic said, so the switcher is unchanged for existing single-package apps.

Then POST /api/v1/packages/com.example.todo/duplicate created a Studio base, giving the pair that matters:

com.example.todo | type=app | scope="project" | writable=false
com.example.todo_copy | type=app | scope=ABSENT | writable=true

The Studio switcher, driven through the objectui HMR console pointed at that server (VITE_SERVER_URL / DEV_PROXY_TARGET, so this branch's code is what rendered — the vendored /_console bundle is stale by construction), lists Todo Manager as Read-only and Todo Copy (writable base) as Writable. That second row is the one a client-side "missing scope means read-only" rule would have broken, and it is scope-less on the wire.

Multi-package boot — BLOCKED

BLOCKED-BY objectstack-ai/objectstack#14439. The card's second acceptance line — switcher shows both packages, the module marked read-only, and the three Studio sections filtering by package — cannot be verified end to end yet, because no producer emits a packages[] artifact. One honest attempt, a two-package packages[] config booted through os dev from bd0ee2fb, refused at the producer door:

◆ Compile
────────────────────────────────────────
→ Loading configuration...
✗ defineStack validation failed (2 issues):
✗ packages.0.manifest.objects.0: Expected string but received object.
✗ packages.1.manifest.objects.0: Expected string but received object.
› Error: defineStack validation failed (2 issues):
✗ Compile failed — fix errors above before starting dev server

ArtifactPackageEntrySchema.manifest is the AUTHORING ManifestSchema, whose objects is z.array(z.string()) — glob patterns — so object DEFINITIONS in a sub-package body are refused before compile finishes.

A second, independent door refuses the same shape after compile, recorded here because it is a separate seam and objectstack-ai/objectstack#14439 will meet both: MetadataPlugin._parseAndRegisterArtifact hard-parses the whole artifact with ObjectStackDefinitionSchema before ADR-0130 D4's load path (which deliberately does not judge bodies) is ever reached, so an assembled packages[i].manifest.objects fails with expected string, received object and packages[i].manifest.permissions fails its union. Measured by parsing artifact variants directly: bodies with objects + permissions fail 4 ways; with permissions removed, 2 ways; with both removed, the parse passes. flows, apps and the other collections are simply undeclared on ManifestSchema, so they are stripped from the parsed copy and survive in the raw body — which is why objects (Data pillar) and permissions (Access pillar) are precisely the two the D4 seam cannot carry today.

This verification will be re-run against that card's examples/app-multi-package fixture once it lands. Nothing was worked around in objectui.

Boundaries

No new authorable key, no spec change, no lenient alias. The fallback is version compatibility with servers that predate the field, not tolerance of a second dialect: the field is either a boolean or it is absent.


🤖 Generated with Claude Code

https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m


Generated by Claude Code

…erdict
`GET /api/v1/packages` rows now carry a top-level `writable: boolean` computed
server-side by `isWritablePackage` (ADR-0070 D2, objectstack#14375) — the same
predicate the authoring and lifecycle gates enforce. `parsePackages` reads it
when present and keeps `scope !== 'project'` only as the fallback for servers
that predate the field.
The heuristic is wrong for exactly one row: a `type: module` sub-package of a
multi-package artifact (ADR-0130 D4) is served with no `scope` key, because the
schema default is applied at parse time while the artifact load path hands the
raw manifest body to `registerApp`. The heuristic reads it as a writable
database base while the server refuses every write to it, and nothing in the raw
row separates it from a scope-less Studio-created base that really is writable.
Pins: scope-less rows in both verdict directions, `scope: 'project'` overridden
in both directions, a non-boolean `writable` ignored, kernel packages hidden
whatever verdict they carry, and a payload with no `writable` key anywhere
producing output byte-identical to the pre-change capture.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.3 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-CljwiCD4.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
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.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.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)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.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.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
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.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.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-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
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 (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
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

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.3 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-CljwiCD4.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
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.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.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)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.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.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
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.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.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-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
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 (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
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

@hotlong
hotlong marked this pull request as ready for review September 2, 2026 07:45
@hotlongClaude

Copy link
Copy Markdown
ContributorAuthor

PM 复审:PASS —— head ac9555bdcef834 + merge origin/main

独立性声明

  • 复审会话 session_01UHvF5hyiZjnCyExFnfQB8m(Fable),与派发的 dev agent 同一会话身份、不同 agent 实例;维护者明示授权 PM 自审。复审对象是全 diff(3 文件 +186/−6)与真实服务器验证结果。
  • 为符合本仓「git merge origin/main、不 rebase、不 force」的同步规矩,PR 当时 behind,由我合入 origin/main(merge commit ac9555b)并推送;合并后三个 packages-io 测试文件 23/23 绿。

对树核实

断言实测
服务端优先、缺失回落typeof p.writable === 'boolean' ? p.writable : scope !== 'project';回落表达式一字未改
非布尔不当裁定✅ pin 4 用 'false' 字串在 scope:'project' 行上把"回落"与"强转"区分开(Boolean('false') 为 true 会在无 scope 行上巧合一致)
system/cloud 只管可见性✅ 过滤保持在 writable 读取之前,注释改写为"visibility, not writability"
逐位负向 pin✅ 期望值在未改动的树 ad3d4029 上采集后粘贴,不是重推
文档注释✅ 把"缺 scope = 可写的 DB base"这条错误规则改写为"服务端是权威,启发式只是旧服务端回落,且对无 scope 的启动 module 行是错的",并写明机制(parse 时默认 vs 原始体)
消融✅ 预测先说:只有两条有区分度的 pin 红(1、3),其余与启发式同向的绿;实测 2 红 13 绿恰如预测;复原 blob = HEAD;无 dist 腿(相对路径导入)——声明正确
真实服务器✅ app-todo:23 行全带 writablecom.example.todo 只读、duplicate 出的无 scope 键 base 可写;HMR console 指向该服务器渲染的是本分支代码
多包验证⏸ 诚实尝试一次,defineStack 在作者态就拒(原文在正文);挂到 objectstack#14439 落地后由 PM 复跑(已记在该卡)。dev 顺带挖出第三道 parse 缝(metadata/plugin.ts:912),已并入 #14439

changeset @object-ui/app-shell: patch ✓;三个文件对受管面零命中(dev 跑过 check-governed-queue-guard --test,CI Governed Surface Queue Guard 亦绿)。

CI(ac9555b

30 项全绿(含 Test 4 分片、Type Check、Lint、Build & E2E、Bundle Analysis、Governed Surface Queue Guard、Changeset 三项)。

收口

ready → squash auto-merge。落地即 Fixes #7177。多包 Studio 复跑与 .objectui-sha 提针由 PM 在 #14439 落地后跟进。


Generated by Claude Code

@hotlong
hotlong added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit ebc05b4Sep 2, 2026
32 checks passed
@hotlong
hotlong deleted the claude/issue-7177-studio-picker-module-packages branch September 2, 2026 08:00
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ADR-0130(objectstack#14122):Studio 包选择器列出 project 域的 module 包

2 participants

@hotlong@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(app-shell): Studio package switcher reads the server's writable verdict, heuristic only as fallback (#7177) - #7331

Merged
hotlong merged 2 commits into
mainfrom
claude/issue-7177-studio-picker-module-packages
Sep 2, 2026
Merged

fix(app-shell): Studio package switcher reads the server's writable verdict, heuristic only as fallback (#7177)#7331
hotlong merged 2 commits into
mainfrom
claude/issue-7177-studio-picker-module-packages

Conversation

@hotlong

Copy link
Copy Markdown
Contributor

Fixes#7177
Part of objectstack-ai/objectstack#14122 — ADR-0130 Consequences row 6, the client half. Server half: objectstack-ai/objectstack#14375 (PR objectstack-ai/objectstack#14430).

Why

parsePackages derived "writable" from manifest.scope alone (scope !== 'project'). That is not the rule the server enforces. isWritablePackage (ADR-0070 D2) reads engine.manifests first — a package booted from an artifact through registerApp is read-only whatever its scope says — and only then the system / cloud scopes. The two rules split on exactly the row ADR-0130 introduces:

rowmanifest.scopein engine.manifestsserverold client heuristic
type: module sub-package of a multi-package artifact (D4/D7 raw body)absentyesread-onlywritable — wrong
Studio-created database baseabsentnowritablewritable

Nothing in the raw row separates those two; only the server's engine.manifests does. A client-side "missing scope means read-only" rule (the first fix proposed on the card, since withdrawn) would have flipped every Studio base read-only. So the verdict moved server-side and this PR consumes it.

Measured on a live server (see Verification): the scope default is applied at PARSE time, while the artifact load path hands the RAW manifest body to registerApp — so the served row has no scope key at all, and the heuristic reads it as a writable database base.

What changed

packages/app-shell/src/views/studio-design/packages-io.ts:

  • parsePackages uses the row's own writable when the server states one (typeof p.writable === 'boolean'), and falls back to the unchanged scope !== 'project' expression when the key is absent (older servers). A non-boolean value is not a verdict and falls back too.
  • The module doc comment is rewritten: it used to state the heuristic as the rule. It now says the server is the authority, the heuristic is only the pre-objectstack-14375 fallback, and it is wrong for the scope-less booted module row — with the mechanism (parse-time default vs raw body) written down.
  • The system / cloud hide filter is untouched, and now says in writing that it is about visibility, not writability.

Changeset: .changeset/7177-studio-switcher-server-writable-verdict.md (@object-ui/app-shell: patch).

Pins

packages/app-shell/src/views/studio-design/packages-io.writableVerdict.test.ts — 5 tests:

  1. writable: false on a scope-less row (the ADR-0130 module) is honoured, and the test asserts the row really carries no scope so the verdict cannot be leaking out of one.
  2. writable: true on a scope-less row (a Studio base) is honoured.
  3. scope: 'project' is overridden in BOTH directions — server false stays false, server true wins over the heuristic.
  4. A non-boolean writable (the string "false") is ignored. Boolean('false') is true, so a coercing read would have agreed with the fallback by accident on the scope-less row; the scope: 'project' row is what tells the two apart.
  5. Kernel packages stay hidden whatever verdict they carry.

Negative pin (same file): a payload with no writable key anywhere produces output deep-equal to the pre-change capture. The expected value was captured by running parsePackages against that payload on the UNTOUCHED tree at ad3d4029abb949cb41815b6ce38d5e0ecad1486a and pasted in, never re-derived.

Reverse verification — ablation, prediction stated first

Subject: packages-io.ts restored to its pre-change bytes from the pinned base commit, with the fix committed first so the restore leg has a real restore point.

  • Mutation proved on disk before the run: removed text count 0, injected text count 1, git hash-object differs from the HEAD blob.
  • Predicted: only the two pins that DISCRIMINATE go red — pin 1 and pin 3 — while pins 2, 4, 5 and the negative stay green, because those agree with the heuristic by construction.
  • Observed: Tests 2 failed | 13 passed (15), failing exactly honours writable:false on a scope-less row and lets the server win over the heuristic in BOTH directions on scope:project.
  • Restore leg: on-disk blob c94002e170b25bc7a2d40500e7adfef3a7328393 equal to the HEAD blob, git diff HEAD empty, 15/15 green again.

No rebuild step applies: the pins import the subject by a relative specifier inside the same package, so nothing resolves through dist.

Verification at dcef834

  • pnpm exec vitest run packages/app-shell/src/views/studio-design/packages-io.writableVerdict.test.ts packages/app-shell/src/views/studio-design/packages-io.test.ts packages/app-shell/src/views/studio-design/packages-io.duplicateEnvelope.test.tsTest Files 3 passed (3), Tests 23 passed (23), exit 0.
  • Whole directory: pnpm exec vitest run packages/app-shell/src/views/studio-design/Test Files 44 passed (44), Tests 236 passed (236), exit 0.
  • pnpm --filter @object-ui/app-shell run type-check — exit 0 (dependency closure built first; the chained tsconfig.test.json project includes src/**/*.test.ts, so the new pins are type-checked).
  • eslint on the two changed files with --no-inline-config — 0 errors, 0 warnings. Narrowing declared: the repo-wide population is eslint .; the file count (2) is read from --format json; eslint.config.js configures no type-aware linting (0 occurrences of projectService / parserOptions / project: / TypeChecked), so this diff cannot move any untouched file's verdict. CI runs the full farm regardless.
  • node scripts/check-changeset-presence.mjs — exit 0, 1 changeset declared for 1 released package.
  • pnpm check:control-bytes, check:vi-mock-specifiers, check:vi-mock-inherit, check:i18n-keys — all green.
  • node scripts/check-governed-queue-guard.mjs --test on the three changed paths — NOT GOVERNED.

Live server verification

Booted from objectstack commit bd0ee2fbb634e7cdcd3c2afffb6258fd8f3a0942 — the head of PR objectstack-ai/objectstack#14430, which is not yet in objectstack main (checked: withWritableVerdict has 0 occurrences in origin/main's packages/runtime/src/domains/packages.ts). The squash content is identical.

Single-package boot — the negative check

examples/app-todo on its own port with a fresh file: DB (the showcase app was tried first and is unusable for this: its GET /api/v1/packages answers HTTP 500 Converting circular structure to JSON … '_ObjectQL' … property 'engine', a pre-existing platform defect the PM is filing, unrelated to this change).

GET /api/v1/packages: 23 rows, all 23 carrying writable. The one row the switcher keeps:

com.example.todo | type=app | scope="project" | writable=false

Identical to what the heuristic said, so the switcher is unchanged for existing single-package apps.

Then POST /api/v1/packages/com.example.todo/duplicate created a Studio base, giving the pair that matters:

com.example.todo | type=app | scope="project" | writable=false
com.example.todo_copy | type=app | scope=ABSENT | writable=true

The Studio switcher, driven through the objectui HMR console pointed at that server (VITE_SERVER_URL / DEV_PROXY_TARGET, so this branch's code is what rendered — the vendored /_console bundle is stale by construction), lists Todo Manager as Read-only and Todo Copy (writable base) as Writable. That second row is the one a client-side "missing scope means read-only" rule would have broken, and it is scope-less on the wire.

Multi-package boot — BLOCKED

BLOCKED-BY objectstack-ai/objectstack#14439. The card's second acceptance line — switcher shows both packages, the module marked read-only, and the three Studio sections filtering by package — cannot be verified end to end yet, because no producer emits a packages[] artifact. One honest attempt, a two-package packages[] config booted through os dev from bd0ee2fb, refused at the producer door:

◆ Compile
────────────────────────────────────────
→ Loading configuration...
✗ defineStack validation failed (2 issues):
✗ packages.0.manifest.objects.0: Expected string but received object.
✗ packages.1.manifest.objects.0: Expected string but received object.
› Error: defineStack validation failed (2 issues):
✗ Compile failed — fix errors above before starting dev server

ArtifactPackageEntrySchema.manifest is the AUTHORING ManifestSchema, whose objects is z.array(z.string()) — glob patterns — so object DEFINITIONS in a sub-package body are refused before compile finishes.

A second, independent door refuses the same shape after compile, recorded here because it is a separate seam and objectstack-ai/objectstack#14439 will meet both: MetadataPlugin._parseAndRegisterArtifact hard-parses the whole artifact with ObjectStackDefinitionSchema before ADR-0130 D4's load path (which deliberately does not judge bodies) is ever reached, so an assembled packages[i].manifest.objects fails with expected string, received object and packages[i].manifest.permissions fails its union. Measured by parsing artifact variants directly: bodies with objects + permissions fail 4 ways; with permissions removed, 2 ways; with both removed, the parse passes. flows, apps and the other collections are simply undeclared on ManifestSchema, so they are stripped from the parsed copy and survive in the raw body — which is why objects (Data pillar) and permissions (Access pillar) are precisely the two the D4 seam cannot carry today.

This verification will be re-run against that card's examples/app-multi-package fixture once it lands. Nothing was worked around in objectui.

Boundaries

No new authorable key, no spec change, no lenient alias. The fallback is version compatibility with servers that predate the field, not tolerance of a second dialect: the field is either a boolean or it is absent.


🤖 Generated with Claude Code

https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m


Generated by Claude Code

…erdict
`GET /api/v1/packages` rows now carry a top-level `writable: boolean` computed
server-side by `isWritablePackage` (ADR-0070 D2, objectstack#14375) — the same
predicate the authoring and lifecycle gates enforce. `parsePackages` reads it
when present and keeps `scope !== 'project'` only as the fallback for servers
that predate the field.
The heuristic is wrong for exactly one row: a `type: module` sub-package of a
multi-package artifact (ADR-0130 D4) is served with no `scope` key, because the
schema default is applied at parse time while the artifact load path hands the
raw manifest body to `registerApp`. The heuristic reads it as a writable
database base while the server refuses every write to it, and nothing in the raw
row separates it from a scope-less Studio-created base that really is writable.
Pins: scope-less rows in both verdict directions, `scope: 'project'` overridden
in both directions, a non-boolean `writable` ignored, kernel packages hidden
whatever verdict they carry, and a payload with no `writable` key anywhere
producing output byte-identical to the pre-change capture.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.3 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-CljwiCD4.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
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.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.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)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.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.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
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.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.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-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
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 (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
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

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.3 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-CljwiCD4.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
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.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.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)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.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.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
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.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.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-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
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 (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
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

@hotlong
hotlong marked this pull request as ready for review September 2, 2026 07:45
@hotlongClaude

Copy link
Copy Markdown
ContributorAuthor

PM 复审:PASS —— head ac9555bdcef834 + merge origin/main

独立性声明

  • 复审会话 session_01UHvF5hyiZjnCyExFnfQB8m(Fable),与派发的 dev agent 同一会话身份、不同 agent 实例;维护者明示授权 PM 自审。复审对象是全 diff(3 文件 +186/−6)与真实服务器验证结果。
  • 为符合本仓「git merge origin/main、不 rebase、不 force」的同步规矩,PR 当时 behind,由我合入 origin/main(merge commit ac9555b)并推送;合并后三个 packages-io 测试文件 23/23 绿。

对树核实

断言实测
服务端优先、缺失回落typeof p.writable === 'boolean' ? p.writable : scope !== 'project';回落表达式一字未改
非布尔不当裁定✅ pin 4 用 'false' 字串在 scope:'project' 行上把"回落"与"强转"区分开(Boolean('false') 为 true 会在无 scope 行上巧合一致)
system/cloud 只管可见性✅ 过滤保持在 writable 读取之前,注释改写为"visibility, not writability"
逐位负向 pin✅ 期望值在未改动的树 ad3d4029 上采集后粘贴,不是重推
文档注释✅ 把"缺 scope = 可写的 DB base"这条错误规则改写为"服务端是权威,启发式只是旧服务端回落,且对无 scope 的启动 module 行是错的",并写明机制(parse 时默认 vs 原始体)
消融✅ 预测先说:只有两条有区分度的 pin 红(1、3),其余与启发式同向的绿;实测 2 红 13 绿恰如预测;复原 blob = HEAD;无 dist 腿(相对路径导入)——声明正确
真实服务器✅ app-todo:23 行全带 writablecom.example.todo 只读、duplicate 出的无 scope 键 base 可写;HMR console 指向该服务器渲染的是本分支代码
多包验证⏸ 诚实尝试一次,defineStack 在作者态就拒(原文在正文);挂到 objectstack#14439 落地后由 PM 复跑(已记在该卡)。dev 顺带挖出第三道 parse 缝(metadata/plugin.ts:912),已并入 #14439

changeset @object-ui/app-shell: patch ✓;三个文件对受管面零命中(dev 跑过 check-governed-queue-guard --test,CI Governed Surface Queue Guard 亦绿)。

CI(ac9555b

30 项全绿(含 Test 4 分片、Type Check、Lint、Build & E2E、Bundle Analysis、Governed Surface Queue Guard、Changeset 三项)。

收口

ready → squash auto-merge。落地即 Fixes #7177。多包 Studio 复跑与 .objectui-sha 提针由 PM 在 #14439 落地后跟进。


Generated by Claude Code

@hotlong
hotlong added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit ebc05b4Sep 2, 2026
32 checks passed
@hotlong
hotlong deleted the claude/issue-7177-studio-picker-module-packages branch September 2, 2026 08:00
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ADR-0130(objectstack#14122):Studio 包选择器列出 project 域的 module 包

2 participants

@hotlong@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(app-shell): Studio package switcher reads the server's writable verdict, heuristic only as fallback (#7177) - #7331

Merged
hotlong merged 2 commits into
mainfrom
claude/issue-7177-studio-picker-module-packages
Sep 2, 2026
Merged

fix(app-shell): Studio package switcher reads the server's writable verdict, heuristic only as fallback (#7177)#7331
hotlong merged 2 commits into
mainfrom
claude/issue-7177-studio-picker-module-packages

Conversation

@hotlong

Copy link
Copy Markdown
Contributor

Fixes#7177
Part of objectstack-ai/objectstack#14122 — ADR-0130 Consequences row 6, the client half. Server half: objectstack-ai/objectstack#14375 (PR objectstack-ai/objectstack#14430).

Why

parsePackages derived "writable" from manifest.scope alone (scope !== 'project'). That is not the rule the server enforces. isWritablePackage (ADR-0070 D2) reads engine.manifests first — a package booted from an artifact through registerApp is read-only whatever its scope says — and only then the system / cloud scopes. The two rules split on exactly the row ADR-0130 introduces:

rowmanifest.scopein engine.manifestsserverold client heuristic
type: module sub-package of a multi-package artifact (D4/D7 raw body)absentyesread-onlywritable — wrong
Studio-created database baseabsentnowritablewritable

Nothing in the raw row separates those two; only the server's engine.manifests does. A client-side "missing scope means read-only" rule (the first fix proposed on the card, since withdrawn) would have flipped every Studio base read-only. So the verdict moved server-side and this PR consumes it.

Measured on a live server (see Verification): the scope default is applied at PARSE time, while the artifact load path hands the RAW manifest body to registerApp — so the served row has no scope key at all, and the heuristic reads it as a writable database base.

What changed

packages/app-shell/src/views/studio-design/packages-io.ts:

  • parsePackages uses the row's own writable when the server states one (typeof p.writable === 'boolean'), and falls back to the unchanged scope !== 'project' expression when the key is absent (older servers). A non-boolean value is not a verdict and falls back too.
  • The module doc comment is rewritten: it used to state the heuristic as the rule. It now says the server is the authority, the heuristic is only the pre-objectstack-14375 fallback, and it is wrong for the scope-less booted module row — with the mechanism (parse-time default vs raw body) written down.
  • The system / cloud hide filter is untouched, and now says in writing that it is about visibility, not writability.

Changeset: .changeset/7177-studio-switcher-server-writable-verdict.md (@object-ui/app-shell: patch).

Pins

packages/app-shell/src/views/studio-design/packages-io.writableVerdict.test.ts — 5 tests:

  1. writable: false on a scope-less row (the ADR-0130 module) is honoured, and the test asserts the row really carries no scope so the verdict cannot be leaking out of one.
  2. writable: true on a scope-less row (a Studio base) is honoured.
  3. scope: 'project' is overridden in BOTH directions — server false stays false, server true wins over the heuristic.
  4. A non-boolean writable (the string "false") is ignored. Boolean('false') is true, so a coercing read would have agreed with the fallback by accident on the scope-less row; the scope: 'project' row is what tells the two apart.
  5. Kernel packages stay hidden whatever verdict they carry.

Negative pin (same file): a payload with no writable key anywhere produces output deep-equal to the pre-change capture. The expected value was captured by running parsePackages against that payload on the UNTOUCHED tree at ad3d4029abb949cb41815b6ce38d5e0ecad1486a and pasted in, never re-derived.

Reverse verification — ablation, prediction stated first

Subject: packages-io.ts restored to its pre-change bytes from the pinned base commit, with the fix committed first so the restore leg has a real restore point.

  • Mutation proved on disk before the run: removed text count 0, injected text count 1, git hash-object differs from the HEAD blob.
  • Predicted: only the two pins that DISCRIMINATE go red — pin 1 and pin 3 — while pins 2, 4, 5 and the negative stay green, because those agree with the heuristic by construction.
  • Observed: Tests 2 failed | 13 passed (15), failing exactly honours writable:false on a scope-less row and lets the server win over the heuristic in BOTH directions on scope:project.
  • Restore leg: on-disk blob c94002e170b25bc7a2d40500e7adfef3a7328393 equal to the HEAD blob, git diff HEAD empty, 15/15 green again.

No rebuild step applies: the pins import the subject by a relative specifier inside the same package, so nothing resolves through dist.

Verification at dcef834

  • pnpm exec vitest run packages/app-shell/src/views/studio-design/packages-io.writableVerdict.test.ts packages/app-shell/src/views/studio-design/packages-io.test.ts packages/app-shell/src/views/studio-design/packages-io.duplicateEnvelope.test.tsTest Files 3 passed (3), Tests 23 passed (23), exit 0.
  • Whole directory: pnpm exec vitest run packages/app-shell/src/views/studio-design/Test Files 44 passed (44), Tests 236 passed (236), exit 0.
  • pnpm --filter @object-ui/app-shell run type-check — exit 0 (dependency closure built first; the chained tsconfig.test.json project includes src/**/*.test.ts, so the new pins are type-checked).
  • eslint on the two changed files with --no-inline-config — 0 errors, 0 warnings. Narrowing declared: the repo-wide population is eslint .; the file count (2) is read from --format json; eslint.config.js configures no type-aware linting (0 occurrences of projectService / parserOptions / project: / TypeChecked), so this diff cannot move any untouched file's verdict. CI runs the full farm regardless.
  • node scripts/check-changeset-presence.mjs — exit 0, 1 changeset declared for 1 released package.
  • pnpm check:control-bytes, check:vi-mock-specifiers, check:vi-mock-inherit, check:i18n-keys — all green.
  • node scripts/check-governed-queue-guard.mjs --test on the three changed paths — NOT GOVERNED.

Live server verification

Booted from objectstack commit bd0ee2fbb634e7cdcd3c2afffb6258fd8f3a0942 — the head of PR objectstack-ai/objectstack#14430, which is not yet in objectstack main (checked: withWritableVerdict has 0 occurrences in origin/main's packages/runtime/src/domains/packages.ts). The squash content is identical.

Single-package boot — the negative check

examples/app-todo on its own port with a fresh file: DB (the showcase app was tried first and is unusable for this: its GET /api/v1/packages answers HTTP 500 Converting circular structure to JSON … '_ObjectQL' … property 'engine', a pre-existing platform defect the PM is filing, unrelated to this change).

GET /api/v1/packages: 23 rows, all 23 carrying writable. The one row the switcher keeps:

com.example.todo | type=app | scope="project" | writable=false

Identical to what the heuristic said, so the switcher is unchanged for existing single-package apps.

Then POST /api/v1/packages/com.example.todo/duplicate created a Studio base, giving the pair that matters:

com.example.todo | type=app | scope="project" | writable=false
com.example.todo_copy | type=app | scope=ABSENT | writable=true

The Studio switcher, driven through the objectui HMR console pointed at that server (VITE_SERVER_URL / DEV_PROXY_TARGET, so this branch's code is what rendered — the vendored /_console bundle is stale by construction), lists Todo Manager as Read-only and Todo Copy (writable base) as Writable. That second row is the one a client-side "missing scope means read-only" rule would have broken, and it is scope-less on the wire.

Multi-package boot — BLOCKED

BLOCKED-BY objectstack-ai/objectstack#14439. The card's second acceptance line — switcher shows both packages, the module marked read-only, and the three Studio sections filtering by package — cannot be verified end to end yet, because no producer emits a packages[] artifact. One honest attempt, a two-package packages[] config booted through os dev from bd0ee2fb, refused at the producer door:

◆ Compile
────────────────────────────────────────
→ Loading configuration...
✗ defineStack validation failed (2 issues):
✗ packages.0.manifest.objects.0: Expected string but received object.
✗ packages.1.manifest.objects.0: Expected string but received object.
› Error: defineStack validation failed (2 issues):
✗ Compile failed — fix errors above before starting dev server

ArtifactPackageEntrySchema.manifest is the AUTHORING ManifestSchema, whose objects is z.array(z.string()) — glob patterns — so object DEFINITIONS in a sub-package body are refused before compile finishes.

A second, independent door refuses the same shape after compile, recorded here because it is a separate seam and objectstack-ai/objectstack#14439 will meet both: MetadataPlugin._parseAndRegisterArtifact hard-parses the whole artifact with ObjectStackDefinitionSchema before ADR-0130 D4's load path (which deliberately does not judge bodies) is ever reached, so an assembled packages[i].manifest.objects fails with expected string, received object and packages[i].manifest.permissions fails its union. Measured by parsing artifact variants directly: bodies with objects + permissions fail 4 ways; with permissions removed, 2 ways; with both removed, the parse passes. flows, apps and the other collections are simply undeclared on ManifestSchema, so they are stripped from the parsed copy and survive in the raw body — which is why objects (Data pillar) and permissions (Access pillar) are precisely the two the D4 seam cannot carry today.

This verification will be re-run against that card's examples/app-multi-package fixture once it lands. Nothing was worked around in objectui.

Boundaries

No new authorable key, no spec change, no lenient alias. The fallback is version compatibility with servers that predate the field, not tolerance of a second dialect: the field is either a boolean or it is absent.


🤖 Generated with Claude Code

https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m


Generated by Claude Code

…erdict
`GET /api/v1/packages` rows now carry a top-level `writable: boolean` computed
server-side by `isWritablePackage` (ADR-0070 D2, objectstack#14375) — the same
predicate the authoring and lifecycle gates enforce. `parsePackages` reads it
when present and keeps `scope !== 'project'` only as the fallback for servers
that predate the field.
The heuristic is wrong for exactly one row: a `type: module` sub-package of a
multi-package artifact (ADR-0130 D4) is served with no `scope` key, because the
schema default is applied at parse time while the artifact load path hands the
raw manifest body to `registerApp`. The heuristic reads it as a writable
database base while the server refuses every write to it, and nothing in the raw
row separates it from a scope-less Studio-created base that really is writable.
Pins: scope-less rows in both verdict directions, `scope: 'project'` overridden
in both directions, a non-boolean `writable` ignored, kernel packages hidden
whatever verdict they carry, and a payload with no `writable` key anywhere
producing output byte-identical to the pre-change capture.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.3 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-CljwiCD4.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
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.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.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)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.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.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
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.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.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-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
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 (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
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

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.3 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-CljwiCD4.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
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.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.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)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.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.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
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.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.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-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
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 (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
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

@hotlong
hotlong marked this pull request as ready for review September 2, 2026 07:45
@hotlongClaude

Copy link
Copy Markdown
ContributorAuthor

PM 复审:PASS —— head ac9555bdcef834 + merge origin/main

独立性声明

  • 复审会话 session_01UHvF5hyiZjnCyExFnfQB8m(Fable),与派发的 dev agent 同一会话身份、不同 agent 实例;维护者明示授权 PM 自审。复审对象是全 diff(3 文件 +186/−6)与真实服务器验证结果。
  • 为符合本仓「git merge origin/main、不 rebase、不 force」的同步规矩,PR 当时 behind,由我合入 origin/main(merge commit ac9555b)并推送;合并后三个 packages-io 测试文件 23/23 绿。

对树核实

断言实测
服务端优先、缺失回落typeof p.writable === 'boolean' ? p.writable : scope !== 'project';回落表达式一字未改
非布尔不当裁定✅ pin 4 用 'false' 字串在 scope:'project' 行上把"回落"与"强转"区分开(Boolean('false') 为 true 会在无 scope 行上巧合一致)
system/cloud 只管可见性✅ 过滤保持在 writable 读取之前,注释改写为"visibility, not writability"
逐位负向 pin✅ 期望值在未改动的树 ad3d4029 上采集后粘贴,不是重推
文档注释✅ 把"缺 scope = 可写的 DB base"这条错误规则改写为"服务端是权威,启发式只是旧服务端回落,且对无 scope 的启动 module 行是错的",并写明机制(parse 时默认 vs 原始体)
消融✅ 预测先说:只有两条有区分度的 pin 红(1、3),其余与启发式同向的绿;实测 2 红 13 绿恰如预测;复原 blob = HEAD;无 dist 腿(相对路径导入)——声明正确
真实服务器✅ app-todo:23 行全带 writablecom.example.todo 只读、duplicate 出的无 scope 键 base 可写;HMR console 指向该服务器渲染的是本分支代码
多包验证⏸ 诚实尝试一次,defineStack 在作者态就拒(原文在正文);挂到 objectstack#14439 落地后由 PM 复跑(已记在该卡)。dev 顺带挖出第三道 parse 缝(metadata/plugin.ts:912),已并入 #14439

changeset @object-ui/app-shell: patch ✓;三个文件对受管面零命中(dev 跑过 check-governed-queue-guard --test,CI Governed Surface Queue Guard 亦绿)。

CI(ac9555b

30 项全绿(含 Test 4 分片、Type Check、Lint、Build & E2E、Bundle Analysis、Governed Surface Queue Guard、Changeset 三项)。

收口

ready → squash auto-merge。落地即 Fixes #7177。多包 Studio 复跑与 .objectui-sha 提针由 PM 在 #14439 落地后跟进。


Generated by Claude Code

@hotlong
hotlong added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit ebc05b4Sep 2, 2026
32 checks passed
@hotlong
hotlong deleted the claude/issue-7177-studio-picker-module-packages branch September 2, 2026 08:00
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ADR-0130(objectstack#14122):Studio 包选择器列出 project 域的 module 包

2 participants

@hotlong@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(app-shell): Studio package switcher reads the server's writable verdict, heuristic only as fallback (#7177) - #7331

Merged
hotlong merged 2 commits into
mainfrom
claude/issue-7177-studio-picker-module-packages
Sep 2, 2026
Merged

fix(app-shell): Studio package switcher reads the server's writable verdict, heuristic only as fallback (#7177)#7331
hotlong merged 2 commits into
mainfrom
claude/issue-7177-studio-picker-module-packages

Conversation

@hotlong

Copy link
Copy Markdown
Contributor

Fixes#7177
Part of objectstack-ai/objectstack#14122 — ADR-0130 Consequences row 6, the client half. Server half: objectstack-ai/objectstack#14375 (PR objectstack-ai/objectstack#14430).

Why

parsePackages derived "writable" from manifest.scope alone (scope !== 'project'). That is not the rule the server enforces. isWritablePackage (ADR-0070 D2) reads engine.manifests first — a package booted from an artifact through registerApp is read-only whatever its scope says — and only then the system / cloud scopes. The two rules split on exactly the row ADR-0130 introduces:

rowmanifest.scopein engine.manifestsserverold client heuristic
type: module sub-package of a multi-package artifact (D4/D7 raw body)absentyesread-onlywritable — wrong
Studio-created database baseabsentnowritablewritable

Nothing in the raw row separates those two; only the server's engine.manifests does. A client-side "missing scope means read-only" rule (the first fix proposed on the card, since withdrawn) would have flipped every Studio base read-only. So the verdict moved server-side and this PR consumes it.

Measured on a live server (see Verification): the scope default is applied at PARSE time, while the artifact load path hands the RAW manifest body to registerApp — so the served row has no scope key at all, and the heuristic reads it as a writable database base.

What changed

packages/app-shell/src/views/studio-design/packages-io.ts:

  • parsePackages uses the row's own writable when the server states one (typeof p.writable === 'boolean'), and falls back to the unchanged scope !== 'project' expression when the key is absent (older servers). A non-boolean value is not a verdict and falls back too.
  • The module doc comment is rewritten: it used to state the heuristic as the rule. It now says the server is the authority, the heuristic is only the pre-objectstack-14375 fallback, and it is wrong for the scope-less booted module row — with the mechanism (parse-time default vs raw body) written down.
  • The system / cloud hide filter is untouched, and now says in writing that it is about visibility, not writability.

Changeset: .changeset/7177-studio-switcher-server-writable-verdict.md (@object-ui/app-shell: patch).

Pins

packages/app-shell/src/views/studio-design/packages-io.writableVerdict.test.ts — 5 tests:

  1. writable: false on a scope-less row (the ADR-0130 module) is honoured, and the test asserts the row really carries no scope so the verdict cannot be leaking out of one.
  2. writable: true on a scope-less row (a Studio base) is honoured.
  3. scope: 'project' is overridden in BOTH directions — server false stays false, server true wins over the heuristic.
  4. A non-boolean writable (the string "false") is ignored. Boolean('false') is true, so a coercing read would have agreed with the fallback by accident on the scope-less row; the scope: 'project' row is what tells the two apart.
  5. Kernel packages stay hidden whatever verdict they carry.

Negative pin (same file): a payload with no writable key anywhere produces output deep-equal to the pre-change capture. The expected value was captured by running parsePackages against that payload on the UNTOUCHED tree at ad3d4029abb949cb41815b6ce38d5e0ecad1486a and pasted in, never re-derived.

Reverse verification — ablation, prediction stated first

Subject: packages-io.ts restored to its pre-change bytes from the pinned base commit, with the fix committed first so the restore leg has a real restore point.

  • Mutation proved on disk before the run: removed text count 0, injected text count 1, git hash-object differs from the HEAD blob.
  • Predicted: only the two pins that DISCRIMINATE go red — pin 1 and pin 3 — while pins 2, 4, 5 and the negative stay green, because those agree with the heuristic by construction.
  • Observed: Tests 2 failed | 13 passed (15), failing exactly honours writable:false on a scope-less row and lets the server win over the heuristic in BOTH directions on scope:project.
  • Restore leg: on-disk blob c94002e170b25bc7a2d40500e7adfef3a7328393 equal to the HEAD blob, git diff HEAD empty, 15/15 green again.

No rebuild step applies: the pins import the subject by a relative specifier inside the same package, so nothing resolves through dist.

Verification at dcef834

  • pnpm exec vitest run packages/app-shell/src/views/studio-design/packages-io.writableVerdict.test.ts packages/app-shell/src/views/studio-design/packages-io.test.ts packages/app-shell/src/views/studio-design/packages-io.duplicateEnvelope.test.tsTest Files 3 passed (3), Tests 23 passed (23), exit 0.
  • Whole directory: pnpm exec vitest run packages/app-shell/src/views/studio-design/Test Files 44 passed (44), Tests 236 passed (236), exit 0.
  • pnpm --filter @object-ui/app-shell run type-check — exit 0 (dependency closure built first; the chained tsconfig.test.json project includes src/**/*.test.ts, so the new pins are type-checked).
  • eslint on the two changed files with --no-inline-config — 0 errors, 0 warnings. Narrowing declared: the repo-wide population is eslint .; the file count (2) is read from --format json; eslint.config.js configures no type-aware linting (0 occurrences of projectService / parserOptions / project: / TypeChecked), so this diff cannot move any untouched file's verdict. CI runs the full farm regardless.
  • node scripts/check-changeset-presence.mjs — exit 0, 1 changeset declared for 1 released package.
  • pnpm check:control-bytes, check:vi-mock-specifiers, check:vi-mock-inherit, check:i18n-keys — all green.
  • node scripts/check-governed-queue-guard.mjs --test on the three changed paths — NOT GOVERNED.

Live server verification

Booted from objectstack commit bd0ee2fbb634e7cdcd3c2afffb6258fd8f3a0942 — the head of PR objectstack-ai/objectstack#14430, which is not yet in objectstack main (checked: withWritableVerdict has 0 occurrences in origin/main's packages/runtime/src/domains/packages.ts). The squash content is identical.

Single-package boot — the negative check

examples/app-todo on its own port with a fresh file: DB (the showcase app was tried first and is unusable for this: its GET /api/v1/packages answers HTTP 500 Converting circular structure to JSON … '_ObjectQL' … property 'engine', a pre-existing platform defect the PM is filing, unrelated to this change).

GET /api/v1/packages: 23 rows, all 23 carrying writable. The one row the switcher keeps:

com.example.todo | type=app | scope="project" | writable=false

Identical to what the heuristic said, so the switcher is unchanged for existing single-package apps.

Then POST /api/v1/packages/com.example.todo/duplicate created a Studio base, giving the pair that matters:

com.example.todo | type=app | scope="project" | writable=false
com.example.todo_copy | type=app | scope=ABSENT | writable=true

The Studio switcher, driven through the objectui HMR console pointed at that server (VITE_SERVER_URL / DEV_PROXY_TARGET, so this branch's code is what rendered — the vendored /_console bundle is stale by construction), lists Todo Manager as Read-only and Todo Copy (writable base) as Writable. That second row is the one a client-side "missing scope means read-only" rule would have broken, and it is scope-less on the wire.

Multi-package boot — BLOCKED

BLOCKED-BY objectstack-ai/objectstack#14439. The card's second acceptance line — switcher shows both packages, the module marked read-only, and the three Studio sections filtering by package — cannot be verified end to end yet, because no producer emits a packages[] artifact. One honest attempt, a two-package packages[] config booted through os dev from bd0ee2fb, refused at the producer door:

◆ Compile
────────────────────────────────────────
→ Loading configuration...
✗ defineStack validation failed (2 issues):
✗ packages.0.manifest.objects.0: Expected string but received object.
✗ packages.1.manifest.objects.0: Expected string but received object.
› Error: defineStack validation failed (2 issues):
✗ Compile failed — fix errors above before starting dev server

ArtifactPackageEntrySchema.manifest is the AUTHORING ManifestSchema, whose objects is z.array(z.string()) — glob patterns — so object DEFINITIONS in a sub-package body are refused before compile finishes.

A second, independent door refuses the same shape after compile, recorded here because it is a separate seam and objectstack-ai/objectstack#14439 will meet both: MetadataPlugin._parseAndRegisterArtifact hard-parses the whole artifact with ObjectStackDefinitionSchema before ADR-0130 D4's load path (which deliberately does not judge bodies) is ever reached, so an assembled packages[i].manifest.objects fails with expected string, received object and packages[i].manifest.permissions fails its union. Measured by parsing artifact variants directly: bodies with objects + permissions fail 4 ways; with permissions removed, 2 ways; with both removed, the parse passes. flows, apps and the other collections are simply undeclared on ManifestSchema, so they are stripped from the parsed copy and survive in the raw body — which is why objects (Data pillar) and permissions (Access pillar) are precisely the two the D4 seam cannot carry today.

This verification will be re-run against that card's examples/app-multi-package fixture once it lands. Nothing was worked around in objectui.

Boundaries

No new authorable key, no spec change, no lenient alias. The fallback is version compatibility with servers that predate the field, not tolerance of a second dialect: the field is either a boolean or it is absent.


🤖 Generated with Claude Code

https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m


Generated by Claude Code

…erdict
`GET /api/v1/packages` rows now carry a top-level `writable: boolean` computed
server-side by `isWritablePackage` (ADR-0070 D2, objectstack#14375) — the same
predicate the authoring and lifecycle gates enforce. `parsePackages` reads it
when present and keeps `scope !== 'project'` only as the fallback for servers
that predate the field.
The heuristic is wrong for exactly one row: a `type: module` sub-package of a
multi-package artifact (ADR-0130 D4) is served with no `scope` key, because the
schema default is applied at parse time while the artifact load path hands the
raw manifest body to `registerApp`. The heuristic reads it as a writable
database base while the server refuses every write to it, and nothing in the raw
row separates it from a scope-less Studio-created base that really is writable.
Pins: scope-less rows in both verdict directions, `scope: 'project'` overridden
in both directions, a non-boolean `writable` ignored, kernel packages hidden
whatever verdict they carry, and a payload with no `writable` key anywhere
producing output byte-identical to the pre-change capture.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.3 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-CljwiCD4.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
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.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.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)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.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.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
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.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.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-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
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 (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
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

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.3 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-CljwiCD4.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
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.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.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)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.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.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
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.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.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-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
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 (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
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

@hotlong
hotlong marked this pull request as ready for review September 2, 2026 07:45
@hotlongClaude

Copy link
Copy Markdown
ContributorAuthor

PM 复审:PASS —— head ac9555bdcef834 + merge origin/main

独立性声明

  • 复审会话 session_01UHvF5hyiZjnCyExFnfQB8m(Fable),与派发的 dev agent 同一会话身份、不同 agent 实例;维护者明示授权 PM 自审。复审对象是全 diff(3 文件 +186/−6)与真实服务器验证结果。
  • 为符合本仓「git merge origin/main、不 rebase、不 force」的同步规矩,PR 当时 behind,由我合入 origin/main(merge commit ac9555b)并推送;合并后三个 packages-io 测试文件 23/23 绿。

对树核实

断言实测
服务端优先、缺失回落typeof p.writable === 'boolean' ? p.writable : scope !== 'project';回落表达式一字未改
非布尔不当裁定✅ pin 4 用 'false' 字串在 scope:'project' 行上把"回落"与"强转"区分开(Boolean('false') 为 true 会在无 scope 行上巧合一致)
system/cloud 只管可见性✅ 过滤保持在 writable 读取之前,注释改写为"visibility, not writability"
逐位负向 pin✅ 期望值在未改动的树 ad3d4029 上采集后粘贴,不是重推
文档注释✅ 把"缺 scope = 可写的 DB base"这条错误规则改写为"服务端是权威,启发式只是旧服务端回落,且对无 scope 的启动 module 行是错的",并写明机制(parse 时默认 vs 原始体)
消融✅ 预测先说:只有两条有区分度的 pin 红(1、3),其余与启发式同向的绿;实测 2 红 13 绿恰如预测;复原 blob = HEAD;无 dist 腿(相对路径导入)——声明正确
真实服务器✅ app-todo:23 行全带 writablecom.example.todo 只读、duplicate 出的无 scope 键 base 可写;HMR console 指向该服务器渲染的是本分支代码
多包验证⏸ 诚实尝试一次,defineStack 在作者态就拒(原文在正文);挂到 objectstack#14439 落地后由 PM 复跑(已记在该卡)。dev 顺带挖出第三道 parse 缝(metadata/plugin.ts:912),已并入 #14439

changeset @object-ui/app-shell: patch ✓;三个文件对受管面零命中(dev 跑过 check-governed-queue-guard --test,CI Governed Surface Queue Guard 亦绿)。

CI(ac9555b

30 项全绿(含 Test 4 分片、Type Check、Lint、Build & E2E、Bundle Analysis、Governed Surface Queue Guard、Changeset 三项)。

收口

ready → squash auto-merge。落地即 Fixes #7177。多包 Studio 复跑与 .objectui-sha 提针由 PM 在 #14439 落地后跟进。


Generated by Claude Code

@hotlong
hotlong added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit ebc05b4Sep 2, 2026
32 checks passed
@hotlong
hotlong deleted the claude/issue-7177-studio-picker-module-packages branch September 2, 2026 08:00
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ADR-0130(objectstack#14122):Studio 包选择器列出 project 域的 module 包

2 participants

@hotlong@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(app-shell): Studio package switcher reads the server's writable verdict, heuristic only as fallback (#7177) - #7331

Merged
hotlong merged 2 commits into
mainfrom
claude/issue-7177-studio-picker-module-packages
Sep 2, 2026
Merged

fix(app-shell): Studio package switcher reads the server's writable verdict, heuristic only as fallback (#7177)#7331
hotlong merged 2 commits into
mainfrom
claude/issue-7177-studio-picker-module-packages

Conversation

@hotlong

Copy link
Copy Markdown
Contributor

Fixes#7177
Part of objectstack-ai/objectstack#14122 — ADR-0130 Consequences row 6, the client half. Server half: objectstack-ai/objectstack#14375 (PR objectstack-ai/objectstack#14430).

Why

parsePackages derived "writable" from manifest.scope alone (scope !== 'project'). That is not the rule the server enforces. isWritablePackage (ADR-0070 D2) reads engine.manifests first — a package booted from an artifact through registerApp is read-only whatever its scope says — and only then the system / cloud scopes. The two rules split on exactly the row ADR-0130 introduces:

rowmanifest.scopein engine.manifestsserverold client heuristic
type: module sub-package of a multi-package artifact (D4/D7 raw body)absentyesread-onlywritable — wrong
Studio-created database baseabsentnowritablewritable

Nothing in the raw row separates those two; only the server's engine.manifests does. A client-side "missing scope means read-only" rule (the first fix proposed on the card, since withdrawn) would have flipped every Studio base read-only. So the verdict moved server-side and this PR consumes it.

Measured on a live server (see Verification): the scope default is applied at PARSE time, while the artifact load path hands the RAW manifest body to registerApp — so the served row has no scope key at all, and the heuristic reads it as a writable database base.

What changed

packages/app-shell/src/views/studio-design/packages-io.ts:

  • parsePackages uses the row's own writable when the server states one (typeof p.writable === 'boolean'), and falls back to the unchanged scope !== 'project' expression when the key is absent (older servers). A non-boolean value is not a verdict and falls back too.
  • The module doc comment is rewritten: it used to state the heuristic as the rule. It now says the server is the authority, the heuristic is only the pre-objectstack-14375 fallback, and it is wrong for the scope-less booted module row — with the mechanism (parse-time default vs raw body) written down.
  • The system / cloud hide filter is untouched, and now says in writing that it is about visibility, not writability.

Changeset: .changeset/7177-studio-switcher-server-writable-verdict.md (@object-ui/app-shell: patch).

Pins

packages/app-shell/src/views/studio-design/packages-io.writableVerdict.test.ts — 5 tests:

  1. writable: false on a scope-less row (the ADR-0130 module) is honoured, and the test asserts the row really carries no scope so the verdict cannot be leaking out of one.
  2. writable: true on a scope-less row (a Studio base) is honoured.
  3. scope: 'project' is overridden in BOTH directions — server false stays false, server true wins over the heuristic.
  4. A non-boolean writable (the string "false") is ignored. Boolean('false') is true, so a coercing read would have agreed with the fallback by accident on the scope-less row; the scope: 'project' row is what tells the two apart.
  5. Kernel packages stay hidden whatever verdict they carry.

Negative pin (same file): a payload with no writable key anywhere produces output deep-equal to the pre-change capture. The expected value was captured by running parsePackages against that payload on the UNTOUCHED tree at ad3d4029abb949cb41815b6ce38d5e0ecad1486a and pasted in, never re-derived.

Reverse verification — ablation, prediction stated first

Subject: packages-io.ts restored to its pre-change bytes from the pinned base commit, with the fix committed first so the restore leg has a real restore point.

  • Mutation proved on disk before the run: removed text count 0, injected text count 1, git hash-object differs from the HEAD blob.
  • Predicted: only the two pins that DISCRIMINATE go red — pin 1 and pin 3 — while pins 2, 4, 5 and the negative stay green, because those agree with the heuristic by construction.
  • Observed: Tests 2 failed | 13 passed (15), failing exactly honours writable:false on a scope-less row and lets the server win over the heuristic in BOTH directions on scope:project.
  • Restore leg: on-disk blob c94002e170b25bc7a2d40500e7adfef3a7328393 equal to the HEAD blob, git diff HEAD empty, 15/15 green again.

No rebuild step applies: the pins import the subject by a relative specifier inside the same package, so nothing resolves through dist.

Verification at dcef834

  • pnpm exec vitest run packages/app-shell/src/views/studio-design/packages-io.writableVerdict.test.ts packages/app-shell/src/views/studio-design/packages-io.test.ts packages/app-shell/src/views/studio-design/packages-io.duplicateEnvelope.test.tsTest Files 3 passed (3), Tests 23 passed (23), exit 0.
  • Whole directory: pnpm exec vitest run packages/app-shell/src/views/studio-design/Test Files 44 passed (44), Tests 236 passed (236), exit 0.
  • pnpm --filter @object-ui/app-shell run type-check — exit 0 (dependency closure built first; the chained tsconfig.test.json project includes src/**/*.test.ts, so the new pins are type-checked).
  • eslint on the two changed files with --no-inline-config — 0 errors, 0 warnings. Narrowing declared: the repo-wide population is eslint .; the file count (2) is read from --format json; eslint.config.js configures no type-aware linting (0 occurrences of projectService / parserOptions / project: / TypeChecked), so this diff cannot move any untouched file's verdict. CI runs the full farm regardless.
  • node scripts/check-changeset-presence.mjs — exit 0, 1 changeset declared for 1 released package.
  • pnpm check:control-bytes, check:vi-mock-specifiers, check:vi-mock-inherit, check:i18n-keys — all green.
  • node scripts/check-governed-queue-guard.mjs --test on the three changed paths — NOT GOVERNED.

Live server verification

Booted from objectstack commit bd0ee2fbb634e7cdcd3c2afffb6258fd8f3a0942 — the head of PR objectstack-ai/objectstack#14430, which is not yet in objectstack main (checked: withWritableVerdict has 0 occurrences in origin/main's packages/runtime/src/domains/packages.ts). The squash content is identical.

Single-package boot — the negative check

examples/app-todo on its own port with a fresh file: DB (the showcase app was tried first and is unusable for this: its GET /api/v1/packages answers HTTP 500 Converting circular structure to JSON … '_ObjectQL' … property 'engine', a pre-existing platform defect the PM is filing, unrelated to this change).

GET /api/v1/packages: 23 rows, all 23 carrying writable. The one row the switcher keeps:

com.example.todo | type=app | scope="project" | writable=false

Identical to what the heuristic said, so the switcher is unchanged for existing single-package apps.

Then POST /api/v1/packages/com.example.todo/duplicate created a Studio base, giving the pair that matters:

com.example.todo | type=app | scope="project" | writable=false
com.example.todo_copy | type=app | scope=ABSENT | writable=true

The Studio switcher, driven through the objectui HMR console pointed at that server (VITE_SERVER_URL / DEV_PROXY_TARGET, so this branch's code is what rendered — the vendored /_console bundle is stale by construction), lists Todo Manager as Read-only and Todo Copy (writable base) as Writable. That second row is the one a client-side "missing scope means read-only" rule would have broken, and it is scope-less on the wire.

Multi-package boot — BLOCKED

BLOCKED-BY objectstack-ai/objectstack#14439. The card's second acceptance line — switcher shows both packages, the module marked read-only, and the three Studio sections filtering by package — cannot be verified end to end yet, because no producer emits a packages[] artifact. One honest attempt, a two-package packages[] config booted through os dev from bd0ee2fb, refused at the producer door:

◆ Compile
────────────────────────────────────────
→ Loading configuration...
✗ defineStack validation failed (2 issues):
✗ packages.0.manifest.objects.0: Expected string but received object.
✗ packages.1.manifest.objects.0: Expected string but received object.
› Error: defineStack validation failed (2 issues):
✗ Compile failed — fix errors above before starting dev server

ArtifactPackageEntrySchema.manifest is the AUTHORING ManifestSchema, whose objects is z.array(z.string()) — glob patterns — so object DEFINITIONS in a sub-package body are refused before compile finishes.

A second, independent door refuses the same shape after compile, recorded here because it is a separate seam and objectstack-ai/objectstack#14439 will meet both: MetadataPlugin._parseAndRegisterArtifact hard-parses the whole artifact with ObjectStackDefinitionSchema before ADR-0130 D4's load path (which deliberately does not judge bodies) is ever reached, so an assembled packages[i].manifest.objects fails with expected string, received object and packages[i].manifest.permissions fails its union. Measured by parsing artifact variants directly: bodies with objects + permissions fail 4 ways; with permissions removed, 2 ways; with both removed, the parse passes. flows, apps and the other collections are simply undeclared on ManifestSchema, so they are stripped from the parsed copy and survive in the raw body — which is why objects (Data pillar) and permissions (Access pillar) are precisely the two the D4 seam cannot carry today.

This verification will be re-run against that card's examples/app-multi-package fixture once it lands. Nothing was worked around in objectui.

Boundaries

No new authorable key, no spec change, no lenient alias. The fallback is version compatibility with servers that predate the field, not tolerance of a second dialect: the field is either a boolean or it is absent.


🤖 Generated with Claude Code

https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m


Generated by Claude Code

…erdict
`GET /api/v1/packages` rows now carry a top-level `writable: boolean` computed
server-side by `isWritablePackage` (ADR-0070 D2, objectstack#14375) — the same
predicate the authoring and lifecycle gates enforce. `parsePackages` reads it
when present and keeps `scope !== 'project'` only as the fallback for servers
that predate the field.
The heuristic is wrong for exactly one row: a `type: module` sub-package of a
multi-package artifact (ADR-0130 D4) is served with no `scope` key, because the
schema default is applied at parse time while the artifact load path hands the
raw manifest body to `registerApp`. The heuristic reads it as a writable
database base while the server refuses every write to it, and nothing in the raw
row separates it from a scope-less Studio-created base that really is writable.
Pins: scope-less rows in both verdict directions, `scope: 'project'` overridden
in both directions, a non-boolean `writable` ignored, kernel packages hidden
whatever verdict they carry, and a payload with no `writable` key anywhere
producing output byte-identical to the pre-change capture.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.3 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-CljwiCD4.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
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.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.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)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.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.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
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.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.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-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
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 (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
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

@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 48 chunks)3160.3 KB3191.4 KB
Main entry chunk (gzip)142.6 KB350 KB
Entry fileindex-CljwiCD4.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)15.33KB5.59KB
app-shell (runtime-config.js)20.68KB7.36KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
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.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.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)514.48KB117.36KB
core (index.js)5.55KB2.23KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)178.20KB49.60KB
fields (index.js)244.25KB61.73KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.98KB10.98KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.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.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)11.71KB4.29KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)6.24KB2.16KB
permissions (discardProofCache.js)1.04KB0.55KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)4.83KB2.27KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)70.02KB19.44KB
plugin-chatbot (index.js)190.53KB45.18KB
plugin-dashboard (index.js)132.63KB34.56KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)250.63KB63.90KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)132.78KB32.58KB
plugin-gantt (index.js)166.77KB40.76KB
plugin-grid (index.js)208.88KB56.59KB
plugin-kanban (index.js)53.21KB14.66KB
plugin-list (index.js)113.51KB27.67KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)29.34KB8.47KB
plugin-tree (index.js)8.98KB3.08KB
plugin-view (index.js)85.90KB21.12KB
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.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)81.07KB26.86KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
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)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.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-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
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 (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
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

@hotlong
hotlong marked this pull request as ready for review September 2, 2026 07:45
@hotlongClaude

Copy link
Copy Markdown
ContributorAuthor

PM 复审:PASS —— head ac9555bdcef834 + merge origin/main

独立性声明

  • 复审会话 session_01UHvF5hyiZjnCyExFnfQB8m(Fable),与派发的 dev agent 同一会话身份、不同 agent 实例;维护者明示授权 PM 自审。复审对象是全 diff(3 文件 +186/−6)与真实服务器验证结果。
  • 为符合本仓「git merge origin/main、不 rebase、不 force」的同步规矩,PR 当时 behind,由我合入 origin/main(merge commit ac9555b)并推送;合并后三个 packages-io 测试文件 23/23 绿。

对树核实

断言实测
服务端优先、缺失回落typeof p.writable === 'boolean' ? p.writable : scope !== 'project';回落表达式一字未改
非布尔不当裁定✅ pin 4 用 'false' 字串在 scope:'project' 行上把"回落"与"强转"区分开(Boolean('false') 为 true 会在无 scope 行上巧合一致)
system/cloud 只管可见性✅ 过滤保持在 writable 读取之前,注释改写为"visibility, not writability"
逐位负向 pin✅ 期望值在未改动的树 ad3d4029 上采集后粘贴,不是重推
文档注释✅ 把"缺 scope = 可写的 DB base"这条错误规则改写为"服务端是权威,启发式只是旧服务端回落,且对无 scope 的启动 module 行是错的",并写明机制(parse 时默认 vs 原始体)
消融✅ 预测先说:只有两条有区分度的 pin 红(1、3),其余与启发式同向的绿;实测 2 红 13 绿恰如预测;复原 blob = HEAD;无 dist 腿(相对路径导入)——声明正确
真实服务器✅ app-todo:23 行全带 writablecom.example.todo 只读、duplicate 出的无 scope 键 base 可写;HMR console 指向该服务器渲染的是本分支代码
多包验证⏸ 诚实尝试一次,defineStack 在作者态就拒(原文在正文);挂到 objectstack#14439 落地后由 PM 复跑(已记在该卡)。dev 顺带挖出第三道 parse 缝(metadata/plugin.ts:912),已并入 #14439

changeset @object-ui/app-shell: patch ✓;三个文件对受管面零命中(dev 跑过 check-governed-queue-guard --test,CI Governed Surface Queue Guard 亦绿)。

CI(ac9555b

30 项全绿(含 Test 4 分片、Type Check、Lint、Build & E2E、Bundle Analysis、Governed Surface Queue Guard、Changeset 三项)。

收口

ready → squash auto-merge。落地即 Fixes #7177。多包 Studio 复跑与 .objectui-sha 提针由 PM 在 #14439 落地后跟进。


Generated by Claude Code

@hotlong
hotlong added this pull request to the merge queueSep 2, 2026
Merged via the queue into main with commit ebc05b4Sep 2, 2026
32 checks passed
@hotlong
hotlong deleted the claude/issue-7177-studio-picker-module-packages branch September 2, 2026 08:00
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ADR-0130(objectstack#14122):Studio 包选择器列出 project 域的 module 包

2 participants

@hotlong@claude