Skip to content

feat(spec,client)!: ActionSchema gains outcomeMessages (success copy per handler outcome, ${result.*}), and environments.delete stops guaranteeing message - #21214

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-21095-action-outcome-messages
Oct 1, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-21095-action-outcome-messages

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

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 ActionRunner: compose the success toast from outcomeMessages[result.outcome], then successMessage (both interpolating ${result.*}), then the default; stop reading data.message (ruling A on cloud#2315) 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 ActionRunner: compose the success toast from outcomeMessages[result.outcome], then successMessage (both interpolating ${result.*}), then the default; stop reading data.message (ruling A on cloud#2315) 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.

@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/cli, @objectstack/client, @objectstack/spec, touching 16 documentable anchor(s). ⚠️ 4 changed file(s) yielded no anchor (packages/spec/authorable-surface/ui.json, packages/spec/liveness/action.json, packages/spec/liveness/state-counts/action.md, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/troubleshooting.mdx (via ActionSchema (symbol, a top-level const))
  • content/docs/protocol/objectui/actions.mdx (via ActionSchema (symbol, a top-level const))

⛔ 3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-1.mdx (via ActionSchema (symbol, a top-level const))
  • content/docs/releases/v17/17-3.mdx (via ActionSchema (symbol, a top-level const))
  • content/docs/releases/v17/index.mdx (via ActionSchema (symbol, a top-level const))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 4 changed file(s) yielded no anchor (packages/spec/authorable-surface/ui.json, packages/spec/liveness/action.json, packages/spec/liveness/state-counts/action.md, …) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 143 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json ce4e205e2b6abd7a49f98a27cd8297a1e52f828b → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 1501544d42ec997a8759031d41b0909e99a61f02 — the merge of head a43024e7feca0f1e5c4116c3d1bfbb4cdee5b51b into base ce4e205e2b6abd7a49f98a27cd8297a1e52f828b, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 1501544d42ec997a8759031d41b0909e99a61f02 && git checkout 1501544d42ec997a8759031d41b0909e99a61f02
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ce4e205e2b6abd7a49f98a27cd8297a1e52f828b a43024e7feca0f1e5c4116c3d1bfbb4cdee5b51b && git checkout -B drift-repro ce4e205e2b6abd7a49f98a27cd8297a1e52f828b && git merge --no-ff a43024e7feca0f1e5c4116c3d1bfbb4cdee5b51b

node scripts/docs-audit/affected-docs.mjs --json ce4e205e2b6abd7a49f98a27cd8297a1e52f828b

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs ce4e205e2b6abd7a49f98a27cd8297a1e52f828b → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…lared keys and names outcomeMessages

ActionSchema gained `outcomeMessages`, so the declared key set the
/meta/types degeneracy pin holds the served document to moves 48 to 49
(46 accepted + the three refused tombstones). The sample of addressed
keys now names the new key, so the pin asserts it is served rather than
only that the count rose.

Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 00855f7662f6675ff64c92f4a5dd01b42e71f212
Local-runs: none

Inputs read, and nothing else: card #21095 (body and all three comments: triage 5926403550, claim 5934282106, os-dev-report 5936384729), PR #21214 (body, 19-file list), the net diff origin/main... the head from merge-base 0d421041 (branch fetched into a review ref; its sha equals the head above), and the check-runs on the head. The dispatch order and the dispatching seat's conclusions were not inputs.

Why FAIL, in one line: the required Test Core context is red on this head, and the red is this diff's — the new top-level action key moves a served-schema key-count pin in packages/metadata-protocol that the PR did not move (item 15 below). Every other judgment below holds; the fix is one line plus its comment, and a fresh record is owed on the head that carries it.

① Derived judgments

  1. ActionSchema.outcomeMessages (packages/spec/src/ui/action.zod.ts): a new optional key, z.record(SnakeCaseIdentifierSchema, I18nLabelSchema), on the strict actionObject(). The registered-action accept set WIDENS for type: 'api' and type: 'script'. Right — exactly the card's scope; SnakeCaseIdentifierSchema was already imported (main :23); type is defaulted to 'script' (main :1010) before the superRefine runs, so an action that never wrote type is in scope, and that case is pinned.
  2. Refusals: refuseInertOutcomeMessages (on url / modal / flow / form; beside resultDialog) and the new DECLARATIVE_UPDATE_REFUSED_KEYS row (beside operation: 'update'), each a custom issue at path outcomeMessages with a rewrite. Right — the declared-but-never-read shape is refused at authoring time, with no consumer fallback and no alias (Prime Directive Add comprehensive test suite for Zod schema validation #12). The test pins issue code + path, not prose; the dev's ablation of the superRefine fails 6 cases and of the table row 1 case.
  3. InlineActionSchema does not pick the key: the inline accept set is unchanged (unrecognized_keys pinned). Right — no inline host reads it.
  4. successMessage keeps its type (I18nLabelSchema.optional()); only its describe() now declares ${result.*} interpolation and the fallback role. Accept set unchanged; meaning declared ahead of its reader, which is ruling A's order (spec first). Right — census: zero existing successMessage values in this repo or objectui carry ${, so nothing that renders today changes meaning; the row stays live because the toast reads the key, and the declared-ahead half is recorded on the row and on the planned row rather than by re-grading a working key.
  5. No new interpolation dialect: both copy surfaces cite the scope onSuccess.navigate already declares (main :926-:936, :1587-:1663). Right.
  6. Translation schema (translation.zod.ts): actionTranslationSchema.outcomeMessages: z.record(SnakeCaseIdentifierSchema, z.string()).optional(), on a strict object, for both objects.OBJECT._actions.ACTION and globalActions.ACTION. The bundle accept set widens by one key family; non-snake_case keys are refused located (invalid_key). Right — beside successMessage, as the card asked.
  7. Overlay (i18n-resolver.ts#resolveActionOutcomeMessages, called by translateAction, reached through translateMetadataDocument, the REST seam): declared outcomes only, object-scoped then global, per locale chain; source not mutated; the key is omitted from the result when nothing resolves (reference identity). ActionLike gains an optional outcomeMessages?: Record of string to string, narrowed to string values exactly as successMessage is — an additive optional member on an exported interface, not breaking. Right.
  8. os i18n extract (packages/cli/src/utils/i18n-extract.ts#pushActionOutcomeMessages): one key per declared outcome on both action walks; inline locale maps are counted but not seeded (the rule every sibling copy key follows). Emitted set is unchanged for every config that validated before (no action could carry the key). Right.
  9. @objectstack/client environments.delete (packages/client/src/index.ts): archive answer message: string becomes message?: string — a NARROWING of the published return-type guarantee, compile-breaking for a reader typing it string — plus outcome?: 'archived' | 'already_archived' | 'purge_deferred'; teardown answer gains outcome?: 'destroyed'. Right. The card said the answer "stops declaring message as REQUIRED" and "gains the outcome fact"; keeping message? rather than deleting it is the honest transitional type, since cloud lands LAST and the last measured wire (cloud eeac7b22, recorded in the test) still carries message and no outcome — and outcome optional is the only declaration the runtime delivers today (Prime Directive chore: version packages #10). The vocabulary split by arm follows the existing deleted discriminant; destroyed on the archive arm would claim a combination the route cannot produce. Pinned in both directions (@ts-expect-error compiled by the package typecheck; satisfies plus Exclude closure over the four members; runtime parse of all four as outcomeMessages keys through the real ActionSchema). In-repo callers of environments.delete( outside that test: none (checked on main across packages/, apps/, examples/); packages/cli/src/commands/environments/ has no delete command.
  10. Liveness (packages/spec/liveness/action.json): outcomeMessages graded planned, verifiedAt 2026-10-01, authorWarn + authorHint, carrier ActionRunner: compose the success toast from outcomeMessages[result.outcome], then successMessage (both interpolating ${result.*}), then the default; stop reading data.message (ruling A on cloud#2315) objectui#11344; state-counts/action.md moves to 45 live / 4 dead / 1 planned = 50; two translation.json notes name the key. Right. The card's acceptance bullet wrote "the liveness row is live (the console reads it)", but the measured fact at both objectui pins (e420df31, 31971ff1) is that ActionRunner.handlePostExecution reads no outcomeMessages; a live verdict would be declared-not-enforced, the exact thing Prime Directive chore: version packages #10 forbids, and the gate's own definition of planned is "declared, intentionally not enforced yet". Spec property liveness is green on this head. The flip to live (dropping authorWarn in the same edit) belongs to objectui#11344's landing, and the row says so.
  11. Generated spec artifacts: authorable-surface/ui.json gains ui/Action:outcomeMessages; five content/docs/references/** pages regenerated (the $ plus backticked brace spelling of the tokens is the generator's existing escaping — see the onSuccess rows on main). api-surface/ untouched — right, no export added or removed (resolveActionOutcomeMessages is module-private; the surface snapshot records export existence, not member shape). json-schema.manifest/ui.json untouched — right, it is a def roster and ui/Action is already listed. Type Check · source gates (the check:generated reconcile, check:authorable-surface, check:docs) is green on the head.
  12. Form reconciliation ledger (metadata-form-zod-reconciliation.test.ts): an omit row for outcomeMessages, no form control until enforced — the shape onSuccess uses. Right.
  13. Card acceptance "a fixture action whose handler returns { outcome: 'archived', … } resolves to the archived copy, interpolated": the SELECTION and the ${result.*} SUBSTITUTION are the console's by ruling A's own order, so this repo cannot deliver that half without building a renderer here, which is out of scope and against the ruling's sequence. What this repo owns is pinned on the cloud-shaped delete_environment fixture: the map survives parse byte-for-byte with tokens intact; the archived copy resolves in zh-CN through translateAction and the REST seam; fallback runs per outcome; a bundle cannot add an outcome. Accepted as the correct split; recorded here so the seat reads that acceptance bullet as carried jointly with objectui#11344.
  14. Governed surfaces: none among the 19 paths (no .claude/**, docs/adr/**, skills/**, AGENTS.md, CLAUDE.md); head repo equals base repo; 733 changed lines, well under the 5,000-line class. This record is owed by the Clause-②: yes declaration, not by Prime Directive feat: Comprehensive CRM example demonstrating all ObjectStack protocol features #14. Right.
  15. The served action meta-type's key-count pin is not moved — WRONG, and the red on the head. packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts:247 pins Object.keys(properties).length + retiredTopLevelCount('action') to 48 — its own comment: "48 is the key set action DECLARES — 45 accepted plus the three that admit no instance". This diff adds a 46th accepted top-level key to ActionSchema, so the served /meta/types document for action now carries 49 and the pin reds; @objectstack/metadata-protocol depends on @objectstack/spec (workspace) and is not in the PR's file list. On the head, Test Core (2/6) concluded failure with the step "Run this shard's tests" failing and the annotation command (packages/metadata-protocol) pnpm run test exited (1); the aggregate Test Core — one of the seven required contexts — then concluded failure. The merge-base 0d421041 is green on Test Core, so the same-signature exception does not apply, and the PR introduces no other change that reaches that package. Honest limit: the job log and the shard's run-summary artifact are served from hosts this seat's egress policy denies (403 on CONNECT), so the first error line was not read; the attribution rests on the annotation naming the package and on the pin's arithmetic read from main. Remedy (one edit, this PR): move the pin to 49 and rewrite its comment ("46 accepted plus the three tombstones"; the pin is per declared key, so every new top-level action key pays it), then run pnpm --filter @objectstack/metadata-protocol test — the dev's derived roster of 114 gates covered spec, client and cli and never reached this dependent, which is why a green local run coexisted with a red shard. If that shard carries a second failure the log would show, it surfaces on the re-run.

② Semver level

  • .changeset/21095-action-outcome-messages.md: @objectstack/spec: minor, @objectstack/client: minor, @objectstack/cli: patch; summary line carries the conventional-commit !; body carries the bold BREAKING banner, Clause-②: yes (narrowing), and exactly one ADR-0087 marker: not-required (runtime-interface-only packages/client/src/index.ts#ObjectStackClient).
  • spec minor — right: a purely additive widening of a published accept set takes at least minor (the WHICH LEVEL ruling).
  • client minor — right: the return-type narrowing is BREAKING; during the launch window major is refused by check-changeset-no-major and breaking-ness is carried by the banner plus the ADR-0087 disposition, both present here.
  • cli patch — accepted: the act is a follow-on walk whose emitted set is unchanged for every config that validated before; utils/i18n-extract is not on the CLI's exports map, so no published symbol widens; the pending stock grades the identical act patch (.changeset/19518-picklist-kind.md, .changeset/page-component-slot-positions.md); and the fixed group makes the published bump minor regardless. Noted, not failed: a stricter reading of WHICH LEVEL ("a new accepted key") would grade it minor; the seat may raise it, and it does not move the release.
  • Clause-② arm yes (narrowing) — right: the diff widens (spec) AND narrows (client), and yes (narrowing) is the one legal spelling for that shape (the ADR-0087 gate's own fixture names it). The gate read it as [BREAKING+bang+clause-②-narrowing] (dev report); in CI that gate and the level axis both run inside the Check Changeset job (pr-automation.yml), which is green on the head.
  • ADR-0087 disposition — right: runtime-interface-only holds on its four predicates (ObjectStackClient is an exported class, declared in a file that is neither *.zod.ts nor packages/spec/src/contracts/**, not a Zod projection, referenced in code by no metadata surface); the spec half is a pure widening, so no stored metadata changes meaning and no conversion entry is owed; nothing an author WRITES is removed or renamed, so no FROM-to-TO mapping is due, and the consumer's fix is stated in the body in prose (read deleted / purgeDeferred, or outcome when present; never the message prose).
  • The PR body's line reads Clause-②: yes (copied from the claim) while the changeset reads yes (narrowing). Both are legal declarations, the value is truthful (the diff does widen), and the carrier the ADR-0087 gate reads is the changeset. The body line is incomplete, not false — see ③.
  • skip-changeset: correctly absent. Check Changeset is green on the head.

③ Boundary flags

Dev deviations (os-dev-report 5936384729), each answered:

  1. PR body Clause-②: yes vs changeset yes (narrowing) — ESCALATED to the owning seat: append (narrowing) to line 3 of the PR body (a seat write; the dev writes the body once). Not a landing blocker on its own: the arm is read off the changeset by the gate, and the body's value is true.
  2. File surface beyond the claim's enumeration (cli extractor and its test; the form-reconciliation ledger row; two translation.json notes; regenerated references, authorable-surface/ui.json, state-counts/action.md) — ANSWERED: each is forced by a gate or by where the producer lives; the claim named "the translation face beside successMessage", and the extractor is that face's third piece. One forced file is MISSING, not extra: the metadata-protocol pin in ① item 15 belongs to the same class (a gate forces it), and the same claim covers it.
  3. One refused MCP call (add_repo on cloud, read) — ANSWERED: no effect; its consequence is that today's cloud wire is not re-measured, which is precisely why outcome? and message? are both optional. Right.
  4. wip(...) commit titles — ANSWERED: squash landing; the PR title is the landed message and carries no model identifier.

open_questions: none declared by the dev.

Out-of-scope findings, each disposed:

  • (class c) os validate does not judge _actions.ACTION.outcomeMessages.OUTCOME bundle keys against the action's declared outcomes — packages/lint/src/validate-translation-references.ts judges only params (checkActionParams); resultDialog.fields has the same gap today — so an undeclared outcome key parses and is silently never read. ESCALATED to the seat to file as its own card (Prime Directive chore: version packages #10: a metadata-authoring trap earns an issue; a dispatched dev cannot amend the claim), best landed before objectui#11344 flips the row live. Not a landing blocker: the fix widens packages/lint, outside this claim, and the schema already refuses malformed keys located.
  • Where the server message rung sits in the toast composition — carrier objectui#11344; ruling B implies it leaves. Noted.
  • Stale onSuccess rationale in the form-reconciliation ledger — outside this card; a one-line text fix for whoever next edits that ledger. Noted.

Check-runs on the head, final read 2026-10-01T17:23Z (first read 17:04Z; 13 were still running then, and all have since concluded): 35 check-runs, all completed — 31 success, 2 skipped (Console Pin Gate, Packed-tarball smoke (opt-in): a path-filtered gate and an opt-in one, neither owed by this diff), 2 failure (Test Core (2/6) and the aggregate Test Core, ① item 15), none in_progress. Green: Lint & Repo Gates, TypeScript Type Check with Type Check · source gates / workspace / consumer gates / debt ledger, Test Core 1, 3, 4, 5, 6 of 6, Dogfood Regression Gate and its three shards, Dogfood Verify CLI, Build Core, Build Docs, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard, Check Changeset, Spec property liveness, and the claim, size, docs-link and single-writer guards.

Implemented-by: claude/issue-21095-action-outcome-messages
Reviewed-by: session_01UtnxvdiN376GF3sgXwAw4d

VERDICT: FAIL


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 3f94d67b9c86503e87023bb130cb39ebb1cd92bb
Local-runs: none

Round 1 on PR #21214 (card #21095), following record 5936832663 (FAIL on 00855f76). Inputs read, and nothing else: the card thread (now four comments; new: the dev's round-1 os-dev-report 5937221515), the PR body as it now stands (seat-edited after round 1), its 20-file list, the net diff origin/main... the head from merge-base 0d421041 (branch re-fetched into the review ref; its sha equals the head above), and the check-runs on the head. The dispatch order and the dispatching seat's conclusions were not inputs.

Delta from the round-0 head, verified by diff and not taken from the brief: git diff 00855f76 3f94d67b --stat is exactly one file, packages/metadata-protocol/src/protocol.meta-types-degenerate-derivation.test.ts (+7 / -4), one commit. Nothing under .changeset/, packages/spec, packages/client, packages/cli or content/ moved, so every round-0 judgment on those surfaces is carried forward below, marked as carried, and re-judged only where the delta or the new inputs can touch it.

① Derived judgments

1-14. Carried from record 5936832663, unchanged by the delta (the one-file delta touches no accept set, no public surface, no generated artifact and no governed path): ActionSchema.outcomeMessages widens the registered-action accept set for type: 'api' | 'script' only — right; the three located refusals (url / modal / flow / form, beside resultDialog, beside operation: 'update') — right; InlineActionSchema untouched — right; successMessage type unchanged, describe() declares ${result.*} ahead of its reader — right; no new interpolation dialect (the onSuccess.navigate scope is cited) — right; translation slot actionTranslationSchema.outcomeMessages on both action roots, snake_case keys refused located — right; translateAction per-outcome overlay, declared outcomes only, reaching the REST seam — right; os i18n extract one key per declared outcome on both walks — right; client environments.delete archive answer message?: string + outcome?: 'archived' | 'already_archived' | 'purge_deferred', teardown outcome?: 'destroyed', pinned both ways, no in-repo reader of message — right (the honest transitional type while cloud lands LAST); liveness row planned with authorWarn, carrier objectui#11344, counts 45/4/1 = 50 — right (a live grade would be declared-not-enforced); authorable-surface/ui.json and five reference pages regenerated, api-surface/ and json-schema.manifest/ rightly untouched — right; the form-reconciliation omit row — right; the acceptance bullet's selection/substitution half is the console's by ruling A's order and is carried jointly with objectui#11344 — accepted; no governed surface among the 20 paths, head repo equals base repo, 744 changed lines — right.
15. The served action meta-type key-count pin — now RIGHT (round 0's one FAIL reason). protocol.meta-types-degenerate-derivation.test.ts:247 moves 48 to 49, its comment now reads "46 accepted plus the three that admit no instance" and records why it moved, and the addressed-key sample gains 'outcomeMessages', so the pin asserts the new key is SERVED by /meta/types, not merely that the count rose — a load-bearing change, not a constant bump. The blast-radius pin (exactly one served type differs from the pre-fix derivation) is untouched, which is correct: the new key moves action's document on both derivations alike. On this head Test Core (2/6) and the aggregate Test Core are green, so the red read in round 0 is confirmed as this pin and nothing else. Corroborated by a read of the head: no other test under packages/** or apps/** pins the action key count at 45-49.

② Semver level

Carried unchanged — the changeset file is byte-identical to round 0 (.changeset/21095-action-outcome-messages.md: @objectstack/spec: minor, @objectstack/client: minor, @objectstack/cli: patch; ! in the summary; the bold BREAKING banner; Clause-②: yes (narrowing); one ADR-0087 marker not-required (runtime-interface-only packages/client/src/index.ts#ObjectStackClient)). spec minor right (additive widening of a published accept set); client minor right (BREAKING under the launch-window convention, carried by the banner plus the ADR-0087 disposition, both present); cli patch accepted on the pending stock's grading of the identical act and the fixed group (a stricter WHICH LEVEL reading gives minor; the seat may raise it, it does not move the release); yes (narrowing) the one legal arm for a diff that widens AND narrows; the runtime-interface-only disposition holds on its four predicates, and the body states the consumer's fix in prose. The delta adds a test file to a package the changeset does not bump — right, a test publishes nothing.

New this round, resolved: the PR body's declaration line now reads Clause-②: yes (narrowing), equal to the changeset's arm (the seat's edit, named in the body's footer line). The two carriers agree. Check Changeset on this head is green; the re-run the body edit fired is accounted for in the check-run reading below.

③ Boundary flags

Round-1 dev deviations (os-dev-report 5937221515), each answered:

  1. PR body cited round-0 readings and did not mention the metadata-protocol pin — RESOLVED by the seat's edit, read back from the body as it stands: a "Patch round 1 (head 3f94d67b, one file)" Tests bullet names the consumer package, its 200 files / 2973 tests, and the 48 to 49 move; the Gates section carries the 115-command re-derivation at 3f94d67b.
  2. PR body Clause-②: yes vs changeset yes (narrowing) (carried from round 0) — RESOLVED by the same edit; see ②.

open_questions: none declared.

Round-0 flags, carried: the dev's four round-0 deviations stay answered as in record 5936832663 (file surface forced by gates and producers — and the metadata-protocol pin that was missing from it is now in the PR; the refused add_repo read with no effect; wip(...) titles for a squash landing).

Out-of-scope findings, each disposed:

  • New: component-action-element-rows-20371.test.ts pins the action:button / action:icon FORWARDED key set measured from objectui's renderer forwards; outcomeMessages is not forwarded there yet. Correct as is on this head (nothing reads it), and a carrier item for objectui#11344: the forward and the ActionButtonPropsSchema / ActionIconPropsSchema rows must grow in the landing that ships the reader, or an outcomeMessages authored on an action:button node is dropped one hop before the runner. Noted; carrier named.
  • Carried: (class c) os validate does not judge _actions.ACTION.outcomeMessages.OUTCOME bundle keys against declared outcomes (packages/lint checks only params; resultDialog.fields shares the gap) — still ESCALATED to the seat to file as its own card, best landed before objectui#11344 flips the row live. Not a landing blocker.
  • Carried: the server-message rung in the toast composition (carrier objectui#11344); the stale onSuccess omission rationale in the form-reconciliation ledger (outside the card). Noted.

Check-runs on the head, final read 2026-10-01T17:56Z: 42 check-runs, all completed — 38 success, 4 skipped, none failure, none in_progress. The seat's body edit fired a second pull_request run of the PR-automation guards, so several names appear twice (Check Changeset, Auto Label, Check PR Size, the claim and single-writer guards); both Check Changeset runs — the push run (started 17:23Z) and the edited-body run (started 17:54Z) — concluded success. The seven required contexts are all green on this head: Lint & Repo Gates, TypeScript Type Check (with Type Check · source gates / workspace / consumer gates / debt ledger), Test Core (all six shards, including the 2/6 shard that was red on 00855f76), Dogfood Regression Gate (and its three shards), Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard; also green: Check Changeset, Spec property liveness, Dogfood Verify CLI, Build Docs, Check Documentation Links. Skipped: Console Pin Gate and Packed-tarball smoke (opt-in) (path-filtered and opt-in, neither owed by this diff) plus the edited-event duplicates of Auto Label and Check PR Size.

Implemented-by: claude/issue-21095-action-outcome-messages
Reviewed-by: session_01UtnxvdiN376GF3sgXwAw4d

VERDICT: PASS


Generated by Claude Code

… yet, instead of forecasting a release

Claude-Session: https://claude.ai/code/session_01UtnxvdiN376GF3sgXwAw4d
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a43024e7feca0f1e5c4116c3d1bfbb4cdee5b51b
Local-runs: none

Round 2 on PR #21214 (card #21095), following records 5936832663 (FAIL on 00855f76) and 5937329231 (PASS on 3f94d67b). Inputs read, and nothing else: the card thread (now five comments; new: the dev's round-2 os-dev-report 5937509065), the PR body as it stands (unchanged since round 1), its 20-file list, the net diff origin/main... the head from merge-base 0d421041 (branch re-fetched into the review ref; its sha equals the head above), and the check-runs on the head. The dispatch order and the dispatching seat's conclusions were not inputs.

Delta from the round-1 head, verified by diff and not taken from the brief: git diff 3f94d67b a43024e7 --stat is exactly one file, .changeset/21095-action-outcome-messages.md (+1 / -1), one commit: on line 27 the forecast "That reader ships in the console's next release. Until then the key is accepted, …" becomes the present fact "The console does not read it yet. Until it does, the key is accepted, …". Frontmatter, the ! summary line, the BREAKING banner, the Clause-②: yes (narrowing) line and the ADR-0087 marker are byte-identical to rounds 0 and 1. Nothing under packages/** or content/** moved, so every round-0 and round-1 judgment on code, schema, artifacts and tests is carried forward below and marked as carried.

① Derived judgments

1-14. Carried from record 5936832663 (the delta touches no accept set, no public surface, no generated artifact, no test and no governed path): ActionSchema.outcomeMessages widens the registered-action accept set for type: 'api' | 'script' only — right; the three located refusals — right; InlineActionSchema untouched — right; successMessage type unchanged, describe() declares ${result.*} ahead of its reader — right; no new interpolation dialect — right; translation slot on both action roots with snake_case keys refused located — right; translateAction per-outcome overlay reaching the REST seam — right; os i18n extract one key per declared outcome on both walks — right; client environments.delete message? plus outcome? split by arm, pinned both ways, no in-repo reader of message — right; liveness planned with authorWarn, carrier objectui#11344 — right; generated artifacts (authorable-surface/ui.json, five reference pages) regenerated, api-surface/ and json-schema.manifest/ rightly untouched — right; form-reconciliation omit row — right; the acceptance bullet's selection half carried jointly with objectui#11344 — accepted; no governed surface among the 20 paths, head repo equals base repo, 744 changed lines — right.
15. Carried from record 5937329231: the served-action key-count pin at protocol.meta-types-degenerate-derivation.test.ts reads 49 and names outcomeMessages in its addressed-key sample — right; Test Core green on this head as on the last.
16. The changeset sentence — RIGHT, and the only judgment the delta owns. A changeset body is copied verbatim into the published CHANGELOG.md, so "That reader ships in the console's next release" was a release forecast made by an AI seat in release-owned text: a release is the maintainer's act (Prime Directive #15), and the sentence advertised copy the console does not show (Prime Directive #10). "The console does not read it yet" is the measured present fact (no reader at objectui pins e420df31 / 31971ff1, ① item 10) and carries no promise. The preceding sentence ("The console picks outcomeMessages[result.outcome], falls back to successMessage, and then to its default text.") stays in the present tense; read with its new neighbour it states the selection rule the console will implement, not shipped behaviour, and the describe() on the key uses the same voice — acceptable as written. If the seat wants the rule unmistakably normative, the dev's one-word edit ("picks" to "is to pick") is a prose nicety in the seat's ACCEPT, not a contract face and not a reason to hold the head.
17. The branch sits behind origin/main (merge-base unchanged at 0d421041; 11 commits behind at reading time), and the gates ran on the merge ref, not the branch tip. Read from the two trees, not from the dev's local probe: the set of files main moved since the merge-base that this PR also touches is exactly one, the generated content/docs/references/ui/component.mdx — main regenerated it for the object-master-detail-form rows (hunks at :950-:975), this PR for the successMessage describe() row (hunk at :179), the hunks are disjoint, and main's copy carries the old describe() text exactly once (the row this PR rewrites), so the text merge GitHub builds equals what gen:docs produces on the merged tree — no jointly-wrong artifact (the check:docs gate on the merge ref is the verdict, read below); main's other generated move, packages/spec/api-surface/ui.json, is not in this PR's diff; scripts/doc-authoring-prose-id.baseline.json lost 30 rows on main (a shrink-only ratchet) and is not in this PR's diff, so the question is whether the PR's own strings survive the shrunken baseline — Lint & Repo Gates on the merge ref is the verdict and it is read below. The dev's local probe of that gate mixed a restored baseline into an un-merged tree and went red on packages/rest files this PR never touched; the record discounts it, as the dev did. A base merge before landing is the seat's call (the queue rebuilds onto current main either way); it is not a contract question.

② Semver level

Carried — the changeset's declaration is byte-identical (@objectstack/spec: minor, @objectstack/client: minor, @objectstack/cli: patch; !; the bold BREAKING banner; Clause-②: yes (narrowing); one marker not-required (runtime-interface-only packages/client/src/index.ts#ObjectStackClient)). spec minor right; client minor right (BREAKING under the launch-window convention, carried by banner plus disposition); cli patch accepted on the pending stock's grading of the identical act and the fixed group; yes (narrowing) the one legal arm for a diff that widens AND narrows; the runtime-interface-only disposition holds on its four predicates and the body states the consumer's fix in prose. The PR body's line reads Clause-②: yes (narrowing), equal to the changeset's arm. The delta improves the changeset without moving any of this: the one sentence it changes is descriptive prose, and the dev's scoped re-derivation of the changeset-path gates (ADR-0087 registration, no-major with the level axis, empty-changeset, their self-tests) answered green at this head; in CI those two gates live in the Check Changeset job, read below.

③ Boundary flags

Round-2 dev deviations (os-dev-report 5937509065), each answered:

  1. No merge of origin/main this round; the branch is behind main — ANSWERED, see ① item 17: CI judged the merge ref, and whether to merge base before arming is the seat's landing decision, not a contract defect. Not a landing blocker on the contract face.
  2. Worktree recreated from the remote head and removed after the report — ANSWERED: housekeeping, no effect on the diff; the remote branch head equals the head reviewed here.

open_questions: none declared.

Carried flags, unchanged: the round-1 deviations stay RESOLVED by the seat's body edit (the Clause-② line and the 3f94d67b Tests/Gates readings; the body needs no round-2 reading, since no code or test moved); the round-0 deviations stay answered as in record 5936832663.

Out-of-scope findings, each disposed (nothing new this round, by the dev's report and by the diff):

  • Carried: (class c) os validate does not judge _actions.ACTION.outcomeMessages.OUTCOME bundle keys against declared outcomes — still ESCALATED to the seat to file as its own card, best landed before objectui#11344 flips the row live. Not a landing blocker.
  • Carried: the action:button / action:icon forwarded-key set must grow with the console reader (carrier objectui#11344); the server-message rung in the toast composition (carrier objectui#11344); the stale onSuccess omission rationale in the form-reconciliation ledger (outside the card). Noted.

Check-runs on the head, final read 2026-10-01T18:31Z (first read 18:09Z, when 18 of 32 were still running): 35 check-runs, all completed — 33 success, 2 skipped (Console Pin Gate, Packed-tarball smoke (opt-in): path-filtered and opt-in, neither owed by this diff), none failure, none in_progress. The seven required contexts are all green on this head: Lint & Repo Gates (the merge-ref verdict ① item 17 defers to), TypeScript Type Check (with Type Check · source gates / workspace / consumer gates / debt ledger), Test Core (all six shards), Dogfood Regression Gate (and its three shards), Build Core, Temporal Conformance (live PG + MySQL), Governed Surface Queue Guard; also green: Check Changeset, Spec property liveness, Dogfood Verify CLI, Build Docs, Check Documentation Links, and the claim, size, docs-flag and single-writer guards.

Implemented-by: claude/issue-21095-action-outcome-messages
Reviewed-by: session_01UtnxvdiN376GF3sgXwAw4d

VERDICT: PASS


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants