Skip to content

feat(spec,ui): an inline object-metric tile declares polarity, so a lower-is-better KPI draws a rise red - #27361

Merged
os-zhuang merged 8 commits into
mainfrom
claude/project-thread-5ky7zm
Oct 29, 2026
Merged

os-zhuang merged 8 commits into
mainfrom
claude/project-thread-5ky7zm

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Warren · project thread
Fixes #27347

Clause-②: yes (widening)
Metadata-protocol: added (object-metric polarity)

维护者速读

  • 改了什么:页面级 object-metric 指标卡新增可声明的 polarity(higher / lower / neutral,与数据集度量同一词表),趋势徽标(compareTo 计算的或静态 trend)按它着色。
  • 为什么改:内联指标卡只按方向着色,"越低越好"的指标(平均周转天数)上涨 23% 被画成绿色,与同一看板上数据集指标卡的颜色相反,是读错数。
  • 风险与回滚:不声明时颜色与现在逐字节一致(测试钉住类名字符串);只新增一个可选键,回滚即还原本 PR。
  • 先看:packages/spec/src/ui/component.zod.ts 的 object-metric 段(新键与拒收提示);ui/packages/plugin-dashboard/src/MetricWidget.tsx 的徽标着色一行;ui/packages/plugin-dashboard/src/utils.ts 的共享 deltaToneClass。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/plugin-dashboard/src/__tests__/ObjectMetricWidget.polarity-27347.test.tsx,预期 7 个用例全绿。
  • 需要你做的:涉及发布的 spec 与 skills/**(Tier H),契约复核 PASS 与席位 ACCEPT 之后再批准合入。

What changes

  • Spec (ObjectMetricPropsSchema, the ComponentPropsMap['object-metric'] row): an optional top-level polarity, the dataset measure's own DatasetMeasurePolarity by reference (no second spelling, no new export). Any other value is refused at the key. betterWhen / goodDirection / trendDirection are aliased to polarity. A top-level direction gets a prescription naming both keys it could mean (trend.direction for the static badge's arrow, polarity for good news), because on this tile the dataset measure's direction alias would send an author who meant the badge arrow to the wrong key. higherIsBetter / lowerIsBetter get the three values.
  • Top-level, not inside compareTo: the badge MetricWidget draws is derivedTrend ?? trend, and the static trend badge was coloured by direction alone too, so one key covers the computed and the static badge. The reason is in the key's docblock.
  • Renderer (@object-ui/plugin-dashboard): deltaToneClass moved from DatasetWidget.tsx to utils.ts (one colouring rule, now also answering "no tone" for a static badge with no direction). ObjectMetricWidget forwards polarity; MetricWidget colours the badge through deltaToneClass and mutes it under neutral. Both prop types name the key by reference (ObjectMetricProps['polarity']). The object-metric registration publishes an enum input; sdui.manifest.json regenerated.
  • Undeclared keeps today's colours exactly: the class string is pinned byte for byte. Nothing infers a polarity from the aggregated field's name.
  • Generated: authorable-surface/ui.json, content/docs/references/ui/component.mdx, sdui.manifest.json + its record, and content/docs/ui/components/index.mdx / plugins.mdx (gen-sdui-reference). liveness/page.json row regions gains evidence entry [121] object-metric.polarity (read points cited in-repo with symbol anchors).
  • Skills (skills/objectstack-ui/rules/dashboards.md, Tier H): the metric-widget bullet says an inline object-metric declares its own polarity.
  • Changeset: @objectstack/spec minor, @object-ui/plugin-dashboard minor, @objectstack/skills patch.

Tests

  • packages/spec/src/ui/object-metric-polarity.test.ts: the three values parse and keep their value; the member IS DatasetMeasurePolarity (identity); absent materializes nothing; up / down / good / inverse / true refused at the key; the three aliases point at polarity; top-level direction names trend and polarity; the two boolean-flag spellings get the values; a tsc pin (@ts-expect-error on polarity: 'up').
  • ui/packages/plugin-dashboard/src/__tests__/ObjectMetricWidget.polarity-27347.test.tsx, through the registered object-metric block: lower rise drawn bad and fall drawn good with the arrow kept; higher as before; neutral muted both ways; CONTROL undeclared class strings byte-identical (field named turnaround_days); the static trend badge coloured by the same key; CONTROL a static badge with no direction takes no tone.

Acceptance notes

  • @object-ui/types has no diff: ObjectMetricBlockSchema takes the spec row by reference (propsBag('object-metric', SpecObjectMetricPropsSchema)), and ObjectMetricBlockNode derives from that arm, so the key reaches the ui zod face and the authoring type with no edit there. No @object-ui/types changeset row, since the package's own files do not change.
  • The near-miss alias map and the not-a-flag prescription are declared on the tile rather than shared with dataset.zod.ts: a shared constant would be a new public export (ui/index.ts re-exports dataset.zod whole) or a new module outside the claimed file surface.
  • Skills token ratchet: rules/dashboards.md 6214 to 6216 tokens of ceiling 6221 (no raise). Paid by cutting the bullet's options.trend reaches no renderer clause, whose surviving home is the same file's Widget options section (a dataset-bound widget's trend styles nothing).

Verification

Readings taken on the merged tree (spec and plugin-dashboard closure built first).

Ablation (one-time, restored)

scripts/ablation-replace.mjs on MetricWidget.tsx: deltaToneClass(shownTrend.direction, polarity), to deltaToneClass(shownTrend.direction, undefined), (anchor 1 to 0, blob changed); the pin went 3 failed / 4 passed: the two lower cases and the static-badge case red. Restored: blob equals HEAD, git diff HEAD empty. The neutral and CONTROL rows stay green under the mutation by design.

Reverse check of the tsc pin: replacing polarity: 'up' with 'neutral' under the @ts-expect-error turned check:test-typecheck red (1 type error in object-metric-polarity.test.ts); restored to HEAD.

Suites and type-checks
  • spec src/ui/ + type-alias-convention.pin.test.ts: 165 files, 5854 tests passed.
  • pnpm --dir ui exec vitest run packages/plugin-dashboard/: 192 files, 1874 passed, 6 skipped.
  • apps/console/src/__tests__/registry-inputs-spec-parity.test.ts: 261 passed.
  • @object-ui/plugin-dashboard type-check (src + tests) exit 0; @objectstack/spec typecheck exit 0.
  • pnpm --filter @objectstack/spec check:generated: all 14 artifacts up to date after the last merge.
Derived gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 122 families (check:sdui-reference joined with the regenerated pages), all run, all exit 0 on head ea930c31 (--ran: 122 derived, 122 run, 0 NOT-MEASURED). Three first exited 3 on prerequisites, not findings, and were re-run green: the plugin-teardown self-test (shallow clone; fixture commit fetched), check:skill-examples (client-react built) and check:dual-build-cjs-loads (full build). Also local: check-dev-prereqs, check:skill-frame-freshness, ui check:new-line-citations (0 new), eslint on the changed files (0 errors).


Generated by Claude Code

@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 2 changed file(s) yielded no anchor (packages/spec/authorable-surface/ui.json, packages/spec/liveness/page.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/authorable-surface/ui.json, packages/spec/liveness/page.json) — pages documenting those are invisible to this run
  • the SDK route bridge reached 58 of 210 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 165 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 28872e68d39a8403fe38eee532476c693e2f960f → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 07c0d56e8d0141dd1e0540449b05f9a46772b0bc — the merge of head ea930c31c0479557aadd4dfb25011c6655891ae9 into base 28872e68d39a8403fe38eee532476c693e2f960f, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 07c0d56e8d0141dd1e0540449b05f9a46772b0bc && git checkout 07c0d56e8d0141dd1e0540449b05f9a46772b0bc
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 28872e68d39a8403fe38eee532476c693e2f960f ea930c31c0479557aadd4dfb25011c6655891ae9 && git checkout -B drift-repro 28872e68d39a8403fe38eee532476c693e2f960f && git merge --no-ff ea930c31c0479557aadd4dfb25011c6655891ae9

node scripts/docs-audit/affected-docs.mjs --json 28872e68d39a8403fe38eee532476c693e2f960f

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Seat note · contract review owed · seat domain:ui#2 · session_01JQwxbXpF772qWJjojJvRXw · 2026-10-29T03:29Z

This PR is Tier H. It adds a key to the published schema (object-metric polarity in packages/spec/src/ui/component.zod.ts), declared Clause-②: yes (widening) and Metadata-protocol: added, and it edits a governed path (skills/objectstack-ui/**, the dashboards reference, one line). So it owes a review at CONTRACT_REVIEW_TIER. The seat runs at TIER_DEFAULT and hands that review to triage. The PR stays draft on purpose.

Contract-review-handoff: triage — CONTRACT_REVIEW_TIER unavailable (ceiling model out for the fleet since 2026-10-27, seat runs at TIER_DEFAULT, 2026-10-29T03:29Z) · head ea930c31c0479557aadd4dfb25011c6655891ae9

Inputs for the reviewer:

  • Card 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: triage filing and grading 6485097275 (p1 for remedy 1). Order 6485179476, claim 6485222036, os-dev report 6487077276.
  • What must hold:
    • Card ⛔: never infer polarity from a field name.
    • An undeclared node renders byte-identically. The class string is pinned.
    • The vocabulary is the dataset measure's polarity (higher | lower | neutral) by reference, not a second spelling.
    • There is one colouring rule: deltaToneClass moved to plugin-dashboard/src/utils.ts, and both the dataset tile and MetricWidget read it.
  • Recorded choices for your eye:
    • The key is top-level and not inside compareTo, because MetricWidget draws derivedTrend ?? trend, so one key covers the computed badge and the static badge.
    • The alias map is tile-local. On this tile direction is trend.direction, so its guidance row names both keys and does not reuse the measure's direction → polarity alias.
    • There is no @object-ui/types diff, because ObjectMetricBlockSchema takes the spec row by reference.
  • Skills ratchet: dashboards.md goes 6214 → 6216 of 6221, so no raise.
  • Open sequence (seat-1 route): be19eec3 held dashboards.md at main's bytes, then pr_create ran, then 10b82523 reverted the hold. requested_reviewers is empty.
  • CI: on 10b82523 it was red only on check:sdui-reference. ea930c31 fixes that with regenerated component reference pages. The run is in progress.

No reviewer is requested, and the PR stays draft until a PASS is on file and the seat's ACCEPT is posted (#26617).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

Contract-review-claim: session_01AavokzJ5DndAwitDXvKy4U · head ea930c31c0479557aadd4dfb25011c6655891ae9 · 2026-10-29T03:54Z

Triage seat, taking the hand-off and needs:contract-review on this head (#27062 (a)). The review runs at CONTRACT_REVIEW_TIER. If the lane claims or posts a record on this head first, triage withdraws.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: ea930c31c0479557aadd4dfb25011c6655891ae9
Local-runs: none
Scope: packages/spec/src/ui/component.zod.ts, skills/objectstack-ui/rules/dashboards.md + packages/spec/src/ui/dataset.zod.ts (DatasetMeasurePolarity 247 / 764, the measure's aliases 380-385 and guidance 391-392, DATASET_POLARITY_NOT_A_FLAG 96-98), packages/spec/src/shared/strict-object.ts (aliases / guidance / guidanceSets, consulted on unrecognized_keys only), packages/spec/src/ui/component.zod.ts outside the hunks (ObjectMetricTrendSchema 5460-5469 with its optional direction, FILTERS_TO_FILTER 4743, COMPONENT_LEVEL_GUIDANCE 434, invert 5733), packages/spec/src/ui/object-metric-polarity.test.ts, ui/packages/plugin-dashboard/src/MetricWidget.tsx (shownTrend = derivedTrend ?? trend 375, the badge 498-507, trend.direction? 274), ui/packages/plugin-dashboard/src/ObjectMetricWidget.tsx (derivedTrend 684-702 over raw fetchedValue / previousValue, invert 589-592, the forward 762-767), ui/packages/plugin-dashboard/src/utils.ts (deltaToneClass), ui/packages/plugin-dashboard/src/DatasetWidget.tsx (the two callers 1398 / 1551, the deleted local copy), ui/packages/plugin-dashboard/src/index.tsx (ObjectMetricBlock spreads the authored bag 257-261; the registration 285-333), ui/packages/types/src/zod/objectql.zod.ts (ObjectMetricBlockSchema 4434-4439, propsBag('object-metric', stripImportedDefaults(SpecObjectMetricPropsSchema))) and ui/packages/types/src/authoring-nodes.ts (ObjectMetricBlockNode 215), ui/packages/plugin-dashboard/package.json (@objectstack/spec workspace dependency) and packages/spec/package.json (./ui subpath), ui/packages/plugin-dashboard/src/tests/ObjectMetricWidget.polarity-27347.test.tsx, packages/spec/liveness/page.json (row regions, evidence entry [121]), packages/spec/authorable-surface/ui.json, sdui.manifest.json and scripts/sdui-manifest.record.json, scripts/check-skills-token-ratchet.mjs (row 854, dashboards.md ceiling 6221), skills/objectstack-ui/rules/dashboards.md 330-338 (the Widget options section the cut clause survives in) and skills/objectstack-ui/rules/pages.md 56 / 87 / 143 / 157 (where the inline object-metric is taught), skills/objectstack-ui/evals/analytics-inline-vs-dataset.json (eval 8), .changeset/27347-object-metric-polarity.md and the precedents .changeset/26602-measure-polarity.md and .changeset/26633-report-period-export-eval.md, packages/skills/package.json (@objectstack/skills), PR #26739 post-landing record 6485049930 (should-fix S1, the card's source) and card #27345 (the ruling that ordered it)

Inputs read: card #27347 body and all four comments (triage grade 6485097275, dispatch order 6485179476, claim 6485222036, os-dev report 6487077276); PR #27361 body, its 17-path file list and all three PR comments (docs-drift, seat hand-off 6485-series, triage claim); the net diff against main at the merge-base eef83734 (17 files, equal to the file list); the 74 check-runs on ea930c31; the #26739 post-landing record that filed S1 and card #27345's body and claim, by gh api GET. Head read as ref prr-27361; nothing checked out, built or run.

① Derived judgments

  • ObjectMetricPropsSchema gains one optional top-level key polarity, typed DatasetMeasurePolarity.optional() — the measure's enum BY REFERENCE (the spec test pins shape.polarity.unwrap() IS DatasetMeasurePolarity), no new export, no second spelling; absent materialises nothing (hasOwnProperty === false pinned) — RIGHT. The card's vocabulary clause and the dispatch order's "⛔ no second spelling" both hold in the tree.
  • Accept-set at the key: higher / lower / neutral accepted; every other value refused with invalid_value at ['polarity'] (pinned on up, down, good, inverse, true) — RIGHT; the tsc pin (@ts-expect-error on 'up') covers the author type.
  • Accept-set at the shape: strictObject already refused every undeclared key; the new aliases rows (betterWhen / goodDirection / trendDirection → polarity) and guidance rows (direction, higherIsBetter, lowerIsBetter) change only the prescription on the unrecognized_keys path (read in strict-object.ts: consulted there and nowhere else) — RIGHT and NOT a narrowing. FILTERS_TO_FILTER is kept by spread, so filters → filter still answers.
  • The measure's direction → polarity alias is deliberately NOT carried: on this tile direction is the static badge's arrow (ObjectMetricTrendSchema.direction, 5469), so the guidance names both trend and polarity — RIGHT; an author who meant the arrow is not sent to the colour key. Pinned.
  • Public surface, @objectstack/spec: authorable-surface/ui.json +1 (ui/ObjectMetricProps:polarity); no api-surface change (nothing new exported) — RIGHT. content/docs/references/ui/component.mdx +1 row (generated; read, not reviewed).
  • Public surface, @object-ui/plugin-dashboard: MetricWidgetProps.polarity? and ObjectMetricWidgetProps.polarity?, both ObjectMetricProps['polarity'] by reference; deltaToneClass moves DatasetWidget.tsx → utils.ts and is NOT re-exported from the package index (read index.tsx: no export … from './utils'), so it is package-internal; the registration gains an enum input with the three values, and sdui.manifest.json + its record regenerate (+10 / +4) — RIGHT. The console registry-inputs/spec-parity census is covered by Test UI success.
  • @object-ui/types: no diff and no changeset — RIGHT. ObjectMetricBlockSchema.properties is propsBag('object-metric', stripImportedDefaults(SpecObjectMetricPropsSchema)) and ObjectMetricBlockNode is the arm's input, so the key reaches the ui zod face and the authoring type through the spec reference with no file change there (the card's "plus the @object-ui/types row" presumed an edit that the mirror does not need). The dispatch order said "measure whether the mirror pairs by reference"; it does.
  • Renderer accept-set: ObjectMetricBlock spreads the authored bag ({...props}), ObjectMetricWidget destructures polarity and forwards it, MetricWidget colours shownTrend through deltaToneClass(shownTrend.direction, polarity) plus the explicit muted class on direction === 'neutral' || polarity === 'neutral' — RIGHT. Traced against the old three-way &&: undeclared + up → emerald, undeclared + down → rose, direction neutral → muted only, a static badge with no direction → no tone (the new direction === undefined arm returns undefined, exactly what the old code did when none of its three conditions fired). The undeclared class string is pinned byte for byte in the ui test on a field literally named turnaround_days; nothing in the diff reads a field name. Card ⛔ holds.
  • DatasetWidget.tsx keeps both call sites (1398 the tile on f?.polarity, 1551 the cross-tab cell on measureField(measure)?.polarity) against the moved helper, whose polarity parameter widened from DatasetResultField['polarity'] to DatasetMeasurePolarity | undefined — RIGHT (same values; the field type is the enum relayed on the wire). One colouring rule, as the order required; the direction === undefined arm is the only semantic addition and is a no-op for the dataset tile, whose delta.direction is never undefined.
  • Liveness: page.json row regions gains evidence entry [121] naming the registration, the forward, the draw site, deltaToneClass and the pin, with symbol anchors — RIGHT; Spec property liveness success.
  • Gates: 74 check-runs on the head, 60 success, 13 skipped (opt-in / conditional jobs), 1 cancelled (Check Changeset in run 40812427634, superseded by two successes of the same job on this head). Nothing pending, nothing red. The 10b8252 check:sdui-reference red is fixed on this head (the two regenerated content/docs/ui/components pages).

② Semver level

  • .changeset/27347-object-metric-polarity.md: @objectstack/spec minor — an added optional key on a published schema, matching the 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 precedent (26602-measure-polarity.md, minor for DatasetMeasure.polarity) — RIGHT. @object-ui/plugin-dashboard minor — two new optional public props and a new registry input; nothing removed — RIGHT. @objectstack/skills patch — one line in skills/objectstack-ui/rules/dashboards.md, matching 26633-report-period-export-eval.md — RIGHT. No @object-ui/types row, because no file of that package changes (①); a row for an unchanged package would bump a version with no diff behind it.
  • Clause-②: yes (widening) on the PR body and in the changeset body; Metadata-protocol: added (object-metric polarity) on both — PRESENT and TRUE: the diff widens the accept-set of ObjectMetricPropsSchema by one optional key and narrows nothing. Check Changeset success.

③ Boundary flags

  • open_questions: [] — none to answer.
  • Deviation 1 (no @object-ui/types changeset or diff): ANSWERED — RIGHT, by reference (①). The card's remedy text presumed an edit the mirror does not need.
  • Deviation 2 (tile-local alias map, direction as guidance naming both keys, no shared constant): ANSWERED — RIGHT. A shared constant would be a new public export (ui/index.ts re-exports dataset.zod whole) or a new module outside the claimed surface; the two string constants restate the measure's prescription with "tile" / "badge" for "measure" / "delta", which is the correct surface wording. The dispatch order's "reuse the same enum, export it from one place if not already" is met: it was already exported.
  • Deviation 3 (report opens with the seat signature line): procedural, not a tree matter. Nothing to escalate.
  • Deviation 4 (model-free commit trailers per AGENTS.md over the harness reminder): AGENTS.md wins in-repo. Nothing to escalate.
  • Deviation 5 (no MCP assignee / reviewer step; Tier H reviewers not requested): CONSISTENT with fleet-write: relay pr_create asks the governed approvers to review a Tier H draft before any contract review exists, so an approver can approve and merge it unreviewed (#26566, #26570) #26617 and card pm-dispatch(contract-review): a seat sends a PR to the maintainer for approval only after a contract PASS on the current head, and a PR that landed still owing one is re-reviewed after landing, with each finding filed as a new card #27345 (approval only after a PASS on the current head). requested_reviewers reads empty on pr.json; Governed Surface Queue Guard success.
  • Deviation 6 (Tier H hold route be19eec3 → 10b82523): the net diff against main is what is judged here; the hold leaves no trace in it. Nothing to escalate.
  • out_of_scope_findings[0] (dispatch-gates does not derive check:sdui-reference from a sdui.manifest.json-only diff; carrier: seat): a tooling observation about scripts/pm/dispatch-gates.mjs, outside this PR's surface; the seat is named carrier. Not a finding against this head, on which that gate is green.
  • Skills token ratchet: dashboards.md is 24862 bytes on the head → ceil(24862 / 4) = 6216 against row 854's ceiling 6221 (main 24853 → 6214); no raise. Lint & Repo Gates success. The cut clause ("options.trend reaches no renderer") survives at 335-337 of the same file (the Widget options section: trend styles nothing, os validate warns unconsumed-widget-option), so nothing taught is lost.
  • Prior record: feat(spec): a dataset measure declares its polarity, so a lower-is-better KPI draws a rising delta as bad news #26739 post-landing record 6485049930 filed S1 on this exact gap (MetricWidget.tsx 489-491 direction-only on derivedTrend ?? trend); this head closes it in the tree, both the compareTo-derived badge and the static one, as that record's reading implied.
  • Gates on this head: none pending, none red (①). Nothing assumed.

④ Design judgment

packages/spec/src/ui/component.zod.ts — judged against card #27347 remedy 1 (the ruling it implements), the #26739 S1 finding, and its first consumer (ObjectMetricWidget → MetricWidget through ObjectMetricBlock):

  • The key's placement holds. Remedy 1 says "a widget-level polarity on the object-metric component props"; the dispatch order asked the dev to measure whether the static trend had the same flaw. It did: shownTrend = derivedTrend ?? trend and the old badge coloured by direction alone for both. One top-level key covers both badges; a compareTo member would have left the static badge direction-only, and compareTo on this tile is "kind ALONE", the executor's closed directive (its own docblock). The reason is in the key's docblock, as the order required.
  • Vocabulary by reference holds (①): the member IS the measure's enum. The docblock's reading of the arrow ("always shows which way the NUMBER moved; polarity decides only the colour") matches MetricWidget 504-506 (the arrow is shownTrend.direction, untouched by polarity).
  • Tried to break it: (a) polarity with trend.direction: 'neutral' → muted, no tone, as before. (b) polarity: 'neutral' with a non-flat delta → deltaToneClass undefined and the explicit muted class; arrow kept; pinned both ways. (c) polarity: 'lower' with a static badge naming no direction → no tone (pinned CONTROL). (d) a zero baseline → derivedTrend.value null, direction from computeMetricDelta; the colour rule reads direction only, unaffected. (e) The direction guidance against the measure's direction alias: an author moving a measure's direction: 'lower' onto the tile gets a message naming polarity with the 'lower' example and trend.direction for the arrow — the right two doors. (f) invert: true (5733: display 1 - value): derivedTrend is computed on the RAW fetchedValue / previousValue (684-688) while the shown number is inverted (589-592), so a raw rise shows as a displayed fall with an up arrow. That mismatch pre-dates this PR and is not introduced by it; polarity declared against the raw aggregate ('lower' for an error rate shown as uptime) colours correctly. But the new docblock says only "the NUMBER", and under invert there are two numbers. Should-fix S1: one sentence in the polarity docblock (or the invert describe) stating that the badge's arrow and polarity read the aggregated value before invert. Not a must-fix: no author is misled into a wrong colour by any declared value; the ambiguity is in the prose and the invert + compareTo combination is a pre-existing arrow question.
  • The near-miss and not-a-flag prescriptions are correct claims about the schema: none of the six keys is declared on the shape, and strict-object.ts consults them only on unrecognized_keys; the spec test pins each.
  • No must-fix.

skills/objectstack-ui/rules/dashboards.md — judged against the spec it teaches on this head (DatasetMeasure.polarity and the new ObjectMetricProps.polarity, one vocabulary, one colouring rule) and the card's only eval touchpoint (eval 8 asserts polarity: 'lower' on a dataset measure; the card has no inline-tile eval and adds none):

  • The bullet is TRUE on the head: "Its colour follows polarity (the measure's; an inline object-metric declares its own)" — the measure's key reaches the dataset tile on fields[].polarity, the inline tile's key reaches MetricWidget from the node; both through deltaToneClass. 'lower' draws a rise red, 'neutral' uncoloured, undeclared = rise green — each matches the traced renderer. "compareTo is the trend" still holds for both tiles.
  • Eval 8 is unchanged and still passes the taught rule (a dataset measure with polarity: 'lower' under compareTo); the new clause does not steer that prompt toward an inline tile. No eval regression.
  • Tried to break it: the clause lives under the dashboard compareTo section whose table teaches dimension, which the inline tile REFUSES (ObjectMetricCompareToSchema guidance); a reader could pair "inline object-metric declares its own" with compareTo.dimension. The bullet itself does not say dimension, and the inline tile's own section is pages.md (rows 56 / 87, example 157), which names object-metric as "Single KPI widget" and says nothing of compareTo or polarity. The card asked for "a skills line saying where an inline KPI declares it"; the line says where (on the tile) and is placed where polarity is taught, which is the right single home for the key. Should-fix S2: a pointer from pages.md's object-metric row (or its example) to dashboards.md's polarity bullet, so an author building an inline KPI from pages.md finds the key; priced under that file's own ratchet row. Not a must-fix: the teaching is correct and findable from the compareTo section, and the governed line budget held (6216 / 6221) without a raise.
  • The cut clause is not lost (③). The ratchet arithmetic is verified from bytes, not from the body's account.
  • No must-fix.

Should-fix list: S1 (docblock: arrow and polarity read the pre-invert aggregate), S2 (pages.md pointer to the polarity bullet). Neither blocks landing; both can ride the next skills or spec card on this tile.

Implemented-by: claude/project-thread-5ky7zm
Reviewed-by: session_01AavokzJ5DndAwitDXvKy4U

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-warren · session_01JQwxbXpF772qWJjojJvRXw

席位核对

ACCEPT · domain:ui#2 · session_01JQwxbXpF772qWJjojJvRXw · head ea930c31c0 · 2026-10-29T04:08Z

席位意见:页面级 object-metric 磁贴新增可选键 polarity,按引用复用数据集度量的 DatasetMeasurePolarity(higher / lower / neutral),趋势徽标(compareTo 算出的和静态 trend)都按它着色。不声明时颜色与今天完全一致。

  1. 契约复核: triage CONTRACT_REVIEW_TIER 在 ea930c31 上 PASS(6487637026),无必修。应修两条:S1 是 polarity 文档注释补一句"箭头和 polarity 读的是 invert 之前的聚合值";S2 是 pages.md 的 object-metric 行指向 dashboards.md 的 polarity 条目。两条都不挡落地,席位另开一张卡承接。
  2. 席位自读(复核 hits 之外的部分):
    • utils.ts 的 deltaToneClass 是原来 DatasetWidget 本地那份原样上移:neutral 方向或 neutral 极性不着色,lower 时下降为绿,其余上升为绿。DatasetWidget 的两个调用点改为引用它。
    • MetricWidget 的徽标把原来三行按方向着色换成 deltaToneClass(direction, polarity),外加 neutral 时的 muted 类。箭头仍只看 direction。
    • ObjectMetricWidget 只是透传 polarity。index.tsx 的 ObjectMetricBlock 展开作者写的属性包,所以节点上的键能到达组件;注册表新增 polarity 输入。
    • 新测试 ObjectMetricWidget.polarity-27347 覆盖 lower / neutral / 不声明 / 静态徽标无方向四种情况。
  3. changeset 逐句对照 diff: spec minor,plugin-dashboard minor,skills patch;Clause-②: yes (widening) 与 Metadata-protocol: added 与树一致;"不声明保持今天的颜色"与 deltaToneClass 的分支一致。
  4. 形态: Fixes #27347。属 Tier H(已发布 schema + skills/**),用席位 1 的路线打开,复核前 requested_reviewers 为空。dashboards.md 6216 / 6221,未抬 ratchet。
  5. 门禁: ea930c31 上 74 个 check-run:60 success、13 skipped、1 cancelled(被同 head 上随后成功的 Check Changeset 取代),0 失败,mergeable clean。本 PR 是 widening,不需要 dogfood 全量读数。
  6. 落地条款: 席位现在翻 ready 并开启自动合并,跟到 MERGED。

维护者速读(席位终稿)

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 29, 2026 04:09
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 29, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Oct 29, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#2 · os-warren · session_01JQwxbXpF772qWJjojJvRXw

Seat note · 2026-10-29T04:22Z

The merge queue removed this PR at 04:22Z. The only failing check on the queue commit 05b3f0cf is Governed Surface Queue Guard, step "Governed surfaces may not enter the merge queue unreviewed". skills/objectstack-ui/rules/dashboards.md is governed, and the PR has no approval yet.

That is not a defect in this diff. The head ea930c31 is unchanged: contract review PASS (6487637026), seat ACCEPT (6487653940), CI green.

Needed: approval from hotlong or os-zhuang, both requested at 04:09Z. Once an approval lands, the seat re-enqueues the PR.

@os-zhuang
os-zhuang added this pull request to the merge queue Oct 29, 2026
Merged via the queue into main with commit dbf576b Oct 29, 2026
77 of 78 checks passed
@os-zhuang
os-zhuang deleted the claude/project-thread-5ky7zm branch October 29, 2026 05:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ui size/m tests tooling

Projects

None yet

3 participants