Dashboard KPI: a compareTo delta is drawn green whenever the number rises — no way to declare lower-is-better, so a slower turnaround reads as good news #26602
Description
Filing gate: ① user-visible defect, measured in the browser on @objectstack 18.2.0 (hotclm legal workbench, zh-CN, as the head of legal).
Symptom. hotclm's 「平均审批周转」 KPI tile (average days from submission to approval, compareTo: previousPeriod, hotclm PR #168) shows 「↑ 23% 较上期」 in emerald for a turnaround that got 23% longer, which is a worse result shown as good news. Every "lower is better" measure has the same problem: durations, overdue counts, open deviations, cost.
Why. The renderer colours delta.direction === 'up' emerald for every measure (packages/plugin-dashboard/src/DatasetWidget.tsx ~1325 in the objectui tree now under ui/). The spec has no key on the widget or measure to declare the polarity, for example compareTo: { …, betterWhen: 'lower' } or a measure-level polarity.
Expected. An author can declare that lower is better for a measure or a compareTo tile, and the delta is coloured accordingly: up is red, or neutral, for such a measure. Without a declaration, the default could be neutral rather than green.
Business reason. A KPI's colour is read before its number. A green arrow on a slowing approval process tells the head of legal the opposite of what happened.
Consumer. objectstack-ai/hotclm (legal workbench approval_turnaround). The tile already drops its own success colour. The delta colour is the platform's.
Duplicate search. objectstack issues, all states: "compareTo delta colour lower is better", "metric trend arrow polarity", "previousPeriod delta direction good bad". The only related one is #25970 (compareTo without a window, closed). No duplicate.
Generated by Claude Code
Activity
objectstack-fleet commentedon Oct 27, 2026
Triage: first grade —
bug·priority:p1(from p2) ·domain:spec(fromdomain:ui) ·area:reports·pm:queue· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-27T10:56Z⛔ Not a claim, ⛔ not a dispatch.
compareTois a new authorable key, which is a protocol addition. Under [Decision] Triage priority caliber for protocol additions: keep 「协议新增定 p1」 for every addition, or grade additions an app asks for at p2 unless a release needs them #26368 ruling A, every protocol addition is p1, app-driven or not. Handled directly in 18.x. Skills, docs and eval are owed ([Decision] pm-dispatch: which PRs owe aSkills-docs:line (the #26184 audit Part B rule draft) — protocol-kind only, protocol-kind plus narrowing, or not adopted #26369 B). Lane: the protocol goes first todomain:spec. The renderer half (colour the delta by the declared direction) may ride the same PR, as feat(spec,automation,app-shell)!: a flow screen field typedfilecollects an upload, and the run resumes with the uploaded file's id #26134 did. Until it lands, an undeclared direction keeps today's colours, ⛔ with no guessing from the measure's name.Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
Claim: PM loop round 1 · 2026-10-27T13:14Z
Session:
session_01WGufzfhobWEgUUrjz4rrbhAccount:
os-support-aiBranch:
claude/project-thread-hjxyd2(card threadsession_01WGufzfjMN88NYp5BcCRyGR. The platform assigns the thread's branch and the thread can push only that one, so the branch is written as measured instead ofclaude/issue-26602-<slug>.)Worktree:
objectstack-issue-26602Domain:
domain:specSeat:
domain:spec#1File surface:
packages/spec/src/ui/dashboard.zod.ts(compareTo) and/orpackages/spec/src/ui/dataset.zod.ts(the measure), for an optional polarity key, the first readerui/packages/plugin-dashboard/src/DatasetWidget.tsx(delta colour), their tests, the docs and skills lines owed under #26369 B, and one changeset. Stop on a breach and explain it in the report.Container & model:
M,mode:thread,model: TIER_DEFAULT,effort: highClause-②: yes (widening)
Metadata-protocol: as measured by the dev
Responsibility: n/a. A consumer defect filed from hotclm (legal workbench KPI).
Thread-read: 6440164879
Premise: holds (read at
origin/main43731031: neithercompareTonor a dataset measure declares a polarity, and the renderer colours every rising delta as good)Serial constraints cleared: yes. No open PR or claim names
compareToorDatasetWidget.tsx.Ruling-ref: none — triage
6440164879(p1 protocol addition, under #26368 ruling A)objectstack-fleet commentedon Oct 27, 2026
Claim amendment ·
domain:specseat 1 (#6017) · sessionsession_01WGufzfhobWEgUUrjz4rrbh· 2026-10-27T13:15ZSupersedes the
File surfaceline of claim 6442814260. Everything else in it stands.File surface:
packages/spec/src/ui/dataset.zod.ts(an optionalDatasetMeasureSchema.polarity:'higher' | 'lower' | 'neutral', absent keeps today's colours),packages/spec/src/api/analytics.zod.tsandpackages/spec/src/contracts/analytics-service.ts(the result column descriptorfields[].polarity, kept mirrored), the spec tests and generated artifacts,packages/spec/liveness/dataset.json,packages/services/service-analytics/src/analytics-service.ts(enrichResultColumnsrelays it, the__comparecolumn included) with its test,ui/packages/plugin-dashboard/src/DatasetWidget.tsx(the KPI tile and the pivot cell delta, one helper) with its test, the skills dataset line, the docs dataset page, and changesets (spec, service-analytics, plugin-dashboard).dashboard.zod.tscompareTois NOT touched. Stop on a breach and explain it in the report.Why: the dev measured that the widget
compareTocannot carry the key.DatasetWidgetforwards it verbatim into the selection, the wireDatasetCompareToSchemais closed, anddashboard-compareto.test.tspins the thin projection (#5011). So the key's home is the measure, and it rides the column descriptor the wayformatandcurrencydo.objectstack-fleet commentedon Oct 27, 2026
os-dev-report
{
"issue": 26602,
"status": "done",
"branch": "claude/project-thread-hjxyd2",
"pr": "#26739",
"session": "session_01WGufzfjMN88NYp5BcCRyGR",
"premise_still_valid": true,
"summary": "A dataset measure declares an optional
polarity(higher|lower|neutral: the better direction, not a sign or a flag), on the measure only, because the widget'scompareTois forwarded verbatim into the strictObjectDatasetCompareToSchemaand pinned assignable to it (dashboard-compareto.test.ts).enrichResultColumnsrelays it onto the answer'sfields[]descriptor (live and preview,__compareincluded; declared on the wire schema and theAnalyticsResultcontract), andDatasetWidget#deltaToneClasscolours the KPI tile and the cross-tab cell by it:lowerdraws a rise red and a fall green,neutraldraws no good/bad colour,higherand undeclared keep today's classes byte-identically, with no inference from the measure's name. The metadata form offers the key (4 locales), liveness row live, Skills-docs updated (dashboards.md rule + eval 8) and docs (ui/dashboards.mdx, api/data-api.mdx, data-modeling/analytics.mdx). Changesets:@objectstack/specminor,@objectstack/service-analyticsminor,@object-ui/plugin-dashboardminor; Clause-②: yes (widening), Metadata-protocol: added. main moved during the run (4373103 → 74db43e); merged in 344e950 (the lifecycle-panel positive control resolves to 686 + 1 = 687; authorable-surface/ui.json regenerated from the merged tree).","tests": "New: packages/spec/src/ui/dataset-measure-polarity.test.ts 17 pass; packages/services/service-analytics/src/tests/measure-column-polarity.test.ts 3 pass; ui/packages/plugin-dashboard/src/tests/DatasetWidget.polarity-26602.test.tsx 9 pass. On the merged tree (344e950 + docs): @objectstack/spec Test Files 813 passed (813), Tests 23281 passed | 1 todo; @objectstack/service-analytics Test Files 192 passed (192), Tests 4650 passed | 282 skipped; @object-ui/plugin-dashboard Test Files 190 passed (190), Tests 1864 passed | 6 skipped; platform-objects translations 25/25 files, 438 tests. Ablation via scripts/ablation-replace.mjs, restored by blob hash: deleting the relay line in enrichResultColumns turns 2/3 service tests red; replacing deltaToneClass with direction-only colouring turns 4/9 renderer tests red (tile lower x2, cell lower, cell neutral; the tile neutral pin stays green because tailwind-merge lets the tile's explicit muted class win, noted in the PR). Typecheck spec / service-analytics / platform-objects exit 0; plugin-dashboard type-check NOT MEASURED locally (133 TS2307 on unbuilt sibling @object-ui dist), CI Type Check UI is the reading.",
"gates": "Local: node scripts/pm/dispatch-gates.mjs --commands derives 138 families at this head (unchanged across the merge); --ran reconciliation exit 0: 138 accounted, 137 run exit 0, 1 NOT-MEASURED (pnpm check:dual-build-cjs-loads exit 3, PREREQUISITE NOT MET: needs a whole-repo build). spec check:generated / check:exported-any / check:skill-examples first exited 3/1/3 against a dist older than the sources, then exit 0 each after
pnpm --filter @objectstack/spec buildunder os-verify-lock; after the merge check:generated exit 0 again, check:i18n exit 0, check-skills-token-ratchet exit 0 (81 files within ceilings). The 19 docs families re-run after each docs commit: all exit 0. CI-only gate fixed: check:spec-parsed-alias (ADR-0122) red on 8b789f7/aef918b95 → Iso_ui_dataset__DatasetMeasurePolarity pin (5bce636), green. CI at head 1576522 so far: Lint & Repo Gates success (all 9 lint lanes success), Type Check source gates / debt ledger / consumer gates success, Spec property liveness success, Check Changeset success, Governed Surface Queue Guard success, PR-body guards success, Build Docs success, Dogfood Regression Gate success, Test Core (8/8) success; Test Core 1-7, Test UI 1-16, Type Check UI, Type Check workspace, Lint UI, Build & E2E UI, Build Core in progress. Earlier red aggregators on aef918b and 344e950 (TypeScript Type Check, Test UI, Lint & Repo Gates) read onlycancelledlanes from runs superseded by the next push.","dogfood_battery": "n/a — no
!in the PR title and no (narrowing) arm","mcp_calls": "~20 (exact count lost across a context compaction) — GitHub MCP read tools only: issue_read, pull_request_read (get, get_check_runs, get_comments), get_job_logs; plus subscribe_pr_activity (session subscription, not a GitHub write). No write tool.",
"api_writes": "3 — via scripts/pm/fleet-write relay: pr_create (POST /repos/objectstack-ai/objectstack/pulls, draft, assignee os-support-ai), pr_request_reviewers (POST /repos/objectstack-ai/objectstack/pulls/26739/requested_reviewers), this os-dev-report comment (POST /repos//issues/26602/comments). Plus git pushes to claude/project-thread-hjxyd2 (not REST).",
"open_questions": [
{
"question": "skills/objectstack-ui/evals/analytics-inline-vs-dataset.json grows +29 tokens for eval 8's polarity; −11 is paid inside the file and the balance (+18, ceiling 1348 → 1366) is raised under the maintainer ruling 「A:先削重复,余额按实测提高」 recorded beside that row in scripts/check-skills-token-ratchet.mjs. Does that ruling cover this eval?",
"options": [
"A — keep the raise under the ruling as quoted in the PR body",
"B — drop the eval change, restore the 1348 ceiling, and declare the skills half
follow-up owed"],
"recommendation": "A, because the ruling names exactly this shape (delete duplicates first, raise the measured balance) and an eval that asserts
polarity: 'lower'is what teaches the key to an authoring agent."}
],
"out_of_scope_findings": [
"carrier: none · noted, not filed — the inline ObjectMetricWidget / MetricWidget trend path reads no dataset measure, so it has no polarity source; DatasetReportRenderer draws no comparison delta (both in the PR's Acceptance notes)."
]
}
Generated by Claude Code
chore(skills): pin both #26602 ratchet rows at their measurements