Skip to content

feat(#72): SPI v1 slice 1c-vi.a — Redux Toolkit substrate#114

Merged
mohanagy merged 1 commit into
mainfrom
feat/spi-v1-slice-1c-vi.a-redux
May 11, 2026
Merged

feat(#72): SPI v1 slice 1c-vi.a — Redux Toolkit substrate#114
mohanagy merged 1 commit into
mainfrom
feat/spi-v1-slice-1c-vi.a-redux

Conversation

@mohanagy
Copy link
Copy Markdown
Owner

@mohanagy mohanagy commented May 11, 2026

Fifth and final framework substrate. Completes the substrate-level coverage for the framework set the user's roadmap calls out (Express, Next.js, React Router, Redux, plus the pre-existing NestJS).

Patterns detected

Factory Role
`createSlice({...})` `redux_slice`
`configureStore({...})` `redux_store`
Legacy `createStore()` from `redux` `redux_store`
`createSelector(...)` / `createDraftSafeSelector(...)` `redux_selector`
`createAsyncThunk(...)` `redux_async_thunk`
`createApi(...)` (RTK Query) `redux_rtk_query_api`

Import-gated to: `@reduxjs/toolkit`, `@reduxjs/toolkit/query`, `@reduxjs/toolkit/query/react`, `redux`, `reselect` (selectors are routinely re-exported from RTK via reselect).

Aliased imports work — `import { createSlice as makeSlice } from '@reduxjs/toolkit'` then `makeSlice({...})` tags correctly.

Projector wiring

Role Framework Node kind
`redux_slice` `redux` `slice`
`redux_store` `redux` `store`
`redux_selector` / `redux_async_thunk` / `redux_rtk_query_api` `redux` `function`

The `slice` and `store` `node_kind` values already exist in the legacy extractor's taxonomy.

Out of scope (deferred)

  • Slice metadata extraction: slice name, reducer keys, auto-generated action creators (`framework_metadata` extension). Slice 1c-vi.b candidate.
  • Property-access factory variants: `api.injectEndpoints({...})` is a derivation off an already-tagged `createApi` result, not a new factory call. Today the derived variable is intentionally untagged.
  • `createReducer` (functional reducer composition) — non-essential, add when a real codebase asks.
  • `useSelector` / `useDispatch` hook detection — structurally non-essential; consumers can locate selectors via `framework_role`.

Test plan

  • `npm run typecheck` — clean
  • `npm run build` — clean
  • `npm run test:run` — 97 files / 1684 tests pass (13 new for Redux + 1671 pre-existing)
  • Tests cover: every factory function, reselect package support, aliased imports, negative cases (local same-named fn not tagged, unrelated initialisers not tagged, property-access derivations not tagged), end-to-end projection through to ExtractionNode shape.
  • CI must pass on Ubuntu/macOS/Windows matrix before merge.

Framework substrate progress

After this slice merges:

Framework Substrate slice Status
NestJS 3b base + 3b-ii (#98, #99) ✅ merged
Express 1c-ii.b/c/d/e/f (#107#111) ✅ merged
Next.js 1c-iv.a (#112) ✅ merged
React Router 1c-v.a (#113) ✅ merged
Redux Toolkit 1c-vi.a (this PR) ⏳ in review

Refs #72.

Summary by CodeRabbit

Release Notes

  • New Features
    • Added Redux Toolkit framework detection that automatically recognizes and analyzes Redux patterns in your codebase, including slices, stores, selectors, async thunks, and RTK Query APIs for enhanced code analysis support.

Review Change Stack

Adds Redux Toolkit substrate detection. Five new SpiFrameworkRole variants under the redux_ prefix:

* redux_slice — createSlice({...}) factory result

* redux_store — configureStore({...}) or legacy createStore() factory result

* redux_selector — createSelector(...) or createDraftSafeSelector(...) factory result

* redux_async_thunk — createAsyncThunk(...) factory result

* redux_rtk_query_api — createApi(...) factory result

Detector (src/pipeline/spi/framework-redux.ts):

* Imports are gated to the canonical module specifiers: @reduxjs/toolkit, @reduxjs/toolkit/query, @reduxjs/toolkit/query/react, redux (for legacy createStore), and reselect (createSelector is routinely re-exported from RTK via reselect).

* Walks top-level VariableStatements. For each initialiser that is a CallExpression whose callee identifier matches an imported factory name (including aliased imports like createSlice as makeSlice), the variable's symbol is tagged with the matching framework_role.

* Property-access factory variants (api.injectEndpoints) are intentionally NOT tagged in this slice — they're derivations off already-tagged factory results, not new factory calls. A future slice may extend.

Projector wiring (src/pipeline/spi/projector.ts):

* frameworkForRole maps redux_* prefix → 'redux'.

* nodeKindForRole: redux_slice → 'slice', redux_store → 'store', other redux_* → 'function' (the existing ExtractionNode node_kind taxonomy already supports 'slice' and 'store' from the legacy redux extractor).

Tests: 13 new in spi-framework-redux.test.ts. Cover every factory function (createSlice, configureStore, legacy createStore, createSelector, createDraftSafeSelector, createAsyncThunk, createApi), reselect package support, aliased imports, negative cases (local same-named function not tagged, unrelated initialisers not tagged, property-access derivations not tagged), end-to-end projection through to ExtractionNode shape (framework=redux, correct node_kind).

Out of scope (deferred):

* Slice metadata extraction: slice name, reducer keys, auto-generated action creators. Slice 1c-vi.b candidate.

* createReducer (functional reducer composition) — non-essential, add when a real codebase asks for it.

* useSelector / useDispatch hook detection — structural noise; consumers can locate selectors via framework_role instead.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 11, 2026

📝 Walkthrough

Walkthrough

This PR adds Redux Toolkit framework detection to the SPI pipeline. It extends the type system with five Redux-specific roles, implements a new detection module that identifies Redux factory API calls in TypeScript source files, integrates detection into the per-file type-checker pass, updates the projection layer to map Redux roles to extraction nodes, and validates the feature with comprehensive unit tests.

Changes

Redux Framework Detection Feature

Layer / File(s) Summary
Type Contract
src/pipeline/spi/types.ts
SpiFrameworkRole union type extended with redux_slice, redux_store, redux_selector, redux_async_thunk, and redux_rtk_query_api.
Detection Module
src/pipeline/spi/framework-redux.ts
New module implementing detectReduxFramework() that tags symbols with framework roles based on Redux Toolkit factory calls. Includes import collection logic, factory-to-role mapping, and symbol registry updates; explicitly excludes property-access call variants and requires at least one Redux module import.
Public API Export
src/pipeline/spi/index.ts
Re-exports detectReduxFramework and DetectReduxFrameworkContext from framework-redux module.
Build Pipeline Integration
src/pipeline/spi/build.ts
Imports detectReduxFramework and invokes it during per-file semantic analysis, passing source file, file ID, and symbol registry alongside existing framework detectors.
Projector Support
src/pipeline/spi/projector.ts
Updates frameworkForRole() to map redux_* roles to framework: 'redux' and nodeKindForRole() to assign legacy node kinds (slice, store, function) for specific Redux roles.
Unit Tests
tests/unit/spi-framework-redux.test.ts
Tests factory detection for createSlice, configureStore, selector factories, createAsyncThunk, and createApi with import aliasing; negative tests for unrelated initializers and RTK Query derivations; projector integration tests verifying extraction node metadata.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • mohanagy/graphify-ts#107: Adds a new SPI framework detector module and wires it into the same pipeline points (build.ts invocation, index re-exports, types.ts role unions, projector mappings).
  • mohanagy/graphify-ts#106: Modifies SPI framework-role propagation in projectSpiToExtraction, extending the same projector role mappings that this PR adds Redux roles to.
  • mohanagy/graphify-ts#98: Adds a different SPI framework-detection pass with the same code-level integration pattern (importing/invoking a detectFramework in build.ts and re-exporting in index.ts).

Poem

🐰 Hops through Redux with joy and cheer,
Slices and stores now crystal clear,
Factory calls tagged from root to tip,
State management mapped—what a trip!
Framework roles flowing, the graph takes flight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(#72): SPI v1 slice 1c-vi.a — Redux Toolkit substrate' clearly and specifically describes the main change: adding Redux Toolkit framework detection as the fifth substrate in the SPI v1 framework set.
Description check ✅ Passed The PR description comprehensively covers patterns detected, projector wiring, scope boundaries, and test verification. It includes a detailed test plan with checkboxes and status updates aligned to the template structure.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/spi-v1-slice-1c-vi.a-redux

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/pipeline/spi/framework-redux.ts`:
- Around line 89-100: collectBindings() currently treats type-only imports as
runtime factories; add guards to skip them by checking importClause.isTypeOnly
before proceeding with a Redux module import and checking element.isTypeOnly
inside the named.elements loop to avoid adding type-only specifiers to
bindings.factories; specifically, when you detect an import declaration for
REDUX_MODULE_SPECIFIERS, return early if stmt.importClause.isTypeOnly, and
inside the loop skip any element where element.isTypeOnly before mapping
element.name.text via FACTORY_TO_ROLE into bindings.factories.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f14ba2ea-dfed-4555-ab3f-07eb73765bd1

📥 Commits

Reviewing files that changed from the base of the PR and between 6c718fc and 23e3608.

📒 Files selected for processing (6)
  • src/pipeline/spi/build.ts
  • src/pipeline/spi/framework-redux.ts
  • src/pipeline/spi/index.ts
  • src/pipeline/spi/projector.ts
  • src/pipeline/spi/types.ts
  • tests/unit/spi-framework-redux.test.ts

Comment on lines +89 to +100
if (!ts.isImportDeclaration(stmt) || !stmt.importClause) continue
if (!ts.isStringLiteral(stmt.moduleSpecifier)) continue
if (!REDUX_MODULE_SPECIFIERS.has(stmt.moduleSpecifier.text)) continue

bindings.hasReduxImport = true
const named = stmt.importClause.namedBindings
if (!named || !ts.isNamedImports(named)) continue
for (const element of named.elements) {
const importedName = element.propertyName?.text ?? element.name.text
const role = FACTORY_TO_ROLE.get(importedName)
if (role) bindings.factories.set(element.name.text, role)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Confirm whether type-only guards exist in the detector.
rg -n "importClause\\.isTypeOnly|element\\.isTypeOnly" src/pipeline/spi/framework-redux.ts

# 2) Check whether tests already cover `import type` for Redux factories.
rg -n "import type|type createSlice|type configureStore|type createSelector" tests/unit/spi-framework-redux.test.ts

Repository: mohanagy/graphify-ts

Length of output: 46


🏁 Script executed:

# Check if the file exists and get repository structure
git ls-files | grep -E "(framework-redux|spi)" | head -20

Repository: mohanagy/graphify-ts

Length of output: 761


🏁 Script executed:

# Verify the file path and read the collectBindings function
if [ -f "src/pipeline/spi/framework-redux.ts" ]; then
  echo "=== File exists ===" 
  wc -l src/pipeline/spi/framework-redux.ts
  echo "=== Lines 80-110 (context around lines 89-100) ==="
  sed -n '80,110p' src/pipeline/spi/framework-redux.ts
else
  echo "File not found at src/pipeline/spi/framework-redux.ts"
  find . -name "*framework-redux*" -type f 2>/dev/null || echo "No matching files found"
fi

Repository: mohanagy/graphify-ts

Length of output: 1246


🏁 Script executed:

# Check test file for type-only import coverage
if [ -f "tests/unit/spi-framework-redux.test.ts" ]; then
  echo "=== Test file exists ==="
  wc -l tests/unit/spi-framework-redux.test.ts
  echo "=== Searching for type import tests ==="
  rg -n "import type|type.*createSlice|type.*configureStore" tests/unit/spi-framework-redux.test.ts --max-count=20
else
  echo "Test file check:"
  find . -name "*spi*framework*redux*test*" -type f 2>/dev/null || echo "No matching test files found"
fi

Repository: mohanagy/graphify-ts

Length of output: 261


Add type-only import guards in Redux factory detection.

The collectBindings() function lacks guards for type-only imports, risking false positives when import type { createSlice } coexists with a local const createSlice = .... Add checks on both importClause.isTypeOnly (line 89) and element.isTypeOnly (within the loop at line 96) to exclude type-only specifiers from the factory bindings map.

🔧 Proposed fix
 function collectBindings(sourceFile: ts.SourceFile): ReduxBindings {
   const bindings: ReduxBindings = {
     factories: new Map<string, SpiFrameworkRole>(),
     hasReduxImport: false,
   }
   for (const stmt of sourceFile.statements) {
     if (!ts.isImportDeclaration(stmt) || !stmt.importClause) continue
+    if (stmt.importClause.isTypeOnly) continue
     if (!ts.isStringLiteral(stmt.moduleSpecifier)) continue
     if (!REDUX_MODULE_SPECIFIERS.has(stmt.moduleSpecifier.text)) continue

     bindings.hasReduxImport = true
     const named = stmt.importClause.namedBindings
     if (!named || !ts.isNamedImports(named)) continue
     for (const element of named.elements) {
+      if (element.isTypeOnly) continue
       const importedName = element.propertyName?.text ?? element.name.text
       const role = FACTORY_TO_ROLE.get(importedName)
       if (role) bindings.factories.set(element.name.text, role)
     }
   }
   return bindings
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!ts.isImportDeclaration(stmt) || !stmt.importClause) continue
if (!ts.isStringLiteral(stmt.moduleSpecifier)) continue
if (!REDUX_MODULE_SPECIFIERS.has(stmt.moduleSpecifier.text)) continue
bindings.hasReduxImport = true
const named = stmt.importClause.namedBindings
if (!named || !ts.isNamedImports(named)) continue
for (const element of named.elements) {
const importedName = element.propertyName?.text ?? element.name.text
const role = FACTORY_TO_ROLE.get(importedName)
if (role) bindings.factories.set(element.name.text, role)
}
function collectBindings(sourceFile: ts.SourceFile): ReduxBindings {
const bindings: ReduxBindings = {
factories: new Map<string, SpiFrameworkRole>(),
hasReduxImport: false,
}
for (const stmt of sourceFile.statements) {
if (!ts.isImportDeclaration(stmt) || !stmt.importClause) continue
if (stmt.importClause.isTypeOnly) continue
if (!ts.isStringLiteral(stmt.moduleSpecifier)) continue
if (!REDUX_MODULE_SPECIFIERS.has(stmt.moduleSpecifier.text)) continue
bindings.hasReduxImport = true
const named = stmt.importClause.namedBindings
if (!named || !ts.isNamedImports(named)) continue
for (const element of named.elements) {
if (element.isTypeOnly) continue
const importedName = element.propertyName?.text ?? element.name.text
const role = FACTORY_TO_ROLE.get(importedName)
if (role) bindings.factories.set(element.name.text, role)
}
}
return bindings
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pipeline/spi/framework-redux.ts` around lines 89 - 100, collectBindings()
currently treats type-only imports as runtime factories; add guards to skip them
by checking importClause.isTypeOnly before proceeding with a Redux module import
and checking element.isTypeOnly inside the named.elements loop to avoid adding
type-only specifiers to bindings.factories; specifically, when you detect an
import declaration for REDUX_MODULE_SPECIFIERS, return early if
stmt.importClause.isTypeOnly, and inside the loop skip any element where
element.isTypeOnly before mapping element.name.text via FACTORY_TO_ROLE into
bindings.factories.

@mohanagy mohanagy merged commit eea4deb into main May 11, 2026
7 checks passed
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