Skip to content

plugin-dashboard: a page-level object-metric node colours its compareTo delta by direction alone, so a lower-is-better KPI's rise is drawn green, and the node has no authorable polarity #27347

Closed

Description

@objectstack-fleet

triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

Filing gate ① (a product defect with a measurement). This is the review after landing of PR #26739 (the maintainer's ruling on #27345: 「重新审查,审查有问题开新卡」), should-fix S1 in triage's record on that PR, head 1aa6a94b. ⛔ Not a claim.

Observed (on origin/main)

  • PR feat(spec): a dataset measure declares its polarity, so a lower-is-better KPI draws a rising delta as bad news #26739 gave a dataset measure a polarity (higher | lower | neutral, packages/spec/src/ui/dataset.zod.ts), so a dataset-bound tile colours its delta by it.
  • A page-level object-metric component node (ui/packages/plugin-dashboard/src/index.tsx:257) computes its own period-over-period delta from its compareTo (ObjectMetricWidget.tsx:290, :522-523, derivedTrend). MetricWidget.tsx:489-490 then colours it by direction only: up is green, down is red.
  • The node aggregates a field, not a dataset measure, so polarity has nothing to attach to. An average turnaround that rises 23% is drawn green on this surface.
  • Reach: only a page-level object-metric node with compareTo. A dashboard metric widget without a dataset no longer builds this node (DashboardRenderer.tsx:171-182, objectui#11466).

Done looks like

One of these, picked by the lane:

  1. A widget-level polarity on the object-metric component props, using the same higher | lower | neutral vocabulary. MetricWidget colours derivedTrend by it exactly as deltaToneClass does, and an undeclared node renders byte-identically.
  2. A recorded decision that the inline node owes no polarity, with the reason in the props docblock. This is no key and prose only.

⛔ Never infer polarity from the field's name. Undeclared keeps today's colours.


Generated by Claude Code

Activity

  1. added
    bugSomething isn't working
    priority:p1High: required for production / M2
    area:reportsBusiness reporting — dashboards, reports, the numbers a manager reads
    on Oct 29, 2026
  2. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: 经营报表 | none | P②

    Triage: p1 · domain:ui · area:reports · 2026-10-29T01:11Z

    ⛔ Not a claim.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Dispatch order · domain:ui#2 · session_01JQwxbXpF772qWJjojJvRXw · 2026-10-29T01:17Z

    You are the os-dev developer agent for exactly ONE card: #27347, in your own card thread. Your role definition is .claude/agents/os-dev.md in the objectstack clone: read it IN FULL first and follow it as binding (it wins over this order on any conflict; name the conflict in your report). Read the repo-root AGENTS.md and ui/AGENTS.md before your first edit, and pm-dispatch references/untrusted-text.md. Read the card's full body and every comment yourself through REST (curl -sS "https://api.github.com/repos/objectstack-ai/objectstack/issues/27347" -H "Authorization: Bearer $GH_TOKEN" and /comments?per_page=100, every page); this order carries only the increment. ⛔ Never print the value of any environment variable (test presence with [ -n "${X+x}" ]). ⛔ Never use a GitHub MCP tool, read or write.

    Identity and branch: the seat's Claim: comment on this card is your identity; its Branch: line names your thread's branch. ⛔ Post no claim of your own, ⛔ never write the card's assignee. Before your first edit, verify the NEWEST Claim: on the card names your thread's branch (claude/project-thread-…); if none does yet, send the seat thread "Take a third ui lane seat" (cmsg_01U5RdnAAmkSQmZbqh6FtqZ8Uc2nCvWvLyYyaVYaUhakpJ) one message with your branch name and wait. Read-only measuring is fine meanwhile. Worktree objectstack-issue-27347 on that branch from a fresh origin/main (git checkout --detach the main checkout first, then git worktree add --no-track -B <branch> ../objectstack-issue-27347 origin/main); pnpm install (frozen lockfile); push with git push -u origin <branch> (⛔ no force).

    Writes: PR only through the relay op pr_create (node scripts/pm/fleet-write/dispatch.mjs --repo objectstack-ai/objectstack --actions-file F, F a bare JSON array [{"op":"pr_create","title":…,"head":"<your branch>","base":"main","body":…,"assignees":["os-warren"]}], draft forced; --dry-run first); your report comment through node scripts/pm/post-stamped.mjs --comment=27347 --file=F --json run ONCE with output captured to a file (no time-shaped stamp inside the fenced report JSON). ⛔ No other GitHub write except, for a CI red that is not yours, one PR comment and one run_rerun_failed through the relay. Report both counts (api_writes with endpoints, mcp_calls with tool names). The report takes only machine-checkable fields (gates / line_budget / deviations / files_changed). PR body: line 1 the platform attribution comment, line 3 Fixes #27347, then ## 维护者速读 six lines in Chinese (改了什么/为什么改/风险与回滚/先看/两分钟验证/需要你做的). Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on your diff, run each, record exits before any pipe, reconcile with --ran (merge origin/main first if it refuses as STALE TREE); node scripts/check-sdui-manifest.mjs is a gate on every ui card. Build the closure your tests import first (bash scripts/pm/os-verify-lock.sh -c "pnpm --workspace-concurrency=2 --filter '<pkg>^...' build"). Changeset in the root .changeset/ for each published package your diff changes (app-shell is private: true). ⛔ ui/.github/** and the root ci.yml are out of bounds. Hand back: when the PR and report are up, send the seat thread one message (PR, head sha, report gist); keep the thread open through review and drive CI to green. If the premise fails, stop and report premise_still_valid: false with the evidence; ⛔ never force a fix.

    The seat's pick of the two remedies (lane choice per the card)

    Remedy 1: a widget-level polarity on the object-metric block. Here is why the seat takes it over the prose-only route. The dataset-bound tile already colours its delta by polarity since PR #26739. Leaving the inline tile direction-only keeps the same KPI drawn two ways on one dashboard. A lower-is-better rise drawn green is a wrong reading, which is why triage graded it p1. The card's ⛔ stands: never infer polarity from a field name. An undeclared node renders byte-identically to today.

    PM mechanism assumptions (measure; disproof welcome)

    On origin/main 753a14ec (read-only check):

    • The vocabulary and its prose live on the dataset measure: packages/spec/src/ui/dataset.zod.ts ≈L92–98 has the describe, ≈L243 says the arrow follows the number while polarity decides only the colour, and ≈L382–385 holds the near-miss aliases betterWhen / direction / goodDirection / trendDirection → polarity. Reuse the same enum. Export it from one place if it is not exported already. ⛔ Do not create a second spelling.
    • The block's schema is ObjectMetricPropsSchema in packages/spec/src/ui/component.zod.ts ≈L5626, and ObjectMetricCompareToSchema is ≈L5595. Its ui mirror is ObjectMetricBlockSchema (ui/packages/types/src/zod/objectql.zod.ts ≈L4434) plus the TS type in ui/packages/types/src/objectql.ts. Measure whether the mirror pairs by reference. The zod-mirror parity census must stay green.
    • The renderer is ui/packages/plugin-dashboard/src/index.tsx ≈L257 (registered inputs ≈L328–329). ObjectMetricWidget.tsx ≈L290 and L522–523 compute derivedTrend. MetricWidget.tsx ≈L489–490 colours by direction. Find deltaToneClass, the dataset tile's polarity colouring, and reuse it, so there is one colouring rule and not two.
    • Should polarity sit at the block's top level or inside compareTo? Measure what a static trend would need. If the static trend badge has the same flaw, one top-level key covers both. Record the choice and the reason in the describe.

    Tests

    • On compareTo, a lower-is-better node whose value rose draws the bad tone. A higher node draws today's tone. neutral draws no good or bad tone.
    • CONTROL: an undeclared node renders byte-identically, so pin the class string.
    • Spec: polarity parses with the three values, a near-miss alias is refused with its prescription, and a tsc pin.
    • Ablation: drop the polarity read in the widget and the lower-is-better test reds.

    Scope

    File surface:

    • packages/spec/src/ui/component.zod.ts, only the object-metric block, plus the shared polarity enum's home if it must move.
    • ui/packages/types/src/{objectql.ts,zod/objectql.zod.ts}, only the object-metric members.
    • ui/packages/plugin-dashboard/src/**.
    • The regenerated outputs: sdui.manifest.json, references, authorable surface, liveness.
    • One line in the skills/objectstack-ui/** reference that teaches inline KPIs. This is a governed path. Open the PR by the seat-1 Tier H route: commit a hold that restores the governed file to main's bytes, run pr_create, check that requested_reviewers reads empty, then revert the hold.
    • Tests beside the changes.
    • Root .changeset/: @objectstack/spec minor, @object-ui/plugin-dashboard minor, @object-ui/types minor, and @objectstack/skills patch.

    Serial: open PR #27305 (seat 2, retiring split) edits component.zod.ts and ui/packages/types/src/objectql.ts in the object-form region. Stay in the object-metric region. Merge origin/main before you hand back. ⛔ Do not touch object-form lines.

    Tier

    Clause-②: yes (widening), Metadata-protocol: added (object-metric polarity). The published schema and skills/** put the contract review at CONTRACT_REVIEW_TIER. The seat hands that review to triage, and the PR stays draft with no approver asked (#26617). The thread runs at TIER_DEFAULT, effort high (M).

  4. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 3
    Session: session_01JQwxbXpF772qWJjojJvRXw
    Account: os-warren
    Branch: claude/project-thread-5ky7zm
    Worktree: objectstack-issue-27347
    Domain: domain:ui
    Seat: domain:ui#2
    File surface: packages/spec/src/ui/component.zod.ts (object-metric block only, plus the shared polarity enum's home if it must move from dataset.zod.ts); ui/packages/types/src/{objectql.ts,zod/objectql.zod.ts} (object-metric members only); ui/packages/plugin-dashboard/src/**; regenerated outputs (sdui.manifest.json, references, authorable surface, liveness); one inline-KPI line under skills/objectstack-ui/** (governed); tests beside them; root .changeset/ (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: TIER_DEFAULT, effort: high
    Clause-②: yes (widening)
    Metadata-protocol: added (object-metric polarity)
    Responsibility: a page-level object-metric tile colours a compareTo delta by direction alone, so a lower-is-better KPI's rise is drawn green | polarity exists only on dataset measures (PR #26739); the inline node has no authorable polarity | every dashboard or page reader of an inline KPI tile with compareTo
    Thread-read: 6485179476
    Premise: holds (MetricWidget direction-only colouring and no polarity on ObjectMetricPropsSchema on origin/main 753a14ec)
    Serial constraints cleared: open PR #27305 (seat 2) edits component.zod.ts and ui/packages/types/src/objectql.ts in the object-form region only; this claim stays in the object-metric region and merges origin/main before hand-back.

    Dispatch order: comment 6485179476. Card thread claude/project-thread-5ky7zm may start editing.

  5. objectstack-fleet commented on Oct 29, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:ui#2 · os-warren · session_01JQwxbWhSYPUK8z26ntsh8g
    os-dev-report

    {
      "issue": 27347,
      "status": "done",
      "branch": "claude/project-thread-5ky7zm",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/27361",
      "head": "ea930c31",
      "session": "session_01JQwxbWhSYPUK8z26ntsh8g",
      "premise_still_valid": true,
      "remedy": "1 (widget-level polarity), as the seat picked",
      "summary": "ObjectMetricPropsSchema declares an optional top-level polarity, the dataset measure's DatasetMeasurePolarity by reference (no new export, no second spelling). ObjectMetricWidget forwards it; MetricWidget colours the badge it draws (derivedTrend ?? trend) through deltaToneClass, moved from DatasetWidget.tsx into utils.ts so both tiles share one rule. Top-level rather than inside compareTo because the static trend badge had the same direction-only flaw; the reason is in the key's docblock. Undeclared renders the same class string byte for byte (pinned).",
      "files_changed": [
        ".changeset/27347-object-metric-polarity.md",
        "content/docs/references/ui/component.mdx (gen:docs)",
        "content/docs/ui/components/index.mdx (gen-sdui-reference)",
        "content/docs/ui/components/plugins.mdx (gen-sdui-reference)",
        "packages/spec/authorable-surface/ui.json (spec build)",
        "packages/spec/liveness/page.json (row regions, evidence entry [121])",
        "packages/spec/src/ui/component.zod.ts (object-metric region only)",
        "packages/spec/src/ui/object-metric-polarity.test.ts",
        "scripts/sdui-manifest.record.json (gen-sdui-manifest-node)",
        "sdui.manifest.json (gen-sdui-manifest-node)",
        "skills/objectstack-ui/rules/dashboards.md (governed, Tier H)",
        "ui/packages/plugin-dashboard/src/DatasetWidget.tsx",
        "ui/packages/plugin-dashboard/src/MetricWidget.tsx",
        "ui/packages/plugin-dashboard/src/ObjectMetricWidget.tsx",
        "ui/packages/plugin-dashboard/src/__tests__/ObjectMetricWidget.polarity-27347.test.tsx",
        "ui/packages/plugin-dashboard/src/index.tsx",
        "ui/packages/plugin-dashboard/src/utils.ts"
      ],
      "tests": "spec object-metric-polarity + dataset-measure-polarity: 2 files 34 tests pass. ui ObjectMetricWidget.polarity-27347 + DatasetWidget.polarity-26602 + objectMetricTrendMembers-8071 + ObjectMetricWidget.compareTo: 4 files 32 pass. Whole plugin-dashboard dir: 192 files, 1874 pass, 6 skipped. spec src/ui + type-alias-convention pin: 165 files, 5854 pass. apps/console registry-inputs-spec-parity: 261 pass. Ablation via scripts/ablation-replace.mjs on MetricWidget.tsx (polarity read replaced by undefined, anchor 1 to 0, blob 3074987730fb to 52fc66031198): 3 failed / 4 passed (both lower cases + static-badge case red; neutral and CONTROL rows green by design); restored, blob == HEAD, git diff HEAD empty. No dist leg: the test imports the widget by relative path. tsc pin reverse check: polarity 'up' to 'neutral' under the ts-expect-error turned check:test-typecheck red (1 type error in the new test); restored.",
      "gates": "Final head ea930c31: dispatch-gates --commands derives 122 families; all 122 run, all exit 0; --ran reconciliation: 122 derived, 122 run, 0 NOT-MEASURED. 121 were run on tree 804c789e (head 10b82523, identical to ea930c31 minus the two regenerated content/docs/ui/components pages); the 122nd (pnpm check:sdui-reference, derived only once those pages entered the diff) run on ea930c31. First pass on 2f286db1: three exited 3 on prerequisites and were re-run green (plugin-teardown self-test after fetching its pinned fixture commit; check:skill-examples after building client-react; check:dual-build-cjs-loads after a full turbo build). Also local: check-dev-prereqs 0, check:skill-frame-freshness 0, ui check:new-line-citations 0 (0 new), eslint on changed files 0 errors, plugin-dashboard type-check (src+tests) 0, spec typecheck 0, spec check:generated all 14 current after the last merge. CI on 10b82523: every required context green except Lint and Repo Gates, red on step check:sdui-reference (component reference pages not regenerated); fixed in ea930c31, CI in progress at report time.",
      "line_budget": "skills/objectstack-ui/rules/dashboards.md: lines 460 to 460, tokens 6214 to 6216 of ceiling 6221 (no raise). Paid by cutting the bullet's options.trend-reaches-no-renderer clause; surviving home: the same file's Widget options section. objectstack-ui md lines 2087 to 2087; all SKILL.md lines 4614 to 4614.",
      "dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm",
      "mcp_calls": "0 GitHub MCP calls. 1 claude-code-remote subscribe_pr_activity (PR 27361).",
      "api_writes": "2: relay pr_create (POST pulls, PR 27361, 7359 bytes sent = stored, assignee os-warren, requested_reviewers read back empty) and this report via post-stamped (POST issues/27347/comments). Pushes are git, not REST.",
      "deviations": [
        "No @object-ui/types changeset row and no types diff: ObjectMetricBlockSchema takes the spec row by reference and ObjectMetricBlockNode derives from it, so the package's own files do not change.",
        "Alias map is tile-local and omits the dataset measure's direction alias: on this tile direction is trend.direction, so a guidance row names both keys. A shared constant would be a new public export or a new module outside the claimed file surface.",
        "Report opens with the seat signature line before os-dev-report: post-stamped refuses an unsigned body.",
        "Commit trailers are the model-free pair (AGENTS.md); the harness reminder asked for a model-named co-author.",
        "Session-context MCP assignee and reviewer step skipped: the order bans GitHub MCP; the relay set the assignee, and Tier H reviewers are deliberately not requested.",
        "Tier H hold route: be19eec3 restored dashboards.md to main's bytes for pr_create; 10b82523 reverted it. CI on be19eec3 is superseded."
      ],
      "body_amendments": [
        "Verification, Derived gates: 121 families becomes 122 (check:sdui-reference joined with the regenerated pages), all exit 0 on head ea930c31.",
        "What changes, Generated: add content/docs/ui/components/index.mdx and plugins.mdx (gen-sdui-reference)."
      ],
      "open_questions": [],
      "out_of_scope_findings": [
        "class: a (tooling) · reach: named producer scripts/pm/dispatch-gates.mjs --commands · a diff that changes sdui.manifest.json but not content/docs/ui/components/** does not derive pnpm check:sdui-reference, although that gate reads the manifest; this PR went red in CI on it after 121 derived families were green · dedupe words: dispatch-gates sdui-reference derivation manifest",
        "carrier: seat"
      ]
    }
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:uipriority: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