Repository navigation
spec: a flow screen field cannot express a numeric bound, help text, or a lookup target — three intents that degrade into prose in the reference app #17306
Description
Activity
- addedpriority:p2Medium: important, M3Medium: important, M3and removed
on Sep 10, 2026 Triage: routed to the decision box — it asks to widen a
.strictpublished schema, which is 功能新增 + Clause-②.domain:spec,needs-user-decision,priority:p2.ScreenFieldConfigSchema(@objectstack/spec/automation) is.strictand its entire key set is:name · label · type · required · options · defaultValue · placeholder · visibleWhen⇒ three ordinary authoring intents — a numeric bound, help text, and a lookup target — have no expression at all, and both degrade in a way the author discovers only by running the flow or reading a rejection.
维护者速读
改了什么 — 什么都还没改。这是问「流程里的输入框,要不要支持三件很常见的事」。
为什么改 — 作者在流程里放一个输入框时,今天没办法表达三件事:① 数字的上下限(比如「1 到 100」);② 一句帮助说明;③ 这个框要从哪个对象里选(查找目标)。
⚠️ 结果是参考应用只能把这些写成散文塞在别的地方 —— 也就是说,我们自己的样板应用在用变通手段绕过我们自己的契约。而且作者不会提前知道:要么跑一遍流程才发现,要么被拒收才发现。风险与代价(含回滚) — A(补这三个键):作者能表达常见意图,参考应用不用再绕;代价是公开契约变宽,三个键从此是永久义务,而且渲染端要真的兑现它们(声明了不兑现比不声明更糟)。B(不补):零成本,但每个应用继续各自绕,绕法各不相同。C(只补最有拉动的一个,比如帮助文本):折中,代价是下次又来一轮。回滚:A 之后删键很贵(已发布能力);B/C 便宜。
席位意见 — 荐 A,但有一个硬条件:三个键必须连同渲染一起交付。理由:这是本轮少数有实测拉动的卡 —— 参考应用已经在付代价了,不是猜测的需求。按分歧推荐序,有实测拉动时应当一次付清长远形态。
⚠️ 但若你判断渲染端一次做不完三个,那 C 比 A 好 —— ⛔ 最不能接受的是声明了三个键而渲染只兑现一个。你要做的(一个动作) — 选一个字母:A(补三个键,连渲染一起)/ B(不补)/ C(只补一个,说明是哪个)。
os-decision-facets
- ① 项目长远合理性:A 收窄「作者意图 vs 可表达面」的缺口,消除参考应用里的变通;
⚠️ 但它扩大公开契约,三个键是永久义务。B 让每个应用各自发明绕法,长期是最贵的一种分散。⇒ 长远偏 A,但不是无代价的偏。 - ② 实际业务拉动:⭐ 有实测拉动 —— 参考应用当前正把这三个意图写成散文。⛔ 不是「读起来像有用」,是已经在付代价。
⚠️ 但只量到一个应用;是否普遍未知(见置信缺口)。 - ③ 防 AI 犯错:⭐ 强烈支持 A,且规定了 A 的形状。今天 AI 想写数字上下限,只能塞进
placeholder或干脆不写 —— 前者静默无效,后者丢失意图。补上闭合的键让它「写得对」在结构上更容易。⚠️ 但同一轴也给出红线:声明即强制 —— 若补了键而渲染不兑现,就制造了本轮反复出现的那类陷阱(Dashboard widgets:options.stageOrderis declared for every chart type, documented for a chart type that does not exist, and silently dropped in every non-enlocale #17344、List viewnavigation.viewis declared in spec but resolves no form view — its only read lands it in theonNavigatenavigation-MODE argument #16885、objectui#8958)。⇒ 这就是「必须连渲染一起交付」这个条件的来源。 - ④ 创业阶段不扩散:
⚠️ 这一轴反对 A,如实呈上:三个新键 = 三份永久义务,而创业阶段对公开面扩张默认从紧。C 是它的自然答案。⇒ 四轴在此冲突;推荐按长远权重与②的实测拉动压过它,但这是权衡不是共识。
推荐:A(三个键连同渲染一次付清)。回退项:C(只补拉动最硬的一个)。⛔ 不荐 B —— 参考应用已经在绕,B 等于把成本永久外包给每个应用。
⚠️ 置信缺口 —— 本分析看不见什么:除了参考应用之外,还有多少应用在绕这三件事,以及三者之中哪一个最常被绕。②的拉动只量到一个应用;若只有它需要,C 甚至 B 都可能更合适。⇒ 这个数在你对下游应用的了解里,本席读不到。分诊席位 ·
session_017VGfRocA8VjczSe84fgjY3· R+166 · 2026-09-10T15:02Z · 本评论来自分诊座位
Generated by Claude Code
- ① 项目长远合理性:A 收窄「作者意图 vs 可表达面」的缺口,消除参考应用里的变通;
Ruling recorded — A: the flow screen field gains a numeric bound, help text and a lookup target, spelled with the object field schema's own key names, and shipped together with their rendering (director seat, decision batch #120 item 2, 2026-09-12)
Maintainer, verbatim (live PM chat, 2026-09-12T04:2xZ), to decision batch #120 presented as
1A·2A·3D·4B·5C′: 「17508 A AI负责翻译就行。其他同意」.Derived first from the long-term axis: one platform, one field vocabulary — a flow screen field expresses bounds, help and a lookup target with the same spellings the object field uses, so authors (and AI) learn one set of names; a screen-local
max/helpTextwould be a second contract. B leaves every app inventing its own workaround (hotcrm already does); C is the fallback if rendering cannot deliver all three at once.What is ruled
ScreenFieldConfigSchema(packages/spec/src/automation/builtin-node-config.zod.ts:380) gains three keys whose names are DERIVED fromFieldSchema's corresponding keys (the bound pair, the help/description key, the lookup-target key) — the spec seat readspackages/spec/src/data/field.zod.tsand uses those spellings; ⛔ no new spelling.- Hard condition: the keys land only together with their rendering — objectui card objectui#9248 (
pm:blockedon this one) renders all three in the console's flow screen dialog, and this card's acceptance includes that PR. A declared key the dialog ignores is the trap this round keeps finding (Dashboard widgets:options.stageOrderis declared for every chart type, documented for a chart type that does not exist, and silently dropped in every non-enlocale #17344, List viewnavigation.viewis declared in spec but resolves no form view — its only read lands it in theonNavigatenavigation-MODE argument #16885, objectui#8958). Clause-②: yes(public contract widens by three authorable keys), contract-review carrier; docsautomation/flowsscreen-field section updated with the three keys and their refusals.- hotcrm's two prose workarounds (
quote_generation's discount ceiling, the lookup screen field) are the acceptance fixtures — relayed to the hotcrm seat when this lands.
State
needs-user-decision→pm:queue;domain:spec/priority:p2kept.
Generated by Claude Code
Claim: session_01MkQhmuuJAVDjmeWNixwDDH · branch claude/issue-17306-screen-field-bound-help-lookup
Branch: claude/issue-17306-screen-field-bound-help-lookup
Clause-②: yes — 裁决自己写明:「public contract widens by three authorable keys」。⇒ 至档复核先行,needs:contract-review自 draft PR 开出之日起随卡随 PR。⛔ 落地前不翻牌。domain:spec执行席,PM 派发,2026-09-13T00:15Z。assignee 与pm:queue→pm:dispatched同一步写入并回读对 diff。dev 轮次继承两者,⛔ 不另发第二条认领。派发前锁读数:lock is free/queue: empty⇒ 到达深度 1,可派。裁决已定,逐条照办(总监席决策批次 #120 第 2 项,维护者原话「17508 A AI负责翻译就行。其他同意」):
- 三个键的名字从
FieldSchema派生 —— 读packages/spec/src/data/field.zod.ts,用它已有的拼法(bound pair / help-description / lookup-target),⛔ 不得发明新拼法。裁决的理由是「one platform, one field vocabulary」。 ⚠️ 硬条件:键只与其渲染一同落地 —— objectui#9248 在 console 的 flow screen 对话框里渲染这三个键,本卡的验收包含那个 PR。裁决点名了这条的理由:一个对话框会忽略的已声明键,正是本轮反复逮到的那个陷阱(Dashboard widgets:options.stageOrderis declared for every chart type, documented for a chart type that does not exist, and silently dropped in every non-enlocale #17344、List viewnavigation.viewis declared in spec but resolves no form view — its only read lands it in theonNavigatenavigation-MODE argument #16885、objectui#8958)。Clause-②: yes,契约复核载体;automation/flows的 screen-field 文档段要写上这三个键与它们的拒收。- hotcrm 的两处散文变通(
quote_generation的折扣上限、那个 lookup screen field)是验收夹具,落地后转交 hotcrm 席。
本席亲取的排序读数(2026-09-13T00:14Z):objectui#9248 是 open、
pm:blocked,正文带Blocked-by: objectstack-ai/objectstack#17306⇒ spec 先行是被设计好的顺序,下游在本卡关闭时解锁。⇒ 硬条件管的是验收而不是本 PR 的合并顺序:spec PR 可以先落,但本卡在 objectui#9248 的 PR 落地前 ⛔ 不关闭,由本席在落地后把它转pm:blocked+Blocked-by: objectstack-ai/objectui#9248。⚠️ 文件面申报:packages/spec/src/automation/builtin-node-config.zod.ts(ScreenFieldConfigSchema在:380附近,⭐ 按符号重新定位,⛔ 不用这个行号)+ 其测试 +content/docs的 flows 段 + changeset。同批另两张(#17319 / #17053)与本卡文件面不相交;⚠️ 若三者都新增 ADR-0087 迁移条目,生成物packages/spec/src/migrations/registry.ts会冲突 —— 用gen:migration-registry重生成,⛔ 永不手工合并。
Generated by Claude Code
- 三个键的名字从
os-dev-report
{ "issue": 17306, "status": "done", "branch": "claude/issue-17306-screen-field-bound-help-lookup", "pr": "https://github.com/objectstack-ai/objectstack/pull/17913", "premise_still_valid": true, "summary": "ScreenFieldConfigSchema (re-located by symbol at builtin-node-config.zod.ts:380 — the ruling's line number still holds) gains four keys for the ruling's three intents, named from FieldSchema: min/max, inlineHelpText, reference. Delivered with the rendering half that lives in THIS repo — the executor forwards all four onto ScreenFieldSpec and the Studio designer form offers all four as repeater columns, which builtin-node-form-zod-ledger.test.ts reconciles against the Zod in both directions. The bound is additionally enforced server-side on resume (validateScreenInputs, min_value/max_value — both already in the ADR-0114 D2 catalog, no new error code), because a screen field's declared contract is the only contract behind it; that half is mine, not the ruling's, and is flagged for the maintainer. reference is deliberately OPTIONAL on type:'lookup' (unlike FieldSchema, where it is required) because shipped flows declare a bare lookup screen field and refusing them would break metadata that parses today. PR uses 'Part of #17306', not a closing keyword: the card stays open until objectui#9248 lands. needs:contract-review hung on BOTH carriers. ⛔ Not flipped ready, not enqueued, no auto-merge.", "adopted_spellings": { "bound_pair": { "adopted": "min / max", "derived_from": "FieldSchema.min / FieldSchema.max (packages/spec/src/data/field.zod.ts:1121-1122)", "ambiguous": false, "why": "FieldSchema declares exactly these two names for a numeric bound; there is no second candidate. Unconditioned on type, exactly as FieldSchema leaves them." }, "help": { "adopted": "inlineHelpText", "derived_from": "FieldSchema.inlineHelpText (field.zod.ts:1736)", "ambiguous": true, "both_readings": "FieldSchema declares TWO help-shaped keys: `description` (:1001, described 'Tooltip/Help text') and `inlineHelpText` (:1736, described 'Help text displayed below the field in forms').", "test_applied": "The ruling's own 'the same spellings the object field uses' test, resolved by asking what FieldSchema itself answers an author reaching for this intent by its natural name. FieldSchema's alias table (:921) reads help/helpText/hint/tooltip -> inlineHelpText — so the object field routes all four natural spellings, INCLUDING the `helpText` this card asked for, onto inlineHelpText. `description` is never an alias target for them; it is a separate declared key for secondary/tooltip copy. Therefore inlineHelpText is the spelling the object field uses for THIS intent, and a screen-local `helpText` would have been the second contract the ruling forbids." }, "lookup_target": { "adopted": "reference", "derived_from": "FieldSchema.reference (field.zod.ts:1213)", "ambiguous": false, "why": "Canonical; FieldSchema renames relatedTo/referenceTo/target/targetObject/lookupObject onto it (:929). The card's proposed `object` / `reference_to` / `referenceTo` are all non-canonical, so none was adopted as the landing key." }, "declined": "`step` — the card floated it as optional; the ruling says 'the bound pair', so it is NOT declared. Left refused by name." }, "gap_reproduction": "Probe run on the pre-change tree against ScreenFieldConfigSchema: max, min, step, helpText, help, hint, inlineHelpText, description, object, reference, referenceTo, targetObject — ALL TWELVE refused with `unrecognized_keys: Unrecognized key(s) on this screen field: KEY`. LIT CONTROL: placeholder -> __ACCEPTED__, so the probe could have come back the other way and is aimed at this schema's key set. Confirms the card's claim that max and helpText are refused BY NAME.", "both_direction_pins": { "accepted": "all four keys with valid values; both hotcrm acceptance fixtures as declared metadata (quote_generation's discount ceiling as min:0/max:20 + inlineHelpText; close_case's Resolved-by-Article as type:'lookup' + reference:'crm_knowledge_article'); a lookup field with NO reference still parses (the non-breaking guarantee).", "still_refused_unknown_key": "unknownKeyMessage(ScreenFieldConfigSchema, {name, type, sparkles:true}) still names `sparkles` and the surface 'this screen field' — the .strict guard did NOT quietly widen past these four.", "exact_key_set_pin": "Object.keys(shape).sort() pinned to the full twelve: defaultValue, inlineHelpText, label, max, min, name, options, placeholder, reference, required, type, visibleWhen. This is the only assertion that can see a FIFTH key arriving.", "type_refusals": "max:'20' and min:'zero' refused (invalid_type); inlineHelpText:42 and reference:['a'] refused.", "alias_refusals": "help/helpText/hint/tooltip each refused NAMING `inlineHelpText`; object/referenceTo/targetObject/lookupObject/relatedTo/target each refused NAMING `reference`. A further pin holds the two levels apart: `object` -> `objectName` on the screen NODE, `object` -> `reference` on a screen FIELD.", "server_enforcement": "8 pins in a new packages/services/service-automation/src/screen-input-contract.test.ts covering both directions of the bound." }, "ablation": { "target": "packages/services/service-automation/src/screen-input-contract.ts (test imports it relatively, so it resolves to src — no dist leg involved); trap RESTORE-FN EXIT INT TERM with absolute paths; HEAD blob 1cb0df14927c2e07058a17b9f465bcc8488e4b31", "leg_A_main_direction": "Disabled the max check. Landed proof: deleted-text occurrences 1 -> 0, injected-text 0 -> 1, mutated blob 9d34433a != HEAD. RED: 3 failed / 5 passed, including 'refuses a value above `max` with the catalog code that mirrors the key'. Restored: blob back to 1cb0df14, `git diff HEAD` 0 changed paths. GREEN: 8/8.", "leg_B_COST_direction": "Dropped the hidden-field guard from the bound loop, i.e. made the bound OVER-refuse. Landed proof: deleted-text lines 13 -> 10, mutated blob 0136f721 != HEAD. RED: exactly 1 failed / 7 passed — 'does not fire on a field the user was never shown'. Restored by hash, GREEN 8/8. This is the cost direction: it proves the pin guaranteeing the bound does NOT start refusing values the user was never asked for is itself able to fail.", "note": "An earlier ablation attempt returned exit 99 (queue-timeout). Recorded as NOT MEASURED, not as a red; the tree was verified unmutated (git status clean, blob == HEAD) before re-queuing on the same kept slot." }, "spec_test_project_exit_codes": { "pnpm --filter @objectstack/spec test (project local)": "LOCAL_EXIT=0 — 474 files, 13495 tests passed", "pnpm --filter @objectstack/spec test:repo (project repo)": "REPO_EXIT=0 — 31 files, 523 tests passed" }, "tests": "spec local exit 0 (13495 passed); spec repo exit 0 (523 passed); @objectstack/service-automation test exit 0 (133 files, 1572 passed) after updating one enumeration pin. Generated artifacts: `pnpm --filter @objectstack/spec check:generated` — check:authorable-surface GREEN with EXACTLY four additions to authorable-surface/automation.json and nothing else; check:docs proved stale and was regenerated via --fix (builtin-node-config.mdx, +8 lines, additive only); final check:generated run VERDICT command-exit 0. Ablation: rebuild not required (src-resolved), on-disk mutation proven by occurrence count AND git hash-object on both legs, each with a restore proven by hash and by `git diff HEAD` being empty. TWO reds found and fixed mid-round, both by existing pins doing their job: (1) automation-api.zod.test.ts TS2344 — it binds TriggerFlowResponse['data'] EQUAL to the AutomationResult contract interface, so widening ScreenFieldSpec alone reddened it by name; the module-local screenFieldSpecShape now carries the same four keys. (2) config-unknown-keys.test.ts pinned the declared-set enumeration verbatim; updated to the new twelve. NOT MEASURED: a packed-bytes confirmation of @objectstack/service-automation (queue-timeout exit 99) — see changeset_measurement.", "changeset_measurement": "A changeset IS owed and is written (.changeset/17306-screen-field-bound-help-lookup.md, '@objectstack/spec': minor + '@objectstack/service-automation': minor). MEASURED for spec with a real `npm pack --dry-run`: dist/automation/index.d.ts (184.9kB), dist/contracts/index.d.ts and src/automation/builtin-node-config.zod.ts (48.7kB — spec's files[] ships `src/**/*.zod.ts` as source too) are all in the pack list. POSITIVE CONTROL: `inlineHelpText` occurs 2x in the packed dist/automation/index.d.ts. NEGATIVE CONTROL: a string that must not be there occurs 0x — so the grep discriminates. NOT MEASURED for @objectstack/service-automation: its packed bytes were not read, because the build needed to produce its dist returned exit 99 (queue-timeout) twice under sibling contention. Its changeset entry rests on files[]=['dist'] + private:false + the edits being in src/ which tsup compiles into dist — reasoning, not a byte measurement, and reported as such. The spec half alone settles that a changeset is owed.", "docs_refusals": "content/docs/automation/flows.mdx gains a screen-field section 'A field's bound, help text and lookup target' with a key table and a worked example, then states the refusals explicitly. WHAT THEY REFUSE: a bound that is not a number (max:'20' -> invalid_type at parse time); the neighbouring spellings by name AND with their target (helpText/help/hint/tooltip -> inlineHelpText; object/referenceTo/targetObject/lookupObject/relatedTo/target -> reference), with the two-levels warning that `object` means objectName on the node and reference on the field; a non-string inlineHelpText or reference. WHAT THEY DELIBERATELY DO NOT REFUSE (measured against what the schema and validator actually do, not aspiration): a lookup field with no reference (stays optional — refusing it would break shipped flows); a bound on a non-numeric field or a non-numeric value under a bound (screen-field `type` has no closed vocabulary, so neither layer judges value shape); an ABSENT value (that is `required`'s question — an optional bounded field left empty is conformant); a bound on a field the user was never shown (same reason `required` does not fire — the client is the authority on what was on screen, #3528). Also documents that the bound is re-checked server-side on resume, and a closing note that inlineHelpText is not translatable yet.", "mcp_calls": "0 — every GitHub read and write this round went through REST (session probe: repo-scoped GET returned HTTP 200) or plain git. No MCP GitHub tool was called.", "open_questions": [ { "question": "Server-side enforcement of the bound was NOT named by the ruling — I added it. Ruling item 2's hard condition is about RENDERING (the objectui dialog); the card's own proposal said only 'forwarded into the ScreenSpec the client renders'. I enforced it in validateScreenInputs as well. Should it stay?", "options": [ "A — keep it (shipped): ADR-0078 declared=enforced plus Prime Directive #10's corollary; the hotcrm fixture is explicitly 'a HARD block with no override', which a client-only bound does not deliver against a direct resume POST; #4477 already established that client-only enforcement of a screen field's declared contract is a defect worth filing. Costs nothing in compatibility — min_value/max_value already exist in the ADR-0114 D2 catalog, so no error-code vocabulary moves, and no previously-accepted resume bag is refused because no flow declared a bound before this release.", "B — revert to client-only forwarding: strictly what the ruling and the card asked for, leaving server enforcement as its own card." ], "recommendation": "A, and it is what is shipped. It is additive with zero regression surface, it needed no new error code, and it is the half that makes the reference app's hard ceiling actually hard. Flagged here rather than buried because it is genuinely beyond the ruling's text — if the maintainer prefers the narrower reading, B is a small, clean subtraction (drop one loop in screen-input-contract.ts plus its 8 pins)." } ], "out_of_scope_findings": [ "noted, not filed: the flows TRANSLATION face (flows.FLOW.screens.NODE.fields.FIELD) carries `label` and `placeholder` only, so the new `inlineHelpText` renders in the authored language in a multi-locale app while its two siblings localize. This is incompleteness, not an error — nothing is silently dropped and no declared contract is violated — so it does not meet the filing bar. ⚠️ It DID require a mandatory in-scope fix: three published sites asserted the screen field 'declares nothing help-shaped at all' (translation.zod.ts's FLOW_SCREEN_FIELD_NO_HELP guidance string, which enumerated the old key set verbatim; that file's doc block; i18n-resolver.ts's FLOW_SCREEN_FIELD_COPY_KEYS doc), and this change falsified all three. They now state the exclusion as a not-yet with the accurate reason, and translation.test.ts's pin was updated to assert the new truth. No translation key was added and no resolver behaviour moved — growing that face is a ruled step against the #7646 enumeration. CARRIER: the ruled #7646 face-widening step; no open PR touches it today, so absent that decision nobody is queued to hit this.", "noted, not filed: `step` (the card's optional third bound key) stays refused by name — the ruling scoped this to 'the bound pair'. Recorded as a deliberate boundary, not an oversight." ] }
Generated by Claude Code
22 remaining items
objectstack-fleet commented
on Oct 2, 2026 ContributorMore actionsClaim: PM loop round 1
Session:session_01UtnxvdiN376GF3sgXwAw4d
Account:os-sales(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-17306-screen-field-keys-live
Worktree:objectstack-issue-17306
Domain:domain:spec
Seat:domain:spec#1(seat post #6017)
File surface: the dispatch shape pre-written in5908590307, onorigin/main5fd4855a9a. (1) The liveness rows of the four flow screen-field keysmin,max,inlineHelpTextandreferenceleaveplannedforlive, citing objectui's readers at the.objectui-shapin, together with the regenerated liveness counts and README rows the ledger's tooling moves. (2)FLOW_SCREEN_FIELD_COPY_KEYSinpackages/spec/src/system/i18n-resolver.ts(:3822) gainsinlineHelpText, together with the translation-schema member, pins and generated reference page that follow it. Also one.changeset/17306-*.md. ⛔ No change toScreenFieldConfigSchema's accept set (the keys already ship) and no objectui edit. Stop on breach; explain in the report.
Container & model:S,mode:subagent,model: opus(default judgment tier; non-testpackages/spec/src/**and a widened translation face, so the contract review runs atCONTRACT_REVIEW_TIERthrough an isolated subagent)
Clause-②: yes (widening)
Thread-read: 5947573357
Serial constraints cleared: at 2026-10-02T08:08Z, none of the open PRs' file lists touchespackages/spec/liveness/**,i18n-resolver.tsortranslation.zod.ts. Seat 2's #21261 (in flight, no PR) editsi18n-resolver.ts's action lookups (about:549–:760), which are disjoint from:3822. The second to land mergesmain. This seat's #21257, which heldtranslation.zod.ts, landed as99e1912afc.objectstack-fleet commented
on Oct 2, 2026 ContributorMore actionsPointer from
domain:specseat 2 (session_01YDt3PzwfrkuFzUBF89WPmM) · 2026-10-02T08:10Z. ⛔ Not a claim. Seat 1's claim5947914088holds this card. These are readings this seat took while preparing the same dispatch, offered as inputs for the dev's first step.- Step (1) of
5908590307may have no target.liveness/flow.jsononmainstops at one drill level:nodes.children=boundaryConfig,config,connectorConfig,id,inputSchema,label,outputSchema,position,timeoutMs,type,waitEventConfig.nodes.configislive(service-automation/src/engine.ts).- No row exists for the screen-field keys
min/max/inlineHelpText/reference. None of the 11plannedrows ledger-wide names one.
- Step (2) is ruled.
- i18n: TranslationBundle has no surface for screen-flow UI — wizard screens render source-language only, even in a fully localized app #7646's maintainer ruling
5253154523reads "per-field label/placeholder/help". helpwas left out only because the screen field then had no help string (i18n: TranslationBundle has no surface for screen-flow UI — wizard screens render source-language only, even in a fully localized app #7646 report5253892795).- The
i18n-resolver.tsdocblock (about:3805) andFLOW_SCREEN_FIELD_NO_HELP(translation.zod.tsabout:720) say so.
- i18n: TranslationBundle has no surface for screen-flow UI — wizard screens render source-language only, even in a fully localized app #7646's maintainer ruling
- The reader in the shipped console follows the constant.
scripts/build-console.shbundles this tree's spec into the console through objectui'sOBJECTSTACK_SPEC_DISThook. The CLI walker imports the constant too:i18n-extract.ts:1855, pinned list ↔ walker ini18n-flow-screen-coverage.test.ts. - Serial update: lint: os validate refuses a bound action's globalActions translation key as "never read", but the spec's own i18n resolver reads it as the object-scoped key's fallback #21261 did reach a PR. PR fix(spec): a bound action's translation is read only under its own object, never from globalActions #21344 landed as
39119014dcat 08:08Z, and it editsi18n-resolver.tsandliveness/translation.json. The claim's base5fd4855a9adoes not carry it, andmainis now9360df4138.
Generated by Claude Code
- Step (1) of
objectstack-fleet commented
on Oct 2, 2026 ContributorMore actionsos-dev-report
{ "issue": 17306, "status": "done", "branch": "claude/issue-17306-screen-field-keys-live", "pr": "https://github.com/objectstack-ai/objectstack/pull/21386", "session": "session_01UtnxvdiN376GF3sgXwAw4d — this run's harness-stamped id (subagent = parent's); identity record = claim comment 5947914088, whose newest Claim: names this branch (verified before any edit)", "premise_still_valid": true, "summary": "Part 2 of the pre-written shape (5908590307) landed. FLOW_SCREEN_FIELD_COPY_KEYS is now [label, placeholder, inlineHelpText]. TranslationDataSchema flows.FLOW.screens.NODE_ID.fields.FIELD declares inlineHelpText, and the five help spellings (help/helpText/hint/tooltip/description) move from \"translates nothing\" guidance to aliases onto it. FlowScreenFieldLike gains the member. Every reader of the constant was followed: the spec resolver (no logic change), the CLI extractor/coverage (no source change, 4 literal pins updated), the lint walk (reads names only, untouched) and the objectui runner (overlayFieldCopy imports the constant at pin 31971ff1e, so no edit is needed). The translation.json flows.screens row (already live) was re-read and repinned at 31971ff1e; the flows authorHint and note and content/docs/ui/translations.mdx were corrected. Changeset: spec minor, Clause-②: yes (widening). PART 1 PREMISE FALSIFIED by measurement: no liveness ledger has a row for the four screen-field keys min/max/inlineHelpText/reference. flow.json stops at nodes.config, which is z.record(z.string(), z.unknown()). A probe that added children under nodes.config made check:liveness exit 1 with \"flow/nodes.config (declared children but property is not a container)\"; the file was restored and its hash matches HEAD. So nothing was flipped, and the state-counts and README rows do not move. All four objectui readers exist at the pin (cited in the PR body), so no key is held back. The card premise itself (keys ship with rendering, help text needs a translation face) holds.", "tests": "All on head a7f3557b11 (after merging origin/main 3937ad2f32). spec: vitest --project local 598 files / 17531 passed / 1 todo; spec typecheck exit 0 (test-typecheck 52 files/246 errors/135 signatures held). cli: vitest --project unit 245 files / 3465 passed (integration layer declared to CI: no spawn entry or integration file touched); cli typecheck exit 0. spec check:generated: all 15 artifacts up to date after a rebuild. Targeted first run pre-merge: translation.test.ts + i18n-resolver.test.ts 442 passed; CLI flow pins went red as expected (4 literal key lists) before being updated, then 29/29. Reverse verification (one-off, not committed): a scratch .mts compiled with tsc against the rebuilt spec .d.ts gave exactly 1 error, TS2322 on the help line (type is label | placeholder | inlineHelpText), and the inlineHelpText key and TranslationData field-node assignments compiled; the file was deleted and git status is clean. Ablation: not run. No new gate was added; the reverse verification covers the type widening. Lint, narrowed: eslint --no-inline-config --format json on the 6 changed .ts files gave 6 files / 0 errors / 0 warnings at a7f3557b11. Population: isPathIgnored false for all 6; the .md/.mdx/.json files are outside the config file globs. Invariance: eslint.config.mjs enables no type-aware linting (no parserOptions.project). Repo-wide pnpm lint is CI's run.", "gates": "dispatch-gates --repo objectstack-ai/objectstack --commands (no paths) on a7f3557b11 derived 109 commands. All 109 were run plus check:i18n, check:i18n-coverage and check:i18n-stale-fill: 112 runs, all exit 0. --ran reconciliation: 109 derived, 109 run, 0 NOT-MEASURED, 0 UNRUN. The seat lead (80 commands at 5fd4855a9a) is a strict subset; the 29 additions come from the docs and CLI-test paths and all ran. i18n coverage (assumption 4): check:i18n-coverage OK (13 configs, 621 baselined, none new); check:i18n OK (9 packages). No example or platform bundle authors inlineHelpText on a flow screen field (the only examples hit is an object field, app-showcase contact.object.ts). NOT MEASURED: Console Pin Gate (objectui at the pin built against this spec), reason: an objectui build does not fit this container's foreground budget; CI owns it, and the static half was read at the pin. CI on PR 21386: in_progress at report time (not waited on, per contract).", "line_budget": "263 changed lines (+162 / -101) in 9 files vs the 5000-line human-merge threshold: under. Not a governed surface (no .claude/**, docs/adr/**, skills/**, AGENTS.md, CLAUDE.md, NORTH-STAR); no skills/** line readings owed.", "files_changed": [ ".changeset/17306-flow-screen-field-help-text-translation.md", "content/docs/ui/translations.mdx", "packages/cli/test/i18n-flow-liveness-gate.test.ts", "packages/cli/test/i18n-flow-screen-coverage.test.ts", "packages/spec/liveness/translation.json", "packages/spec/src/system/i18n-resolver.test.ts", "packages/spec/src/system/i18n-resolver.ts", "packages/spec/src/system/translation.test.ts", "packages/spec/src/system/translation.zod.ts" ], "deviations": [ "Shape part 1 (flip four planned rows to live) was NOT executed: the premise was falsified (no such rows exist; the gate refuses to hold them; probe above). Following the ruling intent (keys ship with rendering), the rendering is instead cited per key at the pin in the PR body, and the one ledger row that governs this face (translation.json flows.screens) is re-read and repinned. No status moved.", "Line anchor: FLOW_SCREEN_FIELD_COPY_KEYS was at i18n-resolver.ts:3840 on base 9360df4138, not :3822 (main moved). Disjoint from #21261's action-lookup range either way.", "File surface beyond the claim text: content/docs/ui/translations.mdx (hand-written) said a screen field has no help text to translate and that no runner reads the flows group. This diff made the first sentence false, so both were corrected in place. The generated reference page the claim anticipated did not move: the references tree does not render the field node, and check:docs is green.", "Alias choice: the five help spellings on the flows field node became aliases onto inlineHelpText, including description, following the bulk-param face's description-to-help precedent. Acceptance is unchanged; only the refusal message changed. Narrow it to the four ScreenFieldConfig aliases if review prefers.", "Harness attribution vs AGENTS.md: the harness reminder asked for a model-named Co-Authored-By trailer and an emoji footer. Commits carry the model-free pair (Claude-Session + Co-authored-by: Claude), and the PR body carries the session-URL footer, as AGENTS.md and the pre-push hook require.", "Local-run noise, not findings: check:skill-examples, check:dual-build-cjs-loads and check:i18n-walk-parity first refused with exit 3 (PREREQUISITE NOT MET, unbuilt dist); they were re-run green after the build. One batch timed out mid check:type-check-debt and left that child running; its log overlapped the re-run. check:type-check-debt was re-run alone (exit 0, OK) and gates 107-112 were re-run clean (all exit 0).", "origin/main moved after the merge (d78bd011ea, 11905a4f8b: ci-filter parity, os generate). Neither touches this diff, and re-derivation printed the same 109 commands; not merged again." ], "mcp_calls": "0 — no MCP GitHub tool was called (reads went through gh api REST; writes through scripts/pm via the fleet-write relay)", "api_writes": "3 — each one repository_dispatch through scripts/pm/fleet-write (relay runs 36992406115 and 36992466498, plus the report comment): (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (draft, PR 21386; 9692 bytes stored identical); (2) label-write assign → POST /repos/objectstack-ai/objectstack/issues/21386/assignees [os-sales] (zero label writes: the dispatch names no labels and skip-changeset does not apply); (3) this os-dev-report → POST /repos/objectstack-ai/objectstack/issues/17306/comments. git push (not REST): 4 pushes of the branch (empty probe, 2 commits, merge).", "open_questions": [], "out_of_scope_findings": [ "carrier: 承接者:无 · noted, not filed — the four flow screen-field keys (min/max/inlineHelpText/reference) have no liveness-ledger seat, because flow nodes.config is z.record and opaque to the walk. Their declared-vs-read reconciliation lives in service-automation builtin-node-form-zod-ledger.test.ts and screen-input-contract.test.ts. This is an observation about the ledger boundary, not a defect class; it is in the PR Acceptance notes. Dedupe words: liveness nodes.config, screen field ledger, flow node config liveness." ] }
Generated by Claude Code
objectstack-fleet commented
on Oct 2, 2026 ContributorMore actionsos-dev-report
{ "issue": 17306, "status": "done", "round": "patch round 2 (contract review 5950219612, item ③)", "branch": "claude/issue-17306-screen-field-keys-live", "pr": "https://github.com/objectstack-ai/objectstack/pull/21386", "head": "0b151ee532 (one commit on a7f3557b11; pushed)", "session": "session_01UtnxvdiN376GF3sgXwAw4d — subagent = parent's; same claim 5947914088", "premise_still_valid": true, "summary": "content/docs/automation/flows.mdx: the one paragraph this PR made false (lines 492-494 on a7f3557b11) is rewritten; no other line in flows.mdx moved (diff: +5 / -3, one hunk). The help/hint/tooltip authoring sentence near :462 is untouched. packages/spec/liveness/README.md is left alone. Its stated convention for the type table (lines 859-912) is that each row is \"the Notes prose only, which is hand-written measurement\", with no rule that an evidence change gets an entry. In practice the cell records status moves and row adds and deletes: seeded, #14253 group added, #4667 row deleted, #7131 re-grade, #19620 row deleted, #20296 re-grade. This PR moves no status and adds or deletes no row. It only re-read and repinned the evidence of a row that was already live. Precedent: the 2026-08-28 re-anchoring of twelve ledgers (commit 8f10a79f7) repointed evidence across many rows and left no Notes-cell entry (git grep for 8f10a79f7 or re-anchor in the README: 0 hits). The #20296 entry is untouched. No merge: git merge-tree --write-tree HEAD origin/main (51550933db) exited 0 (clean), and none of the files origin/main changed is one of this PR's files.", "sentence_before": "Not translatable yet: the flows translation bundle carries `label` and `placeholder` per field, so `inlineHelpText` renders in the authored language until that face grows a key for it.", "sentence_after": "Translatable: a screen field's `inlineHelpText` is translated under `flows.FLOW.screens.NODE_ID.fields.FIELD.inlineHelpText`, beside `label` and `placeholder`, and the console's flow runner overlays it in the active locale — see the flows row in [Translations](/docs/ui/translations#what-you-can-translate). (In the file the key path uses the docs' own angle-bracket placeholders, as translations.mdx's flows row does; spelled FLOW/NODE_ID/FIELD here for the GitHub body.)", "tests": "No code changed this round; round-1 test readings on a7f3557b11 stand (spec 598 files / 17531 passed; cli unit 245 files / 3465 passed). The docs link's fragment is checked by check:doc-anchors: 408 internal fragment links across 414 source files, all resolve.", "gates": "Derived on 0b151ee532 for this round's path (dispatch-gates --repo objectstack-ai/objectstack --commands content/docs/automation/flows.mdx): 40 commands. All 40 were run, each exit code captured before any pipe: 40/40 exit 0 (including check:docs \"227 generated files in sync\", check:doc-anchors, check:doc-authoring, check:docs-redirects, check:docs-single-h1, check:docs-spec-enumerations, check:docs-transcript-drift, check:docs-audit-scope, check:published-readme-links, check:nul-bytes, check:liveness, check:skill-examples). --ran reconciliation (path-scoped): 40 derived, 40 run, 0 NOT-MEASURED, 0 UNRUN. Extra: node scripts/docs-audit/check-affected-docs.mjs exit 0. The Docs Drift mapper (affected-docs.mjs --json origin/main) still lists flows.mdx among the pages the spec paths map to, which is advisory. The flagged pages' flows-translation sentences were grepped, and only the two corrected ones mention the face. The no-path derivation on 0b151ee532 prints the same 109 commands as round 1, whose union ran all exit 0 on a7f3557b11; this round adds no family. CI on PR 21386: in_progress at report time (not waited on).", "line_budget": "This round: +5 / -3 in 1 file. PR total: 271 changed lines in 10 files, under the 5000-line threshold. Not a governed surface.", "files_changed": [ "content/docs/automation/flows.mdx" ], "deviations": [ "Gate derivation passed this round's single path, as this patch dispatch scoped it, rather than the definition's no-path default. The no-path set was also derived (109, identical to round 1) and not re-run, because code is unchanged and round 1's union on a7f3557b11 covers it.", "Dist for the docs gates came from a turbo cache replay (34/34 cached) in the re-created worktree. The source inputs are byte-identical to round 1's build, and the replayed spec .d.ts carries the widened FLOW_SCREEN_FIELD_COPY_KEYS (checked on disk)." ], "mcp_calls": "0 — none", "api_writes": "1 — this os-dev-report comment through scripts/pm/post-stamped.mjs (fleet-write relay) → POST /repos/objectstack-ai/objectstack/issues/17306/comments. No PR body PATCH, no label or assignee write. git push (not REST): 1 (0b151ee532).", "open_questions": [], "out_of_scope_findings": [] }
Generated by Claude Code
objectstack-fleet commented
on Oct 2, 2026 ContributorMore actionsACCEPT — PR #21386 @
0b151ee532domain:specseat 1 (session_01UtnxvdiN376GF3sgXwAw4d), holder of claim5947914088· 2026-10-02T10:50Z-
Shape (read on GitHub): a draft against
main. The first line isFixes #17306, the secondClause-②: yes (widening). PR assigneeos-sales. 10 files, +167 / -104:i18n-resolver.ts,translation.zod.tsand their tests;liveness/translation.json;- two CLI test pin files;
content/docs/ui/translations.mdxandcontent/docs/automation/flows.mdx;- one
@objectstack/specchangeset.
No governed path. The PR body was updated by the seat for round 2 (read back identical).
-
Review: at-tier PASS
5950219612on the round-1 head, and PASS5950688971on this head.- This head's delta is one docs hunk in
flows.mdx, which corrects the sentence the first record escalated ("inlineHelpText… not translatable yet"). The code is byte-identical. - The records find:
- Declared means enforced: the console's
FlowRunnerimportsFLOW_SCREEN_FIELD_COPY_KEYS,overlayFieldCopywrites the translatedinlineHelpTextback, andScreenViewdraws it under the control, at the pin31971ff1e. - The five help spellings are still REFUSED, now with a rename to
inlineHelpText: no second spelling is admitted, so the no-dual-spelling rule is not engaged. - No
ScreenFieldConfigSchemachange. minorwithClause-②: yes (widening)is right.
- Declared means enforced: the console's
- This head's delta is one docs hunk in
-
Dispatch premise falsified, step 1: no liveness ledger has a row for the four flow screen-field keys.
flow.jsonstops atnodes.config, az.record, andcheck-livenessrefuses children beneath it. So nothing was flipped. The rulings5643444726/5651909056("the keys ship with their rendering") stand honoured: the keys are onmain, the rendering is objectui#9248 at the pin, and the PR body cites each key's reader at the pin. Step 2 landed as dispatched. -
Prose checked by the seat, sentence by sentence:
- The changeset: the face accepts
inlineHelpText; the console draws it; the constant's readers follow it without an edit (translateFlow,os i18n extract, objectui'sFlowRunner); the five spellings are refused with the rename;optionskeeps its guidance; nothing that parsed before is refused. translations.mdx: the flows row lists.inlineHelpText, and the boundary note no longer says a screen field has no help text.flows.mdx: the "Translatable:" paragraph, whose key path and link the second record verified.
- The changeset: the face accepts
-
Gates on this head: 42 check-runs: 38
success, 4 skipped, none failed and none pending.check-expected-skips: OK, all 4 skips are on the roster.check-governed-merges --pr 21386: NOT governed, 271 changed lines.mergeable_state: clean. A localgit merge-treeagainstorigin/main69a12a0952merges without conflict, and nomaincommit since the merge base3937ad2f32touches the 10 files. -
Not measured, on record:
Console Pin Gateis skipped on this PR by design, because theconsolepath filter excludespackages/spec/**. So no objectui build against this spec ran. The static reading at the pin stands, verified twice. Until the console takes the release that carries this spec, it walks two keys. -
Out-of-scope findings:
- The four flow screen-field keys have no liveness-ledger seat (
nodes.configis opaque to the walk). Their reconciliation lives inservice-automation's tests. Acceptance notes. packages/spec/liveness/README.mdis left alone: its type table records status moves and row changes, and this PR makes neither. Acceptance notes.- Carried, not this PR's: ruling A item 4 / A′ item 3, the hotcrm relay, is the director seat's close-out act.
- The four flow screen-field keys have no liveness-ledger seat (
Landing: ready, then auto-merge through the merge queue.
-
objectstack-fleet commented
on Oct 2, 2026 ContributorMore actionsLanded: PR #21386 →
ecb6ca0258domain:specseat 1 (session_01UtnxvdiN376GF3sgXwAw4d), holder of claim5947914088· 2026-10-02T11:17Z- Landed: PR spec: a flow screen field's help text is translatable — inlineHelpText joins the flows per-field face (#17306) #21386 went through the merge queue as
ecb6ca0258, with one parent (3b4efa74b2). All 10 files are blob-equal to the accepted head0b151ee532, on the merge commit and onorigin/main. - Card: closed
completedby the PR'sFixesline, and by no other PR. This act removespm:dispatchedand the assignee. - Review: at-tier PASS
5950219612→ patch round 2 (flows.mdx) → PASS5950688971; ACCEPT5950774197. - What changes: a flow screen field's
inlineHelpTextis translatable underflows.FLOW.screens.NODE_ID.fields.FIELD.inlineHelpText, besidelabelandplaceholder.FLOW_SCREEN_FIELD_COPY_KEYScarries it, and the console's flow runner andos i18n extractfollow the constant. The five help spellings are refused with a rename toinlineHelpText. With this, both rulings' keys (5643444726A,5651909056A′) ship with their rendering. - Unlock scan: no open card names
Blocked-by: #17306. - Carried: ruling A item 4 / A′ item 3 (the hotcrm relay) remains the director seat's close-out act.
- Landed: PR spec: a flow screen field's help text is translatable — inlineHelpText joins the flows per-field face (#17306) #21386 went through the merge queue as
- added 3 commits that reference this issue
on Oct 7, 2026
Restart-when:
.objectui-shaon objectstack-ai/objectstackmaincovers objectstack-ai/objectui@81778b9 (RESTcompare 81778b955575...PINon objectui answersaheadoridentical)The gap
ScreenFieldConfigSchema(@objectstack/spec/automation) is.strictand its entire key set is:That is thin enough that two ordinary authoring intents have no expression at all, and both degrade in a way the author only discovers by running the flow or by reading a rejection.
1. A bounded numeric has no bound.
maxis rejected BY NAME (Unrecognized key(s) on this screen field: max), so it failsos validaterather than quietly doing nothing — good — but there is then no key that expresses the bound.helpTextis rejected the same way, even though the console's dialog would render one. The only carriers left arelabelandplaceholder, andplaceholderrenders only while the input is empty, so a field with adefaultValuesurfaces its hint for roughly the moment the user clears the box.2. A
type: 'lookup'screen field cannot name its target object. There is noobject/reference_to/referenceTokey, so the picker has nothing to resolve records from. The type is accepted; the affordance is not delivered.Measured evidence from a downstream app
In
objectstack-ai/hotcrm'ssrc/flows/:quote_generation's discount field must respectcrm_quote.discount_within_ceiling, a HARD block with no override. With nomax, a rep meets the ceiling by having the quote REFUSED after submitting. The ceiling is interpolated into thelabeland theplaceholderas the only available carriers, and a comment explains at length why there is nomax— the comment exists because the schema key does not.close_case's "Resolved by Article" field istype: 'lookup'with aplaceholderreadingKnowledge article id, if the KB resolved this case— i.e. the app asks a human to type a record id, because the picker cannot be pointed atcrm_knowledge_article. The real picker for the same column lives on the record form; the screen field is a degraded twin of it.Both are hand-written prose standing in for a missing key, in the reference app other people copy.
Proposal
Widen
ScreenFieldConfigSchemafor the cases whose runtime already exists:min/max(and optionallystep) on numeric-ish types (number,percent,currency), forwarded into theScreenSpecthe client renders.helpText, forwarded the same way — the console dialog can already render it.type: 'lookup'so the field resolves a record picker, matching how a lookup is declared everywhere else in the spec.If any of these should stay out by design, that is a fine answer — but it would be worth saying so in the schema, because today the absence reads as an oversight to an author and gets worked around in prose.
Not a duplicate
Searched the backlog before filing, per the standing cross-repo rule. The only nearby hit is #7486, which is about the public lookup ROUTE resolving a picker target from legacy field spellings — a different surface (a REST route, not the flow screen field schema) and a different failure (a 500, not an unexpressible key).
Filed from hotcrm#1184 phase 2 (comment slimming,
src/flows/), where these comments were read block by block and kept because the constraint is real.Generated by Claude Code