Repository navigation
fix(automation)!: a currency or percent screen field refuses a non-number on resume, and the console renders percent as a numeric input - #30269
Conversation
…een field, as on `number` The screen-input value domain held `number` and `file` only, so a `percent` or `currency` screen field took any string on resume: `"75"` under a `max` of 60, `"abc"` too, and the run consumed its suspension on a value the bound never judged. Both kinds now share `number`'s predicate (a finite JSON number, refused with `invalid_type`, never coerced). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CZJd2mrcp8CfPkSEEs
… name the three numeric kinds in the screen-field contract text Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CZJd2mrcp8CfPkSEEs
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CZJd2mrcp8CfPkSEEs
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CZJd2mrcp8CfPkSEEs
…ata FROM/TO block Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Utp6CZJd2mrcp8CfPkSEEs
📓 Docs Drift CheckThis PR changes 2 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 170 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin e8beaba8ef7c3cce37cd3732fdfb84523b30726c && git checkout e8beaba8ef7c3cce37cd3732fdfb84523b30726c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 31260ca8e446fa4fbe94ff8fd2a9030ddfb53a9a 3eb877a57f96570eeaba79e7635a3f53056cdb49 && git checkout -B drift-repro 31260ca8e446fa4fbe94ff8fd2a9030ddfb53a9a && git merge --no-ff 3eb877a57f96570eeaba79e7635a3f53056cdb49
node scripts/docs-audit/affected-docs.mjs --json 31260ca8e446fa4fbe94ff8fd2a9030ddfb53a9a
|
|
domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF Contract-review-claim: session_01Utp6CZMkHvuDeVYgTD73qF · head 3eb877a · 2026-11-05T04:24Z |
|
domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF Contract reviewServed-tier: Inputs: card #30178 body and all six comments (triage Gate verdicts (the head's check-runs, read at 2026-11-05T04:48Z): 65 check-runs, 58 success, 0 failure, 7 skipped (the opt-in harnesses ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Non-prose floor — Observation (out of scope, not a finding on this PR): Must-fix: none. Should-fix: none. Implemented-by: VERDICT: PASS |
|
domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF 席位意见:ACCEPT。按裁决 A, 席位核对Head-sha:
|
Requested by Bill · project thread
domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF
Fixes #30178
Ruling-ref: 6648655128
Clause-②: yes (narrowing) — on ruling 6648655128 (letter A): a
percentorcurrencyscreen field now refuses any non-number value withinvalid_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 (theobjectstack-automationscreen row namestypean open widget hint and states nonumberrule either), so nothing there turns false;content/docs/automation/flows.mdxis updated here.维护者速读
currency、percent并入number的数值值域:恢复时值不是有限 JSON 数字就拒(invalid_type,400INVALID_SCREEN_INPUT,挂起保留),不把数字字符串当数字;控制台把percent渲染成数字输入,带上下限、提交数字;spec 契约文字与 flows 文档同步写成三种数值类型。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_DOMAINSgainscurrencyandpercent, onnumber's own predicate (a finite JSON number) and sentence (must be a number). A present value that is not one is refused withinvalid_typeinsideINVALID_SCREEN_INPUT(400, retryable, the suspension kept), ⛔ never coerced:"75"under amaxof 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,textand 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 oneui/file the ruling opens.ScreenView.tsx:percenttakes the numeric input path withnumberandcurrency, so it carriesmin/maxas native attributes, the browser stops an over-limit value before any request, and the runner receives a number (the existingNumber(...)on the numeric input), which the client bound checkscreenFieldBoundViolationsalready compares. No dedicated percent widget (ruling: the ui lane's later choice).Spec prose (
@objectstack/spec), no shape change.ScreenFieldSpec.type/.minjsdoc (contracts/automation-service.ts),ScreenFieldConfigSchema.type/.min/.maxjsdoc and thetype.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.mdxis regenerated from the describe (gen:schema && gen:docs). Every key and type is unchanged.Docs.
content/docs/automation/flows.mdx: the three sentences that namedtype: '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) and30178-screen-field-numeric-kinds-contract-text.md(@objectstack/spec): bothpatch(Metadata-protocol: nonetakespatch, AGENTS.md Post-Task Checklist step 3), both with the BREAKING banner, both declaration lines, and the ADR-0087 dispositionnot-required (no-migration-prescription): no schema key, export or stored row moves, so there is nothing forobjectstack migrate metato rewrite.Tests
screen-input-contract.test.ts, new top-leveldescribe(A percent field on a flow screen ignores its min and max: the console renders it as text and posts a string, and both bound checks compare numbers only #30178), for each ofpercentandcurrency:"75","60","abc"→invalid_type(field named, messagemust be a number);75→max_value,-1→min_value,0and60→[]; absent and''→[]; a hidden field (visibleWhenfalse or unevaluable) is not judged, with the visible control; andrating/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-leveldescribe: apercentfield renderstype="number"carryingmin="0"/max="60", typing75callsonValueChange('discount', 75)with a number, andscreenFieldBoundViolationsflags it.scripts/ablation-replace.mjs, restore proven by blob hash == HEAD and an emptygit diff HEAD):currencyandpercentrows →screen-input-contract.test.ts4 failed | 13 passed (both "refuses a string" cases and both hidden-field controls); restored.|| kind === 'percent'fromScreenView.tsx→ the new console pin fails (1 failed | 14 passed); restored.Both subjects are imported by relative path from
src/, so nodist/leg applies.Acceptance notes
currencyorpercentscreen field is now refused 400INVALID_SCREEN_INPUT(invalid_type); it posts the JSON number. In-tree example flows declare nocurrency/percentscreen field (git grep); the known declaration is hotcrm's Generate Quotediscount, which re-measures on this pin (card body), nothing in that repository touched here.check-adr-0087-registrationreads that label as a migration prescription and refusesno-migration-prescriptionbeside it. The caller's fix is now a plain bullet in the body: nothing at rest moves, so the ledger has nothing to carry.patchon all three packages, by the AGENTS.md rule (Metadata-protocol: none⇒patch, BREAKING banner and disposition kept). The ruling noted A′'s landing tookminor; that landing declaredMetadata-protocolkinds this one does not. The seat can re-level if the gates say otherwise.ui/scope. ExactlyScreenView.tsxand its existing bound test harness; every otherui/**path untouched.Verification
Head
3eb877a57(claude/project-thread-xq1cua),origin/mainmerged once at797f9257(merge0d70b581); the last commit changes the two changeset bodies only, and the changeset gates were re-run on it.Package tests and type-checks
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), thencheck:generated: two artifacts stale,content/docs/references/**(regenerated,gen:schema && gen:docs, committed) andcheck: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.pnpm --filter @object-ui/app-shell type-check(exit 2, every errorTS2307 Cannot find module '@object-ui/…': theui/sibling packages are not built in this worktree).Type Check UIon this PR is the reading.Derived gates: 109 commands, 108 green, 1 NOT MEASURED
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 109 commands from the 10 changed paths at0d70b581; each ran with its exit code captured before any pipe, and--ranreconciles: 109 derived, 108 run, 1 NOT-MEASURED, 0 UNRUN.node scripts/check-adr-0087-registration.mjs --base origin/main(both changesets carried a FROM → TO label besideno-migration-prescription; reworded, exit 0 on3eb877a57).check-plugin-teardown-shape --self-test(shallow clone, after fetching its fixture commit),spec check:skill-examples(after@objectstack/client/client-reactwere built), andpnpm --dir ui exec vitest run scripts/(170 files / 4985 tests passed once@objectstack/clientwas built).pnpm check:dual-build-cjs-loads(exit 3, needs a whole-workspace build).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.Full dogfood (the Clause-② line carries the narrowing arm)
pnpm --filter @objectstack/dogfood testunder the verify lock at0d70b581(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_baselinegrants onsys_comment, which no package registers). It is main's own red: the merge base797f9257failsDogfood Regression Gate (2/3)onmain, 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.Human Minimum
packages/specholds no runtime logic.pnpm lintpasses.main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code