Skip to content

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

Closed

Description

@objectstack-fleet

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.dataSource is ElementApiDataSourceSchema.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, doc and group.
  • Only visible reads the bound value, as data (e.g. data.total == 1).
    • label and the target keys take no data.
    • 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.

What objectui does today (origin/main 4484a11041)

  • packages/layout/src/NavigationRenderer.tsx:682 (passesNavItemGuards) evaluates item.visible against the shell's ambient scope, which has no data in 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: useElementApiDataSource in packages/react/src/element-data-source/, used by SchemaRenderer. Reuse it rather than writing a second fetch.

Done when

  1. A nav entry that declares dataSource is shown or hidden by its visible over the value the binding selects, in the sidebar, the command palette and the nav menu alike. They share one guard, so they cannot disagree.
  2. An entry without dataSource behaves exactly as today.
  3. 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.
  4. Objectstack follow-up: the [PLANNED — not yet rendered] marker leaves the describe, and the liveness row flips to live. That is one domain:spec change on objectstack after this card lands. Name it in the PR.

Why p2 and not target:v18

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

  1. added
    enhancementNew feature or request
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobs
    on Oct 19, 2026
  2. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Unlock: pm:blocked → pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-19T22:02Z

    Triage 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-sha is f4755a68a797.

    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, still pm:blocked) is in the same nav mirror. Its pin does not yet carry objectstack 35450f0732. If both are claimed, land them serially.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 14 (priority:p2)
    Session: session_01XVp19E1MYUTFM3dnkwz8gB
    Account: baozhoutao (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-13029-nav-data-source
    Worktree: objectui-issue-13029
    Domain: domain:ui
    Seat: domain:ui#2
    File surface:

    • packages/layout/src/NavigationRenderer.tsx (passesNavItemGuards, the one guard the sidebar, CommandPalette and the nav menu share). An entry that declares dataSource has its visible evaluated with the bound value as data. A pending or failed read hides it (fail closed). An entry without the key behaves exactly as today.
    • Whatever wiring the guard needs in packages/app-shell/src/chrome/CommandPalette.tsx, views/nav-menu-renderer.tsx and hooks/useNavMemberGates.ts.
    • The reader reuses useElementApiDataSource (@object-ui/react), with no second fetch.
    • packages/types/src/zod/app.zod.ts and packages/types/src/app.ts: only the dataSource describe and doc ("Planned in the spec; the console does not read it yet" → what the renderer now does).
    • Tests beside them.
    • One .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/types describe 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; same app.zod.ts mirror) 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

  4. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    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 visible evaluator now reads an entry's declared API dataSource. Layout: VisibilityEvaluator gains an optional second parameter entry, 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 entry evaluates 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

  5. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Contract review

    Served-tier: CONTRACT_REVIEW_TIER
    Head-sha: ef2abeca8ac183a66b7632998220330fc05e9d32
    Local-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 main at cd4623d063), the head's 35 check-runs, and, read-only, the @object-ui/react reader module (element-data-source/useElementApiDataSource.ts), AppContent.tsx (AppRootLanding), appServesNoNavigation.ts, CommandPalette.tsx and nav-menu-renderer.tsx at the head, AGENTS.md's version-alignment section and scripts/check-changeset-*, and the spec docblock on BaseNavItemSchema.dataSource (objectstack main).

    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

    1. One decision path — right. Every shell surface ends in navEntryVisible in hooks/useNavMemberGates.ts: the sidebar, the landing (AppRootLanding), SidebarItemMenu and the home feeds through useNavMemberGates → useNavVisibility(app); nav:menu and the command palette through useNavVisibility(activeApp) directly. @object-ui/layout's passesNavItemGuards is 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 pass item. nav-menu-renderer's own renderItem keeps the guard order and passes item; the palette's filter passes item (its flattenNavigation pushes the item object itself, so dataSource survives). 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").
    2. Reader reuse, no second fetch — right. The diff adds no fetch of its own: readElementApi (the page element's GET, Accept: application/json, credentials: 'include', with SchemaRendererContext.apiFetch and the same ?? fetch fallback), judgeElementApiDataSource (the spec's ElementApiDataSourceSchema.safeParse, so the same-origin and GET-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').
    3. Request count — right, with the stated bound. useNavEntryData runs its effect on [signature, doFetch] only, where signature is the sorted content of every distinct binding in navigation and areas[].navigation at 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 (a nav:menu block on a page opened later, a second SidebarItemMenu) 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.
    4. Fail-closed — honest. navEntryVisible returns false for any status but ready; readEntry maps absent-from-answers to pending, a rejected read (network, non-2xx, non-JSON) and selected.found === false (path-miss) to failed, and a shape the spec refuses to failed without 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 one if.
    5. Entry without dataSource unchanged — right. navEntryVisible falls straight through to evaluateNavigationVisibility(expression, evaluator), the pre-PR call; with no bindings signature is empty and the effect returns before any read; pin (c) proves first paint plus apiFetch not called, on all three surfaces. One consequence to name: a bound entry whose visible names no data (pin (c) "Tour", whose visible is 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.
    6. data reaches only visible — right. evaluator.withContext({ data }) is built in navEntryVisible and handed to evaluateNavigationVisibility alone. Labels still go through resolveNavItemLabel, hrefs through resolveHref, both untouched; nothing is written onto children (navBindingKeys only reads them). The describe and doc text say the same.
    7. Layout default evaluator hides a bound entry — right, and a real behaviour change. defaultVisibility now returns false for an entry with provider: '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 on VisibilityEvaluator and the changeset both say it; the layout test pins it. NavigationRenderer.tsx imports isElementApiDataSourceShape from @object-ui/react, which packages/layout/package.json already depends on (PageHeader.tsx imports from it), so no phantom dependency; the Repo Gates run that mechanises this is still in progress.
    8. Public surface. @object-ui/layout: VisibilityEvaluator gains an optional second parameter (a widening every existing evaluator satisfies) plus the default-evaluator behaviour above; NavigationVisibilityOptions, NavigationRendererProps unchanged. @object-ui/app-shell: navEntryVisible, useNavVisibility, NavEntryDataRead are exported from useNavMemberGates.ts, which the package entry does not re-export; no entry export added or changed; the published UnifiedSidebar, nav:menu and CommandPalette gain the behaviour. @object-ui/types: describe and TSDoc text only; the schema expression stripImportedDefaults(SpecElementApiDataSourceSchema).optional() is byte-identical, so the accept set is unchanged. The dev's "no new package export" holds.
    9. AppRootLanding — new, user-visible, and under-stated in the dev's note. AppRootLanding filters with navEntriesShownToMember(…, gates) (pending → dropped) and then asks appServesNoNavigation over the already-filtered trees. Two consequences, both new because bound entries were ignored and shown before: (i) an app whose entries are all bound shows AppNoAccessEmptyState until the reads answer, then re-renders (the hook owns the useState, so it does re-render) and navigates; (ii) an app whose FIRST entry is bound and whose second is plain navigates, with replace, 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 ③.
    10. Pins distinguish — right. Ablation 1 (drop withContext): data.total == 1 becomes unresolvable, which evaluateNavigationVisibility withholds (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's vi.fn evaluator pins the hand-off on rows and on hasVisibleNavigationItems; the Favorites path is covered only transitively (same guard function, one call site), which is adequate.
    11. objectstack follow-up named — yes. The PR body's "Objectstack follow-up (not done here)" names the marker removal in packages/spec/src/ui/app.zod.ts and the navigation.children.dataSource liveness row flip to live, one domain:spec change, 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 (no major in a changeset; objectui's own breaking changes are minor with 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.mjs and check-changeset-claims.mjs (report-only, passed). None of them mechanises minor-versus-patch, so the level is a judgment:

    • layout 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 to minor, and the body states the fail-closed semantics.
    • app-shell minor — right. No entry export moved; the published UnifiedSidebar, nav:menu and CommandPalette read a key they ignored before, and AppRootLanding's landing choice changes for bound entries. New behaviour on a published face is a feature, so minor, not patch.
    • types patch — right. Describe and doc text only; the accept set is unchanged.

    Clause-②: yes in the PR body matches the claim's Clause-②: yes (a declared navigation key goes from not read to read).

    ③ Boundary flags

    • open_questions: [] — nothing to answer.
    • Dev flag 1 (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 API dataSource, so its visible sees the selected value as data (the objectui renderer half of objectstack-ai/objectstack#24427) #13029 before merge, so the finding has a carrier.
    • Dev flag 2 (layout hosts without an evaluator hide bound entries): answered, no carrier needed. That is the fail-closed rule the card asks for, stated on the VisibilityEvaluator docblock and in the changeset body, and pinned by the layout test. A host that wants bound entries supplies an evaluator that reads entry.
    • Deviations the dev reported (test-family runs instead of whole-package runs, objectstack's ablation-replace.mjs, test placed under hooks/__tests__/): consistent with the order's lock clause; the full directories are CI's, and the shards are in progress above.
    • Reporting: the os-dev-report lists 9 files_changed, matching the PR's file list; api_writes are the draft PR and the report comment, as claimed.
    • Check-runs still in progress (Type Check, Lint, Repo Gates, Build and E2E, test shards): the merge waits on them; this record does not pre-empt their conclusions.

    Implemented-by: claude/issue-13029-nav-data-source
    Reviewed-by: session_01XVp19E1MYUTFM3dnkwz8gB

    VERDICT: PASS


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #13095 @ ef2abeca8ac183a66b7632998220330fc05e9d32 · domain:ui seat 2 (session_01XVp19E1MYUTFM3dnkwz8gB)

    • Contract review: PASS, 6279480544.
      • The sidebar, nav:menu and the command palette decide through one navEntryVisible.
      • The page element's reader is reused, with no second fetch. Reads are once per distinct binding per hook instance, sharing in flight.
      • Fail-closed holds for pending, failed, refused and path-miss.
      • An entry without dataSource is unchanged on first paint, with no request.
      • data reaches only visible.
      • The ablations (6/21, 3/21) match the pins' logic.
    • Published face: @object-ui/layout VisibilityEvaluator gains an optional entry parameter, and the default evaluator hides a bound entry. No package entry export is added. Changeset: layout minor, app-shell minor, types patch (describe/doc only, accept set unchanged).
    • CI: all 36 check-runs completed on the head, 33 passed and 3 skipped.
    • Path fork: no governed path, 559 changed lines. Seat landing.
    • Findings:
      • filed objectui#13097: AppRootLanding drops 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.
      • Acceptance notes: a layout host without an evaluator hides bound entries. That is the card's fail-closed rule, stated on the VisibilityEvaluator docblock and in the changeset.
      • The objectstack follow-up (the [PLANNED — not yet rendered] marker leaves BaseNavItemSchema.dataSource's describe, and the navigation.children.dataSource liveness row flips to live) is named in the PR body for objectstack's domain:spec seat once this lands.

    Next: ready + auto-merge; landing is verified by content on main.


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 19, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:ui seat 2 · session_01XVp19E1MYUTFM3dnkwz8gB

    pm:dispatched is removed.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatenhancementNew feature or requestpriority:p2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions