Skip to content

Commit db20886

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-22937-ceremony-promotions
2 parents 1bede35 + 614fdec commit db20886

44 files changed

Lines changed: 2772 additions & 260 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: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
feat(spec)!: a page action row's `onSuccess` is refused beside a console runner built-in other than `api` / `script`, and beside a row with no executor; `ActionSchema`'s `onSuccess` refusal now states its true reason
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: registered ui-page-action-row-on-success-beside-executor-refused -->
10+
11+
**BREAKING**: an accept-set narrowing on one key of four published page component rows, shipped as `minor` under the launch-window convention for accept-set narrowings.
12+
13+
The page action rows are `action:button` and `action:icon` (`properties`), and each member of an `action:group` or `action:menu` (`properties.actions[]`). Each forwards `onSuccess` to the console's action runner, which performs the post-success hop after every executor that succeeds. `ActionSchema` refuses `onSuccess` beside any `type` but `api` and `script`; the rows judged it beside no executor at all. They now carry the action's rule on the vocabulary the action's closed `ActionType` can see, read against the row's effective executor (the executor key, or the row's `name` when that key is omitted or empty, as the runner resolves it):
14+
15+
- refused beside the runner built-ins `url`, `navigation`, `modal`, `flow` and `form`;
16+
- refused on a row with no effective executor (no executor key, or an empty one, and no `name`);
17+
- accepted beside `api` and `script`, and beside any other name: that is a handler a host registers, the runner hops after it too, and only the host knows whether its handler navigates.
18+
19+
`ActionSchema`'s own reach is unchanged (`url`, `modal`, `flow` and `form` are refused, as before). Its message used to say that on those types the key "would parse clean and never run" and that "no renderer reads a post-success hop today". Both were false: the runner hops after every executor. The action and the rows now print one refusal text with the true reason for each executor.
20+
21+
### FROM → TO
22+
23+
| before | what to write instead |
24+
| --- | --- |
25+
| `{ actionType: 'url', target: '/tasks', onSuccess: { navigate: '/done' } }` (or `type: 'url'` on a member, or `name: 'url'` with no executor key) | the destination in `target`, the tab in `openIn`, and no `onSuccess`: the url action is itself the navigation, so the hop navigated a second time |
26+
| the same with `navigation` | the same, with `actionType: 'url'` (the alias is refused at the executor key as well) |
27+
| `{ actionType: 'form', target: 'task_form', onSuccess: … }` | no `onSuccess`; declare where a submitted form goes in the form view's `submitBehavior` |
28+
| `{ actionType: 'modal', … , onSuccess: … }` or `{ actionType: 'flow', … , onSuccess: … }` | no `onSuccess`, or move the hop onto an `api` or `script` action, whose result it reads |
29+
| `{ label: 'Go', onSuccess: { navigate: '/tasks' } }` (no executor key and no `name`) | declare the executor whose success the hop follows, `actionType: 'api'` or `actionType: 'script'` (`type` on a member), or drop `onSuccess` |
30+
31+
**The one-line fix: keep `onSuccess` only on a page action row whose executor is `api`, `script` or a host-registered handler, and declare that executor.** `os migrate meta --from 17` lists the D3 entry `ui-page-action-row-on-success-beside-executor-refused`; the rewrite is by hand, because where the hop belongs is the author's call.
32+
33+
**What an author now sees.** `os validate`'s props gate reports the refusal at the row's `onSuccess` (`properties.onSuccess`, or `properties.actions.N.onSuccess` for a member). For example, `{ actionType: 'url', target: '/tasks', onSuccess: { navigate: '/done' } }` on an `action:button` gets: ``An `action:button`'s `onSuccess` (`actionType: 'url'`) would navigate a second time: a `url` action is itself the navigation, and the console's action runner performs the post-success hop after every executor that succeeds.``, followed by what the hop is declared beside and the prescription. The same row without `onSuccess` parses unchanged.
34+
35+
**Measured producers.** None in this repository or in hotcrm (at `0f84c9c`). In objectui (at its pin `1fb1ced36243` and its main `72594f29`), one test row with no executor and no `name` carries `onSuccess` and is asserted accepted; its remedy is to declare `actionType: 'api'` on that row. A host-handler row (`actionType: 'run'`) with `onSuccess` stays accepted.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
'@objectstack/metadata-protocol': minor
3+
'@objectstack/plugin-security': minor
4+
'@objectstack/spec': minor
5+
---
6+
7+
fix(metadata-protocol)!: the metadata save, restore and publish doors refuse a permission set whose body `packageId` disagrees with the package it is saved into, and the audience-binding suggestion reader takes a set's owner from its binding only (ADR-0086 D3)
8+
9+
Clause-②: no (narrowing)
10+
11+
<!-- adr-0087: registered permission-set-package-id-binding-mismatch-refused -->
12+
13+
**BREAKING** (an accept-set narrowing), shipped as `minor` under the launch-window convention for breaking changes.
14+
15+
ADR-0086 D3 declares `PermissionSetSchema.packageId` as the owning package of a package-shipped set, absent for an environment-authored one. The owner every reader uses is the package the set is saved into: the stored row's `package_id`, served as `_packageId`. A body `packageId` that named another package was accepted and then silently overridden, so an author, an AI build above all, never learned that the owner it wrote was not the owner it got.
16+
17+
**FROM.** A permission set saved and published into package `app.crm` with `packageId: 'app.other'` in its body answered `200` and `published`, and was owned by `app.crm` everywhere it was read. A set saved into no package with `packageId: 'app.other'` was stored as an environment-authored set, while the audience-binding suggestion reader offered it, accepted it and derived it into `everyone` as `app.other`'s.
18+
19+
**TO.**
20+
21+
- **`@objectstack/metadata-protocol`.** The save door (`PUT /api/v1/meta/permission/:name`, draft and publish mode, and every in-process caller) the restore doors (`POST /api/v1/meta/permission/:name/rollback` and a commit revert's restore limb) and the draft promotion (`POST /api/v1/meta/permission/:name/publish` and the package publish) refuse a permission set whose body `packageId` is not the package the row is written into, with `400 VALIDATION_ERROR`, the code and status the doors already answer for a body `name` that disagrees with its row. `issues[]` carries the key at `packageId`. A body `packageId` on a set saved into no package is refused too, the empty string included: a present key declares a package-shipped set. The message names both values and both remedies. The package judged is the one the write lands in: a draft saved without naming a package lands in the package of the item's active row, and is judged against it. A package publish that meets such a draft (one stored before this change) aborts the whole batch, `outcome: 'refused'`, with the draft in `failed[]` under `VALIDATION_ERROR`. A rollback to a stored version that carries such a body (one written before this change) is refused, and a commit revert reports it in `failed[]` under the same code; the remedy there is to save the set again with `packageId` dropped or set to its package, instead of restoring that version. Nothing is written, published or restored. Package duplication copies a set whose body names the source package as naming the target package. `os migrate meta --stored --apply` records a stored set it rewrites, and the door refuses, as that row's failure.
22+
- **`@objectstack/plugin-security`.** The audience-binding suggestion reader takes a set's owner from its binding only, never from its body's `packageId`. A set bound to no package has no owner there, so no suggestion is created for it. An acceptance recorded under the package its body named is withheld from `everyone` at the next derivation. The withholding is reported at `warn`, naming the set and its reason. Accepting a pending suggestion of that kind is refused.
23+
- **`@objectstack/spec`.** The `PermissionSetSchema.packageId` description states the refusal. The step-18 semantic migration entry `permission-set-package-id-binding-mismatch-refused` carries the upgrade sweep.
24+
25+
**Unchanged.** A body `packageId` equal to the package the set is saved into is accepted, and an absent one is accepted everywhere. Stored rows are not rewritten. A stored set whose body disagrees with its binding keeps loading and keeps reading as its binding, and is refused on its next save or publish, as is a restore of a stored version that carries one. An installed package's own sets are owned by the package that ships them, as before.
26+
27+
**Remedy, in one line.** Drop `packageId` from the permission set's body, or save the set into the package it names (`?package=<id>` on the REST door, `packageId` in process).
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
A builtin flow node's `config` value that carries a `{token}` is judged for its type at the build doors like any other value: `get_record` with `limit: '{x}'` is refused at `nodes.N.config.limit` ("expected number, received string") by `FlowSchema.parse`, `defineStack`, `objectstack validate`, `objectstack compile`, the metadata save door and `AutomationEngine.registerFlow`, where it used to register and then fail every run that reached the node.
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (already-registered flow-builtin-node-config-values-refused) The step-18 D3 entry registered on this line records the build doors refusing a builtin config value its executor contract refuses; this change removes the one class that entry held back, a value carrying a token, and the entry's text now says so. No key is removed and no shape is rewritten, so there is no tombstone and no D2 conversion: the platform cannot know the literal the author meant. -->
10+
11+
**BREAKING**: an accept-set narrowing on a published authoring surface, shipped as `minor` under the launch-window convention for accept-set narrowings (`.changeset/pre.json` is open on `main` in `next` pre mode, so the release is `18.0.0-next.*`).
12+
13+
**Why.** The build doors' value arm (`flowNodeConfigRefusals`) never refused a value carrying a `{token}` for its type, because the flow interpolator decided what the run would parse there. The interpolator and every call site it had are deleted on this line, and every builtin executor parses its config as authored (an `http` node after its credential channel's `signingSecret`), so the run's contract sees the token the build's sees and refuses it at every run. The value arm passed exactly those values.
14+
15+
**What is refused now.** A token-carrying value at a builtin key its executor contract refuses it at: a number or boolean key (`get_record` `limit`, `loop` `maxIterations`, `update_record` and `delete_record` `multi`, a screen field's `min`, `max` or `required`, a `try_catch` `retry.maxRetries`), a key with a declared set (`notify` `severity`, `screen` `mode`), a record key written as one token (a CRUD `filter: '{f}'`), and a rule finding the token used to hide (a `notify` `templateData` with no `template`). Each is `node-config-refused-by-contract`, `params: { nodeType, key }`, anchored at the key, in the contract's own words — `defineStack` throws `STACK_SCHEMA_INVALID` (422) at `flows.N.nodes.M.config.<key>`.
16+
17+
**What does not change.**
18+
19+
- A token at a plain string key (`outputVariable`, `flowName`, `objectName`) parses and registers as before: the contract takes a string there.
20+
- A token at a value slot (a CRUD `fields` value, a callee `input`, a screen's `defaults`, `defaultValue` or `recordId`, a `loop` or `map` `collection`, a `notify` `recipients`, `templateData` or `sourceId`, an `http` `body`, a filter operand) or at a text slot (a `notify` `title`, `message` or `actionUrl`, a screen `title` or `description`, an `http` `url` or header value) is refused, as before, by that slot's own judge at `registerFlow` and `objectstack validate`, once, with the slot's label and remedy: the value arm exempts each finding that judge owns, so no token is reported twice.
21+
- At a key its executor reads as written (a `notify` `sourceObject`, `actorId` or `payload`; an `http` `method`, `durable`, `timeoutMs` or whole `headers` map) the key is refused once, in the literal's own words, as before.
22+
- An `http` `signingSecret` is still never judged at the build doors: the credential channel may supply what the run signs with.
23+
24+
## FROM → TO
25+
26+
| you wrote | write instead | what changes |
27+
|:--|:--|:--|
28+
| `get_record` `limit: '{page.size}'` | `limit: 25` | refused at `config.limit` at every build door, where it registered and failed every run |
29+
| `loop` `maxIterations: '{cap}'` | `maxIterations: 50` | the same, at `config.maxIterations` |
30+
| `update_record` `multi: '{bulk}'` | `multi: true` | the same, at `config.multi` |
31+
| a screen field `min: '{m}'` | `min: 1` | the same, at `config.fields.N.min` |
32+
| `try_catch` `retry: { maxRetries: '{n}' }` | `retry: { maxRetries: 3 }` | the same, at `config.retry.maxRetries` |
33+
| `notify` `severity: '{level}'` | `severity: 'warning'` | the same, at `config.severity` |
34+
35+
**The one-line fix: write the literal the key declares.** None of these keys is evaluated per run, so no envelope replaces the token there.
36+
37+
**Who is affected, measured.** A census of every builtin node literal whose `config` evaluates statically — 1010 nodes, 244 of them carrying a `{…}` token, across `examples/`, `packages/` and `apps/` in this repository at the base and hotcrm at `0f84c9c` — finds no verdict this change moves (a control node with `limit: '{x}'` moves, so the census can see one). No published docs page and no `@objectstack/lint` rule says a token at a typed key passes the build.
38+
39+
### The kit
40+
41+
- **The judge.** `flowNodeConfigRefusals`'s builtin value arm (`flow-node-config-refusals.ts`): the token carve-out (`carriesInterpolationToken`) is deleted. In its place the arm asks the value-slot judge (`flowNodeValueTemplateRefusals`) and the text-slot judge (`textSlotTemplateRefusal`) which paths they refuse and exempts those, and reports a key whose contract marks its refusal `READ_AS_WRITTEN_PARAM` in that refusal's words alone.
42+
- **ADR-0087.** No new entry: the step-18 D3 entry `flow-builtin-node-config-values-refused` and its rationale fragment now state the class this change judges.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/service-automation': minor
4+
'@objectstack/metadata-protocol': minor
5+
---
6+
7+
fix: `os migrate meta --stored` lists every stored row that protocol 18 refuses and no conversion rewrites. Before, the preview counted each such row as already on protocol.
8+
9+
Clause-②: yes (widening: `IAutomationService.canonicalizeStoredFlow(...).todos?` in `@objectstack/spec`, `AutomationEngine.canonicalizeStoredFlow(...).todos` in `@objectstack/service-automation` and `StoredFlowCanonicalization.todos?` in `@objectstack/metadata-protocol`, each a new output member; nothing narrows, and no door accepts or refuses anything it did not before)
10+
11+
Three step-18 retirements refuse a stored shape that has no lossless rewrite. Upgrading a v17 database left those rows refused at load or at boot, and the stored preview read them as `canonical`. Each shape now has exactly one fate: **refused and listed**. The preview and `--apply` print one `TODO` line per site under its row. The line names the step-18 entry that refuses the shape, the site, and the choice the owner has to make. Nothing is rewritten, and the run's verdict does not change, because no run of the pass could clear these rows.
12+
13+
**#23269, a flow value slot carrying a `{…}` token, including a `get_record` / `update_record` / `delete_record` filter operand: refuse and list.** A whole-path token maps mechanically to `{ dialect: 'cel', source: '<path>' }`, but the rewrite is not lossless. A token the template resolved to nothing (an erased condition, so the node was refused) is a CEL fault on an absent root, and a flow variable named like a query placeholder used to shadow it. ADR-0087 D2 conversions run on the load path and must be lossless, so this one is not a D2. The chain also has no operator-applied rewrite of stored rows separate from the load path: `--stored` writes back the read path's own pass, and `--from 17` rewrites authored sources, not rows. So the flow is listed by flow, node and path (`flow-value-slot-template-dialect-refused`), and `registerFlow` goes on refusing it until the value is rewritten as its CEL envelope. A flow already in the CEL form, or using a query placeholder such as `{current_user_id}`, is not listed.
14+
15+
**#23270, a `controlled_by_parent` object without exactly one required `master_detail`: refuse and list, by object (`cbp-master-relation-tiers-retired`).** No shape here converts losslessly. The upgrade never picks between two masters, either both required or neither. An explicit `required: false` is a contradiction the builder refuses. Turning a required `lookup` into a `master_detail` changes what deleting the master does. An object with no master has nothing to convert. Leaving `controlled_by_parent` changes who can read the rows, so that is the owner's choice and never a silent rewrite. The listing reads the same predicate `ObjectSchema` refuses with, so the preview and the parse refuse one set. The one lossless shape, a single `master_detail` with `required` omitted, is still converted by `cbp-master-detail-required-defaulted` and is not listed.
16+
17+
**#23271, a view whose `data` binds the retired view-level API source: refuse and list, by view (`view-data-api-provider-retired`).** There is no mechanical move. The replacement is a page component's `dataSource`, so a page must exist to carry it. Deleting the view's `data` would make the view query its object instead. Views in every stored body spelling are covered, and so are an object's own `listViews`. The line shows the request URL and method and withholds every other request key, so a credential stored in a view's headers never reaches the report. A view on the `object`, `value` or `schema` provider is not listed.
18+
19+
How it is wired: `applyConversions` reports these shapes through its existing `onTodo` sink, after the conversions run. Each TODO's `conversionId` is the id of the D3 entry that refuses the shape, so the published `ConversionTodoNotice.conversionId` now names either a conversion or a D3 entry. The published flow-canonicalization result gains one member, a widening in three packages:
20+
21+
- `@objectstack/spec`: the contract `IAutomationService.canonicalizeStoredFlow(...)` declares an optional `todos?: ConversionTodoNotice[]` beside `notices` and `conflicts`. An implementation that reports none still conforms.
22+
- `@objectstack/service-automation`: `AutomationEngine.canonicalizeStoredFlow(...)` returns `todos`. `registerFlow` ignores them, because its own refusal names the same sites.
23+
- `@objectstack/metadata-protocol`: `StoredFlowCanonicalization` gains the matching optional `todos?`, and the stored pass lists a flow row's TODOs the way it lists any other row's.
24+
25+
The authoring funnel and the read seams pass no sink, so nothing changes for them.

0 commit comments

Comments
 (0)