Skip to content

docs(skills): the data skill's percent row states the storage scale — a fraction unless the field declares max above 1 - #28513

Merged
os-zhuang merged 1 commit into
mainfrom
claude/project-thread-w041mp
Nov 1, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/project-thread-w041mp

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Tesla · project thread

Fixes #28489

Clause-②: no
Metadata-protocol: none

维护者速读

  • 改了什么:objectstack-data 技能字段类型表的 percent 行,把「Percentage values (0-100)」改成平台真正的存储规则:不声明 max 大于 1 时存 0-1 的小数,声明了(如 max: 100)才存整数百分点;种子、默认值、API 写入都按这个存储刻度。
  • 为什么改:发布出去的技能说错了一句,AI 作者照着在没有 max 的 percent 字段上种 25,界面读出来是 2500%(docs(skills): the objectstack-data field-types rule teaches percentDisplay and lookupAutofill, and the fields page teaches lookupAutofill #28487 契约复审发现,triage 定 p2)。
  • 风险与回滚:纯文档;技能文件 token 上限余量为 1,新增以删除 Vector (AI Embeddings) 示例支付(它唯一的事实「dimensions 是平级键」Enhanced Types 表行已写明);回滚即 revert。
  • 先看:skills/objectstack-data/rules/field-types.md 第 54 行(percent 行)与被删的 Vector 示例块,.changeset/28489-percent-storage-scale-skill.md。
  • 两分钟验证:node scripts/check-skills-token-ratchet.mjs,预期退出 0,field-types.md 读数 3207/3207。
  • 需要你做的:受管面(skills/**,Tier H)——席位完成契约复审后,由授权账号 Approve,再由席位落地。

What changes

One cell of the percent row in skills/objectstack-data/rules/field-types.md (Numbers table). It read Percentage values (0-100); it now reads: Percentage; stored as a fraction (0-1) unless the field declares max above 1 (max: 100), then whole points; seeds, defaults, API values use that scale. The Config cell (min, max, precision, percentDisplay) is unchanged.

The rule is percentScaleOf in packages/spec/src/data/percent-scale.ts on main 1f1a369b: "A percent field stores a FRACTION unless it declares max above 1" — the rule the percent edit widget writes by (typed input divided by 100 unless max is above 1), so min: 0, max: 100 is a storage statement, not only validation; the read side (percentDisplayValue, ui/packages/fields/src/index.tsx PercentCellRenderer via percentCellScale) multiplies a fraction by 100. The cell says "seeds, defaults, API values use that scale" because every write path stores the number as sent: a seeded 25 on a field without max reads back 2500%, and 0.25 is what "25%" needs.

Changeset .changeset/28489-percent-storage-scale-skill.md: @objectstack/skills patch, both declaration lines.

Token ceiling — paid by deletion, never by re-wrap

field-types.md had 1 token of headroom under its 3207 ceiling after PR #28487. The new cell is +128 bytes over the old one. Deleted to pay for it: the ### Vector (AI Embeddings) example under Common Field Configurations (125 bytes) — its one fact, that dimensions is the flat field-level key, is already the Enhanced Types row (`dimensions` (flat sibling, e.g. `1536`)), and the invented-key blockquote at the top of the file already names vectorConfig as refused. No line was re-wrapped.

Readings (bytes · ceil(bytes/4) tokens · lines), base 1f1a369b → head 12f6cdc6:

Surface Before After
skills/objectstack-data/rules/field-types.md 12824 B · 3206 tok · 273 lines 12828 B · 3207 tok · 264 lines (ceiling 3207, headroom 0)

Every other file under skills/ is unchanged.

Tests

Documentation only. skills/objectstack-data/evals/*.json, SKILL.md and the other rules files were grepped for percent, 0-100 and 0–100: no eval or other skill text states a 0–100 scale, so no eval changes (the order's condition did not fire).

Acceptance notes

  • Scope held to the one cell. The scale consequence (FieldSchema.scale counts the decimal places of the displayed percentage-point value, so a fraction-stored field carries scale + 2 stored places) is stated once on the scale describe and not restated in the skill; the cell gives the author the storage answer the scale rule hangs on.
  • The progress type is not a percent. Its row (Progress bar, min, max) is untouched; percentScaleOf answers undefined for it and the renderer reads it as whole points, which is not this card's subject.
  • Not touched: skills/*/references/_index.md (open PR feat(spec): one override declaration for code and Studio, with the .custom pairing helpers and the closed overridable-type list (ADR-0146) #28467), the docs site page (content/docs/data-modeling/fields.mdx Number Types already carries no 0–100 claim for percent), skills/objectstack-ui.
  • Dedupe words for the seat: percent fraction skill, percentScaleOf skill, field-types token ceiling.

Verification

All readings at head 12f6cdc6 (= origin/main 1f1a369b + one commit).

  • Derived gates — node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack → 43 families; every line run with stdin closed after turbo run build --filter='@objectstack/client-react...' under the verify lock, exit codes landed on disk first; dispatch-gates.mjs --repo objectstack-ai/objectstack --ran FILE → ✓ dispatch-gates --ran: 43 derived famil(ies) accounted for — 42 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3). 42 families exit 0, among them check-skills-token-ratchet (+ --self-test), check:skill-identifier-liveness, check:corpus-claim-drift, check:doc-authoring, check:role-word, check:pm-skill-id-lint, check:pm-governed-merges, @objectstack/spec check:skill-examples, check:closing-keyword-parity, check:empty-changeset, check:changeset-no-major.
    • NOT MEASURED: pnpm check:dual-build-cjs-loads, exit 3 (PREREQUISITE NOT MET — it needs every package's dist, a full pnpm build); the diff touches no package entry. CI runs it.
  • Tier — skills/**: claude-fable-5-1 MANDATORY, no one-line exit; this thread runs on that tier.
  • Changed lines — 1 file under skills/ plus the changeset, far under the 3000 human-merge threshold.

Dogfood: not run — the title carries no ! and Clause-② carries no (narrowing) arm.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (No schema touched.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes (CI-owned repo scan; no .ts touched).
  • 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. (Not touched.)
  • 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 by Claude Code

…ates the storage scale

A `percent` field stores a fraction (0-1) unless it declares `max` above 1,
then whole percentage points (`percentScaleOf` in
packages/spec/src/data/percent-scale.ts); seeds, defaults and API values use
that stored scale. The row used to say "Percentage values (0-100)", which had
an author seed `25` on a field without `max` and read back `2500%`. Paid
inside the file's token ceiling by deleting the `Vector (AI Embeddings)`
example, whose one fact the Enhanced Types row already states.

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

Copy link
Copy Markdown
Contributor Author

domain:skills#1 · os-tesla · session_01L9qibzCtJt8Q2Z63fyRNix

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 12f6cdc6bdb8ea740b81b9e8649f1fc1a1145a29
Local-runs: none
Scope: skills/objectstack-data/rules/field-types.md + packages/spec/src/data/percent-scale.ts (percentScaleOf), packages/spec/src/data/field.zod.ts (percent min / max / precision / scale / percentDisplay; vector dimensions; Field.vector), packages/spec/src/data/field-value.zod.ts (vector value shape), ui/packages/fields/src/widgets/PercentField.tsx, ui/packages/fields/src/widgets/number-format.ts (percentCellScale), ui/packages/fields/src/index.tsx (PercentCellRenderer), packages/objectql/src/validation/record-validator.ts (the server's only percentScaleOf reader on the write path), packages/skills/package.json, scripts/check-skills-token-ratchet.mjs (CEILINGS row), .changeset/28489-percent-storage-scale-skill.md

Inputs: card #28489 body and its three comments (6550823056 triage, 6553113511 claim, 6553328866 os-dev-report); PR #28513 body, file list (2 files: .changeset/28489-percent-storage-scale-skill.md +10, skills/objectstack-data/rules/field-types.md +1/-10) and git diff origin/main...refs/review/pr-28513 (merge-base 1f1a369b = origin/main); the check-runs on the head. Head repo is objectstack-ai/objectstack (not a fork); the PR is draft. Nothing was built, run or re-run.

① Derived judgments

The diff publishes no schema and no runtime: it changes one table cell of a published skill, deletes one example block in the same file, and adds a changeset. The public surface it moves is the text of @objectstack/skills (packages/skills, 18.2.0, publishConfig.access: public, whose sync-catalog.mjs ships the repository's skills/** tree). Every judgment the hunks imply, named:

  1. "stored as a fraction (0-1) unless the field declares max above 1 (max: 100)" — right. percentScaleOf (percent-scale.ts L63-66) answers 'whole' iff typeof field.max === 'number' && field.max > 1, else 'fraction'. So max: 1 is a fraction, min alone is a fraction, a negative or absent max is a fraction; the cell's "above 1" draws exactly that line. The spec's scale describe (field.zod.ts L1530) says the same in its own words: "a fraction-stored percent (no max, or a max at or below 1)".
  2. "the field declares max" is a storage statement — right. The edit widget (PercentField.tsx L149-155) computes whole from percentScaleOf({ type: 'percent', max }), displays v * 100 for a fraction and stores storedFraction(n) (decimal shift, never n / 100) unless whole; the read cell (number-format.ts L209-214 percentCellScale → index.tsx L973-974 percentDisplayValue) multiplies a fraction by 100. The widget and the cell both read percentScaleOf by reference, so the cell's rule and the UI's rule cannot drift.
  3. "seeds, defaults, API values use that scale" — right. No server write path rescales a percent: the only non-UI reader of percentScaleOf on the write path is record-validator.ts L1332 / L1384, which uses it solely to derive the decimal-place allowance (scale + 2 for a fraction, scale for whole); the value is stored as sent. A seeded 25 on a field without max is accepted and reads back as 2500%; 0.25 is what "25%" needs. packages/cli generate.ts and runtime action-execution.ts treat percent as a numeric column only.
  4. Config cell left unchanged — right for this card. min, max, precision, percentDisplay are real FieldSchema keys on percent (field.zod.ts L1505, L1597). Observation only: scale also applies to percent (L1530, "Applies to number, percent, rating and slider") and is not listed; that omission predates this PR and is outside its hunk.
  5. Deleting the ### Vector (AI Embeddings) example — right, no rule lost. The spec's vector field carries exactly one type-specific key, dimensions (field.zod.ts L2205, int 1..10000, "e.g., 1536 for OpenAI embeddings"); there is no distance-metric, similarity or index key anywhere in packages/spec/src/data. The deleted block stated type: 'vector', dimensions: 1536 as a flat sibling, and a comment naming ada-002. The Enhanced Types row (head L150) keeps dimensions (flat sibling, e.g. 1536); the invented-key blockquote (head L27-31) keeps the trap (vectorConfig is refused at parse with unrecognized_keys). The ada-002 mention is a model name, not a rule. No eval, SKILL.md or other skill file anchors on the heading or the block (repo grep for "AI Embeddings" hits only docs/spec prose, none in skills/**).
  6. No line re-wrapped — right. The net diff is exactly one changed line (L54) and nine deleted lines (the Vector block, L246-254 on main); every other line of the file is byte-identical to main.
  7. Token ceiling honoured by deletion — right. Blob sizes read from git: 12824 B on main → 12828 B on the head; ceil(bytes / 4) (the gate's convention, TOKEN_CONVENTION L253) is 3206 → 3207; the CEILINGS row for this path is 3207 (L690). Headroom 0, no ratchet breach. Lines 273 → 264 as reported.
  8. Clause-②: no, Metadata-protocol: none — right. No key, value, type, meaning or default an author can write changes; nothing under packages/spec is touched.

② Semver level

.changeset/28489-percent-storage-scale-skill.md names '@objectstack/skills': patch and nothing else. That matches what the diff publishes: a text change inside the one released package that bundles skills/**, and no file in any other released package. Per the Post-Task Checklist, a Metadata-protocol: none change takes patch. The changeset body carries both line-initial declarations (Clause-②: no, Metadata-protocol: none), with no (widening)/(narrowing) arm, identical to the PR body's. Check Changeset: success. The changeset prose states the before/after rule accurately and names the deletion that paid for it.

Clause-②: no

③ Boundary flags

  • pnpm check:dual-build-cjs-loads NOT MEASURED locally (exit 3, prerequisite not met). Answered: immaterial to this diff. The gate loads every package's built CJS entry; the diff touches no package source, entry, package.json or build config, only a Markdown file the @objectstack/skills build copies verbatim. CI hosts the gate in ci.yml Build Core (L3031); that check-run's conclusion is the verdict, and the queue needs it green regardless of this record.
  • open_questions: []. None declared; this review found none to add.
  • out_of_scope_findings: the progress row states no scale. Agreed and correctly left alone: progress is not a percent; percentScaleOf answers undefined for it and the UI's percentCellScale states 'whole' for progress locally (number-format.ts L197). Carrier: none; an observation for the skills lane.
  • The order's eval condition ("an objectstack-data eval only if it states 0–100 for a percent field") did not fire. Verified independently: no percent, 0-100 or 0–100 in skills/objectstack-data/evals/*.json or SKILL.md; the only 0–100 outside field-types.md is references/lint-rules.md L42 (os lint --score, a metadata-quality score, unrelated).
  • The harness's assignee/reviewer MCP step was skipped (relay-only writes). A seat-process note, not a diff matter; the dev reports 0 MCP write tools, consistent with the card's comment history.
  • Check-runs on 12f6cdc6 at review time (gate verdicts). Success: filter, Auto Label, Check Changeset, Check PR Size, PR-body guards, Governed Surface Queue Guard, Dogfood Regression Gate, Lint & Repo Gates, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · CI release & engine gates, Lint · scoped (tooling self-tests, slot lookup & corpus, query-options erasure), Plan Test Core shards, Test Core (2/8), Test UI, Type Check · source gates (the job hosting the published-skills token ratchet step, lint.yml L5465), Type Check · debt ledger. Still in_progress at the last poll: Build Core (hosts dual-build-cjs-loads), Live E2E UI, Test Core (1/8), Type Check · consumer gates, Type Check · workspace. Skipped (path-filtered): UI build/lint/typecheck, docs build, dogfood shards, opt-in harnesses. No failure on the head. This record does not substitute for the running checks; the landing still waits for every check green and, this being Tier H (skills/**), for an authorized APPROVED review.
  • One out-of-scope observation of this review's own: head L15 says a Field.* helper exists for every type "except a few authored as literals (tags, progress, repeater, vector)", yet Field.vector(dimensions, config) exists (field.zod.ts L3602-3604). Present on main before this PR and outside its hunks; carrier: none here. Dedupe words: field-types Field.vector helper literal.

④ Design judgment

The percent cell against the code it cites and the scenario it is accepted against (an AI author seeding, defaulting or POSTing a percent value):

  • Storage rule. Exactly what percentScaleOf and the widget do. Edge probes: max: 1 → fraction (cell: "above 1", correct); min: 0 with no max → fraction (correct); max: 100 → whole (correct); max: 0.5 → fraction (correct); a defaultValue is written at the stored scale with no conversion (correct). A derived: { op: 'ratio' } analytics measure is fraction by definition (analytics-service.ts L3296) and is not a field-level claim, so the cell makes no claim about it. Nothing in the cell can send an author to the wrong magnitude.
  • "then whole points" when scale / precision allow decimals — should-fix. On a whole-percent field with scale: 2 the stored value is 12.34, not an integer: the spec's scale describe says a whole-percent field "stores the displayed number itself and is allowed exactly scale" decimal places, and precision counts digits of that stored value. "whole" here is the spec's PercentScale name ('whole' = percentage points), and the card itself asked for "whole percentage points", so the magnitude the author lands on is right; but an author who has not read percent-scale.ts can read "whole points" as "integers" and round a 12.5 they meant to keep. A phrasing such as "then the percentage points themselves" would be exact; it costs bytes the file no longer has (headroom 0), so paying for it is the lane's call, not a blocker. Related, pre-existing: the Config cell omits scale, the key that decides those decimals.
  • "seeds, defaults, API values use that scale". Holds: the server stores the number as sent on every write path read; the only server-side consumer of the scale is the validator's decimal allowance, which refuses (never rounds) excess places and converts nothing. The dev's and the card's claim that a seeded 25 reads 2500% is what the code does.
  • The deletion. The Vector example carried no rule without another home: dimensions as a flat sibling and its 1536 example live in the Enhanced Types row; the vectorConfig trap lives in the blockquote; the spec has no other vector key (no metric, no index, no normalisation) for the example to have taught. The value shape (number[], field-value.zod.ts L745) was never in the example. Losing the literal spelling of a vector object is a readability cost, not a lost rule, and the row's "flat sibling" already says where the key goes.
  • Re-wrap. None; the hunks are the one row and the one block.
  • Judged on the tree, not the PR body: the body's byte/token/line readings (12824→12828 B, 3206→3207 tok, 273→264 lines, ceiling 3207) are reproduced from the blobs and the CEILINGS row; the body's account of percentScaleOf, the widget and the read cell matches the cited lines.

Findings: must-fix none; should-fix one (the "whole points" wording, above). Observations, no carrier: Config cell lacks scale; L15's "no helper for vector" vs Field.vector.

Implemented-by: claude/project-thread-w041mp
Reviewed-by: session_01L9qibzCtJt8Q2Z63fyRNix

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:skills#1 · os-tesla · session_01L9qibzCtJt8Q2Z63fyRNix

维护者速读

  • 改了什么:objectstack-data 技能字段类型表里 percent 一行,原来写「Percentage values (0-100)」,现在改成实际的存储规则:字段没有声明大于 1 的 max 时存小数(0–1),声明了 max: 100 之类时存百分点本身;种子数据、默认值、API 写入都按同一个比例。
  • 为什么改:原写法会让 AI 在没写 max 的字段里写 25,读回来变成 2500%。规则照抄 percentScaleOf,复审核对了编辑控件、只读单元格和服务端写入路径(服务端不做换算)。
  • 风险与回滚:只改一行技能文本。文件 token 余量是 0,删掉了一段重复的 Vector 示例来抵(dimensions 和 vectorConfig 陷阱在同文件别处都还在)。回滚即 revert。
  • 先看:skills/objectstack-data/rules/field-types.md 第 54 行。
  • 两分钟验证:git grep -n "stored as a fraction" skills/objectstack-data/rules/field-types.md。
  • 需要你做的:改的是 skills/**,属于 Tier H,需要 os-zhuang(或 hotlong)批准后才能合并。契约复审 PASS 记录 6553400762,Head-sha 12f6cdc6。

席位意见:契约复审 PASS,无必须修改项。一条建议修改:「whole points」可能被读成「整数」,而声明了 scale: 2 的字段可以存 12.34。存储量级是对的,文件余量为 0,再改要再删别的内容,席位决定不改,记为已接受偏差。可以批准。

席位核对

ACCEPT · seat domain:skills#1 · head 12f6cdc6 · 2026-11-01T04:32Z

  1. Shape: Fixes #28489, draft, base main, 2 files (field-types.md one row + the deleted Vector example, a patch changeset for @objectstack/skills), +11/−10. No spec or code path.
  2. Ruling: none; triage route 6550823056 as written.
  3. Contract review: PASS 6553400762 at this head. Clause-②: no, Metadata-protocol: none.
  4. Gates / check-runs: dev report 6553328866: 43 derived gates, 42 exit 0, 1 NOT MEASURED locally (check:dual-build-cjs-loads, hosted by CI Build Core). field-types.md 3206 → 3207 of its 3207-token ceiling, paid by deletion. On head 12f6cdc6: 41 check-runs, 28 success, 13 skipped, 0 failed; mergeable_state clean.
  5. Accepted deviations: the review's should-fix ("whole points" can read as integers on a field with scale) is left as is: the magnitude is right, the spec's own name is 'whole', and the file has 0 tokens of headroom. Observations with no carrier: the Config cell omits scale; L15 says vector has no Field.* helper though Field.vector exists (both predate this PR).
  6. Landing: check-governed-merges --pr 28513 reports GOVERNED, Tier H (skills/**). The seat readies the PR and the relay's governed-review leg requests the approvers. No auto-merge is armed by the seat. After an authorized approval, the seat lands it, or the approver enqueues it.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 1, 2026 04:33
@os-zhuang
os-zhuang added this pull request to the merge queue Nov 1, 2026
Merged via the queue into main with commit 35b178b Nov 1, 2026
42 checks passed
@os-zhuang
os-zhuang deleted the claude/project-thread-w041mp branch November 1, 2026 04:48
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 size/s tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

skills(objectstack-data): the percent row says 'Percentage values (0-100)' but a percent field stores a fraction unless it declares max above 1

2 participants