Skip to content

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

Closed

Description

@objectstack-fleet

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

  1. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — bug · priority:p1 (from p2) · domain:spec (from domain:ui) · area:reports · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-27T10:56Z

    ⛔ Not a claim, ⛔ not a dispatch.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1 · 2026-10-27T13:14Z
    Session: session_01WGufzfhobWEgUUrjz4rrbh
    Account: os-support-ai
    Branch: claude/project-thread-hjxyd2 (card thread session_01WGufzfjMN88NYp5BcCRyGR. The platform assigns the thread's branch and the thread can push only that one, so the branch is written as measured instead of claude/issue-26602-<slug>.)
    Worktree: objectstack-issue-26602
    Domain: domain:spec
    Seat: domain:spec#1
    File surface: packages/spec/src/ui/dashboard.zod.ts (compareTo) and/or packages/spec/src/ui/dataset.zod.ts (the measure), for an optional polarity key, the first reader ui/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: high
    Clause-②: 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/main 43731031: neither compareTo nor 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 compareTo or DatasetWidget.tsx.
    Ruling-ref: none — triage 6440164879 (p1 protocol addition, under #26368 ruling A)

  3. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim amendment · domain:spec seat 1 (#6017) · session session_01WGufzfhobWEgUUrjz4rrbh · 2026-10-27T13:15Z

    Supersedes the File surface line of claim 6442814260. Everything else in it stands.

    File surface: packages/spec/src/ui/dataset.zod.ts (an optional DatasetMeasureSchema.polarity: 'higher' | 'lower' | 'neutral', absent keeps today's colours), packages/spec/src/api/analytics.zod.ts and packages/spec/src/contracts/analytics-service.ts (the result column descriptor fields[].polarity, kept mirrored), the spec tests and generated artifacts, packages/spec/liveness/dataset.json, packages/services/service-analytics/src/analytics-service.ts (enrichResultColumns relays it, the __compare column 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.ts compareTo is NOT touched. Stop on a breach and explain it in the report.

    Why: the dev measured that the widget compareTo cannot carry the key. DatasetWidget forwards it verbatim into the selection, the wire DatasetCompareToSchema is closed, and dashboard-compareto.test.ts pins the thin projection (#5011). So the key's home is the measure, and it rides the column descriptor the way format and currency do.

  4. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    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's compareTo is forwarded verbatim into the strictObject DatasetCompareToSchema and pinned assignable to it (dashboard-compareto.test.ts). enrichResultColumns relays it onto the answer's fields[] descriptor (live and preview, __compare included; declared on the wire schema and the AnalyticsResult contract), and DatasetWidget#deltaToneClass colours the KPI tile and the cross-tab cell by it: lower draws a rise red and a fall green, neutral draws no good/bad colour, higher and 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/spec minor, @objectstack/service-analytics minor, @object-ui/plugin-dashboard minor; 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 build under 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 only cancelled lanes 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

  5. added a commit that references this issue on Oct 27, 2026
  6. objectstack-fleet commented on Oct 28, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:spec seat 1 (#6017) · session session_01WGufzfhobWEgUUrjz4rrbh · 2026-10-28T11:15Z

    PR #26739 merged at c26d6436, which is an ancestor of origin/main. On 29 of the 33 paths the PR touched, the merge tree matches the accepted head 1aa6a94b byte for byte. The other 4 are dashboards.mdx, analytics-service.ts, api/analytics.zod.ts and contracts/analytics-service.ts. On each of them the PR's net hunks are identical between the merge-base and the merge. The bytes differ only because #27169 and #27170 landed on main in between.

    A dataset measure now declares its polarity, so a KPI where lower is better draws a rising delta as bad news. The card closed through Fixes #26602, and this seat removes pm:dispatched.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:reportsBusiness reporting — dashboards, reports, the numbers a manager readsbugSomething isn't workingdomain:specpriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions