Skip to content

Commit 4fa4f2c

Browse files
docs(spec,skills): object-metric polarity reads the aggregate before invert; pages.md points at compareTo (#28348)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} --> _Requested by **Tesla** · [project thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6JaoTXwoh1TJYxwy3kcKGFXGqj)_ Fixes #27375 Refs #27347 Clause-②: no Metadata-protocol: none Skills-docs: updated here ## 维护者速读 - 改了什么:`object-metric` 的 `polarity` 说明补一句——徽标箭头和 `polarity` 都读 `invert` 之前的聚合值,所以 `polarity` 按原始值声明(错误率显示成可用率时写 `lower`);`objectstack-ui` 技能的 `rules/pages.md` 在 `object-metric` 那一行加上指向 `dashboards.md` 的 `compareTo` / `polarity` 小节的链接。 - 为什么改:开了 `invert` 的指标卡上有两个数(原始值和 `1 - 值`),原文没说箭头读哪个,作者可能把好坏方向写反;从 `pages.md` 搭内联 KPI 的作者原先找不到 `polarity` 这个键。 - 风险与回滚:纯文字改动,schema、可接受值、默认值和渲染行为都不变;`pages.md` 的新增由同文件删掉三处重复表述抵付,token 数不变,天花板不动。回滚即 revert 本 PR。 - 先看:`packages/spec/src/ui/component.zod.ts` 的 `polarity` describe、`skills/objectstack-ui/rules/pages.md` 第 56 行与三处删减。 - 两分钟验证:`node scripts/check-skills-token-ratchet.mjs`,预期全绿,`pages.md` 为 5689 / 5692。 - 需要你做的:本 PR 触及 `skills/**`(Tier H),需要 os-zhuang 或 hotlong 的 APPROVED 评审,之后由席位落地。 ## What changes **S1: the `polarity` prose says which number it reads (`packages/spec/src/ui/component.zod.ts`).** `ObjectMetricWidget` computes the derived trend from the raw `fetchedValue` / `previousValue`, while the number it shows is `1 - value` under `invert: true` (read at `501c864d`: `ui/packages/plugin-dashboard/src/ObjectMetricWidget.tsx`, the `invert` branch near L590 and `derivedTrend` near L684). The `polarity` describe now ends with one sentence: the badge and polarity read the aggregate before `invert`, so polarity is declared against the raw value, and an error rate shown as uptime is `lower`. The TSDoc above it says the same in one sentence. `content/docs/references/ui/component.mdx` is regenerated (`check:generated` named only `check:docs`). The SDUI manifest does not carry this describe (0 hits for the old text in `sdui.manifest.json`), so it is not regenerated. **S2: `rules/pages.md` points at the polarity rule.** The `object-metric` row of the component catalogue now reads `Single KPI widget (count/sum/avg); trend colour = polarity, see compareTo`, with the link going to `dashboards.md#period-over-period--compareto` (the anchor the file's own table of contents uses). The pointer is paid for by deleting three restatements in the same file: - `(the CSS is scoped to it)` after "needs a stable `id`": the paragraph above already says the renderer compiles each node to id-scoped CSS. - the `// responsive via the model` comment in the styling example: the rule "Responsive lives in the breakpoint maps" above it says this. - `(it is real JS, not constrained JSX)` after "`os build` does NOT lint react source": the tier table at the top of the section says this. Standing ruling (2026-10-27, tap A, verbatim and untranslated): 「A:先削重复,余额按实测提高」; recognised path per ruling record 6513306068. The cuts cover the whole pointer, so there is no remainder and the `pages.md` row in `scripts/check-skills-token-ratchet.mjs` is not touched. ### Skill bundle readings (`skills/**` touched) | | before (`501c864d`) | after | |:---|---:|---:| | `skills/objectstack-ui/rules/pages.md` | 428 lines · 22756 bytes · 5689 tokens | 428 lines · 22753 bytes · 5689 tokens (ceiling 5692) | | all `SKILL.md` in `skills/` | 4607 lines | 4607 lines | | `skills/objectstack-ui/**/*.md` (evals excluded) | 2055 lines | 2055 lines | **Changeset:** `.changeset/27375-object-metric-polarity-before-invert.md`, `@objectstack/spec` patch and `@objectstack/skills` patch, with both declaration lines. The spec patch is measured, not assumed: after `pnpm --filter @objectstack/spec build`, the new describe text is in `packages/spec/dist/chunk-*.{js,mjs}` and `json-schema/ui/ObjectMetricProps.json`, and both `dist` and `json-schema` are in the package's `files[]`. ## Tests No behaviour changes, so there is no new test and no ablation. The derived-gate set ran locally on the head named under Verification; see that section. ## Acceptance notes - Out of scope by the dispatch ruling, and not measured here: whether the arrow should follow the displayed number when `invert` and `compareTo` are both set. Nothing in this PR changes it. ## Verification Head `ebeb7783`, merge base `501c864d`. Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (4 paths, 30 changed lines), each line run with stdin from /dev/null and its exit code written to disk before any pipe. - `pnpm --filter @objectstack/spec build`: VERDICT command-exit 0. `check:generated` named only `check:docs` stale (13 of 14 up to date); `check:generated --fix` rewrote `content/docs/references/ui/component.mdx` and nothing else. - Reconciliation, `dispatch-gates.mjs --repo objectstack-ai/objectstack --ran`: "104 derived, 103 run, 1 NOT-MEASURED, 0 UNRUN". - Seven gates first refused on unbuilt prerequisites (exit 3). After deepening the clone and building the `@objectstack/lint`, `@objectstack/client-react` and `@objectstack/objectql` closures, six of them re-ran green. - NOT MEASURED: `pnpm check:dual-build-cjs-loads`, reason: it loads the built CJS entry of every package and 34 had no `dist/`; a full workspace build was not run for a prose-only diff. CI's `Lint & Repo Gates` runs it. <details><summary>Derived gates, with exit codes</summary> ```text node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 node scripts/check-adr-links.mjs :: exit 0 node scripts/check-adr-links.mjs --self-test :: exit 0 node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 node scripts/check-closing-keyword-parity.mjs :: exit 0 node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 node scripts/check-comment-mask-adoption.mjs :: exit 0 node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0 node scripts/check-comment-mask-corpus.mjs :: exit 0 node scripts/check-dev-prereqs.mjs --self-test :: exit 0 node scripts/check-doc-route-spelling.mjs --advisory :: exit 0 node scripts/check-doc-route-spelling.mjs --self-test :: exit 0 node scripts/check-docs-section-name.mjs :: exit 0 node scripts/check-docs-section-name.mjs --self-test :: exit 0 node scripts/check-dts-emitted.mjs --self-test :: exit 0 node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 node scripts/check-empty-changeset.mjs --self-test :: exit 0 node scripts/check-issue-citations.mjs :: exit 0 node scripts/check-plugin-teardown-shape.mjs :: exit 0 node scripts/check-section-landing-index.mjs :: exit 0 node scripts/check-section-landing-index.mjs --self-test :: exit 0 node scripts/check-skills-token-ratchet.mjs :: exit 0 node scripts/check-skills-token-ratchet.mjs --self-test :: exit 0 node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0 node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0 node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0 node scripts/docs-audit/check-affected-docs.mjs :: exit 0 node scripts/docs-audit/check-drift-comment.mjs :: exit 0 node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 node scripts/release-pending-publish.mjs --self-test :: exit 0 pnpm --filter @objectstack/spec run check:api-surface :: exit 0 pnpm --filter @objectstack/spec run check:authorable-surface :: exit 0 pnpm --filter @objectstack/spec run check:browser-reachable-entries :: exit 0 pnpm --filter @objectstack/spec run check:docs :: exit 0 pnpm --filter @objectstack/spec run check:dual-source-exports :: exit 0 pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 pnpm --filter @objectstack/spec run check:empty-state :: exit 0 pnpm --filter @objectstack/spec run check:entry-nameability :: exit 0 pnpm --filter @objectstack/spec run check:export-origins :: exit 0 pnpm --filter @objectstack/spec run check:exported-any :: exit 0 pnpm --filter @objectstack/spec run check:generated :: exit 0 pnpm --filter @objectstack/spec run check:liveness :: exit 0 pnpm --filter @objectstack/spec run check:llms-txt :: exit 0 pnpm --filter @objectstack/spec run check:skill-refs :: exit 0 pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0 pnpm --filter @objectstack/spec run check:variant-docs :: exit 0 pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0 pnpm check:agent-test-spelling :: exit 0 pnpm check:changeset-gate-self-tests :: exit 0 pnpm check:ci-filter-parity :: exit 0 pnpm check:closed-ceremony-text :: exit 0 pnpm check:corpus-claim-drift :: exit 0 pnpm check:cross-package-test-inputs :: exit 0 pnpm check:dispatcher-error-vocabulary :: exit 0 pnpm check:doc-anchors :: exit 0 pnpm check:doc-authoring :: exit 0 pnpm check:doc-frontmatter :: exit 0 pnpm check:docs-audit-scope :: exit 0 pnpm check:docs-redirects :: exit 0 pnpm check:docs-single-h1 :: exit 0 pnpm check:docs-spec-enumerations :: exit 0 pnpm check:driver-memory-census :: exit 0 pnpm check:dts-closure :: exit 0 pnpm check:gitlink-declared :: exit 0 pnpm check:issue-citations :: exit 0 pnpm check:keyed-text-bounds :: exit 0 pnpm check:logger-receiver-detach :: exit 0 pnpm check:merge-driver :: exit 0 pnpm check:nul-bytes :: exit 0 pnpm check:org-identifier :: exit 0 pnpm check:page-declaration-shape :: exit 0 pnpm check:platform-object-tenancy-census :: exit 0 pnpm check:pm-changeset-deadline-census :: exit 0 pnpm check:pm-governed-merges :: exit 0 pnpm check:pm-prior-rulings :: exit 0 pnpm check:pm-skill-id-lint :: exit 0 pnpm check:published-files :: exit 0 pnpm check:published-readme-links :: exit 0 pnpm check:react-page-adapter-contract :: exit 0 pnpm check:refd-timer-probe :: exit 0 pnpm check:registry-log-declared :: exit 0 pnpm check:rest-log-spy-declared :: exit 0 pnpm check:role-word :: exit 0 pnpm check:skill-compatibility :: exit 0 pnpm check:skill-frame-sync :: exit 0 pnpm check:skill-identifier-liveness :: exit 0 pnpm check:slot-lookup :: exit 0 pnpm check:sourcemap-no-sources-content :: exit 0 pnpm check:spec-docblock-symbol-anchors :: exit 0 pnpm check:spec-parsed-alias :: exit 0 pnpm check:system-context-census :: exit 0 pnpm check:test-source-alias :: exit 0 pnpm check:tier-file-adoption :: exit 0 pnpm check:undeclared-dep-imports :: exit 0 pnpm check:vendor-version-stamps :: exit 0 pnpm check:watch-hint-literal :: exit 0 node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0 pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0 pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0 pnpm --filter @objectstack/spec run check:skill-examples :: exit 0 pnpm check:docs-transcript-drift :: exit 0 pnpm check:dual-build-cjs-loads :: exit 3 pnpm check:lean-entry-closure :: exit 0 ``` </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01L9qibyvwrLkwf1kTpdV1Qc)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent ea1751b commit 4fa4f2c

4 files changed

Lines changed: 23 additions & 7 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/spec': patch
3+
'@objectstack/skills': patch
4+
---
5+
6+
docs(spec,skills): an `object-metric` tile's `polarity` is declared against the aggregate before `invert`, and `pages.md` points at it
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
- **Before.** The `polarity` description said the badge arrow shows which way the number moved. Under `invert: true` the tile shows `1 - value`, so there are two numbers, and the text did not say which one the arrow and `polarity` read. The `objectstack-ui` skill's `rules/pages.md` taught the inline `object-metric` without naming `compareTo` or `polarity`.
12+
- **Now.** The `polarity` description (and the reference page generated from it) says the badge and `polarity` read the aggregated value before `invert`, so `polarity` is declared against the raw value: an error rate shown as uptime is `lower`. The `object-metric` row in `rules/pages.md` names `polarity` and links to the `compareTo` section of `rules/dashboards.md`. The pointer is paid for by deleting three restatements in the same file; the file stays at its token count and no ceiling moves.
13+
- **Unchanged.** No schema, accepted value, default or renderer behaviour moves.

‎content/docs/references/ui/component.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1222,7 +1222,7 @@ Sort field and direction pair
12221222
| **trend** | `{ value: number; label?: string \| Record<string, string>; direction?: Enum<'up' \| 'down' \| 'neutral'> }` | optional | Static trend badge `{ value, label?, direction? }` — `value` painted as a percentage, `direction` up / down / neutral. A `compareTo`-derived trend replaces it |
12231223
| **drillDown** | `{ enabled?: boolean; title?: string; target?: Enum<'drawer' \| 'dialog' \| 'navigate'>; columns?: string[]; … }` | optional | Click-through drill config `{ enabled?, title?, target?, columns?, maxRows?, report? }` — opens the records behind the number, scoped by the metric's own `filter` — or draws the report `report` defines; a present block is on unless `enabled: false`. `filter` and `mode` are refused: a metric tile has no click context and no row |
12241224
| **compareTo** | `{ kind: Enum<'previousPeriod' \| 'previousYear'> }` | optional | Period-over-period comparison `{ kind }` — `previousPeriod` or `previousYear`, shifting the date macros in the tile's own `filter`. `dimension` is refused: this tile never reads a dataset time dimension |
1225-
| **polarity** | `Enum<'higher' \| 'lower' \| 'neutral'>` | optional | Which direction of change is good news: higher (rise green, fall red), lower (fall green, rise red) or neutral (no good/bad colour). Colours the trend badge, computed (`compareTo`) or static (`trend`); undeclared keeps rise green, fall red |
1225+
| **polarity** | `Enum<'higher' \| 'lower' \| 'neutral'>` | optional | Which direction of change is good news: higher (rise green, fall red), lower (fall green, rise red) or neutral (no good/bad colour). Colours the trend badge, computed (`compareTo`) or static (`trend`); undeclared keeps rise green, fall red. The badge and polarity read the aggregate before `invert`, so declare polarity against the raw value: an error rate shown as uptime is lower |
12261226

12271227
### Nested Shape: `ObjectMetricProps.aggregate`
12281228

‎packages/spec/src/ui/component.zod.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5793,12 +5793,15 @@ export const ObjectMetricPropsSchema = lazySchema(() => strictObject({
57935793
* - `neutral`: neither; the badge keeps its arrow and percentage, muted.
57945794
*
57955795
* The arrow always shows which way the NUMBER moved; polarity decides only
5796-
* the colour. Nothing infers a polarity from the aggregated field's name.
5796+
* the colour. Both read the aggregated value BEFORE `invert`, so polarity is
5797+
* declared against the raw aggregate: an error rate shown as uptime is
5798+
* `lower`. Nothing infers a polarity from the aggregated field's name.
57975799
*/
57985800
polarity: DatasetMeasurePolarity.optional().describe(
57995801
'Which direction of change is good news: higher (rise green, fall red), lower (fall green, rise red) '
58005802
+ 'or neutral (no good/bad colour). Colours the trend badge, computed (`compareTo`) or static (`trend`); '
5801-
+ 'undeclared keeps rise green, fall red',
5803+
+ 'undeclared keeps rise green, fall red. The badge and polarity read the aggregate before `invert`, so '
5804+
+ 'declare polarity against the raw value: an error rate shown as uptime is lower',
58025805
),
58035806
}));
58045807
/** Author state (ADR-0122: the bare name is the author state). */

‎skills/objectstack-ui/rules/pages.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ A `slotted` record page has no `tabs` slot: shape the strip with `relatedList` /
5353
| `record:path` | Stage progress bar — `statusField` + `stages: [{ value, label }]` |
5454
| `record:related_list` | Related-list (child records via lookup) |
5555
| `nav:menu` | Quick-create / nav menu bound to current context |
56-
| `object-metric` | Single KPI widget (count/sum/avg) |
56+
| `object-metric` | Single KPI widget (count/sum/avg); trend colour = `polarity`, see [compareTo](dashboards.md#period-over-period--compareto) |
5757
| `object-chart` | Embedded chart |
5858

5959
> **Variable substitution** — `{first_name}`, `{current_user.first_name}`,
@@ -228,7 +228,7 @@ function Page() {
228228
by the host capability `react-pages` — **OFF by default**: a react page's source is code
229229
every viewer runs. A deployment that trusts its authors turns it on server-side
230230
with `OS_PAGE_REACT_ENABLED=true`; while off, the page shows a "disabled" notice instead.
231-
`os build` does NOT lint react source (it is real JS, not constrained JSX) — errors surface
231+
`os build` does NOT lint react source — errors surface
232232
at render behind an error boundary, so always test a react page in the browser.
233233

234234
### Styling a page (ADR-0065) — `responsiveStyles`, NOT `className`
@@ -242,7 +242,7 @@ arbitrary classes (`text-[27px]`, `grid-cols-7`) silently do nothing.
242242
Rules:
243243
- **`responsiveStyles` and `id` are top-level** envelope fields; **child nodes go
244244
in `properties.children`** (the renderer hoists `properties` to schema level).
245-
- Every styled node **needs a stable `id`** (the CSS is scoped to it).
245+
- Every styled node **needs a stable `id`**.
246246
- **Values should be design tokens** for consistency: spacing `var(--space-1..12)`,
247247
radius `var(--radius)` / `var(--radius-xl)`, shadow `var(--shadow-sm|md|lg)`,
248248
colors `var(--surface)` / `var(--surface-sunken)` / `var(--text-strong)` /
@@ -266,7 +266,7 @@ Rules:
266266
backgroundColor: 'var(--surface)', border: '1px solid hsl(var(--primary))',
267267
boxShadow: '0 0 0 3px hsl(var(--primary) / 0.25), var(--shadow-lg)',
268268
},
269-
small: { padding: 'var(--space-4)', gap: 'var(--space-3)' }, // responsive via the model
269+
small: { padding: 'var(--space-4)', gap: 'var(--space-3)' },
270270
},
271271
properties: {
272272
children: [

0 commit comments

Comments
 (0)