Skip to content

Commit 00918c8

Browse files
docs(skills): objectstack-data teaches an object extension's fieldPatches — relabel a vendor field without redeclaring it in fields (#28795)
<!-- claude-projects-attribution: {"github_login":"marchtian"} --> _Requested by **田騏** · [project thread](https://claude.ai/code/project/chan_01VmuUhXU9SSUzqJYyXSmYSi?thread=cmsg_01VmuUhXU9SSUzqJYyXSmYSiHXKqxUqDPJKZyNHP7BoqTb)_ Fixes #28730 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么**:对外发布的 `objectstack-data` skill 的「Object Extension Model」一节新增一段,教对象扩展的 `fieldPatches`:改厂商字段的显示文字(label、description、inlineHelpText、placeholder、按存储 `value` 定位的选项 label)用它,而不是在 `fields` 里把字段重新声明一遍(那会整个接管字段,厂商升级不再到达);租户自己改过的文字优先,两个扩展冲突时 `priority` 高者胜,指向目标对象没有的字段或选项值的补丁在安装时和 `os validate` 被拒,picklist 绑定字段的选项不可补丁;附 `crm_account.industry` → "Sector" 一例。另加一条 `@objectstack/skills` patch changeset。 - **为什么改**:PR #28728 让 `fieldPatches` 真正生效后,skill 还只教 `fields` / `priority`,照 skill 写代码的 AI 想把 "Industry" 显示成 "Sector" 只会在 `fields` 里重声明字段、把它接管过去;这是那张 PR 欠下的 `Skills-docs` 后续。 - **风险与回滚**:只改一个 SKILL.md 与一条 changeset;不动 spec、运行时、`content/docs/**`、ratchet 脚本、任何 ceiling。文件 token 数仍在上限内(6111 / 6123):新段落靠删同文件里的重复句付账——搜索一节第二段的「refused loudly」复述(并入第一段)、索引一节的 blockquote(`rules/indexing.md` 已有)、校验类型清单(`rules/validation.md` 已有)。风险在折叠处:读者要多开一个 rules 文件才看到被折叠的细节。回滚 = revert 本 PR。 - **先看**:① `skills/objectstack-data/SKILL.md` 「Object Extension Model」新段(事实逐条对照 `packages/spec/src/data/field-customization.ts` 的 `BASE_FIELD_DISPLAY_WHITELIST` 与 `packages/objectql/src/registry.ts` 的 `patchedField` / `mergeObjectDefinitions`);② 同文件三处删减是否真是复述;③ `.changeset/28730-skill-field-patches.md`。 - **两分钟验证**:`node scripts/check-skills-token-ratchet.mjs` —— 预期含 `skills/objectstack-data/SKILL.md is 6111 tokens (ceiling 6123; headroom 12)`,末行 `57 authored bundle file(s) within their ceilings`。 - **需要你做的**:Tier H(`skills/**`)—— 授权账号 APPROVE 一次,席位落地;无其它动作。 ## What changes `skills/objectstack-data/SKILL.md`, section *Object Extension Model*: one paragraph on `fieldPatches` plus the `crm_account.industry` example from the `.changeset/28565-field-patch-applied.md` release note. Every claim is read off the live code at `b48eeb16c4` (this branch's merge base), not the card's summary: | Claim in the paragraph | Source | |:--|:--| | Redeclaring the field in `fields` takes it over whole | `packages/objectql/src/registry.ts:701-703` (`merged.fields = { ...base.fields, ...extension.fields }`); `content/docs/data-modeling/object-extensions.mdx:96-100` | | Display keys only: `label`, `description`, `inlineHelpText`, `placeholder`; an option's `label` | `packages/spec/src/data/field-customization.ts:75-80` (`BASE_FIELD_DISPLAY_WHITELIST.field` / `.option`); `packages/spec/src/data/object.zod.ts:4958-4967` (the slot's `.describe()`) | | An option is addressed by its stored `value` | `packages/objectql/src/registry.ts:585-587`; `packages/metadata-core/src/extension-field-patch-targets.ts:130-136` | | Type, values and rules stay the owner's | `field-customization.ts:65-68`; `object.zod.ts:4960-4962` | | A key the tenant edited keeps the tenant's text | `registry.ts:576`, `:592` (`tenantPatched` yield), `:731-734` | | Between two extensions the higher `priority` wins | `registry.ts:4202-4203` (ascending sort), `:735-736` (the higher one written last) | | A missing field or option value is refused at install and by `os validate` | `extension-field-patch-targets.ts:95-103` (field), `:126-137` (option); `packages/cloud-connection/src/marketplace-install-local-plugin.ts:2136-2146` (`400 VALIDATION_ERROR`); `packages/cli/src/commands/validate.ts:732-745` | | A `picklist`-bound field's options are never patchable | `extension-field-patch-targets.ts:112-124` (`picklist-option`); `registry.ts:581-582` | `.changeset/28730-skill-field-patches.md`: `@objectstack/skills` patch — the skill ships in that package's `dist` (`packages/skills/package.json` `files[]`, synced from `skills/**` by its build). **Token ratchet — paid by deleting restatement in the same file** (standing ruling, maintainer 2026-10-27: 「A:先削重复,余额按实测提高」 — quoted for the record; no ceiling moves here): | Deleted | Why it was restatement | |:--|:--| | *Search Fields* L171-176: "Both spellings are refused loudly … Each message carries its own prescription." and "Cross-object search paths are rejected by design … the mirror field is the answer." | Repeats the section's own L122-129 ("is **refused**, not silently dropped"; "This is the one prescription — emit it every time"). The one fact only that paragraph carried, the `searchable-field-unknown` lint code, is folded into L125. | | *Index Patterns* blockquote L360-364 (retired `type` / `partial`, unique scope spellings) | Carried by `rules/indexing.md` L26-34 and L63-77; the pointer sentence below it now names the retired keys and the scope words. | | *Validation Patterns* L331-337: the six-bullet type list with one-line glosses | Carried by `rules/validation.md`'s type table (L16-22); the names stay as one line. | Readings (lines by `wc -l`; tokens in the ratchet's own convention, `ceil(bytes/4)` minus the stamp line): | Reading | Before (`origin/main` `b48eeb16c4`) | After (this head) | |:--|:--|:--| | `skills/objectstack-data/SKILL.md` | 472 lines · 6101 tokens (ceiling 6123) | 468 lines · 6111 tokens (headroom 12) | | Package, all `skills/*/SKILL.md` | 4561 lines · 56328 tokens | 4557 lines · 56337 tokens | ## Tests Docs-only: no package test suite reads `skills/**`. The gates that do are in *Verification*. No ablation (no tested behaviour changed). ## Acceptance notes - `rules/indexing.md` L31 carries the `os migrate meta --from 16` prescription the deleted blockquote also quoted, so nothing an author could act on left the skill — only its second copy. - No eval added: the card's Wanted names one paragraph; the `objectstack-data` evals do not exercise object extensions today, and an eval fixture is outside the token ratchet (ruling record 6513306068, letter B). Noted, not filed — an eval for the extension model is a `carrier: #28730`-adjacent follow-up the seat may order. ## Verification Head `7ba013288b`, the branch's only commit; the tree is clean after every step below, so each reading is of this head. Prerequisites: `pnpm install --frozen-lockfile`; `pnpm --filter @objectstack/spec --filter @objectstack/formula build`; then a whole-repo `pnpm build` under `scripts/pm/os-verify-lock.sh` (`Tasks: 117 successful, 117 total`, `VERDICT command-exit 0`, held 13m57s) for the gates that read built output. `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at head: 45 commands (34 derived for the SKILL.md alone, 11 more once the changeset existed — both derivations kept and run). Every command recorded with its exit code captured before any pipe; `--ran` verdict: **45 derived, 45 run, 0 NOT-MEASURED, 0 UNRUN**, exit 0. First pass, before the whole-repo build, four did not pass and were traced to the checkout, not the diff: `check:doc-formula-expressions`, `check:dual-build-cjs-loads`, `check:lean-entry-closure` exit 3 (PREREQUISITE NOT MET, built output missing) and `check:dts-closure` exit 1 (`@objectstack/lint` declaration files missing — its dts build needs `@objectstack/sdui-parser` built first). After `pnpm build`: exit 0 each. The other 41: exit 0 on their first run. Readings worth quoting: - `node scripts/check-skills-token-ratchet.mjs` — `skills/objectstack-data/SKILL.md is 6111 tokens (ceiling 6123; headroom 12)`; last line `57 authored bundle file(s) within their ceilings; 10 generator-owned file(s) and 29 eval fixture(s) measured, not ratcheted`; exit 0. Its `--self-test`: exit 0. - `pnpm check:skill-frame-sync`, `check:skill-compatibility`, `check:skill-identifier-liveness`, `check:pm-skill-id-lint`, `check:role-word`, `check:doc-authoring`, `check:nul-bytes`, `check:pm-governed-merges` — exit 0 each. - `pnpm --filter @objectstack/spec run check:skill-docs` — exit 0. Extra, not derived: `check:skill-refs` — `9 generated files in sync with packages/spec`, exit 0. - Changeset family: `check-adr-0087-registration --base origin/main`, `check-changeset-no-major`, `check-empty-changeset`, `check:changeset-gate-self-tests`, `check:pm-changeset-deadline-census`, `check:published-files`, `check:closed-ceremony-text` — exit 0 each. - Declared to CI, not run locally: `check:skill-examples` (not derived; the paragraph adds no fenced TypeScript block) and `test:repo`. The local-only families `check:dev-prereqs` / `check:skill-frame-freshness` were not run: the diff touches neither `packages/spec` nor a frame file. Dogfood battery: n/a — no `!` in the title and no `(narrowing)` arm. CI check-runs: not read at report time; the PR is opened by the stroke that carries this body, so the report comment records them as `in_progress`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: objectstack-fleet[bot] <332303061+objectstack-fleet[bot]@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6ea8342 commit 00918c8

2 files changed

Lines changed: 32 additions & 24 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): objectstack-data teaches an object extension's `fieldPatches` — relabel a vendor field without redeclaring it in `fields`
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Before.** `ObjectExtensionSchema.fieldPatches` is applied by the registry merge and refused at install and by `os validate` when it names nothing, but the published `objectstack-data` skill's *Object Extension Model* taught only `fields` / `priority`, so an agent that wanted a vendor field to read "Sector" instead of "Industry" redeclared the field in `fields` and took it over.
11+
- **Now.** The section carries one paragraph on `fieldPatches`: display keys only (`BASE_FIELD_DISPLAY_WHITELIST` — `label`, `description`, `inlineHelpText`, `placeholder`, and an option's `label` keyed by its stored `value`), type / values / rules stay the owner's, a tenant's edit wins, the higher `priority` wins between extensions, a patch naming a missing field or option is refused at install and by `os validate`, a `picklist`-bound field's options are not patchable — with the `crm_account.industry` example.
12+
- The file stays under its token ceiling: the new text is paid for by deleting restatement in the same file (the search section's second refusal paragraph, folded into its first; the index blockquote and the validation type list, each already carried by `rules/indexing.md` / `rules/validation.md`). No ceiling moves, no frontmatter, schema, key or behaviour changes.

‎skills/objectstack-data/SKILL.md‎

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,8 @@ explicitly. Views may narrow it, never widen it.
122122
`$search` scans **the queried object's own columns**. A dotted path is never a
123123
search target: unlike `fields` / `sort` / `filters`, the search axis does not
124124
resolve traversal, and `project_id.name` in `searchableFields` (or in a
125-
`$searchFields` override) is **refused**, not silently dropped.
125+
`$searchFields` override) is **refused** (`os validate`
126+
`searchable-field-unknown`; a request, `400 INVALID_FIELD`), not silently dropped.
126127

127128
**This is the one prescription — emit it every time.** To search by a related
128129
record's title, copy that title into a **stored field on this object** and
@@ -168,13 +169,6 @@ fresh as whatever writes it. Cover both write paths:
168169
Rows written by a path that bypasses hooks (bulk import, direct SQL) need a
169170
one-off backfill. See [Lifecycle Hooks](./references/data-hooks.md).
170171

171-
Both spellings are refused loudly: `os validate` reports
172-
`searchable-field-unknown`, and a request naming the dotted path is `400
173-
INVALID_FIELD`. Each message carries its own prescription.
174-
175-
Cross-object search paths are rejected by design, not pending. Do not invent a
176-
per-project convention for this — the mirror field is the answer.
177-
178172
---
179173

180174
→ Moved verbatim to [references/examples-objects.md](./references/examples-objects.md) § Field Groups (MVP).
@@ -328,13 +322,8 @@ See [rules/relationships.md](./rules/relationships.md) for detailed examples.
328322
> validation predicate — so `record.due_date == null` matches an omitted field the
329323
> same as an explicit `null`. (On update, the prior record supplies it.)
330324
331-
The **complete** set of validation types (`ValidationRuleSchema` discriminators):
332-
- `script` — Formula expression (inverted logic)
333-
- `state_machine` — Legal state transitions
334-
- `format` — Regex or built-in format
335-
- `cross_field` — Compare values across fields
336-
- `json_schema` — Validate a JSON field against a JSON Schema
337-
- `conditional` — Apply a nested rule only `when` a predicate holds
325+
The **complete** set of types (`ValidationRuleSchema` discriminators): `script`,
326+
`state_machine`, `format`, `cross_field`, `json_schema`, `conditional`.
338327

339328
> **There is NO `unique` validation type** (removed from the spec).
340329
> Enforce uniqueness — including composite — with a **unique index**, and state
@@ -357,14 +346,10 @@ indexes: [
357346
]
358347
```
359348

360-
> `type` and `partial` were **retired at protocol 17** (a `tsc` and parse error;
361-
> `os migrate meta --from 16` lists the edits). A unique index **states its
362-
> scope** — `'organization'` or `'global'`: on a declared index bare `unique: true`
363-
> is the deprecated spelling of `'global'` and protocol 18 rejects it; on a FIELD
364-
> it means `'organization'` and stays valid.
365-
366-
See [rules/indexing.md](./rules/indexing.md) for composite indexes, unique scope,
367-
and how to build partial / gin / gist indexes at the database layer.
349+
See [rules/indexing.md](./rules/indexing.md) for composite indexes, unique scope
350+
(`'organization'` / `'global'` — on a declared index bare `unique: true` is
351+
refused), the retired `type` / `partial`, and partial / gin / gist indexes at the
352+
database layer.
368353

369354
→ Moved verbatim to [references/examples-objects.md](./references/examples-objects.md) § Lifecycle Hooks (the hooks reference itself is [references/data-hooks.md](./references/data-hooks.md)).
370355

@@ -373,13 +358,24 @@ and how to build partial / gin / gist indexes at the database layer.
373358
## Object Extension Model
374359

375360
To add fields/validations/indexes to an object you do not own, author
376-
`defineObjectExtension({ extend, fields, priority })` and register it on
361+
`defineObjectExtension({ extend, fields, fieldPatches, priority })` and register it on
377362
`defineStack({ objectExtensions: [...] })`. `priority` sets merge order (default
378363
`200`, range `0–999`); an extension can add but never remove. ⛔ Not the same key
379364
as object-level `ownership` (the record-ownership enum
380365
`'user' | 'business_unit' | 'org' | 'none'`). Schema:
381366
`node_modules/@objectstack/spec/src/data/object.zod.ts` (`ObjectExtensionSchema`).
382367

368+
**Relabel a field the owner declares with `fieldPatches` — never by redeclaring
369+
it in `fields`**, which takes the field over whole and cuts it off from the
370+
owner's upgrades. A patch carries display keys only
371+
(`BASE_FIELD_DISPLAY_WHITELIST`): `label`, `description`, `inlineHelpText`,
372+
`placeholder`, and an option's `label` keyed by its stored `value`; the type,
373+
values and rules stay the owner's. A key the tenant edited keeps the tenant's
374+
text; between two extensions the higher `priority` wins. A patch naming a field
375+
or option value the target lacks is refused at install and by `os validate`, and
376+
a `picklist`-bound field's options are never patchable. Example:
377+
`defineObjectExtension({ extend: 'crm_account', fieldPatches: { industry: { label: 'Sector', options: { technology: { label: 'Tech' } } } } })`.
378+
383379
---
384380

385381
## Security & Access Control

0 commit comments

Comments
 (0)