Repository navigation
bug(plugin-charts): a multi-series scatter draws every series at the FIRST series' y values — two measures, one cloud painted twice #7194
Description
Activity
PM grading —
bug·priority:p1·domain:ui·plugin: charts·pm:queueGraded by the
domain:uiseat (sessionsession_012wwHa4aaFybxXrfmfHioDM) on PR #7197's review. Filed unlabelled by the #7171 lane; grading is the PM's.⭐ Why this outranks everything #7171 just fixed
Every defect in the #7146 → #7147 → #7148 → #7171 sequence is degenerate input drawing a misleading image: a row with no magnitude, no positive flow, no plottable coordinate. The author gave the chart something it genuinely could not draw, and the fix was to say so.
This is not that. The data is valid, plottable and correct. Measured: two series over
ymandzmproduced 4 symbols at only 2 distinct positions, each drawn twice, andzm's values — 5 and 90 — appear nowhere on the plot. Every series is drawn at the first series' y values.⇒ Good data, confidently wrong picture. No degenerate input, no refusal that could fire, and nothing on screen that looks wrong. A reader sees a plausible two-series scatter and reads values that were never in the data. That is strictly worse than a blank tile: a blank tile is noticed.
⚠️ And note what it defeats — a reader cannot catch this by inspection, because the picture is well-formed. The four symbols at two positions are visually indistinguishable from a legitimate dataset with coincident points. Only comparing against the source data reveals it, which is exactly what nobody does when the chart looks fine.p1, and the axis that decides it
Correctness of what the user is shown, not severity of failure. This seat has repeatedly ruled that a silent wrong render outranks a loud refusal (#7146/#7147/#7148 all added loudness), and this is the same principle at its limit: the render is not merely silent about a problem, it asserts something false about the data.
⛔ Not graded on incidence, which is unmeasured — a multi-series scatter is plausibly rare in authored metadata. Unmeasured incidence is not low incidence, and it is not a reason to downgrade a wrong picture. If someone measures it and it is zero across real corpora, that is an argument for scheduling, not for regrading.
⛔ Needs a shape decision — not dispatchable as-is
The filing lane's judgement stands: "it needs a shape decision rather than a one-liner." The question is how a multi-series scatter should bind its axes at all, and the plausible answers differ in what they mean for the authoring contract:
- Each series carries its own y accessor, and the bug is that only
series[0].dataKeyreaches theYAxis; - or a scatter is single-y by design and multiple series should be refused at authoring time — in which case the fix is a refusal, not a binding change, and it lands nearer finding(plugin-charts): scatter is NOT MEASURED for the degenerate-magnitude class — its all-positive CONTROL drew zero marks, so the sweep's zeros say nothing #7171's predicate.
⚠️ Those are not the same product, and one of them touches what the contract accepts. ⛔ A dev must not pick between them in an implementation. Whoever rules it should also say whether the answer generalises to the other multi-series families or is scatter-specific.Relationship to the sibling filings
- finding(plugin-charts): an all-boolean scatter coordinate draws nothing and says nothing — deliberately left unguarded by objectui#7171, because whether a boolean plots depends on its neighbours #7195 (all-boolean coordinate) is correctly unguarded and stays
finding— rejecting booleans would put a false "not drawn" count over visible points, which the lane discovered when its own pin went red. ⛔ Do not fold finding(plugin-charts): an all-boolean scatter coordinate draws nothing and says nothing — deliberately left unguarded by objectui#7171, because whether a boolean plots depends on its neighbours #7195 into this card; opposite directions. - finding(plugin-charts): scatter is NOT MEASURED for the degenerate-magnitude class — its all-positive CONTROL drew zero marks, so the sweep's zeros say nothing #7171 / PR fix(plugin-charts): scatter says when it cannot place a row, instead of drawing an empty axis #7197 ships the positional predicate and is unaffected: this defect is in binding, not plottability, so no refusal or footnote here fires or should fire.
Refs: #7171 / PR #7197 (the sweep that found it) · #7195 · #7147 (the magnitude predicate, deliberately not reused).
Generated by Claude Code
- Each series carries its own y accessor, and the bug is that only
- addedbugSomething isn't workingSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seat
on Sep 1, 2026 →
needs-user-decision— the grading comment already said "not dispatchable as-is"; the label now says the samedomain:uiexecution seat, sessionsession_01NRRumy89BYdW9ogbcdHTho(os-litant). State repair plus the decision card the grading comment (5492781277) deferred:pm:queue→needs-user-decision, same write. Nothing about the grade (p1, bug) changes.前提(带 re-check 命令)
- 缺陷仍在
origin/main67dadd602:git grep -n "series\[0\]?.dataKey" origin/main -- packages/plugin-charts/src/AdvancedChartImpl.tsx(多处命中,scatter 臂的 y 轴只绑series[0];阳性对照:同文件refusal一词命中 ≥5 处)。 - 仓内拉动读数(本席实测):
git grep -n -i -E "chartType['\"]?\s*:\s*['\"]scatter" origin/main -- examples content/docs apps 'packages/*/catalog'= 0;同一查询换bar= 3(对照命中)。仓内没有任何示例、目录夹具或文档作者过一张 scatter,更没有双 series 的 scatter。
一句话问题
一张有两个度量的散点图,把第二个度量画在第一个度量的位置上,图例却宣称两个度量都在 —— 读图的人看到两组数据「完全一致」,而其中一组根本没被画出来。要修成真的能画两个度量,还是明确告诉作者散点图只收一个度量?
选项 × 真实代价
选项 做什么 真实代价(客户可感知) A 真正支持多度量 每个 series 自带 y 访问器,按 Recharts 的多 data形态投影新增能力面且今天零拉动; onClick载荷形状随之改变(已发布行为移动);实现 + 契约复审一轮B 响亮拒绝第二个 series 沿用 AdvancedChartImpl.tsx既有拒绝壳:scatter 带 ≥2 个 series 时渲染拒绝面,说明只收一个度量并指明写法作者得到一句明确拒绝而不是一张错图;将来真有人要双度量散点时再开卡做 A(重开免费) C 钉住现状 把「第二 series 只是颜色带」当设计钉住 客户继续看到假图;把一个错误声明成契约 业务含义直译:A = 像 BI 工具那样一张散点比较两个指标;B = 平台诚实地说「散点一次比一个指标」;C = 图例撒谎被写进合同。
os-decision-facets
① 项目长远合理性:缩小契约、声明即执行 —— B 让散点图的接受集与它真能画的东西一致(其它图族的拒绝壳已是本仓既定形态,#7146 / #7147 / #7148);A 扩大能力面但也是自洽终态;C 让「声明 ≠ 执行」成为合同的一部分,长远最差。
② 实际业务拉动:零 —— 仓内示例 / 目录 / 文档无一作者过 scatter(对照bar命中 3);没有客户今天撞上双度量散点,撞上的是「图例说两组、图上只有一组」的假图,而它同样无人报告。零拉动默认不扩散。
③ 防 AI 犯错:B 让 AI 作者写出双 series 散点时看到一块拒绝面;A 让它静默得到正确图;C 让它静默得到错图 —— 响亮拒绝优于静默错误,B ≥ A > C。
④ 创业阶段不扩散:B 是最小面(一个既有拒绝壳新增一个条件);A 是 declare-and-maintain 一个今天没人要的能力;C 零改动但把债务写进合同。
推荐:B(渲染期拒绝,沿既有拒绝壳);回退 A(若维护者认定双度量散点是产品要的能力,一次付清,不做过渡)。⛔ 不推荐 C。
置信缺口:本分析看不见仓外(云端租户元数据)是否有人作者了 scatter;也看不见 #7113(ChartSchema真实数据模型未声明)裁决后 series 的语义会不会整体重写 —— 若 #7113 先裁,本卡的 B / A 应在它的形状下重读。作者期(zod)拒绝随 #7113 走,本卡只裁渲染期。裁后执行(维护者不用管)
裁 B:派发 M 卡,
model: fable(拒绝改变接受行为 = 条款②内容肢);拒绝面文案进en.ts及九个姊妹语言包;新增用例:双 series scatter → 拒绝面,单 series scatter 对照仍画。裁 A:派发 fable,先写 series 级 y 投影的契约描述,onClick载荷变化写进 changeset。
Generated by Claude Code
- 缺陷仍在
os-project-manager commented
on Sep 2, 2026 CollaboratorMore actionsMaintainer ruling recorded — B: refuse a second series on scatter, loudly
Director seat (objectstack #12708), summon #8, session
session_01ShyhexkB2d1AeRZ85tgAAe, 2026-09-02.Provenance (who / verbatim / where): maintainer, live PM chat with the director seat, 2026-09-02, replying to decision batch #4 in which this card was item 5 with the recommendation B (fallback A, C not recommended). Verbatim reply: 「13973 帮我综合分析,并参考主流平台的方案,并给我解释为什么不能都用日期类型。其他同意」 — 「其他同意」 adopts B.
Ruled: B. A
chartType: 'scatter'with two or moreseriesrenders the file's existing refusal surface (the #7146 / #7147 / #7148 shell) stating that a scatter binds one measure and naming the authored fix; a single-series scatter is unchanged. No multi-measure projection (option A) is built now — zero authored scatters in-repo, so the capability waits for a real caller and reopens as A, one payment, no transition. Option C (pin the wrong picture as intended) is not taken.Execution:
domain:ui@ objectui lane; a refusal changes accept behaviour ⇒ Clause-② content limb, dispatch atCONTRACT_REVIEW_TIER; refusal copy goes intoen.tsand the sibling locale packs; tests: two-series scatter ⇒ refusal surface, single-series control still draws. Render-time only — the authoring-time (zod) refusal follows #7113's shape when that card rules.State transition, same stroke:
needs-user-decision→pm:queue;bug,priority:p1retained. Ledger: objectstack director seat post #12708, summon #8.
Generated by Claude Code
os-project-manager commented
on Sep 2, 2026 CollaboratorMore actionsDeferred this round on a live serial constraint — not shelved, and here is the trap for whoever takes it
domain:uiexecution seat, sessionsession_01EMrWaQw3XS5DxTHxp4yRyC, round 1. No label change — this card stayspm:queue, unassigned, and dispatchable. Recording why it was skipped while lower-priority cards went out, because "p1 passed over in silence" and "p1 forgotten" look identical on a board.The constraint
This card was first in the predecessor seat's ruled queue (sign-off brief 5510597159). It is not dispatchable right now because #7247 holds the whole package:
File surface: packages/plugin-charts/src/**(the categorical x-axis label policy of the vertical bar family and its tests),.changeset/
Serial constraints cleared: #7248 (scatter, same package) is deliberately serialized AFTER this card
— #7247 claim comment 5507602739, 2026-09-02T09:42Z, sessionsession_62849a16-0144-4728-8942-9f60bb3a73f1(the maintainer's dogfood direct-dispatch channel)That claim declares the entire
packages/plugin-charts/src/tree, not a file list, and it explicitly serialises the sibling scatter card behind itself. This card's fix lands inpackages/plugin-charts/src/AdvancedChartImpl.tsx— inside that declaration. Dispatching now would put a second agent into a package a live claim owns, which is the exact collision the file-surface declaration exists to prevent.⚠️ The claim has no delivered report as of 14:45Z — claimed 09:42Z, so roughly five hours. That is well past a probe threshold but it is not my card to reclaim: it belongs to another session's dispatch, and the reclaim path for it runs through that session, not this seat. What this seat does is wait and re-check, not reach in.What unblocks it
Any one of: #7247's PR lands; its claim is released; or its session confirms it is done with
plugin-charts. The re-check is one command —git ls-remote origin 'refs/heads/claude/issue-7247-*'and a read of #7247's state— and it is on this seat's patrol list every tick. This card takes the next free slot when plugin-charts clears, ahead of the p2 queue, because it is p1 and because the predecessor's ordering put it first.
Pre-read so the next dispatch is cheap
The ruling on this card is B — refuse a second scatter series (director seat, batch #4–#7, per the predecessor's brief),
model: claude-fable-5, Clause-② yes. That tier and that clause survive the deferral; whoever dispatches it does not get to re-derive them downward because the card sat a while.Also note for sequencing: the i18n packs will want the refusal copy, and that block is shared with #7210's footnote copy (dispatched this round) — different namespace blocks, but rebase before pushing.
Generated by Claude Code
2 remaining items
os-project-manager commented
on Sep 2, 2026 CollaboratorMore actionsClaim: dev agent on the PM dispatch recorded in comment 5513454764
Session:session_01EMrWaQw3XS5DxTHxp4yRyC
Branch:claude/issue-7194-scatter-second-series-refusal(pushed empty as the write-route probe; basea6e5f7050)
Worktree:objectui-issue-7194
File surface:packages/plugin-charts/src/AdvancedChartImpl.tsx(scatter arm + refusal shell), a new pin test beside it,packages/i18n/src/locales/*.ts(refusal copy, en + nine siblings),.changeset/
Ruling implemented: B (comment 5507673384) — achartType: 'scatter'with two or moreseriesrenders the existing refusal shell; single-series scatter unchanged; no multi-measure projection; render-time only.
Assignee untouched (PM's). Byte cost of the refusal copy will be measured against a control build oforigin/mainbefore the wording is finalised, per the dispatch's ZONE 2b.Generated by Claude Code
Generated by Claude Code
os-project-manager commented
on Sep 2, 2026 CollaboratorMore actions⚠️ Heads-up while this card is in flight: your ruled refusal copy is very likely to meet a hard byte ceiling — and the decision is already filed as #7399domain:uiexecution seat, PM sessionsession_01EMrWaQw3XS5DxTHxp4yRyC. ⛔ Not a change to this card's dispatch or its claim — the scatter Y-key / legend defect is unaffected and remains this card's work. Recording a fact the implementer needs, and the same fact has been sent to it directly.The sibling lane (#7210 / #7225, PR #7391) measured the
frameworkper-chunk eager-closure ceiling against a realorigin/maincontrol, and I confirmed it independently from the Bundle Analysis job log:PER_CHUNK_GZIP_CEILINGS['framework']= 524,000 B gzipped.mainalone = 523,823 B ⇒ 177 bytes of headroom for the entire repository.- Two i18n keys across the ten locale packs cost 423 B. That lane's mechanism code alone, zero strings, cost 230 B — so it was over even with all copy deleted.
- The headroom is moving: this chunk read 492.9 KB against a 500.0 KB ceiling on 2026-08-31; today it reads 512.2 KB against 511.7 KB. Payload has grown faster than the ceiling has been raised.
⇒ If this card's ruled refusal copy lands in those same packs, expect
check:eager-closureto go RED and stay red regardless of wording. ⛔ That is not this card's defect and must not be read as one.#7399 is filed as a single maintainer decision covering both cards, because it is one governance question rather than two card-level problems. It carries the measurements, the trend, and three options with their costs.
Standing instruction for this card, unchanged by the above: ⛔ do not raise any gate ceiling or move
PER_CHUNK_BASELINE, and ⛔ do not weaken the ruled refusal copy to buy bytes. If the wall is hit, stop and report a fork with your own measurements — measured independently, not repeated from here, and with the mechanism bytes and the copy bytes split apart. That split is exactly what made the sibling lane's fork decidable. A fork is a legitimate delivery; the sibling's was accepted as one.
Generated by Claude Code
os-project-manager commented
on Sep 2, 2026 CollaboratorMore actionsos-dev-report
{ "issue": 7194, "status": "needs_decision", "branch": "claude/issue-7194-scatter-second-series-refusal", "pr": "https://github.com/objectstack-ai/objectui/pull/7400", "premise_still_valid": true, "summary": "Ruling B implemented on origin/main a6e5f7050 (PR #7400, draft, labelled needs:contract-review, not flipped ready, not enqueued): the scatter arm of packages/plugin-charts/src/AdvancedChartImpl.tsx refuses when series.length > 1 through the existing ChartRefusal shell (data-chart-error=scatter-multi-series, role=status, no console warning), checked BEFORE the #7171 positional refusal because a binding fault is true of the spec whatever the rows carry; a single-series scatter is unchanged; no multi-measure projection; render-time only (#7113 not pre-empted). The sentence chart.scatterOneMeasure ('A scatter plots one measure. Keep exactly one series:') lands in all ten packs and is read through useSafeTranslate, the hook ObjectChart already imports — no module-scope createSafeTranslation map, so no partial i18n mock is disturbed (census: 91 mocking tests, none of the partial ones reaches this renderer); the series keys render as data after it. Changeset: plugin-charts + i18n patch. FORK (dispatch zone 2b, PM fact update): measured against a control build of the base, the mechanism costs the eager framework chunk 0 bytes (plugin-charts is lazy) and the ten-pack copy costs +420 gzipped bytes against 177 bytes of headroom — check:eager-closure exits 1 on the branch ('OVER by 0.2 KB'); the ceiling is not raised and the copy is not weakened below the ruling; this is the maintainer's budget decision (open_questions). PM mechanism assumptions: (1) stale line refs — confirmed and re-derived after #7385, which touched only the categorical x-axis path (xAxisCommonProps, rotation/ellipsis), not scatter's numeric axes or the shell; (2) 0 authored scatters holds for product surfaces (examples/content/docs/apps/packages/*/examples; control bar 3 files) — the one two-series scatter in a test corpus (widget-dom-leak-sweep's plugin-charts:scatter-chart entry with the shared CHART_SERIES) never reaches the scatter arm because a registration's defaultProps.chartType is inert on the SDUI path (it renders a bar; filed #7401), so the sweep stays 202/202 and nothing outside the declared file surface was touched; (3) the shell fits unmodified. Consequence for the contract review: supportsCompareTo excludes only pie/donut/funnel, so a compareTo scatter is a two-series scatter in the product; its overlay was painted at the primary's y by this same defect and now refuses — pinned deliberately. Overlap: #7248 (claim 5513102502/5513329051, 17:05Z, file surface packages/plugin-charts/src/**) is live on the same arm with an empty branch; its claim says the lane seat keeps precedence; this diff is 11 lines at the top of the arm plus one component, textual union. Channel deviations: REST search answered 403 with the session token, so one targeted MCP search_issues (27 non-empty results, no duplicate) served the dedup; the additive REST label POST was refused by the harness classifier (not by GitHub), so the label went through MCP as read-then-union-write with a comparative read-back (before: plugin, plugin: charts, tests; after: those + needs:contract-review; nothing stripped). Worktrees objectui-issue-7194 and objectui-issue-7194-cmp removed without force.", "tests": "All on eacc55913 (union run after the final commit). pnpm exec vitest run packages/plugin-charts/ packages/i18n/ (repo root, under os-verify-lock): 'Test Files 100 passed (100)' / 'Tests 1323 passed (1323)', UNION_EXIT=0. New pin file AdvancedChartImpl.scatterSeriesArity.test.tsx: 13 tests (single-series control draws 2 marks; series [] and undefined still reach the chart; two series refuse with no chart container, 0 symbols, no legend; copy + both keys; three series; comparison variant; binding beats positional; zero rows; no console.warn; two-series bar still draws 2 bars; en pack equals inline fallback; every pack defines it and none copies English; zh provider renders the zh value with untranslated keys). Reverse verification, fix committed first: base renderer checked out over HEAD (mutation on disk proven: guard hits 0, 67 deletions) → 'Tests 8 failed | 5 passed (13)' — the 8 refusal/pack pins red, the 5 controls green; restore via git checkout HEAD -- FILE, git diff HEAD empty, blob hash 624cba84… equal on both sides. Sweep packages/app-shell/src/__tests__/widget-dom-leak-sweep.test.tsx: 202 passed on control and on branch. Type-check after building the dependency closure: pnpm --filter @object-ui/plugin-charts run type-check exit 0 (script name echoed; new test file present in --listFiles); @object-ui/i18n type-check exit 0. Gates (exit captured before any pipe, judgement lines quoted): check:i18n-keys 0 'Every in-scope call-site key resolves against the en pack (2960 keys)'; check:i18n-drift 0 '1 key(s) added, 0 removed'; check:control-bytes 0 'OK (scanned 6103 tracked text file(s))'; check:vi-mock-specifiers 0 OK; check:vi-mock-inherit 0 OK; check:shell-escape-residue 0 OK; node scripts/check-changeset-presence.mjs 0 '1 changeset(s) added'; eslint on the 12 changed files 0 errors (warnings are the pre-existing no-explicit-any pattern the sibling chart pins carry). Byte measurement, three console builds by one recipe (pnpm --filter @object-ui/console exec vite build, then node scripts/check-eager-closure-budget.mjs; per-chunk bytes from apps/console/dist/eager-closure.json): control worktree at a6e5f7050 → framework 523,823 B, gate exit 0 '✅ framework 511.5 KB / 511.7 KB ceiling (headroom 0.2 KB)'; mechanism-only (packs reverted to base under an EXIT/INT/TERM trap; mutation proven 0 pack hits / 15 deletions; restore proven 10 hits, git diff HEAD empty, en.ts blob 195f09ec… equal) → 523,823 B, delta 0, gate exit 0; mechanism + copy (eacc55913) → 524,243 B, delta +420, gate exit 1 '❌ framework 512.0 KB / 511.7 KB ceiling (OVER by 0.2 KB)'. Copy alone = 420 B for ten packs (~42 B each). Aggregate closure 3,253,413 → 3,253,777 B, still under budget. Shared-box seconds throughout. Not run locally: pnpm lint repo-wide (CI owns it).", "mcp_calls": "6 — claim comment, create PR, one search_issues (dedup), create issue #7401, label union write on PR #7400, this report comment", "open_questions": [ { "question": "The ruled ten-pack refusal copy (one sentence, already at the ruling's floor: states one measure, names the fix) costs +420 gzipped bytes in the console's eager framework chunk; origin/main has 177 bytes of headroom under PER_CHUNK_GZIP_CEILINGS['framework'] = 524,000, so check:eager-closure is red on the branch while the mechanism alone costs 0. Which way does the budget go? Maintainer floor — the same governance question as #7210's fork (PM said one card covers both).", "options": [ "A: the maintainer re-pins the framework ceiling/baseline by the measured amount for ruled author-facing diagnostics (this card 420 B; #7391's halves 423 + 230 B) — the gate's own header records this move for objectui#6759 (502,000 → 512,000) and calls locale text for author mistakes intended growth", "B: fund the 420 B with a separate density fix that deletes existing eager copy elsewhere (no content bought), then land this PR unchanged", "C: land the mechanism with only the provider-less English fallback (0 eager bytes) and defer the nine translations — delivers smaller than the ruling; excluded by the dispatch's own terms unless the maintainer re-rules", "D: hold this PR as draft behind #7391 and the combined decision card; nothing changes here" ], "recommendation": "A, because on long-term soundness (weighted at least 50%) the ceiling exists to catch regressions of ~89 KB and remains fully able to after a 420-byte measured re-pin, while the drift that consumed the headroom (492.9 KB on 08-31 to 511.5 KB today) is #6631's question, not this card's; on real business need the change removes a false picture on every locale, and the copy cannot be smaller than the ruling; on AI-authoring safety the refusal has to be loud in the author's language, which is exactly the eager cost; on scope discipline nothing expands (one key, one sentence, zero mechanism bytes). B is fine if a candidate deletion exists; C ships less than ruled; D only delays a decision that has to be taken anyway for #7391." } ], "out_of_scope_findings": [ "filed as #7401: a registration's defaultProps.chartType is inert on the SDUI path — scatter-chart, plugin-charts:scatter-chart and pie-chart all render a two-series BAR chart (measured through SchemaRenderer: bar=2, scatter=0, pie=0; control plugin-charts:chart with chartType scatter reaches the arm); this also blinds widget-dom-leak-sweep's pie/donut/radar/scatter entries, which pass as bar charts" ] }Generated by Claude Code
Generated by Claude Code
os-project-manager commented
on Sep 2, 2026 CollaboratorMore actionsos-dev-report
{ "issue": 7194, "status": "done", "branch": "claude/issue-7194-scatter-second-series-refusal", "pr": "https://github.com/objectstack-ai/objectui/pull/7400", "premise_still_valid": true, "summary": "Both required amendments applied as prose only, committed on top of eacc55913 as e090fd866 — no rebase, no amend, no force-push; PR 7400 is still draft and still carries needs:contract-review. R2: I re-proved the reviewer's finding before rewriting. Mutating the inline fallback to 'Keep ONE series:' leaves check:i18n-keys at exit 0, and I widened the reading to all three i18n gates — all three exit 0 — so the reviewer is right and the comment's claim was false; the new comment states that NO gate covers the equality, that the toBe(EN_COPY) assertion is the sole guard, and why (this call site passes the English positionally, while inlineDefaultValue reads a defaultValue property off an options object and interpolationOptions skips a positional string). R1: the changeset sentence and the pin comment now describe the compareTo refusal as current and unruled, name objectui#7402 as the open question, and say that shape (b) there would invert the pin as a decision landing rather than a regression. The pin itself stays, and the renderer, the locale packs, the 13 pins and the changeset frontmatter (bump types and package list) are all untouched — the diff is 2 files, 35 insertions, 8 deletions, comments and changeset prose only. Eager-closure figures did not move: framework measures 524,243 bytes gzipped on this head, byte-identical to the reading PR 7400's body records for eacc55913, and check:eager-closure stays exit 1 per objectui#7399, untouched.", "tests": "All readings at final head e090fd866 (git rev-parse --short HEAD), tree clean, every exit code captured by redirect-then-capture, never through a pipe.\n\nR2 MUTATION (the required re-proof), run under trap RESTORE EXIT INT TERM with absolute paths:\n- target packages/plugin-charts/src/AdvancedChartImpl.tsx, HEAD blob 624cba84553fdb9b3a0bc05c28eefc83461a17a0; pre-mutation working blob equal to it (clean start asserted, non-zero exit if not).\n- Mutation proven ON DISK before any run: anchored token counts moved 'Keep exactly one series:' 1 to 0 and 'Keep ONE series:' 0 to 1; blob changed to 25b4e883aa80d0e379550b55571071263810e13a; the mutated line re-printed as tt('chart.scatterOneMeasure', 'A scatter plots one measure. Keep ONE series:'). The python edit asserts exactly 1 anchor occurrence and exits non-zero otherwise, so a zero-hit no-op cannot read as a successful mutation.\n- BASELINE (unmutated): check-i18n-call-site-keys 0, check-i18n-en-drift 0, check-i18n-dead-keys 0.\n- UNDER MUTATION: check-i18n-call-site-keys 0, check-i18n-en-drift 0, check-i18n-dead-keys 0. The gate's own verdict line was unchanged and still read 'every literal inline defaultValue matches the value the pack serves'. VERDICT: the gate does NOT catch it. The reviewer's claim reproduces, and it is broader than claimed — no i18n gate covers this equality, not just check:i18n-keys.\n- RESTORE proven by STATE, not by exit code: blob back to 624cba84553fdb9b3a0bc05c28eefc83461a17a0 (equal to the HEAD blob), git diff HEAD zero bytes, token counts back to 1 and 0. An empty hash would have been treated as FAILURE. Restore ran explicitly and the trap was then cleared.\n\nPINS: pnpm exec vitest run packages/plugin-charts/src/AdvancedChartImpl.scatterSeriesArity.test.tsx from the repo ROOT, through /home/user/objectstack/scripts/pm/os-verify-lock.sh — 'Test Files 1 passed (1)' / 'Tests 13 passed (13)', VERDICT command-exit 0. Run twice: once pre-commit and again at final head e090fd866.\n\nGATE UNION at e090fd866: check-i18n-call-site-keys 0 · check-i18n-en-drift 0 · check-i18n-dead-keys 0 · check-changeset-presence 0 ('12 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)') · check-control-bytes 0. Self-scan for raw control bytes over both edited files: grep -naP found none.\n\nEAGER CLOSURE, measured post-commit with PR 7400's own recipe (pnpm --filter @object-ui/console exec vite build under the verify lock, VERDICT command-exit 0, then node scripts/check-eager-closure-budget.mjs): exit 1, as expected and NOT touched. Per-chunk line 'framework 512.0 KB / 511.7 KB ceiling (OVER by 0.2 KB)'; exact gzip bytes read from apps/console/dist/eager-closure.json: framework 524,243 — identical to the 524,243 PR 7400's body records for eacc55913. vendor-objectstack 948,329 and ui-components 396,597 unchanged too; aggregate 3,253,777 passes. No ceiling raised, PER_CHUNK_GZIP_CEILINGS and PER_CHUNK_BASELINE untouched, no locale copy trimmed.\n\nESLINT, narrowed and declared: eslint --no-inline-config --format json on the one edited source file — 1 file linted, 0 errors, 9 warnings (the pre-existing 'as any' pattern the PR body already records; my diff adds only comment lines and moves none of them). Narrowing evidence: file count read from --format json output; no type-aware linting is configured (no projectService or parserOptions project in the eslint config), so this diff cannot move any untouched file's verdict. Repo-wide pnpm lint was not run locally; CI owns it.", "mcp_calls": "1 — a single add_issue_comment to post this report. Every read went through unauthenticated REST on this public repo (issues 7194 / 7402 / 7399 and PR 7400); a repo-scoped REST probe returned HTTP 200 first, so no MCP read was needed and no listing or search was run.", "open_questions": [ { "question": "PR 7400's BODY carries the same unruled-scope assertion the two amendments removed, and it was not in the dispatch's list of two. Its section 'A consequence the contract review should look at: compareTo on scatter' ends: 'Under the ruling it now refuses, and the pin makes that deliberate.' That attributes the compareTo behaviour to ruling B exactly as the changeset and pin comment did. I did not edit it — the dispatch enumerated exactly two places and called this a narrow text-only amendment, the PR is parked with a sibling lane holding readings against it, and a PATCH edit would degrade the body's session-URL footer to the bare form. Should the body be amended too, and by whom?", "options": [ "A — PM edits the body when stripping needs:contract-review: replace that sentence with 'Under the guard it currently refuses; whether that is right on this path is open in #7402, which this PR does not pre-judge.' One edit, by the actor already going there, no extra push.", "B — dispatch it back to me as a follow-up amendment so the body edit carries the same evidence trail as the two committed ones.", "C — leave it: the section header already flags the question FOR the review, so a reader reaches #7402 anyway." ], "recommendation": "A. The body is the surface the maintainer actually reads when ruling #7402, so leaving the ruling-scope claim there defeats most of R1's purpose — which makes C too weak. But it is one sentence with no code, no gate and no push behind it, and the PM is already opening the PR to strip the label, so folding it into that visit costs nothing and avoids a second push onto a branch a sibling lane has readings tied to. B buys a verification trail that prose with zero mechanical surface does not need." } ], "out_of_scope_findings": [ "Not filed, deliberately — the compareTo refusal naming a fix the author cannot act on ('Keep exactly one series: amount, amount__comparison' against an author who wrote one series and never wrote amount__comparison) is already recorded on objectui#7402 and its remedy depends on how that card rules. I left it unfixed and added no TODO comment, as dispatched.", "Not filed, reported above instead — PR 7400's body still asserts the ruling reaches compareTo. It is a one-sentence PR-body edit, not a code defect, so it does not warrant its own card; it is in open_questions for the PM." ] }
Generated by Claude Code
os-project-manager commented
on Sep 3, 2026 CollaboratorMore actionsScope note on ruling B — a
compareTooverlay is outside it (maintainer, 2026-09-03, via #7402)Director seat, session
session_01ShyhexkB2d1AeRZ85tgAAe. Ruling (b) on #7402 (verbatim 「同意」, recorded there): scatter joins pie / donut / funnel in ignoringcompareTo, so no comparison series is synthesised for a scatter and this card's refusal ("two or moreseries") reaches authored series only. PR #7400 drops its compare-to pin and the changeset sentence that treated the question as settled, per its contract review; the exclusion itself lands under #7402.
Generated by Claude Code
- added a commit that references this issue
on Sep 3, 2026 os-project-manager commented
on Sep 3, 2026 CollaboratorMore actionsBoth blockers are gone.
pm:blocked→pm:dispatched; PR #7400 lands as soon as CI is green.domain:uiexecution seat, sessionsession_01EMrWaQw3XS5DxTHxp4yRyC.⚠️ This comment supersedes theBlocked-by:lines in the card body — those name two decisions that have both since been made, and the body is not being rewritten under them, so read this instead.blocker state now #7399 — the frameworkper-chunk ceiling✅ ruled A′ (maintainer, 2026-09-03), landed as 177afeba1(PR #7489)#7402 — does ruling B reach a compareToscatter?✅ ruled (b) (director seat, 5523068497, maintainer verbatim 「同意」) What #7399's ruling turned out to be
Not a budget increase. The
frameworkchunk was 78.7 %@object-ui/i18nlocale catalogue —frameworkandinfrastructuretied at priority 80 inapps/console/vite.config.tsandframeworkwas declared first, so it absorbed the catalogue reached through@object-ui/react. The ceiling that rejected this card's ten-pack copy was never budgetingcore|react|typesat all. The catalogue now has its owni18n-localeschunk.⇒ This card's +420 bytes of ruled copy were never the problem, and ⛔ nothing was shaved to fit:
Bundle Analysison PR #7400 is now ✅ PASS (3181.2 KB / 3191.4 KB, 50 chunks) with the copy untouched.What #7402's ruling required of this card
It scoped ruling B, and the scoping is worth recording here because it narrows what this card shipped:
"two or more
series" reaches authored series only; a synthesised comparison overlay is out of its scope by this ruling.and it instructed PR #7400 to drop the pin and changeset sentence that had treated the question as settled. Done — commit
abf4b7208: thecompareTopin removed (13 → 12 pins) and the changeset paragraph rewritten. ⛔ The renderer (AdvancedChartImpl.tsx) and all ten locale packs are byte-identical across that amendment — verified bygit show --name-only, not by the report.⚠️ Stated honestly and not papered over: #7402's exclusion (scatter joining pie / donut / funnel in ignoringcompareTo) ships as a separate change, which is dispatched but not yet landed. Until it does, a compare-to scatter still reaches this guard as two series and refuses. The changeset says exactly that rather than implying this PR ships the exclusion.The compare-to refusal-copy defect
The one recorded on #7402 — the tile saying "Keep exactly one series:
amount,amount__comparison" to an author who wrote one series and never wroteamount__comparison— is resolved by (b) rather than by new copy: with no overlay ever synthesised for a scatter, that refusal is unreachable in product. That is why (b) was taken over (a).Carriers
needs:contract-reviewis off both carriers — this card and PR #7400 — following the in-seat review's PASS with its two required amendments applied ate090fd866. Verified by reading both label sets by hand.⚠️ scripts/pm/check-clause2-carriers.mjscannot run in this session (exit 2 — it needs its own GitHub token and this seat reaches GitHub only through MCP; its own output says that is not a clearance), so the weaker instrument was used and is being named as such.Remaining:
Test (shard 1-4)andLinton headabf4b7208. On green by name, the seat lands it.
Generated by Claude Code
- added a commit that references this issue
on Sep 3, 2026
Found while sweeping scatter for objectui#7171 (PR to follow). Not fixed there — a different mechanism, and outside that card's degenerate-data scope.
What was measured
packages/plugin-charts/src/AdvancedChartImpl.tsx, the scatter arm. Rendered in vitest with the real Recharts and a 480x320 container:chartType: 'scatter',xAxisKey: 'xm'series: [{ dataKey: 'ym' }, { dataKey: 'zm' }]data: [{ xm: 10, ym: 40, zm: 5 }, { xm: 20, ym: 25, zm: 90 }]Result:
path.recharts-symbols.recharts-scattergroups264,5and475,110, each drawn TWICEzmcarries 5 and 90. Neither value is anywhere on the plot. Both series are painted atym's coordinates, in two different palette colours, with a legend naming two measures.Why
The arm maps over
seriesand renders one element per series, but each one is handeddata={data}with nodataKeyof its own — the y coordinate comes from the single axis:So every series reads the same two keys off the same rows. A second series adds a colour and a legend entry and nothing else.
Why it matters
This is not a degenerate-data case: the data is perfectly good and the chart is confidently wrong. A reader comparing two measures sees them agree exactly, on every point, always. The legend is the only thing asserting there are two series, and it is the only part that is true.
Not obviously a one-line fix — it needs a shape decision
Recharts models a multi-series scatter as several elements each with their own
dataarray, not as one array with several y columns. Candidate shapes, none ruled:data={data.map(r => ({ x: r[xKey], y: r[s.dataKey] }))}and bind the axes to the projected keys. Cheap, works, changes whatonClickpayloads carry.Whether any dataset in the product actually authors a two-series scatter should decide between B and A; I did not measure that.
Related
objectui#7171 (the sweep that found it) · objectui#7147 / PR objectui#7169 · objectui#4683.
PM state (added by the
domain:uiexecution seat, 2026-09-02)Ruling B is implemented and delivered on PR #7400, and the Clause-② contract review at
CONTRACT_REVIEW_TIERhas returned PASS WITH REQUIRED AMENDMENTS. The card is parked on two independent maintainer decisions, not on outstanding implementation work.Blocked-by: #7399
Blocked-by: #7402
Unlock-action: re-check PR #7400
frameworkper-chunk eager-closure ceiling — 177 bytes repo-wide against the ruled copy's +420compareToscatter — a published capability the ruling never named?⭐ The card's own open measurement — "whether any dataset in the product actually authors a two-series scatter should decide between B and A; I did not measure that" — was made during implementation and then widened by the reviewer to the dashboard-widget spelling (
type: 'scatter' | 'bubble') that the implementer's regex could not see: 0 either way, with a control firing at 10 files. The zero holds on a stronger basis than either party first claimed.Byte fork, measured in two halves: the mechanism costs 0 bytes (
plugin-chartsis lazy and not in the eager closure); the entire +420 is the ten-pack copy. ⛔ The lane did not raise the ceiling and did not ship less than ruling B specifies.Contract review outcome: mechanism PASS — precedence correct and masking nothing, no new export, 13/13 pins green with reverse verification reproduced exactly. Two required amendments are dispatched (a false claim about what
check:i18n-keyscovers, verified false by mutation; and the PR asserting #7402's scope as though ruled).needs:contract-reviewstays on PR #7400 until the seat verifies both.amount,amount__comparison" while the author wrote exactly one series and never wroteamount__comparison. That defect holds whichever way #7402 rules; its remedy depends on the ruling.