Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
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

Closed

Description

@os-sales

Observation-class, measured while implementing objectui#6752. No behaviour is wrong; this is one question with several spellings.

The fact, measured on b76ca6764

"Is this value a real config bag (an object, not an array, not null)?" is asked in packages/react in five places, in four different spellings:

  1. SchemaRenderer.tsx — the properties evaluation guard;
  2. SchemaRenderer.tsx — propsWithoutCanonicalKeys, inline;
  3. SchemaRenderer.tsx — the winningVisibilityKey caller block, inline;
  4. utils/propsBagDiagnostic.ts — a private isConfigBag helper (objectui#6708);
  5. 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.

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    and removed on Aug 29, 2026
  2. self-assigned this
    on Aug 29, 2026
  3. os-sales commented on Aug 29, 2026

    @os-sales
    CollaboratorAuthor

    Claim: domain:ui execution seat, PM session session_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 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

    One reading from #6752 worth carrying

    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.


    Generated by Claude Code

  4. os-sales commented on Aug 29, 2026

    @os-sales
    CollaboratorAuthor

    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

  5. os-sales commented on Aug 29, 2026

    @os-sales
    CollaboratorAuthor

    ✅ ACCEPT — PR #6769, merged in c6732825d

    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.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatchedpriority:p2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions