Repository navigation
navigation: read a nav entry's declared API dataSource, so its visible sees the selected value as data (the objectui renderer half of objectstack-ai/objectstack#24427) #13029
Description
Filed by the triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015, session_01AavokzJ5DndAwitDXvKy4U). The maintainer agreed in chat on 2026-10-19 (「同意你的建议」) to the seat's lean on the 18.0 GA scope: "the backend ships as it is; file an objectui card for 18.1". ⛔ Not a claim.
Blocked-by: #12971
Filing gate ③. This is a follow-up that a landed spec card names. The spec half, objectstack-ai/objectstack#24427 (26501ae24f, Part of objectstack-ai/objectstack#24332), says in its own docblock (packages/spec/src/ui/app.zod.ts:410–:417 on objectstack origin/main fb9c22aa09): "The reader is the objectui renderer half, filed when this lands." No objectui card was filed. A search of this repository's issues (open and closed) for titles with nav × dataSource / data source found none.
What the spec now allows
BaseNavItemSchema.dataSourceisElementApiDataSourceSchema.optional(), the page element's API binding by reference:{ provider: 'api', read: { url }, path? }.- It is carried by all nine branches that spread the base:
object,dashboard,page,url,report,action,component,docandgroup.
- It is carried by all nine branches that spread the base:
- Only
visiblereads the bound value, asdata(e.g.data.total == 1).labeland the target keys take nodata.- The binding is not published to
children.
- The key is described
[PLANNED — not yet rendered].- Its liveness row is
packages/spec/liveness/app.json→navigation.children.dataSource,planned, with an author warning. - The marker leaves the describe, and the row flips to
live, when this card ships.
- Its liveness row is
What objectui does today (origin/main 4484a11041)
packages/layout/src/NavigationRenderer.tsx:682(passesNavItemGuards) evaluatesitem.visibleagainst the shell's ambient scope, which has nodatain it. The two other callers share that guard:packages/app-shell/src/chrome/CommandPalette.tsx:154;packages/app-shell/src/views/nav-menu-renderer.tsx:324.
- The page element's reader already exists:
useElementApiDataSourceinpackages/react/src/element-data-source/, used bySchemaRenderer. Reuse it rather than writing a second fetch.
Done when
- A nav entry that declares
dataSourceis shown or hidden by itsvisibleover the value the binding selects, in the sidebar, the command palette and the nav menu alike. They share one guard, so they cannot disagree. - An entry without
dataSourcebehaves exactly as today. - A failed or pending read hides the entry rather than showing it (fail closed). This matches how the page element's binding treats an unanswered read. Pin it.
- Objectstack follow-up: the
[PLANNED — not yet rendered]marker leaves the describe, and the liveness row flips tolive. That is onedomain:specchange on objectstack after this card lands. Name it in the PR.
Why p2 and not target:v18
- This is a widening reader, so it can land in any 18.x minor without breaking an application. The spec already tells authors the key is not rendered yet.
- spec / objectui navigation: an app's navigation item cannot read data, so a label or a visible gate cannot follow a live count (the control plane's 「环境」 for a single-environment organization) objectstack#24332's ruling also left the named consumer, objectstack-ai/cloud#3002's sidebar row, free to drop that nav entry instead (「如果开发复杂的话,3002 也可以删除左侧的导航项」).
Dedupe words: navigation dataSource reader, nav visible data, NavigationRenderer passesNavItemGuards data scope, ElementApiDataSource nav entry, liveness navigation.children.dataSource planned.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 19, 2026
Unlock:
pm:blocked→pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-19T22:02ZTriage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015). ⛔ Not a claim, ⛔ not a dispatch.
The condition is met by pin ancestry, not by the closure alone. objectui
origin/main's.objectstack-shaisf4755a68a797.26501ae24f, the spec half this card consumes.8016a9d9dd(feat(spec)!: a navigation entry whose visible reads data must declare dataSource, refused at parse objectstack#24490).git merge-base --is-ancestor. TheBlocked-by:card, deps(pin): move.objectstack-shafrom the next.3 release commit to an objectstackmaincommit that carries 586705d85d, 1e001c7df and e15897c0f1, and follow the contract changes since next.3 (the first pin bump under ruling B on objectui#12955) #12971, is closed.Scope is unchanged. This is the 18.1 consumer card the maintainer agreed to, so it carries no
target:v18.Sequence. objectui#13076 (p1,
target:v18, stillpm:blocked) is in the same nav mirror. Its pin does not yet carry objectstack35450f0732. If both are claimed, land them serially.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Claim: PM loop round 14 (
priority:p2)Session:
session_01XVp19E1MYUTFM3dnkwz8gBAccount:
baozhoutao(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-13029-nav-data-sourceWorktree:
objectui-issue-13029Domain:
domain:uiSeat:
domain:ui#2File surface:
packages/layout/src/NavigationRenderer.tsx(passesNavItemGuards, the one guard the sidebar,CommandPaletteand the nav menu share). An entry that declaresdataSourcehas itsvisibleevaluated with the bound value asdata. A pending or failed read hides it (fail closed). An entry without the key behaves exactly as today.packages/app-shell/src/chrome/CommandPalette.tsx,views/nav-menu-renderer.tsxandhooks/useNavMemberGates.ts.useElementApiDataSource(@object-ui/react), with no second fetch.packages/types/src/zod/app.zod.tsandpackages/types/src/app.ts: only thedataSourcedescribe and doc ("Planned in the spec; the console does not read it yet" → what the renderer now does)..changeset/13029-nav-data-source.md, at the levels the published faces moved.⛔ No new export unless the report names it and why. Stop on breach; explain in the report.
Container & model:
M,mode:subagent,model: opus(default tier; no path-derived mandate over this surface; the contract review runs at the review tier)Clause-②: yes (a declared navigation key goes from not read to read: the published layout guard's behaviour and the
@object-ui/typesdescribe change)Responsibility: n/a — not a defect card (a renderer for a spec key landed as objectstack-ai/objectstack#24427)
Thread-read: 6278361000
Serial constraints cleared: objectui#13075 (
df71568) landed in the same nav files. objectui#13076 (blocked; sameapp.zod.tsmirror) lands serially after this card or before it, never alongside. No open objectui PR touches these files (read at 22:24Z).Claimed at: 2026-10-19T22:25Z
Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
os-dev-report
{
"issue": 13029,
"status": "done",
"branch": "claude/issue-13029-nav-data-source",
"pr": "#13095",
"session": "session_01XVp19E1MYUTFM3dnkwz8gB",
"premise_still_valid": true,
"summary": "The navigation
visibleevaluator now reads an entry's declared API dataSource. Layout: VisibilityEvaluator gains an optional second parameterentry, which passesNavItemGuards passes on every path (rows, hasVisibleNavigationItems, Favorites); the default evaluator hides a bound entry (fail closed). App-shell: the seam is useNavVisibility(app) plus navEntryVisible in hooks/useNavMemberGates.ts. It reads each binding through @object-ui/react's reader module (judgeElementApiDataSource, readElementApi with the host apiFetch, selectElementApiPath) and evaluates visible over evaluator.withContext({ data }); pending, failed, refused or path-miss hide the entry. The sidebar (via useNavMemberGates, so UnifiedSidebar is untouched), nav:menu and the command palette all build their evaluator there. The types describe and doc are updated with the accept set unchanged. Published face: only VisibilityEvaluator's optional parameter; no new package export. Changeset: layout minor, app-shell minor, types patch. Objectstack follow-up (marker removal, liveness row to live) is named in the PR body, not done.","tests": "At head ef2abec. New: app-shell navEntryDataSource-13029.test.tsx runs pins (a)(b)(c)(d) across sidebar/nav:menu/palette via describe.each over the real ExpressionProvider, MePermissionsProvider and a stubbed SchemaRendererContext apiFetch; layout NavigationRenderer.entryDataSource-13029.test.tsx. Run: 2 files, 25 tests passed, VERDICT command-exit 0. Family run (every test naming nav-menu-renderer, CommandPalette, useNavMemberGates, NavigationRenderer, evaluateNavigationVisibility, NavigationItemSchema or types app.zod): 149 files, 1857 tests passed in 3 locked chunks (57/1143, 46/332, 46/382), each VERDICT command-exit 0. The full package directories are declared to CI, per the order's lock clause. Closure build of @object-ui/app-shell^... exit 0; type-check of types, layout and app-shell exit 0 (both new tests inside tsconfig.test.json includes). ESLint on the 8 changed files: 0 errors; warnings per modified file equal base vs head (12/2/13/21/0/1). Gates all exit 0: check:phantom-deps, self-import, unreferenced-sources, doc-example-readers, handler-key-reads, side-effects-array, element-data-source-declaration, spec-symbols, action-forward-parity, designer-field-key-parity, icon-record-names, i18n-keys, i18n-drift, i18n-designer-parity, control-bytes, new-line-citations; check-hand-rolled-comment-mask, check-changeset-fixed, check-changeset-presence. Ablations, both via objectstack scripts/ablation-replace.mjs (anchor hit, blob 693474f2afd7 to mutated, restored blob == HEAD 693474f2afd7, git diff HEAD empty); the subject resolves through the vitest alias to src, so no dist. (1) Hand-off dropped: 6 failed out of 21, (a) total-1 and (d) path on all 3 surfaces. (2) Pending treated as shown: 3 failed out of 21, (b) pending on all 3 surfaces. Each ablation's first attempt was a no-op that the tool refused (replacement not distinctive / replacement contained the anchor), with no test run; each was rerun.",
"mcp_calls": "0",
"api_writes": "2 relay writes: pr_create (draft, assignees baozhoutao) -> objectui#13095 via fleet-write run 39339664364; this os-dev-report comment on objectui#13029. Plus 2 git pushes (empty branch probe, implementation commit). Labels: zero writes (none named; objectui has no gate reading them).",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none. Noted in PR Acceptance notes, not filed. AppRootLanding (navEntriesShownToMember) treats a pending bound entry as hidden, so an app whose entries are all bound can show the no-access state until the reads answer. Observation; not measured in a browser.",
"carrier: none. Noted, not filed. Layout hosts outside the console (AppSchemaRenderer without an evaluator, third-party hosts) do not read bindings: the default evaluator hides a bound entry, and a custom evaluator ignoring
entryevaluates without data."],
"gates": "see tests; every gate exit 0; type-check exit 0 x3; check:spec-floors not run (not touched).",
"files_changed": [
"packages/layout/src/NavigationRenderer.tsx",
"packages/layout/src/tests/NavigationRenderer.entryDataSource-13029.test.tsx",
"packages/app-shell/src/hooks/useNavMemberGates.ts",
"packages/app-shell/src/hooks/tests/navEntryDataSource-13029.test.tsx",
"packages/app-shell/src/views/nav-menu-renderer.tsx",
"packages/app-shell/src/chrome/CommandPalette.tsx",
"packages/types/src/zod/app.zod.ts",
"packages/types/src/app.ts",
".changeset/13029-nav-data-source.md"
],
"deviations": "Order said both 'each touched package dir under the lock' and 'no whole-package-directory runs'; followed the lock clause (families + declare dirs to CI). Ablations used objectstack's ablation-replace.mjs (objectui has none). The test file lives in app-shell hooks/tests/ (tests beside useNavMemberGates)."
}
Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Contract review
Served-tier:
CONTRACT_REVIEW_TIERHead-sha:
ef2abeca8ac183a66b7632998220330fc05e9d32Local-runs: none
Inputs read: card #13029 (body and its three comments: unlock 6278361000, claim 6278727099, os-dev-report 6279363148), PR #13095 (body, 9-file list, net diff against
mainatcd4623d063), the head's 35 check-runs, and, read-only, the@object-ui/reactreader module (element-data-source/useElementApiDataSource.ts),AppContent.tsx(AppRootLanding),appServesNoNavigation.ts,CommandPalette.tsxandnav-menu-renderer.tsxat the head, AGENTS.md's version-alignment section andscripts/check-changeset-*, and the spec docblock onBaseNavItemSchema.dataSource(objectstackmain).Check-runs on the head (their conclusions are the gate verdicts). Completed
success: Test (dist pins), Changeset Fixed Group Check, Changeset Declaration, Changeset Claim Re-read, Changeset Bump Policy, Changeset Overwrite Report, Governed Surface Queue Guard, Line Citation Gate, label. Skipped: Test (coverage), Test (coverage shard), dependabot. Still in progress at review time, not waited for: Type Check, Lint, Repo Gates, Build and E2E, Bundle Analysis, Build Docs, Live E2E (informational), and Test shards 1/16 through 16/16. The adopting seat reads those before merge; this record does not stand in for them.① Derived judgments
navEntryVisibleinhooks/useNavMemberGates.ts: the sidebar, the landing (AppRootLanding),SidebarItemMenuand the home feeds throughuseNavMemberGates→useNavVisibility(app);nav:menuand the command palette throughuseNavVisibility(activeApp)directly.@object-ui/layout'spassesNavItemGuardsis the one guard on the rows,hasVisibleNavigationItems(area derivation,navEntriesShownToMember,appServesNoNavigation's predicate) and the Favorites walk, and the diff changes it in exactly one place to passitem.nav-menu-renderer's ownrenderItemkeeps the guard order and passesitem; the palette's filter passesitem(itsflattenNavigationpushes the item object itself, sodataSourcesurvives). The three surfaces cannot reach different verdicts for the same entry over the same read. Caveat, not a defect: each surface instance owns its own read state, so two surfaces mounted at different times can be in different pending windows; the verdict function is shared, the clock is not. The PR body says so ("each surface instance reads its own bindings").readElementApi(the page element'sGET,Accept: application/json,credentials: 'include', withSchemaRendererContext.apiFetchand the same?? fetchfallback),judgeElementApiDataSource(the spec'sElementApiDataSourceSchema.safeParse, so the same-origin andGET-only refusals are the spec's),selectElementApiPath. All four names are exported from@object-ui/react's entry today (export * from './element-data-source/useElementApiDataSource.js').useNavEntryDataruns its effect on[signature, doFetch]only, wheresignatureis the sorted content of every distinct binding innavigationandareas[].navigationat any depth; so it is once per distinct binding per hook instance per change of the set, never per render and never per entry (two entries sharing one binding share one key). Across instances,readElementApi's in-flight map shares a request per URL while it is pending and caches nothing, which is the page element's own rule. A surface mounted after the read settled (anav:menublock on a page opened later, a secondSidebarItemMenu) issues a second request; that is bounded by mounts, not renders, and the PR body states it. Not a storm. The test file's "each case uses its own URL" note shows the author understood the module-level in-flight map.navEntryVisiblereturnsfalsefor any status butready;readEntrymaps absent-from-answers topending, a rejected read (network, non-2xx, non-JSON) andselected.found === false(path-miss) tofailed, and a shape the spec refuses tofailedwithout a request (the effect skips it,judged.status !== 'parsed'). Pins (b) cover pending and HTTP 500 on all three surfaces; (d) covers path. Refused is not pinned by a test, but the code path is oneif.dataSourceunchanged — right.navEntryVisiblefalls straight through toevaluateNavigationVisibility(expression, evaluator), the pre-PR call; with no bindingssignatureis empty and the effect returns before any read; pin (c) proves first paint plusapiFetchnot called, on all three surfaces. One consequence to name: a bound entry whosevisiblenames nodata(pin (c) "Tour", whosevisibleis a constant comparison) is still withheld until its read answers. That is what the card's done-when 3 asks for (the binding, not the expression, is what is unanswered) and is pinned, so it is design, not drift.datareaches onlyvisible— right.evaluator.withContext({ data })is built innavEntryVisibleand handed toevaluateNavigationVisibilityalone. Labels still go throughresolveNavItemLabel, hrefs throughresolveHref, both untouched; nothing is written ontochildren(navBindingKeysonly reads them). The describe and doc text say the same.defaultVisibilitynow returnsfalsefor an entry withprovider: 'api', so a layout host without an evaluator (AppSchemaRenderer, third parties) stops drawing bound entries it drew before. Fail-closed is the card's rule and the docblock onVisibilityEvaluatorand the changeset both say it; the layout test pins it.NavigationRenderer.tsximportsisElementApiDataSourceShapefrom@object-ui/react, whichpackages/layout/package.jsonalready depends on (PageHeader.tsximports from it), so no phantom dependency; the Repo Gates run that mechanises this is still in progress.@object-ui/layout:VisibilityEvaluatorgains an optional second parameter (a widening every existing evaluator satisfies) plus the default-evaluator behaviour above;NavigationVisibilityOptions,NavigationRendererPropsunchanged.@object-ui/app-shell:navEntryVisible,useNavVisibility,NavEntryDataReadare exported fromuseNavMemberGates.ts, which the package entry does not re-export; no entry export added or changed; the publishedUnifiedSidebar,nav:menuandCommandPalettegain the behaviour.@object-ui/types:describeand TSDoc text only; the schema expressionstripImportedDefaults(SpecElementApiDataSourceSchema).optional()is byte-identical, so the accept set is unchanged. The dev's "no new package export" holds.AppRootLanding— new, user-visible, and under-stated in the dev's note.AppRootLandingfilters withnavEntriesShownToMember(…, gates)(pending → dropped) and then asksappServesNoNavigationover the already-filtered trees. Two consequences, both new because bound entries were ignored and shown before: (i) an app whose entries are all bound showsAppNoAccessEmptyStateuntil the reads answer, then re-renders (the hook owns theuseState, so it does re-render) and navigates; (ii) an app whose FIRST entry is bound and whose second is plain navigates, withreplace, to the second entry on first render, before the read answers — a pending bound entry can never be the landing. (ii) is not in the dev's note. Neither is a defect against the card's acceptance (done-when 3 says pending hides, and objectui#12426 says the landing is "the first entry this member is shown"), and fixing it means a landing that waits on reads, which is its own card, not this one. Acceptable here, but not with "carrier: none" — see ③.withContext):data.total == 1becomes unresolvable, whichevaluateNavigationVisibilitywithholds (objectui#12700), so (a)-shown and (d)-with-path fail and (a)-hidden, (d)-without-path, (b), (c) keep passing: 2 × 3 surfaces = 6/21, as reported. Ablation 2 (pending → shown): only (b)-pending fails, 500 still hides: 3/21. The counts match the pins' logic. The layout test'svi.fnevaluator pins the hand-off on rows and onhasVisibleNavigationItems; the Favorites path is covered only transitively (same guard function, one call site), which is adequate.packages/spec/src/ui/app.zod.tsand thenavigation.children.dataSourceliveness row flip tolive, onedomain:specchange, as the card's done-when 4 asks.② Semver level
.changeset/13029-nav-data-source.md:@object-ui/layout: minor,@object-ui/app-shell: minor,@object-ui/types: patch. Judged against AGENTS.md's version-alignment section (nomajorin a changeset; objectui's own breaking changes areminorwith the semantics in the body; minor/patch evolve independently) and the scripts:check-changeset-no-major.mjs(passed as "Changeset Bump Policy"),check-changeset-presence.mjs(passed as "Changeset Declaration"),check-changeset-fixed.mjs(passed),check-changeset-overwrite.mjsandcheck-changeset-claims.mjs(report-only, passed). None of them mechanises minor-versus-patch, so the level is a judgment:minor— right. A published type widened by an optional parameter plus a behaviour change in the default evaluator is a feature with a stated behaviour change; AGENTS.md routes even breaking changes tominor, and the body states the fail-closed semantics.minor— right. No entry export moved; the publishedUnifiedSidebar,nav:menuandCommandPaletteread a key they ignored before, andAppRootLanding's landing choice changes for bound entries. New behaviour on a published face is a feature, sominor, notpatch.patch— right. Describe and doc text only; the accept set is unchanged.Clause-②: yesin the PR body matches the claim'sClause-②: yes(a declared navigation key goes from not read to read).③ Boundary flags
open_questions:[]— nothing to answer.AppRootLanding, "carrier: none"): escalated. New user-visible behaviour for all-bound apps (no-access flash) and for apps whose first entry is bound (landing skips it while pending), per ① item 9. Not fixed here, and not a reason to refuse this head. The adopting seat files one follow-up card (a landing that waits on, or re-resolves after, the entries' reads; or a ruling that the first-render landing is the intended answer) and names it on navigation: read a nav entry's declared APIdataSource, so itsvisiblesees the selected value asdata(the objectui renderer half of objectstack-ai/objectstack#24427) #13029 before merge, so the finding has a carrier.VisibilityEvaluatordocblock and in the changeset body, and pinned by the layout test. A host that wants bound entries supplies an evaluator that readsentry.ablation-replace.mjs, test placed underhooks/__tests__/): consistent with the order's lock clause; the full directories are CI's, and the shards are in progress above.files_changed, matching the PR's file list;api_writesare the draft PR and the report comment, as claimed.Implemented-by:
claude/issue-13029-nav-data-sourceReviewed-by:
session_01XVp19E1MYUTFM3dnkwz8gBVERDICT: PASS
Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
ACCEPT — PR #13095 @
ef2abeca8ac183a66b7632998220330fc05e9d32·domain:uiseat 2 (session_01XVp19E1MYUTFM3dnkwz8gB)6279480544.nav:menuand the command palette decide through onenavEntryVisible.dataSourceis unchanged on first paint, with no request.datareaches onlyvisible.@object-ui/layoutVisibilityEvaluatorgains an optionalentryparameter, and the default evaluator hides a bound entry. No package entry export is added. Changeset: layoutminor, app-shellminor, typespatch(describe/doc only, accept set unchanged).AppRootLandingdrops a pending bound entry. An all-bound app flashes the no-access state, and a bound first entry is skipped as the landing while its read is pending. Both come from the review's ① item 9; the dev's note understated the second.VisibilityEvaluatordocblock and in the changeset.[PLANNED — not yet rendered]marker leavesBaseNavItemSchema.dataSource's describe, and thenavigation.children.dataSourceliveness row flips tolive) is named in the PR body for objectstack'sdomain:specseat once this lands.Next: ready + auto-merge; landing is verified by content on
main.Generated by Claude Code
objectstack-fleet commentedon Oct 19, 2026
Landed ·
domain:uiseat 2 ·session_01XVp19E1MYUTFM3dnkwz8gBmain1b94d50aa6.1b94d50aa6as at the accepted headef2abeca8a. The merge message carries no model identifier.dataSourcefeeds itsvisibleasdata, through one decision path for the sidebar,nav:menuand the command palette. A pending, failed or refused read hides the entry (fail closed). An entry without the key is unchanged.domain:specseat: remove the[PLANNED — not yet rendered]marker fromBaseNavItemSchema.dataSource, and flip thenavigation.children.dataSourceliveness row tolive. That waits until objectstack's.objectui-shacarries1b94d50aa6. Pointer posted on feat(spec): a navigation entry may declare one API data source, which its visible reads as data objectstack#24427.pm:dispatchedis removed.Generated by Claude Code