Skip to content

fix(automation)!: a currency or percent screen field refuses a non-number on resume, and the console renders percent as a numeric input - #30269

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-xq1cua
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-xq1cua

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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 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 (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 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.

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), 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.
Derived gates: 109 commands, 108 green, 1 NOT MEASURED

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.
Full dogfood (the Clause-② line carries the narrowing arm)

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.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

…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
…ata FROM/TO block

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Utp6CZJd2mrcp8CfPkSEEs
@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/service-automation, @objectstack/spec, touching 5 documentable anchor(s).

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

  • content/docs/automation/flows.mdx (via validateScreenInputs (symbol, a top-level function))
What this run could not see
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 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; 103 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 — 170 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 31260ca8e446fa4fbe94ff8fd2a9030ddfb53a9a → packageMentionDocs.

Which tree this was computed on

This run read content/docs from e8beaba8ef7c3cce37cd3732fdfb84523b30726c — the merge of head 3eb877a57f96570eeaba79e7635a3f53056cdb49 into base 31260ca8e446fa4fbe94ff8fd2a9030ddfb53a9a, 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 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

⚠️ 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 31260ca8e446fa4fbe94ff8fd2a9030ddfb53a9a → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF

Contract-review-claim: session_01Utp6CZMkHvuDeVYgTD73qF · head 3eb877a · 2026-11-05T04:24Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 3eb877a57f96570eeaba79e7635a3f53056cdb49
Local-runs: none
Scope: packages/spec/src/automation/builtin-node-config.zod.ts, packages/spec/src/contracts/automation-service.ts + packages/services/service-automation/src/screen-input-contract.ts, packages/services/service-automation/src/screen-input-contract.test.ts, ui/packages/app-shell/src/views/ScreenView.tsx, ui/packages/app-shell/src/views/ScreenView.boundHelpReference-9248.test.tsx (the narrowing-face floor: every non-prose path of the diff), and read in the cited code: packages/services/service-automation/src/engine.ts (the resume door, validateScreenInputs call and the INVALID_SCREEN_INPUT envelope), packages/services/service-automation/src/builtin/screen-nodes.ts (how type / min / max / defaultValue go on the wire), packages/runtime/src/domains/automation.ts (INVALID_SCREEN_INPUT → 400), ui/packages/app-shell/src/views/FlowRunner.tsx (initialScreenValues, screenFieldBoundViolations, screenResumeValues on submit), packages/spec/src/data/aggregation-policy.ts (MEASURE_FIELD_TYPES), content/docs/references/automation/builtin-node-config.mdx (the regenerated mirror of the .describe). Remainder at TIER_DEFAULT (the lane seat's ACCEPT): the two changesets, content/docs/automation/flows.mdx.

Inputs: card #30178 body and all six comments (triage 6641856071, claim 6647305770, needs_decision report 6647359070, seat note 6647378538, ruling 6648655128 letter A, done report 6649598953); PR #30269 body, file list and the net diff against main at the head (merge base 797f9257); the check-runs on the head. Ruling-ref on the PR body: 6648655128 ✓.

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 18.x upgrade harness, Packed-tarball smoke, Temporal Conformance, UI Published Artifacts, Dogfood Verify CLI, the unexpanded dogfood matrix placeholder, and the schedule gate). Green by name: Type Check UI (the Console compiled against this PR's spec and the percent line — the reading the dev left to CI), Type Check · workspace (the spec artifact gates, so the regenerated builtin-node-config.mdx is current), Type Check · consumer gates, Type Check · source gates, Build Core, Test Core (1–8/8), Test UI (1–16/16), Lint UI, Lint · ESLint, Lint · CI, release & engine gates (the changeset gates, check-adr-0087-registration and check-changeset-no-major among them), Lint · docs & code guards, Check Changeset, Governed Surface Queue Guard, Build & E2E UI, Live E2E UI, Dogfood Regression Gate, Check Documentation Links, Spec property liveness, PR-body guards.

① Derived judgments

  1. Resume-door accept set narrows — a present value under a type: 'currency' or type: 'percent' screen field that is not a finite JSON number is refused invalid_type inside INVALID_SCREEN_INPUT (400, retryable, suspension kept: the door returns the envelope before anything is consumed, engine.ts resume path), never coerced. Right — ruling A item 1, the same isFiniteNumber predicate and the same must be a number sentence number carries; "75", "60" and "abc" all land on the shape, a JSON number still reaches the bound pass.
  2. Not widened — rating, slider, progress, text and untyped fields still admit a string (pinned). Right — ruling A item 1 ⛔.
  3. Console — percent takes the numeric input path with number / currency: native min / max, the existing Number(e.target.value) posts a number, screenFieldBoundViolations (present finite number only) now sees it. Right — ruling A item 3, one line, one pin, no dedicated percent widget. ui/ surface is exactly ScreenView.tsx and its existing bound harness; no other ui/** path moves.
  4. Error-code surface — no new code: invalid_type is already a FieldErrorCode member and already the code number uses. Right (ADR-0114 D2).
  5. Published spec — ScreenFieldConfigSchema.type stays z.string().optional(), .min / .max stay z.number().optional(); ScreenFieldSpec keeps every member and type. Only TSDoc and the type .describe() change, so check:docs is the one owed artifact gate and content/docs/references/automation/builtin-node-config.mdx is regenerated (both tables that carry the field row). No export moves, so gen:api-surface is not owed. A grep of the whole tree at the head for the previous describe sentence finds no stale copy. Right.
  6. Headless path (implied, not named in the PR body) — judgeHeadlessScreen shares validateScreenInputs, so a caller-supplied string under a currency / percent field now yields invalid_type → satisfied: false → the screen pauses instead of being skipped on a value the bound never judged. Failure direction is the module's standing one (a pause, never a skip). Right, and the same behaviour number has had since A′.
  7. Metadata protocol unchanged — what an author may write (type: 'percent', min, max, defaultValue) is identical before and after; the narrowing is on a submitted value at the resume door. Metadata-protocol: none is the right declaration.

② Semver level

  • 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. By AGENTS.md Post-Task Checklist step 3 and the launch-window guard's own rule (check-changeset-no-major.mjs: "the level follows the metadata protocol alone — minor when the protocol an author writes against is added to, narrowed or changed in meaning, patch for everything else"), Metadata-protocol: none ⇒ patch. Matches. The ruling's aside that A′ landed minor does not bind here: that landing declared protocol kinds, this one declares none.
  • Clause-②: yes (narrowing) as a line-initial declaration on the PR body and in both changeset bodies, with the BREAKING banner in both and exactly one ADR-0087 marker each, not-required (no-migration-prescription). The exemption holds: no key, export or stored row moves, nothing for objectstack migrate meta to rewrite, and neither body carries a FROM → TO prescription (the earlier draft's block was removed; the caller's fix is a plain bullet). Matches.
  • @object-ui/app-shell is private: true but a member of the fixed group with privatePackages.version: true, so bumping it in the changeset is correct, and riding the service-automation changeset is fine (one changeset, two packages, one behaviour).
  • Title fix(automation)!: carries the !; Fixes #30178 on the body matches the changesets' (#30178).

③ Boundary flags

  • open_questions: [] on the done report (6649598953) — nothing to escalate. The needs_decision fork (6647359070) was answered by the maintainer's ruling A (6648655128), and the diff implements that letter and no other: no coercion (B) and no engine-untouched arm (C).
  • Deviation 1 (console pin in the existing boundHelpReference-9248 harness as a top-level describe): accepted — the shared-harness rule.
  • Deviation 2 (@object-ui/app-shell rides the service-automation changeset): accepted — see ②.
  • Deviation 3 (flows.mdx changes three sentences, the ruling named one): accepted — the other two would have stated the number-only rule after this PR; same file the ruling opened; prose face.
  • Deviation 4 (assignee / reviewer step skipped): seat process, no contract bearing.
  • NOT MEASURED locally: @object-ui/app-shell type-check (unbuilt siblings) and check:dual-build-cjs-loads (whole-workspace build) — both answered by the head's check-runs (Type Check UI, Build Core / Test Core), read above.
  • Dogfood: the one red (permission-set-clone-door) is attributed to main's own red; verified: main red: Dogfood Regression Gate (2/3) fails permission-set-clone-door after PR #30222 (member_default grants sys_comment) #30255 is open and names that test after PR fix(plugin-security): every member reads and posts comments, and reads the activity history, of the records they can read #30222, revert PR revert: PR #30222 (main red #30255) #30262 is open, and nothing in this diff touches permission sets or object registration. Accepted.
  • Skills: skills/objectstack-automation/SKILL.md's screen row names type as an open hint and states no numeric rule, so nothing there turns false. Accepted.

④ Design judgment

packages/spec/src/automation/builtin-node-config.zod.ts — judged against ruling A and its first consumer, validateScreenInputs at the head. The type TSDoc's claim ("four members are value domains … the three numeric kinds take a finite JSON number, a numeric string refused and ⛔ never coerced; and 'file'") is exactly the SCREEN_FIELD_VALUE_DOMAINS table: number / currency / percent on isFiniteNumber, file on isFileIdToken, exact-match on field.type. The min / max TSDoc's claim ("the shape refusal above reads the three numeric kinds alone, not the presence of a bound") is the code: the domain lookup keys off type, the bound pass is unconditioned on it. The .describe() sentence ("number, currency and percent take a finite number on resume (a numeric string is refused)") is true and is mirrored in the regenerated reference page. Shape untouched. Tried to break it: (a) case — the schema's type is a free string; the engine matches 'percent' exactly while the console lowercases, so a 'Percent' field renders numeric, posts a number, and the bound pass still judges it — no loss; a REST string under 'Percent' passes unjudged exactly as it does under 'Number' since A′, a pre-existing asymmetry this PR neither widens nor narrows and which the ruling did not open. (b) The widget hints rating / slider / progress — excluded and pinned, as the ruling requires. Holds. No finding.

packages/spec/src/contracts/automation-service.ts — ScreenFieldSpec.type now claims "the client renders all three as a numeric input, which posts a number": true at the head (ScreenView.tsx htmlType line and the Number(...) on the numeric input), and the client bound check compares a present finite number, so the claim and the gate are one change in one PR, which is the trap the ruling named (engine first, console later) closed. The min / max TSDoc ("a present non-number is refused outright (invalid_type) … ⛔ Not coerced") matches the pass order: shape before bound, absent and hidden left alone. Tried to break it: (a) a defaultValue served as a string on a numeric field — screen-nodes.ts resolves the slot as written and initialScreenValues seeds it as served, so an untouched string default would post a string and be refused (400, retryable). This is the author's off-spec declaration, identical to what number / currency have done since A′, and the spec text now says the field takes a number; reading the string as a number in the console would be the consumer leniency PD-12 forbids. Not a defect of this PR. (b) a whitespace-only string — isPresent is false, so it is required's question, same as number. Holds. No finding.

Non-prose floor — screen-input-contract.ts: two rows plus a named predicate, the docstring's "one member / two members" wording becomes the three numeric kinds plus file; no new pass, no new code path. Its test pins every reading the ruling's item 2 lists ("75", "60", "abc" → invalid_type with the field named and must be a number; 75 → max_value; -1 → min_value; 0 / 60 → []; absent and '' → []; hidden / unevaluable not judged with the visible control; rating / slider / progress / text / untyped admit a string), for both kinds. ScreenView.tsx: one condition, one comment; its pin reads type="number", min="0", max="60", onValueChange('discount', 75) with a number, and the bound check flagging it. Holds.

Observation (out of scope, not a finding on this PR): judgeHeadlessScreen's inline comment "every issue returned here is a missing required" has been loose since A′ added invalid_type to the shared check; this PR widens that by two kinds but the verdict direction (pause) is unchanged.

Must-fix: none. Should-fix: none.

Implemented-by: claude/project-thread-xq1cua
Reviewed-by: session_01Utp6CZMkHvuDeVYgTD73qF

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-bill · session_01Utp6CZMkHvuDeVYgTD73qF

席位意见:ACCEPT。按裁决 A,currency / percent 屏幕字段与 number 用同一个数值判定,不强转;控制台把 percent 渲染成数字输入。契约复核 PASS 在案。

席位核对

Head-sha: 3eb877a57f96570eeaba79e7635a3f53056cdb49 · read 2026-11-05T04:50Z

Item Reading
Shape Draft → main, author objectstack-fleet[bot] (relay pr_create); line 1 attribution, Fixes #30178, Ruling-ref: 6648655128, Clause-② / Metadata-protocol at line start. 10 files, +181/−36.
Ruling 6648655128, letter A (maintainer 「同意」 2026-11-05T03:07Z): percent and currency join the A′ numeric domain, plus the one-line ScreenView.tsx input type. The ui file is opened by that ruling only.
Gates (check-runs on this head) 58 success, 7 skipped, 0 failure, 0 pending (Type Check UI, Check Changeset, Governed Surface Queue Guard, Test Core 1–8, Test UI 1–16 included). Dev's local --ran: 109 derived, 108 exit 0, 1 NOT MEASURED (check:dual-build-cjs-loads); two ablations red then restored; dogfood 418/419, the one red is main's own #30255 (revert PR #30262).
Contract review CONTRACT_REVIEW_TIER record 6649966548 on this head: PASS, no must-fix, no should-fix. It covers the two spec hits files and, under the narrowing face, the resume door and ScreenView.tsx.
Prose read by the seat flows.mdx: the shape sentence now names number, currency and percent, says a string is refused and not coerced, and that the console renders all three as a numeric input; the bound sentence reads "the three numeric kinds alone (not rating, slider or progress)"; the error-envelope sentence lists the numeric kinds and file. Each matches screen-input-contract.ts at this head. The two changesets say the same and carry the BREAKING banner and one adr-0087: not-required (no-migration-prescription) marker each.
Semver Both changesets patch with !; the review's ② judged that right under Metadata-protocol: none (AGENTS.md, check-changeset-no-major.mjs). The dev's question is answered: no level change.
Landing Not governed (check-governed-merges --pr 30269: 0 of 10 paths), 217 lines ≤ 3000. Seat lands: pr_ready + automerge_enable.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 5, 2026 04:51
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 5, 2026 04:51
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 5, 2026
Merged via the queue into main with commit 4be9734 Nov 5, 2026
66 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-xq1cua branch November 5, 2026 05:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant