Skip to content

feat(r3f): add material rendering diagnostics - #1633

Open
aidenybai wants to merge 7 commits into
mainfrom
agent/three-rubric-rules
Open

feat(r3f): add material rendering diagnostics#1633
aidenybai wants to merge 7 commits into
mainfrom
agent/three-rubric-rules

Conversation

@aidenybai

@aidenybai aidenybai commented Aug 12, 2026

Copy link
Copy Markdown
Member

Why

Three.js and React Three Fiber projects could pass static analysis while custom mapped geometry had no UVs, normal-mapped custom geometry had no normals, PBR materials had no lighting source, or strongly metallic materials had no environment reflections.

Before, these defects were left entirely to runtime grading. After, React Doctor reports them only when the local geometry/material/scene graph is closed and statically provable. Dynamic scenes, spreads, custom components, shader materials, hidden surfaces, externally mutable geometry, and ambiguous attachments abstain.

Three.js r183 added derivative-based flat-shading fallback for ordinary lit geometry without normals, so the normals rules deliberately target only active normalMap cases instead of producing obsolete missing-normal warnings.

What changed

  • Add paired Three.js/R3F diagnostics for missing UVs on mapped custom geometry.
  • Add paired diagnostics for normal-mapped custom geometry without normals.
  • Add paired closed-scene diagnostics for unlit PBR materials.
  • Add paired closed-scene diagnostics for strongly metallic materials without environment lighting.
  • Add provenance-aware analyzers for closed R3F canvases, rendered Three.js scenes, geometry attributes, material texture maps, and PBR lighting state.
  • Register all eight rules, add liveness fixtures and adversarial valid/invalid coverage, and document every grading-rubric row as static, runtime, prompt, or craft evidence.
  • Add six generic Three.js mutation-fuzz seeds covering closed valid geometry, intentional defects, interleaved attributes, PMREM environments, helper/class indirection with arrays and spreads, and dynamic property writes.
  • Add patch changesets for both published lint plugins. The final fuzz-only commit needs no additional changeset because @react-doctor/fuzz is private.

Eval results

Check Result
Exact Daytona parity 2,000/2,000 paired projects, 0 failures, +0 / -0, 1,177,984 unchanged diagnostics, 0 skipped
Performance parity 0 project regressions; aggregate within threshold
Permanent fuzz corpus 6 representative seeds with an exact diagnostic matrix; all 8 rules passed 1,000 strict, fire-required iterations
Local RDE sample 100 project roots completed; 0 diagnostics from the new rules
False positives 0 confirmed in parity, RDE, and fuzz validation

The exact parity run compared base 8dfb01306772760201e75ea1478368390eddf58f with detector head ef85e01bde055320957198c1951b605f93195b11. Current head 08eba2bdd1e30ac1739b7fa65a7fff8f6f0eac4c differs only by private fuzz seeds and their test; detector and runtime code are byte-identical to the parity candidate.

Test plan

  • nr test — 15/15 monorepo tasks passed
  • nr lint — passed
  • nr typecheck — 16/16 tasks passed
  • nr format:check — passed across 6,500 files
  • nr smoke:json-report — schema v3 full-mode smoke passed
  • nr -C packages/fuzz test — 216 passed, 884 intentionally skipped
  • FUZZ_STRICT=1 FUZZ_REQUIRE_FIRE=1 FUZZ_ITERATIONS=1000 nr fuzz — passed for each of the 8 new rules
  • nr -C packages/oxlint-plugin-react-doctor gen:check
  • npx react-doctor@latest --verbose --scope changed

Note

Medium Risk
Adds substantial new static analysis over Three.js/R3F scene and material graphs. Risk is mainly false positives or missed abstentions, mitigated by closed-graph gating and extensive fuzz/parity coverage.

Overview
Adds eight paired Three.js / R3F diagnostics for material and geometry setup defects that previously needed runtime grading: missing UVs on mapped custom geometry, normal-mapped custom geometry without normals, unlit PBR materials, and strongly metallic materials without environment lighting.

Rules fire only on closed, locally provable meshes/scenes/canvases. Spreads, custom components, shaders, hidden surfaces, dynamic property writes, and open graphs abstain. Normals rules target active normalMap only, matching Three.js r183’s flat-shading fallback.

Also adds shared closed-graph analyzers, registry/liveness wiring, fuzz corpus seeds, and a grading-rubric coverage doc that maps each probe to static vs runtime/craft evidence.

Reviewed by Cursor Bugbot for commit 08eba2b. Bugbot is set up for automated code reviews on this repo. Configure here.

@pkg-pr-new

pkg-pr-new Bot commented Aug 12, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/eslint-plugin-react-doctor@1633
npm i https://pkg.pr.new/oxlint-plugin-react-doctor@1633
npm i https://pkg.pr.new/react-doctor@1633

commit: 08eba2b

@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Interactive terminal E2E

Terminal Control verified the built CLI at 08eba2b in a real PTY:

  • selected a project interactively and observed Scanning... before the three-second Git delay completed
  • waited for the clean result and exercised the compact report
  • opened copy context and the GitHub Actions confirmation, then cancelled safely

Download the edited MP4 and PNG evidence

@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

React Doctor skipped this pull request — it changed no React files.

Reviewed by React Doctor for commit 08eba2b.

@aidenybai
aidenybai force-pushed the agent/three-rubric-rules branch from 4989434 to 08eba2b Compare August 12, 2026 08:49

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 08eba2b. Configure here.

THREE_LIGHT_CONSTRUCTOR_NAMES.has(constructorName) &&
(intensity === null || intensity > 0)
) {
analysis.hasLight = true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

R3F lights ignore intensity ambiguity

High Severity

analyzeClosedR3fCanvasLighting treats a missing intensity and a non-static intensity the same: both become null and still set hasLight. A Canvas with intensity={amount} is therefore marked lit and stays complete, so r3f-require-lighting-for-pbr abstains instead of failing closed-scene analysis. The paired Three.js helper correctly marks non-static intensity incomplete.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 08eba2b. Configure here.

const metalnessExpression = getStaticObjectPropertyValue(unwrappedParameters, "metalness");
metalness = metalnessExpression ? getStaticNumber(metalnessExpression, context.scopes) : null;
const opacityExpression = getStaticObjectPropertyValue(unwrappedParameters, "opacity");
opacity = opacityExpression ? getStaticNumber(opacityExpression, context.scopes) : null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dynamic opacity skips PBR abstention

Medium Severity

getStaticThreePbrMaterialLighting assigns non-static opacity to null without setting isComplete to false, both for constructor parameters and later writes. With transparent: true, isVisible stays true, so lighting and metal rules can flag fully transparent surfaces. The paired texture-property analyzer already abstains on the same shape.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 08eba2b. Configure here.

context,
);
if (material && (!material.isComplete || material.isVisible)) materials.push(material);
continue;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unparsed mesh materials stay complete

Medium Severity

When a closed-scene Mesh material argument is an array, call, or otherwise fails getStaticThreePbrMaterialLighting, the analyzer skips it and leaves isComplete true. Material arrays such as new Mesh(geo, [new MeshStandardMaterial()]) therefore never contribute PBR facts and never force abstention, so lighting and metal diagnostics miss provable defects.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 08eba2b. Configure here.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant