Skip to content

Commit caf6068

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-11509-element-binding-retirement
Merges origin/main 1b99388 through scripts/pm/os-regen-merge.sh. - packages/spec/dropped-refinements.baseline.json (hand-kept ledger, no generator): main's rows plus this branch's three removals; the measured totals re-counted on the merged entries, 223 schemas / 693 sites (branch 223/685 = 226/688 - 3/3; main 226/696 = 226/688 + 0/8). - docs/protocol-upgrade-guide.md: main's hand-written pointer stub taken verbatim; the branch's generated text is dropped (the os-regen driver had kept the branch side). Claude-Session: https://claude.ai/code/session_01VZqqwTj2wsihZEbfT6yyYN Co-authored-by: Claude <noreply@anthropic.com>
2 parents 5faf122 + 1b99388 commit caf6068

108 files changed

Lines changed: 6113 additions & 2048 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
docs(spec): the protocol upgrade guide has a public address, one docs-site page per protocol major
6+
7+
Clause-②: no
8+
9+
The guide is published on the docs site, one page per protocol major, at `https://objectstack.ai/docs/protocol-upgrade/N`; `https://objectstack.ai/docs/protocol-upgrade` lists them all. A link to one major keeps resolving after the next major opens. The site builds from `main`, so each page says whether its major is released, in prerelease, or not released yet.
10+
11+
`docs/protocol-upgrade-guide.md` in the repository, which earlier READMEs and changelog entries point at, is now a short pointer naming that address.
12+
13+
What a release shipped is unchanged: `protocol-upgrade-guide.md` inside the package and attached to its GitHub Release. Its only change is the header comment, which now names the command that writes the file.
14+
15+
Nothing changes for an author, and nothing is renamed or removed.

‎.changeset/22507-flow-options-toasts-translation.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@ Clause-②: yes (narrowing)
1919
- **Where it is read.** `translateFlow` (`@objectstack/spec/system`) overlays both, and `resolveFlowScreenFieldOptions(bundle, flowName, nodeId, field)` resolves a served screen field's options. The console's flow runner reads them in a following release; until then a translation is stored and the authored text is drawn.
2020
- **`objectstack validate`** warns on an option key that names no declared option of the field, or names it by its label (`translation-option-key-unknown`).
2121
- **`os i18n extract` and the coverage gate** now scaffold and demand each screen field's option labels, the failure toast of every flow that declares one, and the completion toast of a flow with a screen, in the `flow` bucket (`i18n/missing-flow`). A project that declares `supportedLocales` gets one missing-key issue per locale for each until it translates them.
22-
- **Still not keyed: a screen's `description`.** It is a `{{ }}` template the server renders per run, so its translation has to be chosen before that render; a translated `description` is still refused by the schema with that reason.
22+
- **A screen's `description`** is keyed too, in the same release, and translated by the engine rather than the console: see the entry for `flows.<flow>.screens.<node_id>.description`.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@objectstack/service-automation": minor
3+
---
4+
5+
feat(automation): a screen's heading and body text render in the run's locale
6+
7+
Clause-②: yes (widening)
8+
9+
- **What is new.** The `screen` executor serves a screen's `title` and `description` in the language of the person who started the run. For each, it asks the engine for the translated template at `flows.<flow>.screens.<node_id>.title` / `.description` through the `i18n` service, in `AutomationContext.locale`, and fills the `{{ }}` holes of that template. This is the same pick a refusing `end` node's message already makes, through the same channel (`setI18nServiceSource`).
10+
- **When the authored text renders.** With no locale on the run (a record-change, schedule or webhook trigger), no `i18n` service, no entry or an empty one, or a translation that does not compile, the authored template renders as before. A translation that does not compile also logs a `warn` naming the key. A resumed leg renders in the locale the run was started in.
11+
- **The heading key covers the node label.** A screen with no `config.title` shows its node label, and a translated `title` replaces whichever heading the screen shows. The body text is translated only where the screen authors one.
12+
- **`AutomationEngine.renderFlowTextSlot(slot, variables, context)`** is the one translated-template pick, now shared by the refusing `end` node and the `screen` executor. Its argument type, `FlowTextSlotTranslation`, is exported.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@objectstack/cli": minor
3+
---
4+
5+
feat(cli): `os i18n extract` and the coverage gate scaffold and demand a screen's `description`
6+
7+
Clause-②: yes (widening)
8+
9+
- `os i18n extract` now writes `flows.<flow>.screens.<node_id>.description` into the skeleton for every screen that authors body text, seeded with the authored `{{ }}` template so a translator keeps its holes.
10+
- The coverage gate demands it in the `flow` bucket (`i18n/missing-flow`). A project that declares `supportedLocales` gets one missing-key issue per locale for each screen that authors a `description`, until it translates it. A screen with no `description` is never asked for one.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@objectstack/lint": minor
3+
---
4+
5+
feat(lint): `objectstack validate` judges a translated screen `description`
6+
7+
Clause-②: yes (widening)
8+
9+
- A translation of `flows.<flow>.screens.<node_id>.description` over a screen that declares no `config.description` is reported as `translation-target-unknown` (error). The engine translates body text only where the screen authors one, so nothing would read the key. Declare the screen's `description`, or drop the key.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(i18n): a screen's body text has a translation key, and a screen's heading and body text are translated by the engine in the run's locale (`flows.<flow>.screens.<node_id>.description`)
6+
7+
Clause-②: yes (widening)
8+
9+
- **What is new.** A fully translated screen flow no longer has to keep a screen's body text in the source language. Translate it under `flows.<flow_name>.screens.<node_id>.description`, beside the heading's `flows.<flow_name>.screens.<node_id>.title`, whose address does not change.
10+
- **A translation is a template.** A screen's `title` and `description` are `{{ }}` templates rendered for each run, so a translation keeps the holes of the text it translates: `description: '任务“{{ subject }}”创建成功!'` for `'Task "{{ subject }}" created successfully!'`. The schema judges a translated `description` with the same text-slot rule the source text gets, so a single-brace `{subject}` is refused with the `{{ subject }}` spelling. An empty string is accepted: it is the untranslated slot `os i18n extract` writes.
11+
- **The body text translates only where the screen authors one.** A bundle cannot add body text to a screen that declares no `config.description`.
12+
- **Who translates it.** The automation engine, not the console. It picks the translated template in the locale of the person who started the run and then fills the holes, so the screen a paused run serves is already translated. `ScreenSpec.title` and `ScreenSpec.description` now document that a client draws them as served and never overlays a translation on them.
13+
- **`flowScreenCopyKey(flowName, nodeId, key)`** (`@objectstack/spec/system`) spells the key the engine reads, beside `flowRefusalMessageKey`. `FLOW_SCREEN_COPY_KEYS` is now `['title', 'description']`, and `translateFlow` overlays a translated `description` on a flow document where the screen authors one.
14+
- **`resolveFlowScreenTitle` is not for a served screen.** A served `ScreenSpec.title` is already translated and filled, so overlaying the resolver's answer on it would draw the template's holes as literal text. The console stops calling it in a following release, and the function retires after that.
15+
- A translated screen `title` is not judged by the text-slot rule yet, so a single-brace token in it is drawn as literal text, as it was before.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
A locale pack's page `label` no longer replaces the record-derived heading on a record page whose header authors no title
6+
7+
Clause-②: no
8+
9+
On a record page, a `page:header` with no `title` is the sanctioned spelling for "the renderer derives the heading from the record" (`PageHeaderProps.title`). Studio's record-page seed writes exactly that header. `translatePage` used to fill the absent title with the pack's `pages.<name>.label`, so the record's name was replaced by the page's static label. This happened in every locale whose pack carries the label, including the `en` skeleton that `os i18n extract` writes. Measured at `GET /api/v1/meta/page/:name` before this change: the header gained `title: "客户记录页"` in zh-CN, and `title: "Account Record"` in en with the extracted skeleton.
10+
11+
The label now does not fill an untitled root `page:header` when all three of these hold:
12+
13+
- the page's `type` is `'record'` as served (a document with no `type` is not a record page);
14+
- the page's `object` is a non-empty string;
15+
- the header keeps its record chrome (`properties.recordChrome` is not `false`).
16+
17+
Everywhere else the label still fills an absent header title, as before. An authored header title follows the existing order, including one that restates the page's `label`. A pack's `pages.<name>.title` and `subtitle` apply as before.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
'@objectstack/plugin-audit': minor
3+
'@objectstack/spec': minor
4+
---
5+
6+
`sys_comment_reaction`: an emoji reaction to a comment is now the reacting user's own record, one row per comment, emoji and user
7+
8+
Clause-②: yes (widening)
9+
10+
- **The object.** `@objectstack/plugin-audit` registers `sys_comment_reaction` beside `sys_comment`. A row holds `comment_id` (the reacted comment's id), `emoji` (the character sequence, at most 64 characters) and `user_id` (the user who reacted). The data API offers `get`, `list`, `create`, `delete` and `bulk` on it, so `createMany`, `deleteMany` and `/batch` creates and deletes work too. It offers no `update`, so `updateMany` stays refused: a reaction is removed and made again, not edited. Every row of a batch meets the same rules as a single write.
11+
- **Who may read it.** A reaction is readable exactly when its comment is. Every read of the object (`find`, `findOne`, `count`, `aggregate`, and so the list total) is narrowed to the rows whose comment the caller can read, decided by a read of those comments under the caller's own context. A reaction whose comment was deleted is read by nobody.
12+
- **Who may write it.** A caller reacts only to a comment it can read; on any other comment, including one that does not exist, the create is refused with `403 RECORD_NOT_ACCESSIBLE`. `user_id` is set from the signed-in user, and a value the client sends is replaced; a session with no user is refused. A member deletes their own reaction, and not another member's, by the platform's own-record rule (`owner_only_deletes`). The author-only edit rule on `sys_comment` is unchanged: reacting no longer writes the comment's row.
13+
- **One row per reaction.** `(comment_id, emoji, user_id)` is unique within an organization. Two members reacting at once write two rows. The same reaction a second time is refused, and the data API answers `409 UNIQUE_VIOLATION`.
14+
- **Reading reactions for a feed.** Read the reactions of the comments on screen in one request, `comment_id` `$in` those ids, and group the rows by comment and emoji. Nothing is stored or computed on the comment row.
15+
- **Granting it.** The object's create, read and delete bits come from a permission set, as `sys_comment`'s do. The `everyone`-bound baseline (`member_default`) grants nothing on it.
16+
- **`@objectstack/spec`: the platform object registry lists it.** `PLATFORM_OBJECTS_BY_PACKAGE['plugin-audit']` now includes `sys_comment_reaction`, and so do `PLATFORM_PROVIDED_OBJECT_NAMES` and `isPlatformProvidedObjectName`. A stack that references the object is no longer reported as naming an unknown platform object. This is the only change in the package.
17+
- **`sys_comment.reactions` is unchanged in this release.** The column is still declared and still written by clients that write it. It is retired separately, and reactions stored in it are not converted into records.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/platform-objects': patch
4+
---
5+
6+
feat(spec)!: `PageSchema` refuses a page that authors both `slots.details` and `slots.tabs`; the `sys_user` record page carries its details grid as its first tab
7+
8+
Clause-②: yes (narrowing)
9+
10+
<!-- adr-0087: registered page-slots-details-beside-tabs-refused -->
11+
12+
**BREAKING**: an accept-set narrowing on a published authoring surface, shipped as `minor` under the launch-window convention for accept-set narrowings.
13+
14+
**Why.** The slot map read as seven independent slots, and two of them are not. On a slotted record page `details` replaces the body of the Details tab, that tab lives inside the synthesized `page:tabs` strip, and `tabs` replaces the whole strip. The console's default-page synthesizer therefore reads `tabs` and never reads `details` when both are authored. A page authoring both passed `PageSchema.parse`, `objectstack validate` and the metadata save door, and its details body (its `sections`, its `hideFields`) silently never applied. The platform's own `sys_user` record page was one: its Identity and Audit sections never showed, and the ban columns it hides were never hidden by it.
15+
16+
**What is refused.** A page whose `slots` map carries both a `details` and a `tabs` key, at `slots.details`, with a `custom` issue that names both slots and the fix. Either slot may be one component or an array; an empty `details: []` is refused like a full one, and the page's `kind` does not matter. That covers `PageSchema`, `definePage`, `defineStack` (`STACK_SCHEMA_INVALID`, 422), `os validate` and the metadata save door (`422 INVALID_METADATA`). The check is exported as `checkPageSlotPair`, for a mirror built from `PageSchema.shape` to re-attach.
17+
18+
**What is still accepted, byte for byte.** A page authoring only `details`, only `tabs`, or neither.
19+
20+
## FROM → TO
21+
22+
| you wrote | write instead |
23+
|:--|:--|
24+
| `slots: { details: { type: 'record:details', … }, tabs: { type: 'page:tabs', properties: { items: [ … ] } } }` | `slots: { tabs: { type: 'page:tabs', properties: { items: [ { label: 'Details', children: [ { type: 'record:details', … } ] }, … ] } } }` |
25+
| both slots, wanting the synthesized tabs with your details body | `slots: { details: { type: 'record:details', … } }`, with no `tabs` slot |
26+
27+
**The one-line fix: move the `record:details` component into the `tabs` items (the first item's `children`, by convention) and delete `slots.details`.** Its `sections` and `hideFields` move unchanged. The page then shows the details body it always declared, which is a visible change on every page that authored the pair.
28+
29+
**Who is affected, measured.** In this repository, only the `sys_user` record page (`sys_user_detail` in `@objectstack/platform-objects`) authored both slots; no example app page does. hotcrm's one slotted page authors `header` and `discussion` only. Deployed metadata was not measured. A page row already stored with the pair is replayed unchanged at load, so it renders as before (the authored tabs, without the details body); its read diagnostics name the pair, and saving it again is refused until the details body moves.
30+
31+
### The kit
32+
33+
- **The refusal.** `checkPageSlotPair`, attached to `PageSchema` by identifier beside its three other object-level checks. No new error code.
34+
- **The producer.** `sys_user_detail` moves its `record:details`, with its `sections` and `hideFields` unchanged, in as the first `tabs` item.
35+
- **The ledger.** The D3 semantic entry `page-slots-details-beside-tabs-refused` (protocol 18) and its step-18 rationale fragment. No key is removed, so there is no tombstone, and there is no D2 conversion: which tab item carries the details body, and under which label, is the author's decision.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@objectstack/plugin-security': patch
3+
---
4+
5+
`security/explain` answers a transfer of a row the caller cannot read the way the transfer door does: as a record that does not exist.
6+
7+
- **What was wrong.** On the write doors, a row the caller cannot read answers what a nonexistent id answers: the by-id update or delete the caller addresses gets `404 RECORD_NOT_FOUND`. A transfer's door is the `PATCH` that writes `owner_id`, an update, so a transfer of an unreadable row gets that 404 too. explain's matching rewrite (the missing-record shape, `record: { visible: false }` with no `decidedBy`) keyed on the verb as asked, `update` or `delete`, so it never ran for `transfer`. Where the caller's update policy admitted the hidden row, explain answered `visible: true`, decided by `rls`. Where the caller declared no write policy, it answered `visible: false` decided by `rls`, which is the shape of a 403 refusal, not of the door's 404.
8+
- **What it does now.** The rewrite asks which operation the verb's door is through the same mapping the by-id write path reads (`transfer` and `restore` as update, `purge` as delete). So every write verb whose door is a by-id update or delete is asked the read question, and a transfer of a row the caller cannot read is `{ visible: false }` with no decider, beside the door's 404.
9+
- **What is unchanged.** `update` and `delete` are answered exactly as before. `restore` and `purge` are still refused at the object gate for every principal, which answers first. A readable row keeps its verdict for every verb. The object-level `allowed` answers the object question and is not touched. No door changes, and no export is added to the package.

0 commit comments

Comments
 (0)