Skip to content

Commit 4f1e0a4

Browse files
committed
Merge origin/main into claude/project-thread-l8wnbr
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5ni5yjiTswP6crgAkX
2 parents 5f0ffc5 + 1caf672 commit 4f1e0a4

69 files changed

Lines changed: 3437 additions & 274 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: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/platform-objects': patch
3+
---
4+
5+
fix(platform-objects): the Setup Users list and the user record's highlight chips stop showing better-auth's legacy `role` scalar as the platform role (#28105)
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
`sys_user.role` is the column better-auth's admin plugin needs. ObjectStack never writes it: when the plugin is off it is empty, and when it is on the plugin stamps `user` on every new user, the seeded platform admin included. Platform-admin standing comes from ADR-0068 D2 (the `OS_PLATFORM_OWNER_EMAIL` list, or an unscoped `admin_full_access` grant under the `single` posture), not from this column. So a "Platform Role: user" cell on a platform admin was wrong, and it read as authoritative to people and to agents.
11+
12+
- **Users list.** The `all_users` view drops the `role` column. Its columns are now name, email, phone, email verified, banned, identity source and created. `banned` stays (#27863).
13+
- **User record page.** The `record:highlights` chip strip on `sys_user_detail` drops `role`. It shows email, phone and email verification. The details grid still hides `role`, so the record page does not draw it.
14+
- **The field is unchanged.** `sys_user.role` stays a readonly text field in its Admin group, with the same label, storage and description, readable through the data API. Nothing synchronises platform-admin standing into it, and the better-auth `defaultRole` stamp and the import / create-user pass-through are untouched.
15+
16+
A list column that shows real platform-admin standing is tracked separately (#28793).
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@object-ui/app-shell': patch
3+
'@object-ui/i18n': patch
4+
---
5+
6+
feat(studio): customize a read-only base's flow into its pair under the flow's own name, with one ledger switch, the override badge and "Restore package original"
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
On a read-only base, Studio's Automations pillar now customizes a flow the way the Interfaces pillar customizes a view, a dashboard or a report (ADR-0146 D9 items 1 to 3, D2, D4).
12+
13+
- **Customize this flow.** The flow's header offers Customize, with the same control, the same ensure door (`POST /api/v1/packages/:id/ensure-customization`) and the same disabled reasons as the other customizable types. A walled tenant admin sees it disabled. Pressing it only unlocks the flow's editors. Nothing is written until an edit is saved.
14+
- **The first saved edit** asks the ensure door once and writes the flow's draft into the package that answer names, under the flow's own name. The body is the served flow without its `_` keys, declaring `overrides: { package, basedOn }`. The designer then edits that draft. The rest of the package stays read-only.
15+
- **One switch.** The open override keeps the activation-ledger switch on the flow's name. No definition switch is drawn for it, and Studio never writes the override's `status` to mean on or off.
16+
- **The badge.** The header carries the override badge, read from the served declaration (the pair's draft, else the layered read's `overlay`).
17+
- **Restore package original.** The flow's menu offers it for an override. Whether the flow is overridden is read from the declared override's own row, never from the served envelope's `deletable`, which follows the base's seal. The confirmation says the base's flow runs again under the same switch. Confirming deletes the pair's draft and then the pair's row, and the base's flow reads back. Delete flow stays on a writable package's own flow.
18+
- **Refusals.** A refused save into the pair, and a refused publish of it, read as the server sent them.
19+
- **New string.** `@object-ui/i18n`'s studio namespace gains `engine.studio.customize.restoreFlowBody` in `en` and `zh`. The other languages fall back to `en`, as the whole namespace does.
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+
---
4+
5+
Clause-②: no
6+
Metadata-protocol: none
7+
8+
fix(app-shell): Studio and Setup read an item's layered view at the package that owns it
9+
10+
The layered view (`GET /meta/:type/:name/layers`) naming no package is refused `409 RESOURCE_CONFLICT` for a name two
11+
installed packages hold, naming the holders and the `?package=` remedy. Reads in Studio and Setup still asked
12+
it naming no package, so on such a deployment the list-view inspector, the Interfaces leaf and app, the Data pillar's
13+
object, the Automations pillar's flow, the package OWD overview, the permission-set matrix, the embedded item editor and
14+
the item page's reset, publish and discard re-reads failed, and the permission-set matrix read the refusal as "no set
15+
known".
16+
17+
Each read now names the package the caller already reads and saves at: the Studio pillar's package (a read-only base's
18+
own package, whose read serves its declared override), the permission matrix's package door, or the owner the edit
19+
URL names (`?package=`). Where none is known, the read still names no package, and the server's refusal sentence is
20+
shown as the load's error; the permission matrix no longer reads it as "no set known". No holder is picked on the
21+
client.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
"@objectstack/objectql": patch
3+
---
4+
5+
Clause-②: yes (widening) — the published `SchemaRegistry` gains one public method, `restoringOnRefusal`
6+
Metadata-protocol: none
7+
8+
fix(objectql): a refused upgrade leaves the installed version serving
9+
10+
When `ObjectQL.registerApp` re-registers a package that is already installed and the registry refuses the new version, the installed version keeps serving, whole, whatever point of the registration the refusal came from.
11+
12+
- The package record, every object, app and item, picklist options and extensions, namespaces, permission-set names and default record pages read as they did before the refused upgrade.
13+
- Another package's extension of the installed version's objects and a tenant's overlay of them are untouched and still served. The restore never goes through uninstall.
14+
- What is refused, and every refusal's code, status and words, is unchanged. A clean upgrade and a refused first install behave as before.
15+
- `SchemaRegistry.restoringOnRefusal(register)` runs one synchronous registration and, when it throws, puts back exactly the registry entries it wrote, then throws the same error. The engine runs an upgrade's registration through it.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/objectql': minor
3+
'@objectstack/cloud-connection': minor
4+
'@objectstack/spec': minor
5+
---
6+
7+
fix(objectql,cloud-connection)!: an object extension's `fieldPatches` reaches the owner's own fields only, at the registry fold and at the install door alike; the install door refuses a bad patch with 422
8+
9+
Clause-②: no (narrowing) — the marketplace install door now refuses a package whose `fieldPatches` names a field only another installed extension adds, which it admitted before, and the registry fold no longer applies such a patch. Nothing is widened.
10+
Metadata-protocol: narrowed
11+
12+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed, renamed or re-shaped, and no export is removed: the published `ObjectExtensionSchema.fieldPatches` sentence already says a patch changes the display of the target object's own fields, and the fold and the install door now read it that way too. No package in this repository patches a field an extension adds, so there is nothing for a migration to rewrite. -->
13+
14+
**BREAKING** accept-set narrowing at `POST /api/v1/marketplace/install-local` (`@objectstack/cloud-connection`) and in the object fold every read serves (`@objectstack/objectql`).
15+
16+
- **Before.** A patch of a field another extension adds got three answers. The fold applied it only when the adding extension had a lower `priority` than the patching one; the install door judged the fully resolved object, so it admitted it; `os validate` judged the stack's declared objects, so it refused it. A patch of a field the same extension adds in `fields` was applied by the fold and refused by both doors.
17+
- **Now.** A field patch reaches the fields the object's owner declares, and nothing else. The fold skips a patch of any other field, whatever the priorities, and names it once in the log, as it names a patch of a missing field. The install door judges a patch against the owner's declaration (`SchemaRegistry#getObjectOwner`), not the resolved object, so it refuses such a patch; `os validate` already did. The fold judges and patches the record spelling of `fields` only, the spelling the merge reads: a body with array-spelled `fields` handed to `foldObjectExtendersOnto` is not patched, and the patch is named.
18+
- **Status.** The install door's field-patch refusal answers `422 VALIDATION_ERROR` (was 400), the status every other body refusal at that door answers. The code, the message and `details.refusals` are unchanged.
19+
- **Guidance.** `ObjectExtensionSchema`'s `actions` refusal now says the merge also applies `fieldPatches` (`@objectstack/spec`, prose only).
20+
- **Who sees it.** A package that patches a field another extension (or its own `fields`) adds. Measured in this repository: none in `examples/**` or `packages/*/src`.
21+
22+
**The remedy.** Set that field's text where the field is declared: in the `fields` entry of the extension that adds it.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
fix(spec)!: `defineConnector()` refuses a top-level key the connector does not declare, as every other connector door does
6+
7+
Clause-②: no (narrowing)
8+
Metadata-protocol: narrowed
9+
10+
<!-- adr-0087: not-required (already-registered connector-entry-undeclared-key-refused) the same narrowing as that entry, reached through one more door; its surface now names defineConnector() -->
11+
12+
**BREAKING** accept-set narrowing on `defineConnector()` (`@objectstack/spec` and `@objectstack/spec/integration`).
13+
14+
- **Before.** The helper parsed the open `ConnectorSchema`, so a top-level key the connector does not declare was dropped without a word. Because the helper parses first, the closed authored entry behind `defineStack({ connectors })`, `os validate` / `os compile`, the artifact load and the metadata save door never saw the key either: the one helper every documented `connectors:` entry is wrapped in was the one door that stayed silent. TypeScript's excess-property check catches the key only in an object literal, so the gap was reached through a spread, a variable or a cast.
15+
- **Now.** The helper parses the closed connector carrier, behind the same retired-default residue stage as `ConnectorSchema`. Such a key throws a `ZodError` naming it (`unrecognized_keys` at the root), with the message the authored entry gives: a credential-shaped key (`apiKey`, `token`, `secret`, `password`, …) meets the `auth.credentialRef` prescription, a provider setting written at the top level (`baseUrl`, `spec`, `transport`, `headers`, …) is pointed at `providerConfig`, and `timeout` / `timeoutMs` are pointed at `requestTimeoutMs`. The ADR-0010 protection envelope stays declared and a retired key's emitted default (`status: 'inactive'`) stays accepted as residue.
16+
- **Unchanged.** The return type (`ConnectorParsed`). `ConnectorSchema`, the def a plugin hands to `registerConnector`, keeps its open posture. The stack-only rules of an authored entry (no inline `authentication`, `providerConfig` / `auth` need a `provider`, no `actions` on a provider-bound instance) stay at the stack doors, which still apply them to the value the helper returns.
17+
18+
## FROM → TO
19+
20+
```ts
21+
// before: returned without `timeout`; the request timeout stayed the 30 s default
22+
defineConnector({ ...billingApi, timeout: 5000 });
23+
24+
// after: `timeout` is refused by name and pointed at the declared key
25+
defineConnector({ ...billingApi, requestTimeoutMs: 5000 });
26+
```
27+
28+
Move a provider setting under `providerConfig`, put a secret into the secrets layer under the name `auth.credentialRef` gives, and delete any other undeclared key. The upgrade guide entry is `connector-entry-undeclared-key-refused`.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
Clause-②: no
6+
Metadata-protocol: none
7+
8+
`postureStampsOrganization`'s docblock no longer says the open engine does not stamp `organization_id`. It now names the open stampers (`ObjectQL#driverWriteTarget` lands an insert in the caller's active organization, `SqlDriver#injectTenantOnInsert` fills an absent `organization_id`) and keeps the true half: activating a walled posture needs the enterprise `@objectstack/organizations` runtime (ADR-0105 D12). Comment only; no behaviour change.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/service-analytics': patch
3+
---
4+
5+
fix(service-analytics)!: the draft preview refuses one column asked raw beside its own time bucket, as the live faces do
6+
7+
Clause-②: no (narrowing) — the draft preview (`queryDataset` with `previewDrafts: true`) now refuses `INVALID_QUERY` / 400 a selection that asks one column for two values under one name (a dimension `X` beside a `timeDimensions` entry `OBJECT.X` with a granularity, or two buckets of one column), with the message, `path`, `param`, `object` and `fields` the native and ObjectQL strategies already answer; it served both columns. Nothing is widened: no export, answer member, error code or status is added, and the code was already on the wire.
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 of the package root changes: a selection both live faces already refuse is now refused by the draft preview too, with their refusal. There is nothing for a migration to rewrite and no tombstone. -->
11+
12+
**BREAKING** accept-set narrowing on the `queryDataset` draft preview (`@objectstack/service-analytics`).
13+
14+
- **Before.** Measured on real SQLite: `dimensions: ['closed_at']` with `timeDimensions: [{ dimension: 'OBJECT.closed_at', granularity: 'month' }]` was refused by both live strategies with the engine's duplicate-column refusal (`groupBy.1.field names the column 'closed_at', which groupBy.0 already names`), while the draft preview answered one row per raw timestamp carrying both columns. The same held for a month and a year bucket of one column.
15+
- **Now.** The preview judges the keys the ObjectQL strategy would hand the engine for the selection, at the same positions, and refuses with the engine's refusal for them, so all three faces answer one `INVALID_QUERY` / 400 with one message.
16+
- **Unchanged.** A raw dimension beside a bucket of a different column, a dimension that is also its own time dimension (one bucket), and two dimensions over one column with no bucket (#28744) are still served on every face. The live refusal does not move.
17+
- **Who sees it.** An author previewing a draft dataset with such a selection: the preview now says what publish would say, instead of a table publish refuses.
18+
19+
**The remedy.** Ask for the column once: as the bucketed time dimension, or as the raw dimension.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/metadata-protocol': patch
3+
---
4+
5+
fix(metadata-protocol): the boot's sealed-overlay report no longer counts a served `.custom` override or tells the operator to delete it (#28771)
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
At boot, `[metadata_sealed_overlay_unserved]` lists the environment rows that overlay sealed managed content, says no read serves them, and prescribes deleting each stored row. Since #28412 the shipping package's declared override in its paired `<base>.custom` package is served, and for a flow it is armed in place of the base. The report still counted that row, so it told the operator to delete the live customization.
11+
12+
- **A served override is not counted.** The census skips a row that the one served-override predicate (`isServedOverrideRowOf`) admits, judged on the row's `package_id` column, never on a body's `_packageId`.
13+
- **Every unserved row is still reported, with its remedies.** That includes the legacy package-less row a declared override leaves shadowed, the pair's undeclared row, a dependent package's declaration, and a package-less row whose body only names the pair. The count goes down by exactly the served rows.
14+
- **Nothing else changes.** Which row is served, loaded or armed stays the same; only the report line changes.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/spec': patch
3+
'@objectstack/lint': patch
4+
---
5+
6+
fix: the `save_notice` placement refusal names an array `triggerType`, and the flow lint classifies `save_notice` (#28786)
7+
8+
Clause-②: no
9+
Metadata-protocol: none
10+
11+
Follow-ups to #28251. The verdicts do not change: what was refused is still refused, and what was accepted is still accepted.
12+
13+
- **Spec: the array trigger form is named.** A flow whose start writes `triggerType` as an array (`['record-after-create', 'record-after-update']`) was refused at parse with "this flow's start has no record trigger", which misnames the shape. The refusal now quotes the array, says that form is not supported and never binds, and tells the author to give the start node one trigger type (`'record-after-write'` fires on both a create and an update).
14+
- **Lint: `save_notice` joins the node-type lists.** The executor never reads `condition`, so a `condition` written on a `save_notice` node now draws the inert-condition finding, as it does for the other builtins that ignore it. The node can also fail at run time on data alone (a leg resumed after a suspension, a subflow or map child, or a message whose every hole rendered empty), so it is now counted with the other fallible builtins: one in a loop body with no `try_catch` around it draws the uncontained-loop-body warning (`flow-loop-body-uncontained`).

0 commit comments

Comments
 (0)