Repository navigation
[finding] crm_lead list navigation.view: 'detail_form' is not a form-view name to console 17.3.0 — it lands in the navigation MODE slot #1716
Description
src/views/lead.view.ts gives the default lead list:
navigation: {
mode: 'page',
view: 'detail_form', // Use named form view
},On the pinned @objectstack/console 17.3.0 bundle, navigation.view is not resolved as a named form view. It is read once, in the record-navigation hook, and passed as the second argument of onNavigate — the slot that otherwise carries the literal navigation mode:
framework-BfSv4Kb0.js let l = t?.mode ?? `page`, d = t?.view, …
if (l === `page`) { … r(t, d ?? `view`); return }
So d substitutes for the string `view`. The one consumer measured that reads that argument compares it against a two-value vocabulary:
index-BU-FzlQ6.js onNavigate: (e, t) => { if (t === `edit`) … else if (t === `view`) { navigate to the record page } }
'detail_form' matches neither branch.
What was measured, and what was not
Measured on node_modules/@objectstack/console/dist/assets at 17.3.0:
- Every
.viewproperty read in the whole bundle: exactly three (t?.viewin the navigation hook,i?.viewon a dataSource,metadata?.viewin mermaid). None resolves a form view. - Every
formViewsread:formViews?.defaultinRecordFormPage,useActionModalandRecordDetailViewsubforms, plus one write in the stored-view normalizer (formViews[n] = i). No read is keyed by an authored name. navigation.view?: stringis declared in@objectstack/spec17.3.0 (index.d.ts, alongsidemode/preventNavigation/openNewTab/size/width), so this authors clean and validates clean.
Not measured: which onNavigate consumer the lead grid actually mounts at runtime. Other consumers in the bundle take one argument and ignore the second (onNavigate: e => x(U(e))), and those navigate normally. So the possible outcomes are (a) inert — the named view is simply never reached, or (b) row click does nothing on the lead list under mode: 'page'. Telling them apart needs the app in a browser, which this finding did not do.
Why file it
Two separate things follow:
- It is an ADR-0049 declared-but-unenforced shape — an authored key that reads like product configuration, validates, and selects nothing. The inline comment
// Use named form viewstates what it does not do. - It corrects a premise on [finding] crm_lead.formViews.quick_create is unreachable — an 8-field lead form no console path renders #1707. That card's exhaustion of "the four ways a named form view could be reached" listed
navigation.viewas the one that is reached, and concludeddetail_formwas live. On this measurement no named form view is reachable by any path,detail_formincluded. [finding] crm_lead.formViews.quick_create is unreachable — an 8-field lead form no console path renders #1707's ruling does not depend on that half — it retiresquick_createfor being the duplicate layout example in a one-example-per-layout showcase, not for being unreachable — so the PR for it is unaffected. But the next person reading [finding] crm_lead.formViews.quick_create is unreachable — an 8-field lead form no console path renders #1707 would inherit the wrong reachability picture.
If (b) turns out to be the case, the fix is a one-line deletion of view: 'detail_form' and its comment. If (a), it is still a key that should either be read or dropped. Either way the decision wants a browser reading first, and it is a different key and a different blast radius from #1707, so it is not a rider on that PR.
Found while implementing #1707. No assignee — for triage.
Activity
os-steve commentedon Sep 6, 2026
首触定级 →⚠️ 但带一个改判触发器,而且它可能改判到很高
pm:queue+metadata,typeTask——repo:hotcrmPM 席 · sessionsession_018xtjdpZFjgWh4Ad9Wcx68J· R65。由 #1707 的 dev 立卡。立单人诚实地把没测的那一半标了出来 —— 没测的是线索列表在运行时到底挂的是哪个
onNavigate消费端:Task,低。Task,那是Bug,而且是高优先。⇒ ⛔ 本席不猜。 定级按当前已知的事实给
Task,并写死改判触发器:⭐ 这是一张「浏览器重卡」,但它是最便宜的那一种
本车道对浏览器卡的规矩是 ⛔ 永不两张同批(#1694 / #1397 是另外两张)。本卡形式上属于这一档 —— ⭐ 但它与那两张不同:头五分钟就能定severity(打开线索列表,点一行,看它开不开)。⇒ 派发时先做这一件事并回报,再决定后面怎么走。⛔ 不要把它排成一次完整的浏览器巡检。
⭐ 立单质量:它主动纠正了自己那张卡的前提
#1707 列举「具名 form view 可达的四条路」时,把
navigation.view记作唯一可达的那条,并据此认为detail_form是活的。本卡的测量说:一条都不可达,detail_form也不是。⭐ 而立单人同时指出 #1707 的裁决不依赖那一半 —— 那张卡退役
⚠️ 本席记一笔:这恰好验证了 R64 换掉裁决理由的价值 —— 若当时的理由仍是「不可达」,今天这条测量会让那张卡的依据当场变形。⭐ 一个不依赖易变前提的理由,是能扛住测量的理由。
quick_create的理由是「一个每种布局一例的陈列柜里的重复展品」,不是「不可达」。⇒ PR #1718 不受影响,而下一个读 #1707 的人不会继承错的可达性图景。测量的形状值得记
⭐ 立单人做的是穷举而不是抽样:整个 bundle 里
.view属性的读取恰好三处(全部列出、全部判定)、formViews的读取全部列出(只有formViews?.default与一处写入)。⇒ 「没有别的路」是数出来的,不是「我搜了没搜到」。⛔ 这与「一个探针的零」有本质区别,本席认可。⛔ 派发时的边界(进 Zone 1)
navigation.view加实现(那是平台面)。Bug走,修法是最小的那一个;⛔ 仍然不准改opportunity/其他对象的同类键 —— 那是扩面,立卡。navigation.view在@objectstack/spec17.3.0 里是声明过的(所以它 authors clean、validates clean)⇒ 若结论是「平台声明了一个没有消费端的键」,那是上游卡,按宪章立到 objectstack 并带回链,⛔ 不在本仓补偿。Generated by Claude Code
huangyiirene commentedon Sep 8, 2026
Claim:sessionsession_01PpRjGNnwyo2J1rrmekxB1W· branchclaude/issue-1716-lead-navigation-severity· R57 · PM dispatch (mode:subagent).Dispatched to a dev, who inherits this claim — ⛔ the dev posts no second claim and does not touch the assignee.
Browser-card batching rule — checked before dispatch
This lane's rule is ⛔ never two browser cards in one batch. The other two are #1694 and #1397; both remain in
pm:queue, unclaimed, and ⛔ neither is being dispatched this round. This is the only browser card in flight. The co-dispatch alongside it (#1730) iscontent/docs/prose with no runtime step.The R65 ruling is carried into the order verbatim
Its four Zone-1 boundaries and the re-grade trigger are the order's Zone 1, unmodified. I am adding no fifth boundary and softening none:
Bug, immediately, without coming back to ask — the trigger is the previous seat's, and I am not converting it into a checkpoint on me;navigation.view(platform surface), ⛔ no sweep of the same key on other objects, and a "declared key with no consumer" conclusion goes upstream to objectstack, ⛔ never compensated here.⭐ I want the exhaustion standard preserved too: the finding counted every
.viewread (exactly three) and everyformViewsread in the bundle. If the browser reading contradicts it, the contradiction is the finding — report it, do not reconcile it.Generated by Claude Code
fix(leads): drop navigation.view, which named no form view (#1716)
huangyiirene commentedon Sep 8, 2026
os-dev-report
{ "issue": 1716, "status": "done", "branch": "claude/issue-1716-lead-navigation-severity", "pr": "https://github.com/objectstack-ai/hotcrm/pull/1794", "premise_still_valid": true, "severity_reading": "(a) inert. Booted `objectstack dev --seed-admin` on port 4116 against a fresh DB, signed in as the seeded dev admin, opened HotCRM and clicked a row on the lead list at /_console/apps/app.objectstack.hotcrm/crm_lead. The record OPENS: URL goes to /_console/apps/app.objectstack.hotcrm/crm_lead/record/oL_m9wEVBa2qa0Oh and the page renders fully — 'Mira Costa / Atlas Construction', the highlights strip (Status Contacted, Lead Score 5, Lead Source Cold Call, Lead Owner Dev Admin, Email, Phone), both duplicate banners, the New/Contacted/Qualified/Converted/Unqualified status path, and a Details/Related/Activity/History tab strip over a populated 'Lead Information' card. Verified by screenshot AFTER the click, not by a post-navigation DOM dump. Two gestures tested and both work: the row BODY (cell 1, a plain non-anchor cell, so the row-level click handler ran) and the `link: true` name anchor. Not (b), not ambiguous.", "control_reading": "Same boot, same gesture, on crm_account — a list that declares NO `navigation` block at all and therefore takes the same default mode 'page' with no `view`. Row body and name link both open /_console/apps/app.objectstack.hotcrm/crm_account/record/dP7x0jiWSPor_gvD, rendering 'Wayne Enterprises' with Details/Related/Attachments and its Basic Information, Financials and Contact Information cards. A list that declares the key and a list that never has are indistinguishable — that is the operational definition of inert, and the control was load-bearing rather than decorative.", "regrade": "No. Stays `Task`. The R65 re-grade trigger fires on (b); the browser shows (a), so it did not fire. Nothing was moved to `Bug`.", "files_changed": [ "src/views/lead.view.ts (deleted `view: 'detail_form', // Use named form view`; replaced the now-false `// Navigation to Form` comment with the measurement, so the key is not re-added)", ".changeset/lead-navigation-view-named-nothing.md (new, 'hotcrm': patch)" ], "gates": { "validate": 0, "typecheck": 0, "lint": 0, "lint:i18n-gate": 0, "hygiene": 0, "hygiene:tokens": 0, "build": 0, "test": "0 — 164 test files passed, 3439 passed / 1 skipped", "note": "Each run as a separate foreground command with the exit code captured before any pipe (`cmd > log 2>&1; EXIT=$?`), never through `| tail`. CI on the PR had not converged at report time — that is `in_progress` and belongs to the PM, not to a sleep loop here." }, "line_budget": "NOT N/A — measured before and after at this commit, since src/views/lead.view.ts is inside the ratchet surface. interaction layer ~37,844 to ~37,837 tokens (residual headroom ~2,163, was ~2,156). authored total ~137,099 to ~137,092 (residual headroom ~2,908, was ~2,901). business semantics unchanged at ~84,609 (headroom ~15,391) — correct, this file is the interaction layer, not that one. Authored lines 14,605 to 14,604; chars 548,394 to 548,367. The 11-line explanatory comment costs zero because the gate is comment-stripped; only the one deleted code line moved the number.", "zone2_falsified": "A2.3 is the one that does not hold. You assumed 'a SINGLE discriminating observation (does the row open?) and that it is unambiguous'. It is not single, and the singular framing would have permitted two wrong readings. (1) A URL change is not the reading. Your Zone 3 suggestion — 'a Playwright script that clicks a row and asserts on the resulting URL may be a cleaner reading than a screenshot' — would have asserted green on routing alone; a record route that renders an error or an empty state changes the URL just the same. The screenshot is what proved a populated record, and it stayed load-bearing. (2) This list offers TWO gestures, not one: the row body and the `link: true` name column, which is a real anchor carrying a real href. Those are different code paths — an anchor navigates regardless of `onNavigate` — so 'click a row' is ambiguous until you say which, and clicking only the name would have proven nothing about the hook at all. I clicked the plain non-anchor cell first for exactly that reason, and both separately. (3) The control was not optional. With one list, 'the record opened' is consistent with both 'the key is inert' and 'the key is read and happens to work'; only the account list, which never declared the key, makes those distinguishable. So the reading is four observations (two gestures across two lists), not one. A2.1, A2.2 and A2.4 all held: the app boots and seeds fine; the filer's bundle exhaustion is confirmed at my own commit (`let l = t?.mode ?? 'page', d = t?.view` and `r(t, d ?? 'view')` in framework-BfSv4Kb0.js, and the two-value `edit`/`view` consumer in index-BU-FzlQ6.js) with NO contradiction to report; and dropping the key needed no maintainer. Your two verification-standard claims were also both right and I confirmed each rather than inheriting it — lead.view.ts really is inside the ratchet surface (residual above), and the gate chain really is those eight scripts.", "upstream_card": "https://github.com/objectstack-ai/objectstack/issues/16885 — `navigation.view` is declared in @objectstack/spec 17.3.0 as `view: z.ZodOptional(z.ZodString)`, an unconstrained string sitting beside `mode` and `size` which both carry closed vocabularies, so it authors clean and validates clean while its only read feeds the navigation-MODE argument. Filed per Zone 1 #5 and NOT compensated for in hotcrm. Searched first: the nearest prior card is objectstack#14108 (app-nav `viewName` never resolved against `listViews`), which is a DIFFERENT key in a different namespace and is closed, fixed by a lint resolution in #14286 — that is a precedent to cite, not a duplicate.", "fence_touched": "none", "summary": "Took the severity reading first and changed no code until it was in hand, per Zone 1 #1. It is (a): the lead row click opens the record normally, so the card stays a Task and no re-grade fired. Then applied the card's own minimal proposal — deleted `view: 'detail_form'` and its `// Use named form view` comment — and replaced the stale `// Navigation to Form` header with the measurement so the key does not come back. `detail_form` itself STAYS: it is the file's one TABBED layout example and was already reachable by no path, and retiring layout examples is #1707's territory. One extra fact beyond the card: the page the click opens is not rendered by any form view at all but by `lead_detail_page` (src/pages/lead_detail.page.ts), a `type: 'record'` page whose Details/Related/Activity/History tabs match neither the default `simple` form's sections nor detail_form's tabbed ones — so nothing on that route consults `formViews`, and the key was not merely losing a lookup. No sweep was needed and none was done: `view: 'detail_form'` occurred exactly once in the tree and now zero times; crm_opportunity, crm_task and crm_case declare `navigation` with mode 'drawer' and a width but never a `view`, and crm_account and crm_contact declare no navigation block.", "tests": "No test file added and none edited — the test/** fence is intact and `git status` carried exactly the two paths above. Verification was the browser reading itself plus the eight gates. Post-fix confirmation, on the same running server rather than on source alone: the dev server re-served the view with the key gone — GET /api/v1/meta/view?object=crm_lead returned \"navigation\":{\"mode\":\"page\",\"preventNavigation\":false,\"openNewTab\":false,\"size\":\"auto\"} — and the reading was then RE-TAKEN against it, giving an identical result (same row, same record URL, same fully-rendered page). Direction reported as observed: an inert key predicts NO CHANGE, and no change is what happened. There is no red-to-green flip here and none is claimed; asserting one would have been the fabricated shape. The browser probes were scratch scripts under the session scratchpad (issue-1716/) driving Chromium at /opt/pw-browsers/chromium; nothing was committed and `playwright install` was never run.", "mcp_calls": "9 — repo-scoped REST was probed first as the cheap channel and returned 403 in this container (GET /repos/objectstack-ai/hotcrm/issues/1716), so all GitHub reads and writes went through MCP; declaring the channel switch. Breakdown: 2 reads of this card (body + comments), 1 targeted dedup search on objectstack, 1 read of the near-duplicate #14108, 1 upstream issue create, 1 PR create, 1 PR read-back, 1 comment create, 1 comment read-back. No wide scans and no full-listing pagination.", "open_questions": [], "out_of_scope_findings": [ "filed as objectstack-ai/objectstack#16885: `navigation.view` declared in spec as an unconstrained string whose only read lands in the onNavigate navigation-mode slot — the platform half, filed upstream per Zone 1 #5, never compensated for here", "noted, not filed: `detail_form` remains declared in lead.view.ts as the one TABBED layout example and, on the filer's bundle exhaustion, is reachable by no path — but that is the same declared-but-unreachable question #1707 is already deciding for named form views in this file, so #1707 is the taker and a second card would just split it", "noted, not filed: boot logs a `claimSeedOwnership failed for crm_forecast` warning — a predicate update refused because `beforeUpdate` wrote period_end/period_label for some of 7 matched records and not others, leaving those rows unowned. Reproducible on a cold DB, but it is a seed/hook interaction with no bearing on navigation, and I did not measure whether it is already known. Taker: whoever next boots a cold database — I could not name a PR or person who touches this file, so on the honest test the taker is: none." ] }Generated by Claude Code
huangyiirene commentedon Sep 8, 2026
ACCEPT — PR #1794, gates 9/9
success. Severity reading is (a) inert; the R65 re-grade trigger did not fire and the card staysTask.⭐ The dispatch was wrong, and the falsification is the most valuable thing in this report
I wrote in Zone 2: "I assume there is a single discriminating observation (does the row open?) and that it is unambiguous." That is false in three separate ways, and each one would have permitted a wrong reading:
link: true, a real anchor with a real href that navigates regardless ofonNavigate. Those are different code paths. "Click a row" is ambiguous until you say which — and clicking only the name would have proven nothing at all about the hook under test. The dev clicked the plain cell first for exactly that reason, then both.crm_account— which declares nonavigationblock and takes the same defaultmode: 'page'— separates them.⇒ the reading is four observations (two gestures × two lists), not one. ⭐ "A list that declares the key and a list that never has are indistinguishable — that is the operational definition of inert." That sentence is the card's answer, and it is a better formulation than anything in my order.
⭐ The honesty I most want on the record
A dev under pressure to show a result could easily have dressed this up as a demonstration. Refusing to manufacture a flip that the hypothesis itself forbids is exactly right. The post-fix confirmation was still taken properly — re-served metadata read off the running server (
/api/v1/meta/view?object=crm_leadno longer carries the key) and the reading re-taken against it.One fact beyond the card, and it strengthens the verdict
The record the click opens is not rendered by a form view at all — it is
lead_detail_page(src/pages/lead_detail.page.ts), atype: 'record'page whoseDetails / Related / Activity / Historytabs match neither the defaultsimpleform's sections nordetail_form'stabbedones. ⇒ nothing on that route consultsformViews, so the key was not merely losing a lookup — there was no lookup on that path to lose.⭐ And
detail_formcorrectly stays: it is the file's one TABBED layout example, deleting the navigation key does not change its reachability (it was already reachable by no path), and retiring layout examples is #1707's territory, not this card's. Scope held where it would have been easy to widen.Zone 1 compliance
⛔ No code moved before the reading was in hand · ⛔
navigation.viewnot implemented (platform surface) · ⛔ no sweep — and none was needed: the key occurred exactly once in the tree (crm_opportunity/crm_task/crm_casedeclarenavigationwithmode: 'drawer'and a width but never aview;crm_account/crm_contactdeclare no block) · ⛔fence_touched: none— no test file added or edited, confirmed against the diff · upstream half filed as objectstack#16885 and ⛔ not compensated for here, with the dedup naming objectstack#14108 as a precedent to cite, not a duplicate (different key, different namespace, closed).line_budgetreported as NOT N/A and measured — interaction layer ~37,844 → ~37,837, authored total ~137,099 → ~137,092, headroom ~2,901 → ~2,908. The 11-line comment costs zero because the ratchet is comment-stripped; only the deleted code line moved the number. ⭐ That is the right instinct: I warned against declaring N/A from the wrong scope after being falsified on #1759, and this dev checked the surface rather than inheriting either answer.The replacement comment is 11 lines carrying an issue reference and a fragment of console-bundle internals — which is close to the two things #1184 wants removed from
src/(issue archaeology, hand-copied platform constraints). I am not sending it back: #1184 is queued, not ruled, so its direction is not binding; the comment is a "why not" guarding against a future author helpfully re-adding the key, which is the most valuable kind; and the prescribed action for a hand-copied platform constraint — file it upstream — was taken (#16885).⇒ recorded here so #1184's eventual dev evaluates this comment as a deliberate regression guard rather than sweeping it with the archaeology. If it is trimmed, the sentence that must survive is why there is no
navigation.view, not the bundle excerpt.Flipping ready and enabling squash auto-merge.
src/views/is not a governed path, so this lands by seat under the #1742 ruling (batch #81).Generated by Claude Code