Skip to content

feat(app-shell,console): Studio reaches package-less flows at /studio/~org/automations (objectui#11553) - #11565

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-11553-studio-packageless-flows
Oct 3, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-11553-studio-packageless-flows

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #11553

Clause-②: yes

Implements the ruling triage recorded on objectstack-ai/objectstack#21332 (comment 5946433961): the Studio arm, not the package arm. The clone stays package-less (ADR-0126 §7.1). Nothing changes on the objectstack side. Studio gains a surface for package-less, org-owned flows.

Dev run: session_01FjqrwXPfSMkSfkKYDSRkN2 (mode:subagent of the PM seat), branch claude/issue-11553-studio-packageless-flows, cut from origin/main d0c0c7fe9.

What failed, measured on origin/main before the change

  • Routing: studioRoutes declared only /studio, /studio/:packageId and /studio/:packageId/:tab, so every pillar was keyed by a package.
  • The Automations rail called loadPackageSurfaces(client, 'flow', packageId), which reads list('flow', { packageId }) and listDrafts({ packageId, type }). The registry's package filter keeps only items whose _packageId equals that package, so a flow with no package is in no rail.
  • The home (BuilderLanding) listed packages only. On the stock showcase that means "No writable packages yet" plus the read-only showcase package.
  • The deep link: the rail's list effect did setCurrent((c) => c ?? deepLinked ?? items[0] ?? null). A ?surface=flow:NAME link naming a flow the rail did not hold therefore opened the first flow, and the URL mirror rewrote ?surface= to that flow. That is the "redirects to another flow" from the QA run.

The change

One reserved scope, /studio/~org/automations, served by the same StudioDesignSurface with no package under it. There is no second builder.

  • studioScope.ts (new) holds:
    • the segment ~org;
    • the path builder studioOrgScopePath;
    • the package-less predicates. They read the spec's _packageId on served items (from MetadataProtectionFields, which FlowSchema spreads) and packageId on _drafts headers. The DB-authored sys_metadata pseudo-package also reads as "no package", the same reading studioPackageId in utils/appRoute.ts gives it.
    • ~ is outside the spec's MANIFEST_ID_PATTERN and outside this app's PACKAGE_ID_RE and sanitizePackageId, so the segment cannot equal a package id. A test pins this against both rules.
  • packageSurfaces.ts: new loadPackageLessSurfaces. It does an unscoped list('flow') and listDrafts({ type }), keeps only rows and headers bound to no package, and merges them through the existing published-wins merge.
  • AutomationsPillar: packageId: string | null. With null it lists package-less flows, and drafts save with no package=. The scope has no "New" flow (authoring stays package-first).
  • Deep links: a ?surface=flow: link that names a flow the rail does not hold no longer opens the first flow. From a package's pillar, a named package-less flow is found and opened in /studio/~org/automations?surface=flow:NAME, once per mount with replace navigation. A flow found nowhere is reported on the canvas ("The link names flow “NAME”, which is not here.").
  • StudioDesignSurface, in the package-less scope:
    • Writable by derivation. readOnly is packageId !== null && pkgWritable === false, so a read-only answer left over from the package shown before the switch cannot lock the scope. The server write gate stays the authority.
    • Offers its one pillar (another pillar's URL lands on it), no Create app, and no package copilot.
    • The header count and the pending-changes sheet cover package-less flow drafts only.
    • Publish promotes each of those drafts by reference through publishDraft. POST /packages/:id/publish-drafts cannot reach a draft bound to no package; this is the same door Home's publish-all uses for package-less drafts. It is not all-or-nothing, and failures are named per draft.
  • PackageSwitcher and BuilderLanding each get an entry, "Organization flows". On the home it sits under its own "Not in a package" heading, shown whether or not any writable package exists.
  • DraftChangesPanel gets an optional include predicate. The scope passes its own filter, so the sheet reviews exactly what that scope's Publish ships. Package hosts pass nothing, and their behaviour is unchanged.
  • The console's studioRoutes get the bare /studio/~org leg, which lands on automations. The pillar URL itself is served by the generic :packageId/:tab route.
  • i18n: seven new keys in metadata-admin/i18n.ts (en and zh), added beside engine.studio.landing.installedHeading. No key is moved or reordered.
  • Docs: packages/app-shell/README.md and content/docs/guide/console.md. Changeset: .changeset/11553-studio-package-less-flows.md (@object-ui/app-shell minor, @object-ui/console patch).

Landing versus the claim's file surface. Everything sits in the claimed surface except three files in the same package, each needed by it:

  • packages/app-shell/src/preview/DraftChangesPanel.tsx: the include predicate, so the sheet does not review drafts the scope's Publish never ships;
  • packages/app-shell/src/index.ts: exports the segment, pillar and path builder for the console route;
  • the two docs files above, which AGENTS.md commandment Add automated testing infrastructure and CI/CD workflows #2 requires.

The ruling's pins

Pin Where
A package-less flow is listed and opens editable StudioDesignSurface.packageLessFlows-11553.test.tsx: "lists the clone and none of the packaged flows"; "opens it editable, and an edit saves a draft bound to no package"
A packaged flow on a read-only package stays read-only the same file, "lists the package’s flows, not the clone, and offers no edit"; objectui#11124's StudioDesignSurface.automationsReadOnly-11124.test.tsx stays green, unchanged
The deep link to a package-less flow opens that flow, not another "in the package-less scope: the named flow, though another is listed first"; "the measured link, through the showcase pillar: lands on the package-less scope, the clone open and editable"

Further pins: studioScope.test.ts, packageSurfaces.test.ts (loadPackageLessSurfaces), BuilderLanding.orgScope-11553.test.tsx (the entry on the measured shape, with no writable package) and StudioRoute.test.tsx (the ~org leg, behind the same entry gate).

Verification (final head 4831c5a)

  • Build: pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build exited 0 (29 packages). The plugin dists the console needs were built for its type-check.
  • pnpm --filter @object-ui/app-shell type-check: exit 0. This covers both tsc --noEmit and tsconfig.test.json.
  • pnpm --filter @object-ui/console type-check: exit 0, after rebuilding app-shell.
  • pnpm exec vitest run packages/app-shell/src/views/studio-design/: 88 files, 521 tests passed.
  • pnpm exec vitest run packages/app-shell/src/preview/ apps/console/src/components/: 50 files, 562 tests passed.
  • eslint on the 13 changed TS/TSX files: exit 0, 0 errors. Warnings per file equal the base d0c0c7fe9 count for each pre-existing file (StudioDesignSurface 17, DraftChangesPanel 2, StudioRoute 1, BuilderLanding 0).
  • Repo checks, all exit 0:
    • check:i18n-keys, check:i18n-drift, check:i18n-dead-keys, check:i18n-designer-parity
    • check:control-bytes, check:new-line-citations, check:changeset-claims, check:pending-changeset-literals
    • check:spec-symbols, check:readme-exports, check:unreferenced-sources, check:test-path-roots
    • check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape
    • check:metadata-write-doors, check:handler-key-reads, check:self-import, check:phantom-deps, check:esm-specifiers
    • check:doc-fences, check:doc-types
    • check-changeset-presence, check-changeset-no-major, check-changeset-overwrite, check-changeset-fixed, check-doc-links

Ablations. Each mutation was made on committed code through ablation-replace.mjs in WRAP mode. Each anchor hit once, the blob changed, and the restore was proven with blob equal to HEAD and an empty git diff HEAD. The tests import ./StudioDesignSurface from source, so no build was needed.

Ablation Result
A1: the deep-link fallback put back to ?? items[0] 1 red: "a link naming a flow no rail holds opens no other flow"
A2: the package-less redirect removed 1 red: "the measured link, through the showcase pillar"
A3: the package-less loader swapped for the package loader 9 red
A4: the package-less publish arm disabled 1 red: "publishes each package-less flow draft by reference"
A5: the header count unfiltered 1 red: "counts only package-less flow drafts"
A6: the sheet's include dropped 1 red
A7: readOnly put back to pkgWritable === false (at 4831c5a) 2 red, including "from that read-only package, the switcher’s entry opens the package-less scope editable"

Lint was narrowed to the changed files. The repo-wide pnpm lint and the full test shards are left to CI. No browser check was run.

Acceptance notes

  • Not this card: objectui#11546 (a read-only canvas opens no node inspector) remains open; this change does not touch it.
  • The Setup page's clone result does not link to the copy in Studio. Setup › Packaged automation says "Edit the copy in Studio" after a clone. The copy is now reachable from Studio's home and switcher, and studioOrgScopePath({ type: 'flow', name }) could produce a direct link, but none is added here (that file is outside this card). Noted, not filed; carrier: none.
  • The same deep-link fallback lives in two sibling rails. The Interfaces rail (cur ?? deepLinked ?? firstLeaf) and the Data rail (c ?? deepLinked ?? items[0]) still open their first item when a ?surface= link names one they do not hold. That is the same mechanism this card removed for flows, but it was not measured through a public entry here. Noted, not filed; carrier: none.
  • A host that mounts StudioDesignSurface with its own aiSlot (the cloud edition) and reads :packageId itself would see ~org as a package id in this scope. Not measured: there is no such host in this repository.

Generated by Claude Code

claude added 6 commits October 3, 2026 15:32
…io/~org/automations (objectui#11553)

A clone of a packaged flow is correctly package-less (ADR-0126 §7.1), and every
Studio route and rail was keyed by a package, so the clone reached no Studio
surface. Studio gains one reserved scope, served by the same
StudioDesignSurface with no package under it:

- studioScope.ts: the reserved `~org` segment (outside every package-id
  alphabet), the path builder, and the package-less predicates read off the
  spec's `_packageId` (served items) and the draft header's `packageId`.
- packageSurfaces.ts: loadPackageLessSurfaces, the unscoped list and drafts
  narrowed to items bound to no package, through the shared merge.
- AutomationsPillar: `packageId: null` lists package-less flows and saves
  package-less drafts; no "New" there. A deep link naming a flow the rail
  does not hold no longer opens the first flow instead; from a package's
  pillar a package-less flow is found and opened in the package-less scope.
- StudioDesignSurface: the package-less scope is writable, offers only the
  Automations pillar, no Create app and no package copilot; its header counts
  and its Publish ships package-less flow drafts by reference.
- PackageSwitcher and BuilderLanding: an entry for the scope, shown whether or
  not a writable package exists.
- DraftChangesPanel: an `include` predicate so the scope reviews what it ships.
- console studioRoutes: the bare `/studio/~org` leg lands on its pillar.

Claude-Session: https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2
Co-authored-by: Claude <noreply@anthropic.com>
- studioScope: the reserved segment fails the spec's and this app's
  package-id rules; the package-less predicates and the path round-trip.
- loadPackageLessSurfaces: unscoped reads narrowed to items and drafts bound
  to no package.
- StudioDesignSurface on the real route shape: the clone is listed and opens
  editable, its edit saves a package-less draft, the deep link opens the
  named flow (also through a package's pillar), a link naming a flow nobody
  holds opens no other, the read-only package stays read-only, and the
  scope's header counts, reviews and publishes only package-less flow drafts.

Claude-Session: https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2
Co-authored-by: Claude <noreply@anthropic.com>
… package-less scope (objectui#11553)

Claude-Session: https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2
Co-authored-by: Claude <noreply@anthropic.com>
… (objectui#11553)

Claude-Session: https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2
Co-authored-by: Claude <noreply@anthropic.com>
… by a state write (objectui#11553)

A read-only answer from the package the surface showed before stays in state across the switch; the scope now reads as writable whatever it holds. Pinned by the switcher path from a read-only package.

Claude-Session: https://claude.ai/code/session_01FjqrwXPfSMkSfkKYDSRkN2
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added documentation Improvements or additions to documentation apps tests package: app-shell labels Oct 3, 2026
@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

changeset-claim-re-read

⚠️ 7 pending changeset(s) describe a file this change touches

Their bodies publish verbatim into the CHANGELOG at the next release, so this is a request to re-read them against your diff — addressed here because you are the one seat that can answer it without re-deriving anything.

⛔ Nothing here blocks, and nothing here is a verdict on your change. This gate exits 0, is not a required context, and judges name resolution, never meaning: it asked whether a pending body names a file you touched. "Is this sentence still true?" is the one question it will not answer, and the one you are being asked to answer.

.changeset/6238-object-payload-enabled.md

  • names packages/app-shell/src/index.ts → packages/app-shell/src/index.ts — edited by this change

    No published type changed. ObjectMetadataPayload is exported from its module but that module is not re-exported by packages/app-shell/src/index.ts, the package's only entry, so the removed enabled?: boolean was never on the published surface and no **/src/index.ts is touched. What consumers can observe is behaviour: MetadataService is reachable through the published useMetadataService() hook, both method signatures are unchanged (PromiseANGLE-BRACKETS(void)), and the HTTP request they issue changes from a PUT with a body to a DELETE.

.changeset/6310-designer-formula-key-retired.md

  • names packages/app-shell/src/views/metadata-admin/i18n.ts → packages/app-shell/src/views/metadata-admin/i18n.ts — edited by this change

    Not touched: designer.field.formula ('Formula (CEL)') in packages/app-shell/src/views/metadata-admin/i18n.ts, a different and live key belonging to metadata-admin's ObjectFieldInspector — the surface that still authors formula expressions.

.changeset/6559-expression-user-input-contract.md

  • names packages/app-shell/src/index.ts → packages/app-shell/src/index.ts — edited by this change

    WHAT BREAKS, AND FOR WHOM. This tightens a signature published from the package entry (packages/app-shell/src/index.ts), so an external caller that passes an unchecked or under-declared value stops compiling on upgrade. That is accepted (maintainer ruling 2026-08-27, option A): the only calls it refuses are calls that were never conformant with the contract the module already declared. It ships as minor, not major — objectui's major tracks @objectstack's, so its own breaking changes ship as a minor with the break written down (scripts/check-changeset-no-major.mjs). ⛔ Keeping unknown and ⛔ adding a second, wider entry point were both declined.

.changeset/6681-declared-lazy-marketplace-routes.md

  • names views/studio-design/StudioDesignSurface.tsx → packages/app-shell/src/views/studio-design/StudioDesignSurface.tsx — edited by this change

    Both marketplace chunks were held by CHUNK CO-TENANCY, not by any import of the page: rolldown had parked components/SuggestedBindingsPanel.tsx (statically imported by the eager views/studio-design/StudioDesignSurface.tsx) in the first, and console/marketplace/InstalledListWidget.tsx (bare-imported by the package barrel for its SDUI registration) in the second. An app-shell-eager-leaves group in apps/console/vite.config.ts isolates those co-tenants so the three declared-lazy pages chunk by their own dynamic-only reachability. The console eager closure moves 3180.2 KB → 3171.5 KB gzipped (−8,888 bytes, 48 → 45 eager chunks) with the three per-chunk ceilings unmoved.

.changeset/7125-dashboard-empty-state-keys-retired.md

  • names packages/app-shell/src/views/metadata-admin/i18n.ts → packages/app-shell/src/views/metadata-admin/i18n.ts — edited by this change

    Not touched: table.noRows ('No rows to display') and engine.form.noRows (packages/app-shell/src/views/metadata-admin/i18n.ts, read at widgets.tsx) — two different, same-named keys in different namespaces. Nor the comments in WidgetEmptyState.tsx, DatasetWidget.tsx, ObjectDataTable.tsx and PivotTable.tsx that record WHY three widgets with three strings became one shared empty state; the packs' own comment keeps that rationale and now names the retirement instead of a row that is gone.

.changeset/7234-requiredpermissions-author-channel.md

  • names content/docs/guide/console.md → content/docs/guide/console.md — edited by this change

    content/docs/guide/console.md states the hide and where the reason is shown.

.changeset/8632-malformed-picklist-option-loud.md

  • names views/metadata-admin/i18n.ts → packages/app-shell/src/views/metadata-admin/i18n.ts — edited by this change

    Two new strings land in the designer's own en / zh tables — the metadata-admin console owns its strings in views/metadata-admin/i18n.ts and is deliberately outside the ten locale packs (packages/i18n/README.md, "Scope — the engine.* carve-out").

Read the paragraph, not the line: both false halves of the objectui#8617 claim sat in one paragraph, and correcting either alone would have left it asserting the same wrong thing.

If a claim did go false, correct the body. That is precedented and prose-only, frontmatter untouched; check-changeset-overwrite.mjs will report the correction as its own case 2 ("correcting a declaration on purpose … legitimate"), which is the intended shape — one gate asks for the read, the other records the write.

Not covered, stated so nobody reads this as more: a born-false claim that spells no line address at all (objectui#9495 coordinated one by ORDINAL — "a grep finds that member first" — and deciding that means reading what the sentence means), a claim spelled as a symbol or a package rather than a backticked file name, and a file named ambiguously.

Angle-bracketed names in the quoted prose above are rewritten as ANGLE-BRACKETS(name): GitHub deletes tag-shaped fragments from a stored body, and a quote that silently loses the identifier it is about is worse than a visible repair.

Compared the checked-out tree with 072b7e843 (merge-base with origin/main): 15 file(s) changed outside .changeset/, read against 2087 pending declaration(s) that publish a body (2722 pending in total). · run

@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3310.5 KB 3330.4 KB
Main entry chunk (gzip) 151.1 KB 350 KB
Entry file index-koInTRbB.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 17.22KB 6.37KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.17KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 572.97KB 137.34KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 232.57KB 64.51KB
fields (index.js) 261.62KB 66.20KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 35.66KB 9.49KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 39.47KB 11.25KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 53.17KB 15.46KB
plugin-charts (index.js) 83.60KB 22.89KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 142.26KB 38.44KB
plugin-designer (index.js) 231.28KB 48.76KB
plugin-detail (index.js) 245.59KB 64.56KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 175.57KB 45.31KB
plugin-gantt (index.js) 179.16KB 45.06KB
plugin-grid (index.js) 235.03KB 64.66KB
plugin-kanban (index.js) 49.59KB 15.58KB
plugin-list (index.js) 116.46KB 28.93KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 38.80KB 11.71KB
plugin-tree (index.js) 14.51KB 5.15KB
plugin-view (index.js) 90.23KB 22.73KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.06KB 2.68KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 21.42KB 7.05KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.16KB 1.96KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.07KB 2.39KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.25KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4831c5aef2e1087e12a3a3f3e682a8b82e0583c5
Local-runs: none

Written 2026-10-03T16:45Z by a subagent of the domain:ui seat, session_01FjqrwXPfSMkSfkKYDSRkN2, which adopts this verdict. Inputs: card objectui#11553 (body and all three comments), triage ruling 5946433961 on objectstack-ai/objectstack#21332, PR #11565 (body, its 16-file list, both bot comments), the net diff from merge-base d0c0c7fe9 to the head (16 files, +1109 / -42, equal to the PR's file list), the head's check-runs, and both repos' trees read by ref. Nothing was built, run or re-run.

Check-runs on the head, read 2026-10-03T16:43Z: 43 check-runs, 40 success, 3 skipped (dependabot and the two coverage legs), 0 failure, 0 in progress. Every Test shard (1 to 8), Type Check, Spec Main Shape Gate, Lint, Build & E2E, Test (dist pins), Changeset Declaration / Bump Policy / Fixed Group / Overwrite Report, Changeset Claim Re-read, README Export Check and Governed Surface Queue Guard are success. An earlier reading of the same head (2026-10-03T16:36Z) had nine of these still in_progress; none turned red.

① Derived judgments

Accept set and public surface, each named:

  • @object-ui/app-shell entry gains three exports, STUDIO_ORG_SCOPE_SEGMENT (~org), STUDIO_ORG_SCOPE_PILLAR (automations) and studioOrgScopePath() — right: a host that declares the /studio routes needs the segment, and nothing else from studioScope.ts (the predicates, isStudioOrgScope) leaves the package. DraftChangesPanel, AutomationsPillar and PackageSurfaceClient are not on the package entry, so their widened shapes (packageId: string | null, the optional include, listDrafts headers carrying packageId) are internal — right that the changeset does not declare them.
  • Console accept set: /studio/~org (its own static leg, landing on automations) and /studio/~org/automations through the generic :packageId/:tab leg — right. ~ is outside the spec's MANIFEST_ID_PATTERN (lowercase letters, digits, hyphens, dots) and this app's PACKAGE_ID_RE / sanitizePackageId, pinned in studioScope.test.ts, so the segment can never equal a package id. The leg sits behind the same StudioRoute entry gate (pinned: an ungated visitor lands on /home). The only other :packageId reader in the tree, MarketplacePackagePage, is on a different route family.
  • The package-less rail (loadPackageLessSurfaces): an unscoped list('flow') narrowed by isPackageLessItem on the served _packageId, unioned with an unscoped listDrafts({ type }) narrowed by isPackageLessDraft on the header's packageId, through the shared published-wins merge — right. On the server the package-scoped list keeps exactly the rows whose _packageId equals the asked package, listItems tags each served row from its stored package_id, and a package-less body standing in for a code package's row is re-tagged with that package, so a packaged flow (a read-only package's included) cannot enter this rail. Reading the sys_metadata pseudo-package as "no package" matches the existing studioPackageId bridge and the protocol's own resolveWritePackageScope.
  • The readOnly rule, readOnly = packageId !== null && pkgWritable === false — right, and no read-only package path became editable: for every string packageId the expression is the previous pkgWritable === false unchanged; the fetchPackages effect early-returns only for null, so a package route still resets and re-fetches its answer; objectui#11124's StudioDesignSurface.automationsReadOnly-11124.test.tsx is untouched by the diff, and the new file carries the read-only control ("lists the package's flows, not the clone, and offers no edit") beside the switch-to-scope pin. The scope's edits save with no package=; the server's draft-save door inherits a packaged active row's binding and assertAllowed gates the write, so even a package-less save against a packaged name is re-bound and gated server-side — the PR's "the server write gate stays the authority" holds.
  • The package-less Publish (the packageId === null arm of doPublish): re-reads the env-wide _drafts feed, keeps isOrgScopeDraft (stored type folded by canonicalMetaUrlType equals flow, AND the header is bound to no package), promotes each by publishDraft('flow', name) and never calls publishPackageDrafts — pinned ("publishes each package-less flow draft by reference, and nothing through the package batch door"; the fixture holds a package-less object draft and a packaged flow draft, and only the clone is published). Right as to what it selects: it cannot select a packaged draft or a non-flow draft. One caveat, carried to ③ and not a defect of this diff: the per-item door sends no package=, the REST route spreads packageId into the request only when the query names one, and promoteDraft with the key ABSENT resolves "match any package". So for a name holding BOTH a package-less draft and a same-name packaged draft, the server could promote the packaged row. That is the door Home's usePublishAllDrafts already uses for its orphans, the REST door has no spelling for the unbound row, and this scope's own writes cannot produce the double-draft (its rail lists no packaged flow and offers no "New").
  • Header count and the sheet: pendingCount stays null while the feed is unknown, else the env-wide entries filtered by isOrgScopeDraft; DraftChangesPanel receives include={isOrgScopeDraft} only when packageId === null (control pinned: a package's sheet receives undefined). The predicate is applied after listPendingDrafts folds the type and before classification, and load names it as a dependency — right; package hosts are unchanged.
  • Deep link: setCurrent((c) => c ?? deepLinked ?? (named ? null : items[0]) ?? null) — a ?surface=flow:NAME naming a flow the rail does not hold no longer opens items[0]; from a package pillar a once-per-mount probe of the package-less list redirects (replace) to studioOrgScopePath({ type: 'flow', name }); in the ~org scope no probe runs, so there is no redirect loop; a flow found nowhere renders engine.studio.auto.deepLinkMissing — right, and it is the measured "redirects to another flow" mechanism. useSurfaceDeepLink's URL mirror writes only when a current surface exists, so a missing name is not rewritten into another flow.
  • Scope chrome: one pillar (another pillar's URL Navigates to the scope path), no Create app, no package copilot (StudioChatDock gated on packageId !== null), no "New" flow, the PackageSwitcher names the scope and renders its manage button only for a resolved package (openManage(current.id)) — right. The home entry under "Not in a package" renders unconditionally (pinned on the measured shape: no writable package, one read-only package).
  • i18n: seven keys added in en and zh with matching placeholders (parity gate green); the README and content/docs/guide/console.md rows state the behaviour the diff ships; README Export Check is green.

The ruling's three pins are each carried by a named test in StudioDesignSurface.packageLessFlows-11553.test.tsx, as the PR's table says. The clone is not moved into a package and no objectstack file is touched; ADR-0126 §7.1 is respected.

② Semver level

  • Changeset 11553-studio-package-less-flows.md: @object-ui/app-shell: minor, @object-ui/console: patch — matches what the diff publishes: three new entry exports and a new scope in app-shell (minor); a route leg in the console app, whose package is @object-ui/console (patch). Every bullet in the body matches a hunk; no major; Changeset Declaration, Bump Policy, Fixed Group and Overwrite Report are green.
  • Clause-②: yes on the PR body, first thing on its line — right, and the seat's correction from no holds: the diff widens a published package's public surface (three exports on the entry) and the console's accept set (/studio/~org), with no narrowing arm; yes takes at least minor, which app-shell declares.
  • The changeset-claim re-read's seven paragraphs, judged by meaning against the head tree: none went false. 6238 (ObjectMetadataPayload is still not re-exported from the entry), 6310 (designer.field.formula is still in both tables), 6559 (an entry-signature tightening this diff does not touch), 6681 (SuggestedBindingsPanel is still statically imported by the eager StudioDesignSurface.tsx, and app-shell-eager-leaves is still in the console vite config; its KB figures are that changeset's own measurement, not a claim about this head), 7125 (engine.form.noRows is still present; table.noRows lives in packages/i18n), 7234 (the capability-gate paragraph in console.md is untouched; this diff adds one table row above it), 8632 (its two strings are untouched; this diff adds seven keys beside them). No body needs correcting.

③ Boundary flags

The dev report's open_questions is empty. Its flags and deviations, each answered or escalated:

  1. Setup › Packaged automation's clone result says "Edit the copy in Studio" and links nowhere; studioOrgScopePath can now build that link. Out of this card (the ruling's Done-when is Studio reach plus the pin bump). Escalated to the seat as a follow-up card candidate on PackagedAutomationPage.tsx.
  2. The same first-item deep-link fallback remains in the Interfaces rail and the Data rail; not measured at a public door, and the ruling names flows. Escalated as a candidate card for a measurement; not a defect of this diff.
  3. A cloud host mounting StudioDesignSurface with its own aiSlot and reading :packageId itself would see ~org; no such host is in this repo, and the three exports are what such a host would read. Answered: accepted, nothing to do here.
  4. The publish-by-reference door and the double-draft corner named in ①: the REST per-item publish door has no spelling for the unbound row, so no client can pin the package-less draft. Escalated as an objectstack-side candidate (a package spelling for the unbound row, or the door resolving the unbound row first); shared with Home's publish-all today; not a FAIL of this diff.
  5. File surface extended within app-shell (DraftChangesPanel.tsx, index.ts, the README and the console guide) — accepted by the seat's claim amendment; each is needed by the scope. Answered.
  6. The route shape was chosen, not escalated — the ruling allows "a rail, or an entry on Studio's home"; one reserved segment on the same surface satisfies it and is collision-proof by pin. Answered.
  7. The extra package builds for gate preconditions, the write-pace pause and the worktree removal are process notes with no contract effect. Answered.

Implemented-by: claude/issue-11553-studio-packageless-flows
Reviewed-by: session_01FjqrwXPfSMkSfkKYDSRkN2

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

needs:contract-review removed: an at-tier PASS is on record for this head · domain:ui seat 1 · session_01FjqrwXPfSMkSfkKYDSRkN2 · 2026-10-03T16:47Z.

  • Record: 5971232861 on this PR. ## Contract review, Served-tier: CONTRACT_REVIEW_TIER, Head-sha: 4831c5aef2e1087e12a3a3f3e682a8b82e0583c5, Local-runs: none, Implemented-by: claude/issue-11553-studio-packageless-flows / Reviewed-by: session_01FjqrwXPfSMkSfkKYDSRkN2, PASS. The reviewer's transcript shows that one post and its read-back, and no other write.
  • The PR head at this act is 4831c5aef, the head the record judged.
  • Pre-landing checks:
    • every check on the head is success or an expected skip (40 / 3: the two coverage rows and dependabot);
    • check-governed-merges --pr objectstack-ai/objectui#11565: NOT governed, 0 of 16 paths on the register, 1151 changed lines ≤ 5000.
  • Landing in this act: ready, then auto-merge, so it enters the merge queue.
  • The record's escalated items (③ 1, 2, 4), each disposed under the filing gate:
    • ③1, the Setup › Packaged automation clone result ("Edit the copy in Studio" without a link): the instruction is now true, since the copy is reachable from Studio's home under "Not in a package". A link is an enhancement with no measured failure ⇒ Acceptance notes, not filed.
    • ③2, the first-item ?surface= fallback in the Interfaces and Data rails: unmeasured at a public door ⇒ Acceptance notes, not filed.
    • ③4, the per-item publish door with no spelling for the unbound draft row (a name holding both a package-less and a same-name packaged draft could promote the packaged one): no producer of that double-draft is measured. This scope's own writes cannot make it, and Home's publish-all already uses the same door ⇒ Acceptance notes, not filed. A measured producer reopens it as an objectstack card.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 3, 2026 16:49
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 3, 2026 16:49
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 3, 2026
Merged via the queue into main with commit f624f27 Oct 3, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11553-studio-packageless-flows branch October 3, 2026 17:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

apps documentation Improvements or additions to documentation package: app-shell tests

Projects

None yet

2 participants