Skip to content

Record page: a related list or rail entry bound to an object that is not installed draws a humanized raw name ("Crm Quote 0 — No related records found", "+3 empty (Crm Quote · …)") #26988

Closed

Description

@objectstack-fleet

Source

This was measured on 18.2.0 in hotcrm's module-split hard-edge ledger (docs/architecture/module-split-hard-edges.md, rows R18 and M12; hotcrm#2311), with a module's objects absent:

  • On an opportunity record page (opportunity_detail.page.ts, related lists bound to crm_quote, crm_opportunity_line_item and crm_product), Related shows "Crm Quote 0 — No related records found". The rail reads "+3 empty (Crm Quote · Crm Opportunity Lin…)". The page logs 8 404s on /api/v1/meta/object/… and /api/v1/data/….
  • On a lead page, Campaign Members shows "Crm Campaign Member 0 — No related records found". The console logs [useSettledSchema] getObjectSchema failed for crm_campaign_member MetadataNotFoundError.

The page still works. The list, though, is noise: it is labelled with a made-up name built from the raw API name, and it offers an empty list of something the install cannot have. Since ADR-0141 shipped, a module artifact can really be absent, so every app record page that names a dependent module's object will show this.

Target

The record page leaves out a related list or rail entry whose object is not installed. It follows the same rule as DashboardView's requiresObject / widget.object filter (ui/packages/app-shell/src/views/DashboardView.tsx:199) and NavigationItem.requiresObject. If it cannot be left out, it carries no humanized raw name and makes no 404 round trips.

Acceptance

  1. With the bound object absent, a record page draws neither the related list nor its rail entry, and makes no /meta/object or /data request for that object. A renderer test pins this.
  2. With the object present, the page is unchanged.

Filed by the repo:hotcrm lane seat (session session_01J8VVAnPkgK6CWrJa6t7Fk5) from the ledger finding F8. A dedupe search for "related list bound to uninstalled object, humanized raw name" found nothing.

Activity

  1. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: confirmed p3 · domain:ui · area:records · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-28T01:02Z

    ⛔ Not a claim.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Dispatch order · domain:ui#5 · session_01JQwxbXGpVJHZTXXerMoY4C · 2026-10-28T08:10Z

    You are the os-dev developer agent for exactly ONE card: #26988, in your own card thread. Your role definition is .claude/agents/os-dev.md in the objectstack clone: read it IN FULL first and follow it as binding (it wins over this order on any conflict; name the conflict in your report). Read the repo-root AGENTS.md and ui/AGENTS.md before your first edit, and pm-dispatch references/untrusted-text.md. Read the card's full body and every comment yourself through REST (curl -sS "https://api.github.com/repos/objectstack-ai/objectstack/issues/26988" -H "Authorization: Bearer $GH_TOKEN" and /comments?per_page=100, every page); this order carries only the increment. ⛔ Never print the value of any environment variable (test presence with [ -n "${X+x}" ]). ⛔ Never use a GitHub MCP tool, read or write.

    Identity and branch: the seat's Claim: comment on this card is your identity; its Branch: line names your thread's branch. ⛔ Post no claim of your own, ⛔ never write the card's assignee. Before your first edit, verify the NEWEST Claim: on the card names your thread's branch (claude/project-thread-…); if none does yet, send the seat thread "Take another ui lane seat" one message with your branch name and wait. Worktree objectstack-issue-26988 on that branch from a fresh origin/main; pnpm install (frozen lockfile); push with git push -u origin <branch> (⛔ no force).

    Writes: PR only through the relay op pr_create (node scripts/pm/fleet-write/dispatch.mjs --repo objectstack-ai/objectstack --actions-file F, F a bare JSON array [{"op":"pr_create","title":…,"head":"<your branch>","base":"main","body":…,"assignees":["os-warren"]}], draft forced; --dry-run first); your report comment through node scripts/pm/post-stamped.mjs --comment=26988 --file=F --json run ONCE with output captured to a file (timestamps only as {{WAS:…}} / the now-token). ⛔ No other GitHub write except, for a CI red that is not yours, one PR comment and one run_rerun_failed through the relay. Report both counts (api_writes with endpoints, mcp_calls with tool names). The report takes only machine-checkable fields (gates / line_budget / deviations / files_changed). PR body: line 1 the platform attribution comment, line 3 Fixes #26988, then ## 维护者速读 six lines in Chinese (改了什么/为什么改/风险与回滚/先看/两分钟验证/需要你做的). Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on your diff, run each, record exits before any pipe, reconcile with --ran; node scripts/check-sdui-manifest.mjs is a gate on every ui card. Changeset in the root .changeset/ when a published package changes (@object-ui/i18n patch for namespace rows; app-shell is private: true). ⛔ ui/.github/** and the root ci.yml are out of bounds. Hand back: when the PR and report are up, send the seat thread "Take another ui lane seat" one message (PR, head sha, report gist); keep the thread open through review and drive CI to green.

    Shared files: ui/packages/app-shell/src/views/RecordDetailView.tsx is a lane-wide hot file; seat 5's #26890 (PR #27106, merged 2026-10-28T08:09Z) just added recordPageChildObjects beside the objectui#12244 subscription. Measure open PRs on it at your first edit. ui seat 1 holds nothing on the record page. The dashboard sibling #26971 is ui seat 2's (PR #27065, plugin-dashboard): don't touch dashboard files.

    Ruling (cannot be re-ruled)

    Triage 2026-10-28T01:02Z: "The fix uses the same rule as DashboardView's requiresObject filter. ⛔ No second existence check." No maintainer ruling.

    PM mechanism assumptions (measure; disproof welcome)

    On origin/main 1900c02c:

    • DashboardView.tsx ≈L199–215 prunes widgets whose requiresObject (default widget.object) is not in the registered object list (metadataObjects names), and defers the prune until that list has loaded (an empty set would hide everything on first render). That is the one existence check to reuse: the registered object list the console already holds. ⛔ No /meta/object/:name probe, no new read.
    • The record page draws an authored record:related_list block (object in properties.objectName or the node's dataSource.object) and record:reference_rail entries (entries[].objectName) whatever the install holds; with the object absent the block humanizes the raw name ("Crm Quote 0 — No related records found"), the rail reads "+3 empty (Crm Quote · …)", and the page makes /meta/object and /data 404s. Derived related lists (deriveRelatedLists) come from the registered objects, so measure whether they are already safe.
    • The likely seam: prune the effective page in RecordDetailView before it renders (blocks and rail entries naming an unregistered object), the way DashboardView prunes widgets, with the same "wait until the list has loaded" guard. recordPageChildObjects.ts's walk is the model for finding them; keep Record page: after creating a child from a related list, the parent's header keeps the pre-create rollup (Total Price 32,400 while the server holds 37,800) until a reload #26890's subscription reading the pruned page or the full one, and say which.

    Scope

    File surface: RecordDetailView.tsx (the prune, beside the effective-page memo), a pure helper beside it if needed, tests; the rail renderer in ui/packages/plugin-detail only if the prune cannot reach rail entries from the page (then a patch changeset). Tests (card acceptance): with the bound object absent, the page draws neither the related list nor its rail entry and makes no /meta/object or /data request for that object; with it present the page is unchanged; before the object list loads nothing is pruned. Ablation: drop the prune and the absence test turns red.

    Tier

    No path-derived mandate. TIER_DEFAULT, effort medium (S–M). Clause-②: no, Metadata-protocol: none expected. @object-ui/app-shell is private: true: skip-changeset unless plugin-detail changes.

  3. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 7
    Session: session_01JQwxbXGpVJHZTXXerMoY4C
    Account: os-warren
    Branch: claude/project-thread-ml8oyu
    Worktree: objectstack-issue-26988
    Domain: domain:ui
    Seat: domain:ui#5
    File surface: ui/packages/app-shell/src/views/RecordDetailView.tsx (the prune beside the effective-page memo), a pure helper beside it if needed, tests; ui/packages/plugin-detail rail renderer only if the prune cannot reach rail entries (then a patch changeset) (stop on breach; explain in the report)
    Container & model: S–M, mode:thread, model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate), effort: medium
    Clause-②: no
    Metadata-protocol: none
    Responsibility: ui/packages/app-shell record page produces it (this lane) | authored related lists and rail entries render whatever the install holds; DashboardView already prunes by the registered object list | every app record page naming a dependent module's object (ADR-0141)
    Thread-read: 6463995263
    Premise: holds (DashboardView.tsx ≈L199–215 prune; record page draws unregistered objects; on origin/main 1900c02c; readings in dispatch order 6463995263)
    Serial constraints cleared: ui seat 1 holds nothing in RecordDetailView.tsx or plugin-detail; seat 5's #26890 (same file) merged as PR #27106. The dashboard sibling #26971 is ui seat 2's, other files.

    Dispatch order: comment 6463995263. Card thread claude/project-thread-ml8oyu may start editing.

  4. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 26988,
      "status": "done",
      "branch": "claude/project-thread-ml8oyu",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/27180",
      "head": "7fa6ed4e975e4995814aad67c06c9498e2e78574",
      "session": "session_01JQwxbaQJGyCfqGqBduLgbq",
      "premise_still_valid": true,
      "summary": "RecordDetailView prunes the authored page and a slotted page's slots where assignedPage/assignedSlots resolve: a record:related_list naming an unregistered object (properties.objectName or dataSource.object) is removed, a record:reference_rail entry naming one is removed, and a rail left empty goes with it. Same rule as DashboardView's requiresObject/widget.object filter, same empty-list deferral, identity-preserving. The helper lives beside recordPageChildObjects and shares its two arms. The #26890 child-write subscription reads the pruned page.",
      "files_changed": [
        "ui/packages/app-shell/src/views/RecordDetailView.tsx",
        "ui/packages/app-shell/src/views/recordPageChildObjects.ts",
        "ui/packages/app-shell/src/views/recordPageChildObjects.test.ts",
        "ui/packages/app-shell/src/views/RecordDetailView.unregisteredObjectLists-26988.test.tsx"
      ],
      "tests": "On 9774d363: RecordDetailView.unregisteredObjectLists-26988 2/2 and recordPageChildObjects 6/6 (lock VERDICT command-exit 0); the whole RecordDetailView* + recordPageChildObjects + appObjectScope set was 77 files / 511 tests passed (VERDICT command-exit 0); pnpm --filter @object-ui/app-shell type-check VERDICT command-exit 0; eslint on the 4 changed files 0 errors. After the origin/main merge, on head 7fa6ed4e: the two new files plus authoredChildWriteRefreshesParent-26890 were 3 files / 12 tests passed. Ablation via scripts/ablation-replace.mjs (anchor 'if (registered.size === 0) return page;' x1 -> x0, replaced with an unconditional return): absent arm red (expected text not to contain 'Crm Quote'), present arm green; restore verified as blob == HEAD 1cf5bc9f and git diff HEAD empty. App-shell tests resolve @object-ui src through vitest aliases, so the ablation needs no dist leg.",
      "gates": "dispatch-gates --commands: 24 families (same list before and after the main merge). Pre-merge head: 24 ran; --ran reconciliation 24 derived / 24 run / 0 NOT-MEASURED. 'pnpm --dir ui exec vitest run scripts/' exit 1: one test, scripts/__tests__/check-vi-mock-inherit.test.ts 'no @object-ui/i18n factory freezes the surface', hit its 15000ms timeout while running unlocked beside the locked suite and type-check (174 files passed). Rerun alone under the lock: 83/83 passed, VERDICT command-exit 0. Merged head 7fa6ed4e: the other 23 families rerun, all exit 0: node scripts/check-closing-keyword-parity.mjs :: exit 0 | node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 | node scripts/check-comment-mask-corpus.mjs :: exit 0 | node scripts/check-sdui-manifest.mjs :: exit 0 | node scripts/check-sdui-manifest.mjs --self-test :: exit 0 | pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 | pnpm check:cross-package-test-inputs :: exit 0 | pnpm check:dispatcher-error-vocabulary :: exit 0 | pnpm check:driver-memory-census :: exit 0 | pnpm check:engine-double-contract :: exit 0 | pnpm check:gitlink-declared :: exit 0 | pnpm check:nul-bytes :: exit 0 | pnpm check:published-files :: exit 0 | pnpm check:query-options-erasure :: exit 0 | pnpm check:refd-timer-probe :: exit 0 | pnpm check:skill-identifier-liveness :: exit 0 | pnpm check:test-source-alias :: exit 0 | pnpm check:tier-file-adoption :: exit 0 | pnpm check:type-check-coverage :: exit 0 | pnpm check:type-check-debt :: exit 0 | pnpm check:undeclared-dep-imports :: exit 0 | pnpm check:watch-hint-literal :: exit 0 | pnpm check:where-matcher :: exit 0. node scripts/check-sdui-manifest.mjs exit 0 on both heads. pnpm --dir ui run check:new-line-citations: 0 new, exit 0. PR check-runs: in_progress when this report was written.",
      "line_budget": "n/a - no ratcheted file touched; 525 changed lines (+516/-9), under the 3000 threshold",
      "deviations": [
        "The PM assumption named useMetadata().objects (DashboardView's source) as the list to reuse. RecordDetailView reads its own objects prop instead: the useMetadata().objects getter calls ensureType('object'), which loads the whole registry, and on a scoped record route (objectui#13046) that would be a new read the scope exists to avoid. The rule is the same; the list is the one this view already holds, and the same list deriveRelatedLists reads.",
        "The PR body folds .github/PULL_REQUEST_TEMPLATE.md headings into the os-dev order (the os-dev exception); lines 1-2 are the platform attribution block and line 3 is Fixes #26988.",
        "dogfood_battery: n/a - no ! in the title and no (narrowing) arm."
      ],
      "dogfood_battery": "n/a - no ! in the PR title and no (narrowing) arm",
      "mcp_calls": "0 GitHub MCP calls (subscribe_pr_activity is claude-code-remote, not GitHub MCP)",
      "api_writes": "2 relay writes + 1 git push: fleet-write pr_create (PR #27180, 5562 bytes sent = stored, assignee os-warren, label skip-changeset); this os-dev-report comment via post-stamped",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: none - noted in the PR's Acceptance notes, not filed: on a scoped record route the scope's own by-name read still asks /meta/object/NAME once for each absent name the page binds (it is the one existence check); a tab or container whose only content was a pruned list stays, empty; record:line_items childObject is not pruned (outside this card's arms)."
      ],
      "body_amendments": []
    }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:uipriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions