Skip to content

Commit d3ee901

Browse files
committed
Merge origin/main into claude/issue-21836-search-skip-unreadable
Resolve the engine-double ledger by regenerating it with check-engine-double-contract --write (both pins kept). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6
2 parents f6bfdeb + e864db5 commit d3ee901

105 files changed

Lines changed: 5034 additions & 1146 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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@objectstack/cli": patch
3+
---
4+
5+
`os lint` and `os i18n extract` ask for a flow's `flows.<flow>.label` translation only when the flow has a screen node at any depth, the only kind of flow the console's screen-flow runner opens and names, so a scheduled, record-triggered or API flow with no screen no longer draws an `i18n/missing-flow` demand for a label no surface shows.
6+
7+
Clause-②: no
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
Liveness ledger: the `flows` translation group is `live`, and so are both its children. The console's screen-flow runner now names the flow by `flows.<flow>.label` in the active language, in the runner's header and in its completion toast. A locale the bundle does not cover shows the label authored on the flow, and then the flow's API name. `screens` was already `live`.
6+
7+
Clause-②: no
8+
9+
- The `flows` row drops `authorWarn` and its `authorHint`. `os lint` and `os validate` no longer warn `liveness-planned-property` on a bundle that authors `flows`. A warning is not a refusal, so the accept set is unchanged.
10+
- Dropping that bit switches on the CLI's i18n coverage demand for `flows.*`. `os lint` now reports a `flows.<flow>.*` key that a supported locale is missing as `i18n/missing-flow`, and `os i18n extract` scaffolds the group into the bundle. The flow's own `label` is demanded only for a flow with a screen node (see the `@objectstack/cli` entry). Under `--i18n-strict` a missing key is an error: translate it, or run `os i18n extract` to scaffold it.
11+
- The `flows` TSDoc in `translation.zod.ts` and the translations guide's boundary note now say that both halves are applied.
12+
- ⛔ No schema, parse, export or accept-set change.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@objectstack/plugin-auth': minor
3+
'@objectstack/organizations': patch
4+
'@objectstack/types': patch
5+
---
6+
7+
Membership under the `auto` policy is settled when the user is created, per ADR-0093 D7.
8+
9+
Clause-②: yes (widening)
10+
11+
- **At creation.** A user created under `auto` is bound to the default organization at creation, and the first session of that creating request carries it. Membership is not decided again when the user signs in later.
12+
- **One-time backfill.** The ADR-0093 D6 backfill of pre-existing users runs once per deployment, and once per process even if its record cannot be written. Its verdict is recorded in the `sys_migration` ledger with id `adr-0093-membership-backfill`. A pass on a deployment with no organization at all records nothing, and the backfill runs again once the default organization is created. If the ledger is missing or cannot be read, the pass does not run and logs a warning. If the record cannot be written, that is logged as an error. `OS_SKIP_MEMBERSHIP_BACKFILL=1` still disables the pass.
13+
- **Default organization owner.** The platform admin is bound as owner of the default organization once, by the bootstrap that first decides it, in both the single-org and the walled organizations wiring. The decision is recorded in the same ledger with id `adr-0093-default-org-owner-bind` and held for the rest of the process even if the record cannot be written. After that, a missing default organization is recreated without binding anyone. To recover, an administrator re-adds members, including themselves, through member management. On a kernel without the ledger, the owner is bound only when the bootstrap creates the default organization. If the ledger exists but cannot be read, that call binds nobody and the next trigger decides.
14+
- **Full scan.** The backfill reads the user and membership tables page by page with no row cap. A scan that cannot read either table in full binds nobody and records nothing. With organizations present but no default target, as in multi-organization deployments, the refusal is recorded.
15+
- **Upgrade.** The first boot of an upgraded deployment runs the backfill once.
16+
- **Unchanged.** `invite-only` binds nobody. Multi-organization deployments get no automatic binding. Users created through sign-up, admin create-user, import or SSO are bound under `auto` as before.
17+
- **Narrowed.** A `sys_user` row inserted straight through the data engine never passes through user creation. Once the backfill is recorded, a later `app:seeded` pass leaves it unbound. That includes users written by a seed that finishes after its inline budget. Code that inserts users this way must write their membership itself; the showcase approval-demo personas now do.
18+
- **`keysetWalk` (`@objectstack/types`).** The walk now decides that a page did not advance only when it gets back the same cursor key or the same page again. It no longer compares keys in JavaScript string order, which disagrees with database collations and could report a healthy walk as truncated.
19+
- **New public surface of `@objectstack/plugin-auth` (additive).**
20+
- `createEnsureDefaultOrganizationOnce` and `EnsureDefaultOrganizationOnceOptions` are the gated bootstrap both wirings call.
21+
- `ObjectQLAdapterFactoryOptions` adds `onRecordCreated`, passed as the new optional second argument of `createObjectQLAdapterFactory`.
22+
- `EnsureDefaultOrganizationOptions` gains `bindOnlyOnCreate` and `bindOwner`.
23+
- `EnsureDefaultOrganizationResult.reason` gains `'owner_bind_decided'`.
24+
- `BackfillMembershipsResult.reason` gains `'scan-incomplete'`.
25+
- Code that switches exhaustively over those reasons sees one more member.
26+
- **`backfillMemberships` (exported) changed behaviour.** Its `limit` option used to cap the rows scanned (default 5000); it is now the page size of a full scan with no cap. The function now needs a reader that can page by `id`; a reader that cannot gets `scan-incomplete` and binds nobody, where it used to bind. A direct call is not gated by the one-time ledger and decides membership again on every call; call it through the one-time pass instead.
27+
- **Policy switch.** Once a pass under `invite-only` is recorded, switching the policy to `auto` later does not backfill the users who existed then; they get membership through invitation or member management.
28+
- **Deprecated, not removed.** The ungated `ensureDefaultOrganization`, both plugin-auth's helper and the `@objectstack/organizations` wrapper, is `@deprecated` in favour of `createEnsureDefaultOrganizationOnce`.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/metadata-protocol": patch
3+
---
4+
5+
fix(metadata-protocol): a package-scoped metadata list serves a package-less customization of an item the package ships, as `getMetaItem` naming the package does (#21817)
6+
7+
Clause-②: no
8+
9+
ADR-0048 lets one item hold a package-less stored `sys_metadata` row, an ordinary customization, beside the package's own artifact or row. `getMetaItem` naming the package serves the package's own row, else the package-less row, the organization's rows before the env-wide rows (ADR-0005). A list scoped to the package (`getMetaItems({ type, packageId })`, `GET /api/v1/meta/:type?package=`, and the `getMetaItemsForExecution` view of it) read only the package's own rows. So it served the package's artifact over a package-less customization of it, and an env-wide row of the package over the organization's package-less row, while `getMetaItem` naming the package served the customization.
10+
11+
Now each slot of a package-scoped list resolves the way `getMetaItem` naming the package does: the package's own row, else the package-less row, by the same order the unscoped list uses. This holds over a registry item, a MetadataService item and a view the package's stored container expands, and in the `previewDrafts` list, where a package-less draft stands in when the package has none. The list's membership is unchanged: it still lists only the items the package ships, and a package-less row of an item the package does not ship adds no item to it. The lock each item reports is unchanged.
12+
13+
No key, export, status or error code changes.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/metadata-protocol': minor
4+
'@objectstack/service-datasource': patch
5+
---
6+
7+
fix(service-datasource): a destructive re-import's refusal names the remedies that work from the import route, instead of a `?force=true` that route never reads (#21841)
8+
9+
Clause-②: yes (widening)
10+
11+
- **What was wrong.** "Import as Object" (`POST /api/v1/datasources/:name/external/tables/:remote/import`) saves through the metadata door's own `saveMetaItem`. A re-import that would drop or retype a field the stored object still carries is refused by that save's destructive-change gate, and the import route relays the refusal as `400 EXTERNAL_IMPORT_ERROR`. The refusal ended `re-submit with ?force=true to proceed.` The import route reads no `force`, so a caller who did exactly that got the identical refusal back.
12+
- **What the refusal says now.** The import states its own write face, and the refusal ends: this import cannot be forced, because the external-table import route accepts no `force`. Import the table under a new `name`, or save the changed definition through `PUT /api/v1/meta/object/:name?force=true`, which accepts the destructive change on purpose. Both remedies are measured on the showcase: each one answers `201` or `200` where the re-import answered `400`. The same words appear in this package's earlier changeset for the import.
13+
- **What widens.** `SaveMetaItemRequestSchema.writeFace` (`@objectstack/spec`) and `saveMetaItem`'s `writeFace` parameter (`@objectstack/metadata-protocol`) gain one member, `'external-import'`. The member is stated by the server. No door reads it from a request body, and the import's own options cannot carry it, or a `force`, into the save. Nothing accepted today is refused.
14+
- **What does not change.** The refusal itself stays: a destructive re-import is still `400 EXTERNAL_IMPORT_ERROR`, and the stored definition does not move. The import route gains no `force`. Acknowledging a destructive change stays on the metadata door. The other faces' wording is unchanged. A `422 INVALID_METADATA` relayed by the import keeps its full findings in the message, because the import route's envelope carries no `issues`.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: an `action:group` / `action:menu` member refuses a non-array `params` unless its `type` is `api`, with the prescription to author an action with static parameter values as its own `action:button` node
6+
7+
Clause-②: yes (narrowing)
8+
9+
<!-- adr-0087: registered ui-action-group-menu-member-params-array-only -->
10+
11+
**BREAKING**: an accept-set narrowing on a published authoring surface, shipped as `minor` under the repo's launch-window convention for accept-set narrowings. What reads the rows: the component-props gate on `objectstack validate`, `objectstack build` and `objectstack lint`, which reports the refusal as an advisory `component-props-invalid` finding. A stored page still saves and loads, because a page component's `properties` is not parsed on the metadata save or load path.
12+
13+
**Why.** A container member's `params` is its input list: both containers forward an array as the `ActionParam[]` inputs to collect before the action runs. They forward any other `params` value only for a `type: 'api'` member, as its request payload, and drop it for every other `type` (an absent one included), with a development-build warning only. The member declared `params` as any value, so an object `params` on a `navigate_edit` member passed the gate and then had no effect: no error and no static values. `params` carries one shape, and no second value-bag key is declared; a member's `properties.params` is already refused. So static parameter values are not part of the inline action vocabulary at all, and an action that needs them is its own `action:button` node, whose `params` object carries them.
14+
15+
**What is refused.** On an `action:group` or `action:menu` member whose `type` is not `api`, a `params` that is not an array — an object, a string, a number or `null`. The issue's `code` is `custom`, at `actions.N.params`, and its message names the container, the member's `type` and the prescription: *to run an action with static parameter values, author it as its own `action:button` node, whose `params` object carries them* (and, for a `type: 'api'` member's request body, `bodyExtra`).
16+
17+
**What stays accepted, byte for byte.** An array `params` on any member; every `params` value on a `type: 'api'` member (its request-payload window, unchanged); a member with no `params`; and an `action:button` / `action:icon` node's object `params`, which is its static values. The member's `params` stays `unknown` in the types — the narrowing is a refinement on the member, and no export, key or type moves.
18+
19+
## FROM → TO
20+
21+
| you wrote | write instead |
22+
|:--|:--|
23+
| `actions: [{ name: 'edit', type: 'navigate_edit', params: { objectName: 'account', recordId: '${record.id}' } }]` on `action:group` / `action:menu` | the action as its own node: `{ type: 'action:button', properties: { name: 'edit', label: 'Edit', actionType: 'navigate_edit', params: { objectName: 'account', recordId: '${record.id}' } } }` |
24+
| `actions: [{ name: 'save', type: 'api', target: '/api/save', params: { status: 'closed' } }]` | unchanged — or, preferred, `bodyExtra: { status: 'closed' }` |
25+
| `actions: [{ name: 'ask', type: 'script', params: [{ name: 'reason', type: 'text' }] }]` | unchanged — an array is the input list |
26+
27+
**The one-line fix: move a member that carries static parameter values out of its container into its own `action:button` node, with the same `params` object; drop a non-array `params` from any other member.** The container never forwarded those values, so the `action:button` node is the first place they reach the handler.
28+
29+
## Who is affected, measured
30+
31+
A writer is an `action:group` / `action:menu` member authoring a non-array `params` on a non-`api` type. The census walked every `params` key in every file that names either block (TypeScript AST over `.ts`/`.tsx`/`.js`/`.jsx`/`.mjs`/`.cjs`/`.mts`/`.json` and fenced Markdown code, same-file constants resolved; YAML by text), plus every non-array `params` on an element of any `actions` array corpus-wide, and each hit was read by hand. Lit control: a planted fixture with four non-`api` object members (flat, inside a node's `properties` bag, through a same-file constant, in a Markdown fence), an `api` member and an array member — all six found and classified.
32+
33+
- **objectstack** at `5b2d189e28`, this branch's base: 24 files name a block, holding 32 `params` keys, 23 not an array; none is a container member's — conversion fixtures of the inline `element:button` action, schema source, the liveness ledger, CHANGELOG quotations, and one `properties.params` refusal probe. No example, doc, skill or fixture writes the refused spelling.
34+
- **objectui** at the `.objectui-sha` pin `0abd4f9f8` and at `main` `f1a177c41` (the same census at both; the action renderers byte-identical): 89 files, 47 `params` keys, 41 not an array. The only container members with a non-array `params` on a non-`api` type are objectui's own tests asserting that the container drops it (`action-entry-object-params-10462.test.tsx`, `action-container-member-params-10290.test.tsx`); the `type: 'api'` controls beside them stay accepted.
35+
- **hotcrm** at `4054ec2680`: no file names either block.
36+
- **cloud** was not reachable from this session. **Deployed metadata** was not measured.
37+
38+
### The kit
39+
40+
- **The refusal.** A refinement on each container member (`ui/component.zod.ts`), applied to the `action:group` and the `action:menu` member alike; the member's `params` description says what it now takes, and the generated reference page carries it.
41+
- **The ledger.** The D3 semantic entry `ui-action-group-menu-member-params-array-only` (protocol 18) and its step-18 rationale fragment. No key is removed, so there is no tombstone, and there is no D2 conversion: the static values belong on a different node, which no rewrite can build in the author's place.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@objectstack/plugin-security': patch
3+
---
4+
5+
Discard Overlay no longer deletes the stored definition of a permission set saved into a writable runtime package
6+
7+
Clause-②: no
8+
9+
The Discard Overlay action (`POST /api/v1/security/permission-sets/:id/discard-overlay`) is declared to refuse any set that is not package-declared, so that it can never destroy a set the environment authored. It decided that by asking whether any engine-registry item of the set's name carried a package id. The registry also holds the stored definition rows, and a metadata list read (`GET /api/v1/meta/permission`, which every Studio page load issues) stamps a stored row's package binding onto it. A set saved into a writable runtime package (`PUT /api/v1/meta/permission/:name?package=<id>`) therefore passed the check after the first list read: the action answered `200` and deleted the set's only `sys_metadata` row. It now asks the same classifier the packaged-permission-set lock's write doors ask, and refuses every set that classifier does not judge shipped by a code package (including when it cannot decide), with the action's existing `403 PERMISSION_DENIED`.
10+
11+
The drift diagnostics behind the record's `drift_status` / `drift_detail` (whose `overlay_shadow` detail names Discard Overlay as the remedy) read the package id the same way. They now judge the same population: only sets a code package ships.
12+
13+
Unchanged: a set a code package ships still has its overlay discarded and its record resynced to the shipped artifact, its drift is still reported as before, and no error code, route or field moves. The refusal's message now says the set is not shipped by any installed code package.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/objectql': patch
3+
'@objectstack/plugin-approvals': patch
4+
'@objectstack/plugin-webhooks': patch
5+
'@objectstack/service-knowledge': minor
6+
---
7+
8+
Record-change payloads apply the same credential mask and internal-field omission as write responses.
9+
10+
Clause-②: yes (widening)
11+
12+
- **`data.record.created` / `data.record.updated` events.** The engine projects the event's `after` and `changes` bodies through `omitInternalFieldsFromWriteResponse` (`@objectstack/core`), the helper every external write response already uses: credential-class fields (`secret`, and `password` outside the exempt `managedBy` buckets) carry `SECRET_MASK` (or `null` when unset), and `internal: true` fields are omitted. The engine's own write result is unchanged, so a privileged in-process caller that reads the stored value back off `insert` / `update` still sees it.
13+
- **Approval request snapshot.** The record snapshot an approval request stores (`payload_json`) applies the same rule when the request is opened.
14+
- **Outbound webhook body.** The delivered body, and the delivery row that stores it, apply the same rule to `before`, `after` and `changes`.
15+
- **Knowledge index documents.** `recordToDocument` takes the object definition as an optional fourth argument and skips credential-class and `internal` fields, under `'*'` and when a source names one explicitly. `KnowledgeService` passes the definition from the bound engine.
16+
- **New public surface of `@objectstack/service-knowledge` (additive):** `recordToDocument` accepts the object definition as an optional fourth argument; existing three-argument calls behave as before.
17+
- **Receivers see masked values.** Webhook receivers and realtime clients now get `SECRET_MASK` (or `null` when unset) for credential-class fields and no key for `internal` fields.
18+
- **Existing rows are not rewritten.** Approval snapshots, webhook delivery rows and knowledge documents written before this change keep their stored bodies; reindexing a knowledge source refreshes its documents.
19+
- The audit trail already masked these fields and is unchanged. No other accept set or public schema changes.

0 commit comments

Comments
 (0)