Measured (objectui origin/main @ 6eebc54b6, re-read on PR #7684's merged head)
@objectstack/spec@17.2.0 declares ChartSeries.dashArray as "Override stroke dash pattern" and ChartSeries.opacity as "Override series opacity (0–1)" — general per-series overrides, no condition (dist/ui/index.js:2397-2399). normalizeSeries reads both on every series (packages/plugin-charts/src/normalizeChartSchema.ts:248-251), and core/utils/chart-presentation.ts:130-131 forwards both on the dataset path.
But the renderer's ONLY read of the two normalized values is inside comparisonStyle (packages/plugin-charts/src/AdvancedChartImpl.tsx:92-97):
const comparisonStyle = (s: any, kind) => {
if (s?.variant !== 'comparison') return null;
const strokeOpacity = typeof s.opacity === 'number' ? s.opacity : …;
const fillOpacity = typeof s.opacity === 'number' ? s.opacity : …;
const strokeDasharray = s.dashArray ?? …;
return { strokeOpacity, fillOpacity, strokeDasharray };
};
All five call sites (:1825, :1892, :2035, :2046, :2054) apply cmp?.fillOpacity / cmp?.strokeOpacity / cmp?.strokeDasharray, so for any series whose variant is not comparison the result is null and the authored opacity / dashArray are read and then unused. Census of the plugin (grep -rniE 'opacity|dasharray' packages/plugin-charts/src, tests excluded): every other hit is a literal constant or a chart-level prop; no second series-level read exists. Control: the same grep finds the five cmp?.… sites and the two normalizeSeries reads.
Consequence: a hand-authored { name: 'cost', opacity: 0.6 } or { name: 'cost', dashArray: '4 4' } on a primary series draws exactly like one without the keys. The published face (@object-ui/types ChartDataSeriesSchema, PR #7684) declares both keys because they ARE live on comparison series and are spec members — the declaration is right; the gap is the renderer honouring a spec-declared override on one variant only.
Why this is a renderer finding, not a schema one (AGENTS.md #0 / #0.1)
The spec is the contract of record and it describes both keys as unconditional overrides. Narrowing the mirror or the docs to "comparison only" would fossilise the renderer's partial implementation into the contract. The docs in PR #7684 state today's behaviour plainly (the rows say "read and unused" on a primary series) so nobody is misled meanwhile; this card is where the behaviour itself gets decided.
Options
- A — honour both keys on every series (recommended, contract-first): apply the authored
opacity / dashArray regardless of variant, and keep the comparison-specific DEFAULTS (muted opacity, 4 4 dash) gated on variant === 'comparison'. Same effect for every existing comparison series (the override branch already wins over the defaults), new effect only where an author wrote the key on a primary series and got nothing. Pin: a primary-series opacity: 0.3 reaches the mark's fillOpacity / strokeOpacity; a comparison series with no keys keeps its muted defaults (ChartRenderer.specSeries.test.tsx harness).
- B — declare the condition in the spec (
@objectstack/spec change): only if the maintainer decides the keys are comparison-only by design. Then the mirror's .describe and the docs follow the spec.
Related: #7546 (the declaration card), PR #7684 (where the docs currently state the condition), #7682 (the variant union drift these reads sit next to). No closing keyword on purpose — none of those is resolved by this card.
Found during the contract-review remediation of PR #7684 by the Claude Code dev seat, session session_01KbJQ1y1J12nZxYzFWhP8Q3; filed without an assignee for triage.
Generated by Claude Code
Measured (objectui
origin/main@6eebc54b6, re-read on PR #7684's merged head)@objectstack/spec@17.2.0declaresChartSeries.dashArrayas "Override stroke dash pattern" andChartSeries.opacityas "Override series opacity (0–1)" — general per-series overrides, no condition (dist/ui/index.js:2397-2399).normalizeSeriesreads both on every series (packages/plugin-charts/src/normalizeChartSchema.ts:248-251), andcore/utils/chart-presentation.ts:130-131forwards both on the dataset path.But the renderer's ONLY read of the two normalized values is inside
comparisonStyle(packages/plugin-charts/src/AdvancedChartImpl.tsx:92-97):All five call sites (
:1825,:1892,:2035,:2046,:2054) applycmp?.fillOpacity/cmp?.strokeOpacity/cmp?.strokeDasharray, so for any series whosevariantis notcomparisonthe result isnulland the authoredopacity/dashArrayare read and then unused. Census of the plugin (grep -rniE 'opacity|dasharray' packages/plugin-charts/src, tests excluded): every other hit is a literal constant or a chart-level prop; no second series-level read exists. Control: the same grep finds the fivecmp?.…sites and the twonormalizeSeriesreads.Consequence: a hand-authored
{ name: 'cost', opacity: 0.6 }or{ name: 'cost', dashArray: '4 4' }on a primary series draws exactly like one without the keys. The published face (@object-ui/typesChartDataSeriesSchema, PR #7684) declares both keys because they ARE live on comparison series and are spec members — the declaration is right; the gap is the renderer honouring a spec-declared override on one variant only.Why this is a renderer finding, not a schema one (AGENTS.md #0 / #0.1)
The spec is the contract of record and it describes both keys as unconditional overrides. Narrowing the mirror or the docs to "comparison only" would fossilise the renderer's partial implementation into the contract. The docs in PR #7684 state today's behaviour plainly (the rows say "read and unused" on a primary series) so nobody is misled meanwhile; this card is where the behaviour itself gets decided.
Options
opacity/dashArrayregardless ofvariant, and keep the comparison-specific DEFAULTS (muted opacity,4 4dash) gated onvariant === 'comparison'. Same effect for every existing comparison series (the override branch already wins over the defaults), new effect only where an author wrote the key on a primary series and got nothing. Pin: a primary-seriesopacity: 0.3reaches the mark'sfillOpacity/strokeOpacity; a comparison series with no keys keeps its muted defaults (ChartRenderer.specSeries.test.tsxharness).@objectstack/specchange): only if the maintainer decides the keys are comparison-only by design. Then the mirror's.describeand the docs follow the spec.Related: #7546 (the declaration card), PR #7684 (where the docs currently state the condition), #7682 (the
variantunion drift these reads sit next to). No closing keyword on purpose — none of those is resolved by this card.Found during the contract-review remediation of PR #7684 by the Claude Code dev seat, session
session_01KbJQ1y1J12nZxYzFWhP8Q3; filed without an assignee for triage.Generated by Claude Code