Skip to content

Commit 0c15e5f

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/project-thread-26wk5s
# Conflicts: # ui/QUICK_REFERENCE.md # ui/README.md # ui/packages/plugin-ai/README.md # ui/scripts/__tests__/package-files-exist.test.ts # ui/scripts/markdown-test-inputs.mjs
2 parents bde1e72 + acc9c57 commit 0c15e5f

1,918 files changed

Lines changed: 106986 additions & 38870 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+
'@objectstack/objectql': patch
3+
---
4+
5+
fix(objectql)!: inside one bulk create, a child rule that reads a parent summary counts the earlier rows of the same batch its rules admitted, so a batch is judged as its rows would be one by one (#25579)
6+
7+
Clause-②: no (narrowing) — a bulk create (`insert(array)`, `insertMany`, and the doors on them: `createManyData`, `insertManyData`, the import's batched creates) refuses a row whose rule reads a parent `summary` that the earlier admitted rows of the same batch move past the rule's limit, which it admitted before. Nothing is widened.
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, and no export or type changes: an existing validation rule is now enforced on the rows of one bulk create the way it is already enforced one write at a time. The refusal is the rule's own VALIDATION_FAILED, worded by its author, so there is nothing for a migration to rewrite and no tombstone. -->
11+
12+
**BREAKING** accept-set narrowing at the bulk insert (`@objectstack/objectql`), served by every bulk create door.
13+
14+
- **Before.** A bulk insert read the related rows its rules read through a reference once, before any row of the batch was written, and recomputed parent summaries once, after. So "each supplier has at most one primary contact" (a `count` summary filtered on `{ is_primary: true }`, and the child rule `record.is_primary == true && record.supplier.primary_contact_count > 0`) refused a second primary contact written on its own, but admitted two primary contacts for one supplier in one import: both rows read `0`.
15+
- **Now.** Each row's rules read the parent summary as it stands after the earlier rows of the same batch that the rules admitted. The batch's admitted rows are accumulated in memory per summary and per parent; ⛔ nothing is re-read from the database per row. In the card's import, one contact is created and the other row reports the rule's error. A multi-row insert preview (`validate` with an array, mode `insert`) answers the same way.
16+
- **Unchanged.** Which rows land and how a refusal is reported stay each door's own: `insertMany` / `insertManyData` (the import) report the refused row and write the rest; `insert(array)` / `createManyData` refuse the whole batch. A row the rules refuse is not counted. A single write reads the stored summary as before. The summary written after the batch is the same recompute as before.
17+
- **Which summaries accumulate.** `count`, and `sum` / `min` / `max` over number values, with the summary's `filter` evaluated on the new row. An `avg` summary, a `sum` / `min` / `max` that meets a value that is not a number, and a summary whose `filter` reads a dotted path through another reference keep reading the stored value, as before.
18+
- **Not covered.** Concurrent writes: two writes in flight at the same time each read the summary they started from (批量等于逐条,并发不保证).
19+
- **Who sees it.** An app whose child rule reads a parent summary and that bulk-creates children: a batch that broke the rule was admitted whole before, and now its offending rows are refused.
20+
21+
**The remedy.** None needed for a rule that meant what it says. A batch that must land whole has to satisfy the rule row by row, in order.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
'@objectstack/core': patch
3+
---
4+
5+
fix(core): an import row with several bad cells reports every one of them in `errors` (#25582)
6+
7+
Clause-②: yes (widening: a failed import row whose cells did not coerce gains the optional `errors` list, `ImportRowResultSchema.errors`; every existing key keeps its value)
8+
Metadata-protocol: none
9+
Ruling-ref: #25582 ruling `6377844879` letter A, proposal 4; the spec seat's fill rule `6434667735`
10+
11+
A row with several bad cells used to report only the first one, so an operator fixed one cell, imported again, and only then saw the next. The row now carries `errors`, one `{ field, code, message }` element per failed cell, in column order. The first-error keys `field`, `code` and `error` are unchanged and name element 0, so a reader of those keys sees the same answer as before.
12+
13+
- It reaches every path that reports rows: the dry run and the commit, on the synchronous `POST /api/v1/data/:object/import` and on the async import job's results.
14+
- It is filled only from the import's cell-coercion errors (`invalid_date`, `invalid_number`, `invalid_option` and the rest). A row that failed for another reason omits it: the dry run's validate verdict, a refused write, a duplicate or ambiguous match key.
15+
- `ImportRowResult` (`@objectstack/core`) gains the optional `errors?: ValidateDataIssue[]`.
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): `os lint` tells an author that a field's `externalId: true` does nothing
6+
7+
Clause-②: no (a liveness-ledger re-grade: no schema, accept set, served body or error code moves; only the advisory `liveness-dead-property` warning on an authored `externalId: true` starts, and the flag's description is reworded)
8+
Metadata-protocol: none
9+
10+
The shipped liveness ledger (`liveness/field.json`) graded a field's `externalId` flag `live` on the strength of one renderer module that was deleted for having no importers. Re-measured across `packages/**` and `ui/**`, nothing reads the flag: the Studio field inspector edits it and a metadata summary copies it, and no upsert matches on it. The row is now `dead`, so `os lint` warns on a field that sets `externalId: true` (the default `false` is not warned). The flag's schema description, and the reference pages generated from it, no longer say it drives upserts.
11+
12+
- **What to do.** Remove the flag. To match seed records on a field, name the field in the seed dataset's `externalId` (`name` when it is left out). For an import, name it in the mapping's `upsertKey`.
13+
- **Unchanged.** Parsing (the key is still accepted, default `false`), seed loading, imports and every upsert.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/lint': patch
4+
'@objectstack/metadata-core': patch
5+
---
6+
7+
feat(spec)!: retirement kits whose conversion retired at or below protocol 16.x leave the tree; the strict unknown-key refusal names the protocol upgrade guide
8+
9+
Clause-②: no (narrowing)
10+
Metadata-protocol: narrowed
11+
12+
**BREAKING** for a source or a stored `sys_metadata` row still carrying a key whose ≤16.x kit aged out.
13+
14+
- **The rule (ADR-0087, amended 2026-10-26, #25767 ruling D2 = A as scoped by ruling 6434038284).** A retirement kit — the `retiredKey()` tombstone and its D2 conversion with fixtures — lives through the protocol major after the one it retired in and is removed when the next major is cut. At 18 that removes the kits whose conversion retired at or below 16.x; 19 repeats the move one major on. The kit ages out; the migration chain's step stays.
15+
- **Before.** Such a key met a `retiredKey()` tombstone: the parse refused it with the key's own prescription in the error, `os migrate meta --from 16` listed and applied the mechanical edit, and a stored row in that shape was rewritten at read by the ≤16.x conversions.
16+
- **Now.** Where the tombstone sat in a strict container, the key is gone from the shape, so a `strictObject` refuses it as an unknown key. The refusal carries one more sentence, after its fix channels and ahead of its history sentence (`RETIRED_KEY_UPGRADE_GUIDE_SENTENCE`, exported with `PROTOCOL_UPGRADE_GUIDE_URL` from `@objectstack/spec`): it says that keys retired before the previous major are no longer prescribed in the error and names the protocol upgrade guide, `https://objectstack.ai/docs/protocol-upgrade`, where the prescription lives; the CHANGELOG of the release that retired the key carries it too. A tombstone that stays (its container strips unknown keys) ends its error with a sentence that says the edit left `os migrate meta` with the 16.x conversions, to be applied by hand or on a release that still carries the conversion, and links the guide.
17+
- **The chain stays.** `MIGRATION_SUPPORT_FLOOR` stays 16; step 17 and its entry files stay, and `os migrate meta --from 16` still replays 16 → 18, listing every surviving conversion and semantic to-do. Step 17's graduated list drops the ids of the 45 conversions that aged out, so their edits are no longer applied for you.
18+
- **Stored rows.** A `sys_metadata` row still carrying such a spelling is no longer rewritten at read; canonicalize it once with `os migrate meta --stored --apply` on 18.2 or earlier, before moving to 18.3. `app-hidden-to-unpublished` is among the removed conversions, so a pre-split `hidden: true` app row is no longer rewritten to `_unpublished` by any seam, and the artifact door's refusal of it by id is removed as moot (`@objectstack/metadata-core`).
19+
- **What left.** 50 D2 conversions with `retiredAfter` at or below 16.x (5 at 15.0.0, 45 at 16.1.0) and their fixtures; 55 tombstones already `[RETIRED]` at `@objectstack/spec@17.0.0` whose container is strict; 61 `[RETIRED]` lines of the authorable surface and their liveness-ledger rows. 58 same-age tombstones whose container strips unknown keys stay, because deleting one would turn a refusal into a silent strip; the four inline-credential refusals are not retirements and are untouched.
20+
- **The deletion gate.** `check:authorable-surface` proof 1 ages a tombstone one major (was two). A deleted `[RETIRED]` line is dated by an exact `RETIRED_KEYS_BY_MAJOR` row or, new, by the previous major's release tag: a line already `[RETIRED]` on the surface at `@objectstack/spec@<current major − 1>.0.0` was retired before that cut. The tag is immutable, so neither a row a PR writes nor one it deletes moves the clock; where the tag cannot be read the gate dates by the registry alone and says so. No gate was added.
21+
- **Who moves.** No `examples/**`, `ui/` or `hotcrm` source in this repository writes a removed key (the `ui/` hits for the removed names are the console's own same-named props and type mirrors, not readers of the retired spec keys, per the liveness ledger's measurements). A consumer source still carrying one meets the strict refusal and the guide link at `os validate`.
22+
23+
<!-- adr-0087: not-required (no-migration-prescription) the diff registers no retirement: it removes the ≤16.x kits that aged out under the 2026-10-26 ruling (ADR-0087 D2 = A), and nothing here prescribes a rewrite — each removed key's prescription lives in the protocol upgrade guide and in the CHANGELOG of the release that retired it -->
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.

0 commit comments

Comments
 (0)