Skip to content

feat(nav): one entry per destination, one exemplar per nav-item kind (#1259) - #1261

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-1259-nav-slimming
Aug 23, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/issue-1259-nav-slimming

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #1259

Slims the authored nav from 7 groups / 31 items to 6 groups / 27 items, to the shape the maintainer accepted verbatim in the card (「接受你的建议」).

What moved, and what was removed

Removed Pipeline, Calendar, Interaction History, All Tasks
Moved Products → Sales · approvals Inbox → My Work
Dissolved the one-item Approvals group
Unchanged Home, Service, Insights; Marketing kept as a single-item domain group

The excess was almost entirely one pattern: the same object surfaced repeatedly through its own saved views while the list page already carried a tab for each. crm_event held four rows, crm_opportunity three.

Reachability — measured per view, not assumed

Every removed destination is still one click away on its object's tab strip. Read from the built artifact the runtime serves (dist/objectstack.json), not from source:

view in list.tabs in listViews note
pipeline_kanban (crm_opportunity) ✅ pos. 3 ✅
event_calendar (crm_event) ✅ pos. 2 ✅
held_events (crm_event) ✅ pos. 6 ✅
all_tasks (crm_task) ✅ pos. 1 ❌ it is the default list block (isDefault + pinned) — the landing tab of the page My Tasks opens

This partly falsifies the mechanism assumption in the dispatch, which read the strip as "declared listViews in declaration order". all_tasks is not in listViews at all — it is the list block itself — so a listViews-only check would have wrongly reported All Tasks as lost. Both surfaces matter; the repo's own guard (view-references.test.ts → every named list view is reachable) treats tabs as the curator, and it stays green. No view needed adding to a tab strip.

The card's other premise holds: all five My Work entries are viewName-type, so the view-entry exemplar duty really does pass to them when nav_pipeline goes.

The six-exemplar constraint, now guarded

test/app-navigation-shape.test.ts is new and pins the constraint in both directions:

  • floor — each of the six nav-item kinds (object / view / page / dashboard / report / component) keeps ≥1 entry. page and component are down to one each, so the next slimming pass was one deletion away from removing a demonstration silently.
  • ceiling — no two entries open the same destination; no object holds more than one plain list entry. Deliberately not "one entry per object": an object legitimately appears as its own list and as a personal view under My Work.

Reverse-verified, both legs, mutation confirmed on disk before reading the result and restore confirmed after (via an EXIT-trapped script):

  • deleting the only component entry → × still ships at least one \component` entry` (1 failed / 10 passed)
  • injecting a second plain crm_opportunity entry → × no two entries open the same destination and × no object holds more than one plain list entry (2 failed / 9 passed)

Docs and i18n

Removing nav entries falsified prose in ten doc pages across all three doc locales, three of them enforced by docs↔nav parity tests that went red immediately. Each retired name is re-pointed, not deleted — the convention docs-quick-tour-navigation.test.ts already enforces — and the four retired item names joined its PHANTOMS list so they must stay named in italics and can never be bolded again while they are not nav labels.

The three non-English locale bundles drop the orphaned apps.crm_enterprise.navigation keys and keep the labels of the two items that moved. en.ts carries only group entries and needed no change.

Gate blind spot, reported not fixed: lint:i18n-gate counts only i18n/missing-*, so orphaned (extra) locale keys are invisible to it — and to every other gate here. The four dead keys this PR removes would have sat there indefinitely without anyone noticing. Filed as a finding, not touched in this PR.

Verification

All on a8b3514:

  • pnpm typecheck → exit 0
  • pnpm validate → ✓ Validation passed (823ms)
  • pnpm lint:i18n-gate → ✓ i18n lint gate: 0 \i18n/missing-*` issues`
  • pnpm build → exit 0, UI: 1 Apps 14 Views 8 Pages 5 Dashboards 10 Reports 30 Actions
  • pnpm exec vitest run → 121 files, 2851 passed | 2 skipped, 0 failed

Two verification caveats, declared:

  1. The shared verify lock refused on this host — os-verify-lock: VERDICT lock-unusable (exit 99) · never acquired · nothing was built or tested, because macOS ships no flock. Commands were run directly with --maxWorkers=2 and NODE_OPTIONS=--max-old-space-size=4096.
  2. The booted pnpm dev pass is partial. The server boots clean on :4002 (38 plugins, 342 seed rows) and serves the ruled shape — verified by reading the nav and every tab strip out of the artifact it loaded. I did not click through the sidebar: /_console/ is auth-gated, and entering a password to authenticate is outside what I do, even against a local dev instance printing its own default credentials. A human clicking admin@objectos.ai through the six groups is the one check still owed.

Generated by Claude Code

…1259)

The sidebar had grown to 7 groups / 31 items against a docs promise of a
slimmed nav. Almost all the excess was one pattern: the same object surfaced
repeatedly through its own saved views while the list page already carried a
tab for each — crm_event held four rows, crm_opportunity three.

Removes Pipeline, Calendar, Interaction History and All Tasks; every one of
those destinations stays one click away as a tab on its object's list page.
Moves Products into Sales (revenue master data, not campaigns) and the
approvals Inbox into My Work, dissolving the one-item Approvals group.
Marketing stays a single-item domain group on purpose.

All six nav-item kinds keep an exemplar. test/app-navigation-shape.test.ts
pins that floor plus the no-duplicate-destination ceiling, so neither the
growth this fixes nor the over-slimming it invites can land silently.

Locale bundles drop the orphaned nav keys; the docs that enumerate the sidebar
are re-cut in all three doc locales, re-pointing each retired name rather than
deleting it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
hotcrm Ignored Ignored Aug 23, 2026 12:22pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation ci/cd CI plumbing and the verification pipeline metadata Declarative metadata — schema, security posture, UI surfaces labels Aug 23, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 23, 2026 12:41
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 23, 2026
Merged via the queue into main with commit d9a9aaf Aug 23, 2026
10 checks passed
os-steve pushed a commit that referenced this pull request Sep 6, 2026
Every i18n assertion in this repo asks whether an authored surface has a
translation. That direction cannot see a key whose target is gone: deleting a
navigation entry leaves its `apps.*.navigation` rows behind in all four
bundles, and a forward walk has nothing left to iterate that would reach them.
PR #1261 removed five such keys from three locales by hand; had it not,
`pnpm verify` would have stayed green with 15 dead keys.

Measured before choosing the shape. The platform rule
`translation-target-unknown` DOES report a navigation orphan, once per locale,
with the remedy printed — orphans are not invisible, which the card assumed.
What nothing does is fail: the finding is a `warning`, `objectstack lint` exits
0 on warnings, and `scripts/check-lint-i18n-gate.mjs` gates the
`i18n/missing-*` family, the forward direction. The platform rule id carries no
`i18n/` namespace, so it sits outside that gate rather than unlisted in it.

Adds the orphan assertion plus the anti-vacuity half that pins the nav walk and
the per-locale tables really were non-empty. Scope stops at
`apps.*.navigation`; the forward direction already gates as
`i18n/missing-navigation`.

Co-authored-by: Claude
github-merge-queue Bot pushed a commit that referenced this pull request Sep 6, 2026
Every i18n assertion in this repo asks whether an authored surface has a
translation. That direction cannot see a key whose target is gone: deleting a
navigation entry leaves its `apps.*.navigation` rows behind in all four
bundles, and a forward walk has nothing left to iterate that would reach them.
PR #1261 removed five such keys from three locales by hand; had it not,
`pnpm verify` would have stayed green with 15 dead keys.

Measured before choosing the shape. The platform rule
`translation-target-unknown` DOES report a navigation orphan, once per locale,
with the remedy printed — orphans are not invisible, which the card assumed.
What nothing does is fail: the finding is a `warning`, `objectstack lint` exits
0 on warnings, and `scripts/check-lint-i18n-gate.mjs` gates the
`i18n/missing-*` family, the forward direction. The platform rule id carries no
`i18n/` namespace, so it sits outside that gate rather than unlisted in it.

Adds the orphan assertion plus the anti-vacuity half that pins the nav walk and
the per-locale tables really were non-empty. Scope stops at
`apps.*.navigation`; the forward direction already gates as
`i18n/missing-navigation`.

Co-authored-by: Claude

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd CI plumbing and the verification pipeline documentation Improvements or additions to documentation metadata Declarative metadata — schema, security posture, UI surfaces

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Slim the nav: one exemplar per nav-item type, view variants return to in-page tabs (7 groups / 31 items → 6 / ~24)

1 participant