Skip to content

fix(spec): re-point the app liveness ledger off objectui's removed AppSidebar; branding.logo is planned - #20302

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-20146-app-ledger-repoint
Sep 27, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-20146-app-ledger-repoint

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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:

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)

- * 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.

- * 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

…pSidebar

Twelve AppSchema rows in packages/spec/liveness/app.json, and the file
_note, cited objectui's AppSidebar.tsx as read evidence. AppSidebar was
deprecated, never mounted (the console renders UnifiedSidebar), and has
been removed from objectui main, so it was never evidence of a live
reader. Each row now cites the mounted reader, file and symbol, at the
.objectui-sha pin f8a9d0fb (every one also reads its key unchanged at
objectui main fb91ac9b0).

branding is drilled: logo is planned with carrier objectui#10827 (the
designer writes it, nothing mounted renders it), while primaryColor,
accentColor and favicon stay live through ConsoleLayout into AppShell.
The app/branding row leaves undrilled-containers.baseline.json with the
drill. The two AppSidebar prose mentions in app.zod.ts name the live
readers instead. No schema, parse or accept-set change.

Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN
Co-authored-by: Claude <noreply@anthropic.com>
…nding row

app: 56 -> 59 classified (live 47 -> 49, planned 0 -> 1). Written by
gen:liveness-counts; check:liveness reads it back as current.

Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN
Co-authored-by: Claude <noreply@anthropic.com>
liveness/ and src/**/*.zod.ts both ship in the spec tarball, and
@objectstack/lint reads the shipped ledgers, so this ledger and prose
change publishes.

Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:ui tooling labels Sep 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 3 changed file(s) yielded no anchor (packages/spec/liveness/app.json, packages/spec/liveness/state-counts.md, packages/spec/src/ui/app.zod.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 3 changed file(s) yielded no anchor (packages/spec/liveness/app.json, packages/spec/liveness/state-counts.md, packages/spec/src/ui/app.zod.ts) — pages documenting those are invisible to this run
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 733822cf8562d671682794edba717cdd1700b1c0 → packageMentionDocs.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 8a4cb2f02a101336acf5f10c98897a4c8c18151e

① Derived judgments

Reviewed at head 8a4cb2f02a101336acf5f10c98897a4c8c18151e (PR #20302, card #20146), merge-base 1207baf0 with main, diff exactly 5 files (+94/−41). objectui readers were measured with git show / git grep against the .objectui-sha pin f8a9d0fb0596f4521076628e2bbfe27e6ce67d52 (the pin at the head) and controlled at objectui fb91ac9b0c4ae994179afd83ddcf078e0f3fd2c6; objectui's HEAD was never switched. Gates were run in a detached worktree at the head under the scratchpad (installed from the offline store; check:* scripts run outside the heavy-verify lock, as that lock's own header states), and the worktree was removed afterwards.

1. Every re-pointed row names a real, MOUNTED reader at f8a9d0fb — RIGHT.

Preconditions measured: packages/app-shell/src/layout/AppSidebar.tsx exists at the pin and is absent at fb91ac9b0. At the pin its only non-test references are the two deprecated barrel exports (packages/app-shell/src/index.ts:139, layout/index.ts:7), comments, and one JSX occurrence inside the providers/ExpressionProvider.tsx doc comment (line 12). No mount site. Every file the 12 rows cite exists at both shas.

Mount chain, measured at the pin: apps/console/src/App.tsx:389 renders the console AppContent, whose line 36 renders app-shell's DefaultAppContent (packages/app-shell/src/index.ts:263, the alias of console/AppContent.tsx#AppContent) with extraRoutes={systemRoutes}; app-shell AppContent renders ConsoleLayout at line 1028 and {extraRoutes} at line 1116; ConsoleLayout renders AppShell (line 134) with UnifiedSidebar as sidebar (136) and AppHeader as navbar (142); AppHeader:488 renders AppSwitcher; UnifiedSidebar:543 and :673 render NavigationRenderer; App.tsx:127 renders DefaultHomePage (= console/home/HomePage.tsx, index.ts:271), and HomePage:600 renders HomeAppsStrip; apps/console/src/AppContent.tsx:160 routes system/apps to AppManagementPage inside the exported systemRoutes fragment.

Per row, the cited symbol and the read, each confirmed at the pin and identical at fb91ac9b0:

  • label: AppSwitcher.tsx#AppSwitcher reads resolveKeyedI18nLabel(activeApp.label, t) (51) and app.label per row (68); ConsoleLayout.tsx#ConsoleLayout reads activeApp?.label into appLabel (75), hands it to AppHeader (144) and into the AppShell branding title (174–176); HomeAppsStrip.tsx#HomeAppsStrip reads app.label (127).
  • description: apps/console/src/pages/system/AppManagementPage.tsx#AppManagementPage (98) takes apps from useMetadata() (103), renders resolveKeyedI18nLabel(app.description, t) per row (457–458) and searches it in filteredApps (167–173). Routed and reachable as above. Not counted: console/home/AppCard.tsx reads app.description but at the pin is only barrel-exported (console/home/index.ts:10), no JSX mount anywhere — the row says so.
  • icon: AppSwitcher getIcon(app.icon) (67); HomeAppsStrip getIcon(app.icon) (126).
  • branding.primaryColor / accentColor / favicon: ConsoleLayout hands exactly activeApp.branding.primaryColor, .accentColor, .favicon (171–173) to AppShell's branding prop; packages/layout/src/AppShell.tsx#useAppShellBranding (140) maps primaryColor onto --primary, --ring, --sidebar-primary (164–170, with a dark variant), accentColor onto --accent (192), and sets the #favicon link's href (221–225); AppShell calls the hook (268). AppShellBranding (12–25) has no logo field.
  • active: AppSwitcher:45 a.active !== false && a.hidden !== true; UnifiedSidebar:205 the same filter and :207 the active-app lookup over the apps filtered on active !== false only; utils/appRoute.ts#filterActiveApps (86–88), called by HomePage.tsx:355, views/app-launcher-renderer.tsx:119 and resolveHostAppSegment (132); AppHeader.tsx:857 avatar menu a.active !== false && a.hidden === true && a.name !== 'account'. Routing claim: app-shell console/AppContent.tsx:243 matchAppBySegment(apps, appName) over all apps — the corrected note is true.
  • hidden: the same four readers; the UnifiedSidebar:207 lookup filters on active only, so it spans hidden apps as the row says; AppHeader:857 lists hidden === true apps with Account excepted. The in-repo ACCOUNT_APP leg resolves locally (gate: 614/614).
  • navigation[].id: UnifiedSidebar.tsx#useNavOrder (74) persists ids per app (maps the reordered items to their ids and rebuilds through a byId map); UnifiedSidebar calls useNavPins() (211), whose hooks/useNavPins.ts keys favourites by itemId; NavigationRenderer.tsx#NavigationRenderer (1296): useSortable({ id: item.id }) (954), onPinToggle(itemId, …) (939), key={item.id} (1414–1477), item.id === activeNavId (1201); views/nav-menu-renderer.tsx#NavMenuRenderer (147): resolveActiveNavItem(...)?.id (239), key={item.id} (273–326).
  • navigation[].visible: UnifiedSidebar evalVis (215–216, evaluateVisibility from the ExpressionProvider) handed to NavigationRenderer (546, 676) and hasVisibleNavigationItems (273–274); NavMenuRenderer evalVis (164) and if (!evalVis(item.visible)) return null (266). The unchanged NavigationRenderer.tsx:891 leg at @940ba24 corresponds to evaluateVisibility(item.visible) at pin line 407.
  • areas[].id / label / icon / navigation: UnifiedSidebar activeAreaId state (283), lookup a.id === activeAreaId (303), area.id key/click (517), resolveInlineI18nLabel(area.label, language) (523), getIcon(area.icon) (516), activeArea?.navigation || activeApp?.navigation (304). The AppSchemaRenderer.tsx:469 leg is unchanged.
  • File _note: names UnifiedSidebar / AppSwitcher / AppHeader / ConsoleLayout / ContextSelectors, HomeAppsStrip, NavigationRenderer / AppShell, RootLandingRedirect / AppManagementPage, and records the sha choice. All named readers are mounted per the chain above (ContextSelectors, RootLandingRedirect legs are unchanged rows).

One non-blocking nuance: NavMenuRenderer is registered, not statically mounted — ComponentRegistry.register('menu', NavMenuRenderer) (370) through the side-effect import packages/app-shell/src/index.ts:345; no page in the console, app-shell, packages/platform-objects or examples declares a menu component. The two rows that cite it also cite UnifiedSidebar / NavigationRenderer, which are mounted, so no verdict rests on that leg.

Gate-side: pnpm --filter @objectstack/spec check:objectui-pin-citations at the head, with the read-only objectui clone reachable as ../objectui: exit 0, 47 asserting objectui pin citation(s) match .objectui-sha (f8a9d0fb0), 7 anchor content assertion(s) verified against objectui at f8a9d0fb0. check:liveness counts the ledger's foreign pointers 202 → 218 with no local path or anchor change (614/614, 758/758), which confirms every new objectui @f8a9d0fb: clause is attributed to the foreign realm and none was mis-parsed as a local path (evidence.mts#scanEvidence: a realm marker runs to the next ; or )).

2. branding.logo → planned, carrier objectui#10827, producer pointers; no authorWarn — RIGHT.

Census at the pin (control: the same grep for branding.primaryColor hits ConsoleLayout:171 and AppShell.tsx:152–167): the only app-key reader of branding.logo in app-shell / layout / console is AppSidebar.tsx:218 (unmounted); plugin-form/EmbeddableForm.tsx:530 reads a form embed config and runtime-config.ts:403 reads branding.logoUrl — neither is the app key. At fb91ac9b0 there is no reader at all. Producers exist at both shas: plugin-designer/src/AppCreationWizard.tsx#AppCreationWizard (700; writes logo at 604–605), BrandingEditor.tsx#BrandingEditor (105; writes at 308–309, previews at 476/560), pages/EditAppPage.tsx#EditAppPage (19; pre-fills at 52). packages/types/src/app.ts#wizardDraftToAppSchema (16 in the extracted range) copies draft.branding.logo to a top-level logo, and packages/layout/src/AppSchemaRenderer.tsx#InternalSidebar renders schema.logo (384–398) — the row states this nuance correctly. objectui#10827 is open, domain:ui, priority:p2, and names itself the carrier for app.branding.logo.

Status under the README: planned = "Declared, intentionally not enforced yet"; dead = no consumer anywhere the census looked, tracked for enforce-or-remove; live needs a runtime consumer. The key is authored, has a named carrier building its renderer, and triage (5832699195) directed ENFORCE, so planned is the fitting status, dead would contradict the triage direction and live has no reader. The gate scans planned rows' evidence when present and producer at any status (EVIDENCE_SCANNED_STATUSES includes planned; producer resolves through the same resolver, foreign paths counted not resolved); no rule requires evidence on a planned row, and the row's producer is the correct field for the authoring side. authorWarn: README "Author warnings" rule 1 bars marking benign display metadata (description, tags, icon are its examples); packages/lint/src/lint-liveness-properties.ts#shouldWarn (157–159) warns only on authorWarn === true or status === 'experimental', so a planned row without the flag produces no author-facing change, matching the field.useGrouping precedent (planned, no authorWarn, no evidence/producer). Dropping authorWarn is correct.

3. No row lost a reader; no row became live without one; gate green; state-counts.md current; baseline deletion exact — RIGHT.

Structural walk of app.json at 1207baf0 vs the head: 16 rows changed. 11 leaf rows live → live with evidence changed (label, description, icon, active, hidden, navigation.id, navigation.visible, areas.id, areas.label, areas.icon, areas.navigation); branding went from one live leaf to a drilled container with primaryColor / accentColor / favicon live and logo planned. Leaf counts 39 live + 9 dead → 41 live + 1 planned + 9 dead. No status went live → anything else; every live row at the head carries evidence; no AppSidebar remains in any evidence or producer field; no authorWarn changed. Rows still citing @940ba24 (24 legs) never named AppSidebar and are unchanged.

pnpm --filter @objectstack/spec check:liveness at the head: exit 0; app 59 classified (live 49, dead 9, planned 1); evidence paths: 614 repo-local … 614 resolved … 218 attributed to another repo; symbol anchors: 758 … 758; container coverage: 109 … 565 child key(s); ✓ packages/spec/liveness/state-counts.md is current — the same 40 row(s). Control at the base ledger files (1207baf0): exit 0, app 56 classified (live 47, dead 9), 202 foreign, 110 / 569. pnpm --filter @objectstack/spec gen:liveness-counts at the head rewrote state-counts.md byte-identically (git diff empty); the diff's app row 47/0/0/9/0/56 → 49/0/0/9/1/59 and totals 931/5/1/168/11/1116 → 933/5/1/168/12/1119 are what the gate measures.

Baseline: drill.mts fails a baseline row whose container now drills. Control: with app/branding put back into undrilled-containers.baseline.json at the head, the gate exits 1 with ✗ 1 stale undrilled-container row(s) — the gap is already closed: app/branding. The one-row deletion is exactly what the drill requires, and it is a shrink of a shrink-only file.

4. app.zod.ts diff is comments only; the new prose is true at the pin — RIGHT.

typescript.transpileModule with removeComments on the base and head files gives identical output (33276 bytes both); the raw diff is six lines, all inside two JSDoc blocks; counts of describe(, strictObject( and retiredKey( are unchanged; strict-object.ts is identical to origin/main. Prose: packages/layout/src/NavigationRenderer.tsx#resolveHref (561) calls applyNavTemplate (487, substituting current_user_id / current_org_id at 495–496) at 575, 596, 629, 677; UnifiedSidebar supplies templateContext={{ currentUserId, currentOrgId, contextValues }} (558, 684); app-shell console/AppContent.tsx imports resolveHref (38) and its landing route goes resolveLandingRoute (1259) → findFirstRoute (1211) → buildItemRoute (1233) → resolveHref(item, '', ctx) (1236). AREA_ORDER_RETIRED prose: UnifiedSidebar does areas.filter(...) (272) with no .sort( anywhere in the file, and AppSchemaRenderer does areas.filter (545) / areas.map (230) as authored. Both true at the pin and at fb91ac9b0.

5. Semver / changeset — RIGHT. See ②.

6. Top-level app logo: "folds into branding" (order) vs refused (dev) — the dev is RIGHT; the order's word is WRONG on main.

packages/spec/src/ui/app.zod.ts:1387 has logo: 'branding' in the app-level alias table; strict-object.ts uses aliases only to phrase the unrecognized_keys refusal (aliases[aliasProbe(key)] ?? findClosestMatches(…), and markUnknownKeyRefusalTerminal makes the refusal terminal); nothing rewrites the key. Probe run with tsx at the head, which is equivalent to main for this behaviour (app.zod.ts differs from origin/main by the six comment lines only and strict-object.ts is identical): AppSchema.safeParse({ name: 'probe_app', label: 'Probe', logo: 'https://x/l.png' }) → success: false, one issue unrecognized_keys, path [], keys ['logo'], message "Unrecognized key(s) on this app: logo. Did you mean logo → branding? …"; { …, branding: { logo } } → success: true with branding.logo kept; both spellings together → refused on logo. AppSchema declares one spelling (branding.logo, app.zod.ts:927); the order's conclusion (nothing to retire) stands.

② Semver level

patch on @objectstack/spec (.changeset/20146-app-ledger-repoint.md) is right. packages/spec/package.json files[] is ["dist","json-schema","liveness","prompts","llms.txt","README.md","src/**/*.zod.ts",…], so both liveness/app.json and the app.zod.ts comment change publish; packages/lint/src/lint-liveness-properties.ts reads the shipped liveness/ directory with readFileSync (72–133) to decide advisory warnings, so the ledger is consumed data, not internal tooling. Nothing in the parsed surface, refusal text, .describe() or accept set moves (probe and transpile compare above), so no minor or major is owed and skip-changeset would have been false. The changeset's statements were checked: files[] includes liveness (true), lint reads the ledgers for author warnings (true), app 56 → 59 classified with one planned (true, measured), branding drilled and its baseline row removed (true), description narrower but live (true), the active note and the two app.zod.ts mentions corrected (true). Clause-②: no is consistent with all of it. check-changeset-no-major has nothing to object to.

③ Boundary flags

  • Cross-repo evidence: all 12 re-pointed rows cite objectui @f8a9d0fb, the .objectui-sha pin at the head, with evidenceScope: cross-repo and verifiedAt: 2026-09-27; each read was controlled at objectui fb91ac9b0 (post-removal) and answers identically, so the next pin bump does not strand them.
  • Carrier: objectui#10827 (open, domain:ui, p2) is named on the branding.logo row; its own "folds into branding" wording is imprecise and is the seat's to correct there, not this PR's.
  • Registered-not-mounted leg: NavMenuRenderer (see ① nuance); non-blocking because every row citing it also cites a mounted reader.
  • Out of scope, noted by the dev and confirmed: stale rest-server.ts:1870 / :1837 pointers inside note prose on areas.navigation / navigation.children (not scanned by any gate); the unchanged @940ba24 legs.
  • Deviation from the claim's file surface (undrilled-containers.baseline.json row deleted; changeset added): both are mechanical consequences the order anticipated ("whether a changeset is owed is decided by the gates") and are measured above.
  • CI at the head at review time: 39 check runs — 32 success, 6 skipped, 0 failed, 1 still in progress (Lint & Repo Gates). Readiness stays gated on every context finishing green, per the seat's ACCEPT.

Implemented-by: claude/issue-20146-app-ledger-repoint
Reviewed-by: session_01Rjy9MeetSfq34PKn81CRiN

VERDICT: PASS

Blocking items: none.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 27, 2026 20:17
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 27, 2026
Merged via the queue into main with commit 6ac33a5 Sep 27, 2026
44 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20146-app-ledger-repoint branch September 27, 2026 20:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/m tooling

Projects

None yet

2 participants