From 7ba013288b8216e40ea3eeaacb9066f691f399b8 Mon Sep 17 00:00:00 2001 From: "objectstack-fleet[bot]" <332303061+objectstack-fleet[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 19:42:48 +0000 Subject: [PATCH 1/2] docs(skills): objectstack-data teaches an object extension's fieldPatches The Object Extension Model section gains one paragraph: relabel a vendor field with `fieldPatches` instead of redeclaring it in `fields`; the display keys it may carry, who wins, what is refused, and the `crm_account.industry` example. Paid for by deleting restatement in the same file (the search section's second refusal paragraph, the index blockquote and the validation type list, each already carried by the skill's rules files). A patch changeset for the published skills package. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_017ihL2g7RyXa8ErM6GAkHyc --- .changeset/28730-skill-field-patches.md | 12 +++++++ skills/objectstack-data/SKILL.md | 42 +++++++++++-------------- 2 files changed, 31 insertions(+), 23 deletions(-) create mode 100644 .changeset/28730-skill-field-patches.md diff --git a/.changeset/28730-skill-field-patches.md b/.changeset/28730-skill-field-patches.md new file mode 100644 index 00000000000..6a62b43c8db --- /dev/null +++ b/.changeset/28730-skill-field-patches.md @@ -0,0 +1,12 @@ +--- +'@objectstack/skills': patch +--- + +docs(skills): objectstack-data teaches an object extension's `fieldPatches` — relabel a vendor field without redeclaring it in `fields` + +Clause-②: no +Metadata-protocol: none + +- **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. +- **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. +- 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. diff --git a/skills/objectstack-data/SKILL.md b/skills/objectstack-data/SKILL.md index fc622adefed..567a009d62e 100644 --- a/skills/objectstack-data/SKILL.md +++ b/skills/objectstack-data/SKILL.md @@ -122,7 +122,8 @@ explicitly. Views may narrow it, never widen it. `$search` scans **the queried object's own columns**. A dotted path is never a search target: unlike `fields` / `sort` / `filters`, the search axis does not resolve traversal, and `project_id.name` in `searchableFields` (or in a -`$searchFields` override) is **refused**, not silently dropped. +`$searchFields` override) is **refused** (`os validate` +`searchable-field-unknown`; a request, `400 INVALID_FIELD`), not silently dropped. **This is the one prescription — emit it every time.** To search by a related 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: Rows written by a path that bypasses hooks (bulk import, direct SQL) need a one-off backfill. See [Lifecycle Hooks](./references/data-hooks.md). -Both spellings are refused loudly: `os validate` reports -`searchable-field-unknown`, and a request naming the dotted path is `400 -INVALID_FIELD`. Each message carries its own prescription. - -Cross-object search paths are rejected by design, not pending. Do not invent a -per-project convention for this — the mirror field is the answer. - --- → 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. > validation predicate — so `record.due_date == null` matches an omitted field the > same as an explicit `null`. (On update, the prior record supplies it.) -The **complete** set of validation types (`ValidationRuleSchema` discriminators): -- `script` — Formula expression (inverted logic) -- `state_machine` — Legal state transitions -- `format` — Regex or built-in format -- `cross_field` — Compare values across fields -- `json_schema` — Validate a JSON field against a JSON Schema -- `conditional` — Apply a nested rule only `when` a predicate holds +The **complete** set of types (`ValidationRuleSchema` discriminators): `script`, +`state_machine`, `format`, `cross_field`, `json_schema`, `conditional`. > **There is NO `unique` validation type** (removed from the spec). > Enforce uniqueness — including composite — with a **unique index**, and state @@ -357,14 +346,10 @@ indexes: [ ] ``` -> `type` and `partial` were **retired at protocol 17** (a `tsc` and parse error; -> `os migrate meta --from 16` lists the edits). A unique index **states its -> scope** — `'organization'` or `'global'`: on a declared index bare `unique: true` -> is the deprecated spelling of `'global'` and protocol 18 rejects it; on a FIELD -> it means `'organization'` and stays valid. - -See [rules/indexing.md](./rules/indexing.md) for composite indexes, unique scope, -and how to build partial / gin / gist indexes at the database layer. +See [rules/indexing.md](./rules/indexing.md) for composite indexes, unique scope +(`'organization'` / `'global'` — on a declared index bare `unique: true` is +refused), the retired `type` / `partial`, and partial / gin / gist indexes at the +database layer. → 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)). @@ -380,6 +365,17 @@ as object-level `ownership` (the record-ownership enum `'user' | 'business_unit' | 'org' | 'none'`). Schema: `node_modules/@objectstack/spec/src/data/object.zod.ts` (`ObjectExtensionSchema`). +**Relabel a field the owner declares with `fieldPatches` — never by redeclaring +it in `fields`**, which takes the field over whole and cuts it off from the +owner's upgrades. A patch carries display keys only +(`BASE_FIELD_DISPLAY_WHITELIST`): `label`, `description`, `inlineHelpText`, +`placeholder`, and an option's `label` keyed by its stored `value`; the type, +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: { tech: { label: 'Technology' } } } } })`. + --- ## Security & Access Control From cbd8528c5b64fcdf46d8a1a84f261bc419c12b5f Mon Sep 17 00:00:00 2001 From: "objectstack-fleet[bot]" <332303061+objectstack-fleet[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 20:32:18 +0000 Subject: [PATCH 2/2] docs(skills): the fieldPatches example keys its option by a stored value the reference CRM declares Contract review should-fixes: the example's option key `tech` named no option of `examples/app-crm` `crm_account.industry` (stored values technology / finance / healthcare / retail / other), so an author copying it was refused at `os validate` and install; it now patches `technology`. The section's spelled signature gains `fieldPatches`. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_017ihL2g7RyXa8ErM6GAkHyc --- skills/objectstack-data/SKILL.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skills/objectstack-data/SKILL.md b/skills/objectstack-data/SKILL.md index 567a009d62e..7ad09575038 100644 --- a/skills/objectstack-data/SKILL.md +++ b/skills/objectstack-data/SKILL.md @@ -358,7 +358,7 @@ database layer. ## Object Extension Model To add fields/validations/indexes to an object you do not own, author -`defineObjectExtension({ extend, fields, priority })` and register it on +`defineObjectExtension({ extend, fields, fieldPatches, priority })` and register it on `defineStack({ objectExtensions: [...] })`. `priority` sets merge order (default `200`, range `0–999`); an extension can add but never remove. ⛔ Not the same key as object-level `ownership` (the record-ownership enum @@ -374,7 +374,7 @@ 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: { tech: { label: 'Technology' } } } } })`. +`defineObjectExtension({ extend: 'crm_account', fieldPatches: { industry: { label: 'Sector', options: { technology: { label: 'Tech' } } } } })`. ---