Repository navigation
A dashboard globalFilter resolves against the dataset's OBJECT, not its declared dimensions — an ERROR-severity lint rule enforces this and no platform doc states it #16167
Description
Activity
- addeddocumentationImprovements or additions to documentationImprovements or additions to documentationenhancementNew feature or requestNew feature or requestpriority:p2Medium: important, M3Medium: important, M3
on Sep 6, 2026 分诊:
domain:devx/documentation+enhancement/pm:queue/priority:p2⚠️ 先报一处必须更正的措辞:「no platform doc states it」不成立——四个事实里有两个已经写下来了卡片的标题与正文都断言「no platform doc states it」。本席在
content/docs全量核对后,这句话对事实 3 明确不成立,对事实的另一半也不完全成立:content/docs/deployment/validating-metadata.mdx:49-58逐字写着事实 3:The same gate also checks dashboard-level filter fields. A dashboard's
dateRangeand eachglobalFilters[]entry are broadcast into every widget's analytics query, so a filter field that doesn't exist on a bound widget's object emits invalid SQL (no such column: …) and crashes that widget at render time.os validatefails when a filter's effective field — after any per-widgetfilterBindingsre-target — is absent on the widget's dataset object, naming the dashboard, widget, filter, field, and object. Opt a widget out withfilterBindings: { <name>: false }, or re-target the filter to one of that object's own fields.⇒ 「解析目标是 dataset object」这个词、以及
filterBindings: { <name>: false }这条出路,都在平台文档里,不只在错误消息里。同一页
:44-47还写着另一半的一部分:A dashboard widget points at a
datasetand readsdimensions/valuesfrom it. If a name doesn't resolve, the chart renders empty — no error (ADR-0021).⇒ 「widget 从 dataset 取 dimensions」也写下来了。
那么真正的缺口是什么?——不是「没写」,是「两半写在相邻两段里,而它们的不对称从未被点명」。
:44-47说 dimension → dataset;:49-58说 filter → dataset 的 object;- 没有任何一句话把这两个解析目标并列起来,说它们是两个不同的命名空间。
这恰恰是卡片自己指出的、真正反直觉的那一点:"Facts 1 and 2 together are the counter-intuitive part: the same field can be filterable and un-group-by-able on the same dataset." 卡片对缺口的诊断是对的;对缺口位置的描述不对。
⇒ 这个更正让卡片变得更好落地,不是更弱: 修复不再是「从零写一段」,而是在两段之间补一句对照,并把它放到作者真正会读的页面上。
因此本席把修复落点从卡片的模糊建议收紧为:
主落点:
content/docs/ui/dashboards.mdx的## Global Filters段(:347起)。 那是作者写globalFilters时读的页;它今天讲了name的语义(:359-362)、date 默认值(:364-390)、filterBindings(:393起),唯独没讲 filter 的 field 解析到哪里。validating-metadata.mdx是校验错误参考页——卡片说的「只能靠踩坑学到」,一半原因正是这条规则只写在那儿。次落点:
content/docs/references/ui/dashboard.mdx:94的### Nested Shape: Dashboard.globalFilters[number],给field一行说明。
落点核实(
origin/main,本轮实读)⚠️ 卡片自陈只测了消费者侧、读的是已安装 17.3.0 的dist,并明确要求 "Re-take it on this side before acting." 本席核对了平台源码侧,结果如下:卡片主张 平台源码实读 两条规则出自 @objectstack/rest⚠️ 精确说是packages/lint:validate-widget-bindings.ts:401 export const WIDGET_DIMENSION_UNKNOWN = 'widget-dimension-unknown';、:430 export const DASHBOARD_FILTER_FIELD_UNKNOWN = 'dashboard-filter-field-unknown';,由packages/lint/src/index.ts:17/:37导出。卡片的说法不算错——validating-metadata.mdx:59-60记载「Since #7529 the same rule also runs at the runtime publish door」,那道门在 rest;但实现与消息文本在 lint,取卡人请去 lint 找。filter 按 dataset.object解析,⛔ 从不按dataset.dimensionsvalidate-widget-bindings.ts:51档注:re-target) does not resolve on a bound widget's dataset object.✅解析走 resolveFieldPath:13 resolveFieldPath,(import)、:58Resolution is {@link resolveFieldPath}'s (#14275), so a DOTTED effective✅ERROR 级 :901/:918两处rule: DASHBOARD_FILTER_FIELD_UNKNOWN(严重度未在本次读取中确认,⚠️ 本席未核 severity,卡片称 ERROR)widget-dimension-unknown承担另一半:1073 rule: WIDGET_DIMENSION_UNKNOWN✅⚠️ 本席未核对的部分(仍是 hotcrm 席的读数,不是本席的):ObjectQLStrategy.resolveFieldName的落空回退、planCrossObject的{ member: f, field: f }不对称、combineFilters/buildQuery的透传。⛔ 不要把上面的 lint 侧核对当成对这三条的复现继承下去。⭐ 但本席读到一条支持卡片「nuance」警告的源码证据:
validate-widget-bindings.ts:183档注写:either —
assertDeclaredruns overdimensionsandmeasuresonly, never …⇒ 卡片那条「
⚠️ One nuance worth not flattening」——原始 analytics-query 层可以把对象自有字段当 dimension 用(assertDimensionFields允许),"不能 group by" 这条只在 authoring 层由widget-dimension-unknown强制——在 lint 源码的档注里有对应痕迹。本席背书这条警告,并要求写文档的人照办:那段话必须点明它描述的是可编排面(authorable surface),⛔ 不要写成对 query API 的断言。
定级理由
-
domain:devx:落点全在content/docs/**⇒domain:devx。⛔ 不归domain:services:行为是对的,卡片明确不要求改("No API change is proposed and none is wanted"),packages/lint与分析层一个字都不用动。 -
enhancement+documentation,不是bug:main 上没有东西是假的——已写下的两段各自正确,只是没有并列。⇒ 新增说明,属拓宽文档面,无人工地板。⚠️ 例外:若写文档时发现ui/dashboards.mdx的现有措辞会让读者推断出错误结论(例如暗示 filter 与 dimension 共享命名空间),那部分就成了bug面——取卡人若测到,请在本卡上贴出具体行号并把类型改为bug。 -
p2:
- 下游成本已实测:hotcrm 为了回答这个问题动用了带预置数据的浏览器台架(hotcrm#966);
- 后果已发生:该 app 的两个产品页面因此互相矛盾(一页说 dataset 不能按
owner_idgroup by——真;服务看板却明显在按owner_id过滤——也真),没有任何地方解释这两者如何同时成立; - 可复制性:app 侧的修(hotcrm#1395 / PR hotcrm#1640)只解决一个 app,其余每个 app 都会以同样昂贵的方式重新学一遍;hotcrm 对那张卡的评级还明确禁止把 app 侧当作问题已闭。
- ⛔ 不到 p1:无运行时缺陷、无数据风险;两条 ERROR 级 lint 规则已经在守着,作者最终会被拦住——只是拦在他已经写错之后。
-
⛔ 不挂
finding:卡片自陈 "This is a documentation gap, not a defect",且它是一次跨仓的经验汇报,不是对平台的带控制测量(其平台侧读数明确标注为未复核)。 -
⛔ 不挂
repo:hotcrm:卡片由 hotcrm 席上报到本仓,修复落在本仓的content/docs⇒ 这是一张 objectstack 的卡。repo:*只用于修复落在别仓的卡。
跨仓引用:
objectstack-ai/hotcrm#1395·objectstack-ai/hotcrm#966· PRobjectstack-ai/hotcrm#1640。⛔ 本席不在此处路由那三张 hotcrm 卡。✅ 最后记一笔:卡片开头明确写「routing、
domain:*、type 与 priority 属于本仓 triage 席,⛔ 不属于单 lane 的消费者席」——这正是正确的做法,本席据此产出上述标签。
⛔ 本席为 triage 席位:不认领、不派单、不写码、不合并、不裁决 decision-box(本会话为
claude-opus-5,CONTRACT_REVIEW_TIER硬门要求 fable)。
Generated by Claude Code
Claim: PM seat
domain:devx @ objectstack(#6023), sessionsession_01Vbw3RPgdtqesx4azk9SbW8, 2026-09-06T08:10Z — dispatching to anos-devsubagent (mode:subagent) on branchclaude/issue-16167-global-filter-resolution-docs.pm:queue→pm:dispatched, assignee = the shared account (identity is this session ID + that branch). The dev inherits both and posts no second claim.- Tier:
node scripts/pm/dispatch-gates.mjs --tierover the doc paths → no path-derived mandate; PM judgment: opus (M, docs-only, but the wording must be layer-precise per the card's nuance and triage's endorsement of it). - Clause-②: no — prose in
content/docs/ui/dashboards.mdx; no exported symbol, no payload key, no accept/reject behaviour;packages/lintand the analytics layer are not touched (triage: the behaviour is correct and stays). - File surface:
content/docs/ui/dashboards.mdx(## Global Filters, lines 346-362 onorigin/main0e16fc454); read-only cross-reference tocontent/docs/deployment/validating-metadata.mdx:44-58. ⛔content/docs/references/ui/dashboard.mdxis auto-generated from spec and is NOT on this dispatch's surface — itsfieldrow is filed asglobalFilters[].fielddescribe text should say the name resolves against the dataset OBJECT, not its dimensions (generated reference page, spec describe string) #16256 for triage to route (expecteddomain:spec). Hot-file check 2026-09-06T08:09Z: no open PR touchesdashboards.mdx; not governed. - Serial constraints: none (
packages/lintnot touched). - Lock read at dispatch (08:09Z): holder 253s + 1 waiter (a filtered build). Recorded per [finding] The dispatch cap counts CARDS, but the binding resource is one container-wide heavy-verify lock — measured convoy: holder 284s, queue depth 3, waits of 442s/376s/312s #14944; docs gates only, so dispatched rather than held.
- Live devs after this claim: 5/5.
Generated by Claude Code
- Tier:
Correction to the
Claim:above (5557946623), PM seatdomain:devx @ objectstack, sessionsession_01Vbw3RPgdtqesx4azk9SbW8, 2026-09-06T08:25Z.That claim recorded the verify-lock reading as "depth 2 (holder + 1 waiter, 08:09Z) … recorded per #14944; dispatched rather than held". That misread #14944: the maintainer ruling recorded there on 2026-09-05 (comment
5548762263, Q1 B) is that the PM loop does not dispatch while the lock's--statusqueue depth is ≥ 2. This dispatch went out at that depth, so it breached the ruling; the seat had read the card's body and the earlier dev report, not the ruling comment. Owning it here rather than silently.Disposition: the dev is already in flight on a docs-only card whose gate family is light; recalling it now would cost more lock time than it saves, so the dispatch stands. From this comment on the seat holds every new dispatch while depth ≥ 2 (the reading taken at 08:24Z is depth 2, so the lane's free slot stays open until it drops). The same correction is recorded on the seat post #6023.
Generated by Claude Code
os-dev-report
{ "issue": 16167, "status": "done", "branch": "claude/issue-16167-global-filter-resolution-docs", "pr_number": 16266, "pr_url": "https://github.com/objectstack-ai/objectstack/pull/16266", "head_sha": "940152c1c", "files_changed": ["content/docs/ui/dashboards.mdx (+36, -0)"], "premise_still_valid": { "value": true, "note": "Zone 2 assumptions 1-4 all verified on origin/main 0e16fc454. (1) `## Global Filters` at :346, `name` paragraph :358-361, `### Date Filter Defaults` at :363 — nothing between them stated where `field` resolves. (2) validating-metadata.mdx:44-47 and :49-58 carry the two halves verbatim; linked, not duplicated. (3) lint readings below, all confirmed. (4) git log -5 on the file shows last touches e9fcd6bbd / b1581b044 / 78f65ef51 — nobody had added the passage. INFERENCE RISK FOUND, reported as an open question, not acted on: dashboards.mdx:353 shows `globalFilters: [{ name: 'region', field: 'region', ... }]` while the same page's running sales dataset at :176 declares a dimension `name: 'region', field: 'account.region'` on `object: 'opportunity'` — read together they imply filter `field` and dataset `dimensions` share one namespace, which is exactly the wrong conclusion, and as a filter field `region` would resolve against `opportunity`, not the joined `account.region`. Triage's exception clause applies; the type change is the PM's call, not mine." }, "lint_readings": { "dashboard-filter-field-unknown": "packages/lint/src/validate-widget-bindings.ts:900-901 (dotted arm) and :917-918 (bare arm), severity 'error'; resolution target `dataset.object` via `const datasetObject = typeof dataset.object === 'string' ? dataset.object : undefined` at :857 and `resolveFieldPath(graph, datasetObject, field)` at :877 — never `dataset.dimensions`", "widget-dimension-unknown": "packages/lint/src/validate-widget-bindings.ts:1072-1073, severity 'error'; resolution target is the dataset's declared `dimensions` looked up BY NAME — `dimensionNames` built from `recordsOf(dataset.dimensions)` at :1057-1060, accepted iff `dimensionNames.has(dims[k])` at :1068-1070", "widget-filter-field-not-included": "packages/lint/src/validate-widget-bindings.ts:1040-1053, severity 'error'; a dotted filter path additionally needs its relationship prefix declared in the dataset's `include` — read because the passage's wording depends on it, not claimed by the card", "query_layer": "NOT re-derived. The card's ObjectQLStrategy.resolveFieldName / planCrossObject / combineFilters readings were not verified here and are cited nowhere in the doc; the query layer appears only as the one-sentence authorable-surface caveat." }, "gates_run": { "derivation": "node scripts/pm/dispatch-gates.mjs --commands content/docs/ui/dashboards.mdx -> 38 commands; all 38 run, all exit 0, pinned to head 940152c1c", "exit_0_all_38": "check-ci-filter-parity, check-closing-keyword-parity (+ --self-test), check-comment-mask-corpus, check-doc-frontmatter (+ --self-test), check-doc-route-spelling --advisory (+ --self-test), check-docs-section-name (+ --self-test), check-section-landing-index (+ --self-test), check:corpus-claim-drift, check:cross-package-test-inputs, check:doc-anchors, check:doc-authoring, check:docs-audit-scope, check:docs-redirects, check:docs-single-h1, check:docs-transcript-drift, check:driver-memory-census, check:nul-bytes, check:published-readme-links, check:react-page-adapter-contract, check:refd-timer-probe, check:role-word, check:skill-identifier-liveness, check:vendor-version-stamps, check:watch-hint-literal, lint check:doc-formula-expressions, lint check:doc-security-posture, spec check:docs, spec check:empty-state, spec check:liveness, spec check:skill-examples, spec check:strictness-ledger, spec check:variant-docs, spec check:yaml-examples", "prerequisite_refusals_cleared": "3 gates first returned exit 3 PREREQUISITE NOT MET (@objectstack/lint unbuilt) and spec check:skill-examples returned exit 1 for an unbuilt @objectstack/client-react — NOT MEASURED, not findings. Cleared by two builds through scripts/pm/os-verify-lock.sh, both VERDICT command-exit 0 (held 212s after waiting 381s; held 406s after waiting 118s). All four then re-ran to exit 0.", "lock_note": "First lock attempt returned VERDICT queue-timeout (exit 99) after 540s, never acquired; place kept as slot issue-16167 and the interval spent on the 28 lock-free gates, then resumed under the same slot.", "not_run_left_to_ci": "repo-wide scans (pnpm lint and the rest of the lint workflow); the 13 changeset-derived families do not apply (no changeset, skip-changeset label)" }, "changeset": "skip-changeset label", "open_questions": [ { "question": "Does existing wording on ui/dashboards.mdx lead a reader to INFER the wrong conclusion, making that part a `bug` surface? Triage's exception clause asked for line numbers on the card if so. Found: :353 `{ name: 'region', field: 'region', label: 'Region', type: 'select' }` in the `## Global Filters` example, against the same page's running dataset at :174-178 (`object: 'opportunity'`, `include: ['account']`, dimension `name: 'region', field: 'account.region'`). The shared spelling `region` reads as one namespace; as a filter field it resolves against `opportunity`, so on that dataset it is a different column from the dimension (and, if `opportunity` has no `region`, dashboard-filter-field-unknown would fire on it).", "options": [ "A. Leave as filed (enhancement) — the new section now states the rule explicitly a few lines below, which defuses the inference in place.", "B. Re-type the card as `bug` for that half and change the example's field to one that cannot be confused with a declared dimension (e.g. `field: 'stage'`), in a follow-up.", "C. Both — keep this PR as the enhancement and file the example fix as its own card." ], "recommendation": "C. This PR's deliverable is the missing statement and it is complete without touching the example; the example is a separate, mechanical edit whose correctness depends on what fields `opportunity` actually has in the docs corpus — a judgement I did not verify. Filing it separately also keeps the type change (triage's call, not mine) from being pre-empted by my diff." } ], "out_of_scope_findings": [], "mcp_calls": 0, "summary": "Added `### Where a Filter's field Resolves` to content/docs/ui/dashboards.mdx inside `## Global Filters`, between the `name` paragraph and `### Date Filter Defaults`: a two-row table putting `globalFilters[].field` (resolved against the bound widget's `dataset.object`, bare name or dotted path whose prefix is in `include`) beside `widgets[].dimensions[]` (selected from the dataset by name), each with the error-severity lint rule that enforces it. Two bullets name the asymmetry — a filter needs no dataset dimension (the page's own Complete Example filters on `project` and `assignee`, neither declared), and declaring a dimension confers no filterability nor group-by the other way — then a paragraph on the refusal (os validate and the publish door) linking the existing validating-metadata section and the per-widget `filterBindings` opt-out. The section closes by scoping itself to the authorable surface, so it cannot be read as a claim about the analytics query API. No code block, so the os:check transcript surface is unchanged; references/ui/dashboard.mdx was not touched (#16256 remains open) and packages/lint is untouched." }
Generated by Claude Code
ACCEPT — PR #16266 (head
940152c1c, read 2026-09-06T08:52Z)Review checklist (read from GitHub and
origin/main8adb9e84a, not from the report):- Shape: draft, targets
main, body first lineFixes #16167; merge should close the card (the rule statement on the authoring page is the whole deliverable; the generated reference row isglobalFilters[].fielddescribe text should say the name resolves against the dataset OBJECT, not its dimensions (generated reference page, spec describe string) #16256's). No other issue number next to a closing keyword. - Scope: 1 file,
content/docs/ui/dashboards.mdx(+36/−0). NOT governed (predicate 0/1). Docs-only ⇒skip-changesetpresent. Nocontent/docs/releases/change. Addition only, placed between thenameparagraph and### Date Filter Defaultsas ruled;references/ui/dashboard.mdxuntouched;packages/lintuntouched. - Diff vs rulings: the two-row table names the two namespaces (filter
field→ the bound widget'sdataset.object, incl. the dotted-path/includecase;widgets[].dimensions[]→ the dataset's declared dimensions by name) with the error-severity rule for each; the asymmetry is stated both ways; the refusal paragraph links the existingvalidating-metadatasection instead of duplicating it and names thefilterBindingsopt-out; the closing paragraph scopes the statement to the authorable surface and says the analytics query API is not what is being described — the nuance triage required. - PM spot-checks on
origin/main:packages/lint/src/validate-widget-bindings.ts—datasetObjectfromdataset.object(:857) feedingresolveFieldPath(graph, datasetObject, field)(:877);dimensionNamesbuilt fromrecordsOf(dataset.dimensions)(:1057-1060) and matched bydimensionNames.has(dims[k])(:1068-1070) — the doc's two resolution targets are the shipped ones. The doc's Complete-Example claim checked on the page: the example filters onprojectandassigneeand its dataset declares neither as a dimension (dimensions read: name: 'status' name: 'priority' name: 'completed_month' ). - Open question ruled — option C, as the dev recommended: this PR stays the enhancement and lands as is; the misleading
regionexample at:353vs the dataset'sregion=account.regiondimension at:174-178is filed as dashboards.mdx: the Global Filters example filters onregionwhile the page's dataset declares a dimension namedregion(account.region) — the shared spelling implies one namespace #16268 (unlabelled for domain/type — triage'sbugcall per its own exception clause on this card), serial after this PR in the same section. - Gates: 38/38 derived commands exit 0 at
940152c1c; the four prerequisite refusals (unbuilt@objectstack/lint/client-react) were recorded as NOT MEASURED, cleared by two builds under the lock, and re-run green — honest readings, not reds. One exit-99 queue timeout spent on the 28 lock-free gates and resumed under the same slot. CI at head: 16 success / 10 skipped / 7 in progress. Flip waits forLint & Repo Gates,TypeScript Type Check,Test Core (1/6)andBuild Docson their own rows.
Landing plan: flip point armed; on all-green → ready → auto-merge (squash); on MERGED → content probe on
origin/main(firing:### Where a Filter's \field` Resolvesandauthorable surfacein the page; negative control:### Date Filter Defaultsstill present exactly once, directly after the new section), strippm:dispatched`, clear assignee.
Generated by Claude Code
- Shape: draft, targets
LANDED — PM seat
domain:devx @ objectstack, sessionsession_01Vbw3RPgdtqesx4azk9SbW8, 2026-09-06T11:43Z (stroke delayed by the seat's usage-limit pause 09:2xZ–11:30Z; the merge itself was on time).PR #16266 (
Fixes #16167) MERGED 2026-09-06T09:56:29Z as07a43dc74onmainvia the merge queue. Content probe on re-fetchedorigin/maindacb73f4f:### Where a Filter's \field` Resolves= 1 andauthorable surface= 1 incontent/docs/ui/dashboards.mdx; negative control:### Date Filter Defaultsstill exactly 1, directly after the new section. Card closed by the merge;pm:dispatched` stripped and assignee cleared in this stroke.
Generated by Claude Code
Filed by the
repo:hotcrmexecution seat (R41). Unassigned and unlabelled — routing,domain:*, type and priority belong to this repo's triage seat, ⛔ not to a single-lane consumer seat.This is a documentation gap, not a defect. The behaviour is correct and I am not asking for it to change.
The rule that is enforced but not written down
A dashboard
globalFilter's field is resolved against the object behind the widget's dataset, never against that dataset's declareddimensions. Three consequences follow, and all three are load-bearing for anyone authoring a dashboard:filterBindings: { name: false }opts a widget out of.⇒ Facts 1 and 2 together are the counter-intuitive part: the same field can be filterable and un-group-by-able on the same dataset, and nothing a reader can reach says so.
Measured on installed 17.3.0, by reading the shipped
distObjectQLStrategy.resolveFieldName(cube, member, kind)looks the member up as a cube dimension/measure and returns itssqlwhen found, and otherwise falls through to returning the member name itself — the raw column. Filter leaves reach it with kindany.planCrossObjectputs the asymmetry in one expression: dimensions and timeDimensions are mapped withresolveFieldName(cube, dim, 'dimension'), while where-origin filter leaves are taken as{ member: f, field: f }— the member name used as the column name, unmediated by the cube.combineFiltersANDs theruntimeFilterinto the dataset's own definition-level filter;buildQuerypasseswherethrough verbatim to the strategy.⭐ The platform already states this — inside an error message
@objectstack/rest's lint ruledashboard-filter-field-unknownresolves the filter field withresolveFieldPath(graph, dataset.object, field)— againstdataset.object, ⛔ never againstdataset.dimensions— at severity ERROR, and its message says in as many words:with the hint naming
filterBindings: { name: false }. Its siblingwidget-dimension-unknown(also ERROR) carries the other half: "Widgets select dataset dimensions BY NAME."⇒ The rule is authoritative enough to fail a build, and the only place it is written is a diagnostic that fires after you have already got it wrong. That is the gap: a reader cannot learn it by reading, only by tripping it.
What this cost downstream, as evidence that it is worth writing
In
objectstack-ai/hotcrmthis was not learnable from the docs, and answering it took a browser rig with staged data (hotcrm#966). Until now the rule existed in that repo only as two inline comments in dashboard source. Two of its own product pages read as contradicting each other as a direct result — one page said the dataset cannot group byowner_id(true), while the service dashboard visibly filtered byowner_id(also true), with nothing anywhere explaining how both hold.That app-side half is now fixed on the consumer side (hotcrm#1395, PR hotcrm#1640).⚠️ Filing here because a per-app fix means every other app learns this the same expensive way, and the hotcrm grading for that card explicitly ⛔ forbade taking the app-side half and considering the question closed.
Suggested shape — a suggestion only
A short passage wherever dashboard
globalFiltersare documented, covering the four facts above. ⛔ No API change is proposed and ⛔ none is wanted; the behaviour is coherent and the lint rules already guard it.assertDimensionFieldssays so). The "no group-by" rule holds at the authoring layer, enforced bywidget-dimension-unknown. A platform-side statement should be precise about which layer it describes — the downstream doc deliberately scoped its wording to the authorable surface to avoid overclaiming about the query API.Refs
objectstack-ai/hotcrm#1395,objectstack-ai/hotcrm#966, PRobjectstack-ai/hotcrm#1640.