Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/29506-title-format-refused-teaching.md
Original file line number Diff line number Diff line change
@@ -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.
17 changes: 16 additions & 1 deletion docs/adr/0079-record-display-name.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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**:
Expand Down
2 changes: 1 addition & 1 deletion skills/objectstack-data/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)) |
Expand Down
6 changes: 3 additions & 3 deletions skills/objectstack-formula/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}`.

Expand Down
Loading