Skip to content

Commit 04b1eb9

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-22161-s2-lint-slice-16-hints
2 parents 515b093 + dc93781 commit 04b1eb9

20 files changed

Lines changed: 1805 additions & 275 deletions
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): a page component's `visibleWhen` describe no longer says the renderer binds `data` — the shipping renderer mounts no `data` root there (#23036)
6+
7+
Clause-②: no
8+
9+
The `.describe()` on `PageComponentSchema.visibleWhen` said "The shipping renderer additionally mounts `features`, `os.user` and binds `data` to the data-source ADAPTER here", and the TSDoc above it said the same. The console's renderer stopped binding `data` on this surface (objectui ruling on the renderer seam's data scope, 2026-09-13): a `data.*` read there now faults, and a faulting `visibleWhen` renders the component, so a gate written from the old text, such as `data.total > 0`, shows the component whether it is true or false.
10+
11+
The describe now names what the renderer mounts beyond the contract-bound roots (`features` and `os.user`), states that no `data` root is mounted, and says the one declared `data` read on this surface is the PLANNED API binding's (`dataSource: { provider: 'api' }`), once that binding's renderer ships. The `page:tabs` item-level `visibleWhen`, a different evaluator that binds `data` to the record row, is still called out. Nothing an author can write changes: the accept set is the same, and only the description text and the generated reference page move.
12+
13+
**If you wrote a page component gate on `data.*`:** gate on the page's row as `record.*`, or on page state as `page.<var>`.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
The TSDoc on `MetadataConversion.retiredAfter` now says what a retirement stamps under a prerelease package label. Under a stable label `x.y.z` it is the label itself, as before. Under a prerelease label `x.y.z-PRERELEASE` (Changesets pre mode, such as `18.0.0-next.N`) it is the newest published stable release, which sits below `x.y.z`: never the prerelease label, which is not a stable `x.y.z`, and never `x.y.z`, the first stable release that carries the entry retired. The old wording, "the package's version label at the moment the retirement lands", had no value that type-checks under a prerelease label. A stable release published later that does not carry the retirement (a `17.x` maintenance release during the 18 prerelease) still accepted the old shape, so it becomes the value. Text only: `retiredAfter` keeps its `${number}.${number}.${number}` type, and the accepted set does not change.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/service-messaging': patch
3+
---
4+
5+
The notification and HTTP outboxes read their claim credential back from the store, so `ack()` lands on a Postgres table whose epoch-ms columns are still `real`
6+
7+
Clause-②: no
8+
9+
Tables created while the `number` field type still mapped to `real` keep those columns as IEEE-754 binary32. Newer tables get an exact numeric column, and existing columns are not migrated. A binary32 `claimed_at` stores a rounding of the claim's `now`, to the nearest 131 072 ms at today's magnitude, and reads back a nearby decimal. `SqlNotificationOutbox` and `SqlHttpOutbox` handed out the in-memory `now` as the claim credential, so it never equalled the stored value. Every `ack()` was refused with `DELIVERY_NOT_ELIGIBLE` as a lost claim, and the dispatcher absorbed the refusal. The row stayed `in_flight` with `attempts = 0`, and every reap handed it to the next claim. The notification was re-delivered about every 131 s, with no end.
10+
11+
- **What changes.** `claim()` and `claimDigest()` return `claimedAt` as the `claimed_at` value the store returned after the claiming update. The value written is unchanged. The rows a claim took are identified by candidate id, holder and status. `claimed_at` is no longer compared with the client's `now` in that read. `ack()`'s ownership check and its compare-and-set are unchanged. They now compare that stored value with the stored value, which is exact on any numeric column.
12+
- **A stuck row heals by itself.** A row left `in_flight` by a refused ack is reaped by the visibility timeout, claimed again, sent once and acked. No operator act is needed. On a `real` column the reap still runs at binary32 granularity, up to about 131 s after `claimTtlMs`.
13+
- **What is unchanged.** A genuinely lost claim is still refused. If another node, or this node's own later claim, reaped and re-claimed the row while a send ran, the late ack gets `DELIVERY_NOT_ELIGIBLE` and writes nothing. The `ClaimedDeliveryRecord` and `HttpClaimCredential` types are unchanged. Existing `real` columns are not migrated, and duplicate inbox rows that were already delivered stay as they are.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/plugin-audit': minor
3+
---
4+
5+
`sys_comment` edit marks are platform-written: an update that changes a comment's `body` stamps `is_edited: true` and `edited_at` from the server clock, and a client can no longer set or clear either field.
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (no-migration-prescription) No metadata moves: `sys_comment` keeps both fields with their names and types, no spec key, authorable spelling, export or stored shape is added, removed, renamed or re-shaped, and a row stored before this change keeps its bytes, so there is nothing for `objectstack migrate meta` to rewrite. What narrows is one runtime write door: a client's value for two platform-written fields no longer lands, and a predicate body update whose matched rows disagree is refused; the remedy is in the request a caller sends to that door, never a rewrite of a package source or a stored row. The other categories are closed on facts: the package publishes (not unpublished); no ADR-0087 id is named or touched (not registered or already-registered); and no exported declaration is removed or narrowed (not runtime-interface-only, type-surface-only or runtime-export-retired). -->
10+
11+
**BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes.
12+
13+
- **Stamped by the platform.** The comment's `beforeUpdate` sets both fields when the update's `body` differs from the stored body, for every caller, the way the engine stamps `updated_at`. An update that does not name `body`, or sends the stored text again beside new `mentions`, is not an edit and stamps nothing. A create is not an edit either.
14+
- **Read-only at the data door.** `is_edited` and `edited_at` now declare `readonly: true`. On an update that edits the body, the stamp replaces whatever the client sent; on any other update a non-system caller's value is dropped and reported in the response's `droppedFields`; on a create the platform sets both to "not edited" over a client value. Nothing is refused for carrying the fields — a client that still sends them on every edit, as the console released before this change does, keeps working and its values are ignored. Read both fields back from the row instead of writing them.
15+
- **Predicate updates.** A `multi: true` update is stamped per matched row. One whose matched rows disagree — some bodies change, some already hold the new text — is now refused whole with `MULTI_UPDATE_HOOK_KEY_DIVERGENCE` (400) and writes nothing, because one SET clause cannot mark some rows and not others. Narrow the predicate to the rows whose text changes, or update by id.
16+
- A `preserveAudit` (historical import) update keeps a mark it supplies, as it keeps `updated_at`.

‎content/docs/references/ui/page.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ View filter rule
301301
| **style** | `Record<string, string>` | optional | Inline styles or utility classes |
302302
| **className** | `string` | optional | CSS class names |
303303
| **responsiveStyles** | `{ large?: Record<string, string \| number>; medium?: Record<string, string \| number>; small?: Record<string, string \| number>; xsmall?: Record<string, string \| number> }` | optional | Per-breakpoint scoped style maps (ADR-0065) |
304-
| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL) — component rendered only when TRUE. Contract-bound roots: `record`, `current_user` (ADR-0068 aliases `user` / `ctx.user` — one object, three spellings), and page state as `page.<var>`. The shipping renderer additionally mounts `features`, `os.user` and binds `data` to the data-source ADAPTER here — renderer behaviour, NOT contract-guaranteed (ADR-0068 rules the user object only). ⚠️ `data` is surface-dependent: on a `page:tabs` item `visibleWhen` it is the record ROW instead. e.g. "page.selectedProjectId != ''" |
304+
| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL) — component rendered only when TRUE. Contract-bound roots: `record`, `current_user` (ADR-0068 aliases `user` / `ctx.user` — one object, three spellings), and page state as `page.<var>`. The shipping renderer additionally mounts `features` and `os.user` — renderer behaviour, NOT contract-guaranteed (ADR-0068 rules the user object only). It mounts NO `data` root here: a `data.*` read faults and a faulting predicate renders the component, so such a gate never gates; the page's row is `record`. `data` is declared only for an element whose `dataSource` is the PLANNED API binding (`provider: 'api'`), which reads its bound value as `data` once that binding's renderer ships. ⚠️ A `page:tabs` item `visibleWhen` is a different evaluator: it binds `data` to the record ROW. e.g. "page.selectedProjectId != ''" |
305305
| **visibility** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | [DEPRECATED → `visibleWhen`] Visibility predicate (CEL). Normalized to `visibleWhen` at parse. |
306306
| **dataSource** | `{ object: string; view?: string; filter?: object[]; sort?: object[]; … } \| { provider: 'api'; read: object; path?: string }` | optional | Per-element data binding: an object binding `{ object, view?, filter?, sort?, limit? }` that overrides the page object, or an API binding `{ provider: 'api', read: { url }, path? }` that reads one same-origin endpoint |
307307
| **responsive** | `never` | optional | [REMOVED] `page.components[].responsive` was removed in @objectstack/spec 17 (ADR-0049 D2) — no renderer ever read it, so per-breakpoint layout overrides (columns/order/visibility) parsed, validated, and then did nothing. Delete the key. For breakpoint behaviour that IS applied, use the sibling `responsiveStyles` (ADR-0065) — per-breakpoint CSS maps compiled to id-scoped CSS at render, e.g. `responsiveStyles: { xsmall: { display: 'none' } }` to hide a component on the narrowest screens. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
@@ -440,7 +440,7 @@ Print declaration: paper size, orientation, margins, the running header and foot
440440
| **style** | `Record<string, string>` | optional | Inline styles or utility classes |
441441
| **className** | `string` | optional | CSS class names |
442442
| **responsiveStyles** | `{ large?: Record<string, string \| number>; medium?: Record<string, string \| number>; small?: Record<string, string \| number>; xsmall?: Record<string, string \| number> }` | optional | Per-breakpoint scoped style maps (ADR-0065) |
443-
| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL) — component rendered only when TRUE. Contract-bound roots: `record`, `current_user` (ADR-0068 aliases `user` / `ctx.user` — one object, three spellings), and page state as `page.<var>`. The shipping renderer additionally mounts `features`, `os.user` and binds `data` to the data-source ADAPTER here — renderer behaviour, NOT contract-guaranteed (ADR-0068 rules the user object only). ⚠️ `data` is surface-dependent: on a `page:tabs` item `visibleWhen` it is the record ROW instead. e.g. "page.selectedProjectId != ''" |
443+
| **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | Visibility predicate (CEL) — component rendered only when TRUE. Contract-bound roots: `record`, `current_user` (ADR-0068 aliases `user` / `ctx.user` — one object, three spellings), and page state as `page.<var>`. The shipping renderer additionally mounts `features` and `os.user` — renderer behaviour, NOT contract-guaranteed (ADR-0068 rules the user object only). It mounts NO `data` root here: a `data.*` read faults and a faulting predicate renders the component, so such a gate never gates; the page's row is `record`. `data` is declared only for an element whose `dataSource` is the PLANNED API binding (`provider: 'api'`), which reads its bound value as `data` once that binding's renderer ships. ⚠️ A `page:tabs` item `visibleWhen` is a different evaluator: it binds `data` to the record ROW. e.g. "page.selectedProjectId != ''" |
444444
| **visibility** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source: string; ast?: any; meta?: object }` | optional | [DEPRECATED → `visibleWhen`] Visibility predicate (CEL). Normalized to `visibleWhen` at parse. |
445445
| **dataSource** | `{ object: string; view?: string; filter?: object[]; sort?: object[]; … } \| { provider: 'api'; read: object; path?: string }` | optional | Per-element data binding: an object binding `{ object, view?, filter?, sort?, limit? }` that overrides the page object, or an API binding `{ provider: 'api', read: { url }, path? }` that reads one same-origin endpoint |
446446
| **responsive** | `never` | optional | [REMOVED] `page.components[].responsive` was removed in @objectstack/spec 17 (ADR-0049 D2) — no renderer ever read it, so per-breakpoint layout overrides (columns/order/visibility) parsed, validated, and then did nothing. Delete the key. For breakpoint behaviour that IS applied, use the sibling `responsiveStyles` (ADR-0065) — per-breakpoint CSS maps compiled to id-scoped CSS at render, e.g. `responsiveStyles: { xsmall: { display: 'none' } }` to hide a component on the narrowest screens. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |

‎packages/plugins/plugin-audit/src/comment-access-hooks.test.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,11 @@ function install(opts: {
7171
const parentReads: Array<{ object: string; options: any }> = [];
7272
const parentFind = readableParentFind(opts.readable ?? [], parentReads);
7373
const engine: CommentAccessEngine = {
74+
// The FIRST handler per event is the authorization gate these tests drive:
75+
// `beforeUpdate` also carries the edit stamp (#23124), registered after it
76+
// and pinned on the real engine in `comment-edit-stamp.integration.test.ts`.
7477
registerHook: (event, handler) => {
75-
hooks.set(event, handler as any);
78+
if (!hooks.has(event)) hooks.set(event, handler as any);
7679
},
7780
find: async (object, options: any) => {
7881
if (object !== 'sys_comment') return parentFind(object, options);
@@ -644,6 +647,10 @@ const COMMENT_FIELDS = {
644647
thread_id: { name: 'thread_id', label: 'Thread', type: 'text' as const },
645648
author_id: { name: 'author_id', label: 'Author', type: 'text' as const },
646649
body: { name: 'body', label: 'Body', type: 'text' as const },
650+
// The edit stamp `installCommentAccessHooks` also registers (#23124) writes
651+
// these on a body change; declared as `sys_comment` declares them.
652+
is_edited: { name: 'is_edited', label: 'Edited', type: 'boolean' as const, readonly: true },
653+
edited_at: { name: 'edited_at', label: 'Edited At', type: 'datetime' as const, readonly: true },
647654
};
648655
const sysCommentObject = { name: 'sys_comment', label: 'Comment', fields: COMMENT_FIELDS };
649656
const crmOpportunityObject = {

0 commit comments

Comments
 (0)