chore(desktop): check R2 root symbol uses and retained root hooks - #5934
Conversation
The renderer architecture checker gains the rules apache#4582 needs to close R2 by numbers. rootSymbolUses (M0): a generated record, per feature public entry, of the runtime symbols each root zone takes from it: appShell (the AppShell family), composition, and bootstrap (bootstrap/ plus the guarded main.tsx and app.tsx). Uses are attributed through named and default imports, static namespace members including JSX members, and re-exports followed through any module until a feature public entry. Namespace escapes, wildcard or namespace re-exports over an entry, and runtime import() or require of an entry are violations. Against the base the record may only shrink, except for an export the same change adds to the entry, measured on the materialized base tree, or a use moving one way out of appShell into composition or bootstrap. The CLI lists each admitted use. Retained root hooks (M5): the renderer README now has one row per call site the AppShell hook gate allows, naming its consumer, owner, allowed capability and either the root reason or the removal module. The checker reads the gate's ALLOWED literal without running or editing it and fails when an entry has no row, when the row count differs from the gate count, or when a row names a hook the gate no longer lists. --report prints the M3/M5 completion measures: AppShell-family bridge references and action factories, the Conversation transitional rows, hook-gate entries without a row, and root symbol uses per zone. Seven feature exports that only tests or Storybook read move from public entries to testing.ts (or, for the WorkHub coordination lifecycle, to its application contract). The README lists the remaining non-assembly root exports outside Conversation with their consumer and removal module. Generated-by: Claude Opus 5.5
Astro-Han
left a comment
There was a problem hiding this comment.
Reviewed 641982ed9eac5792cf0d7065b55dca4b33865919 (16 files, +1455/−39, a single commit).
No P0–P3 findings, with one question I could not settle and am stating rather than assuming.
What this adds. The PR introduces root-symbol-use checks and retained-root-hook checks for the R2 work, and it adds no production behaviour of its own: 16 files, and app-shell.tsx is not touched at all — which also means it cannot conflict with the three sibling PRs on that file.
The one thing I could not determine. apps/desktop/renderer-architecture.json moves +189/−0 — a pure addition, no deletions. Read as new enforcement, that is the tightening direction: new entries that pin root symbol uses and the retained hook set cannot loosen an existing budget, because nothing was removed. Read as new allowances — for instance quotas that permit hooks to remain — it would be the opposite, and the title's phrase "retained root hooks" is ambiguous between pinning a set and granting it. My attempt to classify the added manifest lines did not complete cleanly, so I am flagging it instead of guessing: the distinction is worth one look by whoever owns the manifest semantics, since the acceptance criterion for this series is that the counts only tighten.
Gate on this head: test and label are green; the commit carries a Generated-by: trailer and there is no Grok involvement.
What I did not judge
- The enforcement semantics of the added manifest entries, as above.
- The check implementation's correctness in the two new test suites.
- No Electron run.
I did not approve, request changes, or merge.
Automated review notice: This comment was posted by an automated review agent operated by Astro-Han. It is not an independent human review and does not replace one.
|
On the open question in the review of
Posted by Claude Code on behalf of the PR author. |
Astro-Han
left a comment
There was a problem hiding this comment.
Independent review of 641982ed9eac5792cf0d7065b55dca4b33865919 (base 1a66e4d5e; it merges cleanly onto current main 887924e14, and check-renderer-architecture.mjs --base origin/main --strict-base passes there).
Verdict: this is new enforcement, not new allowance. One gap in the admission rule (P3) plus further P3s. The four ratchets themselves look right.
What the +189/-0 ledger change means. rootSymbolUses is a record of 89 symbols across 18 feature entries: appShell 66, composition 23, bootstrap 0. It is not a budget:
- Before this PR, a root caller allowed to import a feature public entry could take any symbol from it.
- After it, the tree must equal the record exactly (
validateRootSymbolUses), and against the base the record may only shrink (compareRootSymbolUses). The two exceptions are an export the same change adds, and a one-way move out ofappShell. Dropping the record fails. - The existing 89 uses are grandfathered as a snapshot of what was already unconstrained.
- No existing limit gets looser. The only removed lines in the checker are the
readdirSyncfilter, which is nowlegacyAppShellFiles()with the sameLEGACY_APP_SHELL_FILEregex plus anexistsSyncguard.scripts/check-app-shell-hooks.mjsis untouched. - The retained-root table is read-only validation over the gate's
ALLOWEDliteral (row per call-site count, no rows for retired hooks). It grants nothing; the gate is still the budget.
Local run on this head: check-renderer-architecture.test.mjs 158/158, --base origin/main --strict-base passes, and --report matches the PR body: 43 bridge refs, 6 factories, 4 transitional capabilities, 28 gate entries / 42 call sites / 0 without a row. CI is green.
P3: an alias defeats the "existing export" rule. (Graded P3: it is a hole in a new guard, not a runtime defect, and the ratchet is still strictly tighter than before this PR.) Admission keys on the exported name (baseEntrySurfaces.get(dirname(use.entry))?.has(use.symbol)), so re-exporting an existing binding under a new name counts as a "new public export". I checked this on this head:
- Control: composition newly imports the existing
AppUpdateAboutProjectionConsumer. The check fails withnewly uses existing public export, as it should. - Alias: I added
export { AppUpdateAboutProjectionConsumer as AppUpdateAboutProbe } from './ui/app-update-projection-context.js'tofeatures/app-update/index.ts, imported it in composition, and ran--write.--base HEADthen passes withroot symbol use admitted: … composition AppUpdateAboutProbe (new public export).
The index.ts diff and the CLI line do make it visible, but the PR states that "taking an export the base entry already had fails". Suggested fix: measure the base surface by resolved binding (origin module plus original name, which resolveModuleExports already follows) and admit only bindings that the base entry did not expose under any name. A fixture for the alias case would pin it.
P3: the closure gap is already concrete. rootSymbolCallers records only the 16 top-level AppShell-family files, bootstrap/** and composition/**. The body documents this as "Wrappers / Non-root callers". With #5935, chat-message-surface.tsx, which is AppShell closure rather than a root zone, starts importing TaskReadinessNoticeConsumer from the Conversation entry, and nothing records it. Consider recording the AppShell closure's entry uses as a separate zone, or at least listing the closure files that render root regions.
P3: rows are matched by count only. The "Call site" column is free text, so swapping two useState rows' consumers or reasons passes. This is documented under "Table accuracy". A cheap step up would be to require that the call-site text names an identifier that actually appears at a call of that hook in AppShellContent.
Merge-order evidence. I built the merged trees and ran this PR's checker on each:
- main + #5934 + #5935 fails with one stale retained-root row (
useTaskSubmissionReadiness, README:307), 5 unrecorded new root uses (ComposerSubmissionProvider,TaskReadinessProvider,createComposerSubmissionCommands, and the two services providers) and 9 stale entries. The gate itself is fine (26/33). - main + #5934 + #5937 fails with one unrecorded new export,
ManualDiagnosticReportConsumer. - #5936 conflicts textually with this PR in
features/workhub/index.ts.
All of these are new exports or shrinkage, so --write plus row deletion clears them. Landing this PR first, as its body asks, puts that small cost on each slice.
This is an automated review by Claude (Anthropic), run on behalf of the maintainer. It is not an approval. Please verify the findings before acting on them.
|
Thanks @chihumyum — reviews of the four R2 PRs (#5934, #5935, #5936, #5937) are posted. Each merges cleanly onto |
…rows to calls Review follow-ups on the R2 enforcement rules. Root symbol admission now compares bindings, not export names. Each public export resolves to its declaring module and local name, so re-exporting an existing binding under a new alias no longer counts as a new public export. The AppShell closure stays outside the root zones, but its feature-entry uses are now visible: --report lists them per file, and a --base run prints each one a change adds, reported and never ratcheted. Where a hook-gate entry has several call sites, each retained-root row must name an identifier of exactly one of those calls in app-shell.tsx, and no two rows may name the same call. The two useEffect rows now name setWorkHubEnabled and defaultHostConnections. Generated-by: Claude Opus 5.5
|
Follow-up
Architecture tests: 165/165. Posted by Claude Code on behalf of the PR author. |
Astro-Han
left a comment
There was a problem hiding this comment.
Reviewed 4dbfc2c199ef0de45c50d864fe2218140a4951b7 (16 files, +1795/−39, 2 commits).
The P3 I raised last round is fixed, verified in the code rather than from the reply. My finding was that admission keyed on the exported name, so export { ExistingThing as NewName } from './same-module.js' counted as a new public export. This head introduces a resolved-binding model: a bindings map plus bindingOf(file, name, active), documented as "where the runtime binding exported as name is declared … every alias of one binding shares this key", following re-exports and namespace imports with an active cycle guard. An alias of an export the base entry already had therefore resolves to the same binding and is rejected — and there is a fixture that pins exactly that, "take an existing export through a new alias", asserting a non-zero status under --base --strict-base.
My second finding is resolved as report-only, which I am recording as the deliberate partial resolution it is. The gap was that only the sixteen top-level AppShell-family files counted as appShell, so closure files rendering root regions went unrecorded. The reply says this is addressed by reporting rather than gating, and the code matches: the check now reports the AppShell closure's feature-entry uses per file and prints, for a --base run, each one a change adds, while keeping those files out of the root zones so legacy-to-entry edges stay free. The gap is now auditable instead of silent, but it is not enforced — worth knowing when reading the report.
Checked for this card, with the rest of the set's themes deferred to their own PRs. test is green, and mergeable is true against the base branch, so this PR can still land on its own. The other items in the dispatch's list (knip breakage, getRunningTurnId coverage, silent degradation when a provider is missing) do not belong to this checker-only change; I will take them where they apply.
What I could not judge
- I did not execute the checker or its fixtures; I verified the binding-resolution implementation, the fixture's existence and its assertion, and the report path by reading them.
- The 66-in-24-files closure figure the reply quotes is theirs; I did not reproduce it.
I did not approve, request changes, or merge.
Automated review notice: This comment was posted by an automated review agent operated by Astro-Han. It is not an independent human review and does not replace one.
apache#5934 added the retained-root table and the rootSymbolUses record. This PR removes nine root call sites: - useTaskSubmissionReadiness and useNewTaskChoice; - three useStableActions calls (chat, turn and revision actions); - four useState calls (newTaskSendPending, newChatPlanModeActive, newChatOrchestrationMode, revisionDraft). Their rows are deleted, and the rows whose consumers moved to the Composer submission owner are corrected. rootSymbolUses is regenerated. It gains the five new owner, provider and command-handle exports and drops nine stale uses. Generated-by: Claude Opus 5.5
Brings in apache#5934 and apache#5935. Conflicts were only in generated files: - renderer-architecture.json: taken from main's copy, the workspace-projection ownership entry removed again, and regenerated. The new rootSymbolUses records one admitted appShell use, features/diagnostics: ManualDiagnosticReportConsumer (new export). - docs/astryx-surface-file-inventory.md: regenerated from main's copy. With apache#5935 on main, the legacy src/renderer/session-workspace-errors.ts lost its last importer (the deleted app-shell-project-actions.ts), so it is removed here; knip reports no unused files. Generated-by: Claude Code
…nds (#5951) `e2e/session-workbar.spec.ts:179` ("Terminal survives navigation and reload…") is flaky on main. It always fails at line 224: after `page.reload()`, the owner Session's right Workbar comes back collapsed, so the terminal region never appears. This is a product bug, not a harness problem. A reload can drop every other Session's per-Session Workbar visibility. The cause is a race at startup. After a reload, a `sessions:changed` event from a Session whose turn is still finishing (the test does not wait for the replacement Session's turn to end) is read by the patch drain. `catalog.commitPatch` commits it before `bootstrapSessions()`'s `sessions.list()` does. `commitPatch` moves the catalog `revision` off 0, and `selectAuthoritativeSessionIds` read any `revision > 0` as a membership observation. It therefore published a one-row set. `useWorkbarLayoutState` dispatched `retain-sessions` with it. No Session is active yet at that point, so every other Session's `collapsedBySession` entry was dropped, and the right-visibility effect persisted the loss to `maka-session-workbar-collapsed-v2`. The rate went from 9/40 at 8ad836c to 19/40 at 255ae23 (Fisher p = 0.034) but the root cause is older. Row patches have been able to land before the list since #5532. The range 8ad836c..255ae23 changes nothing on the catalog, Workbar layout, main, preload or runtime path, so #5934/#5935 only moved the timing. Generated-by: Claude Opus 5.5
After #5934–#5937, `npm run check:renderer-architecture -- --report` still listed two retained-root rows scheduled for M5. M5's fourth item, "remove migrated legacy exceptions, public raw-state exports and redundant props/helpers", and its fifth, "document every retained root projection/lifecycle with its actual consumer, owner and allowed capability", were also open. This PR closes all three, in five commits: | Commit | What changes | Who loses what | |---|---|---| | `61cad0bcc` | `OnboardingConnectionSeed`, a render-null watch beside the onboarding authority, seeds the default Host's connection projection from each accepted snapshot, or asks it to refresh when onboarding cannot be read. | `AppShellContent` loses its last `useEffect` and the M5 row for it. | | `e485fa592` | `useAppShellProjectContext` stops returning the default Host's project list, the Local Host's projects and the raw selected id. It also drops the Local Host project subscription (`projects.getLocalSnapshot`, `subscribeLocalChanges`), which nothing has read since #5937 moved project mutations to Task Entry. Its row moves from "M5" to "application lifecycle" (see below). | `use-project-context.ts` loses 2 bridge paths and 1 effect. | | `eae997042` | Props nobody reads are removed (details below). A type-level test pins the trimmed contracts. | AppShell stops computing 3 chat-model values and importing `ProviderLogo`. | Generated-by: Claude Opus 5.5
Summary
Refs #4582 (M0 items 1, 2 and 4; M5's completion measure).
This is the R2 enforcement PR. It adds the checker rules the "Completion measure (M3 + M5)" in #4582 needs, so later slices shrink recorded numbers instead of arguing them. It should merge before the remaining M3/M5 slices.
1. Root symbol uses (M0 item 2)
Allowing an import of a feature public entry does not make every export appropriate for the root.
renderer-architecture.jsonnow has a generatedrootSymbolUses: per feature public entry, the runtime symbols each root zone takes from it.The root zones are:
appShell: the 16 AppShell-family files.composition:composition/**.bootstrap:bootstrap/**plus the guardedmain.tsxandapp.tsx.At
c7fa6bb6athe record holds 89 symbols across 18 entries: 66 forappShell, 23 forcompositionand none forbootstrap.How uses are attributed:
The rule reuses the checker's module graph. It attributes named and default imports, static namespace members (
NS.x,NS.x(),<NS.X>) and named re-exports.It follows
export { x } from, re-exported imports andexport *through any intermediate module until it reaches a feature public entry. Two uses the old analysis could not see are now recorded:NEW_TASK_PENDING_KEY, whichapp-shell.tsxreads throughpending-items.ts, anduseAppShellSessionUiReads, read throughuse-app-shell-session-ui-reads.ts. JSX-only uses are counted too.Uses whose symbols cannot be attributed are violations rather than records:
import x = NS.y, re-exported or rendered whole;export *orexport * asover an entry, directly or through a barrel;import()orrequireof an entry.There are 0 of these on
maintoday.Type positions are not recorded. Deep imports stay with the existing zone rule.
How the record is maintained:
--writeregenerates it, and the tree must match it exactly.export *resolved. Exports are compared by resolved binding (declaring module plus local name), so a new alias of an export the base entry already had is not new. Since 2026-09-15, 39 of the root's 40 symbol additions were new exports.appShell. A use may move one way, fromappShellintocompositionorbootstrap, whenappShellgives it up in the same change. This follows the existing one-way move from the AppShell closure to the root closure. A copy or the reverse move fails.2. Retained-root hook table (M5 completion measure)
apps/desktop/src/renderer/README.mdnow has a table betweenretained-root-hooksmarkers.How the checker enforces it:
ALLOWEDliteral with Babel.scripts/check-app-shell-hooks.mjsis neither run nor edited.app-shell.tsx: a binding it declares, or an identifier in its arguments. No two rows may name the same call. A row therefore cannot drift onto anotheruseStateoruseStableActionscall.Current classification:
useEffects (WorkHub enablement and the onboarding seed),useOnboardingSnapshotandworkHubEnabled.useAppShellSessionUiReads,useTaskSubmissionReadiness,useNewTaskChoice, and the new-task, send and Plan/orchestration state;revisionDraft;useShellChatModel,useShellResume,useShellLiveTurn,useActiveExecutionBoundary,useSessionSettingIntent,useAppShellTurnPresentation,useTurnActionRegistry.Calls that review may contest:
useAppShellSessionWorkspaceis kept as navigation, for Session selection and the catalog. Its transient and interaction commands leave with M3.useShellConnectionscalls are kept as application lifecycle. Host connection fan-out stays outside R2.useAppShellHostEffectsis kept as layout. Itsapp.inforead still has to move behind an adapter.With this PR, the issue's "AppShell hook-gate entries without a retained-root row" goes from 28 to 0.
3.
--reportcheck-renderer-architecture.mjs --reportprints the measures #4582 tracks:app-shell.tsx, with a per-file breakdown;A
--baserun also prints each closure use a change adds. The closure is not a root zone, so this is reported and never ratcheted.It only reports. These numbers fall over several PRs, and the existing no-growth ratchets already stop them rising.
4. Transitional exports (M0 items 1 and 4, outside Conversation)
export *outsidefeatures/conversation.overlays:createAgentGraphPanelModel,reduceAgentGraphPanelModelandshouldShowAgentGraphPanelmove totesting.ts.session-navigation:SessionHistoryNavigationwas already intesting.ts; the story now imports it from there.workhub:workHubLinkedWorkmoves totesting.ts.connection-settings:providerRequestUrlPreviewmoves to a newtesting.ts.startWorkHubCoordinationLifecycleand its host-change type: the test now imports them from their application contract.What this does not enforce
These are static rules. They do not prove runtime semantics.
renderComposerMentionsProviderfromcomposer-mentions.tsx, so the record has no entry forComposerMentionsProvider.--report, and new ones on--baseruns), not ratcheted.rootSymbolUses.validateMonotonicDebtremain uncovered (existing README caveat).Verification
Head
4dbfc2c19on main1a66e4d5e(it merges cleanly intoab5996bdb), Node 24.19.0, after a cleannpm install, the dependency patches, Electron install andbuild:workspace-deps. The Desktop suite, typecheck and Knip ran onc7fa6bb6a. The rebase over #5847 (Storybook pixel-diff scripts only) and the review follow-up4dbfc2c19(checker, its tests and README only) were re-checked with the architecture tests, the strict-base run,lint,format:check, Knip and the hook gate.--base … --strict-base --report.main's checker in place (plus empty stubs for the two new exports so the test file loads), 35 of the 37 new tests fail. The two that pass assert the absence of violations: the accepted table, and no rule without a gate. The follow-up's new tests fail on641982ed9's checker, except the one that accepts a correctly bound table; there the alias step passes where it must fail. The checker was restored and compared byte for byte.check:renderer-architecture -- --base 1a66e4d5e --strict-basepassed, cross-checked under the base checker.--writeleaves the ledger unchanged.build:test+test:distgave 3,143/3,143. That includes the five test files whose imports moved (47 tests).typecheck(root plus Desktop preload/main/renderer/storybook; the story's import changed)lintandformat:checkapps/desktopandpackages/uicheck:asf-headers(the newtesting.tscarries the header)windows:inventoryandastryx:surface-inventorycheck:app-shell-hooks(28 hooks / 42 call sites, file untouched)Merge notes
--writeand deletes the matching retained-root rows in the same change.appShell.AI use
Tool(s) and scope: Claude Opus 5.5 (in Claude Code) implemented these checker rules, the retained-root table and transitional export list, the test-only export moves, tests, documentation and validation under human direction. The commit carries
Generated-by: Claude Opus 5.5.Checklist
Does this PR entail a change in behavior?
Yes. CI now fails when:
appShell);rootSymbolUsesand the tree differ;Seven test-only exports are no longer on production public entries. No product or runtime behavior changes.
No