diff --git a/.changeset/29506-title-format-refused-teaching.md b/.changeset/29506-title-format-refused-teaching.md new file mode 100644 index 00000000000..864fa67058d --- /dev/null +++ b/.changeset/29506-title-format-refused-teaching.md @@ -0,0 +1,11 @@ +--- +'@objectstack/skills': patch +--- + +docs(skills): teach that a save and `os validate` refuse `titleFormat` + +Clause-②: no +Metadata-protocol: none + +- `objectstack-data` (object properties table) and `objectstack-formula` (template carriers) no longer call `titleFormat` deprecated: since #29499 the metadata save gate and `os validate` refuse an object that declares it (`title-format-retired`), and it moves to `nameField`. The formula skill drops the sentence that restated the template grammar cell beside it. +- ADR-0079 D3 gains a dated amendment note citing ruling 6574393684 and #29499; the decision is unchanged. diff --git a/docs/adr/0079-record-display-name.md b/docs/adr/0079-record-display-name.md index 0e85e9fa804..f195ac4574f 100644 --- a/docs/adr/0079-record-display-name.md +++ b/docs/adr/0079-record-display-name.md @@ -1,6 +1,6 @@ # ADR-0079: The record display-name contract — `nameField` is the one canonical primary-title pointer -**Status**: Accepted — implemented. **This file is a RETROACTIVE RECONSTRUCTION written 2026-08-08**, not a contemporaneous record; see [Provenance](#provenance-read-this-before-citing-this-file) before you cite it. +**Status**: Accepted — implemented. **This file is a RETROACTIVE RECONSTRUCTION written 2026-08-08**, not a contemporaneous record; see [Provenance](#provenance-read-this-before-citing-this-file) before you cite it. · **Amended** (2026-11-04, [#29506](https://github.com/objectstack-ai/objectstack/issues/29506), ruling of record [`6574393684`](https://github.com/objectstack-ai/objectstack/issues/29007#issuecomment-6574393684) on #29007, letter B) — `title-format-retired` is an error since PR #29499; see the note under D3. - **Decided**: 2026-06-28 (the decision), reconstructed 2026-08-08 (this file) - **Reconstructed by**: #6634 - **Implemented by**: [#2434](https://github.com/objectstack-ai/objectstack/pull/2434) (foundation — `nameField` + resolver), [#2458](https://github.com/objectstack-ai/objectstack/pull/2458) (designate-only provisioning at the registry seam), [#2463](https://github.com/objectstack-ai/objectstack/pull/2463) (the author-time gate) @@ -126,6 +126,18 @@ output, describe marks the old key deprecated."* ### D3 — `titleFormat` is retired in favour of `nameField` +> **Amendment (2026-11-04, #29506).** The diagnostic is no longer advisory. +> Under the ruling of record on #29007 (6574393684, letter B), PR #29499 +> (`3557366c02`) made `title-format-retired` an **error**: the metadata save +> gate (`422 INVALID_METADATA` on an active save, a draft promotion and a +> restore) and `os validate` / `os build` / `os lint` refuse an object that +> declares `titleFormat`, and the fix is `nameField`. The key is refused, not +> removed: the schema still parses it, so a stored object that carries it keeps +> loading and reading until its next save. The decision below stands; the +> "deprecated and lint-warned" and "advisory, not an error" sentences and their +> citation describe the code before #29499, and D8's "never errors" no longer covers this rule +> (`packages/lint/src/validate-record-title.ts`, `validateTitleFormatRetired`). + `titleFormat` is deprecated and lint-warned, and an explicit `nameField` takes precedence over it. It **still parses** — existing metadata keeps loading — so the diagnostic is advisory, not an error: @@ -222,6 +234,9 @@ seam (#2434's "Staged (deliberate)" section, kept as the `TODO(ADR-0079)` that ### D8 — The author-time gate is advisory by design +> **Amended (2026-11-04, #29506).** `title-format-retired` is an error since #29499; see the +> amendment under D3. This section's "never errors" and its quotation now describe `title-unresolvable` alone. + `@objectstack/lint`'s `validate-record-title` reports two warnings — `title-format-retired` (D3) and `title-unresolvable` (`objectTitleCompleteness` returns `status: 'none'`) — and **never errors**: diff --git a/skills/objectstack-data/SKILL.md b/skills/objectstack-data/SKILL.md index b83b0cc61d5..682acf600aa 100644 --- a/skills/objectstack-data/SKILL.md +++ b/skills/objectstack-data/SKILL.md @@ -87,7 +87,7 @@ the publish door rejects it before that: | `datasource` | `'default'` | Target datasource ID for virtualized data | | `nameField` | derived (e.g. `'name'`/`'title'`) | **Canonical** record-title field — the stored field used as the record's display name. Use a single text/email field; a composite title to import, sort or search by is a stored computed field (a non-`formula` field with an `expression`); a text formula is display-only | | `displayNameField` | — | **Deprecated** alias for `nameField` (still honored as a fallback) | -| `titleFormat` | — | **Deprecated, not removed (ADR-0079)** — render-only: the server can't return or query it. Use `nameField` (it wins); a composite title follows the `nameField` row | +| `titleFormat` | — | **Refused (ADR-0079)**: a save and `os validate` refuse it (`title-format-retired`). Move it to `nameField`; a composite title follows the `nameField` row | | `enable` | — | Capability flags (trackHistory, searchable, apiEnabled, etc.) | | `fieldGroups` | — | Ordered list of logical field groups for forms/detail pages (see [Field Groups](./references/examples-objects.md#field-groups-mvp)) | | `lifecycle` | `record` semantics (permanent) | Data retention/rotation/archival contract. **Required for append-only, high-write-rate objects** — a `telemetry`/`transient`/`event`/`audit` class must declare a bounding policy or parsing fails (see [Data Lifecycle & Retention](./rules/lifecycle.md)) | diff --git a/skills/objectstack-formula/SKILL.md b/skills/objectstack-formula/SKILL.md index 76b7d5ea88e..56bfc9df238 100644 --- a/skills/objectstack-formula/SKILL.md +++ b/skills/objectstack-formula/SKILL.md @@ -456,11 +456,11 @@ a bare string (auto-wrapped) or their helper, and read the same variable scope. | Dialect | Helper | Grammar | Carriers | |:---|:---|:---|:---| | `cron` | `` cron`0 6 * * MON` `` | 5- or 6-field cron, or one of `@yearly` `@annually` `@monthly` `@weekly` `@daily` `@hourly` `@reboot` | `Job.schedule.expression` (canonical) | -| `template` | `` tmpl`Hello {{ record.first_name }}` `` | `{{ path }}` or `{{ path \| formatter[:arg] }}` — double braces only, no conditionals; the formatter whitelist is `TEMPLATE_FORMATTERS`, exported from `@objectstack/formula` | `system/email-template` `subject` / `bodyHtml` / `bodyText`, `ai/model-registry` `promptTemplate.system` / `.user`, `Object.titleFormat` (deprecated → `nameField`, ADR-0079) | +| `template` | `` tmpl`Hello {{ record.first_name }}` `` | `{{ path }}` or `{{ path \| formatter[:arg] }}` — double braces only, no conditionals; the formatter whitelist is `TEMPLATE_FORMATTERS`, exported from `@objectstack/formula` | `system/email-template` `subject` / `bodyHtml` / `bodyText`, `ai/model-registry` `promptTemplate.system` / `.user`, `Object.titleFormat` (a save and `os validate` refuse it; move it to `nameField`, ADR-0079) | `shared/expression.zod.ts` declares both surfaces and their carriers. -Missing template paths render as the empty string. Move logic into a CEL field: -a template holds a path and a formatter, nothing else. A `currency` argument is a +Missing template paths render as the empty string. Move logic into a CEL field. +A `currency` argument is a literal code (`currency:EUR`); only an email template may read it from the send's data instead, `{{ amount | currency:@currency_code }}`.