From 3fc850a13160dd4f61afc77546e4d7bf93c3d988 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Nov 2026 15:00:29 +0000 Subject: [PATCH 1/5] docs(skills,spec): fieldPatches example patches options only on a field that declares them HotCRM's crm_account.industry takes its options from the shared industry picklist, so the taught example's options patch was refused at install and by os validate (picklist-option). The skill, the spec description and TSDoc, and the object-extensions docs page now relabel industry only and patch an option of type, which declares its own options. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01J8VVAmsHMwmra6cg78RoEp --- .../30944-skills-field-patches-example-hotcrm.md | 10 ++++++++++ .changeset/30944-spec-field-patches-example-hotcrm.md | 10 ++++++++++ content/docs/data-modeling/object-extensions.mdx | 5 ++++- packages/spec/src/data/object.zod.ts | 9 ++++++--- skills/objectstack-data/SKILL.md | 4 ++-- 5 files changed, 32 insertions(+), 6 deletions(-) create mode 100644 .changeset/30944-skills-field-patches-example-hotcrm.md create mode 100644 .changeset/30944-spec-field-patches-example-hotcrm.md diff --git a/.changeset/30944-skills-field-patches-example-hotcrm.md b/.changeset/30944-skills-field-patches-example-hotcrm.md new file mode 100644 index 00000000000..fe09c5bbbd8 --- /dev/null +++ b/.changeset/30944-skills-field-patches-example-hotcrm.md @@ -0,0 +1,10 @@ +--- +'@objectstack/skills': patch +--- + +docs(skills): the `fieldPatches` example relabels HotCRM's `industry` and patches options only on a field that declares its own + +Clause-②: no +Metadata-protocol: none + +The `objectstack-data` skill's one `fieldPatches` example patched an option of `crm_account.industry`. HotCRM binds that field to the shared `industry` picklist, so a verbatim copy was refused at install and by `os validate` (`picklist-option`). The example now sets `industry`'s `label` only and patches an option of `type`, which declares its own `options` (`customer` → `Client`); the sentence before it names `industry` as the picklist-bound field whose options no patch reaches. No schema, key or behaviour changes. diff --git a/.changeset/30944-spec-field-patches-example-hotcrm.md b/.changeset/30944-spec-field-patches-example-hotcrm.md new file mode 100644 index 00000000000..3584e0f9461 --- /dev/null +++ b/.changeset/30944-spec-field-patches-example-hotcrm.md @@ -0,0 +1,10 @@ +--- +'@objectstack/spec': patch +--- + +docs(spec): the `fieldPatches` description's example no longer patches an option of a picklist-bound field + +Clause-②: no +Metadata-protocol: none + +`ObjectExtensionSchema.fieldPatches`'s description and the `ObjectExtensionSchema` TSDoc taught `industry: { label: 'Sector', options: { technology: { label: 'Tech' } } }`. HotCRM's `crm_account.industry` takes its options from a shared picklist, so the install door and `os validate` refuse that patch (`picklist-option`). The description now says an option of a picklist-bound field is never patchable and only a field declaring its own `options` takes an option patch, and its example sets `industry`'s label and help text and patches `type`'s `customer` option. The generated `references/data/object.mdx` follows. Description text only: no schema, key or behaviour changes. diff --git a/content/docs/data-modeling/object-extensions.mdx b/content/docs/data-modeling/object-extensions.mdx index b892c60b09a..8c8bdd5276e 100644 --- a/content/docs/data-modeling/object-extensions.mdx +++ b/content/docs/data-modeling/object-extensions.mdx @@ -110,7 +110,10 @@ export const AccountWordingExtension = defineObjectExtension({ industry: { label: 'Sector', inlineHelpText: 'The market the account sells into.', - options: { technology: { label: 'Tech' } }, // addressed by the stored value + // no `options` here: `industry` takes its options from a shared picklist + }, + type: { + options: { customer: { label: 'Client' } }, // addressed by the stored value }, }, }); diff --git a/packages/spec/src/data/object.zod.ts b/packages/spec/src/data/object.zod.ts index 0eca424ae49..bd3d4c8adce 100644 --- a/packages/spec/src/data/object.zod.ts +++ b/packages/spec/src/data/object.zod.ts @@ -5112,8 +5112,10 @@ function fieldPatchesDescription(): string { '(`mergeObjectDefinitions`) after the additive merges, on every object read: a tenant\'s own text keeps ' + 'winning, and between two extensions the higher `priority` wins. A patch naming a field or option value ' + 'the target does not have is refused at install and by `os validate`; a field the same extension ' + - 'replaces in `fields` is refused here. Example: `fieldPatches: { industry: { label: \'Sector\', ' + - 'inlineHelpText: \'The market the account sells into\', options: { technology: { label: \'Tech\' } } } }`.' + 'replaces in `fields` is refused here. An option of a field bound to a shared `picklist` is never ' + + 'patchable: its labels are the picklist\'s, so only a field declaring its own `options` takes an option ' + + 'patch. Example: `fieldPatches: { industry: { label: \'Sector\', inlineHelpText: \'The market the ' + + 'account sells into\' }, type: { options: { customer: { label: \'Client\' } } } }`.' ); } @@ -5128,7 +5130,8 @@ function fieldPatchesDescription(): string { * fields: { sales_stage: Field.select([...]) }, * // A base field's display properties only (ADR-0146 D10), applied by the * // registry merge (`mergeObjectDefinitions`) — see `fieldPatches`. - * fieldPatches: { industry: { label: 'Sector', options: { technology: { label: 'Tech' } } } }, + * // `industry` is picklist-bound, so only its label; `type` declares its own options. + * fieldPatches: { industry: { label: 'Sector' }, type: { options: { customer: { label: 'Client' } } } }, * }] * ``` */ diff --git a/skills/objectstack-data/SKILL.md b/skills/objectstack-data/SKILL.md index bc3cdbcfa9d..78ef45defd5 100644 --- a/skills/objectstack-data/SKILL.md +++ b/skills/objectstack-data/SKILL.md @@ -360,8 +360,8 @@ owner's upgrades. A patch carries display keys only values and rules stay the owner's. A key the tenant edited keeps the tenant's text; between two extensions the higher `priority` wins. A patch naming a field or option value the target lacks is refused at install and by `os validate`, and -a `picklist`-bound field's options are never patchable. Example: -`defineObjectExtension({ extend: 'crm_account', fieldPatches: { industry: { label: 'Sector', options: { technology: { label: 'Tech' } } } } })`. +a `picklist`-bound field's options (HotCRM's `industry`) are never patchable. Example: +`defineObjectExtension({ extend: 'crm_account', fieldPatches: { industry: { label: 'Sector' }, type: { options: { customer: { label: 'Client' } } } } })`. **An object an installed package ships** (ADR-0146 D10) is extended from the base's paired `.custom` package (`hotcrm.custom`; **objectstack-platform** From ad4d96f68c906c258ed23f26d06b090251debc07 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Nov 2026 15:14:46 +0000 Subject: [PATCH 2/5] docs(references): regenerate object.mdx from the fieldPatches description Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01J8VVAmsHMwmra6cg78RoEp --- content/docs/references/data/object.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/docs/references/data/object.mdx b/content/docs/references/data/object.mdx index 59ea4867d42..d3cf4b02a58 100644 --- a/content/docs/references/data/object.mdx +++ b/content/docs/references/data/object.mdx @@ -580,7 +580,7 @@ const result = ApiMethod.parse(data); | :--- | :--- | :--- | :--- | | **extend** | `string` | ✅ | Target object name (FQN) to extend | | **fields** | `Record; description?: string; … }>` | optional | Fields to add/override, keyed by field name (lowercase snake_case, the key grammar of an object's `fields`). | -| **fieldPatches** | `Record` | optional | Display patches of the target object's own fields, keyed by field name (lowercase snake_case, the key grammar of an object's `fields`): change a vendor field's display properties (BASE_FIELD_DISPLAY_WHITELIST: `label`, `description`, `inlineHelpText`, `placeholder` on the field, `label` on an option under `options`, keyed by its stored value) without taking the field over, so its type, values and rules stay the vendor's and its upgrades still apply (ADR-0146 D10). Applied by the registry merge (`mergeObjectDefinitions`) after the additive merges, on every object read: a tenant's own text keeps winning, and between two extensions the higher `priority` wins. A patch naming a field or option value the target does not have is refused at install and by `os validate`; a field the same extension replaces in `fields` is refused here. Example: `fieldPatches: { industry: { label: 'Sector', inlineHelpText: 'The market the account sells into', options: { technology: { label: 'Tech' } } } }`. | +| **fieldPatches** | `Record` | optional | Display patches of the target object's own fields, keyed by field name (lowercase snake_case, the key grammar of an object's `fields`): change a vendor field's display properties (BASE_FIELD_DISPLAY_WHITELIST: `label`, `description`, `inlineHelpText`, `placeholder` on the field, `label` on an option under `options`, keyed by its stored value) without taking the field over, so its type, values and rules stay the vendor's and its upgrades still apply (ADR-0146 D10). Applied by the registry merge (`mergeObjectDefinitions`) after the additive merges, on every object read: a tenant's own text keeps winning, and between two extensions the higher `priority` wins. A patch naming a field or option value the target does not have is refused at install and by `os validate`; a field the same extension replaces in `fields` is refused here. An option of a field bound to a shared `picklist` is never patchable: its labels are the picklist's, so only a field declaring its own `options` takes an option patch. Example: `fieldPatches: { industry: { label: 'Sector', inlineHelpText: 'The market the account sells into' }, type: { options: { customer: { label: 'Client' } } } }`. | | **label** | `string` | optional | Override label for the extended object | | **pluralLabel** | `string` | optional | Override plural label for the extended object | | **description** | `string` | optional | Override description for the extended object | From 22db7aee6047702085db7002930a061ef55a421d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Nov 2026 15:36:39 +0000 Subject: [PATCH 3/5] docs(skills): pay the fieldPatches example's bytes by deleting a restatement The .custom paragraph's closing clause restated the relabel paragraph's lead (a base field changes only through fieldPatches), so it goes, and the example's longer options patch fits under the unchanged token ceiling. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01J8VVAmsHMwmra6cg78RoEp --- .changeset/30944-skills-field-patches-example-hotcrm.md | 2 +- skills/objectstack-data/SKILL.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/30944-skills-field-patches-example-hotcrm.md b/.changeset/30944-skills-field-patches-example-hotcrm.md index fe09c5bbbd8..e430c2d1e57 100644 --- a/.changeset/30944-skills-field-patches-example-hotcrm.md +++ b/.changeset/30944-skills-field-patches-example-hotcrm.md @@ -7,4 +7,4 @@ docs(skills): the `fieldPatches` example relabels HotCRM's `industry` and patche Clause-②: no Metadata-protocol: none -The `objectstack-data` skill's one `fieldPatches` example patched an option of `crm_account.industry`. HotCRM binds that field to the shared `industry` picklist, so a verbatim copy was refused at install and by `os validate` (`picklist-option`). The example now sets `industry`'s `label` only and patches an option of `type`, which declares its own `options` (`customer` → `Client`); the sentence before it names `industry` as the picklist-bound field whose options no patch reaches. No schema, key or behaviour changes. +The `objectstack-data` skill's one `fieldPatches` example patched an option of `crm_account.industry`. HotCRM binds that field to the shared `industry` picklist, so a verbatim copy was refused at install and by `os validate` (`picklist-option`). The example now sets `industry`'s `label` only and patches an option of `type`, which declares its own `options` (`customer` → `Client`); the sentence before it names `industry` as the picklist-bound field whose options no patch reaches. The added bytes are paid by deleting a restatement in the same file: the `.custom` paragraph's closing clause "a base field changes only through `fieldPatches`", whose facts the relabel paragraph above it already states. No token ceiling moves. No schema, key or behaviour changes. diff --git a/skills/objectstack-data/SKILL.md b/skills/objectstack-data/SKILL.md index fa39f7c7f48..64855998ebc 100644 --- a/skills/objectstack-data/SKILL.md +++ b/skills/objectstack-data/SKILL.md @@ -360,7 +360,7 @@ owner's upgrades. A patch carries display keys only values and rules stay the owner's. A key the tenant edited keeps the tenant's text; between two extensions the higher `priority` wins. A patch naming a field or option value the target lacks is refused at install and by `os validate`, and -a `picklist`-bound field's options (HotCRM's `industry`) are never patchable. Example: +a `picklist`-bound field's options are never patchable. Example: `defineObjectExtension({ extend: 'crm_account', fieldPatches: { industry: { label: 'Sector' }, type: { options: { customer: { label: 'Client' } } } } })`. **An object an installed package ships** (ADR-0146 D10) is extended from the @@ -369,7 +369,7 @@ base's paired `.custom` package (`hotcrm.custom`; **objectstack-platform** declaration. Every field the pair adds ends in `__c` (`region__c`). The suffix is reserved to the pair, so a `__c` field in any other package, or an unsuffixed field in a `.custom` extension, is refused at save, publish and install and by -`os validate` / `os build`; a base field changes only through `fieldPatches`. +`os validate` / `os build`. Studio's **Add field** on a base object writes the same extension, as an `object_extension` item of the pair. From 7c661dec13587d9909755807fa4caf90b5b2440d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Nov 2026 16:15:59 +0000 Subject: [PATCH 4/5] docs(spec,skills): scope the TSDoc comment to HotCRM's account and trim the changeset claim The ObjectExtensionSchema example extends `contact`, so the comment on why `industry` takes no option patch now names the case it holds for (a picklist-bound `industry`, as on HotCRM's crm_account). The skills changeset no longer says the skill text names `industry` as picklist-bound. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01J8VVAmsHMwmra6cg78RoEp --- .changeset/30944-skills-field-patches-example-hotcrm.md | 2 +- packages/spec/src/data/object.zod.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.changeset/30944-skills-field-patches-example-hotcrm.md b/.changeset/30944-skills-field-patches-example-hotcrm.md index e430c2d1e57..552d38f7b88 100644 --- a/.changeset/30944-skills-field-patches-example-hotcrm.md +++ b/.changeset/30944-skills-field-patches-example-hotcrm.md @@ -7,4 +7,4 @@ docs(skills): the `fieldPatches` example relabels HotCRM's `industry` and patche Clause-②: no Metadata-protocol: none -The `objectstack-data` skill's one `fieldPatches` example patched an option of `crm_account.industry`. HotCRM binds that field to the shared `industry` picklist, so a verbatim copy was refused at install and by `os validate` (`picklist-option`). The example now sets `industry`'s `label` only and patches an option of `type`, which declares its own `options` (`customer` → `Client`); the sentence before it names `industry` as the picklist-bound field whose options no patch reaches. The added bytes are paid by deleting a restatement in the same file: the `.custom` paragraph's closing clause "a base field changes only through `fieldPatches`", whose facts the relabel paragraph above it already states. No token ceiling moves. No schema, key or behaviour changes. +The `objectstack-data` skill's one `fieldPatches` example patched an option of `crm_account.industry`. HotCRM binds that field to the shared `industry` picklist, so a verbatim copy was refused at install and by `os validate` (`picklist-option`). The example now sets `industry`'s `label` only and patches an option of `type`, which declares its own `options` (`customer` → `Client`). The added bytes are paid by deleting a restatement in the same file: the `.custom` paragraph's closing clause "a base field changes only through `fieldPatches`", whose facts the relabel paragraph above it already states. No token ceiling moves. No schema, key or behaviour changes. diff --git a/packages/spec/src/data/object.zod.ts b/packages/spec/src/data/object.zod.ts index bd3d4c8adce..360d361a5bc 100644 --- a/packages/spec/src/data/object.zod.ts +++ b/packages/spec/src/data/object.zod.ts @@ -5130,7 +5130,8 @@ function fieldPatchesDescription(): string { * fields: { sales_stage: Field.select([...]) }, * // A base field's display properties only (ADR-0146 D10), applied by the * // registry merge (`mergeObjectDefinitions`) — see `fieldPatches`. - * // `industry` is picklist-bound, so only its label; `type` declares its own options. + * // Where `industry` is picklist-bound, as on HotCRM's `crm_account`, only its + * // label is patchable; an option patch goes on a field declaring its own options. * fieldPatches: { industry: { label: 'Sector' }, type: { options: { customer: { label: 'Client' } } } }, * }] * ``` From 5a6e8fad3087ae745d71794bc9bc28db0b3153bb Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Nov 2026 16:41:52 +0000 Subject: [PATCH 5/5] docs(spec): a picklist binding blocks only option patches in the TSDoc comment The display keys of a picklist-bound field stay patchable; only its options are out of reach, as the description's own example (inlineHelpText) shows. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01J8VVAmsHMwmra6cg78RoEp --- packages/spec/src/data/object.zod.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/spec/src/data/object.zod.ts b/packages/spec/src/data/object.zod.ts index 360d361a5bc..5a496a8b13f 100644 --- a/packages/spec/src/data/object.zod.ts +++ b/packages/spec/src/data/object.zod.ts @@ -5130,8 +5130,8 @@ function fieldPatchesDescription(): string { * fields: { sales_stage: Field.select([...]) }, * // A base field's display properties only (ADR-0146 D10), applied by the * // registry merge (`mergeObjectDefinitions`) — see `fieldPatches`. - * // Where `industry` is picklist-bound, as on HotCRM's `crm_account`, only its - * // label is patchable; an option patch goes on a field declaring its own options. + * // Where `industry` is picklist-bound, as on HotCRM's `crm_account`, its options are not + * // patchable (its display keys are); an option patch goes on a field declaring its own options. * fieldPatches: { industry: { label: 'Sector' }, type: { options: { customer: { label: 'Client' } } } }, * }] * ```