You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 9bcd9be
Browse filesBrowse the repository at this point in the historyBrowse files
docs(spec): state the template dialect's real placeholder contract (#16416)
* docs(spec): state the template dialect's real placeholder contract
`TemplateExpressionInputSchema`'s docblock illustrated the `template`
dialect with `{{var}}` interpolation, which reads as a declared grammar.
It is not one: the schema judges the dialect tag and non-emptiness and
nothing else, and the accepted placeholder spelling belongs to whichever
renderer consumes the slot.
Measured on this tree, the two spellings are not interchangeable
everywhere, so "they are equivalent" would be imprecise in the other
direction:
- `@objectstack/formula`'s registered `templateEngine`, the messaging
renderer, the email plugin and the i18n adapters match `{{var}}` only.
- The `titleFormat` renderers accept both and normalize `{{var}}` down
to `{var}` first, which is why single-brace `titleFormat` values are
legal by construction.
Docblock and `.describe()` prose only. No schema, export or accept-set
movement.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno
* docs(spec): regenerate references and add the patch changeset
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T6HeZvT9wdSJD1ZxJb5Eno
---------
Co-authored-by: Claude <noreply@anthropic.com>
`TemplateExpressionInputSchema` documents what the `template` dialect actually accepts, instead of illustrating it with a grammar it does not judge.
6
+
7
+
The docblock introduced the dialect as "anything with `{{var}}` interpolation". Nothing in the schema enforces that: `TemplateExpressionInputSchema` judges the dialect tag and non-emptiness and nothing else, and the same docblock already said so one clause later. Read as a declaration, it made every single-brace `titleFormat` value in the wild look like it was crossing a gate. There is no gate.
8
+
9
+
The corrected prose states where the placeholder grammar really lives — with the renderer that consumes the slot — and that the two spellings in circulation are not interchangeable everywhere:
10
+
11
+
-`{{var}}` is the canonical form and the only one the registered `template` engine reads (`@objectstack/formula`'s `templateEngine`); the messaging renderer, the email plugin and the i18n adapters match it alone and leave a `{var}` in their output verbatim.
12
+
-`titleFormat` is the exception: its renderers accept `{{var}}` and `{var}` as equivalent, normalizing the double form down to the single one before substituting. Single-brace `titleFormat` values are legal by construction, not a grammar the schema failed to enforce.
13
+
14
+
`titleFormat`'s `.describe()` says the same thing at the slot. Documentation only — the schema's judging logic, its accept-set and its exports are untouched, so nothing an author can write changes meaning. Both strings publish (`dist/*.d.ts` and the bundled `.describe()`), which is why this is a `patch` rather than a `skip-changeset`.
Copy file name to clipboardExpand all lines: content/docs/references/api/metadata.mdx
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -787,7 +787,7 @@ Metadata query with filtering, sorting, and pagination
787
787
|**activityMilestones**|`{ field: string; value: string; summary: string; type?: string }[]`| optional | Declarative semantic activity milestones — emit a templated timeline row when a field transitions into a value, no hook code (ADR-0052 §5b.2). |
788
788
|**nameField**|`string`| optional |[ADR-0079] Canonical primary title field — the stored field used as the record display name (e.g. "name", "title"). |
789
789
|**displayNameField**|`string`| optional |[DEPRECATED → nameField] Field to use as the record display name (e.g., "name", "title"). Accepted as an alias for nameField. |
790
-
|**titleFormat**|`string \| { dialect: 'template'; source?: string; ast?: any; meta?: object }`| optional |[DEPRECATED → nameField (ADR-0079)] Render-only title template; the server cannot return or query it, and an explicit nameField now takes precedence. Migrate a single-field title to nameField, a composite to a formula field designated as nameField. |
790
+
|**titleFormat**|`string \| { dialect: 'template'; source?: string; ast?: any; meta?: object }`| optional |[DEPRECATED → nameField (ADR-0079)] Render-only title template; the server cannot return or query it, and an explicit nameField now takes precedence. Migrate a single-field title to nameField, a composite to a formula field designated as nameField. Placeholders may be written `{{field}}` or `{field}` — the title renderers treat the two as equivalent, normalizing `{{field}}` to `{field}` before substituting; neither spelling is judged at parse time. |
791
791
|**highlightFields**|`string[]`| optional |[ADR-0085] Ordered most-important fields; first entry wins where only one fits. Drives default columns, cards, previews, detail highlight strip. Renamed from compactLayout. |
792
792
|**stageField**|`string \| false`| optional |[ADR-0085] Lifecycle stage field (linear/ordered), or false to declare the status field non-linear and suppress stage heuristics. Absent = heuristic detection allowed. |
793
793
|**editMode**|`Enum<'modal' \| 'page'>`| optional | Edit-interaction intent for records of this object: 'modal' opens the edit form as a dialog over the current view; 'page' navigates to a dedicated full-page edit route. Absent = the renderer picks its own default (objectui defaults to modal). Cross-renderer intent, not pixel styling (family). |
Copy file name to clipboardExpand all lines: content/docs/references/data/object.mdx
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -162,7 +162,7 @@ const result = ApiMethod.parse(data);
162
162
|**activityMilestones**|`{ field: string; value: string; summary: string; type?: string }[]`| optional | Declarative semantic activity milestones — emit a templated timeline row when a field transitions into a value, no hook code (ADR-0052 §5b.2). |
163
163
|**nameField**|`string`| optional |[ADR-0079] Canonical primary title field — the stored field used as the record display name (e.g. "name", "title"). |
164
164
|**displayNameField**|`string`| optional |[DEPRECATED → nameField] Field to use as the record display name (e.g., "name", "title"). Accepted as an alias for nameField. |
165
-
|**titleFormat**|`string \| { dialect: 'template'; source?: string; ast?: any; meta?: object }`| optional |[DEPRECATED → nameField (ADR-0079)] Render-only title template; the server cannot return or query it, and an explicit nameField now takes precedence. Migrate a single-field title to nameField, a composite to a formula field designated as nameField. |
165
+
|**titleFormat**|`string \| { dialect: 'template'; source?: string; ast?: any; meta?: object }`| optional |[DEPRECATED → nameField (ADR-0079)] Render-only title template; the server cannot return or query it, and an explicit nameField now takes precedence. Migrate a single-field title to nameField, a composite to a formula field designated as nameField. Placeholders may be written `{{field}}` or `{field}` — the title renderers treat the two as equivalent, normalizing `{{field}}` to `{field}` before substituting; neither spelling is judged at parse time. |
166
166
|**highlightFields**|`string[]`| optional |[ADR-0085] Ordered most-important fields; first entry wins where only one fits. Drives default columns, cards, previews, detail highlight strip. Renamed from compactLayout. |
167
167
|**stageField**|`string \| false`| optional |[ADR-0085] Lifecycle stage field (linear/ordered), or false to declare the status field non-linear and suppress stage heuristics. Absent = heuristic detection allowed. |
168
168
|**editMode**|`Enum<'modal' \| 'page'>`| optional | Edit-interaction intent for records of this object: 'modal' opens the edit form as a dialog over the current view; 'page' navigates to a dedicated full-page edit route. Absent = the renderer picks its own default (objectui defaults to modal). Cross-renderer intent, not pixel styling (family). |
|`cron`| none at parse time — `croner` fires it at schedule time, on the one wired slot | job schedules |
25
-
|`template`|`{{var}}` interpolation at evaluate time (same variable scope as CEL) | notification subjects/bodies, `titleFormat`, prompt templates |
25
+
|`template`|placeholder interpolation at evaluate time (same variable scope as CEL); canonically `{{var}}`, but the accepted spelling belongs to the renderer, not to this schema — see `TemplateExpressionInputSchema`| notification subjects/bodies, `titleFormat`, prompt templates |
26
26
27
27
No cron syntax is judged at parse time: `croner` evaluates a cron slot only
28
28
when `CronSchedule.expression` is scheduled (`toBoundaryJobSchedule` →
Copy file name to clipboardExpand all lines: content/docs/references/system/migration.mdx
+2-2Lines changed: 2 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -338,7 +338,7 @@ Create a new object
338
338
|**activityMilestones**|`{ field: string; value: string; summary: string; type?: string }[]`| optional | Declarative semantic activity milestones — emit a templated timeline row when a field transitions into a value, no hook code (ADR-0052 §5b.2). |
339
339
|**nameField**|`string`| optional |[ADR-0079] Canonical primary title field — the stored field used as the record display name (e.g. "name", "title"). |
340
340
|**displayNameField**|`string`| optional |[DEPRECATED → nameField] Field to use as the record display name (e.g., "name", "title"). Accepted as an alias for nameField. |
341
-
|**titleFormat**|`string \| { dialect: 'template'; source?: string; ast?: any; meta?: object }`| optional |[DEPRECATED → nameField (ADR-0079)] Render-only title template; the server cannot return or query it, and an explicit nameField now takes precedence. Migrate a single-field title to nameField, a composite to a formula field designated as nameField. |
341
+
|**titleFormat**|`string \| { dialect: 'template'; source?: string; ast?: any; meta?: object }`| optional |[DEPRECATED → nameField (ADR-0079)] Render-only title template; the server cannot return or query it, and an explicit nameField now takes precedence. Migrate a single-field title to nameField, a composite to a formula field designated as nameField. Placeholders may be written `{{field}}` or `{field}` — the title renderers treat the two as equivalent, normalizing `{{field}}` to `{field}` before substituting; neither spelling is judged at parse time. |
342
342
|**highlightFields**|`string[]`| optional |[ADR-0085] Ordered most-important fields; first entry wins where only one fits. Drives default columns, cards, previews, detail highlight strip. Renamed from compactLayout. |
343
343
|**stageField**|`string \| false`| optional |[ADR-0085] Lifecycle stage field (linear/ordered), or false to declare the status field non-linear and suppress stage heuristics. Absent = heuristic detection allowed. |
344
344
|**editMode**|`Enum<'modal' \| 'page'>`| optional | Edit-interaction intent for records of this object: 'modal' opens the edit form as a dialog over the current view; 'page' navigates to a dedicated full-page edit route. Absent = the renderer picks its own default (objectui defaults to modal). Cross-renderer intent, not pixel styling (family). |
@@ -622,7 +622,7 @@ Create a new object
622
622
|**activityMilestones**|`{ field: string; value: string; summary: string; type?: string }[]`| optional | Declarative semantic activity milestones — emit a templated timeline row when a field transitions into a value, no hook code (ADR-0052 §5b.2). |
623
623
|**nameField**|`string`| optional |[ADR-0079] Canonical primary title field — the stored field used as the record display name (e.g. "name", "title"). |
624
624
|**displayNameField**|`string`| optional |[DEPRECATED → nameField] Field to use as the record display name (e.g., "name", "title"). Accepted as an alias for nameField. |
625
-
|**titleFormat**|`string \| { dialect: 'template'; source?: string; ast?: any; meta?: object }`| optional |[DEPRECATED → nameField (ADR-0079)] Render-only title template; the server cannot return or query it, and an explicit nameField now takes precedence. Migrate a single-field title to nameField, a composite to a formula field designated as nameField. |
625
+
|**titleFormat**|`string \| { dialect: 'template'; source?: string; ast?: any; meta?: object }`| optional |[DEPRECATED → nameField (ADR-0079)] Render-only title template; the server cannot return or query it, and an explicit nameField now takes precedence. Migrate a single-field title to nameField, a composite to a formula field designated as nameField. Placeholders may be written `{{field}}` or `{field}` — the title renderers treat the two as equivalent, normalizing `{{field}}` to `{field}` before substituting; neither spelling is judged at parse time. |
626
626
|**highlightFields**|`string[]`| optional |[ADR-0085] Ordered most-important fields; first entry wins where only one fits. Drives default columns, cards, previews, detail highlight strip. Renamed from compactLayout. |
627
627
|**stageField**|`string \| false`| optional |[ADR-0085] Lifecycle stage field (linear/ordered), or false to declare the status field non-linear and suppress stage heuristics. Absent = heuristic detection allowed. |
628
628
|**editMode**|`Enum<'modal' \| 'page'>`| optional | Edit-interaction intent for records of this object: 'modal' opens the edit form as a dialog over the current view; 'page' navigates to a dedicated full-page edit route. Absent = the renderer picks its own default (objectui defaults to modal). Cross-renderer intent, not pixel styling (family). |
* cross-repo back-compat). New metadata should set `nameField`.
2120
2120
*/
2121
2121
displayNameField: z.string().optional().describe('[DEPRECATED → nameField] Field to use as the record display name (e.g., "name", "title"). Accepted as an alias for nameField.'),
2122
-
titleFormat: TemplateExpressionInputSchema.optional().describe('[DEPRECATED → nameField (ADR-0079)] Render-only title template; the server cannot return or query it, and an explicit nameField now takes precedence. Migrate a single-field title to nameField, a composite to a formula field designated as nameField.'),
2122
+
titleFormat: TemplateExpressionInputSchema.optional().describe('[DEPRECATED → nameField (ADR-0079)] Render-only title template; the server cannot return or query it, and an explicit nameField now takes precedence. Migrate a single-field title to nameField, a composite to a formula field designated as nameField. Placeholders may be written {{field}} or {field} — the title renderers treat the two as equivalent, normalizing {{field}} to {field} before substituting; neither spelling is judged at parse time.'),
2123
2123
/**
2124
2124
* [ADR-0085] Semantic role: the object's most important fields, in priority
2125
2125
* order (the first entry wins wherever only one field fits, e.g. child-record
* | `cron` | none at parse time — `croner` fires it at schedule time, on the one wired slot | job schedules |
23
-
* | `template` | `{{var}}` interpolation at evaluate time (same variable scope as CEL) | notification subjects/bodies, `titleFormat`, prompt templates |
23
+
* | `template` | placeholder interpolation at evaluate time (same variable scope as CEL); canonically `{{var}}`, but the accepted spelling belongs to the renderer, not to this schema — see `TemplateExpressionInputSchema` | notification subjects/bodies, `titleFormat`, prompt templates |
24
24
*
25
25
* No cron syntax is judged at parse time: `croner` evaluates a cron slot only
26
26
* when `CronSchedule.expression` is scheduled (`toBoundaryJobSchedule` →
@@ -283,7 +283,27 @@ export type CronExpressionInput = z.input<typeof CronExpressionInputSchema>;
283
283
* naming the fix (`TYPED_EXPRESSION_DIALECT_ONLY.template`), as is a blank
284
284
* string (`TYPED_EXPRESSION_SOURCE_REQUIRED.template`). Use this for
285
285
* notification subjects/bodies, titleFormat, prompt templates — anything with
286
-
* `{{var}}` interpolation. No template syntax is judged at parse time.
286
+
* placeholder interpolation.
287
+
*
288
+
* No template syntax is judged at parse time: this schema judges the dialect
289
+
* tag and non-emptiness and nothing else, so it declares no placeholder
290
+
* grammar and refuses no spelling. Which spelling actually interpolates
291
+
* belongs to the CONSUMING RENDERER, and the two spellings in use are not
292
+
* interchangeable everywhere:
293
+
*
294
+
* - `{{var}}` is the canonical form and the only one the registered `template`
295
+
* engine reads (`@objectstack/formula`'s `templateEngine`); the messaging,
296
+
* email and i18n renderers match it alone and leave a `{var}` in their
297
+
* output verbatim.
298
+
* - `titleFormat` is the exception: its renderers take `{{var}}` and `{var}`
299
+
* as equivalent, normalizing the double form down to the single one before
300
+
* substituting (objectui's `formatTitleTemplate`, and the title renderer in
301
+
* `@objectstack/metadata-protocol`). Both spellings resolve identically
302
+
* there — single-brace `titleFormat` values are legal by construction, not
303
+
* a grammar this schema failed to enforce.
304
+
*
305
+
* So write `{{var}}` unless the slot's renderer is known to normalize, and do
306
+
* not read either spelling as declared, preferred or rejected here.
0 commit comments