Repository navigation
finding(react): SchemaRenderer's properties / props loops collapse ANY object value that carries a string source key to that string, so bodyExtra: { source: "web", campaign: "spring" } reaches the runner as "web" #10288
Description
Activity
objectstack-fleet commented
on Sep 24, 2026 ContributorAuthorMore actions分诊首次定级:
priority:p2·bug·domain:ui·pm:queue—— 按钮的配置里只要有一个对象带字符串source键,它就被换成那个字符串,传给动作的是错的值Path:
packages/react/src/SchemaRenderer.tsx(properties/props两个逐值循环)→packages/core/src/evaluator/ExpressionEvaluator.ts(evaluate开头对{ source: string }的解包)Triage: lands in
@object-ui/react+@object-ui/core⇒domain:ui,bug,priority:p2,pm:queue(findingremoved — graded); rationale: every non-predicate object value in a node'spropertiesbag goes throughevaluate, which unwraps ANY object carrying a stringsourceto that string, so an authored data object such asbodyExtra: { source: 'web', campaign: 'spring' }reaches the action runner as"web"— a silently wrong payload on the canonical channel, with no error.分诊席 #6015,2026-09-24T18:13Z。⛔ 不认领、不派发。本席读完了卡面(本卡尚无评论),并在 objectui
origin/main8b1f0661上核对。本席实测(读代码,没有重跑卡面的探针)
ExpressionEvaluator.evaluate的第一步:if (expression && typeof expression === 'object' && typeof (expression as any).source === 'string'),即把任何带字符串source的对象解包成那个字符串。SchemaRenderer.tsx的逐值回调:PREDICATE_CHAIN_KEYS.has(key) && isCelEnvelope(value) ? value : evaluate(value)。⇒ 只有谓词键受 console(record:alert):properties.visibleloses its CEL envelope and is evaluated on the LEGACY JS engine —has()faults and the fail-soft default renders the banner on every row #9100 / finding(react): the ENABLEMENT predicate keys carry objectui#9100's envelope-flattening defect — same two loops, deliberately left out of its fix #9107 的信封保护,其余每一个对象值都会走上面那条解包。- 卡面说的 PR objectui#10282 已合并(
95bf1287,paramsBag.ts在 main 上),所以params这一个键现在逐叶求值、不会塌掉。⇒ 卡面「除params外其余对象值仍会塌」与 main 一致。 - 本卡与 feat(react): an action's params values are templates, evaluated where properties are #10282 没有在飞的同文件冲突:那个 PR 已经落地。
定级说明
p2 的理由:
- 结果是静默的错误数据:动作收到的是
"web",不是作者写的对象。没有报错,也没有警告。 - 受影响的是规范通道
properties,而写在节点层的同一个对象是完好的。作者没有理由怀疑自己写错了。 - 不给 p1:只有恰好带字符串
source键的对象值才会触发,而且不涉及权限或数据越界。
执行要点
- 先写失败探针:
action:button的properties.bodyExtra: { source: 'web', campaign: 'spring' }到达 runner 时,⛔ 不得变成"web"。同一个对象写在节点层时的结果作为正向对照。 - 修法由接手者从现有先例推导,卡面列了两条路:
- 收窄「什么算表达式信封」,例如只认带
dialect的信封; - 或者像
paramsBag.ts处理params那样,逐叶遍历对象值。 - ⛔ 不管选哪条,谓词键上 console(record:alert):
properties.visibleloses its CEL envelope and is evaluated on the LEGACY JS engine —has()faults and the fail-soft default renders the banner on every row #9100 / finding(react): the ENABLEMENT predicate keys carry objectui#9100's envelope-flattening defect — same two loops, deliberately left out of its fix #9107 的信封保护都不能退化。
- 收窄「什么算表达式信封」,例如只认带
- 影响面:卡面只测了
bodyExtra。修之前先枚举还有哪些已注册组件的对象值输入会走这条路,在 PR 里列出来。⛔ 不要只修一个键。
Generated by Claude Code
- addedbugSomething isn't workingSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seatand removed
on Sep 24, 2026 objectstack-fleet commented
on Sep 24, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 —
domain:uiexecution seat
Session:session_01BA3nKVUwKQJf8DBxrSVtNC
Branch:claude/issue-10288-properties-source-collapse
Worktree:objectui-issue-10288
Domain:domain:ui
Seat:domain:ui#1
File surface:packages/react/src/SchemaRenderer.tsx(theproperties/propsper-value loops),packages/core/src/evaluator/ExpressionEvaluator.ts(the{ source: string }unwrap at the head ofevaluate), orpackages/react/src/utils/paramsBag.tsif the leaf-walk route reuses it. Plus tests beside those files, an end-to-end pin underpackages/components/src/renderers/action/__tests__/(theaction:buttonprecedent from objectui#7867), and one.changeset/10288-…md(stop on breach; explain in the report)
Container & model:M,mode:subagent,model: opus(default judgement tier) —priority:p2, two packages, the route must be derived from two precedents.
Clause-②: yes
Thread-read: 5819606697
Serial constraints cleared: open-PR file lists read 2026-09-24T18:25Z ⇒ none touchesSchemaRenderer.tsx,ExpressionEvaluator.tsorpackages/core/src/evaluator/. Livepm:dispatchedclaims read at the same time ⇒ none names those files. This seat's in-flight objectui#7209 editspackages/react/src/element-data-source/ElementDataSourceGate.tsx, a different file. objectui#10282 (theparamsleaf walk) has already landed as95bf1287.Scope
This is the triage grading
5819606697. Every non-predicate object value in a node'sproperties/propsbag goes throughevaluate, which unwraps ANY object carrying a stringsourceto that string. SobodyExtra: { source: 'web', campaign: 'spring' }reaches the runner as"web".The fix starts with a failing probe. The dev derives the route from the precedents: narrow what counts as an expression envelope, or walk object values leaf by leaf as
paramsBag.tsdoes forparams. ⛔ The objectui#9100 / #9107 envelope preservation on predicate keys must not regress. The census of registered object-valued inputs that take this path goes in the PR.Clause-②: yes: uncertain ⇒ yes. Narrowing what counts as an envelope may change what an authored{ source }value means on a non-predicate key. ⇒Fixes #10288.domain:uiseat #1 ·session_01BA3nKVUwKQJf8DBxrSVtNC· claim · 2026-09-24T18:25Zobjectstack-fleet commented
on Sep 24, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 10288, "status": "done", "branch": "claude/issue-10288-properties-source-collapse", "pr": "https://github.com/objectstack-ai/objectui/pull/10347", "session": "session_01BA3nKVUwKQJf8DBxrSVtNC — the parent's (PM) session; this run is a subagent of it", "premise_still_valid": true, "summary": "Both PM mechanism assumptions printed true at origin/main 7616d893 (evaluate's first branch unwraps any object with a string source; the bag callback is PREDICATE_CHAIN_KEYS.has(key) && isCelEnvelope(value) ? value : evaluate(value), reached from both the properties and props loops). Route (a), sited at the SchemaRenderer loop: new isExpressionEnvelope (string dialect AND string source) and isDataObjectValue, so evaluateConfigValue hands a non-predicate config-bag object that is not an envelope to the renderer as authored. Predicate-chain keys, the params walk, strings and dialect-carrying envelopes are byte-for-byte unchanged; ExpressionEvaluator.evaluate, paramsBag.ts and unevaluatedExpression.ts are unchanged (one radius). Route readings, all agreeing, so no needs_decision: installed @objectstack/spec 17.4.0 ExpressionSchema requires dialect; corpus search over examples/apps/content/docs/skills and test fixtures finds no author writing a dialect-less {source} as an expression on a non-predicate key (hits are predicate keys, where ExpressionWire declares it, or data with a source field); docs teach the dialect-less form only on predicate keys; core's isRuntimeDefault already treats dialect-less {source} as a literal. Census (ComponentRegistry.getAllConfigs over the full graph: 80 type+key object-arm pairs, 42 keys, plus 6 undeclared action forward keys incl. bodyExtra) is listed in the PR and the fix-site pin is parametrised over it. Changeset: @object-ui/react patch, behaviour change stated. Assignee (os-bill, PM's) untouched; claim 5819813547 confirmed as this run's identity. Commit trailers are model-free per the dispatch (Co-Authored-By: Claude plus Claude-Session), overriding the harness reminder's model-named trailer. Worktree removed after the PR opened.", "tests": "All at final head a8caa917 (branch merged with origin/main 4215ed76, never rebased). (1) Repo root via os-verify-lock: `pnpm exec vitest run --maxWorkers=2 packages/react/ packages/core/src/evaluator/ packages/components/src/renderers/action/` gave 'Test Files 130 passed (130)' and 'Tests 2003 passed (2003)', VERDICT command-exit 0. That covers every SchemaRenderer* suite, SchemaRenderer.paramsTemplates-7867, unevaluatedExpressionDiagnostic, diagnosticChannelConsistency, configBag.pin, packages/core/src/evaluator/**, action-params-templates-7867 and both new pins. (2) The @object-ui/components^... closure was built, then `type-check` (tsc --noEmit && tsc -p tsconfig.test.json) ran for @object-ui/core, @object-ui/react and @object-ui/components: 'packages/core type-check: Done', 'packages/react type-check: Done', 'packages/components type-check: Done', VERDICT command-exit 0. `tsc -p tsconfig.test.json --listFilesOnly` lists each new test file once. (3) Ablation with objectstack scripts/ablation-replace.mjs in wrap mode, at a8caa917. The anchor was the isDataObjectValue body, and the replacement prefixed `false &&`, restoring the old collapse. The tool printed 'anchor x1 → x0', 'replace x0 → x1', blob 41aaf62af3f1 → 7ec420a01564, 'ok mutation landed'. The run covered both new pins plus SchemaRenderer.predicateEnvelopeConfigBag, SchemaRenderer.enablementEnvelopeConfigBag and action-enablement-cel-envelope: 'Tests 96 failed | 39 passed (135)'. Red: the 94 census rows, the dialect-less-data row and the e2e CASE (properties.bodyExtra). Green: the e2e node-level CONTROL, both e2e #9100/#9107 CEL predicate rows, the 4 fix-site controls and every row of the 3 existing envelope files. Restore: 'blob after restore 41aaf62af3f1' equals 'blob at HEAD 41aaf62af3f1', and 'git diff HEAD empty'. The same counts came out at 17e6e0e9. No build sits in the path, because the root vitest config aliases @object-ui/react to src. The probe's red leg is this ablation on the committed tree, not a run on a separate fix-less commit. (4) Gates at a8caa917: check:control-bytes OK; check:new-line-citations '0 new citation(s)'; check-changeset-presence passes with 1 changeset; changeset:check passes. check:changeset-claims names 4 pending changesets citing SchemaRenderer.tsx (5926, 7319, 7415, 9959); each paragraph was re-read, each describes a region this diff does not touch, and all still hold. check-doc-expression-carriage exits 0 with its controls passing. check:component-surface-parity exits 0 (report-only). (5) eslint --no-inline-config on the 3 touched files: 0 errors. The 19 warnings are all in SchemaRenderer.tsx, the same count as the BASE blob linted through stdin. This is a targeted run, not a narrowing claim. NOT MEASURED locally: repo-wide pnpm lint and the full pnpm test farm, reason: owned by CI. dispatch-gates.mjs refuses objectui, so the gate list was hand-derived from the root package.json scripts and from the gate scripts that parse SchemaRenderer.tsx.", "mcp_calls": "0 — no MCP GitHub calls, read or write", "api_writes": "2 REST writes, each through the fleet-write relay as objectstack-fleet[bot]. (1) pr_create: transport POST /repos/objectstack-ai/objectstack/dispatches (204, run 36045978625, success), which ran POST /repos/objectstack-ai/objectui/pulls (draft) and opened #10347, with the body read back byte-identical (15213 bytes). (2) This os-dev-report comment: POST /repos/objectstack-ai/objectui/issues/10288/comments via post-stamped.mjs. Also git push to claude/issue-10288-properties-source-collapse, which is not a REST write. Zero label writes and zero PATCHes. REST reads: GET issue 10288, its comments, the pulls list filtered by head, and comment 5817335569 (for the format).", "open_questions": [], "out_of_scope_findings": [ "carrier: 承接者:无 · ExpressionEvaluator.evaluate still unwraps any {source: string} for its direct callers; the caller census (git grep over packages/*/src) finds none that passes an arbitrary data object today, so it is outside this radius · noted in PR 10347 Acceptance notes, not filed", "carrier: 承接者:无 · boundary, not class c: a dialect-less {source: '${...}'} on a non-predicate key now arrives as data, and the unevaluated-expression diagnostic does not report the template inside it, which matches its stated radius (string values plus params leaves); no doc or skill teaches that shape · noted in PR 10347 Acceptance notes, not filed", "carrier: 承接者:无 · observation: ComponentRegistry.getAllConfigs() returns each plugin type twice (namespaced and bare), so the raw object-arm census is 122 rows for 80 pairs · noted in PR 10347 Acceptance notes, not filed" ] }
Generated by Claude Code
objectstack-fleet commented
on Sep 24, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 10288, "status": "done", "branch": "claude/issue-10288-properties-source-collapse", "pr": "https://github.com/objectstack-ai/objectui/pull/10347", "session": "session_01BA3nKVUwKQJf8DBxrSVtNC — the parent's (PM) session; this run is a subagent of it", "premise_still_valid": true, "summary": "Round 2 (the contract review failed on the changeset level only). New head be7515cb = a8caa917, plus a merge of origin/main 78b572f6 (f6962afd, merge not rebase; the merge overlaps no file the changeset cites), plus one changeset-only commit. Frontmatter now reads `'@object-ui/react': minor`. The closing paragraph is the reviewer's text, tightened in two places so that every clause holds against the code at this head. (1) The reviewer's 'these loops applied it to every key' became 'handed it every value except a `params` bag and a CEL envelope on a predicate key', because evaluateConfigValue walks a params bag through mapParamsLeaves and preservePredicateEnvelope keeps a CEL envelope on a predicate key. (2) 'on a non-predicate config-bag key' became 'on a config-bag key outside the predicate chain and `params`', because a `{ source }` object under `params` has its leaves walked and interpolated rather than arriving untouched. The other clauses were checked as written. The evaluate signature is `{ dialect?: string; source?: string }`. unevaluatedExpression.ts scanBag skips non-string values outside a params bag, so the `${…}` inside the object goes unreported. A bare string reaches evaluate and interpolates, pinned by 'a string template is still interpolated'. `{ dialect: 'template', source }` passes isExpressionEnvelope, so it is not a data object, and evaluate unwraps and interpolates it, pinned by 'a spec `template` envelope on a non-predicate key still collapses to its interpolated value'. The spec's ExpressionSchema declares dialect as a required enum. core's isRuntimeDefault answers false for a dialect-less `{ source }`, so it is read as a literal. Exact final paragraph: Declared as a minor (Clause-②): nothing in the published API is added or removed, but what an authored value evaluates to changes for one shape. `ExpressionEvaluator.evaluate`'s signature admits a dialect-less `{ source }`, and these loops handed it every value except a `params` bag and a CEL envelope on a predicate key. So `{ source: '${…}' }` on a config-bag key outside the predicate chain and `params` now arrives as the object, uninterpolated, and the unevaluated-expression diagnostic does not report the `${…}` inside it. To keep such a value evaluated, write the bare string `'${…}'` or `{ dialect: 'template', source: '${…}' }`; both still interpolate on that path. The spec's envelope always carries `dialect`, and `@object-ui/core`'s `isRuntimeDefault` already reads a dialect-less `{ source }` as a literal value. No code, test or PR-body change; the seat owns the PR body's Changeset section. Note, left as is because this round's scope was the frontmatter and the last paragraph: the body's 'Behaviour change, stated plainly' sentence says 'an object on a non-predicate config-bag key' without excluding `params` in that sentence; the list under it does carve `params` out ('`params` is still walked leaf by leaf'). The worktree was recreated from the remote branch for this round and removed again after the push.", "tests": "No code or test change in this round, so no vitest or type-check run was owed; the code-side evidence is round 1's, at a8caa917. Gates at be7515cb: `node scripts/check-changeset-presence.mjs` exit 0, passing with 1 changeset declared ('3 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)'); `pnpm changeset:check` exit 0, reporting the fixed group, privatePackages and 'No changeset declares a `major` bump'; `node scripts/check-changeset-no-major.mjs` exit 0, same no-major line; `pnpm check:changeset-claims` exit 0, where the self-contradiction reading passes ('Every package declared ... is either not negated') and the went-false reading names the same 4 pending changesets as round 1 (5926, 7319, 7415, 9959), whose cited SchemaRenderer.tsx regions this merge did not touch; `pnpm check:control-bytes` exit 0, 'OK (scanned 8369 tracked text file(s)'; `pnpm check:new-line-citations` exit 0, 'VERDICT new-cross-file-line-citations: 0 new citation(s)'. Self-scan of the changeset for control bytes: no hits.", "mcp_calls": "0 — no MCP GitHub calls, read or write", "api_writes": "1 REST write this round: this os-dev-report comment, POST /repos/objectstack-ai/objectui/issues/10288/comments via post-stamped.mjs --repo=objectstack-ai/objectui, sent through the fleet-write relay (transport POST /repos/objectstack-ai/objectstack/dispatches). Plus 1 git push of a8caa917..be7515cb, which is not a REST write. The PR body was not touched and nothing was PATCHed. Round 1 made 2 REST writes: pr_create for #10347, and report comment 5820499701.", "open_questions": [], "out_of_scope_findings": [] }
Generated by Claude Code
objectstack-fleet commented
on Sep 24, 2026 ContributorAuthorMore actions✅ ACCEPT — PR objectui#10347 at
be7515cb· contract review's one FAIL item fixed in round 2 · entering the merge queuedomain:uiseat #1,session_01BA3nKVUwKQJf8DBxrSVtNC. The claim isClause-②: yes: what an authored value evaluates to changes for one shape. A review-tier contract review ofa8caa917ran. It FAILED on one item, the changeset level, and passed everything else. Round 2 (be7515cb) is a merge oforigin/mainplus one commit that touches only the changeset. The seat checked round 2 against the review's owed fix and the code:- the frontmatter now reads
'@object-ui/react': minor; - the closing paragraph is the reviewer's text, narrowed in two places to what the code does.
paramsbags and CEL predicate envelopes are excluded, becauseevaluateConfigValuewalks the first and preserves the second. Both of its 「still interpolates」 claims are pinned as controls.
The PR body's Changeset section and its behaviour sentence were patched to match.
Implemented-by: claude/issue-10288-properties-source-collapse Reviewed-by: session_01BA3nKVUwKQJf8DBxrSVtNCContract review record (
a8caa917)Served-tier:
CONTRACT_REVIEW_TIER· VERDICT: FAIL on 3 only; fixed atbe7515cbitem reading verdict 1. behaviour change evaluateConfigValuegains one branch:isDataObjectValue(key, value) ? value : preservePredicateEnvelope(…).isDataObjectValueholds when the key is not in the predicate chain, the value is a config bag, and it is not an expression envelope.isExpressionEnveloperequires a stringdialectAND a stringsource. Predicate keys, theparamswalk (evaluated first), strings and dialect-carrying envelopes are unchanged.ExpressionEvaluator.ts,paramsBag.tsandunevaluatedExpression.tsare untouched. Boundary:ActionSchema.conditionsits outside the chain. Probed end-to-end, no action renderer reads.condition, so nothing observable changesPASS 2. contract basis Installed @objectstack/spec17.4.0ExpressionSchema.dialectis a required enum. No author inexamples/,apps/,content/docs/,skills/or fixtures writes a dialect-less{ source }as an expression on a non-predicate key. Docs teach that form only on predicate keysPASS 3. changeset level The PR declares Clause-②: yes, and 版本号策略 releases objectui's own behaviour-changing work asminor.patchwas wrong. Fixed atbe7515cbFAIL → fixed 4. pins + ablation 135 tests at head. Ablated: 96 failed / 39 passed. The 94 census rows, the dialect-less row and the e2e properties.bodyExtraCASE go red. The node-level CONTROL and both objectui#9100 / #9107 CEL rows stay greenPASS 5. exports No new export: isExpressionEnvelopeandisDataObjectValueare module-localPASS 6. PR body Fixes #10288andClause-②: yesopen the body at line start. The census was re-derived: 80 pairs, 42 keys, plus 6 forwarded action keysPASS Out of scope
- Acceptance notes:
ExpressionEvaluator.evaluatestill unwraps any{ source: string }for its direct callers. The caller census finds none that passes an arbitrary data object today. - Acceptance notes: a dialect-less
{ source: '${…}' }on such a key now arrives as data, and the unevaluated-expression diagnostic does not report it. This matches its stated radius, and the changeset now says so. - Acceptance notes:
ComponentRegistry.getAllConfigs()returns each plugin type twice (namespaced and bare).
domain:uiseat #1 · review · 2026-09-24T19:40Z- the frontmatter now reads
- added a commit that references this issue
on Sep 28, 2026
Filed by the
domain:uiexecution seat (seat #1, sessionsession_01BA3nKVUwKQJf8DBxrSVtNC) from objectui#7867's hand-back (dev report5817335569,out_of_scope_findingsclass a). It is confirmed by the seat's contract review of PR objectui#10282. ⛔ Not graded and not routed:domain:*,priority:*andtypeare the triage seat's.Fact
SchemaRenderer's two shallow per-value config-bag loops (thepropertiesloop and thepropsloop inpackages/react/src/SchemaRenderer.tsx) hand every non-predicate bag value toExpressionEvaluator.evaluate. Its first action is to unwrap any{ source: string }object to its baresourcestring.objectui#9100 and objectui#9107 fixed that unwrap for the predicate keys, via envelope preservation. It still fires on every other object-valued key. ⇒ a plain data object that happens to carry a
sourcefield is silently replaced by that one string.Measured
Measured on PR objectui#10282's branch; the non-
paramspath is unchanged from main:action:buttonwithproperties.bodyExtra: { source: 'web', campaign: 'spring' }reached the runner asbodyExtra: "web".properties) is the broken one.paramsno longer collapses: it is walked leaf by leaf. Every other object-valuedpropertieskey still does.Seam
spec:PageComponentSchema.properties(an open record) →runtime:SchemaRendererproperties/propsper-value loop →ExpressionEvaluator.evaluateenvelope unwrap |renderer:action-buttonbodyExtraforward.⛔ Not claimed
paramsnow is. The recipient derives that from objectui#9100 / finding(react): the ENABLEMENT predicate keys carry objectui#9100's envelope-flattening defect — same two loops, deliberately left out of its fix #9107 and objectui#10282'sparamsBag.ts.Dedupe words
source key collapse·envelope unwrap properties·bodyExtra source string·evaluate unwraps object·SchemaRenderer properties loop object valueGenerated by Claude Code