Skip to content

feat(spec): a percent field declares its read-only face, a progress bar or the plain percentage (percentDisplay) - #28222

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-hosvni
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-hosvni

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

domain:spec#2 · os-tesla · session_01L9qibx5r5nhDMwpRs4SL6D

Requested by Tesla · project thread

Fixes #27973

Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: follow-up #28256 (skills/objectstack-data/rules/field-types.md, to land with the renderer half #28255)

维护者速读

  • 改了什么:百分比字段(percent)新增一个可选键 percentDisplay,取值 bar(进度条)或 plain(只显示百分数文字),决定列表单元格和记录页上怎么显示。
  • 为什么改:现在所有百分比字段一律画进度条,但商机赢率、转化率这类"比率/概率"不是进度,画条会误导(HotCRM crm_opportunity.probability);以前没有任何声明的键能关掉它。
  • 风险与回滚:只加了一个可选键,不填时解析和显示完全不变;只影响显示,不改存储值、校验和小数位。回滚即 revert 本 PR。本 PR 只是契约这一半,界面还不读它,填了也照样画条,描述文字里写明了;渲染器那一半是后续 ui 卡。
  • 先看:packages/spec/src/data/field.zod.ts 中紧跟 useGrouping 的 percentDisplay 声明、packages/spec/liveness/field.json 的 percentDisplay 行(planned)、packages/spec/src/data/field.test.ts 的 percentDisplay 测试组。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/data/field.test.ts -t percentDisplay,预期 7 条全绿。
  • 需要你做的:无,席位落地。

What changes

A percent field had no declared key to choose its read-only face, so the console's percent face (PercentCellRenderer, shared by list cells and the record page) draws a progress bar for every value, including a rate or a probability. This is the spec half of the card: the key, its describe text, its liveness grade and the regenerated references.

  • Spec (packages/spec/src/data/field.zod.ts). FieldSchema declares percentDisplay: z.enum(['bar', 'plain']).optional(), placed beside useGrouping, the other flat numeric display hint. Absent = the renderer's default face (the bar today); bar pins the bar; plain shows the percentage as text alone (a stored 0.25 reads 25%, at the field's scale). Display only: no stored value, validation or width changes. No default is declared and the key is not type-restricted at parse, the same posture useGrouping takes and its tests pin. The describe says the Studio UI does not read the key yet, so an author is not promised a face the console does not draw (Prime Directive 10).
  • Liveness (packages/spec/liveness/field.json). New row percentDisplay: planned, measured at main 9f13ee81, with the flip condition written into the note (the console's percent face reads the key, measured under ui/, and the describe's not-yet-read clause goes in the same change). state-counts/field.md regenerated (planned 6 → 7).
  • Generated. content/docs/references/data/field.mdx, data/object.mdx, system/migration.mdx (gen:docs, the field table is rendered in all three) and authorable-surface/data.json (one data/Field:percentDisplay line).
  • Tests that count or reconcile the field schema. packages/spec/src/system/metadata-form-zod-reconciliation.test.ts gains a root ledger omit row for field.percentDisplay beside the conditionalFormatting row (planned, not yet rendered: the field form offers the key with the renderer half, as useGrouping was offered once enforced). packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts: the served field schema's control count moves 79 → 80. Both were red on the first CI run (Test Core 2/8 and 7/8) and are the direct consequence of the new key.
  • Field-key tables in two published packages. Both tables enumerate every FieldSchema key and their tests red on an unclassified one. packages/metadata-core/src/object-schema-fls-references.ts, FIELD_REFERENCE_POSITIONS (ADR-0106 D1): percentDisplay: keep, beside useGrouping. It names no field, so a served object schema keeps an authored value whatever the caller may read; unclassified, a value that spelled a denied field's name would have been scrubbed. packages/drivers/driver-sql/src/builtin-column-collision.ts, FIELD_KEY_STORAGE_CLASS: percentDisplay: 'presentation', beside useGrouping, since the DDL never reads it. These were red on CI as Test Core 6/8 and 5/8. In both shards the visible log tail was green, because the step runs the cli slice last and exits with the first red leg's status.
  • Changeset. .changeset/27973-percent-display.md: @objectstack/spec, @objectstack/metadata-core and @objectstack/driver-sql, each minor (the Metadata-protocol: added level), with both declaration lines and one sentence per package on its classification.

The shape chosen, and the alternatives not taken

  • Chosen: a flat optional enum on the field, beside useGrouping. The order asked whether a per-type display/options member already exists for numeric types that this should join. Measured: none does. useGrouping is the only numeric display hint and it is a flat sibling; currencyConfig is a currency-only member about WHICH currency, not how an amount is drawn. Joining it would put a percent key under a currency name. A flat key is the house shape for this class, and an enum (not a boolean like showBar) leaves room for a third face without a second key.
  • Not taken: a nested percentConfig: { display } block. One member does not earn a container; it would add a strict object, a form composite and a liveness drill for a single choice.
  • Not taken: giving Field.format a percent vocabulary. format is declared vocabulary-free on every type; teaching it one word on one type is the dialect the card asks to avoid.
  • Not taken here: the list-column key. Out of this PR by the order (a sibling PR is in ui/view.zod.ts). A field-level key reaches list cells once the grid's relay sites copy it onto the cell's field metadata (part of the ui half, as for useGrouping); a per-column override is only owed if a list needs a different face from the record page, which no producer has shown yet.
  • Not taken: authorWarn on the liveness row. It is a display hint with a harmless fallback (the bar it draws today), the same grading useGrouping had while planned. A warned field row would also re-open the lint's field walk, outside this PR's file surface. The describe carries the not-yet-read clause instead.

Tests

packages/spec/src/data/field.test.ts, new group percentDisplay — percent-field read-only face (7 cases): accepts plain and bar; absent stays absent (no default materializes); a value outside the closed set (number, progress, true, null) is refused with an issue on the key's own path; the key is declared, not refused as unknown; Field.percent(...) threads it; the JSON Schema slot is the closed enum ['bar', 'plain'] with no default and not required.

Acceptance notes

  • The ui half is a follow-up domain:ui card, carried by the seat: PercentCellRenderer (ui/packages/fields/src/index.tsx) reads percentDisplay on every host, and the grid / detail relay sites that copy useGrouping onto the cell's field metadata must copy this key too, or plain will not reach the cell on those surfaces. That card also flips the liveness row to live and drops the describe's not-yet-read clause.
  • The Studio field form (field.form.ts) does not offer the key yet, on purpose: the form shows a display hint once it is enforced (the useGrouping row's rule). The ui card adds the row.
  • HotCRM crm_opportunity.probability and the campaign rate fields can set percentDisplay: 'plain' once the renderer reads it.

Verification

spec tests, typecheck, ablation
  • At head b3ca10e1: @objectstack/driver-sql vitest (under the verify lock): Test Files 236 passed, 12 skipped (248); Tests 3847 passed, 214 skipped. Before the row, the same run had 1 failed file, the FIELD_KEY_STORAGE_CLASS completeness test. The other packages planned into Test Core 5/8 were green: plugin-webhooks 25/25 files, service-job 11/11, trigger-schedule 11/11, connector-slack 3/3, client-react 3/3.
  • At head 2fdc1c19: @objectstack/metadata-core vitest: Test Files 21 passed (21); Tests 873 passed.
  • At head 1990c7fc: full @objectstack/spec vitest (under the verify lock): Test Files 843 passed, 1 skipped (844); Tests 24004 passed. @objectstack/metadata-protocol: protocol.meta-types-degenerate-derivation.test.ts 18/18 (the full suite before the fix: 1 failed, 21858 passed, that one count).
  • At 2f944c8d, pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/data/ scripts/liveness/ (under the verify lock): Test Files 161 passed (161), Tests 4975 passed, 1 todo. (At 0c8ff3e4, src/data/ alone: 149/149 files, 4598 tests.)
  • pnpm --filter @objectstack/spec typecheck: lock VERDICT command-exit 0.
  • Ablation (scripts/ablation-replace.mjs, the suite reads src): z.enum(['bar', 'plain']) → z.enum(['bar']), anchor 1 → 0 on disk; -t percentDisplay → 4 failed, 3 passed; restored, blob == HEAD and git diff HEAD empty.
generated artefacts
  • pnpm --filter @objectstack/spec check:generated after a dts build: first run named content/docs/references/** and liveness/state-counts/ stale; regenerated with gen:docs and gen:liveness-counts; after merging origin/main and rebuilding, 14/14 up to date (exit 0).
derived gates
  • At b3ca10e1: the derivation gives 106 families, which are the 105 below plus check:driver-conformance. The delta was run: check:driver-conformance, check:durability-log-level, the changeset family (check-changeset-no-major, check-empty-changeset, check-adr-0087-registration), check:type-check-debt, check:undeclared-dep-imports, check:issue-citations and check:doc-authoring, all exit 0 (record: 27973-gates-delta-b3ca10e1.list).
  • At 2fdc1c19: 105 families derived and 105 run, all exit 0, 0 NOT-MEASURED (record: 27973-gates-ran-2fdc1c19.list).
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 2f944c8d (the head before the two test fixes, which touch only test files): 104 families, each run with stdin from /dev/null, exit code recorded before any pipe (record: 27973-gates-ran-2f944c8d.list in the seat's out folder).
  • --ran reconciliation: 104 derived, 104 run, 0 NOT-MEASURED, 0 unrun, 0 red.
  • Earlier passes: the same 104 at 0c8ff3e4 and at 39dd0028 (after the first merge of origin/main), all exit 0. On the first pass check-plugin-teardown-shape --self-test exited 3 until its pinned control commit 621a4876 was fetched into the shallow clone, then 0. On the final pass spec check:skill-examples exited 3 (PREREQUISITE NOT MET: the merge brought a packages/client source change, so its dist predated it); after a dts build of @objectstack/client-react... (38/38) it exited 0.
  • Full pnpm turbo run build of the non-ui workspace (dts on) before the gate passes: 77/77 tasks.

Generated by Claude Code

…(bar or plain)

A percent field had no declared key to choose its read-only face, so every
host drew a progress bar even for a rate or a probability. Declare a flat,
optional display hint beside useGrouping: absent = the renderer default
(the bar), 'bar' pins it, 'plain' shows the percentage alone. Liveness row
planned until the console's percent face reads it.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibyu6UsPddURvKQb3fE
…ss counts; changeset for percentDisplay

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibyu6UsPddURvKQb3fE
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/driver-sql, @objectstack/metadata-core, @objectstack/spec, touching 2 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/spec/authorable-surface/data.json, packages/spec/liveness/field.json, packages/spec/liveness/state-counts/field.md), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/concepts/metadata-driven.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/data-modeling/external-datasources.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/data-modeling/field-types.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/data-modeling/validation-rules.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/deployment/troubleshooting.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/deployment/validating-metadata.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/getting-started/quick-reference.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/kernel/contracts/data-engine.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/protocol/backward-compatibility.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/protocol/objectql/types.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/protocol/objectui/concept.mdx (via FieldSchema (symbol, a top-level const))

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

  • content/docs/releases/v17/17-0.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/releases/v17/17-1.mdx (via FieldSchema (symbol, a top-level const))

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

Coarse fallback — 170 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 32387500a601958826fe0b5ba72f53948c9dc5d2 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 32387500a601958826fe0b5ba72f53948c9dc5d2

⚠️ 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 32387500a601958826fe0b5ba72f53948c9dc5d2 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract-review-claim: session_01L9qibw58ehQ2gRHP2h2Puz · head 2f944c8dc171d75e22107372817d02fbac4f1856 · 2026-10-30T17:45Z

Contract review lane (Fable): schema face at CONTRACT_REVIEW_TIER (packages/spec/src/data/field.zod.ts) per record-recognisers.mjs --template --paths; reading the card, the PR diff and the head's check-runs only. The record follows on this head once its check-runs complete.

…t it on the served field schema

The form reconciliation gate needs a root ledger row for a key the field
form does not offer yet (the conditionalFormatting precedent), and the
/meta/types control count for field moves 79 -> 80.

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

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract-review-claim: session_01L9qibw58ehQ2gRHP2h2Puz · head 1990c7fcfea91c0d7133be2411c977867180bdae · 2026-10-30T18:16Z

Re-claim after the head moved from 2f944c8 (claim 6529528882): delta is the two test-only fixes for the Test Core red (metadata-form omit row for percentDisplay; /meta/types field property count 79 to 80). Record follows on green.

…(keep)

FIELD_REFERENCE_POSITIONS must classify every FieldSchema key (ADR-0106 D1).
percentDisplay names no field, so it is kept on a served object schema;
unclassified, a value spelling a denied field's name would be scrubbed.

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

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract-review-claim: session_01L9qibw58ehQ2gRHP2h2Puz · head 2fdc1c1910c773de560c22bb7f5c87dea0001dc3 · 2026-10-30T18:32Z

Re-claim after the head moved from 1990c7f (claim at 18:18Z): delta is one commit, percentDisplay: keep in the metadata-core field-level-security classifier (ADR-0106 D1) plus its changeset line. Record follows on green.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-tesla · session_01L9qibx5r5nhDMwpRs4SL6D

Seat judgment before a re-run · 2026-10-30T18:46Z · head 2fdc1c1910c773de560c22bb7f5c87dea0001dc3

Test Core (5/8), job 124239486040 in run 41106510673, is judged a runner flake, not this PR's: the shard's vitest summary is all green (@objectstack/cli 134/134 files, 1411 passed; turbo 63/63 successful), but a pnpm spawned inside the cli run went through corepack and got HTTP 429 from registry.npmjs.org, then "pnpm: Permission denied" / "/usr/bin/env: node: No such file", and the step exits 1 after the green summary. The diff touches no cli code and no install path. The seat re-runs the failed jobs once through rerun-failed.mjs when the run completes; a second failure is real.

FIELD_KEY_STORAGE_CLASS must classify every FieldSchema key; the DDL
never reads a percent field's read-only face.

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

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-tesla · session_01L9qibx5r5nhDMwpRs4SL6D

Correction to the seat's judgment 6530878086 · 2026-10-30T19:21Z

That judgment was wrong: Test Core (5/8) was this PR's red, not a runner flake. The step runs several legs and exits with the first non-zero one, so the green cli tail hid an earlier red leg: packages/drivers/driver-sql/src/builtin-column-collision.ts FIELD_KEY_STORAGE_CLASS did not classify the new percentDisplay key. The card thread fixed it at b3ca10e1 (and the sibling metadata-core FLS classification at 2fdc1c19, which cleared 6/8). The one re-run spent on run 41106510673 is recorded here so it is not counted as a flake confirmation.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract-review-claim: session_01L9qibw58ehQ2gRHP2h2Puz · head b3ca10e1a83a30c221c4ccf69abb326a31610268 · 2026-10-30T19:40Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: b3ca10e1a83a30c221c4ccf69abb326a31610268
Local-runs: none
Scope: packages/spec/src/data/field.zod.ts + ui/packages/fields/src/index.tsx (read on main: PercentCellRenderer, the reader the liveness row says does not read the key yet), packages/spec/liveness/field.json (the new row beside the useGrouping row it cites as precedent), packages/spec/liveness/state-counts/field.md, packages/spec/src/data/field.test.ts, packages/spec/authorable-surface/data.json, .changeset/27973-percent-display.md, the three regenerated reference pages (content/docs/references/data/field.mdx, data/object.mdx, system/migration.mdx), and the two test-only files of the head-move delta: packages/spec/src/system/metadata-form-zod-reconciliation.test.ts (the percentDisplay omit row) and packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts (field 79 to 80), the second delta's packages/metadata-core/src/object-schema-fls-references.ts, and the third delta's packages/drivers/driver-sql/src/builtin-column-collision.ts (percentDisplay: keep in FIELD_REFERENCE_POSITIONS).

Inputs: card #27973 body and its two comments (claim 6526095170, os-dev-report 6529040825); PR #28222 body, the 11-file list and the diff against main; the deltas 2f944c8 to 1990c7f (one test-only commit, read under re-claim after the first head's Test Core red) and 1990c7f to 2fdc1c1 (one classifier line plus its changeset line, read under a second re-claim) and 2fdc1c1 to b3ca10e (one classifier line plus its changeset line, read under a third re-claim after the seat's own correction 6531609274 that the 5/8 red was this table, not a runner flake); the check-runs on b3ca10e1 (read 2026-10-30T19:41Z: check-runs on b3ca10e: 54 total, 43 success (Auto Label, Build & E2E UI, Build Core, Build Docs, Build UI Deps, Check Changeset, Check Documentation Links, Check PR Size, Dogfood Regression Gate, Flag docs affected by code changes, Governed Surface Queue Guard, Lint & Repo Gates, Lint · CI, release & engine gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · slot lookup & corpus, Lint · scoped · tooling self-tests, Live E2E UI, PR-body guards (claim · duplicate · Part-of · single-writer path), Plan Test Core shards, Spec property liveness, Test Core, Test Core (all 8), Test UI, Type Check UI, Type Check · consumer gates, Type Check · debt ledger, Type Check · source gates, Type Check · workspace, TypeScript Type Check, filter); 11 skipped path-filtered (18.x upgrade harness (opt-in), Auto Label, Check PR Size, Dogfood Regression Gate (matrix shard/3), Dogfood Verify CLI, Lint UI, Packed-tarball smoke (opt-in), Schedule gate · skip when a green push run covers the commit, Temporal Conformance (live PG + MySQL), Test UI (matrix shard/16), UI Published Artifacts); 0 failure, 0 in progress.). Author user.login is objectstack-fleet[bot], so no login note is owed. The reviewer is not the drafter (session_01L9qibyu6UsPddURvKQb3fE).

① Derived judgments

  • The key: percentDisplay: z.enum(['bar', 'plain']).optional() on FieldSchema, placed beside useGrouping (field.zod.ts:1501 on main), with no declared default and no parse-time type restriction — the posture useGrouping takes, so an authored key on a non-percent field parses and is the renderer's to ignore, exactly as useGrouping on a non-number is today. The describe names the three readings (absent = the renderer's default face, bar, plain), says the change is display only, and carries the not-yet-read clause, so an author is not promised a face the console does not draw.

  • The reader claim holds: ui/packages/fields/src/index.tsx#PercentCellRenderer (read on main, line 866 on) takes value and field, formats at the field's scale in the display locale and draws the progressbar for every value; no key suppresses the bar and nothing under ui/, packages/lint or packages/cli reads percentDisplay. Field.percent(config) (field.zod.ts:3246) spreads its config, so the helper threads the key without an edit, as the test pins.

  • Liveness: a planned row, verifiedAt 2026-10-30, measured at main 9f13ee81, with the flip condition written down (the console's percent face reads the key, measured under ui/, and the describe's clause is dropped in the same change). That is the shape the useGrouping row records for its own planned period (declared first, never authorWarn'd, flipped when the reader landed); state-counts/field.md moves planned 5 → 6, classified 106 → 107.

  • Tests: the new percentDisplay group pins both accepted values, absence staying absent (no default materializes), a refusal outside the closed set on the key's own path, the key declared rather than stripped, the helper, and the JSON Schema slot as a closed enum with no default and not required. The ablation (['bar'] alone → 4 of 7 red, restored) is the dev's own report, read not re-run.

  • Generated: authorable-surface/data.json gains data/Field:percentDisplay, the baseline move Metadata-protocol: added requires; the field table row appears in the three reference pages that render it.

  • Order: no deviation; ui/** untouched, no list-column key (the order's sibling), no governed path.

  • Head-move delta (2f944c8 to 1990c7f, test-only): the first head redded Test Core on metadata-form-zod-reconciliation.test.ts:999 (percentDisplay accepted by the Zod but unauthorable in the field form and unexcused). The new head adds the omit row for field.(root).percentDisplay in the precedent's own shape (beside conditionalFormatting, spec(ui): field-level conditionalFormatting — a field declares [{ condition, style }] cell rules (CEL over value and record), the row block's own grammar, for presentational rules that carry no semantic (maintainer-directed) #22228: planned, not yet rendered, the row joins the form with the renderer half), which is the declared-not-enforced posture the liveness planned row already states, and bumps the /meta/types field property count 79 to 80 with its [#27973] ledger comment, the arithmetic consequence of the new key. Neither touches the schema, the accept set or the pages; ①②③④ below stand unchanged.

  • Second head-move delta (1990c7f to 2fdc1c1): FIELD_REFERENCE_POSITIONS in metadata-core's field-level-security reference scrubber (ADR-0106 D1) classifies percentDisplay as keep, beside useGrouping: keep. Read against the module's own table: keep is the disposition for a position that names no field, and the key's values are the enum bar / plain, so a served object schema keeps the authored value for every caller and no value that happens to spell a denied field's name is scrubbed; it is the only correct disposition, and it is the consequence the classifier's completeness pin makes mandatory for every new Field key. The changeset adds @objectstack/metadata-core: minor with a paragraph saying so; a classifier entry would also be defensible at patch, but with Metadata-protocol: added the rule only requires that one moved package be graded minor or above, so the grade is within the rule and not a finding. ①②③④ stand.

  • Third head-move delta (2fdc1c1 to b3ca10e): FIELD_KEY_STORAGE_CLASS in driver-sql's builtin-column-collision table classifies percentDisplay as presentation, beside useGrouping: 'presentation', with the comment that the DDL never reads it. Read against the table's own rule (storage for a key createColumn reads, presentation for one it never reads): a read-only face is never a column shape, so presentation is the only correct class, and the entry is what that package's every-key completeness pin (builtin-column-collision.test.ts, which redded Test Core 5/8 on 2fdc1c1) makes mandatory for every new Field key. The changeset adds @objectstack/driver-sql: minor with a sentence saying so, the same grade the metadata-core line took. No schema, accept-set or page changes; ①②③④ stand.

② Semver level

  • @objectstack/spec minor, Clause-②: yes (widening) / Metadata-protocol: added: one optional authorable key joins FieldSchema; nothing an author wrote before changes meaning or is refused. The level axis holds (a declared added grades at least one moved package minor and moves the authorable-surface baseline). The two classifier packages the deltas touched, @objectstack/metadata-core and @objectstack/driver-sql, each carry a minor line: defensible at patch as well, but consistent with each other and with the rule, so the level stands.
  • The three carriers agree: PR body, claim 6526095170 and the changeset.

③ Boundary flags

  • Served nowhere yet, by design: the key is planned; until the domain:ui follow-up lands, an authored plain still draws the bar, which the describe and the changeset both say. No authorWarn: a harmless display fallback, the useGrouping precedent, and a warned field row would re-open the lint field walk outside this PR's surface.
  • Follow-ups the seat carries: the renderer half (PercentCellRenderer reads the key on every host, and the seven relay sites the useGrouping row names as its producer copy this key too, or plain will not reach a grid or detail cell), the Studio field form row, the liveness flip, and the Skills-docs: row in skills/objectstack-data/rules/field-types.md — deferred correctly, since the skill teaches keys that are enforced.
  • Governed: none. Tier: +110/-1 over 9 files, Tier S. Author objectstack-fleet[bot] ⇒ no login note.

④ Design judgment

  • Schema against the card: the card's Wanted is a declared display key on the field with the bar as the default face; the PR gives a flat optional enum beside the one sibling display hint, measured against the alternatives (a percentConfig container for one member, a Field.format vocabulary that format declares it has none of, a list-column override no producer has asked for) — the narrowest shape with the room an enum leaves for a third face. Holds.
  • First consumer: none yet; the key is graded planned and says so in its describe, so the first consumer is the ui card the seat files, whose acceptance (every host, the relay sites, the form row, the flip) the PR body already spells out. Holds.
  • Risk read: the key is not type-restricted at parse, so it is authorable on any field; the describe scopes it to percent and the renderer ignores it elsewhere, the same accepted looseness as useGrouping. Not a finding.

Should-fix, none blocking: when the ui card lands, the liveness row's producer should name the relay sites the way the useGrouping row does, so a dropped copy is visible.

Implemented-by: claude/project-thread-hosvni
Reviewed-by: session_01L9qibw58ehQ2gRHP2h2Puz

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#2 · os-tesla · session_01L9qibx5r5nhDMwpRs4SL6D

维护者速读

  • 改了什么: 百分比字段新增可选键 percentDisplay('bar' 进度条,默认;'plain' 只显示百分数),放在 useGrouping 旁边。这次只落契约这一半,活性台账标为 planned,说明里写明渲染器尚未读取;另外两个按字段键分类的表(字段级安全、SQL 驱动存储类)也登记了这个键。
  • 为什么改: 概率、转化率这类百分比不是进度,画成进度条会误导;作者没有任何键可以关掉进度条(HotCRM 商机概率、活动转化率)。
  • 风险与回滚: 只加一个可选键,不写它的字段解析、存储和显示都不变。回滚即 revert。
  • 先看: packages/spec/src/data/field.zod.ts(percentDisplay)、packages/spec/liveness/ 对应行、packages/drivers/driver-sql/src/builtin-column-collision.ts 与 packages/metadata-core/src/object-schema-fls-references.ts 各一行。
  • 两分钟验证: 跑 PR 正文“验证”一节列的 spec 字段测试,预期全绿。
  • 需要你做的: 无。不属于受管面,契约复审已 PASS,由席位落地。渲染器半边 Percent field shows a plain percentage when percentDisplay is plain: PercentCellRenderer reads the key #28255,技能文档 skills: teach percentDisplay in the objectstack-data field-types rule #28256。
  • 席位意见: ACCEPT,可落地。

席位核对 · ACCEPT · domain:spec seat 2 (#18549) · 2026-10-30T19:44Z

Head b3ca10e1a83a30c221c4ccf69abb326a31610268. Card #27973, claim 6526095170.

  • Shape: draft to main, made by the relay; Fixes #27973; Clause-②: yes (widening), Metadata-protocol: added at line start. Skills-docs: rewritten from follow-up owed to follow-up #28256 in this act (body read back identical). Changeset @objectstack/spec, @objectstack/metadata-core, @objectstack/driver-sql minor.
  • Contract review: PASS at CONTRACT_REVIEW_TIER by the lane review thread, record 6531984191 on this head (three deltas folded: the form-reconciliation omit row and /meta/types count, the metadata-core FLS keep row, the driver-sql presentation row), reviewer not the drafter. No must-fix. Should-fix (name the relay sites in the liveness row's producer when the renderer lands) rides Percent field shows a plain percentage when percentDisplay is plain: PercentCellRenderer reads the key #28255.
  • Diff read by the seat: a two-value enum, no default written, bar stays the face for every existing field; no list-column key (the field key covers every host). The two classifier rows are the same key registered where every FieldSchema key must be.
  • Seat correction on record: the seat's flake judgment 6530878086 on Test Core 5/8 was wrong (corrected in 6531609274); the red was the driver-sql table, fixed at this head.
  • Follow-ups filed: Percent field shows a plain percentage when percentDisplay is plain: PercentCellRenderer reads the key #28255 (renderer half + relay sites + field.form.ts row + liveness flip), skills: teach percentDisplay in the objectstack-data field-types rule #28256 (skills/objectstack-data/rules/field-types.md, Tier H).
  • Gates: check-runs on this head 43 success, 13 skipped, 0 red, 2 in progress (started by this act's body edit); auto-merge waits for them. mergeable_state clean. check-governed-merges --branch: NOT governed, 13 paths, +129/−3.
  • Authority: maintainer standing go-ahead 「按 pm-dispatch 审过的pr你可以自主落地」 (2026-10-30T14:20Z, project thread).
  • Landing: pr_ready + automerge_enable in this act.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 30, 2026 19:45
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 30, 2026 19:45
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 30, 2026
Merged via the queue into main with commit 5b7dbb8 Oct 30, 2026
61 of 62 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-hosvni branch October 30, 2026 20:25
os-tesla pushed a commit that referenced this pull request Oct 30, 2026
Resolve the #28222 overlap: keep percentDisplay and lookupAutofill side by
side in the metadata-form omit ledger, and take the /meta/types field count
to 81 (80 percentDisplay, 81 lookupAutofill).

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibyU9JHYh6atGVnEXr3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Percent field: a display key that shows a plain percentage instead of the progress bar, in list cells and on the record page

1 participant