Repository navigation
feat(spec): a navigation entry may declare one API data source, which its visible reads as data - #24427
Conversation
…y its visible (shape 1) The page element's ElementApiDataSourceSchema by reference on the shared nav base; visible reads the bound value as data. Planned, not yet rendered: liveness row navigation.children.dataSource graded planned with authorWarn. Claude-Session: https://claude.ai/code/session_01WnW7HvfnfHbLg1fpkHX3Mu Co-authored-by: Claude <noreply@anthropic.com>
…ss counts; drill the planned dataSource row Claude-Session: https://claude.ai/code/session_01WnW7HvfnfHbLg1fpkHX3Mu Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WnW7HvfnfHbLg1fpkHX3Mu Co-authored-by: Claude <noreply@anthropic.com>
…v-item-api-data-source
📓 Docs Drift CheckThis PR changes 1 package(s): 25 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 143 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 0943caac806d8462842557cca41a2eeb7df43b96 && git checkout 0943caac806d8462842557cca41a2eeb7df43b96
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e22defb13c171900e2c1c54d1d15c24ed85fe91b 98e3434b0e2300491bd6acc474e48b8962a1efb6 && git checkout -B drift-repro e22defb13c171900e2c1c54d1d15c24ed85fe91b && git merge --no-ff 98e3434b0e2300491bd6acc474e48b8962a1efb6
node scripts/docs-audit/affected-docs.mjs --json e22defb13c171900e2c1c54d1d15c24ed85fe91b
|
Contract reviewServed-tier: Inputs: card #24332 body and its four comments (triage ① Derived judgments
② Semver level
③ Boundary flagsDev deviations (os-dev-report
Open questions (also on the card):
Acceptance notes:
Out of scope: the Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对ACCEPT ·
Generated by Claude Code |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 39195742017 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
|
Queue ejection ·
Generated by Claude Code |
|
Re-queue, once ·
Generated by Claude Code |
|
Consumer pointer from objectui The objectui renderer half has landed. objectstack-ai/objectui#13029 → PR objectstack-ai/objectui#13095 → objectui
Owed on objectstack (
The liveness reading should wait until objectstack's Generated by Claude Code |
…, so its visible sees the selected value as data (objectstack-ai#13095) Fixes objectstack-ai#13029 Clause-②: yes ## What changes A navigation entry may declare the spec's API `dataSource` (`BaseNavItemSchema.dataSource`, objectstack-ai/objectstack#24427): `{ provider: 'api', read: { url }, path? }`. The entry's `visible` now reads the value the binding selects, as `data` (for example `data.total == 1`). The sidebar, the `nav:menu` block and the command palette all read it. - **`@object-ui/layout`** (`NavigationRenderer.tsx`): `VisibilityEvaluator` takes an optional second argument, `entry`, the entry whose `visible` it judges. `passesNavItemGuards` passes it on every path it serves: the rows, `hasVisibleNavigationItems` (derived area visibility) and the Favorites walk. The layout's default evaluator reads no binding, so it hides an entry that declares one (fail closed). An entry without the key renders exactly as before. - **`@object-ui/app-shell`** (`hooks/useNavMemberGates.ts`): one decision, `navEntryVisible`, and one hook, `useNavVisibility(app)`, which every surface builds its evaluator from: - the sidebar and the landing take it through `useNavMemberGates` (the expression context's app). `UnifiedSidebar` is unchanged: it already hands `gates.evaluateVisibility` to `NavigationRenderer`; - `nav:menu` (`views/nav-menu-renderer.tsx`) uses it for the active app, and its `renderItem` passes the entry; - the command palette (`chrome/CommandPalette.tsx`) uses it for the active app, and its filter passes the entry. `useNavVisibility` reads each distinct binding in the app's `navigation` and `areas[].navigation`, at any group depth. It uses the page element's reader module in `@object-ui/react`: `judgeElementApiDataSource` (the spec's schema judges the binding), `readElementApi` (the same GET with the host's `apiFetch`, which shares an in-flight request per URL) and `selectElementApiPath`. There is no second fetch implementation. The reads are keyed on the bindings' content, never on a tree's identity (AGENTS.md objectstack-ai#10). - **`@object-ui/types`**: the `dataSource` describe (`zod/app.zod.ts`) and doc (`app.ts`) now say what the console does. The accept set is unchanged. ### How the binding is honoured - **Ready:** `visible` is evaluated by `evaluateNavigationVisibility` over `evaluator.withContext({ data })`. `data` is the value at `path`, or the whole answer when there is no `path`. - **Hidden:** the entry is hidden while the read is pending, after it fails (a non-2xx answer or a body that is not JSON), when `path` selects nothing, and when the spec refuses the binding. - **No binding:** the entry goes through `evaluateNavigationVisibility` as before, with no request and no wait on first paint. - **Scope:** only `visible` reads `data`. The label and the target do not, and nothing is published to `children`. ### Published faces - `VisibilityEvaluator` (`@object-ui/layout`) gains an optional parameter. An existing evaluator that ignores it still type-checks and behaves as before, except for an entry that declares `dataSource`. No other published export was added. `NavigationVisibilityOptions` and `NavigationRendererProps` are unchanged. - `navEntryVisible`, `useNavVisibility` and `NavEntryDataRead` are exported from `useNavMemberGates.ts`, an internal module that the package entry does not re-export. - Changeset `.changeset/13029-nav-data-source.md`: layout `minor`, app-shell `minor`, types `patch`. ## Tests - `packages/app-shell/src/hooks/__tests__/navEntryDataSource-13029.test.tsx` mounts each surface (`describe.each`) over the real `ExpressionProvider`, the real `MePermissionsProvider` and a stubbed `apiFetch` on `SchemaRendererContext`: - sidebar: a host hands `useNavMemberGates()` to the real `NavigationRenderer`, as `UnifiedSidebar` does; - `nav:menu`: through `SchemaRenderer`; - command palette: through `CommandPaletteProvider` plus `?palette=1`. Pins: - (a) `visible: 'data.total == 1'` shows the entry for `total: 1` and hides it for `total: 2`. - (b) A pending read hides it, and an HTTP 500 hides it. - (c) Controls: an entry without `dataSource` draws on first paint and no request is sent. A `visible: '2 > 1'` on a bound entry still evaluates once the read answers. - (d) `path` is honoured: the answer `{ total: 2, summary: { total: 1 } }` with `path: 'summary'` shows the entry. Without `path` it hides it. - `packages/layout/src/__tests__/NavigationRenderer.entryDataSource-13029.test.tsx`: the guard passes the entry to a host evaluator, on rows and on `hasVisibleNavigationItems`. The default evaluator hides a bound entry and draws a plain one. Ablations, each run through `ablation-replace.mjs` (anchor must hit; restore proved by blob hash `693474f2afd7` equal to HEAD and an empty `git diff HEAD`). Both packages resolve through the root vitest alias to `src`, so no `dist` was involved: 1. Hand-off dropped (`evaluator.withContext({ data: read.data })` replaced by `evaluator`): 6 failed out of 21. That is (a) "shown when total 1" and (d) "value at path" on all three surfaces. The first attempt was a no-op: the replacement text was already present 12 times, so the tool refused and ran nothing. It was rerun with a marked replacement. 2. Pending treated as shown: 3 failed out of 21, the (b) pending pin on all three surfaces. The first attempt was a no-op too: the replacement contained the anchor, and the tool refused. It was rerun. ## Local verification (head `ef2abeca8a`) - **Build:** the closure build `pnpm --filter '@object-ui/app-shell^...' build` passed: `VERDICT command-exit 0`. - **Type-check:** `type-check` passed for `@object-ui/types`, `@object-ui/layout` and `@object-ui/app-shell`: `VERDICT command-exit 0`. Both new test files are inside the `tsconfig.test.json` includes. - **Vitest:** the new tests passed: 2 files, 25 tests. The families naming the touched files also passed: 149 files, 1857 tests in three locked chunks, each `VERDICT command-exit 0`. The family is every test that names `nav-menu-renderer`, `CommandPalette`, `useNavMemberGates`, `NavigationRenderer`, `evaluateNavigationVisibility`, `NavigationItemSchema` or the types `app.zod`. The full directories of the touched packages are left to CI. - **ESLint:** the 8 changed files gave 0 errors. Warnings per modified file are equal at the base (`6058bf8b71`) and at the head. - **Gates:** each of these exited 0: - `check:phantom-deps`, `check:self-import`, `check:unreferenced-sources`, `check:doc-example-readers`; - `check:handler-key-reads`, `check:side-effects-array`, `check:element-data-source-declaration`, `check:spec-symbols`; - `check:action-forward-parity`, `check:designer-field-key-parity`, `check:icon-record-names`; - `check:i18n-keys`, `check:i18n-drift`, `check:i18n-designer-parity`; - `check:control-bytes`, `check:new-line-citations`; - `check-hand-rolled-comment-mask.mjs`, `check-changeset-fixed.mjs`, `check-changeset-presence.mjs`. ## Objectstack follow-up (not done here) One `domain:spec` change on objectstack after this lands: - remove the `[PLANNED — not yet rendered]` marker from `BaseNavItemSchema.dataSource`'s describe in `packages/spec/src/ui/app.zod.ts`; - flip the liveness row `packages/spec/liveness/app.json` → `navigation.children.dataSource` from `planned` to `live`, and drop its author warning. ## Acceptance notes - **Other hosts fail closed.** A host of `@object-ui/layout` outside the console (`AppSchemaRenderer` without an evaluator, a third-party host) does not read bindings. With the default evaluator it hides a bound entry. A host evaluator that ignores `entry` evaluates `visible` without `data`, as before. - **Landing.** The app root's landing (`AppRootLanding`, through `navEntriesShownToMember`) treats a pending bound entry as hidden. If every entry of an app is bound, the no-access state can show until the reads answer. This is an observation, not measured in a browser. - **Request counts.** Each surface instance reads its own bindings. Concurrent reads of one URL share a request (`readElementApi`), and no answer is cached, as for a page element. - `objectstack-ai#13076` (the `navItemVisibleDataNeedsDataSource` chain) is untouched and remains open. The session that wrote this PR is `https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB`. --- _Generated by [Claude Code](https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB)_ Co-authored-by: Claude <noreply@anthropic.com>
Part of #24332 (the
visiblehalf of shape 1 lands here; thelabeland record-target halves stay open on the card as two forks, below).Clause-②: yes (widening)
What this does
A navigation entry may declare ONE API data source, the page element's
ElementApiDataSourceSchemaBY REFERENCE (packages/spec/src/ui/page.zod.ts), and itsvisiblereads the bound value asdata:packages/spec/src/ui/app.zod.ts:BaseNavItemSchemagainsdataSource: ElementApiDataSourceSchema.optional(), so the nine branches that spread the base carry it (object,dashboard,page,url,report,action,component,doc,group). Aseparatorspreads nothing and refuses it with its own prescription (the key joinsSEPARATOR_NAV_ITEM_GUIDANCE). Thevisibledescribe now says an entry with adataSourcereads it asdata, and an entry without one has nodata.[PLANNED — not yet rendered]and claims no reader. Onlyvisiblereadsdata: the describe sayslabeland the target keys (recordId,filters,params) take nodataread, and a group's binding is not published to itschildren.packages/spec/liveness/app.json: rownavigation.children.dataSource, gradedplanned,authorWarn: truewith anauthorHint, and drilledprovider/read.url/read.method/path.check:livenessrefuses an undeclared container inheritance, so the four child keys are drilled. The carrier is the objectui renderer half, which the seat files when this lands. The precedent forplanned+authorWarnin this same file isnavigation.runActionbefore objectui#5216.authorable-surface/ui.json(+9 rows, one per branch),content/docs/references/ui/app.mdx,liveness/state-counts/app.md..changeset/24332-nav-item-api-data-source.md:@objectstack/specminor.The premise, measured before building (ruling item 2)
page.zod.ts's transitive import closure is 52 files and does not containapp.zod.ts. Nothing moved.dataroot.objectui-shapin0707b4ae37and at main749ed76gives the same reading.objectui: packages/react/src/SchemaRenderer.tsxreads a node's API binding (useElementApiDataSource) and puts the answer in that node's evaluation scope asdata(pin:1157/:1231, main:1174/:1248). That oneExpressionEvaluatorserves the node's${…}text holes, inline locale-map members included (objectui#12936), and itsvisibleWhen(CEL envelope viaevaluateCondition). The binding is RENDERED at the pin, so the spec's[PLANNED]text onElementApiDataSourceSchemaand the "nodataroot" text onvisibleWhenare stale. That is #24300's surface, held off here.visibleobjectui: packages/app-shell/src/hooks/useNavMemberGates.tsevaluates a navvisiblethroughevaluateNavigationVisibilitywith the sameExpressionEvaluator.evaluateCondition, so a per-entrydatain scope is the same dialect and the same root name. Built.label${…}in a string or an inline-map member), but the label has a layer a page element does not:translateApp(packages/spec/src/system/i18n-resolver.ts) REPLACES a present label with the bundle entry keyed by the entry id, andwalkNavigationinpackages/cli/src/utils/i18n-extract.tsseeds every present label into the bundle as a translatable literal. The ruled row's own entry (nav_environments) has a zh-CN bundle entry, so a data label there would show the static noun in zh-CN. How a data-bearing label meets translation is a contract decision. Not built, see open question 1.recordIdobjectui: packages/layout/src/NavigationRenderer.tsxresolveHrefsubstitutesrecordIdthroughapplyNavTemplate, whose vocabulary is{current_user_id},{current_org_id}and a context selector's own{id}(single brace, no dots). A${data.id}there is taken as written. Reading data would add a second template dialect to the field, or a second spelling of a data read. Not built, see open question 2.passesNavItemGuards) evaluatesvisibleon every type,groupincluded, so a group can honour its own binding.Tests (all at
98e3434b0e, the PR head, unless noted)packages/spec/src/ui/app-nav-item-api-data-source.test.ts: 19 tests. Every branch parses with the binding and adata.*visible; the app door accepts it on a top-level entry and on a group's child; and the bare binding parses. Five refusals are each compared with the page element binding's own issues for the same value (code+path, not prose): unknown key, off-originread.url(3 shapes), credential key,POST, object binding. A separator refuses the key. The describe marker and theplannedliveness row are pinned.node scripts/ablation-replace.mjsrenamed the key todataSourceAblated, with anchor x1 → x0 and blob791b2943→c37dfcd0. Result: 17 failed / 2 passed (the separator refusal and the ledger row stay green, as they should). Restored, blob == HEAD,git diff HEADempty. The tests import./app.zodfrom source, so no dist leg applies.pnpm --filter @objectstack/spec test: 697 files, 21270 passed (at82e723d6e5; the merge after it brought nopackages/specchange).typecheckpassed, andcheck:test-typecheckOK.pnpm --filter @objectstack/spec check:generated: all 14 artifacts up to date after a rebuild at98e3434b0e.@objectstack/spec):@objectstack/lintfull suite: 145 files, 6864 passed. It includesvalidate-nav-*,validate-translation-referencesandlint-liveness-properties.@objectstack/metadata-protocolfull suite: 246 files passed (3 skipped), 20655 tests.@objectstack/cliunit tier, NARROWED: the full unit tier ran past a 540 s bound (exit 124, NOT MEASURED, and declared to CI). The run coverednav-contribution-groups*and 23test/i18n-*files: 25 files, 298 passed.liveness-planned-propertyfinding. The lint fans out over the top-level entries only, as the ledger README states.Gates
The derived list is
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackon this diff: 111 families, pluscheck:dev-prereqs(local-only, touches the spec build). All 112 were run locally on98e3434b0e, and--ranreconciles 111 of 111. Every one exits 0 except:pnpm check:platform-checklist: exit 1, red onorigin/mainas well. The finding isareas/access-security.json: ABSENT SYMBOL packages/core/src/security/resolve-authz-context.ts#legacyGrantAnchorRetired. This diff touches neither file. Onorigin/main71fa166e96,legacyGranthas 0 hits in that file, against a control ofexportwith 13 hits. Reported to the seat.Six families first answered exit 3 (PREREQUISITE NOT MET: no lint / client / objectql dist). After the consumer closure build (turbo, 62 tasks, all cache hits), all six re-ran with exit 0:
check:doc-formula-expressions,check:doc-security-posture,check:skill-examples,check:docs-transcript-drift,check:lean-entry-closure,check:dual-build-cjs-loads.Open questions for the seat (also in the report on #24332)
recordIdhas its own{…}template dialect.data.*in avisibleon an entry with nodataSource. It would be BREAKING and need an ADR-0087 disposition. Measured reach: 0data.reads among the navvisiblepredicates in this repo's 38 navigation-declaring files. The control is the same files'current_userpredicates, e.g.packages/spec/src/conversions/registry.ts:2091.read.urlof a page element'sapidata source …") on the navigation door too. That text ispage.zod.ts's, a hold-off for this card.Acceptance notes
objectui: packages/types/src/zod/app.zod.ts,NavigationItemObject) is a non-strictz.objectand stripsdataSource. This is renderer-half work for the carrier card.data.*read in a navvisibleFAULTS (and so withholds the entry underevaluateNavigationVisibility) or readsundefinedis NOT MEASURED: objectui is not installed in this container. TheauthorHintsays only that there is no value to read.Generated by Claude Code