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 c3c1f10
Browse filesBrowse the repository at this point in the historyBrowse files
fix(spec): `TranslationItemSchema.name` no longer tells authors to name a translation item after its locale tag
7
+
8
+
Clause-②: no
9
+
Metadata-protocol: none
10
+
11
+
The `name` describe said a translation item was "conventionally the locale code (`zh-CN`)", but the metadata save door's item-name grammar refuses `zh-CN` (uppercase and a hyphen), so an author or agent following the describe got a 400 `INVALID_REQUEST` on the first save. The describe now says a translation item is named in snake_case like every metadata item (e.g. `zh_cn`), with the BCP-47 tag in `locale`. The save door and the runtime sync's read-side name fallback for rows stored before `locale` became required are unchanged; the sync's skip warning now points authors at `locale` only.
12
+
13
+
`@objectstack/core`: the authored-translation sync's no-locale skip warning now tells authors to set `locale` to the item's BCP-47 tag (e.g. `zh-CN`), instead of suggesting they name the item after it.
feat(formula)!: a template hole takes one formatter, and the `currency` formatter's argument is a literal ISO 4217 code — both refused at compile, never rendered as formatter syntax or in US dollars
6
+
7
+
Clause-②: no (narrowing) — the in-app template dialect's compile (and so `os validate` and the publish gate for every template slot) refuses a hole with a second formatter (`{{ x | default:0 | number:2 }}`) and a `{{ x | currency:ARG }}` hole whose `ARG` is not three capital letters, both of which it accepted before; `formatValue('currency', value, ARG)` now throws a `RangeError` for such an `ARG`: a path or another argument `Intl` refuses, and an empty one, used to fall back to US dollars, and a lowercase code (`eur`), which `Intl` formatted in that currency, is now refused too. No accept set is widened; the additions are exports only: `judgeHoleFormatter`, `mailHolePattern`, `parseMailHole`, `judgeMailHoleGrammar`, and the types `TemplateHoleGrammarRefusal`, `TemplateHoleGrammarRule`, `ParsedMailHole` and `MailHoleGrammarFinding`.
8
+
Metadata-protocol: narrowed
9
+
10
+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped and no export is removed: a chained formatter and a currency argument that never rendered as written are now refused where the template is compiled. The refusal names the hole and the rule it breaks, so there is nothing for a migration to rewrite. -->
11
+
12
+
**BREAKING** accept-set narrowing in the template dialect, and a new shared mail hole grammar.
13
+
14
+
-**Before, a chained hole.**`{{ record.n | default:0 | number:2 }}` compiled as `default` with the argument `0 | number:2`, and rendered that text.
15
+
-**Before, a currency path.**`{{ record.amount | currency:currency_code }}` compiled; `Intl` refused the argument and the formatter caught that and used US dollars, so the amount printed in a currency nobody wrote.
16
+
-**Now.**`templateEngine.compile` / `evaluate` answer a `parse` error naming the hole and the rule. `formatValue('currency', …)` throws a `RangeError` for an argument that is not a code, before it reads the value. A lowercase code (`currency:eur`), which formatted in euros before, is refused the same way. No argument still formats in US dollars.
17
+
-**Shared grammar.**`judgeHoleFormatter` is the judgement both template dialects use. The mail template grammar is exported once, `mailHolePattern`, `parseMailHole` and `judgeMailHoleGrammar`, and is what `@objectstack/plugin-email`'s renderer, `judgeMailHtmlHoles` and `@objectstack/lint` read; `judgeMailHtmlHoles` no longer keeps a copy of it.
18
+
-**Who sees it.** A template slot that chained formatters, or wrote a path or a lowercase code as the currency argument. No template shipped in this repository does.
19
+
20
+
**The remedy.** Keep one formatter per hole and decide anything more where the data is assembled. Write the currency code itself (`currency:EUR`), or format the amount where the data is assembled and pass the formatted text.
fix(service-messaging): the email and inbox channels grade a `TEMPLATE_INVALID` template failure permanent, like the other template failures
6
+
7
+
Clause-②: no
8
+
Metadata-protocol: none
9
+
10
+
`@objectstack/plugin-email`'s `sendTemplate` and `renderTemplate` now fail with `TEMPLATE_INVALID` for a stored template whose hole the mail grammar refuses (a second formatter, or a `currency` argument that is not a literal ISO 4217 code). Retrying cannot succeed until the template is republished, so `classifyError` on both channels grades it `permanent`, as it already grades `TEMPLATE_NOT_FOUND`, `TEMPLATE_INACTIVE`, `MISSING_VARIABLES` and `TEMPLATE_UNSUPPORTED`: the delivery goes dead at once with the code on its row, instead of spending the whole retry schedule first. Every other failure keeps its grade.
feat(plugin-email)!: a mail template hole that chains a second formatter, or whose `currency` argument is not a literal ISO 4217 code, fails the render with `TEMPLATE_INVALID` instead of printing formatter syntax or US dollars
6
+
7
+
Clause-②: no (narrowing) — `sendTemplate` and `renderTemplate` refuse a stored template whose `subject`, `body_text` or `body_html` writes a hole with a second formatter, or `{{ x | currency:ARG }}` with an `ARG` that is not three capital letters, both of which rendered before. No accept set is widened, and the package adds no export.
8
+
Metadata-protocol: narrowed
9
+
10
+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped and no export changes: the renderer refuses a hole the authoring rule refuses, for a template that reached it without being judged. There is nothing for a migration to rewrite. -->
11
+
12
+
**BREAKING** a render that printed a wrong currency now fails.
13
+
14
+
-**Before, a chained hole.**`{{ actual_amount | default:0 | number:2 }}` rendered `0 | number:2` for an empty value and `71100` otherwise, in every face.
15
+
-**Before, a currency path.**`{{ planned_amount | currency:currency_code }}` with `currency_code = 'eur'` rendered `US$1,234.50` (zh-CN), `$1,234.50` (en-US) and `1 234,50 $US` (fr-FR), in every face.
16
+
-**Now.** The render throws `TEMPLATE_INVALID: email template NAME (locale=LOCALE): …`, naming the hole and the rule it breaks (one formatter per hole, or the literal-only currency code); `sendTemplate` sends nothing. The authoring doors refuse the same hole first (`email-template-hole-grammar`), so this is reached only by a template stored before that rule, until its next publish.
17
+
-**The grammar.** The renderer parses holes with `@objectstack/formula`'s `parseMailHole`, the grammar the authoring rule and the placement judge read; it keeps no copy of its own.
18
+
19
+
**The remedy.** Republish the template with one formatter per hole and a literal currency code (`currency:EUR`), or format the amount in the caller and pass the formatted text as its own variable.
feat(lint)!: a new gating authoring rule, `email-template-hole-grammar`, refuses an email template hole that chains a second formatter or whose `currency` argument is not a literal ISO 4217 code
6
+
7
+
Clause-②: no (narrowing) — `os validate`, `os build`, `os lint` and the metadata publish door (`422 INVALID_METADATA`) refuse an `email_template` whose `subject`, `bodyText` or `bodyHtml` writes a hole with a second formatter (`{{ x | default:0 | number:2 }}`) or `{{ x | currency:ARG }}` with an `ARG` that is not three capital letters, both of which they accepted before. No accept set is widened; the additions are exports only: `validateEmailTemplateHoleGrammar`, `EMAIL_TEMPLATE_HOLE_GRAMMAR`, the type `EmailTemplateHoleGrammarFinding`, and the rule id `email-template-hole-grammar`.
8
+
Metadata-protocol: narrowed
9
+
10
+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped: a hole that never rendered as written (the renderer printed formatter syntax, or the amount in US dollars) is now refused where it is authored. The refusal names the hole and the rule it breaks, so there is no key for a migration to rewrite and no tombstone. -->
11
+
12
+
**BREAKING** accept-set narrowing at every authoring door for `email_template`.
13
+
14
+
-**Before, a chained hole.**`{{ actual_amount | default:0 | number:2 }}` passed every gate. The second `|` was read as part of `default`'s argument, so the hole printed the text `0 | number:2` for an empty value and the bare `71100` otherwise; `{{ x | upper | lower }}` stayed on the page as written.
15
+
-**Before, a currency path.**`{{ planned_amount | currency:currency_code }}` passed every gate. The argument is not read as a path, so the amount rendered in US dollars whatever the record's currency (`$1,234.50` for a record in euros).
16
+
-**Now.** The rule refuses both at `error`, naming the hole, the face and the template. A hole takes one formatter; anything more is decided in the caller that supplies the data. The currency argument is a literal code in three capital letters (`currency:EUR`), never a path. A lowercase or empty argument is refused the same way. `currency` with no argument is unchanged (US dollars).
17
+
-**Which holes.** The grammar is `@objectstack/formula`'s `judgeMailHoleGrammar`, the one the mail renderer parses with; all three faces are judged.
18
+
-**Who sees it.** A template that chained formatters, or wrote a path or a lowercase code as the currency argument. No template shipped in this repository does.
19
+
20
+
**The remedy.** Keep one formatter per hole and pass any further decided value (a default, a second format) as its own variable. Write the currency code itself (`currency:EUR`). To show each record's own currency, format the amount in the caller that supplies the data and pass the formatted text as its own variable.
fix(metadata-protocol): a seed that stores many `seedFile` values no longer starves sign-ins and other requests while it runs
6
+
7
+
Clause-②: no
8
+
Metadata-protocol: none
9
+
10
+
A seed that stores many `seedFile(path)` values no longer starves the requests that arrive while it runs. On a synchronous driver (better-sqlite3, the wasm fallback) every step of a seed file's work — the `sys_file` row, the bytes, the claim that commits it — resolves as a microtask, so the claims the seed loader settled after each 200-row insert batch ran as one unbroken chain and a sign-in during a background boot seed waited seconds unread behind it. The connection pool was never the bottleneck; the event loop was. The loader now hands the event loop one turn after each row's files are stored and after each row's claim, and writes a dataset that carries seed files in insert batches of 20 rows instead of 200. Datasets without seed files are unchanged.
fix(core): an `api` action's target is interpolated before a registered `api` handler sends it
6
+
7
+
Clause-②: no
8
+
Metadata-protocol: none
9
+
10
+
An `api` action whose `target` carries `${ctx.recordId}` (or any other `${ctx.X}` / `${param.X}` token) went out on the wire with the literal placeholder (`/api/v1/data/gapdata_item/$%7Bctx.recordId%7D`) when a host had registered its own `api` handler, as the Console does, so a record-level PATCH or POST hit a record that does not exist. A `url` action with the same token already resolved it. `ActionRunner` now runs its one target substitution on an `api` target before handing the action to a registered `api` handler, with the same scopes and the same per-value `encodeURIComponent` a `url` target gets. The built-in executor already did this and is unchanged; a registered handler for any other type still receives its target as authored.
fix(app-shell): editing an action in Studio no longer stops the object from saving
7
+
8
+
Clause-②: no
9
+
Metadata-protocol: none
10
+
11
+
Three edits in Studio's action editor (Data › an object › Actions) wrote an action the spec refuses, and from then on every save of the object was refused until a reload, with nothing on screen naming the action:
12
+
13
+
-**Expose to AI agents** wrote a flat `aiExposed` (and its description a flat `aiDescription`). It now writes the spec's nested block, `ai: { exposed, description }`, and reads it back; unticking leaves a draft the spec accepts. A draft that still carries the flat keys is cleaned by the control's next write. The action preview's AI pill reads the same block.
14
+
-**Switching the Type** kept keys the new type refuses (a script `body` on an API action, `onSuccess`, `outcomeMessages`, `undoable: true`). The switch now clears exactly those, and keeps everything the new type accepts.
15
+
-**Body Shape** was a free-text box, so the documented `{ "wrap": "payload" }` was written as a string the spec refuses. It is now a Flat / Wrap control with a key input; a stored `{ wrap }` reads back as Wrap with its key, and Wrap is written only once it has a key.
16
+
17
+
The Data pillar now holds an action that is only unfinished, as it already held a field with no options: after "Run a script" before a source is typed, a Type switch before its endpoint, address or target is filled, or "Expose to AI agents" before a long enough description, no save goes out, and a neutral line names the input on the action until it is filled. A save the server still refuses is shown at the action: the strip names it (and the input, when the editor has one for that key), and "Show me" opens it on the Actions view. The next accepted save clears the strip, with no reload.
18
+
19
+
New `studio` namespace strings (en, zh): the Body Shape control, and the refusal and held lines for an action.
0 commit comments