Repository navigation
feat(spec): an app can name a landing per audience with landing - #28449
Conversation
… name an app navigation entry Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwXXtueDQZXqxv2FsL
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwXXtueDQZXqxv2FsL
…alidator Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwXXtueDQZXqxv2FsL
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwXXtueDQZXqxv2FsL
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwXXtueDQZXqxv2FsL
…census owes it to the landing card Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwXXtueDQZXqxv2FsL
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwXXtueDQZXqxv2FsL
📓 Docs Drift CheckThis PR changes 2 package(s): 15 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 168 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 813f5914466eb6468fb78515ff8655752ef95925 && git checkout 813f5914466eb6468fb78515ff8655752ef95925
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin bede9deaed99f86111194c8955edd7fb3ad15fc2 010f9645a73044bc655376a5e193a6e62f5303c2 && git checkout -B drift-repro bede9deaed99f86111194c8955edd7fb3ad15fc2 && git merge --no-ff 010f9645a73044bc655376a5e193a6e62f5303c2
node scripts/docs-audit/affected-docs.mjs --json bede9deaed99f86111194c8955edd7fb3ad15fc2
|
|
triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract-review-claim: |
|
triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract review of head
|
…der total Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwXXtueDQZXqxv2FsL
|
triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract-review-claim: Delta since the hold note: one commit, |
|
triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract reviewServed-tier: ① Derived judgments
② Rulings cited
③ Narrowing / widening
④ Behaviour / flags
Implemented-by: |
|
domain:spec#2 · os-tesla · session_01L9qibx5r5nhDMwpRs4SL6D 维护者速读
席位核对 · ACCEPT ·
|
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwXXtueDQZXqxv2FsL
|
domain:spec#2 · os-tesla · session_01L9qibx5r5nhDMwpRs4SL6D Seat re-arm · 2026-10-31T18:46Z: the queue removed this PR (MERGE_CONFLICT, 2026-10-31T18:17Z). New head |
|
triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract-review-claim: Delta since PASS 6547108962 (head 5f376c4): merge of |
|
triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz Contract review (delta)Served-tier: ① Derived judgments
② Rulings cited
③ Narrowing / widening
④ Behaviour / flags
Implemented-by: |
domain:spec#2 · os-tesla · session_01L9qibx5r5nhDMwpRs4SL6D
Requested by Tesla · project thread
Fixes #28022
Clause-②: yes (widening)
Metadata-protocol: added
domain:spec#2 · os-tesla · session_01L9qibwXXtueDQZXqxv2FsL
Ruling: record 6545223246 on the card, letter A (maintainer 「同意」 2026-10-31T15:10Z). This is the spec half only. Follow-up: #28450, the domain:ui landing card (console landing redirect, plus attaching
checkAppLandingItemsto objectui'sAppComponentSchema, which drops the onenotAttachableentry this PR adds).维护者速读
landing键,按受众声明落地页:[{ item, visible }],第一个visible成立的条目决定读者打开哪个导航项;无匹配或不写时照旧打开第一个导航项。item指向不存在的导航项、或指向分组/分隔线/动作项,保存时直接拒收(不会像退役的homePageId那样悄悄落到第一项)。回滚即还原本 PR。packages/spec/src/ui/app.zod.ts(checkAppLandingItems与AppLandingEntrySchema)、packages/spec/liveness/app.json(landing标为 planned 并提示作者)、packages/lint/src/validate-expressions.ts(落地谓词的表达式检查)。pnpm --filter @objectstack/spec exec vitest run src/ui/app-landing.test.ts,应见 14 个用例全绿,其中悬空item在landing.0.item处被拒、并列出存在的导航项 id。What changes
AppSchema.landing(packages/spec/src/ui/app.zod.ts): an optional, non-empty list ofAppLandingEntrySchemaentries,{ item, visible }, both required. The console asks the entries in order and opens the first whosevisibleholds for the reader (and whoseitemthe reader can see). No match, or no key: the app opens on its first navigation item byorder, as today. The describe text states both rules.homePageIdtombstone asked for.checkAppLandingItems, asuperRefineonAppSchema, runs on every app parse (sodefineApp,os validate/os buildand the metadata save door all refuse alike). It refuses, atlanding.N.item, anitemthat names no entry of the app's ownnavigationorareas[].navigation(any depth throughchildren), listing the openable ids that exist; and anitemthat names agroup,separatororactionentry, none of which opens a page. It also refuses, atlanding.N.visible, a predicate that readsdata(a landing entry has nodataSource), with the same lexer the navigation entry's owndatarefusal uses. It is exported (and barrel-reachable) for the same reason asnavItemVisibleDataNeedsDataSource: a mirror attaches the function instead of restating the rule.navigation, not an inline target (the order's either/or): the landing is a sidebar entry, so the sidebar can highlight what the reader landed on, and there is one copy of each navigation target. The id's old failure mode, dangling into a silent fallback, is closed by the parse-time refusal above, so the cross-reference is checked rather than trusted.visibleis required: an entry with no predicate would land every reader there, which ishomePageIdagain under a new name. "Open somewhere else for everyone" stays "put that entry first innavigation".landings/landingPagesroute tolanding; on an entry,navigationId/navItem/itemId/target/pageroute toitem, andvisibleWhen/visibleOn/when/conditiontovisible. The retiredhomePageIdprescription (and itshome/homepage/landingpagespellings) now also nameslanding, and the tombstone docblock's last paragraph says howlandinganswers both of its objections.packages/lint/src/validate-expressions.ts):os validate/os buildjudge each landing predicate with the shared validator's data-independent verdicts (CEL syntax, size, unknown function,has()shape). It passes no object and norecordscope, because the slot binds no record and arecord-scoped call would prescriberecord.FIELD.app.landingrowplanned+authorWarn+authorHint(childrenitem,visible); evaluated-slot census 32 → 33; dogfood expression-conformance rowcel-app-landing(experimental,unevaluated); form-reconciliationomitrow (planned, no form control);/meta/typesappcount 22 → 23;dropped-refinements.baseline.json(+1 site in six entries, the new object-level check, hand-edited per the ledger's rule); objectui censusspec-object-refinements-7715.test.tsnamescheckAppLandingItemsundernotAttachable(test-only line, allowed by the claim amendment 6545506798), owed to the follow-up ui card.content/docs/ui/apps.mdxgains "Landing per audience" (shape, order rule, refusals, a Planned callout) and a property-table row; references regenerated..changeset/28022-app-landing-per-audience.md,@objectstack/specand@objectstack/lintminor.Why
An app shared by more than one audience has one landing: its first navigation entry. In HotCRM, moving Sales above Service moves service staff's landing to Leads (#27386 row 5). The maintainer ruled letter A: an app-level landing list with
visiblepredicates, first match wins.Tests
packages/spec/src/ui/app-landing.test.ts(new, 14 cases): accepted shapes (top-level, nested, area-only items; bare string normalised to the CEL envelope), the control (no key parses as before), refusals at the entry's own path (dangling item listing the existing ids,group/action/separatortargets, adataread, a missingvisible, an empty list, near-miss spellings), barrel identity, and bothvisiblespellings through the exported function.packages/lint/src/validate-expressions.test.ts: a non-CEL landing predicate is an error atapp 'crm' · landing[0] ('nav_leads') visible; a healthy reader predicate is clean; the validate-expressions / validate-security-posture 也有同形的 spec 不声明键的??别名读法(#5009 建议 3 的核对结果) #5017 read-surface table gainsappDef(AppSchema) andlandingEntry(AppLandingEntrySchema), andstackgainsapps.git diff HEAD): removing the.superRefine(checkAppLandingItems)mount →app-landing.test.ts5 failed / 9 passed; removing the lintcheck(...)call →validate-expressions.test.ts -t landing2 failed (the landing case and the read-surface row).Verification
Local runs, head 1308512
Derived list (
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack), run at 4437a3b, each line with stdin from /dev/null. Reconciled:dispatch-gates --ran: 108 derived, 108 run, 0 NOT-MEASURED. Five lines first exited 3 or 1 on a missing prerequisite (an unfetched pinned commit; packages with no built declarations) and were rerun aftergit fetchof the pinned sha and a fullpnpm buildwith declarations; the record below carries the rerun codes.Then
mainwas merged (8 commits touching the same generated ui artifacts), the artifacts were regenerated (check:generated --fix, thencheck:generatedexit 0), and these were rerun at 1308512:Acceptance notes
plannedwithauthorWarn, so an author who writeslandingis told no console applies it yet; the describe text opens with[PLANNED — not yet rendered]; the docs page carries a Planned callout. The tombstone's other objection, the silent dangling fallback, is closed now, at parse.validateStackExpressionsdoes not listappin itsruntimeTypes, so the lint half runs atos validate/os buildonly. The parse refusals (dangling item, non-opening target,dataread, blank /ast-only predicate) do run at the save door. Widening the rule's runtime types is a publish-gate change, left out of this card.record.xpasses lint (no bare-identifier or root verdict on this slot). The console redirect card decides the evaluation scope; a root verdict belongs with it.navigationContributionsitems cannot be named. The refusal reads the app as parsed, and a contributed item is not part of it. Stated in the describe text and the docs.landingentry whosevisiblepredicate holds #28450: the domain:ui landing card: the console landing redirect readslanding, flips the liveness row tolive, attachescheckAppLandingItemsto objectui'sAppComponentSchemaand drops thenotAttachableentry, and re-classifies thecel-app-landingconformance row.Human Minimum
packages/specholds no runtime logic.pnpm lintpasses (repo-wide lint is CI's run).main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code