Skip to content

Commit 357b2ba

Browse files
committed
Merge origin/main (c2a5be0) into claude/harden-occ-stale-token-order
Claude-Session: https://claude.ai/code/session_017xRBcVy4VXmkQjn2MGeaWk Co-authored-by: Claude <noreply@anthropic.com>
2 parents 90a6068 + c2a5be0 commit 357b2ba

1,474 files changed

Lines changed: 87636 additions & 11144 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: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/skills': patch
3+
---
4+
5+
docs(skills): objectstack-upgrade says a stored row in a ≤16.x shape stops being rewritten at read on 18.3, and teaches the stored pass on 18.2 or earlier first
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Before.** § "Stored rows" said every stored-row rehydration replays the conversion chain including entries retired from the load path, so "a row written under protocol 16 therefore rehydrates in its protocol-17 shape without anybody editing it", and offered `os migrate meta --stored --apply` only as a way to make that durable. § 3.6 called the read-time rehydration "still the guarantee", the report template marked the stored pass optional ("rows rehydrate correctly today"), and eval 4 expected the same answer. On 18.3, which drops every conversion retired at or below 16.x, that advice leaves such a row unconverted for good.
11+
- **Now.** The section says rehydration replays only the conversions the installed release still carries, that 18.3 drops those retired at or below 16.x, and to run the stored pass on the release being left (18.2 or earlier for such a row) before moving; § 3.6 says the `os migrate meta --stored --json` exit `0` counts only on the release being left, because one that dropped a row's conversions answers `0` without recognising the row; the report template's stored-pass line says it is owed before any release that drops the rows' conversions. Eval 4's expected answer says the same, and new eval 5 asks the 18.1 → 18.3 question directly.
12+
- `SKILL.md` and `references/examples-upgrade.md` stay under their token ceilings (no ceiling moves). No schema, key, command or behaviour changes.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/service-automation': minor
4+
'@objectstack/lint': minor
5+
'@object-ui/app-shell': minor
6+
---
7+
8+
feat(automation): a flow's `successMessage` / `errorMessage` are `{{ }}` text slots over the run's variables
9+
10+
Clause-②: no (narrowing) — `registerFlow` and `objectstack validate` (compile, the publish gate) now refuse a flow whose `successMessage` / `errorMessage` carries a single-brace `{token}` or a `{{ }}` hole that does not compile, where they accepted any string before. A `{{ }}` hole in either key now renders instead of being copied, which changes what a stored message means and widens no accept set.
11+
12+
Metadata-protocol: narrowed (`FlowSchema.successMessage` / `errorMessage`, and the `flows.FLOW.successMessage` / `.errorMessage` translation leaves, are refused at the flow doors and the translation parse when they carry a single-brace token or an uncompilable hole; the meaning shift of a `{{ }}` hole, from literal text to a rendered slot, is the semantic half and ranks below it)
13+
14+
<!-- adr-0087: not-required (no-migration-prescription) No key is added, removed or renamed, and a message no first-party producer writes is the only one refused: measured, 0 first-party flow messages (examples, package sources, dogfood fixtures, skills) carry a brace at all. A refused message is one that would otherwise reach the user with its braces raw, and the refusal names the key and spells the `{{ var }}` form to write. A conversion cannot tell whether a literal brace was meant as a hole or as text, so the author decides: write the hole, or rephrase the brace. -->
15+
16+
A screen flow can now finish on "3 contacts enrolled": `successMessage: '{{ enrolledCount }} contacts enrolled'` renders the count the run bound. Until now the engine copied both messages onto the run result as written, so a `{{ }}` hole reached the user raw.
17+
18+
- **Spec (`@objectstack/spec`).** `FlowSchema.successMessage` and `errorMessage` join the flow text slots (`FLOW_TEXT_SLOTS`, `flowTextSlotSources` in `automation/flow-text-slot-template.ts`): `{{ path }}` and `{{ path | formatter }}` holes over the run's variables, the grammar a refusing `end` node's `message` reads. The `flows.<flow>.successMessage` / `.errorMessage` translations are the same text slot as their source, so a single-brace `{token}` in one is refused, and `flowTerminalMessageKey(flow, key)` spells the key the engine reads.
19+
- **Engine (`@objectstack/service-automation`).** Every terminal exit renders the message instead of copying it: `successMessage` over the variables the run finished with (first run, a retried attempt, a resume), `errorMessage` over the variables at the failure point (`{{ $error.message }}` included; the retry-exhausted exit reports the last attempt's rendering). The translated template is picked in the run's locale before the holes are filled, exactly as for a screen's heading. A hole over a variable the run never bound renders nothing, as in every text slot, and a message with no hole is byte-identical. A message that cannot render (a `{{ x | currency }}` hole with no tenant default currency) is left off the result with a warning; the run's verdict stands.
20+
- **Doors (`registerFlow`, `objectstack validate`, the publish gate).** Both keys are judged as a node's text slot is: a single-brace `{var}` token is refused with its `{{ var }}` spelling, and a hole that does not compile (`{{ a + b }}`) is refused with the compile step's words.
21+
- **Console (`@object-ui/app-shell`).** The flow's toasts are drawn as served. The client overlay of `flows.<flow>.successMessage` / `.errorMessage` is removed, because the engine now serves them translated and filled, and an overlay would put the bundle's template, holes raw, over the filled sentence. `launchConsoleFlow` takes the host's `language` in place of `translation: { i18n, language }`, and `interpretFlowResponse` takes the locale string in place of `FlowCopyLocale`.
22+
23+
**BREAKING** accept-set narrowing at the flow doors (`@objectstack/spec`, `@objectstack/service-automation`, `@objectstack/lint`), shipped as `minor`.
24+
25+
- **Before.** `successMessage: 'Enrolled {count} contacts'` registered, `objectstack validate` exited 0, and the run's result carried the string as written, braces and all. A `{{ enrolledCount }}` hole was copied the same way.
26+
- **Now.** `registerFlow` refuses that flow at `successMessage` with the text-slot refusal an `end` node's `message` gets, spelling `{{ count }}`; `objectstack validate` / compile / the publish gate report the issue at `flows[i].successMessage` and exit 1. A hole that does not compile (`{{ a + b }}`) is refused with the compile step's words. A translated `flows.FLOW.successMessage` / `.errorMessage` with a single-brace token is refused by the translation parse. A well-formed `{{ enrolledCount }}` renders over the run's variables at the terminal exit.
27+
- **Unchanged.** A message with no brace renders byte-identical. An absent message stays absent. A hole over a variable the run never bound renders blank, as in every text slot. The run's own verdict, status and history row do not depend on the message.
28+
- **Who sees it.** An author whose flow message carries a closed single-brace token, a hole that does not compile, or a hole over a `$` root the engine does not bind: 0 first-party flows do.
29+
30+
**The remedy.** Write `{{ var }}` holes over the run's variables, spelled as the refusal prints them. For a `{word}` meant as literal text, rephrase it so no closed single-brace token remains (drop the braces, or use other brackets); a lone `{` that closes nothing is not judged.
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: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/objectql': patch
4+
'@objectstack/runtime': patch
5+
'@objectstack/trigger-record-change': patch
6+
'@objectstack/service-automation': patch
7+
'@objectstack/cli': patch
8+
'@objectstack/lint': patch
9+
---
10+
11+
feat: a hook (shipped body included) and a record-triggered flow can set the save notice the create / update answer carries (#28251)
12+
13+
Clause-②: yes (widening: `HookContext` gains the optional `notice` member, `FlowNodeAction` gains `save_notice`, the `flows` translation group gains `notices`, and `AutomationContext` gains the optional `saveNotice`; nothing an existing stack writes is refused)
14+
Metadata-protocol: added
15+
16+
A single-record create or update has answered with an optional `notice` since #28247, but only an in-process caller that passed `onNotice` could set it. A shipped hook body could not reach that listener, and neither could a flow, so an app that wanted to tell the saver "Sent Acme to your manager for approval" had to fake it with a `record:alert` banner. Now there is one channel with two faces:
17+
18+
- **Hooks: `ctx.notice(message)`.** `HookContextSchema.notice` is the member. The engine binds it at every insert / update dispatch (`beforeInsert`, `afterInsert`, `beforeUpdate`, `afterUpdate`), so every dispatch site gets it, and it calls the write's own `onNotice` listener. A write nobody listens on (a batch, a seed, an import, a flow node's own write) sets nothing, and the call is a no-op there. Anything but a non-empty string is refused with a `TypeError` naming the call, and the write with it. Delete and read dispatches carry no member. The sandbox marshals the member into a shipped body (QuickJS), and no capability is needed to call it.
19+
- **Flows: the `save_notice` node.** `{ type: 'save_notice', config: { message } }` renders its `{{ }}` message and sets it on the write that started the run. The record-change trigger runs the flow inside that write and hands the run the write's channel (`AutomationContext.saveNotice`). The flow parse refuses the node, at any region depth, in a flow whose start is not a create or update record trigger (`record-(before|after)-(create|insert|update|write)`), because no write waits on such a run. At run time the node is refused by name where the parse cannot see it: a leg resumed after an approval or a wait, a subflow or map child (they get no channel), and a run started any other way.
20+
- **Translatable where it is rendered.** A flow notice is picked from `flows.<flow>.notices.<node_id>.message` (`flowNoticeMessageKey`) in the writer's locale before its holes are filled, and the authored template renders when nothing is picked. `os i18n extract` scaffolds the key, the coverage gate demands it, and `objectstack validate` refuses a key over an unknown flow, an unknown node, or a node that is not a `save_notice`. The answer's `notice` stays a plain string: the sentence arrives already rendered.
21+
22+
Scope: single-record create and update only. A batch, a clone and a write a flow node makes pass no listener, so they carry no notice.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@objectstack/skills': patch
3+
---
4+
5+
docs(skills): objectstack-data rules/lifecycle.md teaches the save notice
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Before.** A hook can tell the person who made a create or update what the save set in motion (`ctx.notice(message)`, answered as the create/update `notice`), and a flow a record create/update trigger starts can set the same message with a `save_notice` node, but no published skill taught it, so an AI author writing a save hook from `objectstack-data` kept reaching for a `record:alert` banner.
11+
- **Now.** `objectstack-data/rules/lifecycle.md` gains *Save Notice — what the save set in motion*: the `ctx.notice` spelling for inline `handler` and sandboxed `body` alike (no capability), info severity and the distinction from validation `warnings` and from a `record:alert` banner, one message per write with the last call winning, the dispatches that carry it, the non-string refusal, and the flow face (`save_notice`, refused outside a create/update record trigger and after an `approval` / `wait`).
12+
- The file stays under its token ceiling: the new text is paid for by deleting the Decision Tree (which restated the class table and the syntax examples), the telemetry-growth anecdote and a Safety Semantics restatement. No ceiling moves; no schema, key or behaviour changes.
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+
'@objectstack/spec': minor
4+
---
5+
6+
fix(platform-objects): Setup Create User marks Email required while phone sign-in is off
7+
8+
Clause-②: yes (widening)
9+
Metadata-protocol: semantic
10+
11+
Create User refuses a blank email while phone sign-in is off (the default), and takes an email or a phone number while it is on. The dialog now shows that before submit: with phone sign-in off, Email carries the required mark and a blank one is refused in the dialog; with it on, Email stays optional beside Phone Number.
12+
13+
- **The declaration.** A param's `required` is a static boolean, so `sys_user.create_user` declares two params on the one `email` field. First an optional one with `requiresFeature: 'phoneNumber'`, then a required one with `visible: 'has(features.phoneNumber) && features.phoneNumber != true'`. The console collects and submits only the param whose gate says yes, so exactly one Email input shows. While the console holds no served flag (config not fetched, or the fetch failed), the `has()` term keeps the required param hidden and Email shows unmarked, as before.
14+
- **The action doors (the semantic change).** `validateActionParams` (`@objectstack/spec/ui`), which the REST action route and the MCP `run_action` bridge run before the handler, used to key params by name with the LAST declaration winning. A name declared more than once is now required only when EVERY declaration of it is required, and its value shape is the FIRST declaration's. Without this the REST door refused a phone-only Create User with `400 VALIDATION_ERROR` "Action param \"email\" is required". The door reads no `visible` or `features`, so the endpoint stays the judge: Create User still answers a blank email with phone sign-in off with its own `400` "Email is required". No other action in this repository, its examples or hotcrm declares a param name twice, so nothing else changes meaning.
15+
- **The registry.** `PUBLIC_AUTH_FEATURES.phoneNumber.gatedInputs` books `sys_user.actions.create_user.params.email`. That one path stands for both Email params: the feature-gate guard checks the canonical gate on the first, optional param, and the second, required param is negatively gated, as the row's notes say.
16+
- **Unchanged.** The endpoint's own refusal of a blank email, and the dialog's marks for a refusal the server names per field.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/metadata-protocol': minor
3+
'@objectstack/service-automation': minor
4+
---
5+
6+
feat(service-automation, metadata-protocol): a flow override declared in the shipping package's paired `<base>.custom` is the packaged flow — saved, served and armed in place of the base, and deleting it restores the base (#28412)
7+
8+
Clause-②: yes (widening)
9+
Metadata-protocol: semantic
10+
11+
ADR-0146 D9 item 1 for `flow`. A flow a code package ships could be changed only by cloning it under a new name, so the clone and the base both ran on every record change. Now the base package's paired `.custom` package can override it in place:
12+
13+
- **The save door admits it.** A flow saved into `<base>.custom` under the base flow's name, declaring `overrides: { package: '<base>', basedOn }`, passes the ADR-0146 D2 override write gate (`flow` is open there now; `action` still answers "not yet available"). The draft save, the per-item publish, `publishPackageDrafts`, `os validate` and the install-local gate admit it as they admit a declared view override.
14+
- **Every read serves it as the base.** The flow view (`getMetaItems` / `getMetaItemsForExecution`), the by-name read, the layered read (`code` = the base's flow, `effective` = the override) and the per-package customization list serve the override in the base's slot, under the base's `_packageId`, once per name. The one predicate is N-10's `isServedOverrideRowOf`, judged on the row's stored `package_id` — never on the body's `_packageId`.
15+
- **The automation engine arms it in place of the base.** The boot pull, the `kernel:ready` sync, the `metadata:reloaded` re-sync and the metadata-mutation sync each arm the override. One record change runs one body. The boot pull ranks it above the loader's body (a new `override` contender rank), reading the row's stored binding, which the protocol's registry hydration now records on the hydrated flow body as a non-enumerable symbol (`bindStoredRow` / `storedRowBindingOf`, newly exported). That symbol cannot be authored in JSON and never leaves the process.
16+
- **The ledger identity stays the base's.** The override keeps the base flow's name and owner, so the activation toggle, the ledger row, `FLOW_DISABLED` and the §7.3 subflow guard treat it as the base. Switched off, it is silent. Switched on, it is armed again.
17+
- **Deleting it restores the base.** A `/meta` delete naming the pair removes its row (admitted past the sealed-removal refusal, as the narrowing direction), and each arming step re-arms the base with one registration, never a withdraw first. A run suspended on the base finishes on the base (N-12), while new runs start on the override.
18+
- **Every other stored row stays shadowed.** A dependent package's declaration, a package-less row whose body names the pair, and the pair's undeclared row are not armed. The shadowing warning's remedy is now "declare it as an override in '<base>.custom' (overrides: { package, basedOn }), or save it under a new name". It no longer says "clone".
19+
- **The sealed save sentence for a packaged flow** names the declaration in place of the clone route: "… Declare an override from '<base>.custom' or rename it to customize it, or switch it off (…). See docs/adr/0126-packaged-metadata-customization-model.md." A delete still names the clone and the switch.
20+
21+
No spec key is added or reshaped. A flow item's existing `overrides` declaration, refused before, now takes effect.

0 commit comments

Comments
 (0)