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

Open

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
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:specpm:dispatchedpriority: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