Skip to content

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

Closed as not planned

Description

@os-bob

domain:spec#1 · os-project-manager · session_01FEkQ5mswnWm181XgYcCUag

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

  1. 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.
  2. 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.

Activity

  1. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    Contributor

    triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH

    Path: none

    Triage: first touch → domain:spec · p3 · pm:queue · 2026-11-14T08:46Z

    ⛔ Not a claim.

    • 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.
  2. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    Contributor

    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.

    • 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).
    • State: pm:queue → needs-user-decision. Evaluation brief: /mnt/project-files/pm-orders/spec/33234-decision-brief.md (project share).
  3. added
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 14, 2026
  4. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    Contributor

    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's Ruled: line and the card's state are the seat's.

    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

    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

    1. 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.
      • grep -n "order: z.number" packages/spec/src/ui/app.zod.ts
      • grep -n "group.children.push\|nav.push" packages/objectql/src/registry.ts
      • grep -n "sort(byNavOrder)" ui/packages/layout/src/NavigationRenderer.tsx
      • Read, not run. The dispatch order owes a pin test.
    2. 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).
    3. 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.
    4. 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. spec + objectql + cli, 2 PRs. Precedent #14920 (+1275/−47).
    C No change. 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.

    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

  5. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    Contributor

    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.

    • 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).
    • Docs gap filed: docs: the navigationContributions section does not say a contributed entry's order sorts it among the target app's own entries #33447 (p3, spec lane): the navigationContributions docs never say this, and a pin that the fold keeps order.
    • State: closed not_planned. Reopening is free; the maintainer can overrule.
  6. removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Nov 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:specDomain lane — seat card indexed by label:pm:seatpriority:p3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions