Repository navigation
Commit 4be9734
fix(automation)!: a
<!-- claude-projects-attribution: {"github_login":"os-bill"} -->
_Requested by **Bill** · [project
thread](https://claude.ai/code/project/chan_016iMXWSZDzqWRWvWzdNo8wg?thread=cmsg_016iMXWSZDzqWRWvWzdNo8wgPDswMz5dggCgdCaEMC4bNX)_
domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF
Fixes #30178
Ruling-ref: 6648655128
Clause-②: yes (narrowing) — on ruling 6648655128 (letter A): a `percent`
or `currency` screen field now refuses any non-number value with
`invalid_type` (no coercion, A′ of 2026-09-13), where it passed every
string before; owes a contract-review-tier review before the queue, a
BREAKING banner and the ADR-0087 disposition marker. Amended on the
ruling; the earlier reading is superseded.
Metadata-protocol: none
Skills-docs: none — `skills/**` teaches no screen-field value shape (the
`objectstack-automation` screen row names `type` an open widget hint and
states no `number` rule either), so nothing there turns false;
`content/docs/automation/flows.mdx` is updated here.
## 维护者速读
- **改了什么:** 流程屏幕字段 `currency`、`percent` 并入 `number` 的数值值域:恢复时值不是有限 JSON
数字就拒(`invalid_type`,400 `INVALID_SCREEN_INPUT`,挂起保留),不把数字字符串当数字;控制台把
`percent` 渲染成数字输入,带上下限、提交数字;spec 契约文字与 flows 文档同步写成三种数值类型。
- **为什么改:** HotCRM 报价折扣上限 60:控制台把 `percent` 当文本发
`"75"`,客户端和引擎都不查字符串,运行吃掉挂起,下游规则拒单,销售只能关窗重来。Bill 裁定 A(6648655128):补全
A′,拒,不强转,控制台同 PR 改。
- **风险与回滚:** 收窄:往 `currency`/`percent` 屏幕字段 POST 字符串的 REST/MCP 调用方会被 400
拒(可重试);控制台本 PR 起发数字。不改 Zod 形状、不加错误码、不动存量数据。回滚即撤 PR。
- **先看:**
`packages/services/service-automation/src/screen-input-contract.ts`
的值域表两行;`ui/packages/app-shell/src/views/ScreenView.tsx` 的 `htmlType`
一行;两个 changeset 的 BREAKING 段。
- **两分钟验证:** `pnpm --filter @objectstack/service-automation exec vitest
run src/screen-input-contract.test.ts`,预期 17 个测试全绿,其中
`percent`/`currency` 的 `"75"`、`"60"`、`"abc"` 都得 `invalid_type`。
- **需要你做的:** 无,席位安排入队前契约复审后落地。
## What changes
**Engine (`@objectstack/service-automation`).**
`SCREEN_FIELD_VALUE_DOMAINS` gains `currency` and `percent`, on
`number`'s own predicate (a finite JSON number) and sentence (`must be a
number`). A present value that is not one is refused with `invalid_type`
inside `INVALID_SCREEN_INPUT` (400, retryable, the suspension kept), ⛔
never coerced: `"75"` under a `max` of 60 is refused for its shape, not
judged as 75, and `"60"` is refused the same way. A JSON number on
either kind is judged against the bound as before. `rating`, `slider`,
`progress`, `text` and untyped fields still take a string (ruling: not
widened past the numeric kinds). The module docstring now names the
three numeric kinds as one value domain, the same group the object
protocol names (`MEASURE_FIELD_TYPES`).
**Console (`@object-ui/app-shell`), the one `ui/` file the ruling
opens.** `ScreenView.tsx`: `percent` takes the numeric input path with
`number` and `currency`, so it carries `min` / `max` as native
attributes, the browser stops an over-limit value before any request,
and the runner receives a number (the existing `Number(...)` on the
numeric input), which the client bound check
`screenFieldBoundViolations` already compares. No dedicated percent
widget (ruling: the ui lane's later choice).
**Spec prose (`@objectstack/spec`), no shape change.**
`ScreenFieldSpec.type` / `.min` jsdoc
(`contracts/automation-service.ts`), `ScreenFieldConfigSchema.type` /
`.min` / `.max` jsdoc and the `type` `.describe()`
(`automation/builtin-node-config.zod.ts`) say the three numeric kinds
take a finite number on resume and a numeric string is refused.
`content/docs/references/automation/builtin-node-config.mdx` is
regenerated from the describe (`gen:schema && gen:docs`). Every key and
type is unchanged.
**Docs.** `content/docs/automation/flows.mdx`: the three sentences that
named `type: 'number'` alone as the checked shape now name the three
numeric kinds (the shape paragraph under the bound, the "a bound on a
non-numeric field" bullet, and the refusal list's parenthesis). The
ruling named "the sentence"; the other two would have been false after
this PR.
**Changesets.** `30178-numeric-screen-fields-refuse-strings.md`
(`@objectstack/service-automation`, `@object-ui/app-shell`) and
`30178-screen-field-numeric-kinds-contract-text.md`
(`@objectstack/spec`): both `patch` (`Metadata-protocol: none` takes
`patch`, AGENTS.md Post-Task Checklist step 3), both with the BREAKING
banner, both declaration lines, and the ADR-0087 disposition
`not-required (no-migration-prescription)`: no schema key, export or
stored row moves, so there is nothing for `objectstack migrate meta` to
rewrite.
## Tests
- `screen-input-contract.test.ts`, new top-level `describe` (#30178),
for each of `percent` and `currency`: `"75"`, `"60"`, `"abc"` →
`invalid_type` (field named, message `must be a number`); `75` →
`max_value`, `-1` → `min_value`, `0` and `60` → `[]`; absent and `''` →
`[]`; a hidden field (`visibleWhen` false or unevaluable) is not judged,
with the visible control; and `rating` / `slider` / `progress` / `text`
/ untyped still admit `"75"`. The closure comment in the existing
"closed value-domain members" case is updated.
- `ScreenView.boundHelpReference-9248.test.tsx` (the existing bound
harness), new top-level `describe`: a `percent` field renders
`type="number"` carrying `min="0"` / `max="60"`, typing `75` calls
`onValueChange('discount', 75)` with a number, and
`screenFieldBoundViolations` flags it.
- **Ablations** (`scripts/ablation-replace.mjs`, restore proven by blob
hash == HEAD and an empty `git diff HEAD`):
1. Delete the `currency` and `percent` rows →
`screen-input-contract.test.ts` 4 failed | 13 passed (both "refuses a
string" cases and both hidden-field controls); restored.
2. Delete ` || kind === 'percent'` from `ScreenView.tsx` → the new
console pin fails (1 failed | 14 passed); restored.
Both subjects are imported by relative path from `src/`, so no `dist/`
leg applies.
## Acceptance notes
- **Caller-facing narrowing.** A REST or MCP caller that posted a
numeric string to a `currency` or `percent` screen field is now refused
400 `INVALID_SCREEN_INPUT` (`invalid_type`); it posts the JSON number.
In-tree example flows declare no `currency` / `percent` screen field
(`git grep`); the known declaration is hotcrm's Generate Quote
`discount`, which re-measures on this pin (card body), nothing in that
repository touched here.
- **Changeset wording.** The first draft carried a metadata-style FROM →
TO block for the caller's request bag; `check-adr-0087-registration`
reads that label as a migration prescription and refuses
`no-migration-prescription` beside it. The caller's fix is now a plain
bullet in the body: nothing at rest moves, so the ledger has nothing to
carry.
- **Changeset level.** `patch` on all three packages, by the AGENTS.md
rule (`Metadata-protocol: none` ⇒ `patch`, BREAKING banner and
disposition kept). The ruling noted A′'s landing took `minor`; that
landing declared `Metadata-protocol` kinds this one does not. The seat
can re-level if the gates say otherwise.
- **`ui/` scope.** Exactly `ScreenView.tsx` and its existing bound test
harness; every other `ui/**` path untouched.
## Verification
Head `3eb877a57` (`claude/project-thread-xq1cua`), `origin/main` merged
once at `797f9257` (merge `0d70b581`); the last commit changes the two
changeset bodies only, and the changeset gates were re-run on it.
<details><summary>Package tests and type-checks</summary>
- `pnpm --filter @objectstack/service-automation test`: exit 0. `pnpm
--filter @objectstack/service-automation typecheck`: exit 0.
- `screen-input-contract.test.ts` + `screen-input-file-field.test.ts`: 2
files, 23 tests passed.
- `ui`: `pnpm exec vitest run packages/app-shell/src/views/ScreenView
packages/app-shell/src/views/__tests__/FlowRunner` (under the verify
lock): 22 files, 152 tests passed.
- `pnpm --filter @objectstack/spec build` (declarations on), then
`check:generated`: two artifacts stale, `content/docs/references/**`
(regenerated, `gen:schema && gen:docs`, committed) and
`check:test-typecheck`, which had failed without diagnostics at the 4096
MB heap and passed alone at 6144 MB (52 files / 240 errors / 131
signatures held, unchanged). `pnpm --filter @objectstack/spec
typecheck`: exit 0.
- NOT MEASURED: `pnpm --filter @object-ui/app-shell type-check` (exit 2,
every error `TS2307 Cannot find module '@object-ui/…'`: the `ui/`
sibling packages are not built in this worktree). `Type Check UI` on
this PR is the reading.
</details>
<details><summary>Derived gates: 109 commands, 108 green, 1 NOT
MEASURED</summary>
`node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 109 commands from the 10 changed
paths at `0d70b581`; each ran with its exit code captured before any
pipe, and `--ran` reconciles: 109 derived, 108 run, 1 NOT-MEASURED, 0
UNRUN.
- Red on the first pass and fixed: `node
scripts/check-adr-0087-registration.mjs --base origin/main` (both
changesets carried a FROM → TO label beside `no-migration-prescription`;
reworded, exit 0 on `3eb877a57`).
- Exit 3 on the first pass, re-run green after their prerequisite:
`check-plugin-teardown-shape --self-test` (shallow clone, after fetching
its fixture commit), `spec check:skill-examples` (after
`@objectstack/client` / `client-react` were built), and `pnpm --dir ui
exec vitest run scripts/` (170 files / 4985 tests passed once
`@objectstack/client` was built).
- NOT MEASURED: `pnpm check:dual-build-cjs-loads` (exit 3, needs a
whole-workspace build).
- Re-run on `3eb877a57`: `check-adr-0087-registration`,
`check-changeset-no-major`, `check-empty-changeset`,
`check-closing-keyword-parity`, `check:changeset-gate-self-tests`,
`check:nul-bytes`, `check:issue-citations`, `check:doc-authoring`: all
exit 0.
- `pnpm lint`: exit 0.
</details>
<details><summary>Full dogfood (the Clause-② line carries the narrowing
arm)</summary>
`pnpm --filter @objectstack/dogfood test` under the verify lock at
`0d70b581` (the closure built first): VERDICT command-exit 1, 20m44s.
Test Files 1 failed | 418 passed | 3 skipped (422); Tests 1 failed |
3491 passed | 36 skipped (3528).
The one failure is `test/permission-set-clone-door.dogfood.test.ts` › "a
platform set … copies into its shipping package's pair" (`422
INVALID_METADATA`, `my_member_baseline` grants on `sys_comment`, which
no package registers). It is main's own red: the merge base `797f9257`
fails `Dogfood Regression Gate (2/3)` on `main`, filed as #30255 after
PR #30222, with the revert open as PR #30262. It re-runs red alone (1
failed | 10 passed), and nothing in this diff touches permission sets or
object registration.
</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.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01Utp6CZJd2mrcp8CfPkSEEs)_
---------
Co-authored-by: Claude <noreply@anthropic.com>currency or percent screen field refuses a non-number on resume, and the console renders percent as a numeric input (#30269)1 parent 5db77f7 commit 4be9734
10 files changed
Lines changed: 181 additions & 36 deletions
File tree
- .changeset
- content/docs
- automation
- references/automation
- packages
- services/service-automation/src
- spec/src
- automation
- contracts
- ui/packages/app-shell/src/views
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
800 | 800 | | |
801 | 801 | | |
802 | 802 | | |
803 | | - | |
804 | | - | |
805 | | - | |
806 | | - | |
807 | | - | |
| 803 | + | |
| 804 | + | |
| 805 | + | |
| 806 | + | |
| 807 | + | |
| 808 | + | |
808 | 809 | | |
809 | 810 | | |
810 | 811 | | |
| |||
835 | 836 | | |
836 | 837 | | |
837 | 838 | | |
838 | | - | |
| 839 | + | |
| 840 | + | |
839 | 841 | | |
840 | 842 | | |
841 | 843 | | |
| |||
1353 | 1355 | | |
1354 | 1356 | | |
1355 | 1357 | | |
1356 | | - | |
1357 | | - | |
| 1358 | + | |
| 1359 | + | |
| 1360 | + | |
1358 | 1361 | | |
1359 | 1362 | | |
1360 | 1363 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
289 | 289 | | |
290 | 290 | | |
291 | 291 | | |
292 | | - | |
| 292 | + | |
293 | 293 | | |
294 | 294 | | |
295 | 295 | | |
| |||
311 | 311 | | |
312 | 312 | | |
313 | 313 | | |
314 | | - | |
| 314 | + | |
315 | 315 | | |
316 | 316 | | |
317 | 317 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
89 | 89 | | |
90 | 90 | | |
91 | 91 | | |
92 | | - | |
93 | | - | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
94 | 95 | | |
95 | 96 | | |
96 | 97 | | |
| |||
126 | 127 | | |
127 | 128 | | |
128 | 129 | | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
59 | 59 | | |
60 | 60 | | |
61 | 61 | | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
62 | 67 | | |
63 | 68 | | |
64 | 69 | | |
| |||
67 | 72 | | |
68 | 73 | | |
69 | 74 | | |
70 | | - | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
71 | 82 | | |
72 | 83 | | |
73 | 84 | | |
| |||
111 | 122 | | |
112 | 123 | | |
113 | 124 | | |
114 | | - | |
115 | | - | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
116 | 128 | | |
117 | 129 | | |
118 | 130 | | |
| |||
130 | 142 | | |
131 | 143 | | |
132 | 144 | | |
133 | | - | |
134 | | - | |
135 | | - | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
136 | 151 | | |
137 | 152 | | |
138 | 153 | | |
| |||
143 | 158 | | |
144 | 159 | | |
145 | 160 | | |
146 | | - | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
147 | 165 | | |
148 | | - | |
| 166 | + | |
149 | 167 | | |
150 | 168 | | |
151 | 169 | | |
| |||
167 | 185 | | |
168 | 186 | | |
169 | 187 | | |
170 | | - | |
| 188 | + | |
| 189 | + | |
171 | 190 | | |
172 | 191 | | |
173 | 192 | | |
| |||
206 | 225 | | |
207 | 226 | | |
208 | 227 | | |
209 | | - | |
210 | | - | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
211 | 231 | | |
212 | 232 | | |
213 | 233 | | |
| |||
0 commit comments