Skip to content

feat(spec): a dataset measure declares its polarity, so a lower-is-better KPI draws a rising delta as bad news - #26739

Merged
objectstack-fleet[bot] merged 15 commits into
mainfrom
claude/project-thread-hjxyd2
Oct 28, 2026
Merged

objectstack-fleet[bot] merged 15 commits into
mainfrom
claude/project-thread-hjxyd2

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread

Fixes #26602

Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: updated here — skills/objectstack-ui/rules/dashboards.md (the metric-widget compareTo bullet and the measure presentation line), skills/objectstack-ui/evals/analytics-inline-vs-dataset.json (eval 8 now asks for the polarity and asserts polarity: 'lower'), content/docs/ui/dashboards.mdx

维护者速读

  • 改了什么:数据集度量新增可选键 polarity(higher / lower / neutral),分析服务把它带到答案的列描述上,仪表盘 KPI 卡片和交叉表单元格按它给环比变化上色。
  • 为什么改:「平均审批周转」这类越低越好的指标,周转变慢 23% 却显示绿色上升箭头,把坏消息画成了好消息。
  • 风险与回滚:未声明 polarity 的度量颜色与现在完全一致(上升绿、下降红),纯新增;回滚即还原本 PR。
  • 先看:packages/spec/src/ui/dataset.zod.ts(新键与取值)、ui/packages/plugin-dashboard/src/DatasetWidget.tsx 的 deltaToneClass、packages/services/service-analytics/src/analytics-service.ts 的 enrichResultColumns 一行转发。
  • 两分钟验证:在 ui/ 目录执行 pnpm exec vitest run packages/plugin-dashboard/src/__tests__/DatasetWidget.polarity-26602.test.tsx,预期 9 个用例全部通过。
  • 需要你做的:本 PR 触及 skills/**(Tier H),合并需要你的批准;另请确认 eval 文件 token 上限按裁决 A 抬高 13。

What changes

Where the key lives, and why. The card offered two seats: the widget's compareTo or the dataset measure. Measured at origin/main 4373103, the widget's compareTo cannot carry it:

  • DatasetWidget forwards widget.compareTo verbatim into the dataset selection.
  • The wire DatasetCompareToSchema (packages/spec/src/api/analytics.zod.ts) is a strictObject, so a new member there would be refused on every query.
  • packages/spec/src/ui/dashboard-compareto.test.ts pins "a parsed widget compareTo is assignable to DatasetCompareTo with no mapping". The widget's slot is a thin projection of the executor's own directive, by design.

So the key is declared on the dataset measure only, the semantic layer, where every widget over the measure agrees. A count whose meaning flips with a filter (overdue vs resolved records) is two measures, each with its own measure filter and its own polarity. A widget-level override would be a later, pure widening if a consumer needs one.

The values. DatasetMeasurePolarity = z.enum(['higher', 'lower', 'neutral']). They name the better direction rather than a sign or a boolean, so neutral (no good/bad colour, for a measure where neither direction is news) is a peer value and not a third state of a flag. Undeclared keeps today's colours exactly (a rise green, a fall red). Nothing infers a polarity from the measure's name or unit.

  • Spec (packages/spec/src/ui/dataset.zod.ts): DatasetMeasureSchema.polarity, optional, no default materialized. Near-miss keys betterWhen (the card's own proposal), direction, goodDirection, trendDirection are pointed at polarity; the boolean spellings higherIsBetter / lowerIsBetter are answered with the three values.
  • Wire (packages/spec/src/api/analytics.zod.ts, packages/spec/src/contracts/analytics-service.ts): the column descriptor declares fields[].polarity with the same enum, mirrored in AnalyticsResult and AnalyticsResultResponseSchema (the existing compile-time drift guard binds the two).
  • Producer (packages/services/service-analytics/src/analytics-service.ts#enrichResultColumns): one line relays the authored value, on the live query and the draft preview alike, the __compare column included. Undeclared measures and dimension columns carry no key, so an answer over a dataset that declares none is byte-identical.
  • Renderer (ui/packages/plugin-dashboard/src/DatasetWidget.tsx): one helper, deltaToneClass(direction, polarity), colours both surfaces that colour a delta, the metric tile and the cross-tab cell. lower draws a rise red and a fall green; neutral draws neither (the tile's muted text colour); higher and undeclared draw a rise green and a fall red. The arrow keeps the number's direction under every polarity, and a flat delta stays muted.
  • Metadata form (packages/spec/src/ui/dataset.form.ts): the measures repeater offers polarity, labelled in zh-CN / ja-JP / es-ES (packages/platform-objects/src/apps/translations/*.metadata-forms.generated.ts).
  • Liveness (packages/spec/liveness/dataset.json): measures.polarity live, with the renderer as evidence and enrichResultColumns as producer.
  • Skills and docs: the metric-widget bullet in skills/objectstack-ui/rules/dashboards.md teaches the key (paid for inside the file, and the file's ceiling is lowered 6239 → 6221); eval 8 asks for it; content/docs/ui/dashboards.mdx gains a presentation-home row and a sentence on the compareTo row.
  • Changesets: .changeset/26602-measure-polarity.md (@objectstack/spec minor, @objectstack/service-analytics minor, @objectstack/platform-objects minor for the four regenerated metadata-forms translation catalogs it publishes under ./apps), .changeset/26602-delta-polarity.md (@object-ui/plugin-dashboard minor; ui changesets live in the root directory since release(ui-merge): one release lane for both scopes — @object-ui joins the fixed group, @objectstack/console builds from ui/apps/console, the objectui pin is deleted (#25711) #25745).

Why

A KPI's colour is read before its number. hotclm's average approval turnaround tile showed "↑ 23% vs previous period" in green for a turnaround that got 23% longer.

Tests

All at head aef918b95, on origin/main 4373103 (main has not moved since the branch was cut).

  • packages/spec/src/ui/dataset-measure-polarity.test.ts (17): the three values parse and keep their value; the closed vocabulary; an absent key materializes nothing; up / down / good / inverse / true refused at the key (invalid_value at ['polarity']); betterWhen and direction pointed at polarity; the boolean spellings answered with the values; the wire descriptor accepts exactly the measure's vocabulary and stays optional.
  • packages/services/service-analytics/src/__tests__/measure-column-polarity.test.ts (3), each on the live path and the draft preview: each declared value relayed as written; no key on an undeclared measure or a dimension column; the __compare column carries its measure's polarity.
  • ui/packages/plugin-dashboard/src/__tests__/DatasetWidget.polarity-26602.test.tsx (9): the metric tile for lower (rise bad, fall good, arrows kept), neutral (no tone, muted), higher, undeclared (class string byte-identical to today's), and a flat delta muted under every polarity; the cross-tab cell for lower, neutral and undeclared.
  • Reverse verification, through scripts/ablation-replace.mjs, each restored to HEAD by blob hash: deleting the relay line in enrichResultColumns turns 2 of 3 service tests red (the two positive ones; the absence test stays green); replacing deltaToneClass with the old direction-only colouring turns 4 of 9 renderer tests red (both tile lower cases, the cell lower and the cell neutral cases).
  • Full suites: @objectstack/spec Test Files 732 passed (732), Tests 21938 passed, 1 todo. @objectstack/service-analytics Test Files 192 passed (192), Tests 4650 passed, 282 skipped. @objectstack/platform-objects Test Files 96 passed (96), Tests 1361 passed. @object-ui/plugin-dashboard Test Files 190 passed (190), Tests 1864 passed, 6 skipped.
  • Typecheck: @objectstack/spec, @objectstack/service-analytics, @objectstack/platform-objects exit 0. @object-ui/plugin-dashboard type-check NOT MEASURED locally: it exits 2 with 133 errors, every one a Cannot find module '@object-ui/*' or an implicit any downstream of one, because the sibling @object-ui/* packages have no built declarations in this container. CI's Type Check UI is the reading.

Acceptance notes

  • Skills token ceiling raised once, under a standing ruling. skills/objectstack-ui/evals/analytics-inline-vs-dataset.json grows +29 tokens for the polarity in eval 8, part-funded by trimming two parentheticals, eval 2's note that restated its own expected output and eval 8's "(a rise draws red)". The file is at 5443 bytes. The balance (+13, 1348 → 1361) is raised under the maintainer ruling recorded beside that row in scripts/check-skills-token-ratchet.mjs, quoted verbatim: 「A:先削重复,余额按实测提高」. Seat: please confirm that ruling covers this eval; the alternative is dropping the eval change and declaring the skills half follow-up owed.
  • Skills readings (lines): rules/dashboards.md 464 → 462; evals/analytics-inline-vs-dataset.json 87 → 87 (5389 → 5462 bytes); all SKILL.md 4600 → 4600; whole skills/ tree 19192 → 19190.
  • On the metric tile, cn is tailwind-merge, so the tile's explicit muted class for neutral would win over a tone class even without the helper's neutral branch. The cross-tab cell has no such class, which is why its neutral pin is the one that goes red when the helper's branch is removed.
  • Not carried (noted, not filed): the inline ObjectMetricWidget / MetricWidget trend path reads no dataset measure, so it has no polarity to read; DatasetReportRenderer draws no comparison delta.

Verification

  • pnpm --filter @objectstack/spec check:generated --fix regenerated the 5 stale artifacts (api-surface, export-origins, declaration-map, references docs, liveness counts) from a rebuilt dist and re-checked them green; the other 9 were already current.
  • pnpm --filter @objectstack/spec check:empty-state: all classified (no new permissive empty-state statement).
  • pnpm check:i18n: OK, 10 packages in sync. The dataset form's measures repeater offers polarity (the spec's form/Zod reconciliation pin required it), labelled 极性 / 極性 / Polaridad; the two pinned populations move by one (object-lifecycle-panel-echo-decisions positive control 689 → 690 on today's main, report-form-echo-decisions dataset leaves 39 → 40).
  • node scripts/check-skills-token-ratchet.mjs and --self-test: OK (79 cases).
  • eslint on every changed source file: 0 errors.
  • The derived gate families (node scripts/pm/dispatch-gates.mjs --commands, 138 at this head) are running locally; their verdicts go in the os-dev report on 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, and the PR's check-runs are the CI reading.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes (eslint on the changed files; the repo-wide run is CI's).
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WGufzfjMN88NYp5BcCRyGR


Generated by Claude Code

@github-actions

github-actions Bot commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/platform-objects, @objectstack/service-analytics, @objectstack/spec, touching 8 documentable anchor(s). ⚠️ 7 changed file(s) yielded no anchor (packages/spec/api-surface/ui.json, packages/spec/authorable-surface/ui.json, packages/spec/declaration-map/ui.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via AnalyticsResult (symbol, a top-level interface))
  • content/docs/api/data-api.mdx (via AnalyticsResult (symbol, a top-level interface), AnalyticsResultResponseSchema (symbol, a top-level const))
  • content/docs/data-modeling/analytics.mdx (via DatasetMeasureSchema (symbol, a top-level const))

⛔ 5 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-3.mdx (via AnalyticsResult (symbol, a top-level interface))
  • content/docs/releases/v17/17-5.mdx (via AnalyticsService (symbol, a top-level class))
  • content/docs/releases/v17/17-6.mdx (via AnalyticsResult (symbol, a top-level interface), AnalyticsService (symbol, a top-level class))
  • content/docs/releases/v17/17-7.mdx (via AnalyticsService (symbol, a top-level class))
  • content/docs/releases/v9.mdx (via AnalyticsResult (symbol, a top-level interface))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 7 changed file(s) yielded no anchor (packages/spec/api-surface/ui.json, packages/spec/authorable-surface/ui.json, packages/spec/declaration-map/ui.json, …) — pages documenting those are invisible to this run
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 56 of 208 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 — 147 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 6866e2e7c5a268b2c2e1ec0fb900f469adf5f551 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 12f4ccbae018e4b55a2845280b25e1433e2a4827 — the merge of head 1aa6a94b184b1ebe8c831adb5f71e109f55fe683 into base 6866e2e7c5a268b2c2e1ec0fb900f469adf5f551, 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 12f4ccbae018e4b55a2845280b25e1433e2a4827 && git checkout 12f4ccbae018e4b55a2845280b25e1433e2a4827
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 6866e2e7c5a268b2c2e1ec0fb900f469adf5f551 1aa6a94b184b1ebe8c831adb5f71e109f55fe683 && git checkout -B drift-repro 6866e2e7c5a268b2c2e1ec0fb900f469adf5f551 && git merge --no-ff 1aa6a94b184b1ebe8c831adb5f71e109f55fe683

node scripts/docs-audit/affected-docs.mjs --json 6866e2e7c5a268b2c2e1ec0fb900f469adf5f551

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 6866e2e7c5a268b2c2e1ec0fb900f469adf5f551 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Resolves the positive-control count in object-lifecycle-panel-echo-decisions
(686 from #26548 plus the measures repeater's `polarity` = 687) and regenerates
authorable-surface/ui.json from the merged tree.

Claude-Session: https://claude.ai/code/session_01WGufzfjMN88NYp5BcCRyGR
Co-Authored-By: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 15765222001619d25cfd6e89646dd52a02df6587
Local-runs: none
Scope: packages/spec/src/api/analytics.zod.ts, packages/spec/src/contracts/analytics-service.ts, packages/spec/src/ui/dataset.form.ts, packages/spec/src/ui/dataset.zod.ts, skills/objectstack-ui/evals/analytics-inline-vs-dataset.json, skills/objectstack-ui/rules/dashboards.md + packages/services/service-analytics/src/analytics-service.ts (enrichResultColumns), ui/packages/plugin-dashboard/src/DatasetWidget.tsx (deltaToneClass, the tile and the cross-tab cell), ui/packages/core/src/utils/dataset-format.ts (DatasetResultField) and its spec-parity pin, packages/spec/src/api/analytics.test.ts (AnalyticsResultMatchesContract), packages/spec/src/shared/strict-object.ts (aliases / guidance), packages/spec/liveness/dataset.json, scripts/check-skills-token-ratchet.mjs (the two rows the rules hunks are priced by), packages/spec/src/ui/dashboard.zod.ts (read to confirm it is untouched)

Inputs read: card #26602 body and all four comments (triage 6440164879, claim 6442814260, claim amendment 6442830822, os-dev-report 6445682259); PR #26739 body and file list; the net diff of the head against main at the merge-base 74db43e (33 files, byte-equal to the GitHub file list, so the 344e950 merge contributes nothing of its own); the check-runs on the head. The hits set was confirmed with reviewFace on the file list (face schema, 4 schema + 2 rules paths, exactly the brief's six).

① Derived judgments

Every accept-set and public-surface change the diff implies, each judged against the tree:

  • DatasetMeasureSchema.polarity becomes an authorable key with the closed vocabulary higher | lower | neutral, optional, no default materialised — RIGHT. authorable-surface/ui.json gains ui/DatasetMeasure:polarity; json-schema.manifest/ui.json gains ui/DatasetMeasurePolarity; the spec test pins that an absent key parses to a byte-equal object (hasOwnProperty('polarity') === false), so the renderer's undeclared arm is what decides colour and today's colours hold.
  • DatasetMeasurePolarity is exported as const and type from ui/dataset.zod.ts — RIGHT. api-surface/ui.json +2, declaration-map +1, export-origins +1, and the ADR-0122 isomorphic pin 792 → 793 (Iso_ui_dataset__DatasetMeasurePolarity), consistent with its sibling DatasetDurationUnit.
  • Near-miss keys betterWhen / direction / goodDirection / trendDirection → aliases to polarity; higherIsBetter / lowerIsBetter → guidance — RIGHT and NOT a narrowing: strictObject already refused every undeclared key; these only change the refusal's prescription (read in shared/strict-object.ts: aliases is the curated near-miss answer, guidance the prescription for an undeclared key). No measure key legitimately spelled direction exists on the measure shape.
  • Wire: AnalyticsResultResponseSchema.data.fields[].polarity optional, same enum, imported from ../ui/dataset.zod — RIGHT. api/ importing ui/ has precedent (metadata.zod.ts → ui/app.zod, protocol.zod.ts → ui/view.zod); ui/dataset.zod.ts imports nothing from api/, so no cycle. The wire test at the head pins the three values accepted, up refused, and absence preserved.
  • Contract: AnalyticsResult.fields[].polarity?: DatasetMeasurePolarity — RIGHT and MIRRORED: the module-scope AnalyticsResultMatchesContract pin in api/analytics.test.ts (an Assert-of-Eq between AnalyticsResultResponse['data'] and AnalyticsResult) binds the two at compile time, and Type Check · source gates is green on the head.
  • Consumer type: @object-ui/core's DatasetResultField = AnalyticsResult['fields'][number] (a derived alias, pinned by dataset-result-field-spec-parity.test.ts), so polarity reaches the renderer with no UI-side restatement — RIGHT; Type Check UI is green on the head.
  • Producer: enrichResultColumns relays m.polarity under the same f.x == null && m.x discipline as format; the loop resolves m through measureByName.get(f.name) ?? measureByName.get(f.name.replace(/__compare$/, '')), so the __compare column is covered by construction, and both callers (live query at 2997, draft preview at 2734) share the one seam — RIGHT. The service test pins each value, absence on an undeclared measure and on a dimension column, and turnaround__compare carrying lower while row_count__compare carries no key, on both paths.
  • Renderer: deltaToneClass(direction, polarity) is the only emerald/rose source left in DatasetWidget.tsx (lines 291, 1407, 1560); no direction === 'up' && 'text-emerald…' pattern survives anywhere in ui/packages/plugin-dashboard/src — RIGHT. Traced by hand: lower + up → rose, lower + down → emerald; neutral → undefined (tile adds text-muted-foreground explicitly, the cell inherits its container's text-muted-foreground); higher and undeclared → up emerald / down rose; flat → undefined + muted. For the undeclared tile, cn('…', 'text-emerald-600 dark:text-emerald-400', false) yields the same string the old three-arm cn did, and the test pins that string byte-for-byte.
  • Form: the measures repeater offers { field: 'polarity', label: 'Polarity' }; the repeater's row properties are schema-derived widgets keyed by the item schema's .meta({ title }), which the new key carries ('Polarity') — RIGHT; check:i18n green, four generated catalogs +3 lines each, the two pinned populations move by exactly one (687, 40).
  • Liveness: measures.polarity live, evidence = deltaToneClass, producer = enrichResultColumns; state-counts 31 → 32 — RIGHT; Spec property liveness green.
  • dashboard.zod.ts compareTo: git diff against the merge-base is empty for that path; DatasetCompareToSchema is a strictObject (api/analytics.zod.ts:408) and dashboard-compareto.test.ts is present and unchanged — the claim amendment's "NOT touched" HOLDS.
  • Derived gates on the head at review time: 54 check-runs success, 5 skipped (opt-in / matrix placeholders), 0 failure; Test Core (5/8) and (6/8) still in progress. Lint & Repo Gates (which runs the skills token ratchet), Governed Surface Queue Guard, Check Changeset, Type Check UI, TypeScript Type Check and all sixteen Test UI shards are success.

② Semver level

  • Changeset .changeset/26602-measure-polarity.md: @objectstack/spec minor, @objectstack/service-analytics minor; ui/.changeset/26602-delta-polarity.md: @object-ui/plugin-dashboard minor. Both declaration lines present in the PR body and both changesets.
  • Clause-②: yes (widening) — CORRECT. One optional authorable key, one optional wire member, one optional contract member; nothing an author could write before is refused or re-meant, and the undeclared path is pinned byte-identical at the schema, the wire, and the renderer.
  • Metadata-protocol: added — CORRECT. A new key and three accepted values; not semantic, because the undeclared meaning (a rise drawn good) is the meaning every delta already had, and no default is materialised.
  • Level: added takes at least minor on a package the diff moves; the diff moves @objectstack/spec (the key) and @objectstack/service-analytics (the relay), so minor on both matches the rule as written; plugin-dashboard minor for additive behaviour is right. No BREAKING banner owed, no ADR-0087 disposition owed.

③ Boundary flags

  • Dev flag / open_questions[0] (eval ceiling 1348 → 1366 under 「A:先削重复,余额按实测提高」): ANSWERED — A, covered, and both halves of the ruling honoured. Trim-first: a genuine restatement was deleted first — eval 2's note parenthetical "(date bucketing is the classic wrong answer)" (46 bytes, ~11 tokens) restated the same eval's expected_output ("never passes off a date bucket … as a moving window"). Raise = measured remainder: the file is 5389 → 5462 bytes at the head; ceil(5389/4) = 1348 and ceil(5462/4) = 1366, so the new row sits exactly AT the measurement with zero headroom, and the ratchet's verdict (red only when tokens exceed the ceiling) is green inside Lint & Repo Gates. Coverage: the ruling is recorded on this very file's row, was tapped on a decision card about skills fix cards whose teaching and eval both hit the ceiling, and 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 is such a card (triage: skills, docs and eval owed under [Decision] pm-dispatch: which PRs owe a Skills-docs: line (the #26184 audit Part B rule draft) — protocol-kind only, protocol-kind plus narrowing, or not adopted #26369 B). One should-fix on the arithmetic's purity is in ④.
  • Dev flag (Tier H skills/**): CORRECT. This record is the contract-tier review; it does not lift Tier H. Landing still needs an approving review from a GOVERNED_APPROVERS account, and every check green (two Test Core shards were still running at review time).
  • Dev flag (@object-ui/plugin-dashboard typecheck not measured locally): CLOSED — Type Check UI and TypeScript Type Check are success on the head.
  • Dev note (inline ObjectMetricWidget / MetricWidget trend path and DatasetReportRenderer not carried): ACCEPTED — the inline trend is options.trend-fed and reads no dataset answer, so it has no polarity source; no direction-only colouring remains in plugin-dashboard. Nothing to escalate.
  • Claim amendment (key on the measure, not on compareTo; dashboard.zod.ts untouched; __compare relayed): all three HOLD in the tree (①).
  • Claim line Metadata-protocol: as measured by the dev → measured as added: AGREED.
  • Card "Expected" offered "the default could be neutral rather than green": the dev kept green for undeclared, which is what the triage grade required ("an undeclared direction keeps today's colours, ⛔ with no guessing from the measure's name"); the renderer test pins a measure literally named turnaround_days still reading as higher when undeclared. Not a flag.
  • Merge commit 344e950: judged on the net diff only; the three-dot diff and the merge-base diff are identical (33 files, +619/−39) and equal the GitHub file list.

④ Design judgment

Each hits file, read against the code it cites and the scenario it is accepted against (the hotclm approval-turnaround tile, compareTo: previousPeriod, rise drawn green):

  • packages/spec/src/ui/dataset.zod.ts — HOLDS. The key is on the semantic layer where every widget over the measure agrees; the enum names the better direction so neutral is a peer value, not a flag's third state; the doc comment states what the renderer actually does (traced above). Tried to break: an author writing betterWhen (the card's own spelling) is pointed at polarity; a boolean flag spelling is answered with all three values (test pins the message carries polarity: 'lower' and polarity: 'neutral'); up / down / true refused at ['polarity']. No finding.
  • packages/spec/src/api/analytics.zod.ts — HOLDS. Same enum object, optional, described with the exact presence rule the producer implements (measure columns of a dataset answer, __compare included, absent on dimensions and on a cube answer). No finding.
  • packages/spec/src/contracts/analytics-service.ts — HOLDS. Mirrored member, bound by the existing compile-time guard; the @object-ui/core alias inherits it. No finding.
  • packages/spec/src/ui/dataset.form.ts — HOLDS. One row-property entry, label equal to the schema's .meta({ title }) as the repeater's own comment requires; the reconciliation pin and i18n populations move by one. No finding.
  • skills/objectstack-ui/rules/dashboards.md — HOLDS. The rewritten Metric-widgets bullet states the renderer's real rule ('lower' rise red, 'neutral' uncoloured, undeclared = rise green) and keeps the options.trend / compareTo teaching; the presentation-home line now lists polarity beside format / currency, which is where the code reads it. The deleted sentences described rendering chrome, not an authoring rule, and no test or script pins their wording. The file shrinks (24944 → 24881 bytes). Should-fix, in the gate that prices it (read as one change with the rule): scripts/check-skills-token-ratchet.mjs moves the dashboards.md row 6239 → 6224 with NO row comment, while every other move in CEILINGS carries its arithmetic; and ceil(24881/4) = 6221, so 6224 leaves 3 tokens of headroom instead of being pinned AT the measurement (the dev subtracted the token delta rather than re-measuring). The gate tolerates headroom, so this is not a FAIL; fix by pinning the row at 6221 with a 6239 → 6221 (#26602) line.
  • skills/objectstack-ui/evals/analytics-inline-vs-dataset.json — HOLDS. Eval 8 now asks for a lower-is-better KPI vs the previous period and asserts polarity: 'lower', which is the one thing an authoring agent must learn from 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; the eval-2 trim is a real restatement. Should-fix, optional: the new expected_output parenthetical "(a rise draws red)" (~5 tokens) itself restates the prompt's "slower is worse" and the asserted value; under 先削重复 the raise could have been +13 rather than +18. Note, not a finding: the prompt's "vs the previous period" is not asserted (compareTo absent from must_contain), which is a grader cue only.

Must-fix: none.
Should-fix: (1) pin the dashboards.md ratchet row AT its measurement (6221) and add the move's row comment; (2) optionally drop eval 8's "(a rise draws red)" and re-pin the eval row at the new measurement.

The remainder of the diff (service, renderer, tests, generated spec artifacts, translations, docs, changesets) is the lane seat's ACCEPT at TIER_DEFAULT; it was read here only to judge the hits.

Implemented-by: claude/project-thread-hjxyd2
Reviewed-by: session_01WGufzfhobWEgUUrjz4rrbh

VERDICT: PASS

dashboards.md: 6239 -> 6221 with its history line (ceil(24881 / 4)).
analytics-inline-vs-dataset.json: eval 8's "(a rise draws red)" restated
the prompt and the asserted value; trimmed, and the ceiling is pinned at
the new measurement, 1348 -> 1361 (ceil(5443 / 4)).

Claude-Session: https://claude.ai/code/session_01WGufzfjMN88NYp5BcCRyGR
Co-Authored-By: Claude <noreply@anthropic.com>
Resolves two pinned counts by adding this branch's one to main's:
the lifecycle-panel positive control 689 -> 690 and the ADR-0122
isomorphic pins 796 -> 797. Regenerates authorable-surface/ui.json and
content/docs/references/index.mdx from the merged tree.

Claude-Session: https://claude.ai/code/session_01WGufzfjMN88NYp5BcCRyGR
Co-Authored-By: Claude <noreply@anthropic.com>
ui/.changeset is no longer read since @object-ui joined the root fixed
group (2a61f08); content unchanged.

Claude-Session: https://claude.ai/code/session_01WGufzfjMN88NYp5BcCRyGR
Co-Authored-By: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · domain:spec seat 1 (#6017) · session session_01WGufzfhobWEgUUrjz4rrbh · 2026-10-27T21:43Z

Head 06497be6e3ad9df895d6233271ef84e8a201f73e. Card #26602 (triage 6440164879), claim 6442814260 with amendment 6442830822, dev report 6445682259.

  • Contract review: owed at CONTRACT_REVIEW_TIER on the schema face (four spec paths and two skills/** paths). The review (6446021563) at 15765222 was a PASS with no must-fix. It judged right:
    • the optional DatasetMeasureSchema.polarity key (higher | lower | neutral, no default materialised), and the matching optional wire and contract members, bound at compile time by AnalyticsResultMatchesContract;
    • the enrichResultColumns relay, which covers the __compare column by construction;
    • deltaToneClass as the only emerald/rose source left in DatasetWidget.tsx, with the undeclared tile byte-identical to before;
    • the measures.polarity liveness row, the form row and the near-miss aliases / guidance;
    • the eval ceiling raise under the maintainer ruling 「A:先削重复,余额按实测提高」, trim first and the balance at the measurement.
  • Since then:
    • 4b010c02 closes both should-fixes. The dashboards.md row is pinned at its measurement, 6239 → 6221 (ceil(24881 / 4)), with its history line. Eval 8's "(a rise draws red)" restated the asserted value; it is trimmed, and the eval row is pinned at 1348 → 1361 (ceil(5443 / 4)).
    • 9d318089 merges main (no rebase). Measured against the PR's own paths, it moves only the pinned counts that rebase onto main's baselines: schemas 1556 → 1557, the positive control 689 → 690 and the isomorphic pins 796 → 797. Each is still exactly +1 for this PR.
    • 06497be6 is a seat must-fix the review missed. ui/.changeset/26602-delta-polarity.md sat in a directory changesets no longer reads (gone since release(ui-merge): one release lane for both scopes — @object-ui joins the fixed group, @objectstack/console builds from ui/apps/console, the objectui pin is deleted (#25711) #25745, when @object-ui joined the root fixed group), so the @object-ui/plugin-dashboard entry would have been lost from the release notes. It is now a pure rename into the root .changeset/, with the content byte-unchanged.
  • Body: the seat corrected four stale numbers after 4b010c02 and the changeset path after 06497be6.
  • Shape: Fixes #26602, 33 files, +621/−38. Three root changesets: @objectstack/spec, @objectstack/service-analytics and @object-ui/plugin-dashboard, each minor. Clause-②: yes (widening) and Metadata-protocol: added.
  • Gates: check-runs on this head are 59 success, 7 skipped and 0 red. mergeable_state is clean.
  • Landing: Tier H. check-governed-merges --pr 26739 reports GOVERNED (2 of 33 paths, both skills/objectstack-ui/**). This seat arms nothing. The landing needs the maintainer's merge, or an APPROVED review from a GOVERNED_APPROVERS account after which this seat lands it.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 06497be6e3ad9df895d6233271ef84e8a201f73e
Local-runs: none
Scope: packages/spec/src/api/analytics.zod.ts, packages/spec/src/contracts/analytics-service.ts, packages/spec/src/ui/dataset.form.ts, packages/spec/src/ui/dataset.zod.ts, skills/objectstack-ui/evals/analytics-inline-vs-dataset.json, skills/objectstack-ui/rules/dashboards.md + packages/services/service-analytics/src/analytics-service.ts (enrichResultColumns, the measure resolution at 3078 and the relay at 3117, callers 2734 and 2997), ui/packages/plugin-dashboard/src/DatasetWidget.tsx (deltaToneClass at 273-291, the tile at 1350/1406, the cross-tab cell at 1560), ui/packages/plugin-dashboard/src/MetricWidget.tsx (489-490, the inline trend the dev did not carry), ui/packages/core/src/utils/dataset-format.ts (DatasetResultField, line 53), packages/spec/src/api/analytics.test.ts (AnalyticsResultMatchesContract, line 48), packages/spec/src/shared/strict-object.ts (aliases / guidance), packages/spec/liveness/dataset.json, scripts/check-skills-token-ratchet.mjs (the two rows the skills hunks are priced by, and the ceil(utf8 bytes / 4) convention), packages/spec/src/ui/dashboard.zod.ts and dashboard-compareto.test.ts (read to confirm untouched), .changeset/config.json and ui/AGENTS.md:181 (the changeset lane), .changeset/26602-measure-polarity.md and .changeset/26602-delta-polarity.md, packages/platform-objects/package.json and src/apps/index.ts (what the four regenerated catalogs publish), scripts/check-changeset-no-major.mjs (publishedSourceOwners, the level axis), AGENTS.md Post-Task Checklist step 3, the three precedent changesets 25778-dataset-duration-measure.md, 26619-action-disabled-reason.md, 26548-report-date-range-export.md

Inputs read: card #26602 body and all four comments (triage 6440164879, claim 6442814260, claim amendment 6442830822, os-dev-report 6445682259); PR #26739 body, file list (33 paths, equal to git diff --name-only origin/main...pr-26739) and all three PR comments (docs-drift 6443966603, the earlier record 6446021563, the seat's ACCEPT 6454346139); the net diff of 06497be6e3ad9df895d6233271ef84e8a201f73e against main at the merge-base 6090bb7 (+621/-38), its line-level difference from the net diff at the earlier head 1576522, and the PR's own two commits since (4b010c0, 06497be); the merge 9d31808 recomputed with git merge-tree against its two parents (an object-store read, no checkout) and diffed against the committed merge; the 66 check-runs on the head. Files read with git show pr-26739:PATH. The earlier PASS and the seat's ACCEPT were treated as claims and re-derived from the tree.

① Derived judgments

Every accept-set and public-surface change the diff implies, judged against the tree at 06497be6:

  • DatasetMeasureSchema.polarity becomes an authorable key with the closed vocabulary higher | lower | neutral, optional, no default materialised — RIGHT. authorable-surface/ui.json gains ui/DatasetMeasure:polarity, json-schema.manifest/ui.json gains ui/DatasetMeasurePolarity, and dataset-measure-polarity.test.ts pins that an absent key parses to a byte-equal object with no own polarity property, so the renderer's undeclared arm decides the colour and today's colours hold.
  • DatasetMeasurePolarity exported as const and type from ui/dataset.zod.ts — RIGHT. api-surface/ui.json +2, declaration-map +1, export-origins +1, references docs regenerated (dataset.mdx gains the enum section, index.mdx 1556 → 1557 schemas, UI 171 → 172), and the ADR-0122 pin Iso_ui_dataset__DatasetMeasurePolarity, 796 → 797 at this head.
  • Near-miss keys betterWhen / direction / goodDirection / trendDirection → aliases to polarity; higherIsBetter / lowerIsBetter → guidance — RIGHT and NOT a narrowing: strictObject already refused every undeclared key; aliases is the curated rename answer and guidance the appended prescription (strict-object.ts 151, 158). Only the refusal's text changes.
  • Wire: AnalyticsResultResponseSchema.data.fields[].polarity, optional, the same enum object, imported ../ui/dataset.zod — RIGHT. ui/dataset.zod.ts imports from shared/, kernel/, data/ and ui/, nothing from api/, so the new api → ui edge closes no cycle. The spec test pins the three values accepted, up refused, absence preserved.
  • Contract: AnalyticsResult.fields[].polarity?: DatasetMeasurePolarity — RIGHT and MIRRORED: AnalyticsResultMatchesContract (analytics.test.ts:48, an Assert-of-Eq between AnalyticsResultResponse['data'] and AnalyticsResult) binds the two at compile time; Type Check · source gates is success on the head.
  • Consumer type: @object-ui/core's DatasetResultField = AnalyticsResult['fields'][number] (dataset-format.ts:53), so polarity reaches the renderer with no UI-side restatement — RIGHT; Type Check UI success on the head.
  • Producer: enrichResultColumns resolves m as measureByName.get(f.name) ?? measureByName.get(f.name.replace(/__compare$/, '')) (3078) and relays if (f.polarity == null && m.polarity) f.polarity = m.polarity (3117) under the same discipline as format; both callers (draft preview 2734, live query 2997) share the seam, so the __compare column is covered by construction — RIGHT. measure-column-polarity.test.ts pins each value, absence on an undeclared measure and on a dimension column, and turnaround__compare carrying lower beside row_count__compare carrying no key, on both paths.
  • Renderer: deltaToneClass(direction, polarity) (273-291) is the only emerald/rose source in DatasetWidget.tsx; the tile (1406) reads f = measureField(tileMeasure) (1350) and the cross-tab cell (1560) reads measureField(measure), both the MEASURE's own descriptor, never the __compare column's — RIGHT. Traced: lower + up → rose, lower + down → emerald, neutral → no tone (tile adds text-muted-foreground explicitly; the cell inherits its container's), higher and undeclared → up emerald / down rose, flat → no tone. The undeclared tile's class string is pinned byte-for-byte. MetricWidget.tsx 489-490 keeps direction-only colouring on the inline options.trend path, which reads no dataset answer: that is the dev's declared non-carry (③), not a leak.
  • Form: the measures repeater offers { field: 'polarity', label: 'Polarity' }, the label equal to the schema's .meta({ title: 'Polarity' }) — RIGHT. Four generated catalogs +3 lines each (measures.polarity: Polarity / 极性 / 極性 / Polaridad); the pinned populations move by exactly one at this head (positive control 689 → 690, dataset leaves 39 → 40).
  • Liveness: measures.polarity live, evidence deltaToneClass, producer enrichResultColumns, state-counts 31 → 32 — RIGHT; Spec property liveness success.
  • dashboard.zod.ts and dashboard-compareto.test.ts: git diff --stat 6090bb73a9 pr-26739 on both paths is empty — the claim amendment's "NOT touched" HOLDS. The key lives on the measure only.
  • Merge 9d31808 (main 6090bb7 into 4b010c0): recomputed. Main had touched nine of the PR's 33 paths (both translations tests, four generated catalogs, authorable-surface/ui.json, data-modeling/analytics.mdx, references/index.mdx, type-alias-convention.pin.test.ts); six auto-merged clean and three conflicted. Each conflict was resolved to main's baseline plus exactly this PR's one: index.mdx 1556 → 1557 (UI 171 → 172, the 202 page count main raised kept), the positive control 689 → 690 with main's 688 (ADR-0142) and 689 ([Decision] A greyed-out record action can only say 对此记录不可用 — re-ask for an authored disabled reason now that a second app needs it (re-file per #24034 B′) #26619) history lines kept, the isomorphic pins 796 → 797 with main's ADR-0142 +4 comment kept. The diff between the recomputed auto-merge tree and the committed merge touches only those three files and only the conflict regions — no evil merge. Nothing else from main collides with the PR's paths.
  • Derived gates on the head: 66 check-runs, 59 success, 7 skipped (opt-in / matrix placeholders), 0 failure, 0 in progress. Lint & Repo Gates (runs the skills token ratchet), both Check Changeset runs, Governed Surface Queue Guard, Type Check UI, TypeScript Type Check, Spec property liveness, Test Core 8/8 and Test UI 16/16 are success. mergeable_state clean.

② Semver level

  • Changesets at the head, both in the root .changeset/: 26602-measure-polarity.md grades @objectstack/spec minor and @objectstack/service-analytics minor; 26602-delta-polarity.md grades @object-ui/plugin-dashboard minor. Both carry Clause-②: yes (widening) and Metadata-protocol: added, as does the PR body.
  • The move in 06497be6 is RIGHT and was owed: it is a pure rename (blob 578c9d6119 at both heads, byte-identical), the ui/.changeset tree at the head equals main's (93a19b77), the root config.json has ONE fixed group of 110 packages that includes @object-ui/plugin-dashboard, and ui/AGENTS.md:181 states the root config does not read ui/.changeset/ (left-over pre-merge entries only, nothing new goes there). At the earlier head the plugin-dashboard entry sat where no release reads it; the earlier PASS accepted that location, wrongly. The seat's correction holds.
  • Clause-②: yes (widening) — CORRECT. One optional authorable key, one optional wire member, one optional contract member; nothing an author could write before is refused or re-meant, and the undeclared path is pinned byte-identical at the schema, the wire and the renderer.
  • Metadata-protocol: added — CORRECT. A new key with three accepted values; not semantic, because no default is materialised and the undeclared meaning is the meaning every delta already had.
  • Levels on the three named packages: added takes at least minor on a package the diff moves (AGENTS.md step 3), so @objectstack/spec minor and @objectstack/service-analytics minor are right; @object-ui/plugin-dashboard minor for additive renderer behaviour is right. No BREAKING banner, no ADR-0087 disposition owed.
  • WRONG — the changeset does not carry every package the diff publishes. @objectstack/platform-objects is a published package (no private, files: [dist, …], subpath ./apps), and the diff moves its published source: src/apps/translations/{en,zh-CN,ja-JP,es-ES}.metadata-forms.generated.ts, exported through src/apps/index.ts (export * from './translations/index.js') into the ./apps entry. The gate's own predicate (publishedSourceOwners: src/**, test files excepted) counts those four files as published source of packages/platform-objects; the docs-drift bot likewise lists it as one of the three packages the PR changes. No changeset at the head names it. AGENTS.md step 3: "Add a changeset for anything that publishes", and "a kind takes at least minor on a package the diff moves". The precedent of the identical shape (a measure key, a form row, four regenerated catalogs) named it: Analytics datasets: no measure for the difference between two datetime columns (cycle time, per-stage duration, SLA overrun) — #16737 refused the wrong path, nothing provides the right one #25778 '@objectstack/platform-objects': minor (25778-dataset-duration-measure.md), [Decision] A greyed-out record action can only say 对此记录不可用 — re-ask for an authored disabled reason now that a second app needs it (re-file per #24034 B′) #26619 minor, spec(report): a report declares a dataset-dimension date range and export options #26548 patch. Check Changeset is green only because its level axis is PR-scoped (at least one moved package graded minor or above); it does not enforce completeness. The fixed group bumps the version regardless, but the package's CHANGELOG.md — "the text an upgrading agent greps" (AGENTS.md 708) — would carry no entry for the catalog change, the same class of loss the seat itself called a must-fix for the plugin-dashboard entry one commit earlier. MUST-FIX: add '@objectstack/platform-objects': minor to the frontmatter of .changeset/26602-measure-polarity.md (one line; a sentence that the dataset form's measures repeater offers polarity, labelled in four locales, is welcome but not owed).
  • Clause-②: yes (widening) stands; the must-fix changes the package list, not the declaration.

③ Boundary flags

  • Dev flag / open_questions[0] (the eval ceiling raise under 「A:先削重复,余额按实测提高」): ANSWERED — A, covered, and re-judged on the new head. 4b010c023c re-pins the row 1348 → 1361 (the earlier head's 1366 is gone). Trim first: eval 2's "(date bucketing is the classic wrong answer)" and eval 8's "(a rise draws red)", both restatements, are deleted. Measured balance: the file is 5389 → 5443 bytes, ceil(5443 / 4) = 1361, so the ceiling sits AT the measurement with zero headroom, and the net raise is +13 after the trims, which is what the ruling's second half allows and no more. The eval parses (8 cases). Coverage holds as the row records it: the ruling was tapped for skills fix cards whose teaching and eval hit the ceiling, and 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 owes skills, docs and eval (triage). Lint & Repo Gates is success on the head.
  • 4b010c023c, the dashboards.md row: 6239 → 6221 with a history line, ceil(24881 / 4) = 6221, pinned AT the measurement. Both of the earlier record's should-fixes are closed in the tree, not by assertion.
  • Dev flag (Tier H, skills/**): CORRECT and unchanged. This record is the contract-tier review; it lifts nothing. Landing needs the maintainer's merge or an APPROVED review from a GOVERNED_APPROVERS account, after the must-fix lands and every check is green on that head.
  • Dev flag (@object-ui/plugin-dashboard typecheck not measured locally): CLOSED — Type Check UI (21:29Z) and TypeScript Type Check are success on 06497be6.
  • Dev note (inline ObjectMetricWidget / MetricWidget trend and DatasetReportRenderer not carried): ACCEPTED — MetricWidget.tsx 489-490 colours shownTrend.direction, which is options.trend-fed and reads no dataset answer, so it has no polarity source. Nothing to escalate.
  • Claim amendment (key on the measure, dashboard.zod.ts untouched, __compare relayed): all three HOLD (①).
  • Claim line Metadata-protocol: as measured by the dev → added: AGREED.
  • Skills-docs: line ([Decision] pm-dispatch: which PRs owe a Skills-docs: line (the #26184 audit Part B rule draft) — protocol-kind only, protocol-kind plus narrowing, or not adopted #26369 B as the triage states it; shape per .claude/skills/pm-dispatch/SKILL.md:313): PRESENT and TRUE — updated here — skills/objectstack-ui/rules/dashboards.md, skills/objectstack-ui/evals/analytics-inline-vs-dataset.json, content/docs/ui/dashboards.mdx; the rule bullet teaches the key, eval 8 asserts polarity: 'lower', the docs page gains the presentation-home row and the compareTo sentence. PR-body guards success.
  • Card "Expected" offered a neutral default: the dev kept today's colours for undeclared, which the triage grade required ("no guessing from the measure's name"); the renderer test pins a measure literally named turnaround_days reading as higher when undeclared. Not a flag.
  • PR body staleness: "All at head aef918b95" and "main has not moved since the branch was cut" predate two merges; the body's numbers for the ratchet (6239 → 6221, +13, 5443 bytes) and the changeset path were corrected and match the tree. Body claims are not the gate; the check-runs are. Not a flag.
  • Docs-drift advisory named content/docs/api/client-sdk.mdx (via AnalyticsResult): read at the head, it does not enumerate the fields[] optional members, so no polarity row is owed there. Not a flag.
  • The earlier PASS (6446021563) is superseded by this record on this head: it missed the changeset lane and, like the seat's ACCEPT, the missing @objectstack/platform-objects entry.

④ Design judgment

Each hits file, read against the code it cites and the scenario it is accepted against (the hotclm approval-turnaround tile, compareTo: previousPeriod, a rise drawn green):

  • packages/spec/src/ui/dataset.zod.ts — HOLDS. The key is on the semantic layer where every widget over the measure agrees; the enum names the better direction so neutral is a peer value; the doc comment states what the renderer does (traced in ①). Tried to break: betterWhen (the card's spelling) is pointed at polarity; a boolean spelling is answered with all three values; up / down / good / inverse / true are refused at ['polarity'] with invalid_value. No finding.
  • packages/spec/src/api/analytics.zod.ts — HOLDS. Same enum object, optional, described with the exact presence rule the producer implements (measure columns of a dataset answer, __compare included, absent on dimensions and on a cube answer). No finding.
  • packages/spec/src/contracts/analytics-service.ts — HOLDS. Mirrored member, bound by the existing compile-time guard; the @object-ui/core alias inherits it. No finding.
  • packages/spec/src/ui/dataset.form.ts — HOLDS. One row-property entry, label equal to the schema's .meta({ title }); the reconciliation pin and the i18n populations move by one. The catalog rows this entry generates are the published source the ② must-fix is about; the form entry itself is right.
  • skills/objectstack-ui/rules/dashboards.md — HOLDS. The Metric-widgets bullet states the renderer's real rule ('lower' rise red, 'neutral' uncoloured, undeclared = rise green) and keeps the options.trend / compareTo teaching; the presentation-home line lists polarity beside format / currency, where the code reads it. The deleted sentences described chrome, not an authoring rule. 463 → 461 lines, 24944 → 24881 bytes; the ratchet row now follows the file down to 6221 with its history line. No finding.
  • skills/objectstack-ui/evals/analytics-inline-vs-dataset.json — HOLDS. Eval 8 asks for a lower-is-better KPI vs the previous period and asserts polarity: 'lower', the one thing an authoring agent must learn from 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; both trims are real restatements; the row is at the measurement. Note, not a finding: compareTo is in expected_output but not in must_contain, a grader cue only.

Must-fix: (1) .changeset/26602-measure-polarity.md frontmatter gains '@objectstack/platform-objects': minor — the diff moves that package's published source (four src/apps/translations/*.metadata-forms.generated.ts) and no changeset at the head names it (②).
Should-fix: none.

The remainder of the diff (service, renderer, tests, generated spec artifacts, translations, docs) is the lane seat's ACCEPT at TIER_DEFAULT; it was read here only to judge the hits and the changeset.

Implemented-by: claude/project-thread-hjxyd2
Reviewed-by: session_01AavokzJ5DndAwitDXvKy4U

VERDICT: FAIL


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · domain:spec seat 1 (#6017) · session session_01WGufzfhobWEgUUrjz4rrbh · 2026-10-28T01:49Z

Head 6621b0746ba5ee958e0ce2443f57b0d8fd85ba70. This replaces the seat's ACCEPT 6454346139 at 06497be6. Card #26602, claim 6442814260 with amendment 6442830822, dev report 6445682259.

  • Contract review:
    • A second review (6457686115) at 06497be6 was a FAIL with one must-fix. The four regenerated src/apps/translations/*.metadata-forms.generated.ts catalogs are published source of @objectstack/platform-objects (./apps), and no changeset named that package. The seat's earlier ACCEPT missed this too.
    • c0cd1852 adds '@objectstack/platform-objects': minor and one sentence to .changeset/26602-measure-polarity.md. The re-review (6457977233) at that head is a PASS with no must-fix and no should-fix.
  • Since then: 6621b074 merges main (no rebase). The one conflict, in scripts/check-skills-token-ratchet.mjs, is resolved as the re-review's ③ expected: this PR's eval row stays at 1361 with its history line, and main's new report-period-export.json row is kept beside it. Measured against the PR's own paths, this PR's net hunks are line-for-line identical at c0cd1852 and at 6621b074.
  • Body: the seat patched the Changesets line to name the third package.
  • Shape: Fixes #26602, 33 files, +624/−38. The changesets are @objectstack/spec, @objectstack/service-analytics, @objectstack/platform-objects and @object-ui/plugin-dashboard, each minor, all in the root .changeset/. Clause-②: yes (widening) and Metadata-protocol: added.
  • Gates: check-runs on this head are 57 success, 6 skipped and 0 red. mergeable_state is clean.
  • Landing: Tier H (2 skills/objectstack-ui/** paths). This seat arms nothing. The landing needs the maintainer's merge, or an APPROVED review from a GOVERNED_APPROVERS account after which this seat lands it.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 6621b0746ba5ee958e0ce2443f57b0d8fd85ba70
Local-runs: none
Scope: packages/spec/src/api/analytics.zod.ts, packages/spec/src/contracts/analytics-service.ts, packages/spec/src/ui/dataset.form.ts, packages/spec/src/ui/dataset.zod.ts, skills/objectstack-ui/evals/analytics-inline-vs-dataset.json, skills/objectstack-ui/rules/dashboards.md + scripts/check-skills-token-ratchet.mjs (the merge's one conflict: the #26602 eval row beside main's #26633 row, and the dashboards.md row), content/docs/data-modeling/analytics.mdx and content/docs/api/data-api.mdx (the two PR paths the merge also moved, attributed), .changeset/26602-measure-polarity.md and .changeset/26602-delta-polarity.md, .changeset/config.json (the one fixed group), packages/platform-objects/package.json and src/apps/index.ts (what the four regenerated catalogs publish), packages/services/service-analytics/src/analytics-service.ts (enrichResultColumns, 3078 / 3117, callers 2734 / 2997), ui/packages/plugin-dashboard/src/DatasetWidget.tsx (deltaToneClass 273-291, tile 1350 / 1406, cell 1560), ui/packages/plugin-dashboard/src/MetricWidget.tsx (489-490), ui/packages/core/src/utils/dataset-format.ts (DatasetResultField, 53), packages/spec/src/api/analytics.test.ts (AnalyticsResultMatchesContract, 48), packages/spec/src/shared/strict-object.ts (aliases / guidance), packages/spec/liveness/dataset.json, packages/spec/src/ui/dashboard.zod.ts and dashboard-compareto.test.ts (untouched), scripts/check-changeset-no-major.mjs (publishedSourceOwners, the level axis), AGENTS.md Post-Task Checklist step 3

Inputs read: card #26602 body and all four comments; PR #26739 body (the Changesets bullet names all four packages), its file list (33 paths, equal to git diff --name-only d7df63957d prr-26739) and its three comments; the net diff of 6621b074 against main at the new merge-base d7df639 (+624/-38); the merge itself (parents c0cd1852, the PASSed head, and d7df63957d, an ancestor of origin/main), recomputed with git merge-tree against its two parents (an object-store read, no checkout) and diffed against the committed merge; the diff c0cd1852..6621b074 restricted to the PR's paths and attributed path by path to main or to the PR; the 63 check-runs and 10 workflow runs on the head via gh api GET at 01:55Z. Files read with git show prr-26739:PATH. The PASS on c0cd1852 (record2.md, this session) was re-derived, not carried.

① Derived judgments

The delta from the PASSed head is exactly one first-parent commit, the merge 6621b0746b (01:18Z). Judged in its own right first, then every claim re-read on the merged tree:

  • The merge's one conflict, scripts/check-skills-token-ratchet.mjs, is resolved EXACTLY as record2 ③ wrote the expectation: the recomputed auto-merge of the two parents conflicts in that one file only (stages 1/2/3: base 74134bf99b, ours 49508a7400, theirs 053eb781e4); the committed merge differs from the recomputed tree by 4 deleted lines and nothing else — the three conflict markers and main's stale ['skills/objectstack-ui/evals/analytics-inline-vs-dataset.json', 1348] row. Kept: this PR's row at 1361 with its four-line history comment (770), and main's new ['skills/objectstack-ui/evals/report-period-export.json', 457] row with its skills(objectstack-ui): no eval teaches a report's dateRange / exportOptions, because both ui eval files sit at their token ceiling (follow-up owed by #26548) #26633 comment (781). No evil-merge content: no line in the merge commit is absent from both parents. The dashboards.md row (824) is 6221, untouched by main.
  • The two measurements the rows price still hold on the merged tree: analytics-inline-vs-dataset.json is 5443 bytes → ceil(5443 / 4) = 1361; dashboards.md is 24881 bytes → ceil(24881 / 4) = 6221; both AT the measurement, zero headroom. main did not touch either skills file between the two merge-bases.
  • c0cd1852..6621b074 on the PR's own paths is three files, each MAIN's and none the PR's: scripts/check-skills-token-ratchet.mjs +133/-10 is main's skills(objectstack-ui): no eval teaches a report's dateRange / exportOptions, because both ui eval files sit at their token ceiling (follow-up owed by #26548) #26633 row and its other raised rows (automation / platform SKILL.md, evals/README.md, actions.md 2151 → 2259 for [Decision] A greyed-out record action can only say 对此记录不可用 — re-ask for an authored disabled reason now that a second app needs it (re-file per #24034 B′) #26619, and four other eval rows), auto-merged clean except for the one conflict above, whose resolution keeps exactly the PR's hunk; content/docs/data-modeling/analytics.mdx +7 is main's spec(lint): the view publish door accepts a chart list view whose dataset does not exist or whose values names no measure of its dataset #26847 paragraph on list-view chart bindings resolving (chart-dataset-unknown and siblings), auto-merged, and the PR's polarity sentence survives at 263 with the head-vs-main diff being exactly that sentence; content/docs/api/data-api.mdx +2/-1 is main's runtime: the dispatcher's GET /data/:object/:id silently drops an unknown query parameter (200), where RestServer refuses it 400 #26898 sentence that an unknown query parameter on a by-id read is refused 400 VALIDATION_ERROR, auto-merged, and the head-vs-main diff is exactly the PR's polarity clause at 477. The committed merge equals the recomputed auto-merge on both docs files byte for byte.
  • The merge changes none of the PR's accept-set, public-surface or semver claims: the net diff against the new merge-base lists the same 33 files, +624/-38, as against the old one; the four schema paths, the two skills paths, the service, the renderer, the tests, the generated spec baselines, the liveness row and both changesets are byte-identical to c0cd1852. The 52 .changeset/*.md files that differ between the two heads are main's own changesets carried in by the merge; none is named 26602-*.
  • DatasetMeasureSchema.polarity: optional, closed higher | lower | neutral, no default materialised — RIGHT; authorable-surface/ui.json and json-schema.manifest/ui.json +1 each; the spec test pins absence as a byte-equal parse.
  • DatasetMeasurePolarity exported as const and type — RIGHT; api-surface +2, declaration-map +1, export-origins +1, references docs regenerated (1557 schemas, UI 172), ADR-0122 pin 797.
  • aliases (betterWhen / direction / goodDirection / trendDirection → polarity) and guidance (higherIsBetter / lowerIsBetter) — RIGHT, not a narrowing: strictObject already refused every undeclared key.
  • Wire fields[].polarity, same enum object, optional — RIGHT; ui/dataset.zod.ts imports nothing from api/, no cycle.
  • Contract AnalyticsResult.fields[].polarity? — RIGHT and MIRRORED by AnalyticsResultMatchesContract (analytics.test.ts:48); TypeScript Type Check success on this head.
  • Consumer alias DatasetResultField = AnalyticsResult['fields'][number] (dataset-format.ts:53) — RIGHT; Type Check UI success on this head.
  • Producer enrichResultColumns: measure resolved with the __compare suffix stripped (3078), relay at 3117, one seam for live (2997) and preview (2734) — RIGHT; the service test pins each value, both absences and the __compare relay on both paths.
  • Renderer deltaToneClass the only emerald/rose source in DatasetWidget.tsx; tile and cross-tab cell read the MEASURE's descriptor — RIGHT; undeclared tile pinned byte-for-byte; MetricWidget.tsx 489-490 is the options.trend path the dev declared not carried.
  • Form row equal to the schema's .meta({ title: 'Polarity' }); four catalogs +3 lines; populations 690 and 40 — RIGHT.
  • Liveness measures.polarity live, 31 → 32 — RIGHT; Spec property liveness success on this head.
  • dashboard.zod.ts and dashboard-compareto.test.ts untouched against the merge-base — the claim amendment HOLDS.
  • Derived gates on THIS head, at 01:55Z: 63 check-runs, 57 success, 6 skipped (opt-in / matrix placeholders), 0 failure, 0 pending; all carry head_sha 6621b07. Ten workflow runs: CI, Lint & Type Check, PR Automation, Docs Drift Check, Spec Liveness Check, Check Links, PR-Body Guards, Governed Surface Guard success; Pack Smoke and Upgrade Harness skipped (opt-in). Named: Check Changeset (01:20Z), Governed Surface Queue Guard, PR-body guards, Spec property liveness, Lint & Repo Gates (runs the skills token ratchet, 01:25Z), Build Docs, Build & E2E UI, TypeScript Type Check, Type Check UI, Test Core 8/8, Test UI 16/16 — all success. mergeable_state clean.

② Semver level

  • Changesets at the head, both in the root .changeset/ (the lane the one fixed group of 110 packages reads): 26602-measure-polarity.md grades @objectstack/spec minor, @objectstack/service-analytics minor, @objectstack/platform-objects minor; 26602-delta-polarity.md grades @object-ui/plugin-dashboard minor. Both byte-identical to c0cd1852; both carry Clause-②: yes (widening) and Metadata-protocol: added, as does the PR body.
  • Every package whose published source the net diff moves is graded: packages/spec/src/** (spec), packages/services/service-analytics/src/analytics-service.ts (service-analytics), packages/platform-objects/src/apps/translations/*.generated.ts (platform-objects, exported through src/apps/index.ts into ./apps), ui/packages/plugin-dashboard/src/DatasetWidget.tsx (plugin-dashboard). The merge added no published-source path to the net diff (same 33 files). The remaining paths are tests, generated spec baselines, docs, skills and the ratchet script. The changeset matches what the diff publishes.
  • Clause-②: yes (widening) — CORRECT. One optional authorable key, one optional wire member, one optional contract member; the undeclared path is pinned byte-identical at the schema, the wire and the renderer.
  • Metadata-protocol: added — CORRECT. A new key with three accepted values, no default, the undeclared meaning unchanged.
  • Levels: added takes at least minor on a package the diff moves, so minor on spec, service-analytics and platform-objects is the rule as written; plugin-dashboard minor for additive renderer behaviour is right. No BREAKING banner, no ADR-0087 disposition owed. Check Changeset is success on this head.
  • Clause-② line: Clause-②: yes (widening) — stands.

③ Boundary flags

  • The merge-conflict expectation record2 ③ set (keep the PR's 1361 row, keep main's 457 row, change neither of the PR's two rows): MET exactly, verified against the recomputed auto-merge (①). mergeable_state is now clean.
  • Dev flag / open_questions[0] (the eval ceiling raise under 「A:先削重复,余额按实测提高」): ANSWERED — A, covered, unchanged: both restating parentheticals trimmed first, the balance +13 pinned AT the measurement (1361 = ceil(5443 / 4)); the dashboards.md row 6221 = ceil(24881 / 4). Lint & Repo Gates success on this head.
  • Gates on this head: all 63 check-runs concluded, none pending, none red (①). Nothing is assumed; the conclusions above are read off the head's check-runs at 01:55Z.
  • Dev flag (Tier H, skills/**): CORRECT and unchanged; this record lifts nothing. Landing needs the maintainer's merge or an APPROVED review from a GOVERNED_APPROVERS account; every check is green on this head and the PR is clean.
  • Dev flag (@object-ui/plugin-dashboard typecheck not measured locally): CLOSED — Type Check UI (01:38Z) and TypeScript Type Check (01:37Z) success on 6621b074.
  • Dev note (inline ObjectMetricWidget / MetricWidget trend and DatasetReportRenderer not carried): ACCEPTED — MetricWidget.tsx 489-490 colours options.trend, which reads no dataset answer.
  • Claim amendment (key on the measure, dashboard.zod.ts untouched, __compare relayed): all three HOLD.
  • Claim line Metadata-protocol: as measured by the dev → added: AGREED.
  • Skills-docs: line ([Decision] pm-dispatch: which PRs owe a Skills-docs: line (the #26184 audit Part B rule draft) — protocol-kind only, protocol-kind plus narrowing, or not adopted #26369 B as the triage states it; shape per .claude/skills/pm-dispatch/SKILL.md:313): PRESENT and TRUE — updated here — dashboards.md, analytics-inline-vs-dataset.json, content/docs/ui/dashboards.mdx. PR-body guards success.
  • Card "Expected" offered a neutral default: the dev kept today's colours for undeclared, which the triage grade required; the renderer test pins turnaround_days reading as higher when undeclared. Not a flag.
  • PR body: the Changesets bullet names all four packages; the stale "All at head aef918b95" test line is a body claim, not a gate. Not a flag.
  • Docs: main's spec(lint): the view publish door accepts a chart list view whose dataset does not exist or whose values names no measure of its dataset #26847 paragraph now sits in analytics.mdx a section above the PR's polarity sentence and main's runtime: the dispatcher's GET /data/:object/:id silently drops an unknown query parameter (200), where RestServer refuses it 400 #26898 sentence in data-api.mdx is unrelated to fields[]; neither contradicts the PR's text. Not a flag.
  • Earlier records on this PR (PASS 6446021563 on 15765222, this session's FAIL on 06497be6 and PASS on c0cd1852) are superseded on this head.

④ Design judgment

Each hits file, byte-identical to the previous head, re-read against the code it cites and the scenario it is accepted against (the hotclm approval-turnaround tile, compareTo: previousPeriod, a rise drawn green):

  • packages/spec/src/ui/dataset.zod.ts — HOLDS. The key sits on the semantic layer; the enum names the better direction so neutral is a peer value; betterWhen is pointed at polarity, boolean spellings are answered with the three values, up / down / good / inverse / true are refused at ['polarity']. No finding.
  • packages/spec/src/api/analytics.zod.ts — HOLDS. Same enum object, optional, the description states the exact presence rule the producer implements. No finding.
  • packages/spec/src/contracts/analytics-service.ts — HOLDS. Mirrored member, bound by the existing compile-time guard. No finding.
  • packages/spec/src/ui/dataset.form.ts — HOLDS. One row-property entry, label equal to the schema's .meta({ title }); its catalog rows are carried by the platform-objects changeset entry. No finding.
  • skills/objectstack-ui/rules/dashboards.md — HOLDS. The Metric-widgets bullet states the renderer's real rule and keeps the options.trend / compareTo teaching; the presentation-home line lists polarity where the code reads it; the ratchet row follows the file down to 6221 with its history line, and the merge kept it. No finding.
  • skills/objectstack-ui/evals/analytics-inline-vs-dataset.json — HOLDS. Eval 8 asserts polarity: 'lower'; both trims are real restatements; the row is at the measurement and sits correctly beside main's new skills(objectstack-ui): no eval teaches a report's dateRange / exportOptions, because both ui eval files sit at their token ceiling (follow-up owed by #26548) #26633 row after the merge. Note, not a finding: compareTo is in expected_output but not in must_contain, a grader cue only.

Must-fix: none.
Should-fix: none.

The remainder of the diff (service, renderer, tests, generated spec artifacts, translations, docs) is the lane seat's ACCEPT at TIER_DEFAULT; it was read here only to judge the hits, the changesets and the merge.

Implemented-by: claude/project-thread-hjxyd2
Reviewed-by: session_01AavokzJ5DndAwitDXvKy4U

VERDICT: PASS


Generated by Claude Code

Resolves the ADR-0122 isomorphic pin count: main's HookRefusalSchema
(796 -> 797) plus this branch's DatasetMeasurePolarity = 798. Regenerates
content/docs/references/index.mdx from the merged tree.

Claude-Session: https://claude.ai/code/session_01WGufzfjMN88NYp5BcCRyGR
Co-Authored-By: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT (armed) · domain:spec seat 1 (#6017) · session session_01WGufzfhobWEgUUrjz4rrbh · 2026-10-28T09:38Z

Head 1aa6a94b184b1ebe8c831adb5f71e109f55fe683. This carries the seat's ACCEPT at 6621b074 to this head and lands the PR.

  • Since then: 1aa6a94b merges main at 0f1045bc (no rebase), because the PR had gone dirty. Measured against the PR's own paths, the only hunks that move are the pinned counts that rebase onto main's new baselines: schemas 1558 → 1559, and isomorphic pins 797 → 798 (main's i18n: an app hook's business refusal has no translation key — a zh-CN user reads "A current clean version is required before signing." in English #26170 HookRefusalSchema took 796 → 797). Each is still exactly +1 for this PR.
  • Gates: check-runs on this head are 58 success, 7 skipped and 0 red. mergeable_state is clean.
  • Tier H: lifted by os-zhuang's APPROVED review 5669466996. os-zhuang is a GOVERNED_APPROVERS account. The review is the reviewer's latest decisive review: it has not been dismissed or superseded, and the queue guard does not pin it to a commit. So this seat lands the PR: automerge_enable in this act.

@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 28, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

Triage: triage's PASS is stale: head moved to 1aa6a94b · 2026-10-28T09:55Z

⛔ Not a claim.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

Contract-review-claim: session_01AavokzJ5DndAwitDXvKy4U · head 1aa6a94b184b1ebe8c831adb5f71e109f55fe683 · 2026-10-29T00:50Z

Triage seat, a review after landing under the maintainer's ruling (#27345): this PR landed at 09:59Z on 1aa6a94b, while the last PASS names 6621b074. A must-fix is filed as a new card, not fixed here. 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: 1aa6a94b184b1ebe8c831adb5f71e109f55fe683
Local-runs: none
Scope: packages/spec/src/api/analytics.zod.ts, packages/spec/src/contracts/analytics-service.ts, packages/spec/src/ui/dataset.form.ts, packages/spec/src/ui/dataset.zod.ts, skills/objectstack-ui/evals/analytics-inline-vs-dataset.json, skills/objectstack-ui/rules/dashboards.md + scripts/check-skills-token-ratchet.mjs (the two #26602 rows and the convention they are priced by), .changeset/26602-measure-polarity.md and .changeset/26602-delta-polarity.md, packages/services/service-analytics/src/analytics-service.ts (enrichResultColumns, measure resolution 3112, relay 3153), ui/packages/plugin-dashboard/src/DatasetWidget.tsx (deltaToneClass 285-292, tile 1363 / 1420-1421, cross-tab cell 1573), ui/packages/plugin-dashboard/src/MetricWidget.tsx (276-283 derivedTrend, 489-491 the direction-only colouring), ui/packages/plugin-dashboard/src/ObjectMetricWidget.tsx (290 compareTo, 522-523 the shifted comparison query), ui/packages/plugin-dashboard/src/index.tsx (the object-metric registration, 257-300), ui/packages/plugin-dashboard/src/DashboardRenderer.tsx (171-182 objectui#11466, 872, 1092-1100), ui/packages/core/src/utils/dataset-format.ts (DatasetResultField, 53), packages/spec/src/api/analytics.test.ts (AnalyticsResultMatchesContract, 48), packages/spec/src/ui/dashboard.zod.ts (compareTo 1183-1250) and dashboard-compareto.test.ts (both untouched), packages/spec/liveness/dataset.json, packages/spec/src/type-alias-convention.pin.test.ts and content/docs/references/index.mdx (the merge's two conflicts), content/docs/ui/dashboards.mdx

Inputs read: PR #26739 body, its 33-path file list and all ten PR comments (the four earlier contract records 6446021563 PASS on 15765222, 6457686115 FAIL on 06497be6, 6457977233 PASS on c0cd1852, 6458629959 PASS on 6621b074; the seat's three ACCEPTs; triage's stale notice and claim; the docs-drift bot); card #26602 body and its five comments (triage grade, claim, claim amendment, os-dev-report, the Landed note), read with gh api GET since the brief's card is none and the PR body cites it; the maintainer ruling card #27345 (GET); the 66 check-runs on 1aa6a94b (checkruns.json, all completed, latest 09:59:22Z) and, for the landed-whole question, the 57 check-runs on the merge commit c26d643603 (GET). The diff of what landed, net.diff (c26d643603^1..c26d643603, 33 files, +624/-38), the PASSed head's net-old.diff, and delta-commits.txt (one commit). Trees read with git show 1aa6a94b:PATH, git show c26d643603:PATH and git show origin/main:PATH (origin/main = 6abcb72b0f, 81 commits past the landing). The merge 1aa6a94b was recomputed from its two parents with git merge-tree --write-tree (an object-store computation, no checkout, no worktree, nothing built or run) and diffed against the committed merge. Nothing was built, run or re-run; no gate was re-derived locally.

① Derived judgments

The delta since the last PASS (6621b074, record 6458629959) is exactly one first-parent commit, the merge 1aa6a94b18 "Merge origin/main into claude/project-thread-hjxyd2" (09:15:58Z), parents 6621b074 (the PASSed head) and 0f1045bcc7 (then origin/main). Judged commit by commit, then whole:

  • The merge, in its own right. Between the two merge-bases (d7df63957d..0f1045bc) main moved exactly four of the PR's 33 paths: content/docs/references/index.mdx, packages/spec/src/type-alias-convention.pin.test.ts, scripts/check-skills-token-ratchet.mjs, ui/packages/plugin-dashboard/src/DatasetWidget.tsx. The recomputed auto-merge of the two parents conflicts in the first two only (stages 1/2/3 d862cc1d / a899402b / 22ade5ba and 381b1f9e / 34a2083e / 093c204e); the other two auto-merge clean. The committed merge differs from the recomputed tree in those two files and only inside the conflict regions: index.mdx keeps main's 1558 baseline plus this PR's one (1559; UI 171 → 172 kept from the PR side, 202 pages kept); the ADR-0122 pin test keeps main's 796 → 797 is #26170 (HookRefusalSchema) history line and re-numbers this PR's line to 797 → 798 is #26602, the title and header to 798, toHaveLength(798). Each is main's baseline + exactly this PR's +1; no line in the merge is foreign to both parents other than those two rebased counts. No evil merge — RIGHT.
  • The two clean auto-merges hold. scripts/check-skills-token-ratchet.mjs: main's skills: integrations are declared, not coded — teach the declarative-connector-first rule, the per-archetype recipes and the credential hand-off in objectstack-automation, with an eval that refuses the "write a DocuSign plugin" answer #27223 rows (automation SKILL.md 6466 → 6457, two new eval/reference rows) sit beside this PR's two rows, which are unchanged: ['skills/objectstack-ui/evals/analytics-inline-vs-dataset.json', 1361] with its 1348 → 1361 (#26602) history line, ['skills/objectstack-ui/rules/dashboards.md', 6221] with its 6239 → 6221 (#26602): LOWERED line. DatasetWidget.tsx: main's fix(plugin-dashboard): leave a tile out when the server refuses its data to the viewer #27065 (onForbidden prop) changed the component's signature, so the PR's two tile/cell hunks moved 1391 → 1417 and 1545 → 1570 in net.diff; their content is byte-identical to net-old.diff. The whole net.diff versus net-old.diff differs only in blob ids, @@ offsets, and the two rebased counts (1557/1559, 797/798) — RIGHT.
  • What landed. c26d643603 is a squash of the PR onto 2205863525 (main had moved 9 more commits past 0f1045bc: feat(spec, formula, plugin-email, lint): a mail template formats an amount in the record's own currency with currency:@path #26937, fix(service-analytics): a windowed dataset answer reports its window in drillRanges #27169, fix(spec): a retired filter operator's refusal names every replacement inside the client message cap #27171, test(example-showcase): the related-heavy detail case pins the record page's tabbed tier #27174, refactor(driver-sql): move the presumed-timestamp update out of sql-driver.ts into sql-driver-presumed-timestamp.ts (#25521 cut 21) #27162, docs(ui-merge): merge the 46 objectui field, Console-behaviour and schema pages into the objectstack docs (K2) #27170, test(ui-live-e2e): pin the live lane's backend to the published 18.2.0 #27167, docs(qa): approvals checklist reads a paused run's variables; deep-link APP is com.objectstack.setup #27164, fix(ui): the record form's edit dialog reads the record's field projection #27137). The PR head's diff against its own base 0f1045bc and the landed squash diff net.diff, each with blob ids and @@ offsets stripped, are IDENTICAL (interdiff empty): every hunk the PR authored landed as written, and nothing else landed under its name. The four paths whose bytes differ between 1aa6a94b and c26d643603 (dashboards.mdx, analytics-service.ts, api/analytics.zod.ts, contracts/analytics-service.ts) differ only by fix(service-analytics): a windowed dataset answer reports its window in drillRanges #27169 / docs(ui-merge): merge the 46 objectui field, Console-behaviour and schema pages into the objectstack docs (K2) #27170 / feat(spec, formula, plugin-email, lint): a mail template formats an amount in the record's own currency with currency:@path #26937 hunks of main's own, which the Landed note 6467546126 states and this record confirms — RIGHT.
  • Every accept-set and public-surface claim, re-read on the landed tree (git show c26d643603:…), each byte-identical to the PASSed head on the PR's own hunks:
    • DatasetMeasureSchema.polarity, optional, closed higher | lower | neutral, no default materialised; DatasetMeasurePolarity exported as const and z.input type (dataset.zod.ts 248, 479-482, 764) — RIGHT; authorable-surface/ui.json +1, json-schema.manifest/ui.json +1, api-surface +2, declaration-map +1, export-origins +1, ADR-0122 pin Iso_ui_dataset__DatasetMeasurePolarity (798 on the landed tree).
    • aliases betterWhen / direction / goodDirection / trendDirection → polarity; guidance higherIsBetter / lowerIsBetter → DATASET_POLARITY_NOT_A_FLAG (dataset.zod.ts 380-396) — RIGHT, not a narrowing: strictObject refused every undeclared key before; only the refusal's prescription changes.
    • Wire AnalyticsResultResponseSchema.data.fields[].polarity, the same enum object imported from ../ui/dataset.zod, optional (analytics.zod.ts 139-147); ui/dataset.zod.ts imports nothing from api/, no cycle — RIGHT.
    • Contract AnalyticsResult.fields[].polarity?: DatasetMeasurePolarity (contracts/analytics-service.ts 152-165), bound to the wire by AnalyticsResultMatchesContract (analytics.test.ts 48) — RIGHT and MIRRORED; TypeScript Type Check and Type Check · source gates success on the head.
    • Consumer alias DatasetResultField = AnalyticsResult['fields'][number] (dataset-format.ts 53): polarity reaches the renderer with no UI-side restatement — RIGHT; Type Check UI success.
    • Producer enrichResultColumns (analytics-service.ts 3081-3200 on the landed tree): m resolved with the __compare suffix stripped (3112), relay if (f.polarity == null && m.polarity) f.polarity = m.polarity (3153) under the format discipline, one seam for the live query and the draft preview — RIGHT; measure-column-polarity.test.ts pins each value, both absences and the __compare relay on both paths.
    • Renderer deltaToneClass (DatasetWidget.tsx 285-292) is the only emerald/rose source in DatasetWidget.tsx (git grep on the landed tree: 291 only; the other hits are colorVariants.ts accents and MetricWidget.tsx 489-490, ③/④); the tile (1420-1421) and the cross-tab cell (1573) read the MEASURE's descriptor through measureField — RIGHT. Traced: lower + up → rose, lower + down → emerald, neutral → no tone plus the tile's explicit muted class, higher and undeclared → up emerald / down rose, flat → muted. The undeclared tile's class string is pinned byte-for-byte in DatasetWidget.polarity-26602.test.tsx.
    • Form row { field: 'polarity', label: 'Polarity' } (dataset.form.ts 93), label equal to the schema's .meta({ title: 'Polarity' }); four catalogs +3 lines — RIGHT.
    • Liveness measures.polarity live, evidence deltaToneClass, producer enrichResultColumns, state-counts 31 → 32 — RIGHT; Spec property liveness success.
    • dashboard.zod.ts and dashboard-compareto.test.ts: empty diff in net.diff — the claim amendment's "NOT touched" HOLDS; DatasetCompareToSchema stays a strictObject (analytics.zod.ts 413).
  • Derived gates on THIS head (1aa6a94b, read off checkruns.json): 66 check-runs, all completed, 59 success, 7 skipped (Dogfood Verify CLI, the ${{ matrix.shard }}/3 placeholder, UI Published Artifacts, Temporal Conformance, Schedule gate, Packed-tarball smoke, 18.x upgrade harness — opt-in or matrix placeholders), 0 failure, 0 pending; all carry head_sha 1aa6a94b. Named: Check Changeset, Governed Surface Queue Guard, PR-body guards, Spec property liveness, Lint & Repo Gates (the skills token ratchet), Build Docs, Build & E2E UI, TypeScript Type Check, Type Check UI, Type Check · workspace / source gates / consumer gates / debt ledger, Test Core 8/8, Test UI 16/16 — success. On the merge commit c26d643603 on main: 57 check-runs, 52 success, 5 skipped, 0 failure — the landed whole is green on main too.
  • Since the landing (c26d643603..origin/main, 81 commits): dataset.zod.ts, api/analytics.zod.ts, contracts/analytics-service.ts, dataset.form.ts, analytics-service.ts, DatasetWidget.tsx, liveness/dataset.json, the eval file and both changesets are byte-unchanged; the polarity entries survive in every spec baseline (api-surface 2, authorable-surface 1, json-schema.manifest 1); no commit on main names polarity in its message, and the one -S polarity hit (docs(ui): generate the component reference for all 106 components from sdui.manifest.json; retire the Setup, Console and Studio pages into the metadata pages and rename the section User Interface #27179) is a generated component-reference page using the word in another sense. skills/objectstack-ui/rules/dashboards.md was edited by a later PR (the report-section blockquote: dateGranularity / drilldown sentences out, exportOptions in) — its polarity bullet (249) and presentation line (337) are intact; the file is now 24853 bytes = 6214 tokens under the unchanged 6221 row (7 tokens of headroom left by that later PR, its business, not this PR's). Both 26602-* changesets are still unconsumed on origin/main (no release has run). Nothing on main fixes or changes a finding below.

② Semver level

  • Changesets on the landed tree, both in the root .changeset/ (no ui/.changeset/26602-* exists at c26d643603): 26602-measure-polarity.md grades @objectstack/spec minor, @objectstack/service-analytics minor, @objectstack/platform-objects minor (blob 999bc549e2, the same blob at 6621b074, 1aa6a94b, c26d643603 and origin/main); 26602-delta-polarity.md grades @object-ui/plugin-dashboard minor (blob 578c9d6119, likewise). Both carry Clause-②: yes (widening) and Metadata-protocol: added, as does the PR body.
  • Every package whose published source the landed diff moves is graded: packages/spec/src/** (spec), packages/services/service-analytics/src/analytics-service.ts (service-analytics), packages/platform-objects/src/apps/translations/{en,zh-CN,ja-JP,es-ES}.metadata-forms.generated.ts (platform-objects, the ./apps subpath), ui/packages/plugin-dashboard/src/DatasetWidget.tsx (plugin-dashboard). The remaining paths are tests, generated spec baselines, docs, skills and the ratchet script. The changeset matches what the diff publishes; the FAIL on 06497be6 (missing platform-objects entry) is CLOSED on the landed tree.
  • Clause-②: yes (widening) — CORRECT. One optional authorable key, one optional wire member, one optional contract member; nothing an author could write before is refused or re-meant; the undeclared path is pinned byte-identical at the schema, the wire and the renderer.
  • Metadata-protocol: added — CORRECT. A new key with three accepted values, no default, the undeclared meaning unchanged.
  • Levels: added takes at least minor on a package the diff moves, so minor on spec, service-analytics and platform-objects is the rule as written; plugin-dashboard minor for additive renderer behaviour is right. No BREAKING banner, no ADR-0087 disposition owed. Check Changeset success on the head.
  • Clause-②: yes (widening) — stands.

③ Boundary flags

  • Dev flag / open_questions[0] (the eval ceiling raise under 「A:先削重复,余额按实测提高」): ANSWERED — A, covered, and re-measured on the landed tree: analytics-inline-vs-dataset.json is 5443 bytes → ceil(5443 / 4) = 1361, the row is 1361, zero headroom, after eval 2's and eval 8's restating parentheticals were trimmed first; dashboards.md is 24881 bytes → 6221, the row is 6221. Lint & Repo Gates success on the head. The merge kept both rows as the previous record's ③ expected.
  • Dev flag (Tier H, skills/**): the contract record lifts nothing. The seat armed the PR at 09:38Z under os-zhuang's APPROVED review 5669466996, which 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 measures as naming head 15765222; that the approval and the arming preceded a contract PASS on 1aa6a94b is the subject of 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 (filing gate ③, maintainer ruling) and is ESCALATED there, not re-filed here.
  • Dev flag (@object-ui/plugin-dashboard typecheck not measured locally): CLOSED — Type Check UI and TypeScript Type Check success on 1aa6a94b.
  • Dev note (inline ObjectMetricWidget / MetricWidget trend path and DatasetReportRenderer "not carried, noted, not filed"): the dev's premise is read in the tree and is HALF right. MetricWidget.tsx 489-491 colours shownTrend.direction direction-only, and shownTrend is either the authored options.trend (no comparison at all) or the derivedTrend that ObjectMetricWidget computes from its OWN compareTo (ObjectMetricWidget.tsx 290, 522-523: a parallel aggregate over the shifted filter, computeMetricDelta). So the inline path DOES draw a real period-over-period delta, and it has no polarity source because it aggregates a field, not a dataset measure. Reachability: on a dashboard a type: 'metric' widget without a dataset no longer builds an object-metric node (DashboardRenderer.tsx 171-182, objectui#11466), so the only live surface is a page-level object-metric component node authored with compareTo. Not a carrier for THIS card (hotclm's tile is dataset-bound), but it is the same defect with no authorable remedy, and "noted, not filed" is not a disposition after landing: filed below as should-fix S1. DatasetReportRenderer / plugin-report draw no comparison delta (git grep __compare|computeMetricDelta on origin/main: none) — ACCEPTED.
  • Claim amendment (key on the measure, dashboard.zod.ts untouched, __compare relayed): all three HOLD on the landed tree.
  • Claim line Metadata-protocol: as measured by the dev → added: AGREED.
  • Skills-docs: line ([Decision] pm-dispatch: which PRs owe a Skills-docs: line (the #26184 audit Part B rule draft) — protocol-kind only, protocol-kind plus narrowing, or not adopted #26369 B): PRESENT and TRUE — dashboards.md teaches the key (249, 337), eval 8 asserts polarity: 'lower', content/docs/ui/dashboards.mdx carries the presentation-home row (150) and the compareTo sentence (106). PR-body guards success.
  • Card "Expected" offered a neutral default for undeclared: the dev kept today's colours, which the triage grade required ("⛔ with no guessing from the measure's name"); the renderer test pins a measure literally named turnaround_days reading as higher when undeclared. Not a flag.
  • Gates on this head: none pending, none red (①). Nothing assumed.
  • Prior records: the PASS on 6621b074 is confirmed on the landed tree in every finding it made (①, ②); the FAIL's must-fix is closed; the first PASS's two should-fixes (ratchet row pinned at 6221 with a history line; eval 8's parenthetical trimmed) are closed. This record supersedes 6458629959 on 1aa6a94b.

④ Design judgment

Each hits file, read on the landed tree against the code it cites and the scenario it is accepted against (hotclm's average-approval-turnaround tile, compareTo: previousPeriod, a 23% rise drawn green):

  • packages/spec/src/ui/dataset.zod.ts — HOLDS. The key sits on the measure, the semantic layer, where every widget over it agrees; the enum names the better direction so neutral is a peer value; betterWhen (the card's own spelling), direction, goodDirection, trendDirection are pointed at polarity; higherIsBetter / lowerIsBetter are answered with the three values; up / down / good / inverse / true are refused at ['polarity']. Tried to break: a count whose meaning flips with a filter is two measures each with its own filter and polarity, which the doc comment states; a derived ratio measure may declare one and the relay reads m.polarity regardless of derived, so a ratio KPI is covered. No finding.
  • packages/spec/src/api/analytics.zod.ts — HOLDS. Same enum object, optional, the description states the exact presence rule the producer implements (measure columns of a dataset answer, __compare included; absent on dimensions and on a cube answer; absent reads as a rise drawn good). No finding.
  • packages/spec/src/contracts/analytics-service.ts — HOLDS. Mirrored member, bound by AnalyticsResultMatchesContract; the @object-ui/core alias inherits it. No finding.
  • packages/spec/src/ui/dataset.form.ts — HOLDS. One row-property entry in the measures repeater, label equal to the schema's .meta({ title }) as the repeater's own comment ([Decision] Repeater row-property localisation: 604 catalog leaves across four locales — who authors the translated copy? #17508) requires; the i18n populations move by one; its catalog rows are carried by the platform-objects changeset entry. No finding.
  • skills/objectstack-ui/rules/dashboards.md — HOLDS. The Metric-widgets bullet (246-249) states the renderer's real rule ('lower' rise red, 'neutral' uncoloured, undeclared = rise green), keeps the options.trend / compareTo teaching, and the presentation-home line (337) lists polarity beside format / currency, which is where the code reads it; the ratchet row follows the file down to 6221. One omission probed: the file's other measure-key teaching sites (90 "Format/currency → the measure's format/currency", 181, 325) do not name polarity, but 337 is the file's normative "presentation lives on the measure" sentence and the bullet is where compareTo is taught, so an authoring agent reading for a KPI delta meets the key. No finding.
  • skills/objectstack-ui/evals/analytics-inline-vs-dataset.json — HOLDS on its assertions: eval 8 asks for a lower-is-better KPI vs the previous period and must_contain asserts polarity: 'lower', the one thing an authoring agent must learn from 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; must_not_contain: beforeUpdate keeps the stored-number refusal. SHOULD-FIX S2 on the grader cue: expected_output reads "A dataset measure { aggregate: 'avg', duration: {...}, polarity: 'lower' } under compareTo". A dataset measure is not "under compareTo" — compareTo is the widget's key and DatasetCompareToSchema is a strictObject that refuses polarity, the very placement the PR body rules out. A grader reading the cue literally would reward an answer that puts the measure (or its polarity) inside compareTo, which the spec refuses. The three earlier records called this a note; after landing, with the eval now the published teaching of the key, it is a finding.

Must-fix: none.

Should-fix, each written so it can be filed as its own card (nothing is fixed on the closed PR):

  • S1 — object-metric page node: a compareTo delta is still coloured by direction alone, with no authorable polarity. Evidence on origin/main: ui/packages/plugin-dashboard/src/MetricWidget.tsx:489-490 (shownTrend.direction === 'up' && "text-emerald-600 …", 'down' && "text-rose-600 …"), fed by derivedTrend (276-283) which ui/packages/plugin-dashboard/src/ObjectMetricWidget.tsx:522-523 computes from the node's own compareTo (290) over a shifted filter; the node aggregates a field, so packages/spec/src/ui/dataset.zod.ts polarity has nothing to attach to. Reachable only from a page-level object-metric component node (ui/packages/plugin-dashboard/src/index.tsx:257), not from a dashboard (DashboardRenderer.tsx 171-182, objectui#11466). Done looks like: a widget-level polarity on the object-metric component props (the PR body's own foreseen "later, pure widening"), the same higher | lower | neutral vocabulary, MetricWidget colouring derivedTrend by it exactly as deltaToneClass does, undeclared byte-identical, a pin test, a changeset (@object-ui/plugin-dashboard minor, plus whichever @object-ui/types row declares the component's props), and the skills line that says where an inline KPI declares it — or a recorded ruling that the inline node owes no polarity. Grade: p2 (a user-visible wrong colour on a reachable surface, no consumer filed).
  • S2 — eval 8's expected_output places the measure "under compareTo". Evidence on origin/main: skills/objectstack-ui/evals/analytics-inline-vs-dataset.json:80 ("expected_output": "A dataset measure \{ aggregate: 'avg', duration: { from: 'created_at', to: 'closed_at', unit: 'days' }, polarity: 'lower' }` under `compareTo`; no hook-kept stored number."), against packages/spec/src/api/analytics.zod.ts:413 (DatasetCompareToSchemais astrictObject) and the PR's own rule that the key lives on the measure, never on compareTo. Done looks like: the cue reads "...on the dataset measure, with the widget's compareTo: { kind: 'previousPeriod' }" (or equivalent), must_contain unchanged; the file's ratchet row (scripts/check-skills-token-ratchet.mjs`, 1361, zero headroom) is re-pinned AT the new measurement, paid inside the file first per 「A:先削重复,余额按实测提高」, with a history line. Grade: p3 (a grader cue, the assertions are right).

The remainder of the diff (service, renderer, tests, generated spec artifacts, translations, docs) is the lane seat's ACCEPT at TIER_DEFAULT; it was read here only to judge the hits, the changesets and the merge.

Implemented-by: claude/project-thread-hjxyd2
Reviewed-by: session_01AavokzJ5DndAwitDXvKy4U

VERDICT: PASS


Generated by Claude Code

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/l tests tooling

Projects

None yet

3 participants