You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
app nav: a per-audience landing (and group placement) cannot name an entry contributed by a module package — so an industry module can never be a role's landing page or reorder past the app's own groups #33234
Ruled: C 「不改」 (no change) — maintainer, decision card cmsg_01TenzsRopZj4jMbvjX8uz1X2urAufVvk5hC7JsAsmxj3R; record 6884379643; closed not planned 6884419053; docs gap → #33447. (Seat's edit: these two lines only; the filer's body below is unchanged.)
Found by a downstream app on the published 18.4.0 packages (steedos-labs/os-project-blackcat-crm#70: reorganising one CRM app's navigation around an industry module, measured per role on 10 accounts).
Limits hit
An app's landing item may name only the app's own navigation entries. An entry contributed by a module package through navigationContributions (e.g. the industry module's Sales Order list, its Master Data group, or a service module's Service Overview) can never be a per-audience landing. Consequence downstream: a sales rep cannot land on the module's Sales Orders; the system officer cannot land on Master Data; and the app must keep the Service group ahead of Customers / Sales only because the service staff's landing (Service Overview, a contributed entry) is reached by ordering, not by a landing key.
A contribution is appended after the target group's own items (applyNavContributions, by group id + priority): a module cannot place its entry between two of the app's entries (e.g. Sales Order next to Opportunities), and cannot declare a group in the app — the app must pre-declare every group a module may fill.
Why it matters
Industry modules (the intended way to extend a packaged CRM per customer) cannot shape the daily entry point of the roles they exist for; apps end up editing the upstream app's navigation to make room, which every upstream sync must replay.
Suggested fix shape (for the lane to judge)
Let a landing name any entry present in the merged navigation (own or contributed), validated after contributions are applied; and let a contribution declare a position relative to an app entry (before / after an id) within the bounds the app allows.
Dedupe words: landing contributed entry · navigationContributions position · module landing page · nav contribution before after.
Row: a per-audience landing or group placement cannot name a navigation entry contributed by a module package, so an industry module can never be a role's landing page. This is a capability limit, not a wrong answer: the declaration is refused or ignored as documented. p3.
The navigation contract decides it (spec plus runtime resolution). If the lane finds the docs promise this, it becomes a wrong-running p2.
Seat: not dispatched — half of the ask re-opens a recorded maintainer ruling → needs-user-decision · 2026-11-14T19:05Z
⛔ Not a claim. The lane takes this card as soon as the shape is ruled.
What the card asks: (1) a per-audience landing[].item may name a navigation entry a module package contributes (navigationContributions), checked after contributions are folded; (2) a contribution may declare a position relative to an app entry (before / after an id), within bounds the app allows.
Governing ruling:An app cannot give each audience its own landing page #28022, ruling record 6545223246 (batch 2026-10-31 item 3, maintainer 「同意」 2026-10-31T15:12Z): A ruled, a landing list key on the app; B not taken: "reuse navigationContributions with a declared position, no new key". Part (2) is that declared position, now asked beside A rather than instead of it.
Part (1) widens A: today checkAppLandingItems (packages/spec/src/ui/app.zod.ts) refuses at parse an item that is not the app's own entry, the referential integrity the homePageId tombstone demanded. A parse cannot see contributions, so naming a contributed entry moves that refusal from parse to the post-fold check (registry fold at boot/publish, os build), or trades it for a weaker one.
Options for the evaluation: A: both parts; B: part (1) only, the referential refusal moved after the fold, (2) stays not taken; C: neither, the app keeps owning landings (status quo, the downstream app pre-declares groups and orders).
Ruled: C (no change) · Fable evaluation thread · 2026-11-14T21:20Z
⛔ Not a claim. Written by the evaluation thread the spec seat opened for this card (brief /mnt/project-files/pm-orders/spec/33234-decision-brief.md); labels, the body's Ruled: line and the card's state are the seat's.
What it rules: neither part of this card is built. An app's landing keeps naming only the app's own navigation entries, refused at parse otherwise (checkAppLandingItems, An app cannot give each audience its own landing page #28022 ruling record 6545223246, letter A, stands as written). No before / after position on a contribution: An app cannot give each audience its own landing page #28022's B stays not taken. No landing key on a contribution either (option D below, the evaluation's recommendation, was not chosen).
Left to the seat (not ruled by the tap): the evaluation read, without running it, that part (2) is already expressible: a contributed entry keeps its order through the fold, the console sorts each level by order after the fold, and a contribution's items may carry a group. If the seat confirms that, whether the docs say so (and a pin holds it) is a docs-gap call of the seat's, not a contract change. Closing this card is the seat's hand-over to the maintainer.
The evaluation the ruling was made on
Recommended D; the maintainer ruled C. Kept here whole so the options and readings are on the card.
One-sentence problem
A customer who adds an industry module to a packaged CRM cannot make a sales rep open the app on the module's Sales Orders list, and believes the module cannot place its entry between two of the app's own entries.
Background
An app cannot give each audience its own landing page #28022, ruling record 6545223246 (letter A): the app gained landing, a list of { item, visible }, first match wins. B ("reuse navigationContributions with a declared position, no new key") was not taken.
checkAppLandingItems (packages/spec/src/ui/app.zod.ts) refuses at parse an item that is not an entry of the app's own navigation / areas[].navigation. A contributed entry is not part of the app being parsed, so it can never be named.
The console already resolves a landing against the served (folded) trees: ui/packages/app-shell/src/console/appDeclaredLanding.ts#resolveDeclaredLandingRoute looks the id up in the trees the member is shown, and passes over an entry it does not find.
Governing text
ADR-0130 §1.5: "navigation crosses only through contributions". A package's own app.navigation pointing at a foreign object is rejected; a contribution into a foreign app is accepted. grep -n "navigation crosses only through contributions" docs/adr/0130-release-artifact-as-co-ownership-boundary.md
ADR-0029 D7: navigationContributions is the UI analog of objectExtensions; the app is a shell with group anchors.
The homePageId tombstone (HOME_PAGE_ID_RETIRED, same file): an id into navigation with no referential integrity, resolved by a silent fallback, is the condemned shape.
Protocol declaration
A, B and D change the authorable contract (Clause-②: yes (widening), Metadata-protocol: added for A and D; B changes a refusal's door). C changes nothing.
Premises, with re-check commands
Part (2) of the card is already expressible. Every nav entry carries order ("Sort order within the same level"), the fold keeps a contributed item as authored, and the console sorts each level by order after the fold. A contributed entry with an order between two of the app's numbered entries is drawn between them. An app that numbers none of its entries (all sort as 0) gives a module only "before all" or "after all", which is the app allowing or not allowing interleaving. A contribution's items is an array of NavigationItemSchema, so it may carry a group with children: the app need not pre-declare every group.
Read, not run. The dispatch order owes a pin test.
HotCRM 6.2.1 numbers none of its app entries: grep -c "order:" src/sales/apps/crm.app.ts in hotcrm answers 0. Interleaving into the upstream app needs upstream to number its entries once (a hotcrm card).
The console half needs no change for any option that puts the entry in the served trees and the landing in the served app: resolveDeclaredLandingRoute reads both from the served app.
No cross-package nav-id uniqueness check exists; findEntryById takes the first match, the app's own entries first.
Options
What it does
What the customer feels
Work
A
Seat's A: the app's landing may name a contributed entry (checked after the fold) and a contribution may declare before / after an app entry id.
Both asks answered. Two positioning vocabularies (order and before/after) for one job.
spec + objectql + cli + ui ordering, 3 to 4 PRs. Precedents: #14920 (+1275/−47) for the post-fold check, #28449 (+595/−31) for a new key; console ordering has no precedent.
B
Seat's B: the app's landing may name any entry of the folded navigation. The dangling-id refusal moves from parse to the post-fold doors (os build / os validate over the artifact, the registry fold).
A typo is reported later than today, and at runtime only as a warning. Disabling the module leaves the landing dangling: the reader silently opens somewhere else. The upstream app does not know the module's ids, so the customer still edits the upstream app's landing and replays it on every sync.
A module's entry becomes a landing only by being first in the order, or by the module shipping its own app (ADR-0019 D3 as amended; HotCRM's Service app). "A rep lands on Sales Orders inside the CRM app" stays unanswered.
none
D
New in this evaluation: the contribution carries the landing. navigationContributions[].landing reuses AppLandingEntrySchema; each item must be one of that contribution's own items, refused at parse otherwise. The fold appends contributed landing entries after the app's own, by priority, and drops a disabled package's with its entries.
The module says which audience opens on its entry. A typo is refused when saved. Disabling the module removes entry and landing together, so nothing dangles. No upstream edit. The app's own landing is asked first, so the app owner keeps the last word.
spec + objectql, 1 to 2 PRs. Precedent #28449 (+595/−31) for the spec half; the fold half has no precedent (one register field, one append). No console change (premise 3).
Business reading: A is "let the tenant's shop sign hang anywhere and also let the landlord's directory point at it"; B is "the landlord's directory lists a tenant by name and goes stale when the tenant leaves"; C is "tenants get their own front door or none"; D is "each tenant hangs its own sign on the space it rents".
Recommendation: D, with part (2) answered by order (docs and a pin, no new key)
End state by axis ① alone: in two years an app is a shell that several packages fill, and each package describes only what it brings. ADR-0130 §1.5 already draws that seam for navigation; D keeps the landing on the same side of it, B crosses it in the other direction (the base naming its extension's id). Odoo modules and Dynamics solution layers model it the same way: the extension declares its own entry and where it sits. The Salesforce shape (app plus profile assignment by the admin, or one app per module) is option C's second route and stays available.
B not taken: it moves a loud parse refusal to a later, weaker door, creates a dangling state on disable, and leaves the card's own complaint (editing the upstream app) in place.
Self-check: by ① alone the letter is D; ②③④ flip it: no. ② one downstream project (p3), so it queues behind p1/p2 work, timing only. ③ D refuses at save, B reports later and falls back quietly at runtime. ④ D adds one optional key, a permanent obligation; B adds no key but a diagnostic vocabulary and three check sites. Work differs by less than 3×.
Confidence gaps: the downstream card (steedos-labs/os-project-blackcat-crm#70) is outside this session's access, so whether the customer owns the app or copies the upstream one is inferred from #33234. Premise 1 is read, not run. D's known limit: when the app itself declares a landing for the same audience, the app wins and the module cannot override it.
After the ruling
The spec seat returns the card to pm:queue and dispatches. For D: (1) spec: NavigationContributionSchema.landing, its parse refusal, liveness row with its reader in the same PR, the AppLandingEntrySchema docblock sentence "cannot be named here" rewritten to point at the contribution's key; (2) objectql: the registry stores and folds contributed landing entries after the app's own, by priority, skipping a disabled package; (3) docs and one pin for premise 1 (order on a contributed entry, a contributed group); (4) a hotcrm card to number the CRM app's entries if upstream wants modules to interleave. before / after is not built under B, C or D.
os-decision-facets
① Long-term soundness: D keeps one rule (a landing and its target live in one package, checked at parse); B adds a second validation time for the same key; A adds a second positioning vocabulary.
② Business pull: one downstream project today (#33234, 10 accounts measured); p3.
③ AI-error proofing: D is a loud refusal at save; B is a later report and a quiet runtime fallback, the homePageId shape.
④ No proliferation: C adds nothing; D adds one optional key; B adds a diagnostic code and three check sites; A adds both a key and the checks.
Prior rulings read: "landing contributed entry navigationContributions", "navigationContributions position", "navigation contribution order between entries" → 15 / 22 / 20 hits, read #28022 (6545223246), #14553 (PR #14920), #27928 (6566915346), #32585; ADR-0130 §1.5, ADR-0029 D7, ADR-0019 D3; thread: none
Seat: closed not planned under the maintainer's ruling C · 2026-11-14T21:23Z
⛔ Not a claim.
Ruling-ref: Leehom chose C 「不改」 at 2026-11-14T21:19:19Z (decision thread cmsg_01TenzsRopZj4jMbvjX8uz1XKbi11aY8SWRcPc43gTa4HY, card cmsg_01TenzsRopZj4jMbvjX8uz1X2urAufVvk5hC7JsAsmxj3R, choice record read by the seat on that thread: option index 3, chosen by the maintainer). Record on this card: 6884379643. needs-user-decision leaves with this comment; the body carries the Ruled: line.
Part (1), landing on a contributed entry: not built. An app's landing names only its own entries (An app cannot give each audience its own landing page #28022 record 6545223246, letter A, stands). A module whose audience needs its own landing ships its own app.
Part (2), placing a contributed entry between the app's own: the seat confirmed the evaluation's reading against main (read, not run). applyNavContributions (packages/objectql/src/registry.ts ~:10842) pushes a contribution's items as authored, order included, and the console sorts each level by order after the fold (ui/packages/layout/src/navOrder.ts#byNavOrder: top level, each group's children, mobile tab bar). A contributed entry with order: 25 lands between the app's 20 and 30. It needs the app to number its own entries; unnumbered entries all sort as 0, so a contribution stays after them. For the downstream filer: this is available on 18.4.0 today, with no new key; the upstream app has to number its entries (HotCRM 6.2.1 numbers none).
Activity
objectstack-fleet commentedon Nov 14, 2026
triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH
Path: none
Triage: first touch →
domain:spec· p3 ·pm:queue· 2026-11-14T08:46Z⛔ Not a claim.
objectstack-fleet commentedon Nov 14, 2026
domain:spec#1 · os-project-manager · session_01FEkQ5mswnWm181XgYcCUag
Seat: not dispatched — half of the ask re-opens a recorded maintainer ruling →
needs-user-decision· 2026-11-14T19:05Z⛔ Not a claim. The lane takes this card as soon as the shape is ruled.
landing[].itemmay name a navigation entry a module package contributes (navigationContributions), checked after contributions are folded; (2) a contribution may declare a position relative to an app entry (before/afteran id), within bounds the app allows.navigationContributionswith a declared position, no new key". Part (2) is that declared position, now asked beside A rather than instead of it.checkAppLandingItems(packages/spec/src/ui/app.zod.ts) refuses at parse anitemthat is not the app's own entry, the referential integrity thehomePageIdtombstone demanded. A parse cannot see contributions, so naming a contributed entry moves that refusal from parse to the post-fold check (registry fold at boot/publish,os build), or trades it for a weaker one.pm:queue→needs-user-decision. Evaluation brief:/mnt/project-files/pm-orders/spec/33234-decision-brief.md(project share).objectstack-fleet commentedon Nov 14, 2026
domain:spec#1 · os-project-manager · session_01FEkQ5kYbfZ8Mq31uMJnjeU
Ruled: C (no change) · Fable evaluation thread · 2026-11-14T21:20Z
⛔ Not a claim. Written by the evaluation thread the spec seat opened for this card (brief
/mnt/project-files/pm-orders/spec/33234-decision-brief.md); labels, the body'sRuled:line and the card's state are the seat's.cmsg_01TenzsRopZj4jMbvjX8uz1XKbi11aY8SWRcPc43gTa4HY, cardcmsg_01TenzsRopZj4jMbvjX8uz1X2urAufVvk5hC7JsAsmxj3R). Option text verbatim: "保持现状。模块条目要当落地,只能排到最前,或由模块自带一个应用。同一应用里按角色落到模块条目的场景无解。"landingkeeps naming only the app's own navigation entries, refused at parse otherwise (checkAppLandingItems, An app cannot give each audience its own landing page #28022 ruling record 6545223246, letter A, stands as written). Nobefore/afterposition on a contribution: An app cannot give each audience its own landing page #28022's B stays not taken. No landing key on a contribution either (option D below, the evaluation's recommendation, was not chosen).orderthrough the fold, the console sorts each level byorderafter the fold, and a contribution'sitemsmay carry agroup. If the seat confirms that, whether the docs say so (and a pin holds it) is a docs-gap call of the seat's, not a contract change. Closing this card is the seat's hand-over to the maintainer.The evaluation the ruling was made on
Recommended D; the maintainer ruled C. Kept here whole so the options and readings are on the card.
One-sentence problem
A customer who adds an industry module to a packaged CRM cannot make a sales rep open the app on the module's Sales Orders list, and believes the module cannot place its entry between two of the app's own entries.
Background
landing, a list of{ item, visible }, first match wins. B ("reusenavigationContributionswith a declared position, no new key") was not taken.checkAppLandingItems(packages/spec/src/ui/app.zod.ts) refuses at parse anitemthat is not an entry of the app's ownnavigation/areas[].navigation. A contributed entry is not part of the app being parsed, so it can never be named.ui/packages/app-shell/src/console/appDeclaredLanding.ts#resolveDeclaredLandingRoutelooks the id up in the trees the member is shown, and passes over an entry it does not find.Governing text
app.navigationpointing at a foreign object is rejected; a contribution into a foreign app is accepted.grep -n "navigation crosses only through contributions" docs/adr/0130-release-artifact-as-co-ownership-boundary.mdnavigationContributionsis the UI analog ofobjectExtensions; the app is a shell with group anchors.navigationContributions[].groupthat names no group in the target app is silently RELOCATED to the top level — refuse, warn, or leave to the consumer? #14553 ruling (2026-09-02, option B, PR fix(objectql,cli): a navigation contribution relocated past a missing group now says so, atwarnand at build time #14920): a contribution aimed at a missing group is relocated and reported atwarnand at build time, never refused, because the fold is order-independent and the contributing package may be optional.navigationstays as declared.homePageIdtombstone (HOME_PAGE_ID_RETIRED, same file): an id intonavigationwith no referential integrity, resolved by a silent fallback, is the condemned shape.Protocol declaration
A, B and D change the authorable contract (
Clause-②: yes (widening),Metadata-protocol: addedfor A and D; B changes a refusal's door). C changes nothing.Premises, with re-check commands
order("Sort order within the same level"), the fold keeps a contributed item as authored, and the console sorts each level byorderafter the fold. A contributed entry with anorderbetween two of the app's numbered entries is drawn between them. An app that numbers none of its entries (all sort as 0) gives a module only "before all" or "after all", which is the app allowing or not allowing interleaving. A contribution'sitemsis an array ofNavigationItemSchema, so it may carry agroupwith children: the app need not pre-declare every group.grep -n "order: z.number" packages/spec/src/ui/app.zod.tsgrep -n "group.children.push\|nav.push" packages/objectql/src/registry.tsgrep -n "sort(byNavOrder)" ui/packages/layout/src/NavigationRenderer.tsxgrep -c "order:" src/sales/apps/crm.app.tsin hotcrm answers 0. Interleaving into the upstream app needs upstream to number its entries once (a hotcrm card).resolveDeclaredLandingRoutereads both from the served app.findEntryByIdtakes the first match, the app's own entries first.Options
landingmay name a contributed entry (checked after the fold) and a contribution may declarebefore/afteran app entry id.orderandbefore/after) for one job.landingmay name any entry of the folded navigation. The dangling-id refusal moves from parse to the post-fold doors (os build/os validateover the artifact, the registry fold).landingand replays it on every sync.navigationContributions[].landingreusesAppLandingEntrySchema; eachitemmust be one of that contribution's ownitems, refused at parse otherwise. The fold appends contributed landing entries after the app's own, bypriority, and drops a disabled package's with its entries.Business reading: A is "let the tenant's shop sign hang anywhere and also let the landlord's directory point at it"; B is "the landlord's directory lists a tenant by name and goes stale when the tenant leaves"; C is "tenants get their own front door or none"; D is "each tenant hangs its own sign on the space it rents".
Recommendation: D, with part (2) answered by
order(docs and a pin, no new key)End state by axis ① alone: in two years an app is a shell that several packages fill, and each package describes only what it brings. ADR-0130 §1.5 already draws that seam for navigation; D keeps the landing on the same side of it, B crosses it in the other direction (the base naming its extension's id). Odoo modules and Dynamics solution layers model it the same way: the extension declares its own entry and where it sits. The Salesforce shape (app plus profile assignment by the admin, or one app per module) is option C's second route and stays available.
before/afterduplicatesorder(premise 1); it revives An app cannot give each audience its own landing page #28022's B with no new evidence.Self-check: by ① alone the letter is D; ②③④ flip it: no. ② one downstream project (p3), so it queues behind p1/p2 work, timing only. ③ D refuses at save, B reports later and falls back quietly at runtime. ④ D adds one optional key, a permanent obligation; B adds no key but a diagnostic vocabulary and three check sites. Work differs by less than 3×.
Confidence gaps: the downstream card (steedos-labs/os-project-blackcat-crm#70) is outside this session's access, so whether the customer owns the app or copies the upstream one is inferred from #33234. Premise 1 is read, not run. D's known limit: when the app itself declares a landing for the same audience, the app wins and the module cannot override it.
After the ruling
The spec seat returns the card to
pm:queueand dispatches. For D: (1) spec:NavigationContributionSchema.landing, its parse refusal, liveness row with its reader in the same PR, theAppLandingEntrySchemadocblock sentence "cannot be named here" rewritten to point at the contribution's key; (2) objectql: the registry stores and folds contributed landing entries after the app's own, by priority, skipping a disabled package; (3) docs and one pin for premise 1 (orderon a contributed entry, a contributed group); (4) a hotcrm card to number the CRM app's entries if upstream wants modules to interleave.before/afteris not built under B, C or D.os-decision-facets
① Long-term soundness: D keeps one rule (a landing and its target live in one package, checked at parse); B adds a second validation time for the same key; A adds a second positioning vocabulary.
② Business pull: one downstream project today (#33234, 10 accounts measured); p3.
③ AI-error proofing: D is a loud refusal at save; B is a later report and a quiet runtime fallback, the
homePageIdshape.④ No proliferation: C adds nothing; D adds one optional key; B adds a diagnostic code and three check sites; A adds both a key and the checks.
Prior rulings read: "landing contributed entry navigationContributions", "navigationContributions position", "navigation contribution order between entries" → 15 / 22 / 20 hits, read #28022 (6545223246), #14553 (PR #14920), #27928 (6566915346), #32585; ADR-0130 §1.5, ADR-0029 D7, ADR-0019 D3; thread: none
objectstack-fleet commentedon Nov 14, 2026
domain:spec#1 · os-project-manager · session_01FEkQ5mswnWm181XgYcCUag
Seat: closed not planned under the maintainer's ruling C · 2026-11-14T21:23Z
⛔ Not a claim.
cmsg_01TenzsRopZj4jMbvjX8uz1XKbi11aY8SWRcPc43gTa4HY, cardcmsg_01TenzsRopZj4jMbvjX8uz1X2urAufVvk5hC7JsAsmxj3R, choice record read by the seat on that thread: option index 3, chosen by the maintainer). Record on this card: 6884379643.needs-user-decisionleaves with this comment; the body carries theRuled:line.landingnames only its own entries (An app cannot give each audience its own landing page #28022 record 6545223246, letter A, stands). A module whose audience needs its own landing ships its own app.main(read, not run).applyNavContributions(packages/objectql/src/registry.ts~:10842) pushes a contribution'sitemsas authored,orderincluded, and the console sorts each level byorderafter the fold (ui/packages/layout/src/navOrder.ts#byNavOrder: top level, each group's children, mobile tab bar). A contributed entry withorder: 25lands between the app's20and30. It needs the app to number its own entries; unnumbered entries all sort as0, so a contribution stays after them. For the downstream filer: this is available on 18.4.0 today, with no new key; the upstream app has to number its entries (HotCRM 6.2.1 numbers none).ordersorts it among the target app's own entries #33447 (p3, spec lane): thenavigationContributionsdocs never say this, and a pin that the fold keepsorder.not_planned. Reopening is free; the maintainer can overrule.