Skip to content

Commit f87239a

Browse files
feat(spec): CloneFlowResponseSchema declares an optional data.noticeCode beside notice (#29779)
<!-- claude-projects-attribution: {"github_login":"os-warren"} --> _Requested by **Warren** · [project thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8JbeVEbdsiqjx3ASFPM7H2E)_ Fixes #29758 Clause-②: yes (widening) Metadata-protocol: none This is the contract-first half of #29727 and unblocks it: the server half (sending the code from the flow clone door) stays on #29727. ## 维护者速读 - 改了什么:流程「新建相似」(clone) 接口的响应 schema 在 `notice` 旁新增可选字段 `noticeCode`,用一个稳定代码标识这句提示。 - 为什么改:`notice` 是服务端写死的英文句子,控制台无从得知是哪一句,中文界面只能原样显示英文;有了代码,控制台可以换成自己的译文。 - 风险与回滚:仅为可选键,旧服务端不发它时照常解析,`notice` 仍必填、仍是英文兜底;回滚即撤掉该键,无数据迁移。 - 先看:`packages/spec/src/api/automation-api.zod.ts`(`CloneFlowResponseSchema` 的 `noticeCode` 与其说明),`packages/spec/src/api/automation-api.zod.test.ts`(四条 pin),`.changeset/29758-clone-flow-notice-code.md`。 - 两分钟验证:`pnpm --filter @objectstack/spec exec vitest run src/api/automation-api.zod.test.ts`,预期 72/72 通过。 - 需要你做的:无,席位落地(键名 `noticeCode` 在否决窗口内,有异议请在本 PR 说)。 ## What changes - `CloneFlowResponseSchema.data` (`packages/spec/src/api/automation-api.zod.ts`) declares `noticeCode: z.string().optional()` beside `notice`, described as a stable code naming the `notice` sentence: a client maps a code it knows to its own translated sentence and falls back to `notice` verbatim for an unknown or absent code. No code values are enumerated; the vocabulary is the server's (#29727). `notice` and its describe are unchanged. The schema's TSDoc gains one sentence saying the same. - Name: `noticeCode`, the seat's call under the veto window. A key named `THING` + `Code` for the code of a sibling sentence follows `declaredCode` in `packages/spec/src/api/contract.zod.ts`. - Generated: `content/docs/references/api/automation-api.mdx` (via `gen:docs`) is the only artefact the member moves. `check:authorable-surface`, `check:api-surface`, `check:export-origins`, `check:declaration-map` and `check:strictness-ledger` stayed green with no regeneration: the authorable-surface shard lists `api/CloneFlowResponse:data` and nothing below it, and no export was added. - Changeset: `@objectstack/spec` **patch**, not the minor the dispatch named. `Metadata-protocol: none` (a REST answer shape is nothing a metadata author writes, and no authorable-surface shard moved), and `scripts/check-changeset-no-major.mjs` refuses a `minor` beside `none`; a non-protocol widening ships `patch` and keeps `Clause-②: yes (widening)`. `@objectstack/client` gets no entry: `automation.clone()` returns `CloneFlowResponse['data']` imported from `@objectstack/spec` (a dependency, external to the client's tsup build), so its emitted types do not change. ## Why The Console prints `data.notice` verbatim, so the flow "New similar" notice shows English under zh-CN (#29706 row 1). Nothing in the answer named which sentence it was. A code beside it lets the Console translate it, contract-first, before the server sends one. ## Tests `packages/spec/src/api/automation-api.zod.test.ts`, new `describe('CloneFlowResponseSchema — data.noticeCode names the notice sentence')`: 1. accepts a string `noticeCode` beside `notice` and keeps it on the parsed value; 2. an answer without `noticeCode` still parses; 3. `notice` is still required (one issue, `invalid_type` at `data.notice`); 4. a non-string `noticeCode` is refused (one issue, `invalid_type` at `data.noticeCode`). Ablation, once, on the committed head `d419dc32` through `scripts/ablation-replace.mjs` (the test imports `./automation-api.zod` from source, so no dist leg): renaming the key to `noticeCodeAblated` landed on disk (anchor 1 → 0, blob `767587bb7ef0` → `c06b7927ece5`); pins 1 and 4 went red (2 failed, 70 passed); restored to blob `767587bb7ef0` with `git diff HEAD` empty. ## Acceptance notes Consumers of `CloneFlowResponse`, read on `origin/main` `c9c68288`: - `packages/client/src/index.ts` `automation.clone()` returns `CloneFlowResponse['data']`: gains the optional key through the type, no edit. - `packages/runtime/src/domains/automation.ts` (the clone handler) answers `{ flow, packageId, notice: FLOW_CLONE_NOTICE }` from `packages/runtime/src/flow-clone.ts`: unchanged here, and still parses (the key is optional). Serving the code is #29727. - `packages/runtime/src/route-ledger.ts` names `CloneFlowResponseSchema` for the clone row; its note spells `data` as `{ flow, packageId, notice }`, which #29727 updates when it serves the code. - `packages/runtime/src/domains/automation-flow-clone-custom-package.test.ts` parses each answer through the schema: still green by construction (optional key). - `ui/packages/app-shell/src/views/studio-design/AutomationsPillar.newSimilar-28567.test.tsx` names the schema in a comment only. The Console mapping is #29706. `packages/metadata-protocol/src/protocol.ts` is not touched (freeze). ## Verification All readings on head `be18e3d1` (`objectstack-ai/objectstack`, merge base `c9c68288`). <details><summary>Local runs</summary> - `pnpm --filter @objectstack/spec build` under the verify lock: VERDICT command-exit 0. - `pnpm --filter @objectstack/spec check:generated`: first run named one stale artefact (`content/docs/references/**`), regenerated with `gen:docs`; re-run "All 14 generated artifacts are up to date", exit 0. - `pnpm --filter @objectstack/spec typecheck` under the lock: VERDICT command-exit 0. - `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2` (the whole spec suite) under the lock: VERDICT command-exit 0, Test Files 892 passed (892), Tests 25326 passed, 1 todo. - `pnpm lint` (`eslint . --no-inline-config`): exit 0. - `node scripts/pm/dispatch-gates.mjs --commands`: 102 derived families. Run with exit codes recorded before any pipe, then `--ran`: "102 derived famil(ies) accounted for — 101 run, 1 NOT-MEASURED". Six first exited 3 on missing builds; after `turbo run build` for `@objectstack/lint...`, `@objectstack/objectql`, `@objectstack/formula` and `@objectstack/client-react...`, five re-ran at exit 0. NOT MEASURED: `check:dual-build-cjs-loads` (needs every package's dist; left to CI). - `node scripts/check-changeset-no-major.mjs --base c9c6828 --event EVENT` with this body: exit 0, "LEVEL AXIS: clause-② yes (widening) and metadata-protocol none … a non-protocol change at patch". </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. Card thread session `session_01JQwxbWyGo9rhjNEzeo5Ujt`, dispatched by `domain:server#1`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01JQwxbWyGo9rhjNEzeo5Ujt --- _Generated by [Claude Code](https://claude.ai/code/session_01JQwxbWyGo9rhjNEzeo5Ujt)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 69dbb3c commit f87239a

4 files changed

Lines changed: 79 additions & 2 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
feat(spec): the flow clone answer declares an optional `noticeCode` beside its English `notice`
6+
7+
Clause-②: yes (widening)
8+
Metadata-protocol: none
9+
10+
`CloneFlowResponseSchema` (the answer of `POST /api/v1/automation/:name/clone`, "New similar") declared `data: { flow, packageId, notice }`, and `notice` is the server's English sentence with nothing that names it, so a console could only print it verbatim. `data` now also declares `noticeCode?: string`: a stable code naming the `notice` sentence. A client maps a code it knows to its own translated sentence and falls back to `notice` verbatim for a code it does not know, or when the key is absent.
11+
12+
- **Optional.** An answer without `noticeCode` (every server before the one that sends it) still parses; `notice` is still required and keeps its meaning as the English fallback.
13+
- **The values are the server's.** The schema does not enumerate them; the server that serves the clone door owns the vocabulary.
14+
- **Not metadata.** This is a REST answer shape, not something a metadata author writes, so the metadata protocol does not move. `@objectstack/client`'s `automation.clone()` returns `CloneFlowResponse['data']`, so its return type gains the optional key with no change to the client package.

‎content/docs/references/api/automation-api.mdx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ const result = AutomationApiErrorCode.parse(data);
124124
| **success** | `boolean` | ✅ | Operation success status |
125125
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| …>; declaredCode?: string; message: string; userMessage?: string; … }` | optional | Error details if success is false |
126126
| **meta** | `{ timestamp: string; duration?: integer; requestId?: string; traceId?: string }` | optional | Response metadata |
127-
| **data** | `{ flow: object; packageId: string; notice: string }` | ✅ | |
127+
| **data** | `{ flow: object; packageId: string; notice: string; noticeCode?: string }` | ✅ | |
128128

129129
### Nested Shape: `CloneFlowResponse.error`
130130

@@ -156,6 +156,7 @@ const result = AutomationApiErrorCode.parse(data);
156156
| **flow** | `{ name: string; label: string; description?: string; successMessage?: string; … }` | ✅ | The copy as stored: switched off, in `packageId`, with no lineage. |
157157
| **packageId** | `string` | ✅ | The package the copy landed in: the one the request named, else the source's paired customization package `<base>.custom`. |
158158
| **notice** | `string` | ✅ | One line for the administrator, in the server's words: the copy is switched off until someone switches it on, and its references are not re-pointed. |
159+
| **noticeCode** | `string` | optional | A stable code naming the `notice` sentence beside it, so a client can show that sentence in its own language: a client maps a code it knows to its own translated sentence, and falls back to `notice` verbatim for a code it does not know or when this key is absent (an older server sends none). The values are the server's; `notice` stays the English fallback. |
159160

160161

161162
---

‎packages/spec/src/api/automation-api.zod.test.ts‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
TriggerFlowResponseSchema,
1515
ToggleFlowRequestSchema,
1616
ToggleFlowResponseSchema,
17+
CloneFlowResponseSchema,
1718
ListRunsRequestSchema,
1819
ListRunsResponseSchema,
1920
GetRunRequestSchema,
@@ -710,6 +711,58 @@ describe('ToggleFlowResponseSchema', () => {
710711
});
711712
});
712713

714+
// ==========================================
715+
// Clone Flow
716+
// ==========================================
717+
718+
describe('CloneFlowResponseSchema — `data.noticeCode` names the `notice` sentence', () => {
719+
const cloneAnswer = (data: Record<string, unknown>) => ({
720+
success: true,
721+
data: {
722+
flow: {
723+
name: 'acme_lead_followup',
724+
label: 'Lead Follow-up (ACME)',
725+
type: 'autolaunched',
726+
status: 'obsolete',
727+
nodes: [
728+
{ id: 'start', type: 'start', label: 'Start' },
729+
{ id: 'end', type: 'end', label: 'End' },
730+
],
731+
edges: [{ id: 'e1', source: 'start', target: 'end' }],
732+
},
733+
packageId: 'crm.custom',
734+
notice: 'The copy is switched off until someone switches it on; its references are not re-pointed.',
735+
...data,
736+
},
737+
});
738+
739+
it('accepts a string `noticeCode` beside `notice` and keeps it', () => {
740+
const result = CloneFlowResponseSchema.safeParse(cloneAnswer({ noticeCode: 'FLOW_CLONE_DISABLED_COPY' }));
741+
expect(result.success).toBe(true);
742+
expect(result.data?.data.noticeCode).toBe('FLOW_CLONE_DISABLED_COPY');
743+
});
744+
745+
it('still parses an answer without `noticeCode` (an older server sends none)', () => {
746+
const result = CloneFlowResponseSchema.safeParse(cloneAnswer({}));
747+
expect(result.success).toBe(true);
748+
expect(result.data?.data.noticeCode).toBeUndefined();
749+
});
750+
751+
it('still requires `notice`, the English fallback', () => {
752+
const answer = cloneAnswer({ noticeCode: 'FLOW_CLONE_DISABLED_COPY' });
753+
delete (answer.data as Record<string, unknown>).notice;
754+
const result = CloneFlowResponseSchema.safeParse(answer);
755+
expect(result.success).toBe(false);
756+
expect(result.error?.issues.map((i) => [i.code, i.path.join('.')])).toEqual([['invalid_type', 'data.notice']]);
757+
});
758+
759+
it('refuses a non-string `noticeCode`, located on the key', () => {
760+
const result = CloneFlowResponseSchema.safeParse(cloneAnswer({ noticeCode: 42 }));
761+
expect(result.success).toBe(false);
762+
expect(result.error?.issues.map((i) => [i.code, i.path.join('.')])).toEqual([['invalid_type', 'data.noticeCode']]);
763+
});
764+
});
765+
713766
// ==========================================
714767
// List Runs
715768
// ==========================================

‎packages/spec/src/api/automation-api.zod.ts‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -709,7 +709,10 @@ export type CloneFlowRequestParsed = z.infer<typeof CloneFlowRequestSchema>;
709709
* relabelled and switched off (`status: 'obsolete'`), with no package
710710
* envelope and no reference to the source (ADR-0126 §9: no clone lineage).
711711
* `data.packageId` is the package the copy landed in, so a console opens the
712-
* copy where it lives.
712+
* copy where it lives. `data.notice` is the server's English sentence;
713+
* `data.noticeCode`, when present, names that sentence by a stable code so a
714+
* console can show its own translation, falling back to `notice` for a code it
715+
* does not know or when the code is absent.
713716
*/
714717
export const CloneFlowResponseSchema = lazySchema(() => BaseResponseSchema.extend({
715718
data: z.object({
@@ -722,6 +725,12 @@ export const CloneFlowResponseSchema = lazySchema(() => BaseResponseSchema.exten
722725
'One line for the administrator, in the server\'s words: the copy is switched off until '
723726
+ 'someone switches it on, and its references are not re-pointed.',
724727
),
728+
noticeCode: z.string().optional().describe(
729+
'A stable code naming the `notice` sentence beside it, so a client can show that sentence in '
730+
+ 'its own language: a client maps a code it knows to its own translated sentence, and falls '
731+
+ 'back to `notice` verbatim for a code it does not know or when this key is absent (an older '
732+
+ 'server sends none). The values are the server\'s; `notice` stays the English fallback.',
733+
),
725734
}),
726735
}));
727736
export type CloneFlowResponse = z.input<typeof CloneFlowResponseSchema>;

0 commit comments

Comments
 (0)