Skip to content

Commit bdbb479

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/project-thread-2i41wk
2 parents f57e048 + 1831836 commit bdbb479

75 files changed

Lines changed: 4823 additions & 242 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: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@object-ui/components': patch
3+
'@object-ui/i18n': patch
4+
'@object-ui/app-shell': patch
5+
---
6+
7+
feat(console): on a narrow window the sidebar is the icon rail, and a click expands it over the content
8+
9+
Clause-②: yes (widening)
10+
Metadata-protocol: none
11+
12+
On a 1280 px laptop window the expanded console sidebar left 1024 px of content, too little for a record's split view beside its list. Now:
13+
14+
- **The fold.** Below the width the record split needs beside an expanded sidebar (its 1160 px of content plus the 256 px sidebar, so 1416 px), the console sidebar mounts as the icon rail and the content takes the rest. At that width and above the sidebar behaves as before; below the phone breakpoint the drawer is unchanged. The width is derived in one named constant, `CONSOLE_SIDEBAR_RAIL_BELOW_WIDTH`, from the sidebar's own width.
15+
- **Expand over the content.** A click on the rail's background, the app bar's sidebar button or ⌘B / Ctrl+B expands the full sidebar over the page without moving it; choosing a navigation entry, a press outside it or Esc folds it again. Hovering does nothing, and a click on a rail icon still opens that entry.
16+
- **Keep sidebar open.** While the rail is expanded, its foot offers "Keep sidebar open": pressed, the narrow window keeps the wide-window sidebar in this browser, and pressing it again brings the fold back.
17+
- **The wide window keeps its state.** The open or collapsed state the reader chose on a wide window is no longer overwritten by the fold: it comes back when the window widens, and on the next load that opens wide.
18+
19+
`@object-ui/components`: new `useIsViewportBelow(breakpoint)` hook, `true` while the window is narrower than `breakpoint` CSS pixels, read synchronously on the first render and live through `matchMedia`. Nothing existing changes.
20+
21+
`@object-ui/i18n`: the `sidebar` namespace gains `sidebar.keepOpen` in all ten packs. No key is removed or renamed.
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-detail': patch
4+
---
5+
6+
feat(fields): every record reference shows a hover card with the target's declared highlights, without navigating
7+
8+
Clause-②: yes (widening)
9+
Metadata-protocol: none
10+
11+
Hovering a record reference for 400 ms, or pressing Space on it, shows a floating card: the target object's icon and label, the record's name as a link that opens it the way a click on the reference does, and the object's declared `highlightFields` minus the name field, at most six, drawn by the same cell renderers a list uses (option labels, currency, dates, lookups). Esc, moving the pointer away or blurring the reference hides it, and one card is open at a time. Focus alone does not show it.
12+
13+
- **Where.** Every lookup and master-detail value that links to its record (`LookupCellRenderer`): a relation column in a list, in a related list's table and in its enlarged view, and a lookup in the record's details. On the record page's related column, the name in a compact related row shows the card too, and its title opens the row as the name does.
14+
- **What it reads.** Nothing until a card is shown; then one `findOne` for the record, with `$expand` naming only the reference fields the card draws. The answer is kept per record and read again after the data bus says the record changed, so a list of 200 rows reads nothing on render.
15+
- **Metadata only.** Only the object's declared `highlightFields` are drawn: a name the object does not define, the name field and a field the viewer may not read are left out. An object that declares none shows the record's name and the object label. A record the viewer cannot read shows the reference's name and the object label.
16+
- **Layout.** The card is portalled to the document body, so the row, cell or chip under it keeps its layout.
17+
18+
`@object-ui/fields` exports `RecordHoverCard` (with `RecordHoverCardProps` and `RecordHoverCardObjectDef`) for a host that draws a reference of its own: wrap the element that names the record and pass the target's `objectName`, `recordId` and the object definition the host already holds.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@object-ui/data-objectstack': patch
3+
'@object-ui/app-shell': patch
4+
---
5+
6+
perf(console): opening a record no longer re-reads the list under it, asks its field access once, and the bell polls once a minute
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
Measured in the console on app-crm, opening one record from a list and closing it again:
12+
13+
- **The list under the drawer is not read again.** The first record opened replaced the list with the route skeleton (the drawer made the route read the whole object registry instead of the app's object scope, and the route waited for it), so the list mounted again and re-read its rows; every later open and close handed the list a different document for its object, which rebuilt its columns and re-ran the list query. The drawer now reads the whole registry beside the route's scope, without holding the route, and the registry joins the scope once it lands with the scope's own documents kept as they are. The list page also holds `columns` and `searchableFields` by value, as it holds the view-type blocks. The record form overlay (`?form=`) still reads the whole registry.
14+
- **One field-access request per record.** The record's sections, highlights, form and page each asked `GET /data/:object/:id/field-access` at once; `ObjectStackAdapter.getRecordFieldAccess` now shares one request between concurrent asks for the same record, remembers a 404 for that record until a write to its object, and stops asking a server that answers 501. A settled answer is not cached: the next ask after a save reaches the server.
15+
- **The bell polls once a minute.** The inbox and approvals feeds polled every 10 s and 30 s on every console page. With no realtime channel in the platform, they now poll every 60 s in the foreground, still stop in a hidden tab, and stop polling altogether while a host reports an open push channel (a pushed change reads the feed it names). A new inbox message can show up to about a minute later.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec): the object meta reads declare `disabledActions`, the packaged actions the activation ledger switches off, beside the item
6+
7+
Clause-②: yes (widening)
8+
Metadata-protocol: added
9+
10+
`GetMetaItemResponseSchema` and `GetMetaItemCachedResponseSchema` gain the optional `disabledActions` (an array of action names), and `GetMetaItemsResponseSchema` gains the optional `disabledActions` keyed by object name. On an `object` read it names the actions in `item.actions` that this deployment's activation ledger switches off (an administrator turned them off under Setup › Packaged automation), so a call to any of them is refused 409 `ACTION_DISABLED`. It sits beside the item, never inside it: `item.actions` is served as declared, a switched-off action included. The key is absent when no activation ledger answered, which never means "nothing is switched off"; `[]` means a ledger answered and none of this object's actions is off. On the cached default door it is read live and kept out of the ETag, so a read that carries it is never answered 304. `IObjectQLEngine` gains the optional member `getDisabledActions()`, which the metadata read calls through the kernel's service table: it returns the switched-off names, or `undefined` when no ledger is attached.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/metadata-protocol': patch
3+
'@objectstack/rest': patch
4+
'@objectstack/objectql': patch
5+
'@objectstack/runtime': patch
6+
---
7+
8+
fix: `GET /meta/object` serves which packaged actions are switched off beside the item, and a switched-off action is refused with a plain sentence
9+
10+
Clause-②: yes (widening)
11+
Metadata-protocol: none
12+
13+
- **Served beside the object** (`@objectstack/metadata-protocol`, `@objectstack/rest`). `GET /api/v1/meta/object/:name` serves `disabledActions` beside `item`: the names in `item.actions` that the deployment's activation ledger switches off. `GET /api/v1/meta/object` serves the same answer keyed by object name. The default cached door carries it too, read live on every call and kept out of the ETag, so a read that carries it is answered in full rather than `304 Not Modified`. The key is absent where no ledger answered (no engine, or an engine with no ledger attached), on a draft read, and on every type other than `object`. `item.actions` is unchanged. The protocol reads the engine's `getDisabledActions` through the kernel's service table by name and imports no engine.
14+
- **The fact it serves** (`@objectstack/objectql`). `ObjectQL.getDisabledActions()` returns the names the engine's activation projection switches off, the same projection both dispatch doors consult before they refuse a call, or `undefined` when no ledger is attached. `listDisabledActions()` is unchanged.
15+
- **The refusal sentence** (`@objectstack/objectql`, `@objectstack/runtime`). A switched-off action is still refused 409 `ACTION_DISABLED` at the REST and MCP doors, and the message now reads: "This action is turned off for this deployment. An administrator can turn it on again under Setup › Packaged automation, or you can use another action." It no longer names the ledger table or the ADR. The engine writes those, with the code and the action's name, to an `info` log line each time it refuses one.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): `composeStacks` says why a top-level connector beside a package body's same-named entry is ambiguous (#28006)
6+
7+
Clause-②: no (message text only: every composition `composeStacks` refused it still refuses, under the same `STACK_CROSS_REFERENCE_INVALID` envelope, and every one it accepted it still accepts)
8+
Metadata-protocol: none
9+
10+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped, no export or type changes, and no verdict moves: only the wording of one refusal changes. There is nothing for `objectstack migrate meta` to rewrite. -->
11+
12+
A datasource whose `external.connector` names a connector that a package body and the composed artifact's top level both declare, one able to lend and the other not, was already refused as ambiguous. The refusal gave the reason that holds between two package bodies ("the runtime keeps one entry per package and lends from whichever registers first"), which is not what happens here. The boot does not register a top-level entry whose name a package body declares, so the body's entry lends, and a dev reload replaces it with the artifact's top-level entries of the name, the first of which lends. The finding now says that, and opens with "which the composed artifact declares both in a package body and at its top level".
13+
14+
- **Where it applies.** A `last` composition with a nested multi-package input, where another input's top level declares the same name as one of its bodies. A `preserve` composition with an input that carries `packages` beside its own top-level collections, where another package declares the same name.
15+
- **Top-level entries only.** Two inputs' top-level entries that disagree, beside package bodies that do not declare the name, get a sentence of their own: no door registers either as a package body's entry, and a dev reload lends from the first top-level entry of the name.
16+
- **Unchanged.** Two package bodies that disagree keep the first-registered wording. The remedy is the same: rename or remove the entry that cannot lend.

‎.claude/skills/pm-dispatch/references/contract-review.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
- 新 head 即清;点名现 head 时 (b) 不起;条件满足由持有车道挂等复核标记或落交接行。
3131
- 逾 24 小时条件未满的 hold 列入分诊轮次记录(`check-owed-reviews.mjs` 标出)。
3232
- 复核记录 = 一条评论落 PR 或卡,席内与子代理同形;散文、dev 自评、`os-dev-report` 恒不算。
33-
- 同形 = `## Contract review` 题头、head sha 独占码段、①②③ 逐项、独立性对、PASS/FAIL。
33+
- 同形 = `## Contract review` 题头、`Head-sha:` 行独占码段、①②③ 逐项、独立性对、PASS/FAIL。
3434
- 同形含首行 `Served-tier:`:值写该面所定档的常量名 `CONTRACT_REVIEW_TIER` 或 `TIER_DEFAULT`;
3535
无此行不成裁决;模板 `node scripts/pm/record-recognisers.mjs --template`,申报面加 `--face declaration`。
3636
- 同形带 `Local-runs:` 行:`none` 或 `probe — REASON` 具名唯一探针;有行必校,缺行旧记录不改判。

‎content/docs/references/api/protocol.mdx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1332,6 +1332,7 @@ This schema accepts one of the following structures:
13321332
| **notModified** | `boolean` | optional (default: `false`) | True if resource has not been modified (304 response) |
13331333
| **version** | `string` | optional | Metadata version identifier |
13341334
| **registration** | `{ state: 'registered' } \| { state: 'refused'; reason: object } \| { state: 'unregistered' }` | optional | The automation engine's verdict on the flow in `data`, read live on every call and never part of `etag` (see `FlowRegistrationSchema`). Present only for a `flow` read answered by a process that runs an automation service; a read that carries it is never answered `notModified`. Absent otherwise, and absence means no engine judged it, never `registered`. |
1335+
| **disabledActions** | `string[]` | optional | The names in the `actions` of the object in `data` that this deployment's activation ledger switches off (a call to any of them is refused 409 `ACTION_DISABLED`), read live on every call and never part of `etag`. Present only for an `object` read answered by a process whose data engine has an activation ledger attached, `[]` when none is switched off; a read that carries it is never answered `notModified`. Absent otherwise, and absence means no ledger answered, never that nothing is switched off. |
13351336

13361337
### Nested Shape: `GetMetaItemCachedResponse.etag`
13371338

@@ -1458,6 +1459,7 @@ This schema accepts one of the following structures:
14581459
| **resettable** | `boolean` | optional | Whether the item can be reset to its packaged default — true iff it is artifact-backed, i.e. there is a baseline to reset TO. |
14591460
| **version** | `string \| null` | optional | ADR-0008 version token of the STORED row a save to this item compares against, at this read's scope (the environment's partition and `?package=`) and lifecycle (`?state=draft` reads the draft row, the plain read the active row). The same keyed token a save receipt's `version` serves for that row: send it back as the `If-Match` request header on `PUT /meta/:type/:name` (with `?mode=draft` for the draft) and a concurrent edit is refused 409 `METADATA_CONFLICT` instead of overwritten. `null` when no stored row is there — an item served from code or a package artifact, or from a row in another scope (a package-less row a package read falls back to) — so the next save creates one: pin it with `If-None-Match: *`. A `null` beside `editable: false` is no such invitation: that item takes no save at this address, so no pin is prescribed for it. ABSENT on the cached published-value branch and on a `?preview=draft` read, which publish no version: absence means "not published here", never "no row". Opaque — echo it verbatim, never parse it. |
14601461
| **registration** | `{ state: 'registered' } \| { state: 'refused'; reason: object } \| { state: 'unregistered' }` | optional | The automation engine's verdict on this flow (see `FlowRegistrationSchema`): `registered`, or `refused` with the engine's `reason` verbatim, or `unregistered`. Present only on a `flow` read answered by a process that runs an automation service; absent otherwise, and absence means no engine judged it, never `registered`. Unlike the protection keys it is published by the cached branch too, read live and never part of the ETag, so a cached read that carries it is never answered 304; it is absent on a draft read, whose pending body no engine holds. The verdict is the answering process's engine. Beside `item`, never inside it: the item's `status` stays what its author declared. |
1462+
| **disabledActions** | `string[]` | optional | The names in `item.actions` that this deployment's activation ledger switches off (an administrator turned them off under Setup › Packaged automation): a call to any of them is refused 409 `ACTION_DISABLED`. Present only on an `object` read answered by a process whose data engine has an activation ledger attached, and then `[]` when none of this object's actions is switched off. Absent otherwise, and absence means no ledger answered, never that nothing is switched off. Unlike the protection keys it is published by the cached branch too, read live and never part of the ETag, so a cached read that carries it is never answered 304; it is absent on a draft read. The ledger is the answering process's engine's. Beside `item`, never inside it: `item.actions` is served as declared, a switched-off action included. |
14611463
| **draftChanges** | `{ path: string[]; by: Enum<'copilot' \| 'mcp' \| 'studio' \| 'api'>; actor: string \| null; client?: object; … }[]` | optional | Per-key provenance of the pending draft: which authoring surface last changed each key path of `item`, under which actor, and when. The draft save records it: each draft save is compared with the body that save replaced (the previous draft, or the stored published row for the draft's first save; a draft of an item with no stored published row is recorded as `[]`, the whole body). A `migrate-stored` rewrite of a draft row records nothing, since it is not authoring. The entries are kept from the first recorded save until the draft is published or discarded, which remove the draft row and the entries with it; and no two entries share a path: a save replaces every entry at or under each path it changes, so the last write to change a key is the entry whose `path` is the longest prefix of that key's path, and entries under a key's path are changes inside it. The entries name which writes changed what; they are not a list of every difference from the published body (compare the two bodies for that). Served only on a `?state=draft` read that answers a stored draft row. ABSENT on the active read, the cached branch and a `?preview=draft` read, and for a draft row whose saves recorded none: absence means "not recorded", never "no changes". |
14621464

14631465
### Nested Shape: `GetMetaItemResponse.sortability`
@@ -1522,6 +1524,7 @@ One changed key path of the pending draft, and the last write that changed it.
15221524
| **type** | `string` | ✅ | Metadata type name |
15231525
| **items** | `any[]` | ✅ | Array of metadata items |
15241526
| **registrations** | `Record<string, { state: 'registered' } \| { state: 'refused'; reason: object } \| { state: 'unregistered' }>` | optional | The automation engine's verdict on each listed flow, keyed by the item's `name`: one entry per listed item that is not a pending draft. Present only on a `flow` list answered by a process that runs an automation service; absent otherwise, and absence means no engine judged them, never `registered`. The verdict is the answering process's engine. Beside the items, never inside one: an item's `status` stays what its author declared. |
1527+
| **disabledActions** | `Record<string, string[]>` | optional | For each listed object, keyed by the item's `name`: the names in its `actions` that this deployment's activation ledger switches off, so a call to any of them is refused 409 `ACTION_DISABLED` (the item read's `disabledActions`). One entry per listed object that is not a pending draft, `[]` when none of its actions is switched off. Present only on an `object` list answered by a process whose data engine has an activation ledger attached; absent otherwise, and absence means no ledger answered, never that nothing is switched off. Beside the items, never inside one: each item's `actions` is served as declared. |
15251528

15261529
### Nested Shape: `GetMetaItemsResponse.registrations[string][state='registered']`
15271530

0 commit comments

Comments
 (0)