From d94744927177a8bdbfbef468cd9e9b6995201f55 Mon Sep 17 00:00:00 2001 From: Pascal Date: Wed, 3 Jun 2026 20:08:47 +0000 Subject: [PATCH 1/2] fix: resolve lint and type errors to enable CI - Remove stale biome-ignore suppressions (rules no longer fire). - Refactor forEach callbacks that returned a value to satisfy useIterableCallbackReturn. - Replace assign-in-expression patterns in ceiling tool with explicit guards (noAssignInExpressions). - Move chimney panel useMemo/useCallback above the early return so hooks always run in the same order (useHookAtTopLevel). - Convert chained && check to optional chaining in dormer panel (useOptionalChain). - Exclude packages/mcp from biome scope; it has its own CI pipeline (mcp-ci.yml). - Add ^build to the turbo check-types task so consumers get the current built declarations of internal packages. --- apps/editor/lib/bootstrap.ts | 2 -- biome.jsonc | 3 +- .../core/src/store/actions/node-actions.ts | 4 ++- .../first-person/build-collider-world.ts | 4 ++- packages/nodes/src/ceiling/tool.tsx | 8 +++-- packages/nodes/src/chimney/panel.tsx | 30 +++++++++---------- packages/nodes/src/dormer/csg-geometry.ts | 3 -- packages/nodes/src/dormer/move-tool.tsx | 1 - .../src/dormer/panel-position-section.tsx | 1 - packages/nodes/src/dormer/panel.tsx | 2 +- packages/nodes/src/dormer/window-assembly.tsx | 2 -- .../renderers/parametric-node-renderer.tsx | 3 -- packages/viewer/src/r3f.d.ts | 3 -- turbo.json | 2 +- 14 files changed, 31 insertions(+), 37 deletions(-) diff --git a/apps/editor/lib/bootstrap.ts b/apps/editor/lib/bootstrap.ts index 0186914e5..10788771a 100644 --- a/apps/editor/lib/bootstrap.ts +++ b/apps/editor/lib/bootstrap.ts @@ -45,7 +45,6 @@ function loadBuiltinsSync(): void { if (isDev()) { const kinds = Array.from(nodeRegistry.entries(), ([k]) => k) if (typeof console !== 'undefined') { - // biome-ignore lint/suspicious/noConsole: dev-only verification log console.info( `[pascal:registry] loaded ${builtinPlugin.id} v${builtinPlugin.apiVersion} (${kinds.length} kinds: ${kinds.join(', ') || '∅'})`, ) @@ -74,7 +73,6 @@ export async function loadExternalPlugins(): Promise { await loadPlugin(plugin) } if (isDev() && externals.length > 0 && typeof console !== 'undefined') { - // biome-ignore lint/suspicious/noConsole: dev-only verification log console.info(`[pascal:registry] + ${externals.length} discovered plugin(s)`) } } diff --git a/biome.jsonc b/biome.jsonc index 9b347988e..9edc891f0 100644 --- a/biome.jsonc +++ b/biome.jsonc @@ -110,7 +110,8 @@ "!**/build", "!**/public", "!**/components/ui", - "!**/next-env.d.ts" + "!**/next-env.d.ts", + "!packages/mcp" ] }, "overrides": [ diff --git a/packages/core/src/store/actions/node-actions.ts b/packages/core/src/store/actions/node-actions.ts index 2c5768a36..ead76ad5f 100644 --- a/packages/core/src/store/actions/node-actions.ts +++ b/packages/core/src/store/actions/node-actions.ts @@ -416,7 +416,9 @@ export const applyNodeChangesAction = ( return { nodes: nextNodes, rootNodeIds: resolvedRootIds, collections: nextCollections } }) - nodesToMarkDirty.forEach((id) => get().markDirty(id)) + nodesToMarkDirty.forEach((id) => { + get().markDirty(id) + }) parentsToMarkDirty.forEach((id) => { get().markDirty(id) const parent = get().nodes[id] diff --git a/packages/editor/src/components/editor/first-person/build-collider-world.ts b/packages/editor/src/components/editor/first-person/build-collider-world.ts index 4366ffbf2..363735da4 100644 --- a/packages/editor/src/components/editor/first-person/build-collider-world.ts +++ b/packages/editor/src/components/editor/first-person/build-collider-world.ts @@ -294,7 +294,9 @@ export function buildFirstPersonColliderWorldFromRegistry(): FirstPersonCollider } const mergedGeometry = mergeGeometries(geometries, false) - geometries.forEach((geometry) => geometry.dispose()) + geometries.forEach((geometry) => { + geometry.dispose() + }) if (!mergedGeometry || mergedGeometry.getAttribute('position') == null) { mergedGeometry?.dispose() diff --git a/packages/nodes/src/ceiling/tool.tsx b/packages/nodes/src/ceiling/tool.tsx index 57f693490..1a889dfc4 100644 --- a/packages/nodes/src/ceiling/tool.tsx +++ b/packages/nodes/src/ceiling/tool.tsx @@ -192,8 +192,12 @@ export const CeilingTool: React.FC = () => { if (points.length === 0) { mainLineRef.current.visible = false closingLineRef.current.visible = false - groundMainLineRef.current && (groundMainLineRef.current.visible = false) - groundClosingLineRef.current && (groundClosingLineRef.current.visible = false) + if (groundMainLineRef.current) { + groundMainLineRef.current.visible = false + } + if (groundClosingLineRef.current) { + groundClosingLineRef.current.visible = false + } return } const ceilingY = levelY + CEILING_HEIGHT diff --git a/packages/nodes/src/chimney/panel.tsx b/packages/nodes/src/chimney/panel.tsx index ab2ddc9fd..7eb189c81 100644 --- a/packages/nodes/src/chimney/panel.tsx +++ b/packages/nodes/src/chimney/panel.tsx @@ -133,6 +133,21 @@ export default function ChimneyPanel() { } }, [selectedId, node, deleteNode, setSelection]) + // Match the current store node against the preset table so the + // segmented control highlights "the preset you'd land on if you + // applied X again". Compare against the store node, not the live- + // override-merged `node`, so the highlight is stable across slider + // drags. Null means the user has tweaked away from any preset; the + // segmented control will then render with no segment selected. + const activePreset = useMemo(() => detectActiveChimneyPreset(storeNode), [storeNode]) + const applyPreset = useCallback( + (key: ChimneyPresetKey) => { + commitProp(chimneyPresets[key] as Partial) + triggerSFX('sfx:item-pick') + }, + [commitProp], + ) + if (!(node && node.type === 'chimney' && selectedId)) return null const scenestate = useScene.getState() @@ -321,21 +336,6 @@ export default function ChimneyPanel() { commitProp({ rotation: newWorldRot - ancestorWorldY }) } - // Match the current store node against the preset table so the - // segmented control highlights "the preset you'd land on if you - // applied X again". Compare against the store node, not the live- - // override-merged `node`, so the highlight is stable across slider - // drags. Null means the user has tweaked away from any preset; the - // segmented control will then render with no segment selected. - const activePreset = useMemo(() => detectActiveChimneyPreset(storeNode), [storeNode]) - const applyPreset = useCallback( - (key: ChimneyPresetKey) => { - commitProp(chimneyPresets[key] as Partial) - triggerSFX('sfx:item-pick') - }, - [commitProp], - ) - return ( { }) } } - // biome-ignore lint/correctness/useExhaustiveDependencies: capture-on-mount; meta is intentionally not re-read on changes. }, [node.id, isNew]) const { activeBuildingId, segmentXform, hitLocal, ghostRotation } = useDormerPlacement({ diff --git a/packages/nodes/src/dormer/panel-position-section.tsx b/packages/nodes/src/dormer/panel-position-section.tsx index c36e06291..2a4fefe32 100644 --- a/packages/nodes/src/dormer/panel-position-section.tsx +++ b/packages/nodes/src/dormer/panel-position-section.tsx @@ -79,7 +79,6 @@ export function DormerPositionSection({ if (Number.isFinite(lo_x)) bounds = { minX: lo_x, maxX: hi_x, minZ: lo_z, maxZ: hi_z } } return { worldX, worldZ, worldRotation, bounds } - // biome-ignore lint/correctness/useExhaustiveDependencies: roofChildrenKey is the stable signature of `roof.children`; intentionally omitting `roof` (object identity) in favor of the joined ids. }, [selectedId, px, py, pz, nodeRotation, segmentId, roofChildrenKey]) const worldX_now = worldXform.worldX diff --git a/packages/nodes/src/dormer/panel.tsx b/packages/nodes/src/dormer/panel.tsx index be6d1676f..7eff4733b 100644 --- a/packages/nodes/src/dormer/panel.tsx +++ b/packages/nodes/src/dormer/panel.tsx @@ -107,7 +107,7 @@ export default function DormerPanel() { }, [node, selectedId, setMovingNode, setSelection]) const handleDuplicate = useCallback(() => { - if (!(node && node.roofSegmentId)) return + if (!node?.roofSegmentId) return triggerSFX('sfx:item-pick') // Deep clone and strip the id so the move tool's onClick branch // (`isNew || !node.id`) takes the "create fresh" path. Setting diff --git a/packages/nodes/src/dormer/window-assembly.tsx b/packages/nodes/src/dormer/window-assembly.tsx index 12ca12a6e..0fa016474 100644 --- a/packages/nodes/src/dormer/window-assembly.tsx +++ b/packages/nodes/src/dormer/window-assembly.tsx @@ -144,7 +144,6 @@ const DormerWindowAssembly = ({ {winGeo.glassPanes.map((pane, i) => ( { const ref = useRef(null!) const n = node as RenderableNode - // biome-ignore lint/suspicious/noExplicitAny: useNodeEvents is keyed by - // literal kind; the registry path passes a runtime kind union. Routing - // through the type cast is safer than widening the hook signature. const handlers = useNodeEvents(node as any, node.type as any) const liveTransform = useLiveTransforms((s) => s.get(node.id as AnyNodeId)) // Registry arrow handles (rotation gizmo, position-affecting patches) diff --git a/packages/viewer/src/r3f.d.ts b/packages/viewer/src/r3f.d.ts index 8e0796cfd..172e7de50 100644 --- a/packages/viewer/src/r3f.d.ts +++ b/packages/viewer/src/r3f.d.ts @@ -78,21 +78,18 @@ interface ThreeJSXElements { } declare module 'react' { - // biome-ignore lint/style/noNamespace: Required for JSX module augmentation namespace JSX { interface IntrinsicElements extends ThreeJSXElements {} } } declare module 'react/jsx-runtime' { - // biome-ignore lint/style/noNamespace: Required for JSX module augmentation namespace JSX { interface IntrinsicElements extends ThreeJSXElements {} } } declare module 'react/jsx-dev-runtime' { - // biome-ignore lint/style/noNamespace: Required for JSX module augmentation namespace JSX { interface IntrinsicElements extends ThreeJSXElements {} } diff --git a/turbo.json b/turbo.json index 256dbb71a..dd97f6521 100644 --- a/turbo.json +++ b/turbo.json @@ -23,7 +23,7 @@ "dependsOn": ["^lint"] }, "check-types": { - "dependsOn": ["^check-types"] + "dependsOn": ["^build", "^check-types"] }, "dev": { "dependsOn": ["^build"], From 3ef1f5f82e856973127eb7d95e8d6dddf8bfe9aa Mon Sep 17 00:00:00 2001 From: Pascal Date: Wed, 3 Jun 2026 20:08:52 +0000 Subject: [PATCH 2/2] ci: add lint and typecheck workflow on push/PR Runs bun run check and bun run check-types on push to main and on pull requests. Concurrency group cancels superseded runs. --- .github/workflows/ci.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..fdacf1586 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + quality: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: oven-sh/setup-bun@v2 + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Lint & format check + run: bun run check + + - name: Type check + run: bun run check-types