Skip to content

Commit e8df41e

Browse files
Merge origin/main into claude/project-thread-un9ezo
Resolves the adjacent-row conflict in scripts/check-skills-token-ratchet.mjs: main's objectstack-automation row (#26390) and this branch's objectstack-data examples-objects.md row both kept. Claude-Session: https://claude.ai/code/session_01JQwxbXTWvHG6MXFD7Gywf2 Co-authored-by: Claude <noreply@anthropic.com>
2 parents e63f6ec + 08ed7e2 commit e8df41e

148 files changed

Lines changed: 5929 additions & 2182 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: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/plugin-email': patch
3+
---
4+
5+
fix(plugin-email)!: retire `renderTemplate`, the render export that takes an email template body with no save moment
6+
7+
The hole-placement rule for mail templates is judged where a template is authored or published: `os validate`, `os build`, `os lint` and the runtime publish gate. The `renderTemplate` function on the package entry took a template body inline and rendered its html face at once, with no save moment, so the rule could not see it. It leaves the published entry of `@objectstack/plugin-email`. Nothing in this repository imported it from that entry. The renderer stays inside the package: `services.email` still renders every stored `email_template` through it, unchanged.
8+
9+
Clause-②: no (narrowing)
10+
Metadata-protocol: none
11+
12+
<!-- adr-0087: not-required (runtime-export-retired @objectstack/plugin-email#renderTemplate) One runtime function leaves a published package entry; no key of any metadata schema, no object definition and no stored shape is added, removed, renamed or re-shaped, so there is nothing for `objectstack migrate meta` to rewrite and no tombstone. A TypeScript consumer meets the break at its own import, and the line below names the door to use instead. -->
13+
14+
**BREAKING** (a runtime export is removed). It ships as `patch` because the metadata protocol does not move. There is no staged window.
15+
16+
- FROM `renderTemplate` TO an `email_template` item for the name and locale, rendered by `services.email` through `renderTemplate({ template, data, locale })` (render only) or `sendTemplate` (render and send).
17+
18+
Unchanged: `IEmailService.renderTemplate` (the render-by-name method of the same name), `requireVars`, `htmlToText` and the template loader; and how every stored template renders.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
**BREAKING** — the two unread dependency spellings leave the authoring surface: `PackageDependency.optional` (an entry of the marketplace snapshot's `manifestJson.dependencies[]`) and `AppManifest.dependencies` (the bare-id list on the app install manifest) are `retiredKey()` tombstones (#25792, ADR-0141 D8 items 2 and 3).
6+
7+
A `major`-class change, recorded as `minor` under the launch-window convention. ADR-0141 D8 left both fates to "a measured reader count"; the count was zero for both, so both retire at once, with no retirement stage (ADR-0049 enforce-or-remove).
8+
9+
Clause-②: no (narrowing)
10+
Metadata-protocol: narrowed
11+
12+
**Why.** A dependency is declared in one place, the package's `manifest.dependencies` record (`{ [packageId]: versionRange }`), and the ADR-0141 D2 pre-install check reads that record alone and treats every entry as required. Two more spellings stayed declared beside it and nothing read either. `optional: true` promised "missing dep is a warning, not an error", but the record has no optional arm, so no install path could honour it. `AppManifest.dependencies` restated the record without its range, and no install path consulted it. An author, often an AI, who wrote either got nothing enforced and no error. Measured at `origin/main` `6390fe52ce`: zero readers and zero writers of either key outside `packages/spec` (`packages/`, `ui/`, `examples/`, `apps/`), and none in HotCRM at `9464673`, whose three modules declare their dependency in the record.
13+
14+
### FROM → TO
15+
16+
| removed | from | what to write instead |
17+
| --- | --- | --- |
18+
| `optional` on a `PackageDependency` entry (`PackageManifestSchema.dependencies[]`) | `@objectstack/spec/marketplace` | delete the key. An entry carries `packageId` and `versionRange` only. Declare a dependency in `manifest.dependencies` only when the package cannot install without it. |
19+
| `dependencies` on `AppManifest` | `@objectstack/spec/system` | delete the key. Declare each dependency with its range in the package's `manifest.dependencies`, for example `{ 'com.acme.core': '^1.0.0' }`. |
20+
21+
**The one-line fix: delete the key.** A TypeScript author fails `tsc` first (each key is typed as the retired mark), and a parse of `PackageDependencySchema`, `PackageManifestSchema` or `AppManifestSchema` refuses the key with its prescription, the old default (`false`, `[]`) included. The parsed output no longer carries either key, so `PackageDependencyParsed` loses `optional` and `AppManifestParsed` loses `dependencies`.
22+
23+
⚠️ Runtime behaviour is deliberately **unchanged**: nothing ever read either key, so every install answers exactly as before. The removal retracts a false claim, not a capability. **No deprecation window** (startup-stage posture: retirements take effect immediately).
24+
25+
⚠️ **The out-of-repo consumer population beyond HotCRM is NOT MEASURED.** `@objectstack/spec` is published, and the control plane's reading of `sys_package_version.manifest_json` is not readable from this repository.
26+
27+
The ADR-0087 D3 semantic entry `dependency-unread-spellings-retired` carries the judgement, and both keys are registered in `RETIRED_KEYS_BY_MAJOR[18]`. There is no D2 conversion: neither shape is a stack collection member, so a conversion would have no source to rewrite.
28+
29+
<!-- adr-0087: registered dependency-unread-spellings-retired -->
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@object-ui/plugin-detail': patch
3+
---
4+
5+
fix(plugin-detail): the record page's inline-edit pencil shows on keyboard focus, not only on hover
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
In read mode, the pencil beside an editable field (in the details body and in the highlights strip) was transparent until the pointer hovered its field. The pencil is a real button, so a keyboard user tabbing through the record landed on a control they could not see, focus ring included. It now also shows while it has keyboard focus, or while anything else in its field does. Hover, the row's hint and double-click to edit are unchanged.
11+
12+
What to do: nothing.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@object-ui/fields': patch
3+
'@object-ui/plugin-grid': patch
4+
---
5+
6+
fix(fields,plugin-grid): a grid's `date` column reads in one absolute face when its field declares no `format`
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
A list grid's `date` column used to mix three faces in one column, for example "Yesterday · 3 days ago · Sep 25 · Sep 20, 2025". The cell's default face was `'relative'`, which writes a relative phrase within ±7 days of today and an absolute date outside that window. A sorted column did not look sorted, and "Yesterday" in an expiry column read as a status.
12+
13+
- **Default change for an undeclared `format` (visible from metadata).** In a grid column, a `date` field that declares no `format` now renders the locale's short absolute date: `Oct 25`, with the year added when the date is not in the current year. Every row of the column uses this one face. The group headings of a grid grouped by that field change with it, so a heading still reads like the cell under it.
14+
- **The relative phrase moves to the tooltip.** It is shown on hover ("Yesterday", "In 2 days", or "Overdue 3d" on a declared `dueLike` field). A date outside the ±7-day window has no relative phrase, so it keeps its ISO `title` as before. A past declared deadline is still red.
15+
- **A declared `format` still wins.** A field that declares `format: 'relative'` keeps the relative face in the grid. This is how an author who wants relative wording in a list keeps it.
16+
- **Other surfaces are unchanged.** The record detail body, the header highlights strip, kanban and gallery cards, the tree, a record page's related list and the grid's record panel render `date` cells as before. `DateCellRenderer` paints the absolute default only when its host passes the new `inGridColumn` prop, and `ObjectGrid` passes it on every column it builds. On mobile, a grid card draws its title through the column's cell, so a `date` title reads absolute there too; the card's other date slots were already absolute. `datetime` cells are not affected, because their default face was already absolute (`compact`).
17+
18+
What to do: nothing. To keep the relative wording in a grid column, declare `format: 'relative'` on the field.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
fix(app-shell): a record locked by an approval is shown locked to a reader outside that approval
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
A member who can read a record but is not on its approval (not an approver, not the submitter, no override) was offered inline edit while a `lockRecord: true` approval held the record, and the save was then refused with `409 RECORD_LOCKED`. The approvals list serves a request only to its participants, so the record page read that member's empty list as "no approval is running".
11+
12+
When the list finds no pending request, the record page now also reads the record's own lock, `client.approvals.getRecordLock(object, recordId)`, which the server answers to every caller who can read the record with `{ locked }` and nothing else. A locked answer shows the record locked: Edit is disabled, inline edit is not offered, and the "Locked for approval" band is drawn without a recall button, because a reader who cannot see the request did not submit it. The reader still sees nothing of the request itself (approvers, comments, attachments).
13+
14+
Unchanged: a pending request the viewer can see still decides with its own node's `lockRecord`, and a composition without the approvals service (the lock read answers `501`) still opens an answered empty record editable.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
refactor(app-shell): the Console `ObjectView`'s header toolbar and list URL state move into sibling hooks
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
Three concerns move verbatim out of `ObjectViewInner` in `views/ObjectView.tsx`. The header's `list_toolbar` actions, the nav `runAction` deep link and the `action:bar` node move into `useObjectViewHeaderToolbar` in `objectViewHeaderToolbar.ts`. The list's toolbar state in the URL (the seed, its write-back and "Reset view") moves into `useObjectViewListUrlState`, and the `filter[...]` equality filters with the restored user-filter selections into `useObjectViewUrlFilters`, both in `objectViewListUrlState.ts`. The component passes every input in and reads the returned values. No logic, name or rendered output changes, and the package entry is unchanged.
11+
12+
What to do: nothing.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
---
4+
5+
refactor(app-shell): the Console `ObjectView`'s view tab strip, plugin schema and header chrome move into sibling hooks
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
Three concerns move verbatim out of `ObjectViewInner` in `views/ObjectView.tsx`. The view tab items, the `ViewTabBar` handed to the list toolbar and its own-row face move into `useObjectViewTabBar` in `objectViewTabBar.tsx`. The views with the open draft merged in and the `ObjectViewSchema` handed to the plugin `ObjectView` move into `useObjectViewPluginSchema` in `objectViewPluginSchema.ts`. The header's create button, its folded actions menu and the list area for a refused view move into `useObjectViewHeaderChrome` in `objectViewHeaderChrome.tsx`. The component passes every input in and reads the returned values. No logic, name or rendered output changes, and the package entry is unchanged.
11+
12+
What to do: nothing.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
---
2+
'@objectstack/spec': patch
3+
'@object-ui/app-shell': patch
4+
---
5+
6+
**BREAKING** — `PositionGrantVia` (`@objectstack/spec/contracts`) loses its `'name'` member, and the Setup position list drops its "(by name)" badge (#26022).
7+
8+
Clause-②: no (narrowing)
9+
Metadata-protocol: none
10+
Ruling-ref: #13419 ruling B (`5472608110`); 「甲」 as amended (`6303865025`); triage unlock on #26022 (`6424661273`)
11+
12+
**Why.** The security plugin no longer grants a position's holders the permission set that only shares the position's name: a position grants the sets its definition names in `permissionSets`, and nothing else (#24757). Since then nothing serves `via: 'name'` on `describePositionGrants` or on `GET /api/v1/meta/position?include=grants`. The spec still published the member and the console still drew a badge for it, so both described a grant the runtime no longer makes. #13419 ruling B retires the channel in 18.x with no retirement stage.
13+
14+
What changes:
15+
16+
- `@objectstack/spec`: `PositionGrantVia` is `'definition'`. Every entry of `PositionGrantSummary.permissionSets` carries `via: 'definition'`, as the security plugin already serves it.
17+
- `@object-ui/app-shell`, Setup → Positions: the granted-sets column shows each set's name alone. The "(by name)" / 「(同名)」 badge and its hint are gone, with the i18n keys `engine.position.grants.byName` and `engine.position.grants.byNameHint` (en and zh).
18+
- `@object-ui/app-shell`, Setup → Users on a managed environment: the 「未授权」 marker judges a held position only by the sets its definition lists. It no longer counts a permission set that shares the position's name, and it no longer reads the permission-set catalogue to do so. A holder of such a position is now marked when nothing else grants them a set beyond the baseline, which is what the server enforces. Before this change, such a holder was left unmarked.
19+
20+
For TypeScript consumers: code that compares a `PositionGrantVia` value with `'name'` no longer compiles (TS2367). Delete that branch, because no server serves the value. To have a position grant the set that shares its name, name the set in the position's `permissionSets`. No stored row or authored metadata carries the type, so nothing is converted.
21+
22+
The ADR-0087 D3 semantic entry `position-grant-via-name-retired` records the retirement. `PositionGrantVia` types a served description, not a stack collection or a metadata type, so there is no source for a D2 conversion to rewrite.
23+
24+
<!-- adr-0087: registered position-grant-via-name-retired -->
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/plugin-approvals': patch
3+
'@objectstack/plugin-sharing': patch
4+
'@objectstack/plugin-security': patch
5+
---
6+
7+
fix(plugin-approvals,plugin-sharing): the approver a pending step waits on can read the record the request is about
8+
9+
Clause-②: no
10+
Metadata-protocol: none
11+
12+
An approver was asked to decide on a record they could not open. Unless the app happened to grant them read of that exact row, the request page showed only the snapshot and the record card read "unable to open this record", so a contract was approved without its document being read.
13+
14+
- **Who reads.** While an approval request is pending, each person on its current step's slate (a slot naming their user id, or the email their own account carries) and the override holder (the administrator who may decide any pending request of their organization) may READ the one record the request is about. A slot left as a `position:` or `team:` literal names no person and grants no read.
15+
- **How far.** Read only: the write gates never read this grant, so the approver still cannot edit or delete the record. Only the record itself, as a reader of it reads it: attachments follow the record read, as they do for any reader. Only while the step is pending: a decision that resolves the step, or advances the request to its next step, ends the grant at the next read. Never across organizations, the platform administrator's override included. An on-behalf-of (agent) principal gets no approval grant.
16+
- **Where it is decided.** `@objectstack/plugin-approvals` derives the records from the slate it already resolves (`ApprovalService.pendingApproverReadGrants`, one indexed read of the pending-approver index per request context, plus one request read per object, memoized per request and retired by any write). `@objectstack/plugin-sharing` ORs those record ids into its read filter beside the caller's record shares, so every read door that composes the sharing filter serves them; it grants nothing when the approvals service is absent or its read fails.
17+
- **Explain.** `@objectstack/plugin-security`'s access explanation names the grant on the sharing layer as a read share "via approval request N".
18+
19+
The grant is the row-level reach of a read share; the approver still needs read on the object itself. Nothing is declared: no metadata key, no app setting.
20+
21+
What to do: nothing. An app that wrote sharing rules mirroring its approval routing only so approvers could open the record may drop them.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@object-ui/react': patch
3+
'@object-ui/components': patch
4+
'@object-ui/app-shell': patch
5+
---
6+
7+
fix(react,components,app-shell): a delegated admin's out-of-scope save shows the server's reason instead of the fixed no-permission line
8+
9+
Clause-②: yes (widening: `@object-ui/react` exports a new `delegationScopeMessage`)
10+
Metadata-protocol: none
11+
12+
When a delegated administrator saved something outside their own scope, the server answered `403 DELEGATION_SCOPE_VIOLATION` with the boundary that refused it (a business unit outside the delegated subtree, a permission set not in the scope's allowlist, a target user outside the subtree). The console read every 403 as a plain denial, so the toast and the in-form banner said only "You don't have permission to save this record."
13+
14+
- **`@object-ui/react`.** New `delegationScopeMessage(err)`: the server's reason, without the `[Security] ` machine prefix, when the error's code (`code` or `details.code`) is `DELEGATION_SCOPE_VIOLATION`; `null` for everything else. It is read before `isPermissionError`, which still answers true for any 403.
15+
- **`@object-ui/components`.** The record form's toast and banner show that reason. A producer-marked `userMessage` still comes first.
16+
- **`@object-ui/app-shell`.** A flow run whose write node met the same refusal (`innerRefusal`) shows the same reason.
17+
18+
Every other denial is unchanged: `PERMISSION_DENIED`, `FORBIDDEN`, a bare 403 and a row-level-security refusal still show the fixed localized line and never the server's text.
19+
20+
What to do: nothing.

0 commit comments

Comments
 (0)