Skip to content

Commit 6ac33a5

Browse files
fix(spec): re-point the app liveness ledger off objectui's removed AppSidebar; branding.logo is planned (#20302)
Fixes #20146 Clause-②: no Twelve `AppSchema` rows in `packages/spec/liveness/app.json`, and the file `_note`, cited objectui's `AppSidebar.tsx` as read evidence. `AppSidebar` was deprecated (objectui#5720), never mounted (the console renders `UnifiedSidebar`), and objectui PR #10617 (objectui#5817) has removed it from objectui main. So it was never evidence of a live reader. This PR re-points every one of those rows to the reader that is actually mounted, file and symbol, with no line numbers. It moves `branding.logo` to `planned` with carrier objectui#10827, and fixes the two `AppSidebar` prose mentions in `packages/spec/src/ui/app.zod.ts`. Ledger evidence and comment prose only. ⛔ No schema, parse or accept-set change; no `.describe()` and no refusal text changed. ## Which sha the rows cite, and why All re-pointed readers are cited as `objectui f8a9d0fb`, the `.objectui-sha` pin this repo builds against. That is the convention the `navigation.runAction` row states ("read at the pin this repo builds against, not at objectui main"). - The pin **still carries** `AppSidebar.tsx`: `packages/app-shell/src/layout/AppSidebar.tsx` is present at `f8a9d0fb` and absent at objectui main `fb91ac9b0`. At the pin its only references are the two barrel exports (`packages/app-shell/src/index.ts` and `packages/app-shell/src/layout/index.ts`, both marked deprecated), tests and comments. Its one JSX occurrence outside tests is a usage example inside a doc comment (`providers/ExpressionProvider.tsx`). There is no mount site. That is why the old pointers kept resolving while proving nothing. - Every named reader file exists at the pin. Every reader was re-read there. **Control per row:** the same read, re-run at objectui main `fb91ac9b0` (after the removal), answers identically. So the next pin bump does not strand these pointers. - `check:liveness` attributes a path after an `objectui` realm marker to the other repo. It counts that path and never resolves it (`packages/spec/scripts/liveness/evidence.mts#scanEvidence`). A realm marker only reaches the next `;` or closing paren, so every clause carries its own `objectui @f8a9d0fb:` marker. The gate's foreign-pointer count moves 202 → 218, and its local-path (614) and symbol-anchor (758) counts are unchanged. ## Row → reader, at objectui `@f8a9d0fb` | row | mounted reader (file · symbol) | what it reads | control at objectui main `fb91ac9b0` | |:--|:--|:--|:--| | `label` | `packages/app-shell/src/layout/AppSwitcher.tsx` · `AppSwitcher`; `.../layout/ConsoleLayout.tsx` · `ConsoleLayout`; `.../console/home/HomeAppsStrip.tsx` · `HomeAppsStrip` | `resolveKeyedI18nLabel(activeApp.label, t)` on the pill and `app.label` per row; `appLabel` into `AppHeader` plus the `AppShell` branding title; launcher tiles | same reads | | `description` | `apps/console/src/pages/system/AppManagementPage.tsx` · `AppManagementPage` (routed at `system/apps`) | each list row renders `resolveKeyedI18nLabel(app.description, t)`; `filteredApps` searches it | same reads | | `icon` | `AppSwitcher.tsx` · `AppSwitcher`; `HomeAppsStrip.tsx` · `HomeAppsStrip` | `getIcon(app.icon)` per row / per tile | same reads | | `branding.primaryColor` | `ConsoleLayout.tsx` · `ConsoleLayout` into `packages/layout/src/AppShell.tsx` · `useAppShellBranding` | handed to `AppShell`'s `branding`; mapped onto `--primary` / `--ring` / `--sidebar-primary` | same reads | | `branding.accentColor` | same handoff | mapped onto `--accent` | same reads | | `branding.favicon` | same handoff | sets the favicon link's href | same reads | | `branding.logo` | **none mounted** → `planned` | see below | no reader at main either | | `active` | `AppSwitcher`; `.../layout/UnifiedSidebar.tsx` · `UnifiedSidebar`; `.../utils/appRoute.ts` · `filterActiveApps`; `.../layout/AppHeader.tsx` · `AppHeader` | `a.active !== false` in the switcher list, the sidebar's list and active-app lookup, the openable-apps predicate (called by `HomePage`, `app-launcher-renderer`, `resolveHostAppSegment`), and the avatar menu | same reads | | `hidden` | `AppSwitcher`; `UnifiedSidebar`; `filterActiveApps`; `AppHeader` | `a.hidden !== true` out of the lists; the avatar menu lists `a.hidden === true` apps (Account excepted); the in-repo `ACCOUNT_APP` leg is unchanged | same reads | | `navigation[].id` | `UnifiedSidebar.tsx` · `useNavOrder` and `UnifiedSidebar` (`useNavPins`); `packages/layout/src/NavigationRenderer.tsx` · `NavigationRenderer`; `.../views/nav-menu-renderer.tsx` · `NavMenuRenderer` | persisted reorder by id; pins; sortable id, render keys, active row | same reads | | `navigation[].visible` | `UnifiedSidebar` · `evalVis`; `NavMenuRenderer` · `evalVis` | CEL through `evaluateVisibility`, into `NavigationRenderer` and `hasVisibleNavigationItems`; per-row skip when false (the `NavigationRenderer` item-gate leg is unchanged) | same reads | | `areas[].id` | `UnifiedSidebar` | `activeAreaId` state and the lookup by `a.id`; each area row's key and click | same reads | | `areas[].label` | `UnifiedSidebar` | `resolveInlineI18nLabel(area.label, language)` | same reads | | `areas[].icon` | `UnifiedSidebar` | `getIcon(area.icon)` | same reads | | `areas[].navigation` | `UnifiedSidebar` (the `AppSchemaRenderer` leg is unchanged) | the active area's `navigation` replaces the app's top-level tree | same reads | | file `_note` | shell list re-pointed to UnifiedSidebar / AppSwitcher / AppHeader / ConsoleLayout / ContextSelectors, HomeAppsStrip, NavigationRenderer / AppShell, RootLandingRedirect / AppManagementPage | plus a dated paragraph recording the sha choice | — | Mount sites, measured at the pin: `UnifiedSidebar` in `ConsoleLayout`; `AppSwitcher` in `AppHeader`; `HomeAppsStrip` in `HomePage` and `app-launcher-renderer`; `nav-menu-renderer` registered by import in `packages/app-shell/src/index.ts`; `AppManagementPage` on the console route `system/apps`. ## `description`: still `live`, narrower The one mounted reader is the admin app list (`/system/apps`). The card asked whether `AppManagementPage` still makes the row `live`, and it does. Its `apps` come from `useMetadata()`, the app metadata list, and the page is routed. The "under the active app's title" presentation has **no successor**. Not counted: `packages/app-shell/src/console/home/AppCard.tsx` also reads `app.description`, but at the pin it is only barrel-exported (no mount site), which is the same trap `AppSidebar` was. ## `branding.logo` → `planned` (carrier objectui#10827) `branding` is now **drilled**, so `logo` gets its own verdict while `primaryColor`, `accentColor` and `favicon` keep `live`. Census at the pin, with the same grep run for `branding.primaryColor` as the positive control (it hits `ConsoleLayout` and `AppShell`): - **Authored** (the row's `producer`): `packages/plugin-designer/src/AppCreationWizard.tsx` · `AppCreationWizard` and `BrandingEditor.tsx` · `BrandingEditor` write it with a preview; `pages/EditAppPage.tsx` · `EditAppPage` pre-fills from it. - **Rendered by nothing mounted.** The only reader of `branding.logo` in app-shell or layout at the pin is `AppSidebar` (unmounted). At main there is none. `ConsoleLayout` hands `AppShell` `primaryColor` / `accentColor` / `favicon` and never `logo`, and `AppShellBranding` has no logo field. `EmbeddableForm`'s `config.branding.logo` is a form embed config, and `runtime-config.ts`'s `branding.logoUrl` is console runtime config; neither is the app key. - **The nearest thing to a read:** `packages/types/src/app.ts` · `wizardDraftToAppSchema` copies `draft.branding.logo` into a top-level `logo`, and `packages/layout/src/AppSchemaRenderer.tsx` · `InternalSidebar` renders that top-level `schema.logo`. - **`authorWarn`: not set,** following the ledger's rules. README "Author warnings" rule 1 bars marking benign display metadata (its examples: `description`, `tags`, `icon`). The `field.useGrouping` precedent is a `planned` row whose consumer is being built: "an author who sets this today loses nothing and is not misled". A logo URL is that case, because it takes effect when objectui#10827 lands, with no re-authoring. `@objectstack/lint` warns on a `planned` row only with an explicit `authorWarn: true` (`packages/lint/src/lint-liveness-properties.ts#shouldWarn`), so author-facing lint output does not change. - The drill removes `app/branding` from `packages/spec/scripts/liveness/undrilled-containers.baseline.json`, which is shrink-only; the gate fails on a row whose container now drills. Container debt goes from 110 containers / 569 keys to 109 / 565. ### Spelling (Zone 1 measured) `AppSchema` declares only `branding.logo`. A top-level `logo` on an app is **refused**, not folded: ```text AppSchema.safeParse({ name: 'probe_app', label: 'Probe', logo: 'https://x/l.png' }) -> unrecognized_keys, path [], keys ['logo'] "Unrecognized key(s) on this app: `logo`. Did you mean `logo` → `branding`? ..." AppSchema.safeParse({ name: 'probe_app', label: 'Probe', branding: { logo: 'https://x/l.png' } }) -> accepted, branding.logo kept ``` The alias table (`logo: 'branding'`) feeds the refusal's "did you mean". It does not rewrite the key (`packages/spec/src/shared/strict-object.ts` header), and no ADR-0087 conversion carries `logo`. So the conclusion holds (one declared spelling, nothing to retire), and the word "folds" in the dispatch and in objectui#10827's spelling paragraph is imprecise. ## One note corrected while re-reading The `active` note said the sidebar's active-app lookup "spans all apps so a direct /apps/NAME URL keeps rendering". At the pin, that comment is about **hidden** apps. The sidebar lookup filters `active !== false`. What keeps an inactive app routable is app-shell `AppContent`, which resolves `matchAppBySegment(apps, appName)` over all apps. The note now says so. The verdict (`live`, and routing not disabled) is unchanged. ## Prose in `packages/spec/src/ui/app.zod.ts` (comments only) ```diff - * time by the shell (see Console's `AppSidebar` / `AppContent`): + * time by the shell (see objectui's `NavigationRenderer`, whose `resolveHref` + * substitutes them from the session ids the mounted `UnifiedSidebar` and the + * console's `AppContent` supply): ``` Measured at the pin: `NavigationRenderer.tsx` `applyNavTemplate` inside `resolveHref`, fed `templateContext` by `UnifiedSidebar`; `AppContent`'s landing route goes `findFirstRoute` → `buildItemRoute` → `resolveHref` with the session context. ```diff - * sorted (`NavigationRenderer.tsx:1154`). Area-level order is not — `AppSidebar` + * sorted (`NavigationRenderer.tsx:1154`). Area-level order is not — `UnifiedSidebar` ``` Measured at the pin and at main: `UnifiedSidebar` filters then maps `areas` with no sort, and `AppSchemaRenderer` maps `areas` as authored. ## Pins No test pins these rows' evidence text or the `_note`. `git grep AppSidebar` outside `app.json` hits only the two comments above, plus CHANGELOG and audit history, which are untouched. The tests that read the real ledger (`packages/spec/scripts/liveness/check-liveness.test.ts`, which copies it and mutates `navigation.children.children`) and the consumer suite (`packages/lint/src/lint-liveness-properties.test.ts`) were run; the readings are below. No pin flips. ## Changeset `.changeset/20146-app-ledger-repoint.md`, `@objectstack/spec: patch`. `liveness/` and `src/**/*.zod.ts` are both in `packages/spec/package.json` `files[]`, and `@objectstack/lint` reads the shipped ledgers at runtime, so this diff publishes. That is why this PR carries no `skip-changeset`. ## Verification (at HEAD `8a4cb2f0`) Every reading below was taken at `8a4cb2f0` (this branch's head). `packages/spec` `dist` was rebuilt from that source before any gate that reads it. - **`check:liveness`, before and after.** At base `1207baf0`: exit 0, `app 56 classified (live 47, dead 9)`. At `8a4cb2f0`: exit 0, `app 59 classified (live 49, dead 9, planned 1)`, with this verdict line: `✓ packages/spec/liveness/state-counts.md is current`. Also at `8a4cb2f0`: `evidence paths: 614 repo-local … 614 resolved …; 218 attributed to another repo` (base: 202); `symbol anchors: 758 … 758 naming a symbol the cited file contains`; `container coverage: 109 container entr(ies) … 565 child key(s)` (base: 110 / 569). - **`state-counts.md`** was regenerated with `gen:liveness-counts`, and the gate reads it back as current. - **Spec package, full `local` project.** Run through the verify lock: `pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2`, exit 0, `Test Files 552 passed (552)`, `Tests 16251 passed | 1 todo (16252)`. This includes `scripts/liveness/check-liveness.test.ts`, which copies and mutates the real ledger. - **Spec typecheck.** `pnpm --filter @objectstack/spec run typecheck` (tsc, then `check:scripts-typecheck`, then `check:test-typecheck`): exit 0. - **Consumer suite** (`@objectstack/lint` reads the shipped ledgers). `pnpm --filter @objectstack/lint exec vitest run --maxWorkers=2 src/lint-liveness-properties.test.ts src/component-field-specs-liveness.test.ts`: exit 0, `Test Files 2 passed (2)`, `Tests 91 passed (91)`. - **Derived gate families.** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, derived from the actual 5 changed paths: 81 commands. 80 exit 0. Reconciled with `--ran`: `✓ dispatch-gates --ran: 81 derived famil(ies) accounted for — 80 run, 1 NOT-MEASURED`, exit 0. - **NOT MEASURED: `pnpm check:dual-build-cjs-loads`.** Reason: exit 3, `PREREQUISITE NOT MET — this gate reads built output`. It needs every one of the workspace's 82 packages built, and this diff changes no emitted code (JSON data, one comment, a baseline row, a markdown table). This is a declared narrowing; CI builds the workspace and runs it. - `check:lean-entry-closure` exited 3 on its first run, before `@objectstack/objectql` was built. After that build it exits 0: `2 published condition(s) measured from a real load`. - **ESLint, narrowed and proven.** Of the 5 changed paths, 1 is in eslint's population (`files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']` in `eslint.config.mjs`; the other four are JSON or markdown). `pnpm exec eslint --no-inline-config --format json packages/spec/src/ui/app.zod.ts`: exit 0, 1 file, 0 errors, 0 warnings. Invariance: the config never enables type-aware linting (no `parserOptions.project`, no typed rule sets), so this diff cannot move the verdict on any untouched file. ## Acceptance notes - **Rows not in this card's scope still cite `@940ba24` with line numbers.** They are the `NavigationRenderer` / `ContextSelectors` / `RootLandingRedirect` / `surfaceAgent` legs, which never named `AppSidebar`. They were left as they are. - **Stale `rest-server.ts` pointers inside `note` prose** remain on the `areas.navigation` (`:1870`) and `navigation.children` (`:1837`) rows. The file `_note` already records that the gate now lives in `meta-item-read-gate.ts#filterAppForUserWithReason`. `note` is not scanned by any gate. Left alone, because it is outside this card. ## Deviations from the claim's file surface - `packages/spec/scripts/liveness/undrilled-containers.baseline.json`: one row deleted (`app/branding`). The ordered `branding.logo` → `planned` requires drilling `branding`, and `check:liveness` fails on a baseline row whose container drills. This is a shrink of a shrink-only ratchet. - `.changeset/20146-app-ledger-repoint.md`: added, because the diff publishes (see Changeset). --- _Generated by [Claude Code](https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7b1e4a4 commit 6ac33a5

5 files changed

Lines changed: 94 additions & 41 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
`liveness/app.json`: every `AppSchema` row that cited objectui's `AppSidebar.tsx` as read evidence now cites the reader that is actually mounted, and `branding.logo` is `planned` instead of riding on its container's `live`. Ledger evidence and comment prose only. ⛔ No schema, parse or accept-set change.
6+
7+
The ledgers ship inside this package (`files[]` includes `liveness`), and `@objectstack/lint` reads them to decide which authored keys draw an advisory warning, so the rows an upgrading reader or tool consults are these.
8+
9+
- **Twelve rows re-pointed, and the file `_note`.** `label`, `description`, `icon`, `branding`, `active`, `hidden`, `navigation[].id`, `navigation[].visible`, `areas[].id`, `areas[].label`, `areas[].icon` and `areas[].navigation` cited `AppSidebar`. That component was deprecated, never mounted (the console renders `UnifiedSidebar`), and objectui has since removed it, so it was never evidence of a live reader. Each row now names its mounted reader, file and symbol: `AppSwitcher`, `UnifiedSidebar`, `AppHeader`, `ConsoleLayout` into `AppShell`'s `useAppShellBranding`, `HomeAppsStrip`, `filterActiveApps`, `NavigationRenderer`, `NavMenuRenderer` and `AppManagementPage`. All are read at the `.objectui-sha` pin f8a9d0fb, and each reads its key unchanged at objectui main fb91ac9b0.
10+
- **`branding.logo` is `planned`.** Plugin-designer's app wizard and branding editor write it, and nothing mounted renders it: the console's branding handoff passes `primaryColor`, `accentColor` and `favicon` and never `logo`. The row names the objectui card that will render it (objectui#10827). It is not `authorWarn`'d: a logo URL is display metadata, and an author who sets it now loses nothing, because the value takes effect when that renderer lands. The `branding` row is drilled so `primaryColor`, `accentColor` and `favicon` keep their own `live` verdicts. Its entry in `undrilled-containers.baseline.json` goes away with the drill, and `state-counts.md` is regenerated (`app`: 56 → 59 classified, one `planned`).
11+
- **`description` is still `live`, but narrower.** Its one mounted reader is the admin app list at `/system/apps`, which shows each app's description and searches it. The sidebar presentation this row used to describe has no successor.
12+
- **Two corrections found while re-reading.** The `active` row's note said the sidebar's active-app lookup spans all apps. It skips `active: false` apps, and what keeps an inactive app routable by direct URL is the console's route resolver, which matches against every app. The two `AppSidebar` mentions in `ui/app.zod.ts` (the `recordId` template-variable note and the `AREA_ORDER_RETIRED` note) now name `NavigationRenderer` / `UnifiedSidebar`.

0 commit comments

Comments
 (0)