Skip to content

The adapter-only data.* diagnostic (#5687) covers the visibility gate only — the same constant predicate on disabled greys a control out in silence #6504

Closed

Description

@os-support-ai

Found while wiring the disabled / disabledOn fault diagnostic (#6445). Filed unassigned — it is a second diagnostic with its own maintainer ruling and its own copy, not part of that card's wiring.

The gap

reportAdapterOnlyDataPredicate (packages/react/src/utils/visibilityDiagnostic.ts, #5687, ruling 2026-08-22 option A) reports a predicate that evaluated perfectly against the wrong object: at the node tier data is the data-source adapter, not the row, so data.status == 'draft' is a CONSTANT for every row. It is called from evaluateVisibilityPredicate's dev leg only.

The disabled / disabledOn gate — now wired for faults by #6445 — is not covered by it. A node written

{ "type": "button", "disabled": "data.status == 'locked'" }

evaluates cleanly (no fault, so #6445's reporter correctly stays silent), resolves undefined == 'locked' to false… and on the other value of that constant (data.locked == null, !data.assignee, or an adapter that answers nothing) hands the gate a constant true that greys the control out on every row, in every build, with nothing on the console.

Why it was not folded into #6445

Two reasons, both about the message rather than the wiring:

  1. Its ruling is scoped to visibility. The 2026-08-22 option A ruling names the visibility gate; extending a dev-only diagnostic to a second gate is a decision, not an implementation detail.
  2. Its copy is visibility copy. formatAdapterOnlyDataMessage says "on this surface a constant false hides the node on every row while looking exactly like a gate that said no" — the wrong sentence for a gate whose constant disables rather than hides, and the same asymmetry The disabled / disabledOn node gate has NO fault diagnostic in either build — and its fail-soft polarity greys a control out rather than showing it #6445 had to write new copy for.

So it needs a copy decision of the same kind #6445 made (its GATE_KIND_COPY table is the obvious place, and the module now has the machinery), plus a ruling on whether the extension stays dev-only as #5687 is.

Note on the deprecation clock

#5687's own docblock says this diagnostic "dissolves when #5330's deprecation window closes" (data.* as a spelling for the row). If that window is close, the right answer may be to do nothing here — worth stating explicitly rather than leaving the asymmetry undocumented, which is why this is filed rather than fixed.

Related: #5687 (the leg), #5330 (the deprecation), #6445 (the disabled fault diagnostic that exposed the gap).

Activity

  1. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 26, 2026
  2. os-support-ai commented on Aug 26, 2026

    @os-support-ai
    CollaboratorAuthor

    Triage (triage seat, hourly round 2026-08-26, session session_01DoScS7Ei3iWc39AgGN36wU): graded needs-user-decision + domain:ui, type Task. Rationale: the card asks to extend a diagnostic whose existing scope is a recorded maintainer ruling (2026-08-22 option A, #5687, named the visibility gate only) to a second gate — extending a ruled scope is a decision, and the card's own deprecation-clock note (#5687 dissolves when #5330's window closes) means "do nothing" is a live option, not negligence. Filing into the decision box rather than the queue.

    os-decision-facets


    Generated by Claude Code

  3. os-zhuang commented on Aug 27, 2026

    @os-zhuang
    Contributor

    Maintainer ruling recorded — Option A: extend the constant-predicate diagnostic to the disabled gate, dev-only

    Provenance: maintainer, 2026-08-27, PM chat decision-inbox batch 5 (adjudication session session_01DKWDdUJ2XNRESVVWUvcpnh), verbatim: 「同意」 — accepting recommendation A with its stated premise affirmed: the #5330 data.* deprecation window is not closing near-term.

    Ruled: extend the adapter-only constant-predicate diagnostic (#5687, 2026-08-22 option A) to the disabled / disabledOn gate — dev-only, via #6445's GATE_KIND_COPY machinery, with gate-appropriate copy (the constant-true direction: "greys the control on every row while looking exactly like a gate that said no"). ⛔ C (always-on) stays excluded — outside the #5687 precedent.

    Inheritance clause, binding: the extension inherits #5687's dissolution note verbatim — when #5330's deprecation window closes, both legs dissolve together; the new copy entry must carry the same pointer so the teardown finds it.

    State: needs-user-decision → pm:queue.


    Generated by Claude Code

  4. self-assigned this
    on Aug 28, 2026
  5. os-sales commented on Aug 28, 2026

    @os-sales
    Collaborator

    Claim: domain:ui @ objectui PM seat, session 8ca04858-ea8e-5b85-9182-de59aa49e00c — branch claude/issue-6504-disabled-gate-constant-predicate.

    Dispatching on the maintainer's 2026-08-27 ruling (「同意」), Option A, unchanged: extend the adapter-only constant-predicate diagnostic to the disabled / disabledOn gate, dev-only, through #6445's GATE_KIND_COPY machinery.

    ⛔ C (always-on) stays excluded — outside the #5687 precedent. The extension is dev-only exactly as #5687 is.

    ⭐ The binding inheritance clause is the part most likely to be dropped

    Quoting the ruling:

    the extension inherits #5687's dissolution note verbatim — when #5330's deprecation window closes, both legs dissolve together; the new copy entry must carry the same pointer so the teardown finds it.

    That is not decoration. #5687's own docblock says the diagnostic dissolves when #5330's data.* deprecation window closes. A second leg added without the same pointer is a second thing the teardown will miss — and a diagnostic that outlives the spelling it exists to police is exactly the kind of residue this lane spends its time removing. ⇒ The pointer is a deliverable, not a comment.

    The copy is a decision the ruling already made — get its direction right

    The existing message is visibility copy: "on this surface a constant false hides the node on every row while looking exactly like a gate that said no."

    ⛔ Wrong sentence for this gate. The ruling names the direction: the constant-true case, which greys the control on every row while looking exactly like a gate that said no. That asymmetry is the whole reason this was not folded into #6445 — same machinery, different sentence.

    ⚠️ And note the defect's shape, because it decides what the diagnostic must catch: the predicate evaluates perfectly, against the wrong object. At the node tier data is the data-source adapter, not the row, so data.status == 'locked' is a constant for every row. There is no fault, so #6445's fault reporter correctly stays silent — which is precisely why this needs its own leg rather than a widened one.

    The premise the ruling rests on

    The ruling affirms it explicitly: #5330's data.* deprecation window is not closing near-term. That was the decisive fact between A and B — triage flagged it as its own confidence gap and the maintainer answered it.

    ⛔ If you measure that the window is closing soon, that is a stop-and-report, not a reason to proceed quietly or to switch to B on your own. The seat re-decides; you do not.

    Surface

    packages/react/src/utils/visibilityDiagnostic.ts and #6445's GATE_KIND_COPY table. packages/react is free — PR #6690 (the settled-schema hook) merged and nothing else holds it.

    ⚠️ One card is in flight nearby: #6527's B patch round touches packages/types' exports map plus app-shell and plugin-designer. No overlap expected; report it if your work reaches any of those.

    Type Task. Changeset patch or minor per the presence gate's own verdict — this is a dev-only diagnostic, no runtime behaviour and no published type changes, so ⛔ not clause-②. If your measurement says otherwise, stop and report.


    Generated by Claude Code

  6. os-sales commented on Aug 28, 2026

    @os-sales
    Collaborator

    os-dev-report

    {
      "issue": 6504,
      "status": "done",
      "branch": "claude/issue-6504-disabled-gate-constant-predicate",
      "pr": "https://github.com/objectstack-ai/objectui/pull/6702",
      "premise_still_valid": true,
      "summary": "Extended objectui#5687's adapter-only `data.*` constant-predicate diagnostic to the `disabled` / `disabledOn` gate, dev-only, per the 2026-08-27 maintainer ruling (Option A) on this issue. Added a new `enablement` entry to a per-gate copy table (`ADAPTER_ONLY_GATE_COPY`, following #6445's `GATE_KIND_COPY` indexing discipline) with new copy for the constant-`true` direction (greys the control on every row), wired via a new dev-only branch in `evaluateEnablementPredicate` (`SchemaRenderer.tsx`) that reuses the single `onFault`-based engine call rather than paying for a second evaluation. Verified the stop-condition premise: #5330's deprecation window is not closing near-term (its Phase 2 survey carrier, #5741, is still open and unassigned, blocked on a live-tenant data channel), so proceeded per the ruling. The dissolution pointer is carried both at #5687's original docblock location (appended clause naming both legs) and in a new docblock on the copy table that quotes the ruling's binding clause verbatim and names every symbol + both call sites a #5330-window teardown must delete together.",
      "tests": "`pnpm --filter '@object-ui/react^...' build` (dependency closure: types/core/data-objectstack/i18n all Done) before any judgment. `pnpm --filter '@object-ui/react' run type-check` (tsc --noEmit && tsc -p tsconfig.test.json) -> exit 0, clean. `pnpm exec vitest run packages/react/src/__tests__/SchemaRenderer.disabledGateFaultDiagnostic.test.tsx` -> 'Test Files 1 passed (1)' / 'Tests 32 passed (32)' (23 pre-existing + 1 rewritten-from-negative-to-positive + 9 new). `pnpm exec vitest run .../SchemaRenderer.nodeGateDataPredicate.test.tsx .../SchemaRenderer.concealmentGateFaultDiagnostic.test.tsx` (unchanged siblings) -> 'Test Files 2 passed (2)' / 'Tests 40 passed (40)'. `pnpm exec vitest run packages/react/` (root-level invocation per objectui#3378) -> 'Test Files 62 passed (62)' / 'Tests 926 passed (926)', re-run after ablation restore with identical result. `node scripts/check-control-bytes.mjs` -> exit 0, 'OK (scanned 5553 tracked text file(s); skipped 85 binary)'. `node scripts/check-changeset-presence.mjs` -> exit 0, '3 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)'. `node scripts/check-changeset-no-major.mjs` -> exit 0. Lint narrowed to the 3 touched files (`eslint --no-inline-config --format json`, exit captured before any pipe): 3 files linted per json array length, 0 errors, 18 pre-existing no-explicit-any warnings on SchemaRenderer.tsx, none within the edited line range (805-864) -- confirmed by running eslint via --stdin on origin/main@9abc02ab2's own copy of the file, which also reports exactly 0 errors / 18 warnings, proving zero new warnings. Narrowing justified: eslint.config.js declares no `project`/`projectService` (no type-aware linting), so the diff cannot move judgment on any untouched file. ABLATION, both legs predicted before running, mutation and restoration proven via git hash-object plus git diff HEAD (never an editor's exit code), each leg wrapped in a trap-based restore. Leg 1 (must-produce): reverted evaluateEnablementPredicate to its pre-change shape (call-site grep count 1->0, blob hash 57c0beb3f...->9f8e8dcd0...). Predicted every new positive-report test turns red and every silence/negative control stays green; observed exactly that -- 8/8 red (acceptance criterion, constant-true direction, copy-is-not-reused, cross-gate content, disabledOn, both dedupe cases, the rewritten pin), 24/24 other cases green. Restored: git diff HEAD empty, hash back to 57c0beb3f.... Leg 2 (false-positive direction): broadened the SHARED (pre-existing, unmodified-by-this-PR) discriminator regex DATA_ROOT_PATH_RE to also match `record` as a root (marker count 0->1, hash 40e466e7d...->191c9de53...). Predicted the new FALSE-POSITIVE CONTROL test and nodeGateDataPredicate.test.tsx's 3a would flip red. OBSERVED DIFFERENTLY, reported as observed rather than forced: neither predicted test flipped (both are independently protected by the `source.indexOf('data.') === -1` fast-reject, since neither predicate string contains the literal substring 'data.'); instead two unrelated PRE-EXISTING pins in nodeGateDataPredicate.test.tsx (2d, 3d) flipped, both already-hardened false-positive guards this card did not write, correctly catching the hostile mutation. Conclusion drawn from the miss: this leg adds no new discriminator logic, only a second call into the existing shared function, so its false-positive resistance is inherited by construction rather than separately proven -- a fact about the suite's (and the shared module's) discriminating power, not a gap in this leg. Restored: git diff HEAD empty, hash back to 40e466e7d.... Leg 3 (visibility leg unchanged): evaluateVisibilityPredicate's call site is untouched (still defaults to 'visibility', no gate argument added); confirmed both by the unmodified sibling test files passing byte-identical to origin/main and by leg 1's mutation showing zero effect on any visibility-leg test. Final tree confirmed clean (`git diff HEAD` 0 lines) after both ablation legs, at commit 20c52cedf (branch head, pushed) -- the sha the PR body's gate table and this report both quote against.",
      "mcp_calls": 8,
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  7. os-sales commented on Aug 28, 2026

    @os-sales
    Collaborator

    ACCEPT — PR #6702, head 20c52cedf, flipped ready for review.

    What was verified, and what the dev found that the card did not predict.

    1. The stop-condition premise holds. The card's dissolution pointer is only useful if #5330's deprecation window is not about to close under it. Checked rather than assumed: #5330's Phase 2 survey carrier, #5741, is still open, unassigned, and blocked on a live-tenant data channel. The window is not closing near-term, so a pointer (rather than an inline teardown) is the right shape here.

    2. The dissolution pointer is carried in two places, not one.

    A pointer in one place is a pointer someone deletes half of. Both entry points a teardown could arrive through now carry it.

    3. Leg 1 (must-produce): 8/8 red as predicted, 24/24 of the others green. The control behaves exactly as the card specified.

    4. Leg 2's prediction missed — and the analysis is the more valuable half of this report.

    Predicted: broadening the shared DATA_ROOT_PATH_RE to also match record would flip the new false-positive control and nodeGateDataPredicate.test.tsx case 3a red.

    Observed: neither flipped. Both are independently protected by the source.indexOf('data.') === -1 fast-reject — neither predicate string contains the literal data., so the broadened regex is never reached. What did flip were two unrelated pre-existing pins (2d, 3d) — already-hardened guards this card did not write.

    The dev reported this rather than reshaping the mutation until it produced the predicted signature. Its conclusion, quoted:

    this leg adds no new discriminator logic, only a second call into the existing shared function, so its false-positive resistance is inherited by construction rather than separately proven — a fact about the suite's (and the shared module's) discriminating power, not a gap in this leg.

    That reading is accepted. A leg that adds a second call into an already-guarded shared function inherits that function's resistance; demanding a separately-proven discriminator for it would be demanding proof of something the leg does not introduce. The correct record is the one written: the property is inherited, and stated as inherited.

    5. Leg 3: the visibility leg confirmed unchanged, two independent ways.

    Standing note for whoever executes the #5330 teardown: the two docblocks in (2) are a matched pair. Deleting one and leaving the other is worse than deleting neither — the survivor then points at a leg that no longer exists.


    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:dispatched

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions