Skip to content

fix(console): the docs portal's book sidebar keeps a doc placed by its own group key from outside the book's package (objectui#11245) - #11274

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-11245-book-sidebar-explicit-group
Sep 30, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-11245-book-sidebar-explicit-group

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11245
Clause-②: no

What

scopeDocsToBook in apps/console/src/pages/book-nav.ts now keeps a doc whose own group names one of the book's group keys, whatever package the doc lives in. Everything else about the scoping step is unchanged. When the book declares a package, docs from other packages are still dropped before resolving, so they never reach the synthetic Uncategorized group or an include rule. include is still scoped by the local resolveBookTree port itself, to group.package or else the book's package. resolveBookTree, homeBook and the direct-route refusal are not touched. There is no new helper. DocPage.tsx, BookPage.tsx and countBookDocs pick up the fix through the helper they already call.

Why

The spec's resolveBookTree (packages/spec/src/system/book.zod.ts in objectstack, byte-identical at the @objectstack/spec@17.5.0 tag this repo installs and on objectstack main) places a doc in a group by d.group === group.key with no package scope. GET /meta/book/:name/tree answers that resolver over every doc (createMetaBookTreeAnswer → readableTree, with bookPackage = book._packageId). The portal dropped the doc before resolving, so a doc an admin placed from the doc editor was listed by the endpoint and missing from the sidebar.

Measured (the order's Zone 2)

These readings come from a scratch suite that ran the port and the spec resolver on the same fixtures. The suite was not committed.

  1. Red first. Before the fix, through scopeDocsToBook → port, the book gave start=[docprobe_gs_welcome] reference=[]. The spec resolver on the endpoint's inputs gave reference=[live_guide,probe_curl]. After the fix the two agree.
  2. The port's own scope. The PM's assumption is partly falsified. With the scope passed and no pre-filter, the port matches the spec on the explicit-group leg. But neither the port nor the spec scopes the orphan catch-all. Both put another package's ungrouped doc into Uncategorized: uncategorized=[other_note] from each. So "stop pre-filtering and pass the scope" would break the ruled control. The fix therefore keeps the pre-filter and lets explicit members through it, as the ruling words it: "scopeDocsToBook keeps any doc whose group names one of the book's group keys". The portal's scoped Uncategorized is a deliberate, ruled narrowing of the endpoint's. The pins compare the authored groups with the spec's answer.
  3. Group-level package override. In both resolvers the override replaces the book's scope for that group's include; it does not add to it. Fixture: own gets only package a, ext gets package b through include plus c_1 through its own group. The portal's answer equals the spec's, and one pin holds it.
  4. homeBook / findBookContainingDoc. Nothing changes for a book that carries a stamped packageId. homeBook keeps only books of the doc's own package, and that package is already in scope for them. One edge case moves. The book must carry no stamped packageId, its name prefix must equal the doc's package, and its only package must be a group package override. If such a doc names one of that book's groups, it is now claimed by that book. homeBook then answers that book instead of the package's first book (the measured fixture moved from q_other to q_manual). That follows homeBook's documented rule (the home is the book that claims the doc), and homeBook was not changed.

Divergences beyond the explicit-group leg, reported and not widened:

  • An unstamped doc under a scoped include. The spec admits a doc with no packageId into any scoped include. The port falls back to the name prefix (pkgOf) and refuses it. Measured: port all=[x_a], spec all=[x_a,y_unstamped].
  • A pages pin naming a doc from another package. The pre-filter drops that doc's header, so the entry keeps its place but loses its label and description. Measured: port {"doc":"c_9"}, spec {"doc":"c_9","label":"C9 Label"}.

Tests

  • book-nav.explicitGroup-11245.test.ts: the explicit member is listed in the group it names; the authored groups equal the spec resolver's answer (@objectstack/spec/system) for the same docs; countBookDocs counts it; the group-override case; control: another package's ungrouped doc, and one naming a key the book lacks, stay out, with no Uncategorized.
  • docs-portal.explicitGroup-11245.test.tsx renders through the real route table. The Reference group of the rendered sidebar links the placed doc, and marks it current when reading it. A book whose only member is placed that way opens to it instead of "This book has no documents yet". Control: another package's ungrouped doc is absent from the sidebar.
  • Reverse verification, run from the committed fix through ablation-replace.mjs with a disk-verified mutation and restore:
    • Drop the explicit-member leg: 7 of the 9 new tests go red and both controls stay green.
    • Drop the package filter entirely: both new controls go red, plus three existing docs-portal.test.tsx cases (that is the existing pin the triage names).
    • Each restore was proven by blob equal to HEAD and an empty git diff HEAD.

Gate verdicts and the head they ran on are in the os-dev-report comment on the card.

Acceptance notes

  • homeBook edge case (reading 4 above): the canonical URL moves for a doc placed into an own-package book that has no stamped packageId and a group-level package override.
  • The two port divergences above: pkgOf falls back to the name prefix on an unstamped doc, and a pages pin from another package loses its label. Carrier: none.
  • A common group key such as reference places a doc in every book that has a group with that key. The spec resolver and the tree endpoint do the same; the pins do not cover it.

Generated by Claude Code

…key from outside the book's package (objectui#11245)

`scopeDocsToBook` dropped every doc outside the book's packages before
`resolveBookTree` ran, so a doc whose own `group` names one of the book's
group keys was missing from the sidebar while `GET /meta/book/:name/tree`
(the spec resolver: explicit `group` carries no package scope) listed it.

It now keeps such a doc whatever its package. The package filter still
narrows what `include` and the synthetic Uncategorized group can collect.

Pins: the helper answer against the spec resolver's for the same docs, the
count, a group-level `package` override, the Uncategorized control, and the
rendered sidebar / book landing through the real route table.

Claude-Session: https://claude.ai/code/session_0122Knsowci76D2rBWReCzzZ
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 330 chunks) 3583.7 KB 3607.4 KB
Main entry chunk (gzip) 149.9 KB 350 KB
Entry file index-CQnWmwR8.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 16.88KB 6.25KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.17KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.70KB 10.94KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.13KB 7.95KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 570.05KB 136.37KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 228.86KB 63.53KB
fields (index.js) 261.11KB 66.26KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.24KB 2.27KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.40KB 12.91KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 34.35KB 9.18KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 41.23KB 11.53KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.01KB 3.93KB
plugin-calendar (index.js) 52.17KB 15.06KB
plugin-charts (index.js) 84.09KB 22.93KB
plugin-chatbot (index.js) 198.22KB 46.97KB
plugin-dashboard (index.js) 138.72KB 37.05KB
plugin-designer (index.js) 216.32KB 44.56KB
plugin-detail (index.js) 244.72KB 64.45KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 173.61KB 44.59KB
plugin-gantt (index.js) 173.03KB 43.07KB
plugin-grid (index.js) 231.25KB 63.50KB
plugin-kanban (index.js) 48.46KB 15.12KB
plugin-list (index.js) 116.42KB 28.89KB
plugin-map (index.js) 23.50KB 7.82KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.04KB 12.21KB
plugin-timeline (index.js) 33.05KB 9.67KB
plugin-tree (index.js) 11.20KB 3.89KB
plugin-view (index.js) 90.32KB 22.76KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 119.55KB 39.23KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.25KB 2.04KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 6.06KB 2.68KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 21.42KB 7.05KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 3.19KB 1.62KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.26KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.82KB 7.15KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 24b3255af0f160ec32e01e2770531d591ff2ef64
Local-runs: none

Inputs read: card objectui#11245 (body; comments 5917816890 triage, 5919539369 claim, 5920033027 dev report), PR objectui#11274 (body, its 4-file list, the net diff against the merge-base d6a1a80d5d), the 42 check-runs on the head; read-only for context, resolveBookTree in packages/spec/src/system/book.zod.ts at objectstack origin/main, PR objectui#11258 (merged, homeBook) and card objectui#11197 (bookNamedBy).

① Derived judgments

The diff is one hunk in apps/console/src/pages/book-nav.ts (scopeDocsToBook keeps a doc when pkgs.has(pkgOf(d)) OR the doc's own group names one of the book's group keys), two new pin files and one changeset. Each derived judgment, named right or wrong:

  1. Explicit group membership passes the package pre-filter: right. The spec's resolveBookTree claims a doc by d.group === group.key with no package scope; only matchesInclude reads the scope. The fix admits exactly that leg. resolveBookTree is not re-implemented (the local port predates the card), there is no second scoping helper (an inline groupKeys set inside the existing function), and homeBook, the direct-route refusal and the app-menu guard are untouched: every ⛔ in the claim holds. The file surface is the claim's (book-nav.ts, its pins beside the existing docs-portal pins, one changeset); no caller changed.

  2. The route deviation from triage's suggested route: right, and the measurement is verified at the spec source. Triage's "stop pre-filtering and pass the scope" was measured and falsified by the dev for the orphan catch-all. Confirmed by reading: the spec's orphan step is docs.filter over !claimed.has(d.name) with no scope argument, and the port's is the same line, so without the pre-filter both put another package's ungrouped doc in Uncategorized, which breaks triage's second pin. The include scope (group.package ?? bookPackage) never bounds orphans. Keeping the pre-filter and admitting explicit members is the only route that satisfies both pins without re-implementing the resolver, and it is the literal execution of triage's first sentence (the package filter only narrows what the synthetic Uncategorized group and include can collect). Stated in the PR (Measured 2) and in the report's deviations.

  3. The portal's Uncategorized is narrower than the endpoint's: right and ruled. The endpoint's synthetic group lists another package's ungrouped doc; the portal's does not. That is triage's second pin (another package's ungrouped docs stay out of Uncategorized, the existing pin), and the PR names it as "a deliberate, ruled narrowing of the endpoint's". The pins compare the AUTHORED groups of the port (after scopeDocsToBook) with @objectstack/spec/system's resolveBookTree over every doc with bookPackage = book.packageId, the endpoint's inputs. For the case the card measured (a doc placed by its own group from outside the book's package) the authored groups are equal, so "matches the tree endpoint's answer" is satisfied where the card asks it; the one remaining difference is the ruled one.

  4. A doc naming a pages-override group's key from another package: right, unstated. It now passes the scope; unless the override pins it by name or carries ..., the port leaves it unclaimed and it lands in the portal's Uncategorized. The spec does the same, so the sidebar matches the endpoint there too; it is a grouped doc, so the second pin is not touched. Not stated in the PR; a consequence of the rule, not a defect.

  5. Common group keys: right, stated. A doc with group: 'reference' now joins every book with a reference group, in any package; the spec and the endpoint place it the same way, and the PR's third Acceptance note says so. The widest instance is the implicit per-package book, whose one group is keyed all in both deriveImplicitPackageBooks: a doc with group: 'all' joins every implicit book's group. Within the stated note.

  6. The homeBook edge: right, inside the documented rule, stated. homeBook still filters ownBooks to the doc's own package (pkgOfBook(b) === pkgOf(doc), unchanged) and prefers the one findBookContainingDoc finds by authored membership, which is what this diff widens. The only book whose answer can move carries no stamped packageId (the API stamps it, per the Book.packageId docblock, so this is the pre-stamp fallback), a name prefix equal to the doc's package, and only a group package override, and only for a doc naming one of its groups: that doc's home moves from the package's first book to the book that claims it, which is PR objectui#11258's rule ("prefers, among the doc's own package's books, the one that claims it, else that package's first book"). What moves is the flat-permalink redirect in DocsSlug (/docs/NAME to /docs/HOME/NAME). A bookmarked in-book URL keeps resolving: DocPage fetches the doc by name and renders the slug's book, so /docs/q_other/NAME still opens with q_other's sidebar. The objectui#11197 redirect is bookNamedBy (a book NAME to its slug), which this diff does not touch. Stated in the PR (Measured 4; Acceptance notes 1).

  7. Port divergence 1, an unstamped doc under a scoped include: right as an Acceptance note; reach not readable within this review's inputs. Pre-existing (pkgOf's name-prefix fallback predates the card), not widened by the diff. Read at source: the fallback yields the segment before the first _, which never equals a dotted package id, so IF GET /meta/doc returns a runtime row without _packageId, every such doc a scoped include glob matches is dropped by the portal and listed by the endpoint: the card's axis, on the include leg. Whether the list read returns unstamped rows is an objectstack read-layer question outside this review's inputs, and the card's own probe does not distinguish it (the fix admits live_guide through the group leg whether it is stamped sys_metadata or not; the two new fixtures model both). Under the filing gate this stays an Acceptance note; one list read on the 17.5.0 backend after a runtime save decides its reach. Escalated to the seat as a reach measurement, not a filing.

  8. Port divergence 2, a pages pin naming another package's doc: right as an Acceptance note. Pre-existing. BookSidebar renders entry.label ?? entry.doc, so the visible effect is the machine name where the label would be; the entry keeps its place, its link and its count, and DocPage fetches it by name. Reach needs an authored book pinning a cross-package doc by name, unmeasured. Right under the gate; not fileable on measured reach.

  9. The rendered book-landing pin: real, through BookPage. /docs/notes_manual renders DocsSlug (the slug is a book), which renders BookPage, which computes firstDoc(resolveBookTree(found, scopeDocsToBook(found, docs))). notes_manual (_packageId com.example.notes, one group notes, no include) scoped live_note (_packageId sys_metadata, group: 'notes') out before the fix, so opensTo was undefined and the "no documents yet" state rendered; after it, live_note is claimed and Navigate lands on /docs/notes_manual/live_note. The test asserts the rendered body and the location, through the real DocsLayout / DocsSlug / DocPage routes with use-book-data mapping _packageId to packageId. The 9-test count (5 unit + 4 rendered) and ablation A's 7 red / 2 green (the two controls) are consistent with the files.

  10. countBookDocs and the book cards now count explicit members from other packages, as the endpoint's authored groups do; pinned. A group-level package override replaces the book scope for that group's include in both resolvers (group.package ?? scopeDefault in the port, group.package ?? bookPackage in the spec); pinned against the spec. Right.

  11. Public surface: none. book-nav.ts is app-internal to @object-ui/console (the package's exports is . to ./plugin.js); the only reference outside apps/console is a docblock line in packages/layout/src/NavigationRenderer.tsx. No schema, prop type or package export changes.

  12. Base drift. origin/main is now 5 commits past the merge-base (the report said 3 at its time); none touches apps/console/src/pages/ (an empty git diff --stat on that path), so the fix and its pins meet no conflicting edit. The PR reads mergeable: true, mergeable_state: behind; the queue rebuilds on main.

② Semver level

.changeset/11245-book-sidebar-explicit-group.md declares '@object-ui/console': patch. @object-ui/console is a released package (17.6.0, not private, exports . to ./plugin.js), and the diff is a bug fix in it with no export, prop type or schema change: the level objectstack AGENTS.md's Post-Task Checklist item 3 requires (a bug fix in a released package takes patch, never skip-changeset). The PR body carries Clause-②: no with no arm, matching the claim's Clause-②: no and the diff: nothing widens what any schema or prop accepts; the console lists a doc the spec's resolver and the tree endpoint already place. The changeset paragraph's claims are each pinned (the sidebar, the count, the "no documents yet" landing, the override scoping, the ungrouped control). The changeset gates on the head (Bump Policy, Declaration, Claim Re-read, Fixed Group Check, Overwrite Report) are all success.

③ Boundary flags

  • open_questions: []. Nothing to answer.
  • Deviation 1 (the route): answered in ① 2, right, verified at the spec source.
  • Deviation 2 (Zone 3 plus one, the rendered landing pin): answered in ① 9, real.
  • Deviation 3 (main moved, no rebase): answered in ① 12, no overlap.
  • Deviation 4 (attribution trailers): the head commit ends in the session-URL trailer plus a Co-authored-by line, the pair objectui AGENTS.md asks for. Not a contract matter.
  • Deviation 5 (worktree removed): housekeeping, nothing to judge.
  • out_of_scope_findings[0] (unstamped include): ① 7, the Acceptance note is right; escalated to the seat as one reach measurement before any filing.
  • out_of_scope_findings[1] (pages pin label): ① 8, the Acceptance note is right; not fileable on measured reach.
  • out_of_scope_findings[2] (homeBook edge): ① 6, inside the documented rule and stated in the PR.
  • PR Acceptance note 3 (common keys): ① 5, within the spec; the implicit book's all key is named here as the widest instance.
  • Check-runs on the head: 42 in all; 28 completed success (Build & E2E, Test (dist pins), Docs Route Eager Closure Check, Bundle Analysis, the five changeset gates, Line Citation Gate, Control Byte Scan, Governed Surface Queue Guard, Live E2E (informational), and the doc, skill and convention checks); 3 skipped (dependabot, Test (coverage), Test (coverage shard)); 0 failure; 11 still in_progress and NOT concluded: Test (shard 1/8) through Test (shard 8/8), Lint, Type Check, Spec Main Shape Gate. They are read as neither pass nor fail here; landing waits on their conclusions. The dev's local gates (build closure, console type-check, 54 files / 387 tests, ESLint on the 3 changed files, the repo checks) are the report's, not this review's.
  • Governed surface: none (Governed Surface Queue Guard is success; no skills/**, ADR or governed path in the file list).

Implemented-by: claude/issue-11245-book-sidebar-explicit-group
Reviewed-by: session_0122Knsowci76D2rBWReCzzZ

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 30, 2026 21:42
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 30, 2026
Merged via the queue into main with commit f16c01e Sep 30, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-11245-book-sidebar-explicit-group branch September 30, 2026 21:56
huangyiirene pushed a commit that referenced this pull request Sep 30, 2026
Carries main through PR #11267, #11268, #11269, #11271, #11273 and #11274.
No conflicts.

Claude-Session: https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh
Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Oct 7, 2026
…, mirror two chatbot keys, file eighteen runtime slots by name (objectui#6152, round 4) (objectstack-ai#11285)

Refs objectstack-ai#6152
Clause-②: yes (narrowing). The two mirrors widen `@object-ui/types/zod`
and the strict authoring face to keys the TypeScript face already
invited and the renderers read (`requestBody`, `floatingConfig`). The
four retirements (`label.content`, `report.chartConfig`,
`report.reportType`, `detail-view.autoDiscoverRelated`) narrow both
faces at once, under the startup no-gradualism rule. It is priced in
`.changeset/6152-unmirrored-round4.md` as `minor`, with the break
stated.

Round 4 of objectui#6152, dispatched under claim `5919877490` (the
`domain:devx` seat 2, session `session_01TdiauJaVCHuj45EzZGUxHh`, on the
maintainer's direct order). The rulings are the seat review
`5914490915`: Q1 B, Q2 A, Q3 B. ⛔ This is a draft from a dispatched dev:
do not mark it ready and do not enable auto-merge. The card stays open,
because `ObjectGridSchema`, the spec-derived pairs and three
`DataTableSchema` keys (below) are not this round's.

## What changed

1. **Q1 B: `LabelSchema.content` is retired.** It is `?: never` on
`form.ts` and a `retirementTombstone` on `form.zod.ts` that names the
kept spellings `text` and `label`. The `label` renderer drops its third
read limb (`schema.text || schema.label`). Re-measured: 0 documents
author it (JSON, Markdown JSON fences and typed literals); its only
author was a test in `form-renderers.test.tsx`, which now pins the
refusal. Which of `text` / `label` is canonical is not decided here.
2. **Q2 A: `DetailViewSchema` `activities`, `comments`, `history` and
`recordNavigation` go to `RuntimeOnlyDeclared` by name.** Each has a row
with its reason on `RuntimeOnlyNamedAllowList`, in round 3's shape. They
carry host-fetched runtime data, and `recordNavigation.onNavigate` is a
required function.
3. **Q3 B, under objectui#7654 ruling B: `displayMode` keeps its
TypeScript-only refusal.** It gets no zod arm, because stored designer
documents carry `displayMode: 'floating'` (pinned by
`chatbot-display-mode-retired.test.ts`). The ledger entries no longer
call the mirror half "owed"; the key stays listed by decision.
Separately, after a reader census for each:
- `ChatbotSchema.requestBody` is mirrored through the shared pick. All
three chatbot registrations read it (`body: schema.requestBody`).
- `ChatbotFloatingSchema.floatingConfig` is mirrored through a
module-private restatement of `FloatingChatbotConfig`. That restatement
judges the six members `FloatingChatbot` destructures, and carries the
`triggerIcon` tombstone's runtime half that the objectui#7654 tripwire
asked for.
4. **The three zero-read candidates are retired on both faces.** They
are `ReportComponentSchema.chartConfig`,
`ReportComponentSchema.reportType` and
`DetailViewSchema.autoDiscoverRelated`, each `?: never` plus a
`retirementTombstone`. `reportType`'s one in-code producer,
`specReportToPresentation` in `spec-report.ts`, no longer writes it, and
`LegacyReportPresentationLike` no longer declares it.
5. **`DataTableSchema`: 14 of 17 keys go to `RuntimeOnlyDeclared` by
name.** They are produced only in code, by `ObjectGrid`'s in-code
`data-table` node or `RelatedList`'s, and authored by no document. Three
keys fit none of the classes, so the round stopped at them (see
"Stopped" below).
6. **The pagination corpus fix, carried from round 3.**
`components-basic-pagination/with-item-count.json` no longer authors
`pageSize` / `totalItems`, which neither face declares and nothing
reads. The plaintext interface block on
`content/docs/components/basic/pagination.mdx` stops listing them, and
its section is retitled from "With Page Size" to "On a Later Page".
7. **Changesets.** One new: `.changeset/6152-unmirrored-round4.md`
(`@object-ui/types` minor, `@object-ui/components` minor, with the break
stated). Four dated, append-only corrections were added, each 2 lines
with frontmatter unchanged: `6639-objectgrid-title-mirrored.md` (the
stale "97 + 1 + 23" sentence),
`7654-floating-chatbot-trigger-icon-tombstone.md`,
`7655-chatbot-registration-authoring-faces.md` and
`9447-detail-view-handler-key-refusal.md`. Every other pending changeset
was re-read after the merges, including the 8 that landed since the
base, and none is made false by this round.

## Ledger totals, before and after

These are read by the file's own pins (`objectui#7279` derives every
figure) and re-counted with the TypeScript parser on the final head.

| ledger | before (`origin/main` `e420df3`) | after (`2cc4ab9`) |
| --- | --- | --- |
| `UnmirroredDeclared` | 9 entries / 44 keys: 3 / 14 spec-derived, 6 /
30 local | **6 / 20**: 3 / 14 spec-derived, 3 / 6 local |
| `RuntimeOnlyDeclared` | 4 / 9 | **4 / 27** |
| `RuntimeOnlyNamedAllowList` | 1 entry / 2 rows | 3 / 20 |
| cross-ledger | 2 of the 4 are in `UnmirroredDeclared`; union 11 = 9 +
2 | **1 of the 4**; union **9** = 6 + 3 |
| `KnownDrift` | 49 / 86 | 49 / 86, unchanged. This round does not edit
its total line; PR objectstack-ai#11269's 87 to 86 edit is kept as merged. |
| `WiderThanDeclared`, `MirroredUndeclared` | 3 / 3, 2 / 2 | unchanged |

The 24 keys that left `UnmirroredDeclared` moved by three routes, which
must not be added up as one repair: 2 were mirrored, 4 were retired, and
18 were reclassified by name. The reclassification is one move seen from
both ledgers: it is not new debt, and it mirrors nothing.

## Per-key table

| pair · key | measured (type-checker read census over 5548 sources;
authored census over tracked JSON, Markdown JSON fences and `type`
literals) | route |
| --- | --- | --- |
| `label` · `content` | 1 read (the third limb in `label.tsx`), 0
documents (1 test) | retired on both faces; the read is dropped |
| `report` · `chartConfig` | 0 typed reads. The untyped
`widget?.chartConfig` hits are plugin-dashboard's `DatasetWidget`
(`widget: any`, a dashboard widget), which plugin-report never imports.
0 documents | retired on both faces |
| `report` · `reportType` | 0 reads; one in-code producer
(`specReportToPresentation`); 0 documents | retired on both faces; the
producer write is removed |
| `detail-view` · `autoDiscoverRelated` | 0 reads (typed or untyped), 0
documents | retired on both faces |
| `chatbot` · `requestBody` | 3 reads (all three chatbot registrations)
| mirrored (shared pick) |
| `chatbot-floating` · `floatingConfig` | 1 read
(`floatingConfig={schema.floatingConfig}`); 6 of 7
`FloatingChatbotConfig` members read, and `triggerIcon` 0 | mirrored,
with a `FloatingChatbotConfig` restatement |
| `chatbot`, `chatbot-floating` · `displayMode` | 0 reads; stored
documents carry it | unchanged by ruling (TypeScript-only refusal) |
| `detail-view` · `activities` (7 reads), `comments` (4), `history` (7)
| host-fetched rows; 0 documents | `RuntimeOnlyDeclared` by name |
| `detail-view` · `recordNavigation` | 11 reads; its value carries a
required function | `RuntimeOnlyDeclared` by name |
| `data-table` · `disableInnerScroll`, `manualPagination`,
`manualSearch`, `manualSorting`, `page`, `rowActionDefs`,
`rowClassName`, `rowCount`, `rowStyle`, `search`, `selectionResetKey`,
`showAddRow`, `showSelectionCount`, `sort` | read by `data-table`;
produced only in code (`ObjectGrid`, `RelatedList`); 0 docs and 0
catalog documents | `RuntimeOnlyDeclared` by name; the reasons are on
the allow-list |
| `data-table` · `editable`, `singleClickEdit` | read; produced in code
by `ObjectGrid`; 0 documents, but `data-table.mdx` lists both and
teaches `editable: true` | **stopped**, ruling needed |
| `data-table` · `selectionStyle` | read; 0 producers and 0 documents (0
hits in objectstack too) | **stopped**, ruling needed |

## Runtime probe for the three zero-read retirements

AGENTS.md requires more than a source census, because leftover keys
reach a renderer as spread props. So the probe used the real
`SchemaRenderer` and the real registry, and varied only the key. It was
a one-off: the files were deleted afterwards and `git diff HEAD` is
empty.

- `report`: the DOM is identical with and without `chartConfig` plus
`reportType` (797 = 797 characters, byte-equal). Lit control: changing
`title` changes the DOM (805).
- `detail-view`: the DOM is identical with and without
`autoDiscoverRelated: true`, after normalising React's `useId` tokens.
The only raw difference was `radix-_r_5_` against `radix-_r_d_`. The
data-source and `fetch` call log is identical:
`getObjectSchema(account)` and two `security/explain` fetches. Lit
control: changing `title` changes the DOM (5943 against 5951).

## Ablations

The predictions were written at `4c272dc` before any run, and every leg
was restored with the blob equal to `HEAD` and `git diff HEAD` empty.

- **A1:** plant a `LabelSchema` literal carrying `content` into
`label.tsx`. Components `tsc --noEmit` exits 2 with exactly one TS2322
there (`'string' is not assignable to type 'undefined'`), as predicted.
This red direction also shows the consumer read the rebuilt `.d.ts`.
- **A2:** restore the four `zod/*.zod.ts` files to base `f61dab1`.
`unmirrored-declared-round4-6152.test.ts` reads `27 failed | 6 passed
(33)`, exactly as predicted, and `tsc -p tsconfig.test.json` exits 2
with its only error in the ledger reconciliation of
`zod-mirror-parity.test.ts`.
- **A3:** rename the allow-list row `rowCount` to `rowCountRenamed`.
`tsc -p tsconfig.test.json` exits 2 with exactly two TS2344, at the two
allow-list pins, as predicted.

## Verification on the final head `2cc4ab9`

This round resumed after two container restarts, so every run below was
redone on `2cc4ab9`. That head carries `origin/main` `e420df3`, merged
twice (never rebased): once through PR objectstack-ai#11275, and before that through
PRs objectstack-ai#11267, objectstack-ai#11268, objectstack-ai#11269, objectstack-ai#11271, objectstack-ai#11273 and objectstack-ai#11274. Each run went
through the shared verify lock with at most 2 workers.

- `@object-ui/types`: `type-check` (the `--noEmit`,
`tsconfig.examples.json` and `tsconfig.test.json` legs) exits 0; `vitest
run packages/types/` gives 299 files and 7498 tests passed; `build`
exits 0 (138 emitted files verified).
- Consumers, from the type-checker census: `type-check` exits 0 with 0
TS errors for `components`, `plugin-report`, `plugin-detail` and
`plugin-chatbot`. `vitest run packages/plugin-report/
packages/plugin-chatbot/` plus `form-renderers.test.tsx` gives 78 files,
827 passed and 7 skipped. All 228 `plugin-detail` test files, run in two
halves, give 227 files passed plus 1 skipped, and 2264 tests passed plus
8 skipped.
- Zod-face consumers: `vitest run examples/schema-catalog/
packages/cli/` plus the 17 app-shell files that import
`@object-ui/types/zod` or sit in `metadata-admin/previews/__tests__`
give 77 files and 2741 tests passed.
- Gates, all exit 0: `check:changeset-claims`,
`check:pending-changeset-literals`, `check-changeset-presence` (16
source files of 2 released packages, 1 changeset),
`check-changeset-no-major`, `check-changeset-fixed`,
`check-changeset-overwrite` (report-only; its 4 findings are the 4 dated
appends above), `check:control-bytes` (9717 files), `check:doc-fences`,
`check:doc-snippets` (697 of 697 blocks judged, 0 failed),
`check:doc-examples`, `check:doc-types`, `check:doc-example-ids`,
`check:doc-example-readers`, `check:skill-examples`,
`check:handler-key-reads`, `check:component-surface-parity`
(report-only), `check:designer-field-key-parity`,
`check:element-data-source-declaration`, `check:new-line-citations` (0
new), `check:prompt-keys`, `check:readme-exports`, `check:spec-symbols`,
`check:test-path-roots`, `check:unreferenced-sources` and
`check-type-check-coverage`. `doc-snippets` and `doc-examples` ran after
the scoped build their own `--build-filter` names (35 turbo tasks, all
successful).
- Lint, narrowed: `eslint --no-inline-config --format json` over the 16
lintable changed files gives 0 errors. All 16 were judged and 0 were
ignored, a population read from eslint's own output. The 39 warnings are
pre-existing `no-explicit-any`, and the diff adds no explicit `any`.
`eslint.config.js` declares no `parserOptions.project` or
`projectService`, and no custom rule in `eslint-rules/` reads the disk,
so this diff cannot move any untouched file's verdict. The repo-wide
`pnpm lint` is CI's.
- Governed surface: `check-governed-queue-guard.mjs --test` over the 23
paths reports NOT GOVERNED.
- NOT MEASURED:
- `check:sdui-registration-pins`: it needs a console build, and this
diff changes no registration.
  - The full `pnpm test` farm and repo-wide lint: CI's.
- `plugin-grid`: its code and tests are untouched. The 14 reclassified
keys moved between two test ledgers and no declaration changed.

## Stopped (ruling needed; reported in the card report with options)

- `DataTableSchema.editable` and `singleClickEdit`. `data-table` reads
both and `ObjectGrid` sets both in code, while no document authors them.
But `content/docs/components/complex/data-table.mdx` lists both in its
interface block and teaches `editable: true`. Filing them runtime-only
would contradict that page. Mirroring them would rest on no authored
document, and, from the source, an authored `editable: true` stages cell
edits locally and persists nothing without the `onRowSave` /
`onCellChange` function slots.
- `DataTableSchema.selectionStyle`. `data-table` reads it (`'always'` or
`'hover'`), and nothing authors or produces it in either repository.
- Noted, not narrowed: `ChatbotSchema.floatingConfig` has 0 reads on the
`chatbot` node (only `chatbot-floating` reads it), which makes it an
ADR-0049 narrowing candidate. The `chatbot` half of
`floating-chatbot-trigger-icon-retired.test.ts` pins its absence as a
tripwire.

## Parallel discipline

PR objectstack-ai#11269 and PR objectstack-ai#11273 edited `zod-mirror-parity.test.ts` in disjoint
hunks: the `KnownDrift` total line, the Kanban entry and one
`EXCLUSIONS` row. Both merges were clean. This round's ledger edits stay
in the `UnmirroredDeclared` / `RuntimeOnlyDeclared` /
`RuntimeOnlyNamedAllowList` blocks, their prose totals and its own
pairs' entries. Every prose total was reconciled to the measured count
above. The diff does not touch `zod/objectql.zod.ts`,
`zod/index.zod.ts`, `zod/nested-component-walk.ts`,
`strict-authoring-face.ts`, `zod/README.md`, `field-types.ts`, the
`DetailViewField` member, or `data-table.tsx` (read only).

## Acceptance notes

- 23 files, under the ~25-file size guard, so no pair was cut.
- Every pending changeset this round made false carries a dated append.
The ones it leaves true were re-read:
`7654-chatbot-display-mode-tombstone.md`, `6150-…`, `7804-…`, `6175-…`,
`7722-…`, `7947-…` and `8072-…`.

Session: `https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh`

---
_Generated by [Claude
Code](https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants