Skip to content

Commit 96d3bf9

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/project-thread-pxk0us
2 parents ad3a211 + d31329f commit 96d3bf9

270 files changed

Lines changed: 11125 additions & 706 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: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/objectql': minor
4+
'@objectstack/metadata-core': minor
5+
'@objectstack/metadata': minor
6+
'@objectstack/plugin-security': minor
7+
'@objectstack/plugin-sharing': minor
8+
'@objectstack/plugin-approvals': minor
9+
'@objectstack/lint': minor
10+
'@objectstack/runtime': minor
11+
'@objectstack/platform-objects': minor
12+
'@object-ui/app-shell': minor
13+
'@objectstack/skills': minor
14+
'@objectstack/cli': minor
15+
---
16+
17+
feat!: ownership queues: records wait in a queue until a member claims them, and assignment rules route new records (ADR-0142)
18+
19+
Clause-②: yes (narrowing)
20+
Metadata-protocol: narrowed
21+
22+
<!-- adr-0087: registered object-queue-field-reserved -->
23+
24+
**BREAKING** for one shape only: an object whose records have a user owner (`ownership: 'user'`, the default) can no longer declare its own field named `queue`. The platform now injects a `queue` column on every such object, and `registerObject` refuses the declaration with `422 INVALID_METADATA` (`ObjectQueueColumnRefusedError`), naming the object. An `objectExtensions` entry that declares `queue` on such an object is not refused, because an extension is folded over its object at read time: its `queue` is ignored, the platform's column stands, and a warning names the extension. The remedy: rename the business field (for example `support_queue`) and copy its data across, or, when the object's records have no user owner, declare `ownership: 'business_unit'`, `'org'` or `'none'`. The upgrade guide entry is `object-queue-field-reserved`.
25+
26+
Beside that, two enum additions: `ShareRecipientType` gains `queue` (a sharing rule can now name a queue as its recipient), and the `queue` approver type is authorable again (it leaves `NON_AUTHORABLE_APPROVER_TYPES`, so it is no longer marked deprecated). Two metadata types are new: `queue` and `assignment_rule`.
27+
28+
- **`@objectstack/spec`**: new `queue` and `assignment_rule` metadata types (`QueueSchema`, `AssignmentRuleSchema`) and the stack keys `queues` and `assignmentRules`. A declared queue's members are positions. New `IOwnershipQueueService` contract, registered under `ownership-queue`. `queue` joins the injected system columns of `ownership: 'user'` objects and `PUBLIC_FORM_SERVER_MANAGED_FIELDS`. `APPROVER_VALUE_BINDINGS.queue` reads both a declared queue and an organization's `sys_queue` row, and `APPROVER_ORG_SCOPED.queue` is `true`. The explain rule kinds `queue_membership` and `queue_claim` are new. `QUEUE_NOT_FOUND` (422) is registered under plugin-security.
29+
- **`@objectstack/objectql`**: objects with `ownership: 'user'` get a system-managed `queue` column beside `owner_id`. Declaring your own `queue` field on one is refused at registration (`INVALID_METADATA`), and an extension's own `queue` field on one is ignored with a warning. An update can carry a write precondition: when it no longer matches, the update answers `409 CONCURRENT_UPDATE`. Declared assignment rules run once for each inserted row that has no owner and no queue after the `beforeInsert` hooks. A rule can park the row in a queue, or pick a member by `round_robin` or `least_loaded`. Assignment never refuses an insert. `kernel:ready` refuses to boot a stack that declares queues or assignment rules when no `ownership-queue` service is registered.
30+
- **`@objectstack/metadata-core`**: `queue` is classified as an injected system column.
31+
- **`@objectstack/metadata`**: the artifact door registers a stack's `queues` and `assignmentRules`, so an artifact-only runtime serves them.
32+
- **`@objectstack/plugin-sharing`**: three new objects, `sys_queue`, `sys_queue_member` and `sys_assignment_cursor`, and the `ownership-queue` service. The service resolves a queue name from the registry first, then from the organization's own rows, and has one member expander. A record with no owner that sits in a queue can be read and edited by the queue's members, never deleted. The `queue` sharing recipient grants matched records to the queue's members, and its grants are recomputed when membership changes or the queue is switched on or off.
33+
- **`@objectstack/plugin-security`**: the owner gate handles `queue`. A **claim** sets `owner_id` from empty to yourself, by id. It is admitted when the sharing model allows you the record, and only one claimant wins: the loser gets `409`. A **release** gives your own record back to its home queue. A **move** to another queue needs the transfer grant, and a queue name that does not resolve is refused with `422 QUEUE_NOT_FOUND`. An insert that names a queue needs the transfer grant or membership of that queue. A guest can supply neither. Explain reports the queue reasons on the sharing layer.
34+
- **`@objectstack/plugin-approvals`**: a `queue` approver resolves to the queue's members when the step opens. An empty queue adds nobody and logs a warning. The `queue:<name>` placeholder is never written again.
35+
- **`@objectstack/lint`**: a queue-only approval node is reported `approval-approvers-may-resolve-empty`, and the org-axis red line admits the queue recipient. Two new advisory rules come with `os explain` entries: `assignment-rule-user-target` (warning) and `assignment-rule-queue-unstaffed` (info).
36+
- **`@objectstack/cli`**: the organization-ownership inventory lists `sys_queue`, `sys_queue_member` and `sys_assignment_cursor`, so `os migrate organization-ownership` plans them instead of refusing.
37+
- **`@objectstack/runtime`**: `POST /security/_activation/:type/:name` also switches a declared `queue` or `assignment_rule` on and off.
38+
- **`@objectstack/platform-objects`**: the two new metadata types' labels, and the queue recipient option, are translated in zh-CN, ja-JP and es-ES.
39+
- **`@object-ui/app-shell`**: the flow designer no longer warns that a queue approver resolves to nobody.
40+
- **`@objectstack/skills`**: the platform skill lists `queues` and `assignmentRules` among the stack's top-level keys.
41+
42+
What to do: rename any `queue` field on a user-owned object, or in an extension of one, as above. Every other existing stack behaves as before. To use queues, declare `queues` and `assignmentRules` in your stack, and make sure plugin-sharing is installed: it registers the `ownership-queue` service.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@object-ui/plugin-grid': patch
3+
---
4+
5+
refactor(plugin-grid): `ObjectGrid`'s row-height sync, column state and mobile affordances move into three sibling hooks
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
Three adjacent blocks at the top of the `ObjectGrid` component move verbatim out of `ObjectGrid.tsx`. The effect that follows a changed `schema.rowHeight` goes to `useObjectGridRowHeightSync` (`objectGridRowHeight.ts`). The column-state persistence (the storage key, the `columnState` seed, the sync of a host-supplied `columnState` and `saveColumnState`) goes to `useObjectGridColumnState` (`objectGridColumnState.ts`). Pull to refresh and the card-view breakpoint go to `useObjectGridMobile` (`objectGridMobile.ts`). `ObjectGrid` calls each hook where its block stood, so its hook order, its stored column state and its rendered output do not change. No name is renamed, and the package entry is 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+
'@objectstack/spec': minor
3+
'@objectstack/cli': minor
4+
---
5+
6+
feat(spec): a joined report's block headings and a master-detail field's inline grid heading are translatable
7+
8+
Clause-②: yes
9+
Metadata-protocol: added
10+
11+
Two authored, user-visible labels had no translation slot, so a zh-CN user read them in the authoring language and `os i18n check` could not see the gap:
12+
13+
- **A joined report's blocks.** The `reports` translation group declares `blocks`, keyed by the block's `name`: `reports.<report>.blocks.<block>.label` / `.description`. `translateReport` overlays them on each block of the served report (`GET /meta/report`), which is what the joined renderer draws above each block.
14+
- **A field's `inlineTitle`.** `FieldTranslationSchema` declares `inlineTitle`: `objects.<child>.fields.<field>.inlineTitle`, on the child object whose master-detail (or lookup) field declares the heading. `translateObject` overlays it on the served child object (`GET /meta/object`), which is what the console copies onto the parent's form as the inline grid's heading.
15+
16+
Both follow the packaged-base rule (ADR-0029 D9.2a): a string an org re-authored on top of the packaged declaration keeps its text. An authored `inlineTitle` with no entry keeps its authored text; the child object's plural label is never substituted for it.
17+
18+
`os i18n extract` scaffolds both keys and `os i18n check` counts them: a block's `label` / `description` under the report bucket, `inlineTitle` under the field bucket. A block without a `label`, or a field without an `inlineTitle`, is recorded without being demanded.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@object-ui/plugin-detail': patch
3+
---
4+
5+
fix(plugin-detail): `record:path` leaves out a stage the record's status option rules out, instead of ticking it
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
Ruling-ref: #26581 ruling `6442747364` letter A, with its guardrail
10+
11+
A path ticked every stage before the current one by position, so a stage the record could never have stood on read as passed: an order contract that skips legal review by type showed 「审查中 ✓」 after approval. The status field's option already says which records it applies to. Its `visibleWhen` means "offered only when TRUE (else omitted)", and the server refuses a write of a value whose predicate is false. So `record:path` now omits, on both the desktop and the mobile row, any stage whose matching option declares a `visibleWhen` that is FALSE for the record.
12+
13+
- Stages match options by value, so authored `stages[]` are filtered the same way as stages derived from the picklist.
14+
- A predicate that reads the acting user (`current_user`, `user`, `ctx` or `os`) hides nothing. It says who may pick the value, not which route the record takes.
15+
- A predicate that cannot be evaluated keeps its stage, as an option list keeps an option (render side fail-open, ADR-0137 D3).
16+
- The stage the record stands on is always drawn.
17+
18+
No new key and no new read: the predicate is evaluated against the record and object definition the record page already holds.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
'@object-ui/i18n': patch
4+
---
5+
6+
fix(app-shell): after an approver's own decision ends their read, the record page says so instead of "Record not found"
7+
8+
Clause-②: yes
9+
Metadata-protocol: none
10+
11+
A pending approver with no standing access reads the record only while their approval step waits, and the server revokes that read when the step ends. The record page re-reads the record after every decision taken from its approval bar, so for that approver the re-read answered 404 and the page drew "Record not found" right under the "Approved" toast, for a record that exists.
12+
13+
What changes:
14+
15+
- When that re-read answers 404 right after this viewer's own decision on this record, the page draws "Decision recorded" with "Your approval step is complete, so your access to this record has ended." and a link to the approvals inbox (`/apps/APP/system/approvals`, in the app the page is open in).
16+
- Any other 404 still draws "Record not found": a record that never existed, a record opened with no decision, and another record opened after the decision.
17+
- An approver who keeps standing access still sees the record after deciding, as before.
18+
- The read is not kept any longer than the step: this is copy, not access.
19+
- `@object-ui/i18n` adds `empty.recordAccessEnded` and `empty.recordAccessEndedDescription` in all ten locales. The link reuses `notifications.openApprovalsInbox`.
20+
21+
What to do: nothing.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/formula': minor
3+
'@objectstack/objectql': minor
4+
---
5+
6+
feat(formula): `label(record.<field>)` reads a select field's option label in the reader's locale, so a record name composed in a formula renders `v2 · 清稿` to a zh-CN reader and `v2 · Clean` to an en-US one (#26608)
7+
8+
Clause-②: yes (widening)
9+
Metadata-protocol: added
10+
11+
A select field stores its option's value, so a formula `nameField` such as `'v' + string(record.version_no) + ' · ' + record.kind` printed `v2 · clean` to every reader. Wrap the field in `label()` and the name prints the label the select control shows that reader.
12+
13+
- **The function (`@objectstack/formula`).** `label(record.<field>)` is a new CEL stdlib function, advertised in `CEL_STDLIB_FUNCTIONS`. It takes exactly `record.<field>`; any other argument is refused when the expression compiles. It reads only data: the new `EvalContext.optionLabels` (field → stored value → label, type `EvalOptionLabels`), which the caller resolves in the reader's locale before evaluating. An empty value reads `''`; a value no option declares reads as itself; a field with no options, a multi-value select, and an evaluation whose context carries no `optionLabels` fail loudly.
14+
- **Where it answers (`@objectstack/objectql`).** The engine pins the map for every read and write it serves formula fields on: `find`, `findOne`, and the insert and update responses. The labels are the object's declared `options` with the i18n service's bundle laid over them by `translateObject`, the function `GET /meta/object/:name` serves the object with, in `ExecutionContext.locale` (the request's `Accept-Language`). The field-level key `objects.<object>.fields.<field>.options.<value>` wins over a shared picklist's `picklists.<picklist>.options.<value>`, and both over the declared label. No new keys and no new bundle. A read with no locale, an engine with no i18n service, and an i18n service that throws all read the declared labels.
15+
- **Stored values.** A stored computed field (a non-formula field with an `expression`) is computed once, on the write, so it stores the declared label whatever the writer's locale. Search never indexes formula fields, so what search indexes is unchanged.
16+
- **`setI18nService`** accepts the optional `getTranslations`, `getFallbackLocale` and `getDefaultLocale` the engine now reads. An `II18nService` already carries all three.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
'@objectstack/spec': patch
3+
'@objectstack/service-automation': patch
4+
'@object-ui/app-shell': patch
5+
---
6+
7+
feat(automation): a paused screen says whether another screen can follow, and the screen-flow runner reads Next or Submit and counts steps from it
8+
9+
Clause-②: yes (widening)
10+
Metadata-protocol: none
11+
12+
A screen-flow wizard's primary button read Submit on every step, so a requester believed the whole wizard was submitted on step 1, and no step counter could be drawn: the paused screen carried nothing about the screens after it, and a flow can branch after any screen, so only the engine can tell.
13+
14+
- **`@objectstack/spec` — `ScreenSpec.progress`** (optional, `ScreenProgressSpec`) on the paused screen a run serves, and its wire spelling on `TriggerFlowResponseSchema`. `nextScreen` is `'none'` (no path out of this screen reaches another one: the last step), `'certain'` (every path pauses on another screen first) or `'possible'` (a branch, a fault path, a loop, a pause that serves no screen, or a `subflow` decides — the engine does not guess). `step` (1-based) and `stepCount` are present only when every path agrees on them. Nothing a metadata author writes changes.
15+
- **`@objectstack/service-automation`** fills it from the flow graph of the run when a `screen` node pauses (`readScreenProgress`). A screen that never pauses is not counted; one the run's caller may already have answered counts as possible, never certain. A screen inside a child run (`subflow`) carries only `nextScreen` when its own graph shows another screen, and no `progress` when it does not, because the parent may continue with screens of its own.
16+
- **`@object-ui/app-shell` — `FlowRunner`** labels the flat step's button `common.next` while another screen is certain or possible and `common.submit` on the last, and draws `form.stepOf` beside the flow's name only when both `step` and `stepCount` are served. A screen served without `progress` keeps Submit and no counter, as before.
17+
18+
What to do: nothing. A custom runner that reads the paused screen can read `progress` the same way.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@object-ui/components': patch
3+
'@object-ui/types': patch
4+
---
5+
6+
fix(ui): a `destructive` action block confirms through the destructive button (#26665)
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
The four action blocks (`action:button`, `action:icon`, `action:group`, `action:menu`) now forward their `variant` to the action runner. `@objectstack/spec` spells the value in two dialects, each on its own surface: the blocks' rows take the Button primitive's vocabulary plus `primary`, and `ActionSchema`, which the runner dispatches, takes `primary | secondary | danger | ghost | link`. The forward translates: `destructive` becomes `danger`, `primary` / `secondary` / `ghost` / `link` pass through, and `default` / `outline` forward nothing. So a block declared `variant: 'destructive'` with a `confirmText` now asks its question through the destructive confirm button, where before it asked through a neutral one.
12+
13+
`UIActionSchema.variant` (`@object-ui/types`) now takes the block rows' vocabulary by reference (`ActionButtonProps['variant']`), which adds `link`, a value those rows already accept. `danger` stays refused there, as the block rows refuse it.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
feat(cli): `os migrate audit-key-material` removes the encrypted key-material columns from the audit-ledger copies 18.2.0 and earlier wrote
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
**Run `os migrate audit-key-material --apply` once after upgrading from 18.2.0 or earlier.** The audit writer no longer copies the encrypted key-material column of a secret-store or settings row into `sys_audit_log` (`new_value`, `old_value`) or `sys_activity` (both halves of `metadata`). Copies written before that still carry it.
11+
12+
- **The command.** `os migrate audit-key-material` removes those columns with the writer's own helper, through `@objectstack/plugin-audit`'s `migrateKeyMaterialCopies` (the hot tables) and `migrateArchivedKeyMaterialCopies` (the archive datasource `sys_audit_log` archives to). It is a dry run by default (counts, writes no row), `--apply` writes, and its flags are `os migrate audit-mail-bodies`'s, one for one (`--database-url`, `--apply`, `--yes`, `--force`, `--json`). The report is counts only, per table, and never prints a value.
13+
- **What changes in a row.** Only the key-material keys of the copy are removed. Every other key of the copy and every other column of the row stay byte-identical, the row's `updated_at` included. A column of the same name in a copy of any other object is left alone.
14+
- **Idempotent.** A copy without the columns has nothing to remove, so a second run rewrites nothing; re-running finishes an interrupted run. A failed read or write exits `1`, and so does an archive datasource the run could not examine (`not-connected` / `unknown` in the `--json` document's `archive` entries, as for `audit-mail-bodies`).

‎AGENTS.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,8 +276,8 @@ localStorage / auth gotchas.
276276
the governed files. **The landing is tiered by the register; one Tier H path makes the whole PR Tier H.** **Tier H**
277277
(人合: every register row except `.claude/**`): those four lift only for an
278278
authorized APPROVED review by an account in `GOVERNED_APPROVERS`, on ANY commit and not dismissed; that word is
279-
spent once per PR — the OWNING seat then lands it, later pushes included; the director seat requests the word as
280-
ONE batch of at most five rows, and the click stays the maintainer's. **Tier S** (席内达档复核落地: all of
279+
spent once per PR — the OWNING seat then lands it, later pushes included; the click stays the maintainer's.
280+
**Tier S** (席内达档复核落地: all of
281281
`.claude/**`): those four lift once the PR thread or its card carries a `## Contract review` record for the PR's
282282
current head with `Served-tier: CONTRACT_REVIEW_TIER` and a PASS verdict and every check is green — the owning
283283
seat then lands it through the queue; the post-merge audit is the compensating control. A PR whose only governed

0 commit comments

Comments
 (0)