Repository navigation
Commit d6d6e87
feat(spec,client)!: ActionSchema gains outcomeMessages (success copy per handler outcome, ${result.*}), and environments.delete stops guaranteeing message (#21214)
Fixes #21095
Clause-②: yes (narrowing)
The first link of ruling A on objectstack-ai/cloud#2315: spec and client
first, the console reader (objectstack-ai/objectui#11344) second,
cloud's three actions LAST. Ruling B there is not reopened: 「server
returns facts, the console composes the message in the user's locale」.
## What changes
**`ActionSchema.outcomeMessages`**
(`packages/spec/src/ui/action.zod.ts`). This is a map from a snake_case
outcome key (`SnakeCaseIdentifierSchema`) to an `I18nLabel`. The
handler's success payload carries a closed `outcome` fact, and the
console picks `outcomeMessages[result.outcome]`, then falls back to
`successMessage`, then to its default text.
- Refused loudly, each at the key's own path:
- an array or a string (`invalid_type`);
- a key that is not snake_case (`invalid_key` at `outcomeMessages.KEY`);
- the map on `url` / `modal` / `flow` / `form` (no success payload can
carry an outcome);
- the map beside `resultDialog` (which suppresses the toast);
- the map beside `operation: 'update'` (no handler, so no outcome). This
one is a row in the existing declarative-update table, next to its
sibling `onSuccess`.
- `InlineActionSchema` deliberately does not pick it, because no inline
host reads it.
- Interpolation reuses the scope `onSuccess.navigate` already declares:
`successMessage` and each outcome message may interpolate `${result.*}`.
There is no new dialect. The docblock points at the `onSuccess` scope
rather than describing a second one.
**Translation face, beside `successMessage`:**
- `TranslationData` carries
`objects.OBJECT._actions.ACTION.outcomeMessages.OUTCOME` and
`globalActions.ACTION.outcomeMessages.OUTCOME`. Outcome keys there are
snake_case too.
- `translateAction`, which the REST localization seam calls through
`translateMetadataDocument`, overlays each outcome separately: the
object-scoped copy first, then the global one, falling back to the
authored copy. It only overlays outcomes the action declares.
- `os i18n extract` emits one key per declared outcome, on both action
walks.
**`client.environments.delete`** (`packages/client/src/index.ts`). The
archive answer now declares `message?: string` and `outcome?: 'archived'
| 'already_archived' | 'purge_deferred'`. The teardown answer declares
`outcome?: 'destroyed'`.
**Liveness:** the `outcomeMessages` row is `planned`, its carrier is
objectstack-ai/objectui#11344, and it has `authorWarn` set. The
`successMessage` row stays `live` and records the interpolation half
that is declared ahead of its reader.
## Mechanism assumptions, measured (dispatch zone 2)
1. **Holds.** Read at base `ebdb6f2a`: `outcomeMessages` had 0 hits in
`action.zod.ts`. The control `successMessage` hit, as a static
`I18nLabelSchema.optional()`.
2. **Holds.** `onSuccess` declares the `${result.*}` scope in
`action.zod.ts`: its docblock says "`${result.X}` — the action's server
response payload. For a `type:'api'` action, the response body of the
`target` call; for a `type:'script'` action, the handler's return
value". Its `navigate` describe() says the same. Both copy surfaces cite
it and add no new members.
3. **The row lands `planned`, not `live`.** The ledger defines `planned`
as "Declared, intentionally not enforced yet". `check-liveness.mts`
scans a planned row's `evidence` the same way it scans a live row's. Its
authorWarn guidance says: "If nothing reads the key yet, the STATUS is
what is wrong: grade it `planned` (a consumer is being built against
it)". The gate also reds a warned `live` row, so the flip PR must drop
`authorWarn`, and the row's note says so.
- The console reader is absent at both pins. Measured at objectui
`e420df31` (the pin at base) and again at `31971ff1` (the pin after the
base merge): `ActionRunner.handlePostExecution` composes
`result.data.message || action.successMessage || runner default` and
reads no `outcomeMessages`. The objectui main checkout (`3c3ce15`) has 0
hits too.
- `os validate` on a probe stack shows the author warning firing:
`liveness-planned-property`, with the row's hint.
4. **`message` becomes optional, `outcome` is added as optional, and the
vocabulary is split by arm.** The wire at the last measured cloud sha
(`eeac7b22`, recorded in the client test) carries `message` and no
`outcome`. The cloud repo cannot be read from this session (`add_repo`
refused), so today's wire is not re-measured.
- The four outcomes split cleanly across the two existing arms.
`archived`, `already_archived` and `purge_deferred` are all `deleted:
false, archived: true`, and today only the English `message` tells
`archived` from `already_archived`. `destroyed` is the `deleted: true,
purged: true` arm, which also covers the one-call teardown of a `failed`
environment.
- Declaring `destroyed` on the archive arm would claim a combination the
route cannot produce, so each arm carries only its own members.
- A compile-time pin ties the four-member list to the type in both
directions, and a runtime pin parses all four as `outcomeMessages` keys
through the real `ActionSchema`.
5. **No in-repo reader of `message`.** `git grep` over `packages/`,
`apps/` and `examples/` finds only the client's own test. There is no
`os environments delete` command
(`packages/cli/src/commands/environments/` has bind, create, list, show
and switch). The objectui tree at HEAD has no `environments.delete`
caller. The one reader, the test's `const message: string` pin, now pins
the optional shape both ways.
6. **The translation face exists, and the outcome copy rides beside
`successMessage`.** The pieces are `translation.zod.ts`
`actionTranslationSchema` (the slot), `i18n-resolver.ts`
`translateAction` (the overlay) and `cli/src/utils/i18n-extract.ts` (the
extractor). No new face was invented.
## Acceptance, item by item
- **The schema accepts `outcomeMessages` on api/script and refuses an
unknown shape loudly.** Pinned on issue `code` + `path` in
`packages/spec/src/ui/action-outcome-messages.test.ts`, not on prose.
- **"A fixture action whose handler returns `{ outcome: 'archived', … }`
resolves to the archived copy, interpolated."** The SELECTION and the
`${result.*}` SUBSTITUTION are the console's, so they move to
objectstack-ai/objectui#11344. No renderer is built here. What this repo
pins on the cloud-shaped `delete_environment` fixture:
- the map survives parse byte-for-byte, tokens included;
- the archived copy resolves in `zh-CN` through `translateAction` and
`translateMetadataDocument`, with its `${result.environmentId}` /
`${result.retentionDays}` tokens intact for the renderer;
- fallback runs per outcome (object-scoped, then global, then authored);
- a bundle cannot add an outcome the action does not declare.
- **The client type no longer requires `message`.** Pinned by
`@ts-expect-error` in `environments-delete-two-step.test.ts`, which the
package's `typecheck` compiles (`tsc -p tsconfig.test.json --listFiles`
names the file).
- **The changeset names the new key.**
`.changeset/21095-action-outcome-messages.md` carries `Clause-②: yes
(narrowing)`. It widens the spec and narrows the client's return type,
and it holds the ADR-0087 disposition `not-required
(runtime-interface-only
packages/client/src/index.ts#ObjectStackClient)`. The gate verified that
disposition: `check-adr-0087-registration` reports
`[BREAKING+bang+clause-②-narrowing] … verified:
packages/client/src/index.ts#ObjectStackClient (class)`.
## Tests (all at HEAD `00855f76`, after the `origin/main` merge)
- **Patch round 1 (head `3f94d67b`, one file):**
`@objectstack/metadata-protocol` (a consumer) `vitest run` passed: 200
files (3 env-gated live-DB suites skipped), 2973 tests. Its
served-`action` key-count pin in
`protocol.meta-types-degenerate-derivation.test.ts` moves 48 → 49 (46
accepted keys plus 3 tombstones) and now names `outcomeMessages`, so it
asserts the key is served. The readings below are from `00855f76`; the
only diff since is that test file.
- **`@objectstack/spec`:** `pnpm --filter @objectstack/spec test`
passed: 595 files, 17431 tests, 1 todo.
- **`@objectstack/client`:** `vitest run` passed: 50 files, 644 tests.
- **`@objectstack/cli`:** `vitest run --project unit` passed: 243 files,
3439 tests. The integration layer is declared to CI; the diff touches no
spawn entry.
- **Typecheck:** `typecheck` passed for spec, client and cli. Each
includes `check:test-typecheck`. These runs were before the merge, and
the merge touched none of the three packages' TypeScript except spec's
`component`/`view`/`dataset`, which `check:generated` and the full spec
suite above cover.
- **Ablations**, each run with `scripts/ablation-replace.mjs`. Each
landing is proven by anchor count and blob hash, and each restore by
blob equal to HEAD with an empty `git diff HEAD`:
- Deleting `.superRefine(refuseInertOutcomeMessages)`: **6 failed / 14
passed**. These are the url/modal/flow/form/resultDialog refusals and
the metadata-door case.
- Renaming the declarative-update table's `outcomeMessages` row: **1
failed / 19 passed**, the `operation: 'update'` refusal.
- Removing the `translateAction` overlay: **3 failed / 17 passed**, the
locale, fallback and REST-seam cases.
- Reverse verification of the client type: restoring `message: string`
makes `tsc -p tsconfig.test.json` answer `TS2578: Unused
'@ts-expect-error' directive` at the optional-`message` pin.
## Gates
- **At `3f94d67b` (patch round 1):** re-derived 115 commands (round 0's
114 plus `pnpm check:durability-log-level`, matched by the
metadata-protocol path); all 115 run at `3f94d67b`, all exit 0; `--ran`:
"115 derived, 115 run, 0 NOT-MEASURED, 0 UNRUN".
- `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands` (no paths, change set from git, merge base `0d421041`)
derived 114 commands. (round 0)
- All 114 were run at `00855f76`, and all exit 0.
- `--ran` with recorded exit codes answers: "114 derived, 114 run, 0
NOT-MEASURED, 0 UNRUN".
- Three gates first answered exit 3 (PREREQUISITE NOT MET: missing
sibling `dist/`) and were re-run green after the builds:
`check:skill-examples`, `check:dual-build-cjs-loads` and
`check:i18n-coverage`.
- `check:type-check-debt` hit a 400s wrapper timeout once. On re-run it
passed: "1 ledger entr(ies) re-measured … none above its recorded
number".
- `check:generated` reports all 15 artifacts current after the merge.
- **Lint, narrowed and measured.** I ran `eslint --no-inline-config
--format json` on the 9 touched `.ts` files: 9 results, 0 errors, 0
warnings. The config sets no `parserOptions.project` (0 hits; its own
header says it never enables type-aware linting), so the diff cannot
move a verdict on an untouched file. The full `pnpm lint` is CI's.
## Acceptance notes
- **Translation lint gap, introduced with the key.** `os validate` does
not judge `outcomeMessages.OUTCOME` translation keys against the
action's declared outcomes.
- Probe: a stack whose `zh-CN` bundle carries
`_actions.delete_probe.outcomeMessages.restored` for an action declaring
only `archived`.
- Result: `os validate --json` reports nothing for `restored`, while the
same probe's undeclared `params.not_a_param` answers
`translation-target-unknown`.
- Effect: `translateAction` never reads such a key, so it is silently
dropped.
- Not fixed here: it widens `packages/lint`, outside this claim's file
surface. It is handed to the seat in the report.
- **No form row.** `action.form.ts` gains no control for the map.
`metadata-form-zod-reconciliation.test.ts` records it as a `planned`
omission, the shape its siblings use.
- **Stale rationale.** The existing `onSuccess` omission in that ledger
still says its children are `planned`, but they are `live` since the
2026-09-27 flip. I left it untouched; it is outside this card.
- **Hand-written docs untouched on purpose.**
`content/docs/ui/actions.mdx` and
`content/docs/protocol/objectui/actions.mdx` are not updated, because
they should not advertise copy the console does not show yet. They
belong with the flip, in objectstack-ai/objectui#11344's landing.
- **Interpolation is declared ahead of its reader.** `successMessage`'s
`${result.*}` interpolation is in the contract before the console
implements it, which is ruling A's order. Census: zero existing
`successMessage` values in this repo or objectui carry `${`, so nothing
that renders today changes meaning.
_Body edited by the `domain:spec` seat 1
(`session_01UtnxvdiN376GF3sgXwAw4d`) after patch round 1: the `Clause-②`
line carries the changeset's arm, and the Tests and Gates sections carry
the `3f94d67b` readings._
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 9969228 commit d6d6e87
20 files changed
Lines changed: 722 additions & 22 deletions
File tree
- .changeset
- content/docs/references
- data
- kernel
- system
- ui
- packages
- client/src
- cli
- src/utils
- test
- metadata-protocol/src
- spec
- authorable-surface
- liveness
- state-counts
- src
- system
- ui
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
460 | 460 | | |
461 | 461 | | |
462 | 462 | | |
463 | | - | |
| 463 | + | |
| 464 | + | |
464 | 465 | | |
465 | 466 | | |
466 | 467 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
330 | 330 | | |
331 | 331 | | |
332 | 332 | | |
333 | | - | |
| 333 | + | |
| 334 | + | |
334 | 335 | | |
335 | 336 | | |
336 | 337 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
125 | 125 | | |
126 | 126 | | |
127 | 127 | | |
| 128 | + | |
128 | 129 | | |
129 | 130 | | |
130 | 131 | | |
| |||
219 | 220 | | |
220 | 221 | | |
221 | 222 | | |
| 223 | + | |
222 | 224 | | |
223 | 225 | | |
224 | 226 | | |
| |||
416 | 418 | | |
417 | 419 | | |
418 | 420 | | |
| 421 | + | |
419 | 422 | | |
420 | 423 | | |
421 | 424 | | |
| |||
574 | 577 | | |
575 | 578 | | |
576 | 579 | | |
| 580 | + | |
577 | 581 | | |
578 | 582 | | |
579 | 583 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
47 | 47 | | |
48 | 48 | | |
49 | 49 | | |
50 | | - | |
| 50 | + | |
| 51 | + | |
51 | 52 | | |
52 | 53 | | |
53 | 54 | | |
| |||
299 | 300 | | |
300 | 301 | | |
301 | 302 | | |
302 | | - | |
| 303 | + | |
303 | 304 | | |
304 | 305 | | |
305 | 306 | | |
| |||
0 commit comments