Skip to content

Commit 1f1a369

Browse files
docs(skills): the objectstack-data field-types rule teaches percentDisplay and lookupAutofill, and the fields page teaches lookupAutofill (#28487)
<!-- claude-projects-attribution: {"github_login":"os-tesla"} --> _Requested by **Tesla** · [project thread](https://claude.ai/code/project/chan_01AXsQkLFKRdQCqQ3RTr6Jao?thread=cmsg_01AXsQkLFKRdQCqQ3RTr6Jao1GXHCbqC69qQauk9Qizcus)_ Fixes #28256 Fixes #28246 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:objectstack-data 技能的字段类型表 `percent` 行新增 `percentDisplay`(不填或 `'bar'` 画进度条,`'plain'` 只显示数字,用于比率和概率);`lookup` / `master_detail` 行列出 `lookupAutofill`,表下新增一段教它(选中一条记录后,把它的指定字段带入本记录,默认不覆盖用户已填的值);文档站 `fields.mdx` 的关系字段小节同步教 `lookupAutofill`。 - 为什么改:两个键的规范与控制台都已落地(#28255 的渲染、#28245 的表单填值),但技能和文档都没教,AI 作者写概率字段仍会得到一条进度条,写报价行也不知道选产品可以带出标价。 - 风险与回滚:纯文档改动,不动代码;技能文件的 token 上限余量为零,新增内容以删除三段重复示例支付(`Select with Default`、`Email with Uniqueness`、`Lookup (Many-to-Many)`),并把 `vectorConfig` 提示并入文件顶部「不要臆造键」列表,被删的规则在技能别处仍各有一处;回滚即 revert 本 PR。 - 先看:`skills/objectstack-data/rules/field-types.md`(Relational 表下的 `lookupAutofill` 段、`percent` 行、三处删除),`content/docs/data-modeling/fields.mdx`(新小节 Lookup-pick autofill),`.changeset/28256-data-skill-percent-display-lookup-autofill.md`。 - 两分钟验证:`node scripts/check-skills-token-ratchet.mjs`,预期退出 0,`field-types.md` 读数 3206/3207。 - 需要你做的:受管面(`skills/**`,Tier H)——席位完成契约复审后,由授权账号 Approve,再由席位落地。 ## What changes Two p3 skill-teaching cards folded into one `skills/objectstack-data` PR, one commit per card. **#28256 — the `percent` row teaches `percentDisplay`** (`skills/objectstack-data/rules/field-types.md`, Numbers table). The row's Config column now lists `percentDisplay` — the read-only face: unset or `'bar'` draws a progress bar, `'plain'` the number alone, for a rate or a probability. Read against `packages/spec/src/data/field.zod.ts:1597` (`z.enum(['bar', 'plain']).optional()`, the describe) and the renderer `ui/packages/fields/src/index.tsx:978-988` (`PercentCellRenderer`: only the literal `plain` drops the bar; every read-only host draws this one renderer, the relay sites `ui/packages/plugin-grid/src/objectGridGenerateColumns.tsx:358,622,815` and `ui/packages/plugin-detail/src/fieldEnrichment.ts:230` copy the key). **#28246 — the Relational section teaches `lookupAutofill`.** The `lookup` and `master_detail` rows list the key, and one paragraph under the table teaches it: the pair shape `[{ target, source, overwrite? }]`, single-value `lookup` / `master_detail` only (`field.zod.ts:2647-2667`, the superRefine); a pick copies the picked record's `source` into this object's `target`, in the form and the line-item grid (`ui/packages/fields/src/widgets/LookupField.tsx:1061-1085` `fillDeclaredTargets`, `ui/packages/components/src/renderers/form/form.tsx:3984-3991` `onAutofill`, `ui/packages/fields/src/widgets/GridField.tsx:283-300` `lookupAutofillPatch`); the example `[{ target: 'unit_price', source: 'list_price' }]` is the describe's own (`field.zod.ts:2105`); `target` is a field of this object an author writes — not the lookup, `readonly` or computed — named once (`packages/spec/src/data/object.zod.ts:2255-2300` `refuseDishonestLookupAutofill`); `source` is a declared, non-`internal` field of `reference` the target can hold (`packages/spec/src/stack.zod.ts:4156-4225` `collectLookupAutofillErrors`, the type rule `lookupAutofillTypeMismatch` at 4124); the overwrite rule — a pick fills an empty target or an earlier pick's fill, `overwrite: true` always replaces (`ui/packages/fields/src/widgets/lookupAutofill.ts` `declaredLookupAutofillPatch`, the describe at `field.zod.ts:2106`); client-side only (`field.zod.ts:2107`); in a grid a declared target leaves the column's same-named `autofill` copy (`GridField.tsx:290-294`). **`content/docs/data-modeling/fields.mdx`** (the lookup section only, triage's one-PR exception, comment 6531273599): the relationship property table gains a `lookupAutofill` row, the code block gains a product-lookup example, and a new `#### Lookup-pick autofill` subsection before *Referential integrity* carries the same rules in full — the target rule, the source rule with the type table (same type and multiplicity; a string-valued source into a text target; `number` and `currency` into each other; a relationship only into a relationship over the same object — `stack.zod.ts:4124-4150`), the overwrite rule, client-side only, the grid `autofill` relation, and the bound that a master-detail collection whose relationship and every column are authored loads no child fields and so sees no pairs (`ui/packages/plugin-form/src/MasterDetailForm.tsx:429-435`; named in PR #28322's acceptance notes as the text this card should state). It links the generated [Field reference](/docs/references/data/field), which already carries both keys. **Changeset** `.changeset/28256-data-skill-percent-display-lookup-autofill.md`: `@objectstack/skills` patch, both declaration lines. ### Token ceiling — paid by deletion, never by re-wrap `field-types.md` sat at exactly its `check-skills-token-ratchet` ceiling (3207 = ceil(bytes / 4), zero headroom), so every added byte is paid inside the file. Deleted, each with the home that still carries the rule: - `### Select with Default` (251 bytes) — restated the Selection section's own option example in this file (label, value, color, `default: true`). - `### Email with Uniqueness` (329 bytes) — the unique scope and the field-level `unique: true` reading live in `rules/indexing.md` § *Unique scope — always state it (ADR-0120)*. - `### Lookup (Many-to-Many)` (117 bytes) — the multi-value lookup lives in `SKILL.md` (the Many-to-Many row and the `multiple: true` blockquote); its `max: 10` read as a selection cap, while `FieldSchema.max` is the numeric bound (`field.zod.ts:1532`). - The vector paragraph (132 bytes) — its one rule, that `vectorConfig` is refused at parse, folds into the invented-key list in the blockquote at the top of the file (`theme`, `rows`, `vectorConfig`, `fileAttachmentConfig`); the table row and the example already say `dimensions` is the flat key. No line was re-wrapped to buy room. **Readings** (bytes · ceil(bytes/4) tokens · lines), base `768ec4bf` → head `0984e3e9`: | Surface | Before | After | |:---|:---|:---| | `skills/objectstack-data/rules/field-types.md` | 12826 B · 3207 tok · 305 lines | 12824 B · 3206 tok · 273 lines (ceiling 3207) | | `skills/objectstack-data` bundle, all 15 `.md` files | 163627 B · 40907 tok | 163625 B · 40907 tok | | all `skills/*/SKILL.md` | 226235 B | 226235 B (unchanged) | ## Tests Documentation only — no test file changes. The gates below are the verification; the skill's eval fixtures are unchanged (no eval case was asked) and the eval gate was run. ## Acceptance notes - **Taught only what is on `main`.** Every sentence is read against the spec schema and the renderer cited above; nothing is taught that `main` `768ec4bf` does not do. - **The skill carries the short form, the docs page the full rules.** The fully-authored master-detail bound and the source type table live in `fields.mdx`, not in the skill paragraph, which the token ceiling holds to 639 bytes. - **`percent` row's "Percentage values (0-100)" is untouched.** The row's When-to-Use text predates this card; the storage statement (`percentScaleOf`: a fraction unless `max` is above 1) is not this card's subject. Noted, not filed. - **Not touched:** `skills/objectstack-data/evals/*` (no eval case asked; #28435 owns `hooks-security-seeds-datasources.json`), `skills/*/references/_index.md` (open PR #28467), `skills/objectstack-ui` (its `autofill: false` line is correct as is). - **Dedupe words** for the seat: percentDisplay skill, lookupAutofill skill, field-types token ceiling. ## Verification All readings at head `0984e3e9` (= `origin/main` `768ec4bf` + the two commits). - **Derived gates** — `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` → 67 families (the docs gates included); every line run with stdin closed, exit codes landed on disk first, then `dispatch-gates.mjs --repo objectstack-ai/objectstack --ran FILE` → `✓ dispatch-gates --ran: 67 derived famil(ies) accounted for — 66 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)`. 66 families exit 0, among them `check-skills-token-ratchet` (+ `--self-test`), `check:skill-identifier-liveness`, `check:corpus-claim-drift`, `check:doc-authoring`, `check:docs-spec-enumerations`, `check:doc-anchors`, `check:docs-single-h1`, `check:role-word`, `check:pm-skill-id-lint`, `check:pm-governed-merges`, `@objectstack/spec check:docs` / `check:liveness` / `check:skill-examples` (after `turbo run build --filter='@objectstack/client-react...'` under the verify lock), `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. - **Skill evals** — `pnpm --dir ui run check:skill-eval-tokens --self-test` (29 cases) and `pnpm --dir ui run check:skill-eval-tokens` → exit 0, `Every must_contain token is taught by its own skill bundle` (the `objectstack-data` evals still find every token they grade). - **Tier** — `node scripts/pm/dispatch-gates.mjs --tier` on the two surfaces: `claude-fable-5-1` MANDATORY (`skills/**`, no one-line exit); this thread runs on that tier. - **Changed lines** — 112 (+66 / −46, 3 files), under the 3000 human-merge threshold (read by `--ran`). Dogfood: not run — the title carries no `!` and `Clause-②` carries no `(narrowing)` arm. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (No schema touched.) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes (CI-owned repo scan; no `.ts` touched). - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. (Two cards, folded by the dispatch order; one branch, one PR.) - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. (Not touched.) - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. --- _Generated by [Claude Code](https://claude.ai/code/session_01L9qibvc9vjAiGhyhnLTp34)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f763615 commit 1f1a369

3 files changed

Lines changed: 67 additions & 46 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/skills': patch
3+
---
4+
5+
docs(skills): the objectstack-data field-types rule teaches `percentDisplay` and `lookupAutofill`
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- The `percent` row of the field-type table lists `percentDisplay`: unset or `'bar'` draws the progress bar, `'plain'` prints the number alone, for a rate or a probability.
11+
- The Relational section teaches `lookupAutofill: [{ target, source, overwrite? }]` on a single-value `lookup` / `master_detail`: which targets a pick may fill, which sources it may copy, the overwrite rule, that the fill is client-side only, and how it relates to a line-item grid column's `autofill`.
12+
- Paid inside the file's token ceiling: the `Select with Default`, `Email with Uniqueness` and `Lookup (Many-to-Many)` examples go (each restated a rule the skill carries elsewhere), and the `vectorConfig` trap folds into the invented-key list at the top of the file.

‎content/docs/data-modeling/fields.mdx‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,13 @@ primary_contact: Field.lookup('contact', {
212212
],
213213
}),
214214

215+
// A pick fills sibling fields: the picked product's list_price lands in unit_price
216+
product: Field.lookup('product', {
217+
label: 'Product',
218+
required: true,
219+
lookupAutofill: [{ target: 'unit_price', source: 'list_price' }],
220+
}),
221+
215222
// Master-detail (cascade delete)
216223
project: Field.masterDetail('project', {
217224
label: 'Project',
@@ -236,6 +243,40 @@ order: Field.masterDetail('order', {
236243
| `inlineEdit` | `boolean \| 'grid' \| 'form'` | Render child records inline on the parent create/edit form (`true` = auto-pick, `'grid'`, or `'form'`) |
237244
| `inlineColumns` | `array` | Optional explicit columns for the inline grid |
238245
| `inlineAmountField` | `string` | Optional numeric child field for the inline running total |
246+
| `lookupAutofill` | `{ target, source, overwrite? }[]` | Fields of this object a pick fills from the picked record (single-value `lookup` / `master_detail` only) — see below |
247+
248+
#### Lookup-pick autofill: `lookupAutofill`
249+
250+
A single-value `lookup` / `master_detail` can declare `lookupAutofill: [{ target,
251+
source, overwrite? }]`. When a user picks a record in that field, each pair copies
252+
the picked record's `source` field (a field of the `reference` object) into this
253+
object's `target` field — in the record form, and in a line-item grid column over
254+
the field. The example above fills a quote line's `unit_price` from the picked
255+
product's `list_price`.
256+
257+
- **`target`** is a field of this object that an author writes: not the lookup
258+
itself, not `readonly`, and not computed (`formula`, `summary`, `autonumber`, or
259+
a stored field with an `expression`). Each target is named once per field. The
260+
object's parse refuses anything else.
261+
- **`source`** is a field the referenced object declares and the record carries
262+
(not `internal`), and its value must be one the target can hold: the same type
263+
and multiplicity (a list only into a list),
264+
a string-valued field into a text target, `number` and `currency` into each
265+
other, a relationship only into a relationship over the same object. `os
266+
validate` / `os build` judge the source against the referenced object.
267+
- **Overwrite rule.** By default a pick never replaces a value somebody wrote: the
268+
target is filled only while it is empty or still holds the value an earlier pick
269+
filled in. A pair with `overwrite: true` replaces the target on every pick.
270+
- **Client-side only.** The fill edits the form, and the filled value is saved as an
271+
ordinary write. Nothing on the write path reads the key: an import or an API
272+
write that sets the lookup fills nothing.
273+
- **In a line-item grid**, the column key `autofill` still governs only the grid's
274+
same-named copy; a declared pair applies under its own overwrite rule and takes
275+
its target out of that copy. A master-detail collection whose `relationshipField`
276+
is set and whose every column carries its `type` loads no child fields, so it
277+
sees no pairs and keeps only the same-named copy.
278+
279+
The key's full contract is in the [Field reference](/docs/references/data/field).
239280
240281
#### Referential integrity
241282

‎skills/objectstack-data/rules/field-types.md‎

Lines changed: 14 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ fields: {
2626

2727
> **Config columns list only real `FieldSchema` keys.** Per-type display knobs
2828
> beyond these do **not** exist — an unknown field key is REFUSED at parse
29-
> (`unrecognized_keys`), so don't invent `theme`, `rows`, or
30-
> `fileAttachmentConfig`. Source of truth:
29+
> (`unrecognized_keys`), so don't invent `theme`, `rows`,
30+
> `vectorConfig` or `fileAttachmentConfig`. Source of truth:
3131
> `node_modules/@objectstack/spec/src/data/field.zod.ts`.
3232
3333
## Text & Content
@@ -51,7 +51,7 @@ fields: {
5151
|:-----|:------------|:-------|
5252
| `number` | Generic numeric value | `min`, `max`, `precision`, `scale` |
5353
| `currency` | Monetary amounts | `currencyConfig` (currencyMode, defaultCurrency, currencyField, homeAmount) |
54-
| `percent` | Percentage values (0-100) | `min`, `max`, `precision` |
54+
| `percent` | Percentage values (0-100) | `min`, `max`, `precision`, `percentDisplay` — the read-only face: unset or `'bar'` draws a progress bar, `'plain'` the number alone, for a rate or a probability |
5555

5656
## Date & Time
5757

@@ -90,11 +90,20 @@ options: [
9090

9191
| Type | When to Use | Key Config |
9292
|:-----|:------------|:-----------|
93-
| `lookup` | Reference another object (independent) | `reference`, `lookupFilters`, `multiple`, `deleteBehavior` |
94-
| `master_detail` | Parent–child with lifecycle control | `reference`, `deleteBehavior` (`cascade`/`restrict` — `set_null` is refused) |
93+
| `lookup` | Reference another object (independent) | `reference`, `lookupFilters`, `multiple`, `deleteBehavior`, `lookupAutofill` |
94+
| `master_detail` | Parent–child with lifecycle control | `reference`, `deleteBehavior` (`cascade`/`restrict` — `set_null` is refused), `lookupAutofill` |
9595
| `tree` | Hierarchical self-reference | `reference` — optional; when written, this object's own name (any other object is refused at parse) |
9696
| `user` | Person picker — a lookup specialized to `sys_user` (assignee, watchers). Stored identically to `lookup` | `multiple` (collaborators), `defaultValue: 'current_user'` |
9797

98+
**`lookupAutofill: [{ target, source, overwrite? }]`** (single-value `lookup` /
99+
`master_detail`): a pick copies the picked record's `source` into this object's
100+
`target` (`[{ target: 'unit_price', source: 'list_price' }]` on a product lookup),
101+
in the form and the line-item grid. `target`: a field of this object an author
102+
writes (not the lookup, `readonly` or computed), named once. `source`: a declared,
103+
non-`internal` field of `reference` the target can hold. A pick fills an empty
104+
target or an earlier pick's fill; `overwrite: true` always replaces. Client-side
105+
only; in a grid a declared target leaves the same-named `autofill` copy.
106+
98107
## Media
99108

100109
| Type | When to Use | Config |
@@ -142,19 +151,6 @@ Stored as JSON on the parent row — no separate table / FK:
142151

143152
## Common Field Configurations
144153

145-
### Email with Uniqueness
146-
147-
```typescript
148-
{
149-
type: 'email',
150-
required: true,
151-
// State the scope (ADR-0120): 'organization' = one holder per organization
152-
// (NULL-safe), 'global' = one across the whole installation. Bare `true` at
153-
// field level still means 'organization' and stays valid.
154-
unique: 'organization',
155-
}
156-
```
157-
158154
### Currency
159155

160156
```typescript
@@ -182,20 +178,6 @@ home-currency amount in `field` (a `'dynamic'` `currency`) at the `rateDate`
182178
(`date`/`datetime`) rate, and that rate in `rateField` (`number`), both
183179
`readonly: true`. Cross-currency totals sum `field`.
184180

185-
### Select with Default
186-
187-
```typescript
188-
{
189-
type: 'select',
190-
required: true,
191-
options: [
192-
{ label: 'Low', value: 'low' },
193-
{ label: 'Medium', value: 'medium', default: true },
194-
{ label: 'High', value: 'high', color: '#e74c3c' },
195-
],
196-
}
197-
```
198-
199181
### Lookup (One-to-Many)
200182

201183
```typescript
@@ -209,17 +191,6 @@ home-currency amount in `field` (a `'dynamic'` `currency`) at the `rateDate`
209191
}
210192
```
211193

212-
### Lookup (Many-to-Many)
213-
214-
```typescript
215-
{
216-
type: 'lookup',
217-
reference: 'tag',
218-
multiple: true,
219-
max: 10,
220-
}
221-
```
222-
223194
### Formula
224195

225196
```typescript
@@ -281,9 +252,6 @@ The `format` is literal text interleaved with `{...}` tokens:
281252
}
282253
```
283254
284-
There is **no** `vectorConfig` block — an authored `vectorConfig` is refused at
285-
parse. `dimensions` is the flat field-level key.
286-
287255
## Incorrect vs Correct
288256
289257
### ❌ Incorrect — Autonumber interpolating an optional / adjacent field

0 commit comments

Comments
 (0)