Skip to content

refactor(app-shell): stop declaring 28 symbols under names the spec owns - #3169

Merged
os-zhuang merged 1 commit into
mainfrom
claude/objectui-spec-symbol-ci-rule-4r3xlc-batch3
Aug 1, 2026
Merged

refactor(app-shell): stop declaring 28 symbols under names the spec owns#3169
os-zhuang merged 1 commit into
mainfrom
claude/objectui-spec-symbol-ci-rule-4r3xlc-batch3

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes objectui#3157 (ledger burn-down batch 3/8). Refs objectstack#4115.

@object-ui/app-shell was the ledger's largest single package: 28 symbols declared under names @objectstack/spec already exports. All 28 are dispositioned here — 20 derived, 8 renamed — and the package drops out of the ledger entirely.

The comment justifying the largest cluster (external/api.ts) — "kept local so app-shell does not take a build dependency on the framework spec package" — was already false. @objectstack/spec is a direct dependency in this package's own package.json. That is the objectstack#4115 failure mode in miniature: a stale reason, still authoritative-sounding, holding a copy in place long after the reason expired.

Three live defects the copies were hiding

Not "one spec release away from drifting" — already drifted, already user-visible:

  1. SchemaDiffEntryKind was missing index_mismatch and unmapped_index (framework#3728). ValidationPanel labels diff rows from a Record<SchemaDiffEntryKind, string> — a total map. The local union had 6 of the server's 8 kinds, so an index divergence the validate route already emits arrived as a row this UI could not name. Importing the real union made the compiler demand the two missing labels; they are added in this PR.
  2. ExplainLayer.contributors[].state ('active' | 'expired') did not exist locally. An expired position or permission-set contribution rendered identically to a live one in the access-explain panel.
  3. Optional-vs-required skew on ExternalColumn.primaryKey, ExplainRecordAttribution.rules, ExplainDecision.principal.positions / .permissionSets — optional here, required in the spec, so every reader carried a nullish branch that can never fire.

Disposition of all 28

Derived — plain re-export (18)

symbol from note
RemoteTable, GenerateDraftOpts, ObjectDraft, SchemaValidationResult spec/contracts first three were byte-identical
SchemaDiffEntry, SchemaDiffEntryKind spec/shared defect 1
ExternalCatalog, ExternalColumn, ExternalTable spec/data defect 3 (primaryKey)
ExplainDecision, ExplainLayer, ExplainMatchedRule, ExplainRecordAttribution spec/security defects 2 + 3
ScreenFieldSpec spec/contracts byte-identical, incl. visibleWhen
PluginPermissions spec/kernel member-for-member identical
FilterCondition spec/data was Record<string, any> — a local declaration under the spec's name carrying none of its structure
isAggregatedViewContainer spec verbatim copy of the spec's implementation

FieldGroupObjectFieldGroup is rename and derive, and is the most interesting one. @objectstack/spec/studio exports a FieldGroup — the Studio field-editor's group config (order, no collapse). The object designer's type is key-for-key the spec's ObjectFieldGroup (spec/data), a different export. The local doc comment claimed description and collapse were "spec-defined" — true of ObjectFieldGroup, false of the FieldGroup the name actually resolved to. A correct sentence filed under a name pointing somewhere else.

It derives from z.input<typeof ObjectFieldGroupSchema>, not the exported z.infer type — the zod trap the guard's header warns about. collapse carries .default('none'), so it is optional to author and required after parsing. This designer authors (addGroup emits {key, label}), so the output type would make the editor's own new-group shape unrepresentable. tsc caught this: the z.infer version failed at addGroup.

Derived — structural, one documented divergence each (2)

  • ScreenSpec = Omit<SpecScreenSpec, 'fields'> & { fields?: ScreenFieldSpec[] }. An object-form step, or a message-only screen from a third-party node executor, legitimately sends no fields; an absent array used to throw the moment the dialog opened (#3528).
  • DecisionOutputDef extends the spec's, adding required — enforced by the server's decide(), not yet modelled by the spec. Deriving also narrowed type from a bare string to the spec's closed enum, so a typo'd picker kind now fails to compile instead of degrading to a raw record-id text box, which is exactly the objectui#2955 failure this module exists to prevent.

Both pinned so the divergence collapses to a plain re-export the day the spec catches up.

Renamed — genuine collisions (8)

Breaking for importers of @object-ui/app-shell.

was now what the spec's same-named export actually is
FieldInput ScreenFieldInput Omit<Partial<Field>, 'type'> — an object FIELD's authoring shape (local was a React component)
ConversationSummary ConversationListItem the AI context-compaction record (keyPoints, tokensSaved, messageRange). Zero keys in common
RuntimeConfig AppShellRuntimeConfig the engine runtime config (engine, engineConfig, resourceLimits). Zero keys in common
PageHeaderProps PageHeaderComponentProps the authored SDUI header zod schema (strings, action ids) vs. React props (ReactNode, elements)
FlowNode / FlowEdge FlowDesignerNode / FlowDesignerEdge a complete authored node/edge
PackageManifest PackageManifestRow the full authored manifest (~40 keys)
InstalledPackage InstalledPackageRow the full install record (installedAt, upgradeHistory, …)

Every new name was run through the guard before being adopted — the objectui#3074 lesson. That check paid off immediately: ObjectFieldGroup, FlowCanvasNode and FlowCanvasEdge were all candidates, and all three are already spec exports. FlowCanvas* is especially treacherous: despite the name it is the pure visual overlay ({nodeId, x, y, collapsed, …}), not the node. The parity test pins those two as taken so a future rename cannot walk into them.

Why Flow* was renamed rather than derived

Deriving was tried first and tsc rejected it, correctly. A canvas holds nodes the user has dropped but not finished — no label yet, no edge id until committed — so the spec's complete-node type makes the editor's own intermediate state unrepresentable. This is the two-layer split objectui#3090 named, not drift.

Two genuine findings there are recorded, not silently changed, because both alter what gets written to metadata and that is a behaviour change with no place in a symbol burn-down:

  • The designer persists geometry as ui: {x, y} while the spec models the same thing twice alreadyFlowNode.position (spec/automation) and FlowCanvasNode (spec/studio). Three spellings of one concept.
  • The edge inspector can build a condition object missing ADR-0089's required dialect discriminant — a shape the server's own FlowEdgeSchema would reject.

PackageManifestRow / InstalledPackageRow are likewise deliberate: lenient read projections over wire payloads from runtimes of different vintages, which must stay assignable-from anything the server sends — the opposite of what the spec type is for.

Ledger

115 → 87 untriaged collisions; 19 → 18 packages. --ledger regenerated and diffed line by line: removed is exactly the 28 app-shell symbols, added is empty, every other package's entries byte-identical (parallel batches are running).

Verification

  • check-spec-symbol-derivation.mjs ✅ green
  • Full-repo turbo type-check --force78/78 — after turbo build --filter=@object-ui/app-shell, since per-package tsc resolves to stale dist and false-greens (objectstack#4115 lesson). Covers apps/console, which is a consumer.
  • Full test suite ✅ 9326 passed / 0 failed (798 files), incl. 25 new assertions
  • ESLint on all changed files: 0 errors

Mutation tests — all three fire

  1. Re-fork isAggregatedViewContainer → guard reports by name and by file.
  2. Revert FlowDesignerNodeFlowNode → guard reports it.
  3. Leave a burned name in DEBTrot ratchet reports it.

Plus the one that matters most here: the new parity test checks isAggregatedViewContainer by reference identity, and mutation 1 failed it. The copy it replaced was line-for-line identical to the spec's implementation, so no value or behavioural comparison could ever have caught it — objectui#3003's argument, now with a live instance.

The tripwire reads the spec's export names through the TypeScript checker, not import * as. A runtime namespace import sees values only, and almost every symbol here is a type; a namespace-based tripwire would have passed for all of them while proving nothing. It also asserts in both directions — a rename fails if the spec ever claims the new name, and also if the spec ever drops the old one, so the workaround cannot outlive its reason.

Cross-package decisions (for sibling batches)

  • FilterCondition — app-shell's was not the FilterBuilder row, unlike @object-ui/types and @object-ui/components. That concept already lives here under its own name, BuilderCondition. app-shell's was the spec's ObjectQL AST, loosely typed, and is now re-exported. Do not fold it into the FilterBuilderCondition rename (batch 5).
  • PageHeaderPropsPageHeaderComponentProps@object-ui/layout carries the same collision (batch 7, objectui#3161). Suggest adopting this name so the two packages do not invent two dialects.

Open questions for a maintainer

  1. The two Flow* findings above (three spellings of node geometry; condition missing dialect) — worth their own issue?
  2. spec/automation's DecisionOutputDef has no required, though the server enforces it. Should the spec adopt it?

Generated by Claude Code

Batch 3 of the objectstack#4115 debt burn-down (objectui#3157), and the
ledger's largest single package. Twenty-eight app-shell symbols were declared
under names `@objectstack/spec` already exports. Twenty are now imported or
derived from the spec; eight are renamed because they model something the
spec's same-named export does not.

The comment justifying the largest cluster -- "kept local so app-shell does
not take a build dependency on the framework spec package" -- was already
false: `@objectstack/spec` is a direct dependency of this package.

Three live defects the copies were hiding, each fixed by importing the type:

  * `SchemaDiffEntryKind` was missing `index_mismatch` / `unmapped_index`
    (framework#3728). ValidationPanel labels diffs from a total map, so an
    index divergence the server already emits arrived unlabelled. Widening
    the union made the compiler demand the two missing labels.
  * `ExplainLayer.contributors[].state` ('active' | 'expired') was absent, so
    an EXPIRED position / permission-set contribution rendered as a live one.
  * Several keys were optional locally and required in the spec
    (`ExternalColumn.primaryKey`, `ExplainRecordAttribution.rules`,
    `ExplainDecision.principal.positions` / `.permissionSets`), leaving
    nullish branches that can never fire.

Renamed, with what the spec's same-named export actually is:

  FieldInput           -> ScreenFieldInput           (an object FIELD's authoring shape)
  ConversationSummary  -> ConversationListItem       (the AI context-compaction record)
  RuntimeConfig        -> AppShellRuntimeConfig      (the ENGINE runtime config)
  PageHeaderProps      -> PageHeaderComponentProps   (the authored SDUI header schema)
  FlowNode / FlowEdge  -> FlowDesignerNode / ...Edge (a COMPLETE authored node/edge)
  PackageManifest      -> PackageManifestRow         (the full authored manifest)
  InstalledPackage     -> InstalledPackageRow        (the full install record)

`FieldGroup` becomes `ObjectFieldGroup` -- the spec's own name for this exact
shape -- and is derived from `z.input<typeof ObjectFieldGroupSchema>`, not the
exported `z.infer` type: `collapse` carries `.default('none')`, so it is
optional to author and required after parsing, and this designer authors.

Two symbols are derived structurally with one pinned divergence each:
`ScreenSpec` keeps `fields` optional (#3528) and `DecisionOutputDef` adds
`required`, which the server enforces but the spec does not model yet.
Deriving the latter narrowed `type` from a bare string to the spec's closed
enum, so a typo'd picker kind fails to compile rather than degrading to a raw
record-id text box (objectui#2955).

FlowNode/FlowEdge were NOT derived on purpose: a canvas holds nodes the user
has dropped but not finished (no label yet, no edge id yet), so the spec's
complete-node type would make the editor's own intermediate state
unrepresentable. Two genuine findings there are recorded rather than silently
changed, because both alter what gets written to metadata: the designer
persists geometry as `ui: {x,y}` while the spec models it twice already
(`FlowNode.position`, `FlowCanvasNode`), and the edge inspector can build a
`condition` object missing ADR-0089's required `dialect`.

Ledger 115 -> 87 collisions; `@object-ui/app-shell` drops out entirely.
Mutation-tested in three directions: re-forking a burned symbol trips the
guard by name and file, reverting a rename trips it, and leaving a burned
name in DEBT trips the rot ratchet. The new parity test also checks
`isAggregatedViewContainer` by REFERENCE identity -- the copy it replaced was
line-for-line identical, so no value comparison could have caught it
(objectui#3003).

Refs objectui#3157, objectstack#4115

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Jdef6ZFhJmiNRhNCd4DW3
@vercel

vercel Bot commented Aug 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 1, 2026 2:27pm

Request Review

@github-actions github-actions Bot added the tests label Aug 1, 2026
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

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

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 8.45KB 3.08KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 7.57KB 2.97KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 22.10KB 4.37KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.12KB 3.41KB
auth (LoginForm.js) 17.86KB 5.29KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.43KB 2.09KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 35.76KB 9.11KB
auth (createAuthenticatedFetch.js) 4.37KB 1.69KB
auth (index.js) 2.35KB 1.07KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 4.91KB 0.87KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 18.38KB 4.49KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 3.65KB 1.42KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.25KB 0.53KB
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) 475.95KB 104.41KB
core (index.js) 2.25KB 0.80KB
create-plugin (index.js) 9.28KB 2.98KB
data-objectstack (index.js) 136.20KB 34.74KB
fields (index.js) 223.45KB 54.66KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.32KB 1.77KB
i18n (index.js) 2.46KB 0.96KB
i18n (pickLocalized.js) 1.70KB 0.83KB
i18n (provider.js) 5.37KB 1.72KB
i18n (useObjectLabel.js) 26.14KB 6.07KB
i18n (useSafeTranslation.js) 3.26KB 1.44KB
layout (index.js) 38.44KB 10.66KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.74KB
mobile (index.js) 1.50KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.05KB 1.53KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 8.75KB 3.06KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 3.67KB 1.12KB
permissions (evaluator.js) 4.41KB 1.44KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.55KB 0.71KB
plugin-ai (index.js) 15.71KB 3.79KB
plugin-calendar (index.js) 44.90KB 12.35KB
plugin-charts (index.js) 60.53KB 17.12KB
plugin-chatbot (index.js) 180.09KB 42.72KB
plugin-dashboard (index.js) 111.86KB 28.81KB
plugin-designer (index.js) 210.51KB 42.50KB
plugin-detail (index.js) 224.52KB 54.98KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 111.38KB 26.93KB
plugin-gantt (index.js) 162.26KB 39.53KB
plugin-grid (index.js) 184.69KB 48.91KB
plugin-kanban (index.js) 47.82KB 13.18KB
plugin-list (index.js) 104.82KB 25.30KB
plugin-map (index.js) 16.80KB 5.24KB
plugin-markdown (index.js) 13.65KB 4.67KB
plugin-report (index.js) 40.47KB 10.57KB
plugin-timeline (index.js) 25.75KB 7.32KB
plugin-tree (index.js) 8.34KB 2.82KB
plugin-view (index.js) 83.54KB 20.39KB
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.71KB 3.53KB
providers (index.js) 0.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.67KB 2.37KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 19.28KB 6.38KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.02KB 0.55KB
sdui-parser (codegen.js) 4.09KB 1.74KB
sdui-parser (index.js) 4.47KB 2.03KB
sdui-parser (parse.js) 10.04KB 2.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 4.69KB 1.48KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-retry.js) 4.32KB 2.02KB
types (index.js) 2.46KB 1.21KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 0.20KB 0.18KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

@os-zhuang
os-zhuang marked this pull request as ready for review August 1, 2026 14:43
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 1, 2026
Merged via the queue into main with commit 10bead2 Aug 1, 2026
16 checks passed
@os-zhuang
os-zhuang deleted the claude/objectui-spec-symbol-ci-rule-4r3xlc-batch3 branch August 1, 2026 14:43
os-zhuang pushed a commit that referenced this pull request Aug 1, 2026
objectui#3169 (ledger batch 3, `@object-ui/app-shell`) landed first and edited
the same DEBT block — the expected hot spot for parallel batches.

Resolved by REGENERATING the whole block with `--ledger` against the merged
working tree rather than picking conflict lines, so the guard's two-way ratchet
(a missed deletion and an over-deletion both fail) certifies the result.

Verified against the post-#3169 main: removed is exactly this batch's ten
symbols, added is empty, and 17 of 18 packages are byte-identical — including
`@object-ui/app-shell`, which stays fully burned down. 87 -> 77.

The guard header's new case 2b (`unknown` erasure, objectui#3155) and the
`@object-ui/types:ListViewSchema` ALLOW entry merged cleanly; #3169 touched
neither, so nothing of either side was dropped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015Jdef6ZFhJmiNRhNCd4DW3
Sign up for free to 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.

2 participants