Skip to content

Commit 85d27dc

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/project-thread-39lx12
Claude-Session: https://claude.ai/code/session_01FEkQ5oZ6me4dwmHNpWg6kT Co-authored-by: Claude <noreply@anthropic.com>
2 parents e77aac4 + 6746140 commit 85d27dc

106 files changed

Lines changed: 3968 additions & 1779 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: 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: 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 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: 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+
fix(app-shell): on a narrow window, a toast raised beside the record drawer no longer covers the record's Edit and ⋯ buttons
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
When a right-edge drawer is open and the window is too narrow for a toast beside it (a 720px record drawer on a 1024px window, a split screen, a phone), the console's toaster drops below the drawer's header. It cleared only the drawer's own header, so the success toast raised by creating a record sat on the record header's Edit button and ⋯ menu for its whole four seconds. The toast now also drops below the drawer's first row of controls, measured from the drawer's layout, so it clears the record's actions row for any drawer content. Wide windows keep the toast in the strip beside the drawer, as before.
11+
12+
What to do: nothing.

‎.github/actions/setup-pnpm/action.yml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@
5656
# written. The separation is kept anyway, because the callers' own pins are
5757
# already in place and moving them buys nothing -- ⛔ it is no longer a
5858
# constraint, and a future composition is free to hold one.
59+
#
60+
# `.github/actions/setup-workspace` is that composition: it runs setup-node,
61+
# then this action, then the pnpm store and the install. Most jobs call it;
62+
# the ones that still call this action directly are named in its header.
5963

6064
name: Setup pnpm
6165
description: >-
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
#
3+
# setup-workspace -- the one setup sequence every job that installs the
4+
# workspace runs after its checkout: Node from .nvmrc, the pinned pnpm, the
5+
# pnpm store restored from cache, `pnpm install --frozen-lockfile`, and the
6+
# store saved back on `push` and `schedule` runs only.
7+
#
8+
# Why one place. The same five steps were copied by hand into about forty
9+
# jobs, each carrying its own `node-version` literal and its own copy of the
10+
# store cache key, so a change to any of them was a forty-site edit and a
11+
# site that missed it drifted silently. `scripts/check-node-version.mjs`
12+
# reads `.github/actions/**` as well as the workflows, so the Node pin here is
13+
# audited like any other.
14+
#
15+
# The store is RESTORED on every run and SAVED only on `push` and `schedule`.
16+
# A pull request or merge-queue run that touches the lockfile used to post-save
17+
# a multi-GB store entry under its own key into the same 10 GB Actions cache
18+
# pool that holds `main`'s Turbo seeds, and those entries evicted the seeds.
19+
# `main`'s push and scheduled runs keep the store warm for everyone: a cache
20+
# entry saved on the default branch is restorable from every pull request and
21+
# queue branch, while one saved on a pull request is restorable only from that
22+
# pull request. ⛔ No `pull_request` or `merge_group` run saves the store.
23+
#
24+
# The save is a main-phase step right after the install, not `actions/cache`'s
25+
# post-job save: the store changes only during the install, and a composite
26+
# action has no post phase of its own. It is `continue-on-error` -- a store
27+
# that failed to upload says nothing about the code under test.
28+
#
29+
# Pnpm is acquired through `.github/actions/setup-pnpm` (the cached Corepack
30+
# store), called as a step here rather than inlined, because three callers
31+
# still use it directly and keep their own steps:
32+
# - ci.yml's five ui jobs (`ui-deps`, `type-check-ui`, `test-ui`, `lint-ui`,
33+
# `build-e2e-ui`), whose steps pins under `ui/scripts/__tests__/` read by
34+
# name and by text;
35+
# - governed-surface-guard.yml, whose toolchain steps are each gated and
36+
# each degrade on their own, pinned one by one by
37+
# `scripts/pm/check-governed-queue-guard.mjs --self-test`;
38+
# - release.yml's `release-integrity`, which installs the version commit's
39+
# tree, not the checkout.
40+
#
41+
# Inputs:
42+
# install 'false' stops after pnpm is on PATH: no store, no install.
43+
# For jobs that run pnpm but install nothing.
44+
# install-args appended to `pnpm install --frozen-lockfile`.
45+
# save-store 'false' never saves the store, on any event. For jobs that
46+
# deliberately restore only.
47+
48+
name: Setup workspace
49+
description: >-
50+
Set up Node from .nvmrc and the repo-pinned pnpm, restore the pnpm store,
51+
run `pnpm install --frozen-lockfile`, and save the store on push and
52+
schedule runs only.
53+
54+
inputs:
55+
install:
56+
description: "'false' sets up Node and pnpm only, with no store cache and no install."
57+
required: false
58+
default: 'true'
59+
install-args:
60+
description: Extra arguments appended to `pnpm install --frozen-lockfile`.
61+
required: false
62+
default: ''
63+
save-store:
64+
description: "'false' never saves the pnpm store. Otherwise it is saved on push and schedule runs, on a miss of its exact key."
65+
required: false
66+
default: 'true'
67+
68+
outputs:
69+
store-path:
70+
description: The pnpm store directory (empty when `install` is 'false').
71+
value: ${{ steps.store.outputs.path }}
72+
store-cache-hit:
73+
description: "'true' when the store was restored from its exact key."
74+
value: ${{ steps.restore.outputs.cache-hit }}
75+
76+
runs:
77+
using: composite
78+
steps:
79+
- name: Setup Node.js
80+
uses: actions/setup-node@v7
81+
with:
82+
node-version-file: .nvmrc
83+
# pnpm is not on PATH yet, and a setup-node that caches for the
84+
# `packageManager` it reads shells out to it and dies here
85+
# (partof-closing-keyword-guard.yml carries the receipt). The store is
86+
# cached by the steps below instead.
87+
package-manager-cache: false
88+
89+
- name: Setup pnpm
90+
uses: ./.github/actions/setup-pnpm
91+
92+
- name: Get pnpm store directory
93+
id: store
94+
if: inputs.install != 'false'
95+
shell: bash
96+
run: |
97+
path="$(pnpm store path --silent)"
98+
echo "path=$path" >> "$GITHUB_OUTPUT"
99+
echo "STORE_PATH=$path" >> "$GITHUB_ENV"
100+
101+
# The one definition of the store cache key. `-v3-` and the lockfile hash
102+
# are unchanged from the hand-copied steps, so the entries `main` already
103+
# holds stay valid.
104+
- name: Restore pnpm store
105+
id: restore
106+
if: inputs.install != 'false'
107+
uses: actions/cache/restore@v6
108+
with:
109+
path: ${{ steps.store.outputs.path }}
110+
key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }}
111+
restore-keys: |
112+
${{ runner.os }}-pnpm-store-v3-
113+
114+
- name: Install dependencies
115+
if: inputs.install != 'false'
116+
shell: bash
117+
env:
118+
INSTALL_ARGS: ${{ inputs.install-args }}
119+
run: |
120+
# shellcheck disable=SC2086 # INSTALL_ARGS is a word list on purpose.
121+
pnpm install --frozen-lockfile $INSTALL_ARGS
122+
123+
- name: Save pnpm store
124+
if: >-
125+
inputs.install != 'false'
126+
&& inputs.save-store != 'false'
127+
&& (github.event_name == 'push' || github.event_name == 'schedule')
128+
&& steps.restore.outputs.cache-hit != 'true'
129+
continue-on-error: true
130+
uses: actions/cache/save@v6
131+
with:
132+
path: ${{ steps.store.outputs.path }}
133+
key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }}

‎.github/dependabot.yml‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,13 @@ updates:
2323
- "minor"
2424
- "patch"
2525

26-
# Enable version updates for GitHub Actions
26+
# Enable version updates for GitHub Actions. The composite actions under
27+
# .github/actions/ pin actions of their own (setup-node, actions/cache), and
28+
# Dependabot reads only the directories listed here.
2729
- package-ecosystem: "github-actions"
28-
directory: "/"
30+
directories:
31+
- "/"
32+
- "/.github/actions/*"
2933
schedule:
3034
interval: "monthly"
3135
time: "02:00"

‎.github/workflows/board-snapshot.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ jobs:
206206
- name: Setup Node.js
207207
uses: actions/setup-node@v7
208208
with:
209-
node-version: '22'
209+
node-version-file: .nvmrc
210210

211211
# No `pnpm install`: the archiver imports only `node:` builtins, global
212212
# `fetch`, and two repo-local modules — no npm dependency, so installing

0 commit comments

Comments
 (0)