From dfe57921fe70dec8b5ca6ccc92cdf718c247a006 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 13 Aug 2026 04:53:36 +0000 Subject: [PATCH 01/24] feat(plugin): add Rstack Context workflows --- .claude-plugin/marketplace.json | 4 +- .claude-plugin/plugin.json | 3 +- .codex-plugin/plugin.json | 20 ++- .mcp.json | 12 ++ README.md | 27 ++++ package.json | 3 +- scripts/test-rstack-context-plugin.mjs | 164 ++++++++++++++++++++ skills-test/rstack-context/evals/evals.json | 83 ++++++++++ skills-test/rstack-context/report.md | 18 +++ skills/analyze-build/SKILL.md | 17 ++ skills/assess-change-impact/SKILL.md | 14 ++ skills/debug-dev-cycle/SKILL.md | 16 ++ skills/explain-dead-code/SKILL.md | 16 ++ skills/find-unused-code/SKILL.md | 16 ++ skills/review-context-change/SKILL.md | 16 ++ skills/rsdoctor-analysis/SKILL.md | 5 + 16 files changed, 423 insertions(+), 11 deletions(-) create mode 100644 .mcp.json create mode 100644 scripts/test-rstack-context-plugin.mjs create mode 100644 skills-test/rstack-context/evals/evals.json create mode 100644 skills-test/rstack-context/report.md create mode 100644 skills/analyze-build/SKILL.md create mode 100644 skills/assess-change-impact/SKILL.md create mode 100644 skills/debug-dev-cycle/SKILL.md create mode 100644 skills/explain-dead-code/SKILL.md create mode 100644 skills/find-unused-code/SKILL.md create mode 100644 skills/review-context-change/SKILL.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 3076f8c..1b72292 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1,14 +1,14 @@ { "$schema": "https://json.schemastore.org/claude-code-marketplace.json", "name": "rstack", - "description": "Agent Skills for debugging, tracing, upgrading, and analyzing Rstack projects.", + "description": "Agent Skills and project context tools for debugging, tracing, upgrading, and analyzing Rstack projects.", "owner": { "name": "RstackJS" }, "plugins": [ { "name": "rstack", - "description": "Agent Skills for debugging, tracing, upgrading, and analyzing Rstack projects.", + "description": "Agent Skills and project context tools for debugging, tracing, upgrading, and analyzing Rstack projects.", "version": "0.1.0", "source": "./", "author": { diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 556062c..2164c41 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,13 +1,14 @@ { "name": "rstack", "version": "0.1.0", - "description": "Agent Skills for debugging, tracing, upgrading, and analyzing Rstack projects.", + "description": "Agent Skills and project context tools for debugging, tracing, upgrading, and analyzing Rstack projects.", "author": { "name": "RstackJS" }, "homepage": "https://github.com/rstackjs/agent-skills", "repository": "https://github.com/rstackjs/agent-skills", "license": "MIT", + "mcpServers": "./.mcp.json", "keywords": [ "rspack", "rsbuild", diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index db20b50..9739b5e 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "rstack", "version": "0.1.0", - "description": "Agent Skills for debugging, tracing, upgrading, and analyzing Rstack projects.", + "description": "Agent Skills and project context tools for debugging, tracing, upgrading, and analyzing Rstack projects.", "author": { "name": "RstackJS", "url": "https://github.com/rstackjs" @@ -21,19 +21,25 @@ "skills" ], "skills": "./skills/", + "mcpServers": "./.mcp.json", "interface": { "displayName": "Rstack", - "shortDescription": "Debug, migrate, optimize, and document Rstack projects.", - "longDescription": "Use Rstack skills to configure, migrate, debug, trace, optimize, and document projects with Rspack, Rsbuild, Rslib, Rstest, Rspress, Rsdoctor, and Rslint, including Storybook integration with Rsbuild.", + "shortDescription": "Debug, analyze, migrate, and optimize Rstack projects.", + "longDescription": "Use Rstack skills and checkout-local project context to configure, migrate, debug, trace, optimize, and document projects with Rspack, Rsbuild, Rslib, Rstest, Rspress, Rsdoctor, and Rslint.", "developerName": "RstackJS", "category": "Developer Tools", - "capabilities": ["Interactive", "Read", "Write"], + "capabilities": [ + "Project context", + "Build analysis", + "Lint and test capture", + "Write" + ], "composerIcon": "./assets/rspack-logo.png", "logo": "./assets/rspack-logo.png", "defaultPrompt": [ - "Debug and optimize this Rspack build.", - "Migrate this webpack or Vite project to Rsbuild.", - "Create or improve the Rspress documentation for this project." + "Show the current Rstack build, lint, and test context.", + "Find artifact-scoped unused-code candidates.", + "Debug and optimize this Rspack build." ], "websiteURL": "https://github.com/rstackjs/agent-skills" } diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..3c84587 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,12 @@ +{ + "mcpServers": { + "rstack": { + "command": "node", + "args": [ + "--input-type=module", + "--eval", + "import { spawn } from 'node:child_process'; import { createRequire } from 'node:module'; import { dirname, join, resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; const require = createRequire(join(process.cwd(), 'package.json')); let packageJsonPath; try { packageJsonPath = require.resolve('rstack/package.json'); } catch (error) { if (error?.code !== 'MODULE_NOT_FOUND') throw error; } if (packageJsonPath) { const { bin } = require(packageJsonPath); const binPath = typeof bin === 'string' ? bin : (bin.rs ?? bin.rstack); if (!binPath) throw new Error('The workspace-local rstack package does not declare an rs binary.'); const cliPath = resolve(dirname(packageJsonPath), binPath); process.argv = [process.execPath, cliPath, 'mcp']; await import(pathToFileURL(cliPath).href); } else { try { const child = spawn('rs', ['mcp'], { stdio: 'inherit' }); const { code, signal } = await new Promise((resolve, reject) => { child.once('error', reject); child.once('exit', (code, signal) => resolve({ code, signal })); }); if (signal) process.kill(process.pid, signal); else process.exitCode = code ?? 1; } catch (error) { if (error?.code !== 'ENOENT') throw error; console.error('Unable to launch Rstack MCP server: install rstack in this workspace or put rs on PATH.'); process.exitCode = 1; } }" + ] + } + } +} diff --git a/README.md b/README.md index 8f77811..25ef452 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ A collection of Agent Skills for [Rstack](https://rspack.rs/guide/start/ecosyste ## Table of Contents - [Usage](#usage) +- [Rstack Context](#rstack-context) - [Rspack Skills](#rspack-skills) - [Rsbuild Skills](#rsbuild-skills) - [Rslib Skills](#rslib-skills) @@ -48,6 +49,32 @@ Install any skill with: npx skills add rstackjs/agent-skills --skill ``` +## Rstack Context + +The Codex and Claude Code plugin includes one local MCP server named `rstack`. It starts the +workspace-local `rstack` package's `rs mcp` command, falling back to an `rs` executable on `PATH`. +Install Rstack CLI in the project when you want build, lint, test, coverage, or Rsdoctor evidence: + +```bash +pnpm add -D rstack +``` + +The MCP process may start at a monorepo root. It discovers checkout-local contexts recorded by +Rstack commands and identifies packages, products, environments, targets, configs, and variants by +`contextId`; it does not treat the server's current working directory as the selected package. + +The plugin adds these context workflows: + +- `analyze-build`: summarize a matching Rsdoctor artifact with build context. +- `assess-change-impact`: trace artifact-scoped module dependents and affected chunks. +- `debug-dev-cycle`: inspect stored Rslint or Rstest failures, with consent-gated capture. +- `explain-dead-code`: explain one artifact module's roots, shipment, and retention. +- `find-unused-code`: prioritize artifact-scoped unreachable module candidates. +- `review-context-change`: compare compatible lint or test snapshots. + +The plugin contains agent guidance and the MCP launcher. The evidence engine itself is provided by +Rstack CLI, so installing this repository does not duplicate or independently version that runtime. + ## Rspack Skills ### rspack-best-practices diff --git a/package.json b/package.json index 73c6cff..9ab04c8 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "format": "prettier -w .", "lint": "rslint && prettier -c . && pnpm run check-spell", "lint:write": "rslint --fix && prettier -w .", - "prepare": "skills-package-manager install && simple-git-hooks && pnpm build" + "prepare": "skills-package-manager install && simple-git-hooks && pnpm build", + "test:plugin": "node scripts/test-rstack-context-plugin.mjs" }, "simple-git-hooks": { "pre-commit": "pnpm run lint:write" diff --git a/scripts/test-rstack-context-plugin.mjs b/scripts/test-rstack-context-plugin.mjs new file mode 100644 index 0000000..af149ef --- /dev/null +++ b/scripts/test-rstack-context-plugin.mjs @@ -0,0 +1,164 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { + chmod, + mkdtemp, + mkdir, + readFile, + rm, + writeFile, +} from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const repositoryRoot = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '..', +); +const skillNames = [ + 'analyze-build', + 'assess-change-impact', + 'debug-dev-cycle', + 'explain-dead-code', + 'find-unused-code', + 'review-context-change', +]; + +const readJson = async (relativePath) => + JSON.parse(await readFile(path.join(repositoryRoot, relativePath), 'utf8')); + +const runServer = (configuration, cwd, env = {}) => + spawnSync(configuration.command, configuration.args ?? [], { + cwd, + encoding: 'utf8', + env: { ...process.env, ...env }, + }); + +const testManifest = async () => { + const codex = await readJson('.codex-plugin/plugin.json'); + const claude = await readJson('.claude-plugin/plugin.json'); + const mcp = await readJson('.mcp.json'); + + assert.equal(codex.name, 'rstack'); + assert.equal(claude.name, 'rstack'); + assert.equal(codex.mcpServers, './.mcp.json'); + assert.ok( + mcp.mcpServers?.rstack, + 'the plugin must register one rstack MCP server', + ); + assert.match(codex.description, /context/i); + assert.match(claude.description, /context/i); +}; + +const testSkills = async () => { + for (const skillName of skillNames) { + const source = await readFile( + path.join(repositoryRoot, 'skills', skillName, 'SKILL.md'), + 'utf8', + ); + assert.match(source, new RegExp(`name: ${skillName}`)); + assert.match(source, /project_status/); + } + + const rsdoctor = await readFile( + path.join(repositoryRoot, 'skills/rsdoctor-analysis/SKILL.md'), + 'utf8', + ); + assert.match(rsdoctor, /Rstack Context/); + + const evals = await readJson('skills-test/rstack-context/evals/evals.json'); + assert.equal(evals.skill_name, 'rstack-context'); + assert.ok(evals.evals.length >= 6); +}; + +const testWorkspaceLocalLauncher = async () => { + const workspace = await mkdtemp( + path.join(os.tmpdir(), 'rstack-agent-skills-local-'), + ); + const recordPath = path.join(workspace, 'record.json'); + + try { + const packageRoot = path.join(workspace, 'node_modules/rstack'); + await mkdir(path.join(packageRoot, 'bin'), { recursive: true }); + await writeFile( + path.join(packageRoot, 'package.json'), + JSON.stringify({ + name: 'rstack', + type: 'module', + exports: { './package.json': './package.json' }, + bin: { rs: './bin/rs.js' }, + }), + ); + await writeFile( + path.join(packageRoot, 'bin/rs.js'), + [ + "import { writeFileSync } from 'node:fs';", + 'writeFileSync(process.env.RSTACK_PLUGIN_TEST_RECORD, JSON.stringify({', + ' argv: process.argv.slice(2),', + ' cwd: process.cwd(),', + '}));', + ].join('\n'), + ); + + const configuration = (await readJson('.mcp.json')).mcpServers.rstack; + const result = runServer(configuration, workspace, { + RSTACK_PLUGIN_TEST_RECORD: recordPath, + }); + assert.equal(result.status, 0, result.stderr); + assert.deepEqual(await readJsonFrom(recordPath), { + argv: ['mcp'], + cwd: workspace, + }); + } finally { + await rm(workspace, { recursive: true, force: true }); + } +}; + +const readJsonFrom = async (filePath) => + JSON.parse(await readFile(filePath, 'utf8')); + +const testPathLauncher = async () => { + const workspace = await mkdtemp( + path.join(os.tmpdir(), 'rstack-agent-skills-path-'), + ); + const binRoot = path.join(workspace, 'bin'); + const recordPath = path.join(workspace, 'record.json'); + + try { + await mkdir(binRoot); + const executable = path.join(binRoot, 'rs'); + await writeFile( + executable, + [ + '#!/usr/bin/env node', + "const { writeFileSync } = require('node:fs');", + 'writeFileSync(process.env.RSTACK_PLUGIN_TEST_RECORD, JSON.stringify({', + ' argv: process.argv.slice(2),', + ' cwd: process.cwd(),', + '}));', + ].join('\n'), + ); + await chmod(executable, 0o755); + + const configuration = (await readJson('.mcp.json')).mcpServers.rstack; + const result = runServer(configuration, workspace, { + PATH: `${binRoot}${path.delimiter}${process.env.PATH}`, + RSTACK_PLUGIN_TEST_RECORD: recordPath, + }); + assert.equal(result.status, 0, result.stderr); + assert.deepEqual(await readJsonFrom(recordPath), { + argv: ['mcp'], + cwd: workspace, + }); + } finally { + await rm(workspace, { recursive: true, force: true }); + } +}; + +await testManifest(); +await testSkills(); +await testWorkspaceLocalLauncher(); +await testPathLauncher(); + +console.log('Rstack Context plugin contract passed.'); diff --git a/skills-test/rstack-context/evals/evals.json b/skills-test/rstack-context/evals/evals.json new file mode 100644 index 0000000..e323d20 --- /dev/null +++ b/skills-test/rstack-context/evals/evals.json @@ -0,0 +1,83 @@ +{ + "skill_name": "rstack-context", + "evals": [ + { + "id": 1, + "eval_name": "stored-test-failure", + "prompt": "A test failed in packages/ui during my last Rstack run. Diagnose it from the Rstack Context tools without starting a new run unless I approve one.", + "expected_output": "The agent selects the package's Rstest context, queries stored results, reports freshness and completeness separately, and surfaces the first actionable failure without executing tests.", + "files": [], + "assertions": [ + "Calls project_status before selecting evidence", + "Uses the matching packageRoot and contextId", + "Does not run test_snapshot without approval", + "Separates freshness from completeness" + ] + }, + { + "id": 2, + "eval_name": "monorepo-context-selection", + "prompt": "This monorepo has five Rslib packages and one Rsbuild app. Explain which build context contains packages/runtime and what evidence is current.", + "expected_output": "The agent uses project_status and context identities instead of assuming the MCP process CWD identifies the package.", + "files": [], + "assertions": [ + "Uses project_status", + "Selects by packageRoot/product/environment/target", + "Deduplicates repeated runs by contextId", + "Reports snapshot freshness" + ] + }, + { + "id": 3, + "eval_name": "unused-project-module", + "prompt": "Use this build's explicit Rsdoctor JSON to find one likely unused project module, but do not treat dependencies or an unimported source file as proof.", + "expected_output": "The agent queries product roots and bounded unused candidates, prioritizes project ownership, explains one candidate, and preserves artifact bounds.", + "files": [], + "assertions": [ + "Calls product_roots before unused_candidates", + "Uses the same explicit contextId and dataFile", + "Stops when ownership.project is zero", + "Calls the result an artifact-scoped candidate" + ] + }, + { + "id": 4, + "eval_name": "dead-code-evidence-axes", + "prompt": "Why is src/legacy.ts still present in this build, and is it safe to remove?", + "expected_output": "The agent explains artifact reachability and retention, optionally composes compatible code evidence, and never collapses coverage into dead-code proof.", + "files": [], + "assertions": [ + "Uses dead_code_explain", + "Reports root path or its absence", + "Keeps shipment, public contract, reachability, and execution independent", + "Does not claim deletion safety" + ] + }, + { + "id": 5, + "eval_name": "snapshot-regression", + "prompt": "Compare the last two lint snapshots for packages/core and tell me what regressed.", + "expected_output": "The agent selects a compatible pair in the correct order and leads with newly introduced diagnostics.", + "files": [], + "assertions": [ + "Calls project_status and snapshot_list", + "Uses older snapshot as left", + "Stops on incompatibility", + "Reports freshness before interpreting the diff" + ] + }, + { + "id": 6, + "eval_name": "missing-artifact-recovery", + "prompt": "Analyze tree shaking for my Rslib package, but there is no Rsdoctor artifact yet.", + "expected_output": "The agent identifies the library context, offers the minimal rs lib artifact capture, and asks before running or installing anything.", + "files": [], + "assertions": [ + "Selects a library product", + "Offers RSDOCTOR_OUTPUT=json with rs lib", + "Mentions the required Rsdoctor plugin", + "Asks before capture or installation" + ] + } + ] +} diff --git a/skills-test/rstack-context/report.md b/skills-test/rstack-context/report.md new file mode 100644 index 0000000..c3e29e5 --- /dev/null +++ b/skills-test/rstack-context/report.md @@ -0,0 +1,18 @@ +# Rstack Context skill evaluation + +## Setup + +- Date: 2026-08-13 +- Candidate: `codex/rstack-context-plugin` +- Executor runs: not yet recorded +- Deterministic validation: plugin launcher contract and skill schema validation + +## Current result + +The tracked evaluation set defines six representative and boundary workflows: stored failures, monorepo context selection, unused candidates, dead-code evidence axes, snapshot regression, and missing-artifact recovery. + +Matched Codex/Claude benchmark runs have not yet been recorded for this repository revision. No pass-rate, token, or timing claim is made. The initial deployment gate is the deterministic plugin contract plus skill validation; matched runs should replace this report before reliability claims are published. + +## Iteration decision + +Keep the workflows narrow and context-first. Revisit wording only when a matched run demonstrates a repeatable routing, evidence-boundary, or recovery failure. diff --git a/skills/analyze-build/SKILL.md b/skills/analyze-build/SKILL.md new file mode 100644 index 0000000..29233f7 --- /dev/null +++ b/skills/analyze-build/SKILL.md @@ -0,0 +1,17 @@ +--- +name: analyze-build +description: Use when summarizing Rstack build health, errors, chunks, packages, bundle opportunities, or build-wide tree-shaking evidence from an Rsdoctor artifact. +--- + +# Analyze an Rstack build + +1. Call `project_status` to establish available contexts and latest build observations. Analysis may still proceed from an explicit artifact when no context exists. +2. Obtain the explicit Rsdoctor `dataFile`. If it is missing, identify the product and offer the minimal capture command: `RSTACK_CONTEXT=1 RSDOCTOR=true RSDOCTOR_OUTPUT=json rs build` for an application or `rs lib` for a library. Ask before installing, building, or capturing. +3. When a context exists, call `product_roots` with the same `contextId` and `dataFile`. Use context-bound claims only when `artifactBinding` is `exact`; report `mismatch` or `explicit-unverified` as artifact-only evidence. +4. Call `rsdoctor_analyze` with the narrowest suitable tool: `build_summary`, `errors_list`, `chunks_list`, `bundle_optimize`, one `tree_shaking_*` view, or one `packages_*` view. +5. Treat omitted sections as unavailable. Reserve zero or healthy labels for evidence the tool actually returned. +6. Call `report_link` only when an optional navigable report would materially help. + +Never require a GUI or infer source execution or repository-wide dead code from an artifact. + +If Rstack Context is unavailable, use the `rsdoctor-analysis` skill with the explicit artifact instead. diff --git a/skills/assess-change-impact/SKILL.md b/skills/assess-change-impact/SKILL.md new file mode 100644 index 0000000..346308d --- /dev/null +++ b/skills/assess-change-impact/SKILL.md @@ -0,0 +1,14 @@ +--- +name: assess-change-impact +description: Use when estimating artifact-scoped dependents, affected product roots, or bundled chunks for one Rstack module. +--- + +# Assess artifact module impact + +1. Call `project_status` and select the context whose package root, product, environment, and target match the build. Deduplicate repeated runs by `contextId`. +2. Confirm the subject is an artifact module ID, exact path/name, or unique suffix. Route function, class, export, and other source-symbol questions to source analysis. +3. Obtain the explicit Rsdoctor `dataFile`. If missing, offer the matching `RSTACK_CONTEXT=1 RSDOCTOR=true RSDOCTOR_OUTPUT=json rs build` or `rs lib` capture and ask before running it. +4. Call `module_impact` with `direction: "dependents"` and an optional `maxDepth` from 1 to 16. +5. Report visited dependents, `totalVisited` versus `returned`, reached product roots by kind, distinct chunks, truncation, bounds, and provenance. + +Describe only the explicit artifact graph. Source-only, test-only, runtime-created, and external consumers may be unobserved. diff --git a/skills/debug-dev-cycle/SKILL.md b/skills/debug-dev-cycle/SKILL.md new file mode 100644 index 0000000..0788678 --- /dev/null +++ b/skills/debug-dev-cycle/SKILL.md @@ -0,0 +1,16 @@ +--- +name: debug-dev-cycle +description: Use when diagnosing one current Rstack Rslint or Rstest failure from stored evidence or an explicitly approved one-shot capture. +--- + +# Debug an Rstack development cycle + +1. Call `project_status` first. Match the package and producer by `context.packageRoot`, then retain its `contextId`. +2. Prefer stored evidence. Use `snapshot_list` for that context, then `diagnostics_list` or `test_results`; follow `nextCursor` only when more results are needed. +3. Report freshness (`fresh`, `stale`, `partial`, or `unknown`) independently from completeness, including changed paths and coverage bounds. +4. Ask before calling `lint_snapshot` or `test_snapshot`. For monorepos, pass checkout-relative `packageRoot`; pass `configPath` only for a nonstandard Rstack config. Never start watch mode through these tools. +5. Lead with the first actionable failure and briefly summarize the rest. +6. For a specific file, call `code_evidence` with the relevant test or lint snapshot ID. Keep diagnostics, test outcome, aggregate execution coverage, and build state independent. +7. Use `lint_fix_preview` only when already captured. Do not apply it. + +For related-test selection, recommend `rs test list --related --json`. diff --git a/skills/explain-dead-code/SKILL.md b/skills/explain-dead-code/SKILL.md new file mode 100644 index 0000000..34763c2 --- /dev/null +++ b/skills/explain-dead-code/SKILL.md @@ -0,0 +1,16 @@ +--- +name: explain-dead-code +description: Use when explaining why one Rstack artifact module is reachable, conservatively preserved, retained, shipped, or apparently unused. +--- + +# Explain an artifact module + +1. Call `project_status` and select the matching build context by package root, product, environment, and target. +2. Confirm the subject is an artifact module selector. Local symbols and exports require source analysis. +3. Obtain the explicit Rsdoctor `dataFile`; offer a consent-gated application or library capture if it is absent. +4. Call `dead_code_explain` with `contextId`, `dataFile`, and the module selector. +5. Lead with the returned classification: reachable, conservatively preserved, unreachable candidate, or insufficient evidence. +6. Show one shortest root-to-module path when present. Report production reachability, public contract, shipment, optimizer retention, truncation, bounds, provenance, and `artifactBinding`. +7. When test or runtime evidence helps, call `code_evidence` with the exact checkout-relative path and matching artifact selector. Keep every axis independent. + +Never infer local-symbol usage. Aggregate execution does not prove code is dead. diff --git a/skills/find-unused-code/SKILL.md b/skills/find-unused-code/SKILL.md new file mode 100644 index 0000000..e2e58dc --- /dev/null +++ b/skills/find-unused-code/SKILL.md @@ -0,0 +1,16 @@ +--- +name: find-unused-code +description: Use when listing or prioritizing artifact-scoped Rstack modules that are unreachable from observed product and contract roots. +--- + +# Find unused-code candidates + +1. Call `project_status` and select the matching build context. Deduplicate repeated runs by `contextId`. +2. Obtain the explicit Rsdoctor `dataFile`; offer the matching consent-gated application or library capture if absent. +3. Call `product_roots` with `contextId` and `dataFile`, then report production, published-contract, and conservative roots plus graph issues. +4. Call `unused_candidates` with the same inputs and an optional `limit` from 1 to 100. Prefer project-owned source modules. If `ownership.project` is zero, stop without paging and say the artifact has no project-owned candidate. +5. Follow `nextCursor` only for a requested exhaustive inventory. Reuse unchanged filters. +6. Call `dead_code_explain` for the strongest candidate. Add `code_evidence` when compatible test or execution evidence helps prioritize it. +7. Report root exhaustion, state axes, truncation, bounds, provenance, and artifact binding. + +Call every result an **artifact-scoped unreachable module candidate**. Completely unimported files are outside the artifact graph, and no candidate is deletion proof. diff --git a/skills/review-context-change/SKILL.md b/skills/review-context-change/SKILL.md new file mode 100644 index 0000000..ddf22ce --- /dev/null +++ b/skills/review-context-change/SKILL.md @@ -0,0 +1,16 @@ +--- +name: review-context-change +description: Use when comparing two compatible Rstack lint or test snapshots, including freshness, diagnostics, test outcomes, and stored lint fix previews. +--- + +# Review a context change + +1. Call `project_status`, match the package by `context.packageRoot`, and use `snapshot_list` with that `contextId`. +2. Select two completed snapshots for the same producer, context, package root, and config. The list is newest-first; pass the older ID as `leftSnapshotId`. +3. If a pair is missing, explain which consent-gated `lint_snapshot` or `test_snapshot` supplies it, including `packageRoot` and nonstandard `configPath`. +4. Call `snapshot_diff` with `diagnostics` for Rslint or `tests` for Rstest. Stop on incompatibility and report every reason. +5. Report both freshness values before the delta. Lead with new failures, then resolved items, then lower-severity or timing changes. +6. Call `code_evidence` for one changed file when exact-path diagnostics or aggregate execution evidence helps. Keep it separate from the snapshot delta. +7. Use `lint_fix_preview` only as review material and never apply it. + +Do not run a capture without approval. Recommend an explicit `rs lint`, `rs test`, or `rs test list --related` verification command. diff --git a/skills/rsdoctor-analysis/SKILL.md b/skills/rsdoctor-analysis/SKILL.md index a1f8b9e..e908d24 100644 --- a/skills/rsdoctor-analysis/SKILL.md +++ b/skills/rsdoctor-analysis/SKILL.md @@ -5,6 +5,11 @@ description: Use when analyzing Rspack/Webpack bundles from local `rsdoctor-data # Rsdoctor Analysis Assistant Skill +When the installed Rstack plugin exposes Rstack Context and the project has a matching context, +prefer its `analyze-build` workflow. It binds the explicit artifact to project/build observations and +uses the maintained in-process Rsdoctor tool catalog. Continue with this standalone workflow when +Rstack Context is unavailable or the user only has an explicit `rsdoctor-data.json`. + Use the globally installed `rsdoctor-agent` CLI from `@rsdoctor/agent-cli` only after a real `rsdoctor-data.json` path exists. Keep analysis read-only unless the user explicitly asks for install/config setup. Response order (required): High-Priority Issues -> Proposed Solutions -> Optional Reference-Chain Follow-up Choices -> Next Deep-Dive Issue Categories (Not commands). From 8839fd8e6f6665b450b7248d100c6d5219292dfc Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 13 Aug 2026 05:16:56 +0000 Subject: [PATCH 02/24] chore(plugin): version Rstack Context integration --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- scripts/test-rstack-context-plugin.mjs | 4 ++++ 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 1b72292..79d417d 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ { "name": "rstack", "description": "Agent Skills and project context tools for debugging, tracing, upgrading, and analyzing Rstack projects.", - "version": "0.1.0", + "version": "0.2.0", "source": "./", "author": { "name": "RstackJS" diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 2164c41..5c2e162 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "rstack", - "version": "0.1.0", + "version": "0.2.0", "description": "Agent Skills and project context tools for debugging, tracing, upgrading, and analyzing Rstack projects.", "author": { "name": "RstackJS" diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 9739b5e..c9e16ce 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "rstack", - "version": "0.1.0", + "version": "0.2.0", "description": "Agent Skills and project context tools for debugging, tracing, upgrading, and analyzing Rstack projects.", "author": { "name": "RstackJS", diff --git a/scripts/test-rstack-context-plugin.mjs b/scripts/test-rstack-context-plugin.mjs index af149ef..76255b6 100644 --- a/scripts/test-rstack-context-plugin.mjs +++ b/scripts/test-rstack-context-plugin.mjs @@ -38,10 +38,14 @@ const runServer = (configuration, cwd, env = {}) => const testManifest = async () => { const codex = await readJson('.codex-plugin/plugin.json'); const claude = await readJson('.claude-plugin/plugin.json'); + const claudeMarketplace = await readJson('.claude-plugin/marketplace.json'); const mcp = await readJson('.mcp.json'); assert.equal(codex.name, 'rstack'); assert.equal(claude.name, 'rstack'); + assert.equal(codex.version, '0.2.0'); + assert.equal(claude.version, codex.version); + assert.equal(claudeMarketplace.plugins[0].version, codex.version); assert.equal(codex.mcpServers, './.mcp.json'); assert.ok( mcp.mcpServers?.rstack, From b207fc1ef576d54e39027b7318492b7f7432e12b Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 13 Aug 2026 05:58:23 +0000 Subject: [PATCH 03/24] fix(plugin): resolve package-local Rstack --- .mcp.json | 2 +- scripts/test-rstack-context-plugin.mjs | 54 ++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/.mcp.json b/.mcp.json index 3c84587..dca4992 100644 --- a/.mcp.json +++ b/.mcp.json @@ -5,7 +5,7 @@ "args": [ "--input-type=module", "--eval", - "import { spawn } from 'node:child_process'; import { createRequire } from 'node:module'; import { dirname, join, resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; const require = createRequire(join(process.cwd(), 'package.json')); let packageJsonPath; try { packageJsonPath = require.resolve('rstack/package.json'); } catch (error) { if (error?.code !== 'MODULE_NOT_FOUND') throw error; } if (packageJsonPath) { const { bin } = require(packageJsonPath); const binPath = typeof bin === 'string' ? bin : (bin.rs ?? bin.rstack); if (!binPath) throw new Error('The workspace-local rstack package does not declare an rs binary.'); const cliPath = resolve(dirname(packageJsonPath), binPath); process.argv = [process.execPath, cliPath, 'mcp']; await import(pathToFileURL(cliPath).href); } else { try { const child = spawn('rs', ['mcp'], { stdio: 'inherit' }); const { code, signal } = await new Promise((resolve, reject) => { child.once('error', reject); child.once('exit', (code, signal) => resolve({ code, signal })); }); if (signal) process.kill(process.pid, signal); else process.exitCode = code ?? 1; } catch (error) { if (error?.code !== 'ENOENT') throw error; console.error('Unable to launch Rstack MCP server: install rstack in this workspace or put rs on PATH.'); process.exitCode = 1; } }" + "import { spawn } from 'node:child_process'; import { globSync, readFileSync } from 'node:fs'; import { createRequire } from 'node:module'; import { dirname, join, resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; const cwd = process.cwd(); const roots = [cwd]; const patterns = []; try { const rootPackage = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8')); const workspaces = Array.isArray(rootPackage.workspaces) ? rootPackage.workspaces : rootPackage.workspaces?.packages; if (Array.isArray(workspaces)) patterns.push(...workspaces); } catch (error) { if (error?.code !== 'ENOENT') throw error; } try { const lines = readFileSync(join(cwd, 'pnpm-workspace.yaml'), 'utf8').split(/\\r?\\n/u); let inPackages = false; for (const line of lines) { if (/^packages:\\s*$/u.test(line)) { inPackages = true; continue; } if (inPackages && /^\\S/u.test(line)) break; const match = inPackages ? /^\\s*-\\s*['\"]?([^'\"#]+?)['\"]?\\s*$/u.exec(line) : null; if (match) patterns.push(match[1]); } } catch (error) { if (error?.code !== 'ENOENT') throw error; } const includes = [...new Set(patterns.filter((pattern) => typeof pattern === 'string' && !pattern.startsWith('!')).map((pattern) => `${pattern.replace(/\\/$/u, '')}/package.json`))]; const excludes = patterns.filter((pattern) => typeof pattern === 'string' && pattern.startsWith('!')).map((pattern) => `${pattern.slice(1).replace(/\\/$/u, '')}/package.json`); if (includes.length > 0) for (const packageJson of globSync(includes, { cwd, exclude: [...excludes, '**/node_modules/**'] })) roots.push(dirname(resolve(cwd, packageJson))); let packageJsonPath; let require; for (const root of roots) { const candidate = createRequire(join(root, 'package.json')); try { packageJsonPath = candidate.resolve('rstack/package.json'); require = candidate; break; } catch (error) { if (error?.code !== 'MODULE_NOT_FOUND') throw error; } } if (packageJsonPath) { const { bin } = require(packageJsonPath); const binPath = typeof bin === 'string' ? bin : (bin.rs ?? bin.rstack); if (!binPath) throw new Error('The workspace-local rstack package does not declare an rs binary.'); const cliPath = resolve(dirname(packageJsonPath), binPath); process.argv = [process.execPath, cliPath, 'mcp']; await import(pathToFileURL(cliPath).href); } else { try { const child = spawn('rs', ['mcp'], { stdio: 'inherit' }); const { code, signal } = await new Promise((resolve, reject) => { child.once('error', reject); child.once('exit', (code, signal) => resolve({ code, signal })); }); if (signal) process.kill(process.pid, signal); else process.exitCode = code ?? 1; } catch (error) { if (error?.code !== 'ENOENT') throw error; console.error('Unable to launch Rstack MCP server: install rstack in this workspace or a declared workspace package, or put rs on PATH.'); process.exitCode = 1; } }" ] } } diff --git a/scripts/test-rstack-context-plugin.mjs b/scripts/test-rstack-context-plugin.mjs index 76255b6..8442d28 100644 --- a/scripts/test-rstack-context-plugin.mjs +++ b/scripts/test-rstack-context-plugin.mjs @@ -119,6 +119,59 @@ const testWorkspaceLocalLauncher = async () => { } }; +const testPackageLocalLauncher = async () => { + const workspace = await mkdtemp( + path.join(os.tmpdir(), 'rstack-agent-skills-package-'), + ); + const recordPath = path.join(workspace, 'record.json'); + + try { + await writeFile( + path.join(workspace, 'package.json'), + JSON.stringify({ private: true, workspaces: ['packages/*'] }), + ); + const packageRoot = path.join(workspace, 'packages/app'); + const rstackRoot = path.join(packageRoot, 'node_modules/rstack'); + await mkdir(path.join(rstackRoot, 'bin'), { recursive: true }); + await writeFile( + path.join(packageRoot, 'package.json'), + JSON.stringify({ name: '@fixture/app', private: true }), + ); + await writeFile( + path.join(rstackRoot, 'package.json'), + JSON.stringify({ + name: 'rstack', + type: 'module', + exports: { './package.json': './package.json' }, + bin: { rs: './bin/rs.js' }, + }), + ); + await writeFile( + path.join(rstackRoot, 'bin/rs.js'), + [ + "import { writeFileSync } from 'node:fs';", + 'writeFileSync(process.env.RSTACK_PLUGIN_TEST_RECORD, JSON.stringify({', + ' argv: process.argv.slice(2),', + ' cwd: process.cwd(),', + '}));', + ].join('\n'), + ); + + const configuration = (await readJson('.mcp.json')).mcpServers.rstack; + const result = runServer(configuration, workspace, { + PATH: path.dirname(process.execPath), + RSTACK_PLUGIN_TEST_RECORD: recordPath, + }); + assert.equal(result.status, 0, result.stderr); + assert.deepEqual(await readJsonFrom(recordPath), { + argv: ['mcp'], + cwd: workspace, + }); + } finally { + await rm(workspace, { recursive: true, force: true }); + } +}; + const readJsonFrom = async (filePath) => JSON.parse(await readFile(filePath, 'utf8')); @@ -163,6 +216,7 @@ const testPathLauncher = async () => { await testManifest(); await testSkills(); await testWorkspaceLocalLauncher(); +await testPackageLocalLauncher(); await testPathLauncher(); console.log('Rstack Context plugin contract passed.'); From af6ec09cf6b1f64b6ded4798ea9fcd3901650d5e Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 13 Aug 2026 06:32:54 +0000 Subject: [PATCH 04/24] fix(skill): gate Rsdoctor JSON capture --- scripts/test-rstack-context-plugin.mjs | 7 +++++++ skills-test/rstack-context/evals/evals.json | 9 +++++---- skills/analyze-build/SKILL.md | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/scripts/test-rstack-context-plugin.mjs b/scripts/test-rstack-context-plugin.mjs index 8442d28..e744915 100644 --- a/scripts/test-rstack-context-plugin.mjs +++ b/scripts/test-rstack-context-plugin.mjs @@ -56,6 +56,7 @@ const testManifest = async () => { }; const testSkills = async () => { + let analyzeBuild; for (const skillName of skillNames) { const source = await readFile( path.join(repositoryRoot, 'skills', skillName, 'SKILL.md'), @@ -63,8 +64,14 @@ const testSkills = async () => { ); assert.match(source, new RegExp(`name: ${skillName}`)); assert.match(source, /project_status/); + if (skillName === 'analyze-build') { + analyzeBuild = source; + } } + assert.match(analyzeBuild, /plugin version is at least `1\.5\.11`/); + assert.match(analyzeBuild, /output\.mode='brief'/); + const rsdoctor = await readFile( path.join(repositoryRoot, 'skills/rsdoctor-analysis/SKILL.md'), 'utf8', diff --git a/skills-test/rstack-context/evals/evals.json b/skills-test/rstack-context/evals/evals.json index e323d20..49df6f9 100644 --- a/skills-test/rstack-context/evals/evals.json +++ b/skills-test/rstack-context/evals/evals.json @@ -70,13 +70,14 @@ "id": 6, "eval_name": "missing-artifact-recovery", "prompt": "Analyze tree shaking for my Rslib package, but there is no Rsdoctor artifact yet.", - "expected_output": "The agent identifies the library context, offers the minimal rs lib artifact capture, and asks before running or installing anything.", + "expected_output": "The agent identifies the library context, checks the Rsdoctor plugin version, offers the compatible rs lib artifact capture, and asks before running, installing, or configuring anything.", "files": [], "assertions": [ "Selects a library product", - "Offers RSDOCTOR_OUTPUT=json with rs lib", - "Mentions the required Rsdoctor plugin", - "Asks before capture or installation" + "Uses RSDOCTOR_OUTPUT=json only for Rsdoctor plugin 1.5.11 or newer", + "Uses brief JSON plugin configuration when the plugin is missing, unknown, or older", + "Uses rs lib for the library product", + "Asks before capture, installation, or configuration" ] } ] diff --git a/skills/analyze-build/SKILL.md b/skills/analyze-build/SKILL.md index 29233f7..8a7986c 100644 --- a/skills/analyze-build/SKILL.md +++ b/skills/analyze-build/SKILL.md @@ -6,7 +6,7 @@ description: Use when summarizing Rstack build health, errors, chunks, packages, # Analyze an Rstack build 1. Call `project_status` to establish available contexts and latest build observations. Analysis may still proceed from an explicit artifact when no context exists. -2. Obtain the explicit Rsdoctor `dataFile`. If it is missing, identify the product and offer the minimal capture command: `RSTACK_CONTEXT=1 RSDOCTOR=true RSDOCTOR_OUTPUT=json rs build` for an application or `rs lib` for a library. Ask before installing, building, or capturing. +2. Obtain the explicit Rsdoctor `dataFile`. If it is missing, identify the product and inspect the matching `@rsdoctor/rspack-plugin` or `@rsdoctor/webpack-plugin` version before offering a capture. Use `RSDOCTOR_OUTPUT=json` only when the plugin version is at least `1.5.11`. For a missing, unknown, or older plugin, follow the `rsdoctor-analysis` Generation Gate: install/register the plugin when missing, configure `output.mode='brief'` with JSON output, and build with `RSDOCTOR=true` without `RSDOCTOR_OUTPUT`. Use `rs build` for an application or `rs lib` for a library, include `RSTACK_CONTEXT=1`, and ask before installing, configuring, building, or capturing. 3. When a context exists, call `product_roots` with the same `contextId` and `dataFile`. Use context-bound claims only when `artifactBinding` is `exact`; report `mismatch` or `explicit-unverified` as artifact-only evidence. 4. Call `rsdoctor_analyze` with the narrowest suitable tool: `build_summary`, `errors_list`, `chunks_list`, `bundle_optimize`, one `tree_shaking_*` view, or one `packages_*` view. 5. Treat omitted sections as unavailable. Reserve zero or healthy labels for evidence the tool actually returned. From 10e6fde0b5aeafc5f5812699e66bb019bb4019af Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 13 Aug 2026 06:42:26 +0000 Subject: [PATCH 05/24] fix(skill): match snapshot capture selections --- scripts/test-rstack-context-plugin.mjs | 5 +++++ skills-test/rstack-context/evals/evals.json | 1 + skills/review-context-change/SKILL.md | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/test-rstack-context-plugin.mjs b/scripts/test-rstack-context-plugin.mjs index e744915..daa234d 100644 --- a/scripts/test-rstack-context-plugin.mjs +++ b/scripts/test-rstack-context-plugin.mjs @@ -57,6 +57,7 @@ const testManifest = async () => { const testSkills = async () => { let analyzeBuild; + let reviewContextChange; for (const skillName of skillNames) { const source = await readFile( path.join(repositoryRoot, 'skills', skillName, 'SKILL.md'), @@ -67,10 +68,14 @@ const testSkills = async () => { if (skillName === 'analyze-build') { analyzeBuild = source; } + if (skillName === 'review-context-change') { + reviewContextChange = source; + } } assert.match(analyzeBuild, /plugin version is at least `1\.5\.11`/); assert.match(analyzeBuild, /output\.mode='brief'/); + assert.match(reviewContextChange, /capture selection/); const rsdoctor = await readFile( path.join(repositoryRoot, 'skills/rsdoctor-analysis/SKILL.md'), diff --git a/skills-test/rstack-context/evals/evals.json b/skills-test/rstack-context/evals/evals.json index 49df6f9..cb0a5f7 100644 --- a/skills-test/rstack-context/evals/evals.json +++ b/skills-test/rstack-context/evals/evals.json @@ -61,6 +61,7 @@ "files": [], "assertions": [ "Calls project_status and snapshot_list", + "Requires the same capture selection on both snapshots", "Uses older snapshot as left", "Stops on incompatibility", "Reports freshness before interpreting the diff" diff --git a/skills/review-context-change/SKILL.md b/skills/review-context-change/SKILL.md index ddf22ce..abc6468 100644 --- a/skills/review-context-change/SKILL.md +++ b/skills/review-context-change/SKILL.md @@ -6,7 +6,7 @@ description: Use when comparing two compatible Rstack lint or test snapshots, in # Review a context change 1. Call `project_status`, match the package by `context.packageRoot`, and use `snapshot_list` with that `contextId`. -2. Select two completed snapshots for the same producer, context, package root, and config. The list is newest-first; pass the older ID as `leftSnapshotId`. +2. Select two completed snapshots for the same producer, context, package root, config, and capture selection. The list is newest-first; pass the older ID as `leftSnapshotId`. 3. If a pair is missing, explain which consent-gated `lint_snapshot` or `test_snapshot` supplies it, including `packageRoot` and nonstandard `configPath`. 4. Call `snapshot_diff` with `diagnostics` for Rslint or `tests` for Rstest. Stop on incompatibility and report every reason. 5. Report both freshness values before the delta. Lead with new failures, then resolved items, then lower-severity or timing changes. From 41e062909dc57de404923ebf8f69ef40e01a3629 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 13 Aug 2026 06:49:14 +0000 Subject: [PATCH 06/24] fix(skill): gate impact artifact capture --- scripts/test-rstack-context-plugin.mjs | 6 ++++++ skills/assess-change-impact/SKILL.md | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/test-rstack-context-plugin.mjs b/scripts/test-rstack-context-plugin.mjs index daa234d..786888e 100644 --- a/scripts/test-rstack-context-plugin.mjs +++ b/scripts/test-rstack-context-plugin.mjs @@ -57,6 +57,7 @@ const testManifest = async () => { const testSkills = async () => { let analyzeBuild; + let assessChangeImpact; let reviewContextChange; for (const skillName of skillNames) { const source = await readFile( @@ -68,6 +69,9 @@ const testSkills = async () => { if (skillName === 'analyze-build') { analyzeBuild = source; } + if (skillName === 'assess-change-impact') { + assessChangeImpact = source; + } if (skillName === 'review-context-change') { reviewContextChange = source; } @@ -75,6 +79,8 @@ const testSkills = async () => { assert.match(analyzeBuild, /plugin version is at least `1\.5\.11`/); assert.match(analyzeBuild, /output\.mode='brief'/); + assert.match(assessChangeImpact, /plugin version is at least `1\.5\.11`/); + assert.match(assessChangeImpact, /output\.mode='brief'/); assert.match(reviewContextChange, /capture selection/); const rsdoctor = await readFile( diff --git a/skills/assess-change-impact/SKILL.md b/skills/assess-change-impact/SKILL.md index 346308d..be52a18 100644 --- a/skills/assess-change-impact/SKILL.md +++ b/skills/assess-change-impact/SKILL.md @@ -7,7 +7,7 @@ description: Use when estimating artifact-scoped dependents, affected product ro 1. Call `project_status` and select the context whose package root, product, environment, and target match the build. Deduplicate repeated runs by `contextId`. 2. Confirm the subject is an artifact module ID, exact path/name, or unique suffix. Route function, class, export, and other source-symbol questions to source analysis. -3. Obtain the explicit Rsdoctor `dataFile`. If missing, offer the matching `RSTACK_CONTEXT=1 RSDOCTOR=true RSDOCTOR_OUTPUT=json rs build` or `rs lib` capture and ask before running it. +3. Obtain the explicit Rsdoctor `dataFile`. If it is missing, inspect the matching Rsdoctor plugin version before offering a capture. Use `RSDOCTOR_OUTPUT=json` only when the plugin version is at least `1.5.11`. For a missing, unknown, or older plugin, follow the `rsdoctor-analysis` Generation Gate: install/register the plugin when missing, configure `output.mode='brief'` with JSON output, and build with `RSDOCTOR=true` without `RSDOCTOR_OUTPUT`. Use `rs build` for an application or `rs lib` for a library, include `RSTACK_CONTEXT=1`, and ask before installing, configuring, building, or capturing. 4. Call `module_impact` with `direction: "dependents"` and an optional `maxDepth` from 1 to 16. 5. Report visited dependents, `totalVisited` versus `returned`, reached product roots by kind, distinct chunks, truncation, bounds, and provenance. From f71f56cd838f7e2742e8cd001fb4cbc6b42d7877 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 13 Aug 2026 21:36:58 +0000 Subject: [PATCH 07/24] fix(plugin): align portable manifest --- plugin.json | 4 ++-- scripts/test-rstack-context-plugin.mjs | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/plugin.json b/plugin.json index 63d5487..5e091e7 100644 --- a/plugin.json +++ b/plugin.json @@ -1,8 +1,8 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "rstack", - "version": "0.1.0", - "description": "Agent Skills for debugging, tracing, upgrading, and analyzing Rstack projects.", + "version": "0.2.0", + "description": "Agent Skills and project context tools for debugging, tracing, upgrading, and analyzing Rstack projects.", "author": { "name": "RstackJS", "url": "https://github.com/rstackjs" diff --git a/scripts/test-rstack-context-plugin.mjs b/scripts/test-rstack-context-plugin.mjs index 786888e..07835ca 100644 --- a/scripts/test-rstack-context-plugin.mjs +++ b/scripts/test-rstack-context-plugin.mjs @@ -36,13 +36,16 @@ const runServer = (configuration, cwd, env = {}) => }); const testManifest = async () => { + const portable = await readJson('plugin.json'); const codex = await readJson('.codex-plugin/plugin.json'); const claude = await readJson('.claude-plugin/plugin.json'); const claudeMarketplace = await readJson('.claude-plugin/marketplace.json'); const mcp = await readJson('.mcp.json'); + assert.equal(portable.name, 'rstack'); assert.equal(codex.name, 'rstack'); assert.equal(claude.name, 'rstack'); + assert.equal(portable.version, codex.version); assert.equal(codex.version, '0.2.0'); assert.equal(claude.version, codex.version); assert.equal(claudeMarketplace.plugins[0].version, codex.version); @@ -51,6 +54,7 @@ const testManifest = async () => { mcp.mcpServers?.rstack, 'the plugin must register one rstack MCP server', ); + assert.match(portable.description, /context/i); assert.match(codex.description, /context/i); assert.match(claude.description, /context/i); }; From 32a728022e2f35b1cc93091e36910d664476d8bf Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Thu, 13 Aug 2026 22:42:34 +0000 Subject: [PATCH 08/24] fix(plugin): discover nested Rstack packages --- .mcp.json | 2 +- scripts/test-rstack-context-plugin.mjs | 54 ++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/.mcp.json b/.mcp.json index dca4992..f7ecec8 100644 --- a/.mcp.json +++ b/.mcp.json @@ -5,7 +5,7 @@ "args": [ "--input-type=module", "--eval", - "import { spawn } from 'node:child_process'; import { globSync, readFileSync } from 'node:fs'; import { createRequire } from 'node:module'; import { dirname, join, resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; const cwd = process.cwd(); const roots = [cwd]; const patterns = []; try { const rootPackage = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8')); const workspaces = Array.isArray(rootPackage.workspaces) ? rootPackage.workspaces : rootPackage.workspaces?.packages; if (Array.isArray(workspaces)) patterns.push(...workspaces); } catch (error) { if (error?.code !== 'ENOENT') throw error; } try { const lines = readFileSync(join(cwd, 'pnpm-workspace.yaml'), 'utf8').split(/\\r?\\n/u); let inPackages = false; for (const line of lines) { if (/^packages:\\s*$/u.test(line)) { inPackages = true; continue; } if (inPackages && /^\\S/u.test(line)) break; const match = inPackages ? /^\\s*-\\s*['\"]?([^'\"#]+?)['\"]?\\s*$/u.exec(line) : null; if (match) patterns.push(match[1]); } } catch (error) { if (error?.code !== 'ENOENT') throw error; } const includes = [...new Set(patterns.filter((pattern) => typeof pattern === 'string' && !pattern.startsWith('!')).map((pattern) => `${pattern.replace(/\\/$/u, '')}/package.json`))]; const excludes = patterns.filter((pattern) => typeof pattern === 'string' && pattern.startsWith('!')).map((pattern) => `${pattern.slice(1).replace(/\\/$/u, '')}/package.json`); if (includes.length > 0) for (const packageJson of globSync(includes, { cwd, exclude: [...excludes, '**/node_modules/**'] })) roots.push(dirname(resolve(cwd, packageJson))); let packageJsonPath; let require; for (const root of roots) { const candidate = createRequire(join(root, 'package.json')); try { packageJsonPath = candidate.resolve('rstack/package.json'); require = candidate; break; } catch (error) { if (error?.code !== 'MODULE_NOT_FOUND') throw error; } } if (packageJsonPath) { const { bin } = require(packageJsonPath); const binPath = typeof bin === 'string' ? bin : (bin.rs ?? bin.rstack); if (!binPath) throw new Error('The workspace-local rstack package does not declare an rs binary.'); const cliPath = resolve(dirname(packageJsonPath), binPath); process.argv = [process.execPath, cliPath, 'mcp']; await import(pathToFileURL(cliPath).href); } else { try { const child = spawn('rs', ['mcp'], { stdio: 'inherit' }); const { code, signal } = await new Promise((resolve, reject) => { child.once('error', reject); child.once('exit', (code, signal) => resolve({ code, signal })); }); if (signal) process.kill(process.pid, signal); else process.exitCode = code ?? 1; } catch (error) { if (error?.code !== 'ENOENT') throw error; console.error('Unable to launch Rstack MCP server: install rstack in this workspace or a declared workspace package, or put rs on PATH.'); process.exitCode = 1; } }" + "import { spawn } from 'node:child_process'; import { globSync, readFileSync } from 'node:fs'; import { createRequire } from 'node:module'; import { dirname, join, resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; const cwd = process.cwd(); const roots = [cwd]; const patterns = []; try { const rootPackage = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8')); const workspaces = Array.isArray(rootPackage.workspaces) ? rootPackage.workspaces : rootPackage.workspaces?.packages; if (Array.isArray(workspaces)) patterns.push(...workspaces); } catch (error) { if (error?.code !== 'ENOENT') throw error; } try { const lines = readFileSync(join(cwd, 'pnpm-workspace.yaml'), 'utf8').split(/\\r?\\n/u); let inPackages = false; for (const line of lines) { if (/^packages:\\s*$/u.test(line)) { inPackages = true; continue; } if (inPackages && /^\\S/u.test(line)) break; const match = inPackages ? /^\\s*-\\s*['\"]?([^'\"#]+?)['\"]?\\s*$/u.exec(line) : null; if (match) patterns.push(match[1]); } } catch (error) { if (error?.code !== 'ENOENT') throw error; } const includes = [...new Set(patterns.filter((pattern) => typeof pattern === 'string' && !pattern.startsWith('!')).map((pattern) => `${pattern.replace(/\\/$/u, '')}/package.json`))]; const excludes = patterns.filter((pattern) => typeof pattern === 'string' && pattern.startsWith('!')).map((pattern) => `${pattern.slice(1).replace(/\\/$/u, '')}/package.json`); if (includes.length > 0) for (const packageJson of globSync(includes, { cwd, exclude: [...excludes, '**/node_modules/**'] })) roots.push(dirname(resolve(cwd, packageJson))); for (const packageJson of globSync('*/package.json', { cwd, exclude: ['node_modules/**'] })) roots.push(dirname(resolve(cwd, packageJson))); let packageJsonPath; let require; for (const root of new Set(roots)) { const candidate = createRequire(join(root, 'package.json')); try { packageJsonPath = candidate.resolve('rstack/package.json'); require = candidate; break; } catch (error) { if (error?.code !== 'MODULE_NOT_FOUND') throw error; } } if (packageJsonPath) { const { bin } = require(packageJsonPath); const binPath = typeof bin === 'string' ? bin : (bin.rs ?? bin.rstack); if (!binPath) throw new Error('The workspace-local rstack package does not declare an rs binary.'); const cliPath = resolve(dirname(packageJsonPath), binPath); process.argv = [process.execPath, cliPath, 'mcp']; await import(pathToFileURL(cliPath).href); } else { try { const child = spawn('rs', ['mcp'], { stdio: 'inherit' }); const { code, signal } = await new Promise((resolve, reject) => { child.once('error', reject); child.once('exit', (code, signal) => resolve({ code, signal })); }); if (signal) process.kill(process.pid, signal); else process.exitCode = code ?? 1; } catch (error) { if (error?.code !== 'ENOENT') throw error; console.error('Unable to launch Rstack MCP server: install rstack in this repository, a declared workspace package, an immediate child package, or put rs on PATH.'); process.exitCode = 1; } }" ] } } diff --git a/scripts/test-rstack-context-plugin.mjs b/scripts/test-rstack-context-plugin.mjs index 07835ca..d840cba 100644 --- a/scripts/test-rstack-context-plugin.mjs +++ b/scripts/test-rstack-context-plugin.mjs @@ -194,6 +194,59 @@ const testPackageLocalLauncher = async () => { } }; +const testNestedPackageLauncher = async () => { + const workspace = await mkdtemp( + path.join(os.tmpdir(), 'rstack-agent-skills-nested-'), + ); + const recordPath = path.join(workspace, 'record.json'); + + try { + await writeFile( + path.join(workspace, 'package.json'), + JSON.stringify({ name: 'repository-root', private: true }), + ); + const packageRoot = path.join(workspace, 'frontend'); + const rstackRoot = path.join(packageRoot, 'node_modules/rstack'); + await mkdir(path.join(rstackRoot, 'bin'), { recursive: true }); + await writeFile( + path.join(packageRoot, 'package.json'), + JSON.stringify({ name: 'frontend', private: true }), + ); + await writeFile( + path.join(rstackRoot, 'package.json'), + JSON.stringify({ + name: 'rstack', + type: 'module', + exports: { './package.json': './package.json' }, + bin: { rs: './bin/rs.js' }, + }), + ); + await writeFile( + path.join(rstackRoot, 'bin/rs.js'), + [ + "import { writeFileSync } from 'node:fs';", + 'writeFileSync(process.env.RSTACK_PLUGIN_TEST_RECORD, JSON.stringify({', + ' argv: process.argv.slice(2),', + ' cwd: process.cwd(),', + '}));', + ].join('\n'), + ); + + const configuration = (await readJson('.mcp.json')).mcpServers.rstack; + const result = runServer(configuration, workspace, { + PATH: path.dirname(process.execPath), + RSTACK_PLUGIN_TEST_RECORD: recordPath, + }); + assert.equal(result.status, 0, result.stderr); + assert.deepEqual(await readJsonFrom(recordPath), { + argv: ['mcp'], + cwd: workspace, + }); + } finally { + await rm(workspace, { recursive: true, force: true }); + } +}; + const readJsonFrom = async (filePath) => JSON.parse(await readFile(filePath, 'utf8')); @@ -239,6 +292,7 @@ await testManifest(); await testSkills(); await testWorkspaceLocalLauncher(); await testPackageLocalLauncher(); +await testNestedPackageLauncher(); await testPathLauncher(); console.log('Rstack Context plugin contract passed.'); From 65552873b4bfd414c2810fb442cede5d97101409 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Fri, 14 Aug 2026 00:00:46 +0000 Subject: [PATCH 09/24] feat(plugin): guide related Rstest evidence --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- plugin.json | 2 +- scripts/test-rstack-context-plugin.mjs | 19 ++++++++++++++++++- skills/debug-dev-cycle/SKILL.md | 9 ++++++--- skills/explain-dead-code/SKILL.md | 5 ++++- skills/find-unused-code/SKILL.md | 7 +++++-- skills/review-context-change/SKILL.md | 2 +- 9 files changed, 38 insertions(+), 12 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 79d417d..c4407cb 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ { "name": "rstack", "description": "Agent Skills and project context tools for debugging, tracing, upgrading, and analyzing Rstack projects.", - "version": "0.2.0", + "version": "0.2.1", "source": "./", "author": { "name": "RstackJS" diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 5c2e162..b4b7082 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "rstack", - "version": "0.2.0", + "version": "0.2.1", "description": "Agent Skills and project context tools for debugging, tracing, upgrading, and analyzing Rstack projects.", "author": { "name": "RstackJS" diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index c9e16ce..60fa190 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "rstack", - "version": "0.2.0", + "version": "0.2.1", "description": "Agent Skills and project context tools for debugging, tracing, upgrading, and analyzing Rstack projects.", "author": { "name": "RstackJS", diff --git a/plugin.json b/plugin.json index 5e091e7..8c8affc 100644 --- a/plugin.json +++ b/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "rstack", - "version": "0.2.0", + "version": "0.2.1", "description": "Agent Skills and project context tools for debugging, tracing, upgrading, and analyzing Rstack projects.", "author": { "name": "RstackJS", diff --git a/scripts/test-rstack-context-plugin.mjs b/scripts/test-rstack-context-plugin.mjs index d840cba..d247d53 100644 --- a/scripts/test-rstack-context-plugin.mjs +++ b/scripts/test-rstack-context-plugin.mjs @@ -46,7 +46,7 @@ const testManifest = async () => { assert.equal(codex.name, 'rstack'); assert.equal(claude.name, 'rstack'); assert.equal(portable.version, codex.version); - assert.equal(codex.version, '0.2.0'); + assert.equal(codex.version, '0.2.1'); assert.equal(claude.version, codex.version); assert.equal(claudeMarketplace.plugins[0].version, codex.version); assert.equal(codex.mcpServers, './.mcp.json'); @@ -62,6 +62,9 @@ const testManifest = async () => { const testSkills = async () => { let analyzeBuild; let assessChangeImpact; + let debugDevCycle; + let explainDeadCode; + let findUnusedCode; let reviewContextChange; for (const skillName of skillNames) { const source = await readFile( @@ -76,6 +79,15 @@ const testSkills = async () => { if (skillName === 'assess-change-impact') { assessChangeImpact = source; } + if (skillName === 'debug-dev-cycle') { + debugDevCycle = source; + } + if (skillName === 'explain-dead-code') { + explainDeadCode = source; + } + if (skillName === 'find-unused-code') { + findUnusedCode = source; + } if (skillName === 'review-context-change') { reviewContextChange = source; } @@ -85,6 +97,11 @@ const testSkills = async () => { assert.match(analyzeBuild, /output\.mode='brief'/); assert.match(assessChangeImpact, /plugin version is at least `1\.5\.11`/); assert.match(assessChangeImpact, /output\.mode='brief'/); + for (const source of [debugDevCycle, explainDeadCode, findUnusedCode]) { + assert.match(source, /test_snapshot/); + assert.match(source, /statically related/i); + assert.match(source, /execution.*coverage/i); + } assert.match(reviewContextChange, /capture selection/); const rsdoctor = await readFile( diff --git a/skills/debug-dev-cycle/SKILL.md b/skills/debug-dev-cycle/SKILL.md index 0788678..6a9e9b1 100644 --- a/skills/debug-dev-cycle/SKILL.md +++ b/skills/debug-dev-cycle/SKILL.md @@ -10,7 +10,10 @@ description: Use when diagnosing one current Rstack Rslint or Rstest failure fro 3. Report freshness (`fresh`, `stale`, `partial`, or `unknown`) independently from completeness, including changed paths and coverage bounds. 4. Ask before calling `lint_snapshot` or `test_snapshot`. For monorepos, pass checkout-relative `packageRoot`; pass `configPath` only for a nonstandard Rstack config. Never start watch mode through these tools. 5. Lead with the first actionable failure and briefly summarize the rest. -6. For a specific file, call `code_evidence` with the relevant test or lint snapshot ID. Keep diagnostics, test outcome, aggregate execution coverage, and build state independent. -7. Use `lint_fix_preview` only when already captured. Do not apply it. +6. For a specific source file, an approved `test_snapshot` with `related: [path]` asks Rstest to select and run only statically related tests. Use one source per capture so `code_evidence.testRelation` is attributable to that source. +7. Call `code_evidence` with the relevant test or lint snapshot ID. Keep static test relation, exact-path test outcome, aggregate execution coverage, diagnostics, and build state independent. +8. Use `lint_fix_preview` only when already captured. Do not apply it. -For related-test selection, recommend `rs test list --related --json`. +Use `rs test list --related --json` when the user wants listing without an MCP capture or test execution. + +Use only producers configured for the selected package. If Rstest or Rslint is absent, report that axis as unavailable; do not install or configure it as part of diagnosis. diff --git a/skills/explain-dead-code/SKILL.md b/skills/explain-dead-code/SKILL.md index 34763c2..14672c2 100644 --- a/skills/explain-dead-code/SKILL.md +++ b/skills/explain-dead-code/SKILL.md @@ -11,6 +11,9 @@ description: Use when explaining why one Rstack artifact module is reachable, co 4. Call `dead_code_explain` with `contextId`, `dataFile`, and the module selector. 5. Lead with the returned classification: reachable, conservatively preserved, unreachable candidate, or insufficient evidence. 6. Show one shortest root-to-module path when present. Report production reachability, public contract, shipment, optimizer retention, truncation, bounds, provenance, and `artifactBinding`. -7. When test or runtime evidence helps, call `code_evidence` with the exact checkout-relative path and matching artifact selector. Keep every axis independent. +7. When test or runtime evidence helps, call `code_evidence` with the exact checkout-relative path and matching artifact selector. If no relation was captured and the user approves running tests, call `test_snapshot` with `related: [path]` for that one source, then query its snapshot ID. +8. Keep statically related tests, exact-path test outcomes, and aggregate execution coverage independent. A source can be production-reachable but unobserved in one test run, or test-related without being executed. Never infer local-symbol usage. Aggregate execution does not prove code is dead. + +Rstest, Rslint, coverage, and Rsdoctor observations are independent optional evidence. A build-only or library-only repository can still answer artifact questions; report missing axes as unavailable without requiring full-stack adoption. diff --git a/skills/find-unused-code/SKILL.md b/skills/find-unused-code/SKILL.md index e2e58dc..61dbaa6 100644 --- a/skills/find-unused-code/SKILL.md +++ b/skills/find-unused-code/SKILL.md @@ -10,7 +10,10 @@ description: Use when listing or prioritizing artifact-scoped Rstack modules tha 3. Call `product_roots` with `contextId` and `dataFile`, then report production, published-contract, and conservative roots plus graph issues. 4. Call `unused_candidates` with the same inputs and an optional `limit` from 1 to 100. Prefer project-owned source modules. If `ownership.project` is zero, stop without paging and say the artifact has no project-owned candidate. 5. Follow `nextCursor` only for a requested exhaustive inventory. Reuse unchanged filters. -6. Call `dead_code_explain` for the strongest candidate. Add `code_evidence` when compatible test or execution evidence helps prioritize it. -7. Report root exhaustion, state axes, truncation, bounds, provenance, and artifact binding. +6. Call `dead_code_explain` for the strongest candidate. Add `code_evidence` when compatible test or execution evidence helps prioritize it. If no relation was captured and the user approves running tests, call `test_snapshot` with `related: [path]` for that one source, then reuse its snapshot ID. +7. Keep statically related tests, exact-path test outcomes, and aggregate execution coverage independent. `unrelated` is meaningful only for an isolated one-source relation capture; it is still not deletion proof. +8. Report root exhaustion, state axes, truncation, bounds, provenance, and artifact binding. Call every result an **artifact-scoped unreachable module candidate**. Completely unimported files are outside the artifact graph, and no candidate is deletion proof. + +Rstest, Rslint, coverage, and Rsdoctor observations are independent optional evidence. Do not install, configure, or run a missing producer just to fill an axis; report it as unavailable and continue with the evidence that exists. diff --git a/skills/review-context-change/SKILL.md b/skills/review-context-change/SKILL.md index abc6468..6a048ed 100644 --- a/skills/review-context-change/SKILL.md +++ b/skills/review-context-change/SKILL.md @@ -10,7 +10,7 @@ description: Use when comparing two compatible Rstack lint or test snapshots, in 3. If a pair is missing, explain which consent-gated `lint_snapshot` or `test_snapshot` supplies it, including `packageRoot` and nonstandard `configPath`. 4. Call `snapshot_diff` with `diagnostics` for Rslint or `tests` for Rstest. Stop on incompatibility and report every reason. 5. Report both freshness values before the delta. Lead with new failures, then resolved items, then lower-severity or timing changes. -6. Call `code_evidence` for one changed file when exact-path diagnostics or aggregate execution evidence helps. Keep it separate from the snapshot delta. +6. Call `code_evidence` for one changed file when exact-path diagnostics, statically related tests, or aggregate execution evidence helps. Keep every axis separate from the snapshot delta. 7. Use `lint_fix_preview` only as review material and never apply it. Do not run a capture without approval. Recommend an explicit `rs lint`, `rs test`, or `rs test list --related` verification command. From 7aa40015f89286c12dcdc44a8551013cc619634f Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Fri, 14 Aug 2026 01:19:28 +0000 Subject: [PATCH 10/24] fix(plugin): parse commented pnpm workspaces --- .mcp.json | 2 +- scripts/test-rstack-context-plugin.mjs | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.mcp.json b/.mcp.json index f7ecec8..30c6274 100644 --- a/.mcp.json +++ b/.mcp.json @@ -5,7 +5,7 @@ "args": [ "--input-type=module", "--eval", - "import { spawn } from 'node:child_process'; import { globSync, readFileSync } from 'node:fs'; import { createRequire } from 'node:module'; import { dirname, join, resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; const cwd = process.cwd(); const roots = [cwd]; const patterns = []; try { const rootPackage = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8')); const workspaces = Array.isArray(rootPackage.workspaces) ? rootPackage.workspaces : rootPackage.workspaces?.packages; if (Array.isArray(workspaces)) patterns.push(...workspaces); } catch (error) { if (error?.code !== 'ENOENT') throw error; } try { const lines = readFileSync(join(cwd, 'pnpm-workspace.yaml'), 'utf8').split(/\\r?\\n/u); let inPackages = false; for (const line of lines) { if (/^packages:\\s*$/u.test(line)) { inPackages = true; continue; } if (inPackages && /^\\S/u.test(line)) break; const match = inPackages ? /^\\s*-\\s*['\"]?([^'\"#]+?)['\"]?\\s*$/u.exec(line) : null; if (match) patterns.push(match[1]); } } catch (error) { if (error?.code !== 'ENOENT') throw error; } const includes = [...new Set(patterns.filter((pattern) => typeof pattern === 'string' && !pattern.startsWith('!')).map((pattern) => `${pattern.replace(/\\/$/u, '')}/package.json`))]; const excludes = patterns.filter((pattern) => typeof pattern === 'string' && pattern.startsWith('!')).map((pattern) => `${pattern.slice(1).replace(/\\/$/u, '')}/package.json`); if (includes.length > 0) for (const packageJson of globSync(includes, { cwd, exclude: [...excludes, '**/node_modules/**'] })) roots.push(dirname(resolve(cwd, packageJson))); for (const packageJson of globSync('*/package.json', { cwd, exclude: ['node_modules/**'] })) roots.push(dirname(resolve(cwd, packageJson))); let packageJsonPath; let require; for (const root of new Set(roots)) { const candidate = createRequire(join(root, 'package.json')); try { packageJsonPath = candidate.resolve('rstack/package.json'); require = candidate; break; } catch (error) { if (error?.code !== 'MODULE_NOT_FOUND') throw error; } } if (packageJsonPath) { const { bin } = require(packageJsonPath); const binPath = typeof bin === 'string' ? bin : (bin.rs ?? bin.rstack); if (!binPath) throw new Error('The workspace-local rstack package does not declare an rs binary.'); const cliPath = resolve(dirname(packageJsonPath), binPath); process.argv = [process.execPath, cliPath, 'mcp']; await import(pathToFileURL(cliPath).href); } else { try { const child = spawn('rs', ['mcp'], { stdio: 'inherit' }); const { code, signal } = await new Promise((resolve, reject) => { child.once('error', reject); child.once('exit', (code, signal) => resolve({ code, signal })); }); if (signal) process.kill(process.pid, signal); else process.exitCode = code ?? 1; } catch (error) { if (error?.code !== 'ENOENT') throw error; console.error('Unable to launch Rstack MCP server: install rstack in this repository, a declared workspace package, an immediate child package, or put rs on PATH.'); process.exitCode = 1; } }" + "import { spawn } from 'node:child_process'; import { globSync, readFileSync } from 'node:fs'; import { createRequire } from 'node:module'; import { dirname, join, resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; const cwd = process.cwd(); const roots = [cwd]; const patterns = []; try { const rootPackage = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8')); const workspaces = Array.isArray(rootPackage.workspaces) ? rootPackage.workspaces : rootPackage.workspaces?.packages; if (Array.isArray(workspaces)) patterns.push(...workspaces); } catch (error) { if (error?.code !== 'ENOENT') throw error; } try { const lines = readFileSync(join(cwd, 'pnpm-workspace.yaml'), 'utf8').split(/\\r?\\n/u); let inPackages = false; for (const line of lines) { if (/^packages:\\s*$/u.test(line)) { inPackages = true; continue; } if (inPackages && /^\\S/u.test(line)) break; const match = inPackages ? /^\\s*-\\s*(?:['\"]([^'\"]+)['\"]|([^#]+?))(?:\\s+#.*)?\\s*$/u.exec(line) : null; if (match) patterns.push((match[1] ?? match[2]).trim()); } } catch (error) { if (error?.code !== 'ENOENT') throw error; } const includes = [...new Set(patterns.filter((pattern) => typeof pattern === 'string' && !pattern.startsWith('!')).map((pattern) => `${pattern.replace(/\\/$/u, '')}/package.json`))]; const excludes = patterns.filter((pattern) => typeof pattern === 'string' && pattern.startsWith('!')).map((pattern) => `${pattern.slice(1).replace(/\\/$/u, '')}/package.json`); if (includes.length > 0) for (const packageJson of globSync(includes, { cwd, exclude: [...excludes, '**/node_modules/**'] })) roots.push(dirname(resolve(cwd, packageJson))); for (const packageJson of globSync('*/package.json', { cwd, exclude: ['node_modules/**'] })) roots.push(dirname(resolve(cwd, packageJson))); let packageJsonPath; let require; for (const root of new Set(roots)) { const candidate = createRequire(join(root, 'package.json')); try { packageJsonPath = candidate.resolve('rstack/package.json'); require = candidate; break; } catch (error) { if (error?.code !== 'MODULE_NOT_FOUND') throw error; } } if (packageJsonPath) { const { bin } = require(packageJsonPath); const binPath = typeof bin === 'string' ? bin : (bin.rs ?? bin.rstack); if (!binPath) throw new Error('The workspace-local rstack package does not declare an rs binary.'); const cliPath = resolve(dirname(packageJsonPath), binPath); process.argv = [process.execPath, cliPath, 'mcp']; await import(pathToFileURL(cliPath).href); } else { try { const child = spawn('rs', ['mcp'], { stdio: 'inherit' }); const { code, signal } = await new Promise((resolve, reject) => { child.once('error', reject); child.once('exit', (code, signal) => resolve({ code, signal })); }); if (signal) process.kill(process.pid, signal); else process.exitCode = code ?? 1; } catch (error) { if (error?.code !== 'ENOENT') throw error; console.error('Unable to launch Rstack MCP server: install rstack in this repository, a declared workspace package, an immediate child package, or put rs on PATH.'); process.exitCode = 1; } }" ] } } diff --git a/scripts/test-rstack-context-plugin.mjs b/scripts/test-rstack-context-plugin.mjs index d247d53..b4c887b 100644 --- a/scripts/test-rstack-context-plugin.mjs +++ b/scripts/test-rstack-context-plugin.mjs @@ -158,7 +158,7 @@ const testWorkspaceLocalLauncher = async () => { } }; -const testPackageLocalLauncher = async () => { +const testPnpmWorkspaceLauncher = async () => { const workspace = await mkdtemp( path.join(os.tmpdir(), 'rstack-agent-skills-package-'), ); @@ -167,9 +167,13 @@ const testPackageLocalLauncher = async () => { try { await writeFile( path.join(workspace, 'package.json'), - JSON.stringify({ private: true, workspaces: ['packages/*'] }), + JSON.stringify({ private: true }), ); - const packageRoot = path.join(workspace, 'packages/app'); + await writeFile( + path.join(workspace, 'pnpm-workspace.yaml'), + "packages:\n - 'packages/**' # applications\n", + ); + const packageRoot = path.join(workspace, 'packages/apps/app'); const rstackRoot = path.join(packageRoot, 'node_modules/rstack'); await mkdir(path.join(rstackRoot, 'bin'), { recursive: true }); await writeFile( @@ -308,7 +312,7 @@ const testPathLauncher = async () => { await testManifest(); await testSkills(); await testWorkspaceLocalLauncher(); -await testPackageLocalLauncher(); +await testPnpmWorkspaceLauncher(); await testNestedPackageLauncher(); await testPathLauncher(); From 79de40a1e9261b474f5a3a57cd03d2ceb1778e0f Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Fri, 14 Aug 2026 01:30:11 +0000 Subject: [PATCH 11/24] chore(plugin): refresh Codex cache --- .codex-plugin/plugin.json | 2 +- scripts/test-rstack-context-plugin.mjs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 60fa190..c5f5df1 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "rstack", - "version": "0.2.1", + "version": "0.2.1+codex.20260814012911", "description": "Agent Skills and project context tools for debugging, tracing, upgrading, and analyzing Rstack projects.", "author": { "name": "RstackJS", diff --git a/scripts/test-rstack-context-plugin.mjs b/scripts/test-rstack-context-plugin.mjs index b4c887b..e507cd1 100644 --- a/scripts/test-rstack-context-plugin.mjs +++ b/scripts/test-rstack-context-plugin.mjs @@ -45,10 +45,10 @@ const testManifest = async () => { assert.equal(portable.name, 'rstack'); assert.equal(codex.name, 'rstack'); assert.equal(claude.name, 'rstack'); - assert.equal(portable.version, codex.version); - assert.equal(codex.version, '0.2.1'); - assert.equal(claude.version, codex.version); - assert.equal(claudeMarketplace.plugins[0].version, codex.version); + assert.equal(portable.version, '0.2.1'); + assert.ok(codex.version.startsWith(`${portable.version}+codex.`)); + assert.equal(claude.version, portable.version); + assert.equal(claudeMarketplace.plugins[0].version, portable.version); assert.equal(codex.mcpServers, './.mcp.json'); assert.ok( mcp.mcpServers?.rstack, From 88acbe94a6a31a09d8204f41e2aae703cd6aca0c Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Fri, 14 Aug 2026 02:32:30 +0000 Subject: [PATCH 12/24] docs(plugin): link standalone context runtime --- README.md | 6 ++++-- scripts/test-rstack-context-plugin.mjs | 8 ++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3a32496..4db2dca 100644 --- a/README.md +++ b/README.md @@ -95,8 +95,10 @@ The plugin adds these context workflows: - `find-unused-code`: prioritize artifact-scoped unreachable module candidates. - `review-context-change`: compare compatible lint or test snapshots. -The plugin contains agent guidance and the MCP launcher. The evidence engine itself is provided by -Rstack CLI, so installing this repository does not duplicate or independently version that runtime. +The plugin contains agent guidance and the MCP launcher. The evidence engine is implemented and +published by [`rstackjs/context`](https://github.com/rstackjs/context), while Rstack CLI provides +the `rs mcp` command and configuration adapters. Installing this repository does not duplicate or +independently version the runtime. ## Rspack Skills diff --git a/scripts/test-rstack-context-plugin.mjs b/scripts/test-rstack-context-plugin.mjs index e507cd1..1a2bda4 100644 --- a/scripts/test-rstack-context-plugin.mjs +++ b/scripts/test-rstack-context-plugin.mjs @@ -115,6 +115,13 @@ const testSkills = async () => { assert.ok(evals.evals.length >= 6); }; +const testRuntimeOwnershipDocumentation = async () => { + const readme = await readFile(path.join(repositoryRoot, 'README.md'), 'utf8'); + + assert.match(readme, /github\.com\/rstackjs\/context/); + assert.match(readme, /Rstack CLI provides[\s\S]*`rs mcp`/); +}; + const testWorkspaceLocalLauncher = async () => { const workspace = await mkdtemp( path.join(os.tmpdir(), 'rstack-agent-skills-local-'), @@ -311,6 +318,7 @@ const testPathLauncher = async () => { await testManifest(); await testSkills(); +await testRuntimeOwnershipDocumentation(); await testWorkspaceLocalLauncher(); await testPnpmWorkspaceLauncher(); await testNestedPackageLauncher(); From 82d09be6fb1b0e1bc697b91d1a89f1f1185fa9b4 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Fri, 14 Aug 2026 02:33:39 +0000 Subject: [PATCH 13/24] chore(plugin): refresh context bundle --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- plugin.json | 2 +- scripts/test-rstack-context-plugin.mjs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index c4407cb..e174adb 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ { "name": "rstack", "description": "Agent Skills and project context tools for debugging, tracing, upgrading, and analyzing Rstack projects.", - "version": "0.2.1", + "version": "0.2.2", "source": "./", "author": { "name": "RstackJS" diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index b4b7082..2bd4cae 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "rstack", - "version": "0.2.1", + "version": "0.2.2", "description": "Agent Skills and project context tools for debugging, tracing, upgrading, and analyzing Rstack projects.", "author": { "name": "RstackJS" diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index c5f5df1..52bba78 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "rstack", - "version": "0.2.1+codex.20260814012911", + "version": "0.2.2+codex.20260814023314", "description": "Agent Skills and project context tools for debugging, tracing, upgrading, and analyzing Rstack projects.", "author": { "name": "RstackJS", diff --git a/plugin.json b/plugin.json index 8c8affc..77aa2b0 100644 --- a/plugin.json +++ b/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "rstack", - "version": "0.2.1", + "version": "0.2.2", "description": "Agent Skills and project context tools for debugging, tracing, upgrading, and analyzing Rstack projects.", "author": { "name": "RstackJS", diff --git a/scripts/test-rstack-context-plugin.mjs b/scripts/test-rstack-context-plugin.mjs index 1a2bda4..9473e5f 100644 --- a/scripts/test-rstack-context-plugin.mjs +++ b/scripts/test-rstack-context-plugin.mjs @@ -45,7 +45,7 @@ const testManifest = async () => { assert.equal(portable.name, 'rstack'); assert.equal(codex.name, 'rstack'); assert.equal(claude.name, 'rstack'); - assert.equal(portable.version, '0.2.1'); + assert.equal(portable.version, '0.2.2'); assert.ok(codex.version.startsWith(`${portable.version}+codex.`)); assert.equal(claude.version, portable.version); assert.equal(claudeMarketplace.plugins[0].version, portable.version); From 4c82de00ac1058ca56758c034fc6972738b38812 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Fri, 14 Aug 2026 02:40:58 +0000 Subject: [PATCH 14/24] fix(plugin): clear inherited inline launcher arguments --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- .mcp.json | 2 +- plugin.json | 2 +- scripts/test-rstack-context-plugin.mjs | 4 +++- 6 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index e174adb..d6d9a21 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ { "name": "rstack", "description": "Agent Skills and project context tools for debugging, tracing, upgrading, and analyzing Rstack projects.", - "version": "0.2.2", + "version": "0.2.3", "source": "./", "author": { "name": "RstackJS" diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 2bd4cae..acef687 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "rstack", - "version": "0.2.2", + "version": "0.2.3", "description": "Agent Skills and project context tools for debugging, tracing, upgrading, and analyzing Rstack projects.", "author": { "name": "RstackJS" diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 52bba78..e360952 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "rstack", - "version": "0.2.2+codex.20260814023314", + "version": "0.2.3+codex.20260814024032", "description": "Agent Skills and project context tools for debugging, tracing, upgrading, and analyzing Rstack projects.", "author": { "name": "RstackJS", diff --git a/.mcp.json b/.mcp.json index 30c6274..739d4b2 100644 --- a/.mcp.json +++ b/.mcp.json @@ -5,7 +5,7 @@ "args": [ "--input-type=module", "--eval", - "import { spawn } from 'node:child_process'; import { globSync, readFileSync } from 'node:fs'; import { createRequire } from 'node:module'; import { dirname, join, resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; const cwd = process.cwd(); const roots = [cwd]; const patterns = []; try { const rootPackage = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8')); const workspaces = Array.isArray(rootPackage.workspaces) ? rootPackage.workspaces : rootPackage.workspaces?.packages; if (Array.isArray(workspaces)) patterns.push(...workspaces); } catch (error) { if (error?.code !== 'ENOENT') throw error; } try { const lines = readFileSync(join(cwd, 'pnpm-workspace.yaml'), 'utf8').split(/\\r?\\n/u); let inPackages = false; for (const line of lines) { if (/^packages:\\s*$/u.test(line)) { inPackages = true; continue; } if (inPackages && /^\\S/u.test(line)) break; const match = inPackages ? /^\\s*-\\s*(?:['\"]([^'\"]+)['\"]|([^#]+?))(?:\\s+#.*)?\\s*$/u.exec(line) : null; if (match) patterns.push((match[1] ?? match[2]).trim()); } } catch (error) { if (error?.code !== 'ENOENT') throw error; } const includes = [...new Set(patterns.filter((pattern) => typeof pattern === 'string' && !pattern.startsWith('!')).map((pattern) => `${pattern.replace(/\\/$/u, '')}/package.json`))]; const excludes = patterns.filter((pattern) => typeof pattern === 'string' && pattern.startsWith('!')).map((pattern) => `${pattern.slice(1).replace(/\\/$/u, '')}/package.json`); if (includes.length > 0) for (const packageJson of globSync(includes, { cwd, exclude: [...excludes, '**/node_modules/**'] })) roots.push(dirname(resolve(cwd, packageJson))); for (const packageJson of globSync('*/package.json', { cwd, exclude: ['node_modules/**'] })) roots.push(dirname(resolve(cwd, packageJson))); let packageJsonPath; let require; for (const root of new Set(roots)) { const candidate = createRequire(join(root, 'package.json')); try { packageJsonPath = candidate.resolve('rstack/package.json'); require = candidate; break; } catch (error) { if (error?.code !== 'MODULE_NOT_FOUND') throw error; } } if (packageJsonPath) { const { bin } = require(packageJsonPath); const binPath = typeof bin === 'string' ? bin : (bin.rs ?? bin.rstack); if (!binPath) throw new Error('The workspace-local rstack package does not declare an rs binary.'); const cliPath = resolve(dirname(packageJsonPath), binPath); process.argv = [process.execPath, cliPath, 'mcp']; await import(pathToFileURL(cliPath).href); } else { try { const child = spawn('rs', ['mcp'], { stdio: 'inherit' }); const { code, signal } = await new Promise((resolve, reject) => { child.once('error', reject); child.once('exit', (code, signal) => resolve({ code, signal })); }); if (signal) process.kill(process.pid, signal); else process.exitCode = code ?? 1; } catch (error) { if (error?.code !== 'ENOENT') throw error; console.error('Unable to launch Rstack MCP server: install rstack in this repository, a declared workspace package, an immediate child package, or put rs on PATH.'); process.exitCode = 1; } }" + "import { spawn } from 'node:child_process'; import { globSync, readFileSync } from 'node:fs'; import { createRequire } from 'node:module'; import { dirname, join, resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; process.execArgv = []; const cwd = process.cwd(); const roots = [cwd]; const patterns = []; try { const rootPackage = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8')); const workspaces = Array.isArray(rootPackage.workspaces) ? rootPackage.workspaces : rootPackage.workspaces?.packages; if (Array.isArray(workspaces)) patterns.push(...workspaces); } catch (error) { if (error?.code !== 'ENOENT') throw error; } try { const lines = readFileSync(join(cwd, 'pnpm-workspace.yaml'), 'utf8').split(/\\r?\\n/u); let inPackages = false; for (const line of lines) { if (/^packages:\\s*$/u.test(line)) { inPackages = true; continue; } if (inPackages && /^\\S/u.test(line)) break; const match = inPackages ? /^\\s*-\\s*(?:['\"]([^'\"]+)['\"]|([^#]+?))(?:\\s+#.*)?\\s*$/u.exec(line) : null; if (match) patterns.push((match[1] ?? match[2]).trim()); } } catch (error) { if (error?.code !== 'ENOENT') throw error; } const includes = [...new Set(patterns.filter((pattern) => typeof pattern === 'string' && !pattern.startsWith('!')).map((pattern) => `${pattern.replace(/\\/$/u, '')}/package.json`))]; const excludes = patterns.filter((pattern) => typeof pattern === 'string' && pattern.startsWith('!')).map((pattern) => `${pattern.slice(1).replace(/\\/$/u, '')}/package.json`); if (includes.length > 0) for (const packageJson of globSync(includes, { cwd, exclude: [...excludes, '**/node_modules/**'] })) roots.push(dirname(resolve(cwd, packageJson))); for (const packageJson of globSync('*/package.json', { cwd, exclude: ['node_modules/**'] })) roots.push(dirname(resolve(cwd, packageJson))); let packageJsonPath; let require; for (const root of new Set(roots)) { const candidate = createRequire(join(root, 'package.json')); try { packageJsonPath = candidate.resolve('rstack/package.json'); require = candidate; break; } catch (error) { if (error?.code !== 'MODULE_NOT_FOUND') throw error; } } if (packageJsonPath) { const { bin } = require(packageJsonPath); const binPath = typeof bin === 'string' ? bin : (bin.rs ?? bin.rstack); if (!binPath) throw new Error('The workspace-local rstack package does not declare an rs binary.'); const cliPath = resolve(dirname(packageJsonPath), binPath); process.argv = [process.execPath, cliPath, 'mcp']; await import(pathToFileURL(cliPath).href); } else { try { const child = spawn('rs', ['mcp'], { stdio: 'inherit' }); const { code, signal } = await new Promise((resolve, reject) => { child.once('error', reject); child.once('exit', (code, signal) => resolve({ code, signal })); }); if (signal) process.kill(process.pid, signal); else process.exitCode = code ?? 1; } catch (error) { if (error?.code !== 'ENOENT') throw error; console.error('Unable to launch Rstack MCP server: install rstack in this repository, a declared workspace package, an immediate child package, or put rs on PATH.'); process.exitCode = 1; } }" ] } } diff --git a/plugin.json b/plugin.json index 77aa2b0..2e15220 100644 --- a/plugin.json +++ b/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "rstack", - "version": "0.2.2", + "version": "0.2.3", "description": "Agent Skills and project context tools for debugging, tracing, upgrading, and analyzing Rstack projects.", "author": { "name": "RstackJS", diff --git a/scripts/test-rstack-context-plugin.mjs b/scripts/test-rstack-context-plugin.mjs index 9473e5f..ad5391e 100644 --- a/scripts/test-rstack-context-plugin.mjs +++ b/scripts/test-rstack-context-plugin.mjs @@ -45,7 +45,7 @@ const testManifest = async () => { assert.equal(portable.name, 'rstack'); assert.equal(codex.name, 'rstack'); assert.equal(claude.name, 'rstack'); - assert.equal(portable.version, '0.2.2'); + assert.equal(portable.version, '0.2.3'); assert.ok(codex.version.startsWith(`${portable.version}+codex.`)); assert.equal(claude.version, portable.version); assert.equal(claudeMarketplace.plugins[0].version, portable.version); @@ -147,6 +147,7 @@ const testWorkspaceLocalLauncher = async () => { 'writeFileSync(process.env.RSTACK_PLUGIN_TEST_RECORD, JSON.stringify({', ' argv: process.argv.slice(2),', ' cwd: process.cwd(),', + ' execArgv: process.execArgv,', '}));', ].join('\n'), ); @@ -159,6 +160,7 @@ const testWorkspaceLocalLauncher = async () => { assert.deepEqual(await readJsonFrom(recordPath), { argv: ['mcp'], cwd: workspace, + execArgv: [], }); } finally { await rm(workspace, { recursive: true, force: true }); From 77ef1464dcb9899a9cb9d2fdfb6a242af27a91d7 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Fri, 14 Aug 2026 08:49:46 +0000 Subject: [PATCH 15/24] test(plugin): validate real Rstack MCP runtime --- scripts/test-rstack-context-plugin.mjs | 99 +++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/scripts/test-rstack-context-plugin.mjs b/scripts/test-rstack-context-plugin.mjs index ad5391e..f9ccbf7 100644 --- a/scripts/test-rstack-context-plugin.mjs +++ b/scripts/test-rstack-context-plugin.mjs @@ -1,5 +1,5 @@ import assert from 'node:assert/strict'; -import { spawnSync } from 'node:child_process'; +import { spawn, spawnSync } from 'node:child_process'; import { chmod, mkdtemp, @@ -10,6 +10,7 @@ import { } from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; +import { createInterface } from 'node:readline'; import { fileURLToPath } from 'node:url'; const repositoryRoot = path.resolve( @@ -318,6 +319,101 @@ const testPathLauncher = async () => { } }; +const listRealRuntimeTools = async (configuration, cwd) => { + const command = + configuration.command === 'node' ? process.execPath : configuration.command; + const child = spawn(command, configuration.args ?? [], { + cwd, + env: { + ...process.env, + PATH: process.env.RSTACK_PLUGIN_INTEGRATION_PATH ?? '', + }, + stdio: ['pipe', 'pipe', 'pipe'], + }); + const stderr = []; + child.stderr.setEncoding('utf8'); + child.stderr.on('data', (chunk) => stderr.push(chunk)); + + const pending = new Map(); + const output = createInterface({ input: child.stdout }); + output.on('line', (line) => { + let message; + try { + message = JSON.parse(line); + } catch { + return; + } + if (message.id === undefined) return; + const request = pending.get(message.id); + if (!request) return; + pending.delete(message.id); + if (message.error) request.reject(new Error(message.error.message)); + else request.resolve(message.result); + }); + + const request = (id, method, params) => + new Promise((resolve, reject) => { + pending.set(id, { reject, resolve }); + child.stdin.write( + `${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`, + ); + }); + const rejectPending = (error) => { + for (const { reject } of pending.values()) reject(error); + pending.clear(); + }; + child.once('error', rejectPending); + child.once('exit', (code, signal) => { + if (pending.size === 0) return; + rejectPending( + new Error( + `Rstack MCP exited before responding (${signal ?? code ?? 'unknown'}). ${stderr.join('')}`, + ), + ); + }); + const timeout = setTimeout(() => { + rejectPending( + new Error(`Timed out waiting for Rstack MCP. ${stderr.join('')}`), + ); + child.kill('SIGKILL'); + }, 15_000); + + try { + await request(1, 'initialize', { + protocolVersion: '2025-03-26', + capabilities: {}, + clientInfo: { name: 'rstack-agent-skills-test', version: '1.0.0' }, + }); + child.stdin.write( + `${JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' })}\n`, + ); + const result = await request(2, 'tools/list', {}); + return result.tools; + } finally { + clearTimeout(timeout); + output.close(); + child.stdin.end(); + child.kill(); + } +}; + +const testRealRuntimeLauncher = async () => { + const integrationRoot = process.env.RSTACK_PLUGIN_INTEGRATION_ROOT; + if (!integrationRoot) return; + + const configuration = (await readJson('.mcp.json')).mcpServers.rstack; + const tools = await listRealRuntimeTools(configuration, integrationRoot); + const names = tools.map(({ name }) => name); + for (const name of [ + 'project_status', + 'product_roots', + 'test_snapshot', + 'code_evidence', + ]) { + assert.ok(names.includes(name), `real Rstack MCP must advertise ${name}`); + } +}; + await testManifest(); await testSkills(); await testRuntimeOwnershipDocumentation(); @@ -325,5 +421,6 @@ await testWorkspaceLocalLauncher(); await testPnpmWorkspaceLauncher(); await testNestedPackageLauncher(); await testPathLauncher(); +await testRealRuntimeLauncher(); console.log('Rstack Context plugin contract passed.'); From c5d200adbc4323b5d61b42a8bd35586636f16c37 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Fri, 14 Aug 2026 09:17:07 +0000 Subject: [PATCH 16/24] docs(plugin): improve evidence recovery --- skills/analyze-build/SKILL.md | 4 ++-- skills/debug-dev-cycle/SKILL.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/skills/analyze-build/SKILL.md b/skills/analyze-build/SKILL.md index 8a7986c..7dab272 100644 --- a/skills/analyze-build/SKILL.md +++ b/skills/analyze-build/SKILL.md @@ -7,8 +7,8 @@ description: Use when summarizing Rstack build health, errors, chunks, packages, 1. Call `project_status` to establish available contexts and latest build observations. Analysis may still proceed from an explicit artifact when no context exists. 2. Obtain the explicit Rsdoctor `dataFile`. If it is missing, identify the product and inspect the matching `@rsdoctor/rspack-plugin` or `@rsdoctor/webpack-plugin` version before offering a capture. Use `RSDOCTOR_OUTPUT=json` only when the plugin version is at least `1.5.11`. For a missing, unknown, or older plugin, follow the `rsdoctor-analysis` Generation Gate: install/register the plugin when missing, configure `output.mode='brief'` with JSON output, and build with `RSDOCTOR=true` without `RSDOCTOR_OUTPUT`. Use `rs build` for an application or `rs lib` for a library, include `RSTACK_CONTEXT=1`, and ask before installing, configuring, building, or capturing. -3. When a context exists, call `product_roots` with the same `contextId` and `dataFile`. Use context-bound claims only when `artifactBinding` is `exact`; report `mismatch` or `explicit-unverified` as artifact-only evidence. -4. Call `rsdoctor_analyze` with the narrowest suitable tool: `build_summary`, `errors_list`, `chunks_list`, `bundle_optimize`, one `tree_shaking_*` view, or one `packages_*` view. +3. Call `rsdoctor_analyze` with the narrowest suitable tool: `build_summary`, `errors_list`, `chunks_list`, `bundle_optimize`, one `tree_shaking_*` view, or one `packages_*` view. When artifact metadata is returned, compare its compiler environment and compilation hash with the build summaries from `project_status` before selecting a context. +4. When a context exists, call `product_roots` with the selected `contextId` and `dataFile`. Use context-bound claims only when `artifactBinding` is `exact`; report `mismatch` or `explicit-unverified` as artifact-only evidence. 5. Treat omitted sections as unavailable. Reserve zero or healthy labels for evidence the tool actually returned. 6. Call `report_link` only when an optional navigable report would materially help. diff --git a/skills/debug-dev-cycle/SKILL.md b/skills/debug-dev-cycle/SKILL.md index 6a9e9b1..44e10af 100644 --- a/skills/debug-dev-cycle/SKILL.md +++ b/skills/debug-dev-cycle/SKILL.md @@ -8,7 +8,7 @@ description: Use when diagnosing one current Rstack Rslint or Rstest failure fro 1. Call `project_status` first. Match the package and producer by `context.packageRoot`, then retain its `contextId`. 2. Prefer stored evidence. Use `snapshot_list` for that context, then `diagnostics_list` or `test_results`; follow `nextCursor` only when more results are needed. 3. Report freshness (`fresh`, `stale`, `partial`, or `unknown`) independently from completeness, including changed paths and coverage bounds. -4. Ask before calling `lint_snapshot` or `test_snapshot`. For monorepos, pass checkout-relative `packageRoot`; pass `configPath` only for a nonstandard Rstack config. Never start watch mode through these tools. +4. Ask before calling `lint_snapshot` or `test_snapshot`. Always copy checkout-relative `context.packageRoot` from `project_status`; do not substitute the agent's current directory with `.` in a nested package. Pass `configPath` only for a nonstandard Rstack config. Never start watch mode through these tools. 5. Lead with the first actionable failure and briefly summarize the rest. 6. For a specific source file, an approved `test_snapshot` with `related: [path]` asks Rstest to select and run only statically related tests. Use one source per capture so `code_evidence.testRelation` is attributable to that source. 7. Call `code_evidence` with the relevant test or lint snapshot ID. Keep static test relation, exact-path test outcome, aggregate execution coverage, diagnostics, and build state independent. From 604a968c11bb38f645e5a24a5b351d5f39041e4d Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Fri, 14 Aug 2026 09:28:14 +0000 Subject: [PATCH 17/24] docs(plugin): surface test startup failures --- skills/debug-dev-cycle/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/debug-dev-cycle/SKILL.md b/skills/debug-dev-cycle/SKILL.md index 44e10af..b77de0e 100644 --- a/skills/debug-dev-cycle/SKILL.md +++ b/skills/debug-dev-cycle/SKILL.md @@ -9,7 +9,7 @@ description: Use when diagnosing one current Rstack Rslint or Rstest failure fro 2. Prefer stored evidence. Use `snapshot_list` for that context, then `diagnostics_list` or `test_results`; follow `nextCursor` only when more results are needed. 3. Report freshness (`fresh`, `stale`, `partial`, or `unknown`) independently from completeness, including changed paths and coverage bounds. 4. Ask before calling `lint_snapshot` or `test_snapshot`. Always copy checkout-relative `context.packageRoot` from `project_status`; do not substitute the agent's current directory with `.` in a nested package. Pass `configPath` only for a nonstandard Rstack config. Never start watch mode through these tools. -5. Lead with the first actionable failure and briefly summarize the rest. +5. When `test_snapshot` fails, inspect its `errors` first. A file- or run-scoped error can explain why `test_results` contains no cases. Lead with the first actionable failure and briefly summarize the rest. 6. For a specific source file, an approved `test_snapshot` with `related: [path]` asks Rstest to select and run only statically related tests. Use one source per capture so `code_evidence.testRelation` is attributable to that source. 7. Call `code_evidence` with the relevant test or lint snapshot ID. Keep static test relation, exact-path test outcome, aggregate execution coverage, diagnostics, and build state independent. 8. Use `lint_fix_preview` only when already captured. Do not apply it. From 32a082491435db777f3eac6a935c3e26b7c20901 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Fri, 14 Aug 2026 09:35:29 +0000 Subject: [PATCH 18/24] docs(plugin): clarify code evidence inputs --- skills/debug-dev-cycle/SKILL.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/debug-dev-cycle/SKILL.md b/skills/debug-dev-cycle/SKILL.md index b77de0e..96f0b59 100644 --- a/skills/debug-dev-cycle/SKILL.md +++ b/skills/debug-dev-cycle/SKILL.md @@ -11,7 +11,7 @@ description: Use when diagnosing one current Rstack Rslint or Rstest failure fro 4. Ask before calling `lint_snapshot` or `test_snapshot`. Always copy checkout-relative `context.packageRoot` from `project_status`; do not substitute the agent's current directory with `.` in a nested package. Pass `configPath` only for a nonstandard Rstack config. Never start watch mode through these tools. 5. When `test_snapshot` fails, inspect its `errors` first. A file- or run-scoped error can explain why `test_results` contains no cases. Lead with the first actionable failure and briefly summarize the rest. 6. For a specific source file, an approved `test_snapshot` with `related: [path]` asks Rstest to select and run only statically related tests. Use one source per capture so `code_evidence.testRelation` is attributable to that source. -7. Call `code_evidence` with the relevant test or lint snapshot ID. Keep static test relation, exact-path test outcome, aggregate execution coverage, diagnostics, and build state independent. +7. Call `code_evidence` with the relevant test or lint snapshot ID. Pass `contextId` only when also joining an explicit Rsdoctor `dataFile`; omit both for test/lint-only evidence. Keep static test relation, exact-path test outcome, aggregate execution coverage, diagnostics, and build state independent. 8. Use `lint_fix_preview` only when already captured. Do not apply it. Use `rs test list --related --json` when the user wants listing without an MCP capture or test execution. From 7a846d10d4f52dd9df38afad9efb667ec772cf48 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Fri, 14 Aug 2026 09:42:58 +0000 Subject: [PATCH 19/24] fix(plugin): isolate workspace MCP runtime --- .claude-plugin/marketplace.json | 2 +- .claude-plugin/plugin.json | 2 +- .codex-plugin/plugin.json | 2 +- .mcp.json | 2 +- plugin.json | 2 +- scripts/test-rstack-context-plugin.mjs | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index d6d9a21..c02cb10 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -9,7 +9,7 @@ { "name": "rstack", "description": "Agent Skills and project context tools for debugging, tracing, upgrading, and analyzing Rstack projects.", - "version": "0.2.3", + "version": "0.2.4", "source": "./", "author": { "name": "RstackJS" diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index acef687..6e29fd9 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "rstack", - "version": "0.2.3", + "version": "0.2.4", "description": "Agent Skills and project context tools for debugging, tracing, upgrading, and analyzing Rstack projects.", "author": { "name": "RstackJS" diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index e360952..87cac95 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "rstack", - "version": "0.2.3+codex.20260814024032", + "version": "0.2.4+codex.20260814093600", "description": "Agent Skills and project context tools for debugging, tracing, upgrading, and analyzing Rstack projects.", "author": { "name": "RstackJS", diff --git a/.mcp.json b/.mcp.json index 739d4b2..09a4c1d 100644 --- a/.mcp.json +++ b/.mcp.json @@ -5,7 +5,7 @@ "args": [ "--input-type=module", "--eval", - "import { spawn } from 'node:child_process'; import { globSync, readFileSync } from 'node:fs'; import { createRequire } from 'node:module'; import { dirname, join, resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; process.execArgv = []; const cwd = process.cwd(); const roots = [cwd]; const patterns = []; try { const rootPackage = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8')); const workspaces = Array.isArray(rootPackage.workspaces) ? rootPackage.workspaces : rootPackage.workspaces?.packages; if (Array.isArray(workspaces)) patterns.push(...workspaces); } catch (error) { if (error?.code !== 'ENOENT') throw error; } try { const lines = readFileSync(join(cwd, 'pnpm-workspace.yaml'), 'utf8').split(/\\r?\\n/u); let inPackages = false; for (const line of lines) { if (/^packages:\\s*$/u.test(line)) { inPackages = true; continue; } if (inPackages && /^\\S/u.test(line)) break; const match = inPackages ? /^\\s*-\\s*(?:['\"]([^'\"]+)['\"]|([^#]+?))(?:\\s+#.*)?\\s*$/u.exec(line) : null; if (match) patterns.push((match[1] ?? match[2]).trim()); } } catch (error) { if (error?.code !== 'ENOENT') throw error; } const includes = [...new Set(patterns.filter((pattern) => typeof pattern === 'string' && !pattern.startsWith('!')).map((pattern) => `${pattern.replace(/\\/$/u, '')}/package.json`))]; const excludes = patterns.filter((pattern) => typeof pattern === 'string' && pattern.startsWith('!')).map((pattern) => `${pattern.slice(1).replace(/\\/$/u, '')}/package.json`); if (includes.length > 0) for (const packageJson of globSync(includes, { cwd, exclude: [...excludes, '**/node_modules/**'] })) roots.push(dirname(resolve(cwd, packageJson))); for (const packageJson of globSync('*/package.json', { cwd, exclude: ['node_modules/**'] })) roots.push(dirname(resolve(cwd, packageJson))); let packageJsonPath; let require; for (const root of new Set(roots)) { const candidate = createRequire(join(root, 'package.json')); try { packageJsonPath = candidate.resolve('rstack/package.json'); require = candidate; break; } catch (error) { if (error?.code !== 'MODULE_NOT_FOUND') throw error; } } if (packageJsonPath) { const { bin } = require(packageJsonPath); const binPath = typeof bin === 'string' ? bin : (bin.rs ?? bin.rstack); if (!binPath) throw new Error('The workspace-local rstack package does not declare an rs binary.'); const cliPath = resolve(dirname(packageJsonPath), binPath); process.argv = [process.execPath, cliPath, 'mcp']; await import(pathToFileURL(cliPath).href); } else { try { const child = spawn('rs', ['mcp'], { stdio: 'inherit' }); const { code, signal } = await new Promise((resolve, reject) => { child.once('error', reject); child.once('exit', (code, signal) => resolve({ code, signal })); }); if (signal) process.kill(process.pid, signal); else process.exitCode = code ?? 1; } catch (error) { if (error?.code !== 'ENOENT') throw error; console.error('Unable to launch Rstack MCP server: install rstack in this repository, a declared workspace package, an immediate child package, or put rs on PATH.'); process.exitCode = 1; } }" + "import { spawn } from 'node:child_process'; import { globSync, readFileSync } from 'node:fs'; import { createRequire } from 'node:module'; import { dirname, join, resolve } from 'node:path'; const cwd = process.cwd(); const roots = [cwd]; const patterns = []; try { const rootPackage = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8')); const workspaces = Array.isArray(rootPackage.workspaces) ? rootPackage.workspaces : rootPackage.workspaces?.packages; if (Array.isArray(workspaces)) patterns.push(...workspaces); } catch (error) { if (error?.code !== 'ENOENT') throw error; } try { const lines = readFileSync(join(cwd, 'pnpm-workspace.yaml'), 'utf8').split(/\\r?\\n/u); let inPackages = false; for (const line of lines) { if (/^packages:\\s*$/u.test(line)) { inPackages = true; continue; } if (inPackages && /^\\S/u.test(line)) break; const match = inPackages ? /^\\s*-\\s*(?:['\"]([^'\"]+)['\"]|([^#]+?))(?:\\s+#.*)?\\s*$/u.exec(line) : null; if (match) patterns.push((match[1] ?? match[2]).trim()); } } catch (error) { if (error?.code !== 'ENOENT') throw error; } const includes = [...new Set(patterns.filter((pattern) => typeof pattern === 'string' && !pattern.startsWith('!')).map((pattern) => `${pattern.replace(/\\/$/u, '')}/package.json`))]; const excludes = patterns.filter((pattern) => typeof pattern === 'string' && pattern.startsWith('!')).map((pattern) => `${pattern.slice(1).replace(/\\/$/u, '')}/package.json`); if (includes.length > 0) for (const packageJson of globSync(includes, { cwd, exclude: [...excludes, '**/node_modules/**'] })) roots.push(dirname(resolve(cwd, packageJson))); for (const packageJson of globSync('*/package.json', { cwd, exclude: ['node_modules/**'] })) roots.push(dirname(resolve(cwd, packageJson))); let packageJsonPath; let localRequire; for (const root of new Set(roots)) { const candidate = createRequire(join(root, 'package.json')); try { packageJsonPath = candidate.resolve('rstack/package.json'); localRequire = candidate; break; } catch (error) { if (error?.code !== 'MODULE_NOT_FOUND') throw error; } } const run = async (command, args) => { const child = spawn(command, args, { stdio: 'inherit' }); const { code, signal } = await new Promise((resolve, reject) => { child.once('error', reject); child.once('exit', (code, signal) => resolve({ code, signal })); }); if (signal) process.kill(process.pid, signal); else process.exitCode = code ?? 1; }; try { if (packageJsonPath) { const { bin } = localRequire(packageJsonPath); const binPath = typeof bin === 'string' ? bin : (bin.rs ?? bin.rstack); if (!binPath) throw new Error('The workspace-local rstack package does not declare an rs binary.'); await run(process.execPath, [resolve(dirname(packageJsonPath), binPath), 'mcp']); } else { await run('rs', ['mcp']); } } catch (error) { if (error?.code !== 'ENOENT') throw error; console.error('Unable to launch Rstack MCP server: install rstack in this repository, a declared workspace package, an immediate child package, or put rs on PATH.'); process.exitCode = 1; }" ] } } diff --git a/plugin.json b/plugin.json index 2e15220..6143d10 100644 --- a/plugin.json +++ b/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "rstack", - "version": "0.2.3", + "version": "0.2.4", "description": "Agent Skills and project context tools for debugging, tracing, upgrading, and analyzing Rstack projects.", "author": { "name": "RstackJS", diff --git a/scripts/test-rstack-context-plugin.mjs b/scripts/test-rstack-context-plugin.mjs index f9ccbf7..f9cbdc4 100644 --- a/scripts/test-rstack-context-plugin.mjs +++ b/scripts/test-rstack-context-plugin.mjs @@ -46,7 +46,7 @@ const testManifest = async () => { assert.equal(portable.name, 'rstack'); assert.equal(codex.name, 'rstack'); assert.equal(claude.name, 'rstack'); - assert.equal(portable.version, '0.2.3'); + assert.equal(portable.version, '0.2.4'); assert.ok(codex.version.startsWith(`${portable.version}+codex.`)); assert.equal(claude.version, portable.version); assert.equal(claudeMarketplace.plugins[0].version, portable.version); From a169d070a8761bb8ad9cbaa1fc949defd60c6d66 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Fri, 14 Aug 2026 10:10:33 +0000 Subject: [PATCH 20/24] fix(plugin): expose MCP from portable repository --- .agents/plugins/marketplace.json | 2 +- .prettierignore | 3 + .../rstack/.codex-plugin}/plugin.json | 2 +- plugins/rstack/.mcp.json | 12 + plugins/rstack/assets/README.md | 7 + plugins/rstack/assets/rspack-logo.png | Bin 0 -> 5432 bytes plugins/rstack/skills/analyze-build/SKILL.md | 17 + .../skills/assess-change-impact/SKILL.md | 14 + .../rstack/skills/debug-dev-cycle/SKILL.md | 19 ++ .../rstack/skills/explain-dead-code/SKILL.md | 19 ++ .../rstack/skills/find-unused-code/SKILL.md | 19 ++ .../rstack/skills/migrate-to-rsbuild/SKILL.md | 48 +++ .../migrate-to-rsbuild/references/cra.md | 22 ++ .../migrate-to-rsbuild/references/vite.md | 22 ++ .../migrate-to-rsbuild/references/vue-cli.md | 22 ++ .../migrate-to-rsbuild/references/webpack.md | 22 ++ .../rstack/skills/migrate-to-rslib/SKILL.md | 46 +++ .../skills/migrate-to-rslib/references/tsc.md | 25 ++ .../migrate-to-rslib/references/tsup.md | 25 ++ .../rstack/skills/migrate-to-rslint/SKILL.md | 42 +++ .../references/eslint-flat-config.md | 268 +++++++++++++++ .../rstack/skills/migrate-to-rstest/SKILL.md | 43 +++ .../references/config-module-type.md | 61 ++++ .../dependency-bundling-performance.md | 98 ++++++ .../references/dependency-install-gate.md | 50 +++ .../references/detect-test-framework.md | 59 ++++ .../references/discovery-parity.md | 51 +++ .../references/global-api-migration.md | 61 ++++ .../references/jest-migration-deltas.md | 52 +++ .../references/mocked-module-build-graph.md | 70 ++++ .../references/performance-diagnosis.md | 78 +++++ .../references/vitest-migration-deltas.md | 49 +++ .../skills/review-context-change/SKILL.md | 16 + .../skills/rsbuild-best-practices/SKILL.md | 57 ++++ .../rstack/skills/rsbuild-v2-upgrade/SKILL.md | 34 ++ .../rstack/skills/rsdoctor-analysis/SKILL.md | 122 +++++++ .../references/command-map.md | 113 +++++++ .../references/common-analysis-patterns.md | 222 ++++++++++++ .../references/install-rsdoctor-common.md | 88 +++++ .../references/install-rsdoctor-rspack.md | 138 ++++++++ .../references/install-rsdoctor-webpack.md | 71 ++++ .../references/install-rsdoctor.md | 39 +++ .../references/rsdoctor-data-types.md | 103 ++++++ .../skills/rslib-best-practices/SKILL.md | 58 ++++ .../skills/rslib-modern-package/SKILL.md | 173 ++++++++++ .../skills/rspack-best-practices/SKILL.md | 70 ++++ .../rstack/skills/rspack-debugging/SKILL.md | 86 +++++ .../references/guide_a_hmr_crash.md | 23 ++ .../references/guide_b_build_crash.md | 17 + .../guide_c_attach_to_stuck_process.md | 43 +++ .../guide_d_coredump_analysis_dev.md | 27 ++ .../guide_e_coredump_analysis_build.md | 22 ++ .../references/guide_f_async_deadlock.md | 34 ++ .../rspack-debugging/references/lldb.md | 61 ++++ .../scripts/setup_debug_deps.cjs | 117 +++++++ .../skills/rspack-split-chunks/SKILL.md | 315 ++++++++++++++++++ .../references/repo-behavior.md | 191 +++++++++++ plugins/rstack/skills/rspack-tracing/SKILL.md | 75 +++++ .../rspack-tracing/references/bottlenecks.md | 47 +++ .../references/tracing-guide.md | 38 +++ .../rspack-tracing/scripts/analyze_trace.js | 165 +++++++++ .../rstack/skills/rspack-v2-upgrade/SKILL.md | 33 ++ .../skills/rspress-best-practices/SKILL.md | 82 +++++ .../skills/rspress-custom-theme/SKILL.md | 242 ++++++++++++++ .../references/css-variables.md | 177 ++++++++++ .../references/eject-components.md | 154 +++++++++ .../references/layout-slots.md | 153 +++++++++ .../rspress-description-generator/SKILL.md | 118 +++++++ .../skills/rspress-docs-generator/SKILL.md | 60 ++++ .../references/create-new-docs.md | 73 ++++ .../references/doc-structure-conventions.md | 117 +++++++ .../references/maintain-docs-for-prs.md | 31 ++ .../references/rspress-version-guard.md | 13 + .../rstack/skills/rspress-v2-upgrade/SKILL.md | 37 ++ .../skills/rstest-best-practices/SKILL.md | 133 ++++++++ .../rstack/skills/rstest-debugging/SKILL.md | 31 ++ .../references/dependency-bundling.md | 84 +++++ .../references/mocked-module-build-graph.md | 57 ++++ .../references/performance-measurement.md | 86 +++++ .../references/runtime-output-memory.md | 45 +++ .../rstack/skills/storybook-rsbuild/SKILL.md | 120 +++++++ scripts/test-rstack-context-plugin.mjs | 67 +++- 82 files changed, 5797 insertions(+), 9 deletions(-) rename {.codex-plugin => plugins/rstack/.codex-plugin}/plugin.json (97%) create mode 100644 plugins/rstack/.mcp.json create mode 100644 plugins/rstack/assets/README.md create mode 100644 plugins/rstack/assets/rspack-logo.png create mode 100644 plugins/rstack/skills/analyze-build/SKILL.md create mode 100644 plugins/rstack/skills/assess-change-impact/SKILL.md create mode 100644 plugins/rstack/skills/debug-dev-cycle/SKILL.md create mode 100644 plugins/rstack/skills/explain-dead-code/SKILL.md create mode 100644 plugins/rstack/skills/find-unused-code/SKILL.md create mode 100644 plugins/rstack/skills/migrate-to-rsbuild/SKILL.md create mode 100644 plugins/rstack/skills/migrate-to-rsbuild/references/cra.md create mode 100644 plugins/rstack/skills/migrate-to-rsbuild/references/vite.md create mode 100644 plugins/rstack/skills/migrate-to-rsbuild/references/vue-cli.md create mode 100644 plugins/rstack/skills/migrate-to-rsbuild/references/webpack.md create mode 100644 plugins/rstack/skills/migrate-to-rslib/SKILL.md create mode 100644 plugins/rstack/skills/migrate-to-rslib/references/tsc.md create mode 100644 plugins/rstack/skills/migrate-to-rslib/references/tsup.md create mode 100644 plugins/rstack/skills/migrate-to-rslint/SKILL.md create mode 100644 plugins/rstack/skills/migrate-to-rslint/references/eslint-flat-config.md create mode 100644 plugins/rstack/skills/migrate-to-rstest/SKILL.md create mode 100644 plugins/rstack/skills/migrate-to-rstest/references/config-module-type.md create mode 100644 plugins/rstack/skills/migrate-to-rstest/references/dependency-bundling-performance.md create mode 100644 plugins/rstack/skills/migrate-to-rstest/references/dependency-install-gate.md create mode 100644 plugins/rstack/skills/migrate-to-rstest/references/detect-test-framework.md create mode 100644 plugins/rstack/skills/migrate-to-rstest/references/discovery-parity.md create mode 100644 plugins/rstack/skills/migrate-to-rstest/references/global-api-migration.md create mode 100644 plugins/rstack/skills/migrate-to-rstest/references/jest-migration-deltas.md create mode 100644 plugins/rstack/skills/migrate-to-rstest/references/mocked-module-build-graph.md create mode 100644 plugins/rstack/skills/migrate-to-rstest/references/performance-diagnosis.md create mode 100644 plugins/rstack/skills/migrate-to-rstest/references/vitest-migration-deltas.md create mode 100644 plugins/rstack/skills/review-context-change/SKILL.md create mode 100644 plugins/rstack/skills/rsbuild-best-practices/SKILL.md create mode 100644 plugins/rstack/skills/rsbuild-v2-upgrade/SKILL.md create mode 100644 plugins/rstack/skills/rsdoctor-analysis/SKILL.md create mode 100644 plugins/rstack/skills/rsdoctor-analysis/references/command-map.md create mode 100644 plugins/rstack/skills/rsdoctor-analysis/references/common-analysis-patterns.md create mode 100644 plugins/rstack/skills/rsdoctor-analysis/references/install-rsdoctor-common.md create mode 100644 plugins/rstack/skills/rsdoctor-analysis/references/install-rsdoctor-rspack.md create mode 100644 plugins/rstack/skills/rsdoctor-analysis/references/install-rsdoctor-webpack.md create mode 100644 plugins/rstack/skills/rsdoctor-analysis/references/install-rsdoctor.md create mode 100644 plugins/rstack/skills/rsdoctor-analysis/references/rsdoctor-data-types.md create mode 100644 plugins/rstack/skills/rslib-best-practices/SKILL.md create mode 100644 plugins/rstack/skills/rslib-modern-package/SKILL.md create mode 100644 plugins/rstack/skills/rspack-best-practices/SKILL.md create mode 100644 plugins/rstack/skills/rspack-debugging/SKILL.md create mode 100644 plugins/rstack/skills/rspack-debugging/references/guide_a_hmr_crash.md create mode 100644 plugins/rstack/skills/rspack-debugging/references/guide_b_build_crash.md create mode 100644 plugins/rstack/skills/rspack-debugging/references/guide_c_attach_to_stuck_process.md create mode 100644 plugins/rstack/skills/rspack-debugging/references/guide_d_coredump_analysis_dev.md create mode 100644 plugins/rstack/skills/rspack-debugging/references/guide_e_coredump_analysis_build.md create mode 100644 plugins/rstack/skills/rspack-debugging/references/guide_f_async_deadlock.md create mode 100644 plugins/rstack/skills/rspack-debugging/references/lldb.md create mode 100644 plugins/rstack/skills/rspack-debugging/scripts/setup_debug_deps.cjs create mode 100644 plugins/rstack/skills/rspack-split-chunks/SKILL.md create mode 100644 plugins/rstack/skills/rspack-split-chunks/references/repo-behavior.md create mode 100644 plugins/rstack/skills/rspack-tracing/SKILL.md create mode 100644 plugins/rstack/skills/rspack-tracing/references/bottlenecks.md create mode 100644 plugins/rstack/skills/rspack-tracing/references/tracing-guide.md create mode 100644 plugins/rstack/skills/rspack-tracing/scripts/analyze_trace.js create mode 100644 plugins/rstack/skills/rspack-v2-upgrade/SKILL.md create mode 100644 plugins/rstack/skills/rspress-best-practices/SKILL.md create mode 100644 plugins/rstack/skills/rspress-custom-theme/SKILL.md create mode 100644 plugins/rstack/skills/rspress-custom-theme/references/css-variables.md create mode 100644 plugins/rstack/skills/rspress-custom-theme/references/eject-components.md create mode 100644 plugins/rstack/skills/rspress-custom-theme/references/layout-slots.md create mode 100644 plugins/rstack/skills/rspress-description-generator/SKILL.md create mode 100644 plugins/rstack/skills/rspress-docs-generator/SKILL.md create mode 100644 plugins/rstack/skills/rspress-docs-generator/references/create-new-docs.md create mode 100644 plugins/rstack/skills/rspress-docs-generator/references/doc-structure-conventions.md create mode 100644 plugins/rstack/skills/rspress-docs-generator/references/maintain-docs-for-prs.md create mode 100644 plugins/rstack/skills/rspress-docs-generator/references/rspress-version-guard.md create mode 100644 plugins/rstack/skills/rspress-v2-upgrade/SKILL.md create mode 100644 plugins/rstack/skills/rstest-best-practices/SKILL.md create mode 100644 plugins/rstack/skills/rstest-debugging/SKILL.md create mode 100644 plugins/rstack/skills/rstest-debugging/references/dependency-bundling.md create mode 100644 plugins/rstack/skills/rstest-debugging/references/mocked-module-build-graph.md create mode 100644 plugins/rstack/skills/rstest-debugging/references/performance-measurement.md create mode 100644 plugins/rstack/skills/rstest-debugging/references/runtime-output-memory.md create mode 100644 plugins/rstack/skills/storybook-rsbuild/SKILL.md diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index 9ded5da..89c38dd 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -8,7 +8,7 @@ "name": "rstack", "source": { "source": "local", - "path": "./" + "path": "./plugins/rstack" }, "policy": { "installation": "AVAILABLE", diff --git a/.prettierignore b/.prettierignore index cab3337..00898e9 100644 --- a/.prettierignore +++ b/.prettierignore @@ -11,3 +11,6 @@ skills-lock.yaml skills/**/scripts/*.js skills/**/scripts/*.cjs skills/**/scripts/*.mjs +plugins/rstack/skills/**/scripts/*.js +plugins/rstack/skills/**/scripts/*.cjs +plugins/rstack/skills/**/scripts/*.mjs diff --git a/.codex-plugin/plugin.json b/plugins/rstack/.codex-plugin/plugin.json similarity index 97% rename from .codex-plugin/plugin.json rename to plugins/rstack/.codex-plugin/plugin.json index 87cac95..1d57ef6 100644 --- a/.codex-plugin/plugin.json +++ b/plugins/rstack/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "rstack", - "version": "0.2.4+codex.20260814093600", + "version": "0.2.4+codex.20260814100840", "description": "Agent Skills and project context tools for debugging, tracing, upgrading, and analyzing Rstack projects.", "author": { "name": "RstackJS", diff --git a/plugins/rstack/.mcp.json b/plugins/rstack/.mcp.json new file mode 100644 index 0000000..09a4c1d --- /dev/null +++ b/plugins/rstack/.mcp.json @@ -0,0 +1,12 @@ +{ + "mcpServers": { + "rstack": { + "command": "node", + "args": [ + "--input-type=module", + "--eval", + "import { spawn } from 'node:child_process'; import { globSync, readFileSync } from 'node:fs'; import { createRequire } from 'node:module'; import { dirname, join, resolve } from 'node:path'; const cwd = process.cwd(); const roots = [cwd]; const patterns = []; try { const rootPackage = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8')); const workspaces = Array.isArray(rootPackage.workspaces) ? rootPackage.workspaces : rootPackage.workspaces?.packages; if (Array.isArray(workspaces)) patterns.push(...workspaces); } catch (error) { if (error?.code !== 'ENOENT') throw error; } try { const lines = readFileSync(join(cwd, 'pnpm-workspace.yaml'), 'utf8').split(/\\r?\\n/u); let inPackages = false; for (const line of lines) { if (/^packages:\\s*$/u.test(line)) { inPackages = true; continue; } if (inPackages && /^\\S/u.test(line)) break; const match = inPackages ? /^\\s*-\\s*(?:['\"]([^'\"]+)['\"]|([^#]+?))(?:\\s+#.*)?\\s*$/u.exec(line) : null; if (match) patterns.push((match[1] ?? match[2]).trim()); } } catch (error) { if (error?.code !== 'ENOENT') throw error; } const includes = [...new Set(patterns.filter((pattern) => typeof pattern === 'string' && !pattern.startsWith('!')).map((pattern) => `${pattern.replace(/\\/$/u, '')}/package.json`))]; const excludes = patterns.filter((pattern) => typeof pattern === 'string' && pattern.startsWith('!')).map((pattern) => `${pattern.slice(1).replace(/\\/$/u, '')}/package.json`); if (includes.length > 0) for (const packageJson of globSync(includes, { cwd, exclude: [...excludes, '**/node_modules/**'] })) roots.push(dirname(resolve(cwd, packageJson))); for (const packageJson of globSync('*/package.json', { cwd, exclude: ['node_modules/**'] })) roots.push(dirname(resolve(cwd, packageJson))); let packageJsonPath; let localRequire; for (const root of new Set(roots)) { const candidate = createRequire(join(root, 'package.json')); try { packageJsonPath = candidate.resolve('rstack/package.json'); localRequire = candidate; break; } catch (error) { if (error?.code !== 'MODULE_NOT_FOUND') throw error; } } const run = async (command, args) => { const child = spawn(command, args, { stdio: 'inherit' }); const { code, signal } = await new Promise((resolve, reject) => { child.once('error', reject); child.once('exit', (code, signal) => resolve({ code, signal })); }); if (signal) process.kill(process.pid, signal); else process.exitCode = code ?? 1; }; try { if (packageJsonPath) { const { bin } = localRequire(packageJsonPath); const binPath = typeof bin === 'string' ? bin : (bin.rs ?? bin.rstack); if (!binPath) throw new Error('The workspace-local rstack package does not declare an rs binary.'); await run(process.execPath, [resolve(dirname(packageJsonPath), binPath), 'mcp']); } else { await run('rs', ['mcp']); } } catch (error) { if (error?.code !== 'ENOENT') throw error; console.error('Unable to launch Rstack MCP server: install rstack in this repository, a declared workspace package, an immediate child package, or put rs on PATH.'); process.exitCode = 1; }" + ] + } + } +} diff --git a/plugins/rstack/assets/README.md b/plugins/rstack/assets/README.md new file mode 100644 index 0000000..0789464 --- /dev/null +++ b/plugins/rstack/assets/README.md @@ -0,0 +1,7 @@ +# Plugin assets + +`rspack-logo.png` is the official Rspack favicon, copied without modification from +[Rstack Design Resources](https://github.com/rstackjs/rstack-design-resources/blob/main/rspack/favicon-128x128.png). + +The asset is licensed under +[CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/). diff --git a/plugins/rstack/assets/rspack-logo.png b/plugins/rstack/assets/rspack-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..daec16f4e4898ddc270f8d1ce09694a3102614da GIT binary patch literal 5432 zcmV-8702p{P)C0007iP)t-s00013 zKtEVPKU6+GRzEmdK|WhUKv+OOSU^5lKs{JMK36|HS3f;hKRsAKJXkZ( z|F@NjQ%q$=LI1px|Hpp+!HI52L;d^v|F|FjhH3w^kpFW}|9fF#Nkjkq)&I7W|HFg% zvy%VgM*nU`c1lG5#(Dq!_y4^h|B__?{_<*0MSW38|NP!?QbzyGe*bt`gHTH6c4Ytk z>i>*q|Eq%k{pA0HZ?Iogf?iVp_sjpFf&caK^RJEjfNI%rUwm6m|C(TqXjf!RO8?I` z?(6FRuZw?dYX8G2|Ei1edS#JTO|gG#rFLexU0qQ*H^F6Dd`v`}jEm{0iT{~?iFS1U zjC1K`Nc(O@|L^9fn3m+Cg!sXG{-k*IeQ9!CTle+!^YQT7(a_nNeRyPI&TU?%T~pR# zNdMbE=;Gng$jGpvpyQ2W?}TEyV^pA6P_|b@_p>4Yzo71`i~psE&z5^_SXO9KPL_1sCFP(z1JL;uJx|Kh6Pe`NokUBhi#o_Jb` zV>jU3-2dRw|JT9)?!N!cv;V}XwW_H9lX%07be3RGkWfR5aY5;;C&|FS|Ja@Ltc1LW zajR!kj$Kg8S4F8)MAMikmV<%Gk9Om(b?}#S`Hp0?iedhRVa9%4=FCT(ghazvLdA|N zy05N}etgB4XY-a^=WJ7+V^9C?+R(nd?YDT*qHFSPN}F&rs(Ug2(TmV}X4k$)u9if% zg)ijQ(#EVt|Hq!Z!RuWB0010xQchC<@!kFrruXO0OTH}{IazHBf{u7VBeG-tm;UTn zZvBA(01`||L_t(|+U(Rli{d~W$MMYgkQg`7xN30W4>PIuz$S1EEJ7gG+l3{CFTxjK zYnOBv$QJGkh*%4Pa$vP`x9H*e=4#_Ib^GEum?y1Kgh4^%6<*64H^ z72zK!swD}Fh;V?bLf)Dsi~y(?t#-Q=Eh|E<>P-pp_-Y#j0b@Y~l8_^wmkG62TV^DJ zh>CeWfx6Nr)ZPU~Y8io;=NW7@2zaZdap-#lH-+33Rf$+Uf3S@TQjQ0(+943_jibI- zXy^F+Rb24)L6&Dblaz`=Uh0}i_}$M0D~h!I`Zpy+cfoN#0+4FDTFK`bRQc)KGb6(y zAW%jKi6U;-il4tArJ`~brBxR$pkU%{jbnQY04Oz0lLC~n`k8l0(PWW8iXf?Us}#@l zMTnM+9S>m3We~8KKYjNo(l4!7Z!w{!Wjl{g+@oXLmI(wml$*cYe+-d3$_8YZ!3Vg5 z?oW_fmtA~^faOfq^I<>r@VZ&tITj_d`lks+nY;{Kf5c+HFJL1#?DfVT(l7cWLDYU) z594lz{~B&4j#-eKAD;)UY)=QFn@%`vV2P0*4tm}352BsFd|d*2vPt9j_xyX%@4qe_ zinP{8=T(C5y@Jc${nY3MXHxgGQsn?1Wjevj{2-XXv0@kER1PW*h zg0U(XG-_f<jtE4jnLvtx4zEE^{jR-gx5B6f7RVjDPOf zv3`m@PX91I&2uSrT@AKpSTMqL@;($Od1fbsL&u zv+c3jQj~h68%--u)a6g-A-1ud!Cz|A!8-_GhbScq$N6@l1d!xLx(d$i6Fa4K)&kAxI8E<{fZ1BGac> z`3JEj6F!-|0V^nfO5v;YlQdz4t@4>cPODeB>)Q^bqBSw5Z#RPg;O7P<6S8LROj?1R zhP+?EKLH)U_$uD%be5pUcB7a?W*AT6Y!2jdqjFBn{jA$_wBFiPJjpY{Oo z+4313{tm|+v3Ti8Q9KzQ(+lL>dF4m;6f0ePWuUIENzw#5JAW1$moRTQgA93%j1SW`zIcK186IAM znAIvp#=UhNRaG4`5_A7 z#)pZU192;u(NZVthj`4T@4Do;a-z(N4ip=dGhOVI?3XjWeU zHt@B?OGOw0!=n!Onla!mIm&TGM4HqDqHRo|S8SOH23iuen-4O98EWG75|n1Os|sms z2Q)qi=s@z0jK$oHuLg#53CD4_bpk^K5sF%%wP?_K6Jc4PA!I^D1qJ1wy>M0}ZWD%z zMpgjz##2_GtHRjH3p72RK6Pi`;kZH&m{&n!ALi+W0MA1^_H?MvBbBm1<#;IQzklS& zktR$g8yk0QCp=Pl!7--hEfFKBNinj6X z9H$t3R>qQ{xRLDuEdFCLM!fg2*YwG~wLmts0Bu6pT9NVpeDV8ywO$_giOqFd4?+_X zo+^Z@dW3RLF}T>}A@V)VD9y?OWv$g$O-S$*m6!WX)wx?20CoWKfIw;cTHr-#j%4{g z9*^HgnyQ>FZNau7b!TRHd1J<-*jK=*1~0v6ADshuV0LL~_W3}biT6Kfzt0tNZ%j2? z3*?k=G;ieYYUqe`bbj&3@#1;W8O5Nb?u^eX@H~=+I-mzR)!_7JVzJTtaB9Us#Z_Vw zJl#yMun1>uEpV-ngWU9h0O5IGh36fm-|~1!07mnq3F8@MXQDN=)Bpv<@!*?6P6^!v zAKFPz!@X%3BD}xCu@tuRZ_BpeyK+8oGzGO(KoDyE3f?31L;$*gp)P%*=oH0B%{!03 z76c}n7f=xhXa=u|h5asUw;f^OQOUC#kY^=#3%vN=#^BugTqdAw09P)60K9uXl`qi& z+|ocswZ%nY=eOJ~g<75;xV%f9+cBOH)x8 zXWrbj?8TSNtP|&u_XH=JjWN2!*cdqsHo9TD)^?0G2b>lPU}9v}AvjII&mDkc6MV3}-C%%UGG_7hJgvVdXFh}xRqRqF zk%q??A2gsL_qkp0P+g`uLhvXgeHq4N8}ii!0)aZGg%HAR9GOjqf-YAQPY-AKFJJCw zP!L{x(%v^VHrChv_MzFZw` zP#O5?-0gmS3|? z4~VW3VbINk$3Yu8jC}22niO95$i(in4Z1}v zAZsMT(6L{x97vhT2%UgG9Y3&qg&z3VXK<>%ktNu_HgGV5Nj1NA==X?etl0Q<-TICQDM}(LJIwrE0vuq3mAhvGYFah zz`KY8!|4is)JY7!vA*X%!dlbLZ?8%@L3hOA?)D?{`+Zuz4mrbmz1|3@kO80c`jHTj zZ_@)hR}u(|J3^|a@vMvvC-Kg8)TRt!8ZEb3EEcPyDYH{U!Vv$#4O*=DPZua9dD!{k z-7WnN+-h`%QskVSuO9f<69mDoDsUMyxvJS~@difI2e&EQ8xAUecG)5LCoW&+Qd+3? zj}40>r5aM!`|f%gF4voCpyu2%J>YI<4D1gpQ4rMcbeQm5bMsVULp9v?HK8}b#{JUgo0{$X9VOCwXLKbtMK#Ay>@ogO zn4_g_iCE{|yPQ;E2YLw!-bJ3U^o=8cM*SYES5B6ooI|d00W0eX32t3Z?0t22{IEvL z+7@C_5U~yLy*46DH;0Y3CM*L9>>s> zC}EVHg7dNkc>Y(1LO3}gcQ$#3Bq9JX#%1AbH(-FFB9#tsR6}Pkoon($m_Q`aKQ-kC z9nRkDHh{-KuF7eW(`eq^)b9`xh1NwAWZrWvA``s5>Nf!R2W%Lj!;peh;4l*Bt&9j_ zzx&dq)QR5G;*hXyQRVu%0oLXPnn+NssyHatYPASs>bb-L#%KirBEg2T4s{N|vmk<_ zaBH2RTs1+EUs#lj=~8udN`Sr9Au>vN@7>$np)Ly#STG_BTUVk?TdZn0s-P4tr>cWX zZjYmicOn^suZY+dP3QlSpGSZdBapt(Svg45O?MtawsAAyV{$gaz!_)l-QC^xe)x<6 z!z%c>0itbqVROQ-$_Q|$w0zV2@!5FXUqy(u9~?o3U4)cS`;QI?{D`hYH7Q{hnM65i z1Qe041LKn-!x+=A&$usaw-+Sz4;@ADKY9O2g4bBB4D(o2BcPZtD??a^upw@iol&mD z?Gc8_hyC(T%Kv!+ATzPe%4?s#BAip2uWCTEG$$vov@lmgC(UNAAT7b&-vaSy8~wZO zE9CH7o1}t#i8nK7|Mbj?`Bk z|Jvtyf{8SSv{JkH2G)n+l?n2VP-UT!OZpzP=TB1IgW!eVolH_!FHT8zA2gX0g=WPgU__|LWJ)t&^@^kZakV}_o zaW>Q9ITq1t!5Wwa$t}!3neEgq?1M&|-I#k}9yj>u;%v#4fumd{531Y1Q^#Pe_5cSs izyS_$fCC)h|Klfj!0_ZE_AxyG0000 --json` when the user wants listing without an MCP capture or test execution. + +Use only producers configured for the selected package. If Rstest or Rslint is absent, report that axis as unavailable; do not install or configure it as part of diagnosis. diff --git a/plugins/rstack/skills/explain-dead-code/SKILL.md b/plugins/rstack/skills/explain-dead-code/SKILL.md new file mode 100644 index 0000000..14672c2 --- /dev/null +++ b/plugins/rstack/skills/explain-dead-code/SKILL.md @@ -0,0 +1,19 @@ +--- +name: explain-dead-code +description: Use when explaining why one Rstack artifact module is reachable, conservatively preserved, retained, shipped, or apparently unused. +--- + +# Explain an artifact module + +1. Call `project_status` and select the matching build context by package root, product, environment, and target. +2. Confirm the subject is an artifact module selector. Local symbols and exports require source analysis. +3. Obtain the explicit Rsdoctor `dataFile`; offer a consent-gated application or library capture if it is absent. +4. Call `dead_code_explain` with `contextId`, `dataFile`, and the module selector. +5. Lead with the returned classification: reachable, conservatively preserved, unreachable candidate, or insufficient evidence. +6. Show one shortest root-to-module path when present. Report production reachability, public contract, shipment, optimizer retention, truncation, bounds, provenance, and `artifactBinding`. +7. When test or runtime evidence helps, call `code_evidence` with the exact checkout-relative path and matching artifact selector. If no relation was captured and the user approves running tests, call `test_snapshot` with `related: [path]` for that one source, then query its snapshot ID. +8. Keep statically related tests, exact-path test outcomes, and aggregate execution coverage independent. A source can be production-reachable but unobserved in one test run, or test-related without being executed. + +Never infer local-symbol usage. Aggregate execution does not prove code is dead. + +Rstest, Rslint, coverage, and Rsdoctor observations are independent optional evidence. A build-only or library-only repository can still answer artifact questions; report missing axes as unavailable without requiring full-stack adoption. diff --git a/plugins/rstack/skills/find-unused-code/SKILL.md b/plugins/rstack/skills/find-unused-code/SKILL.md new file mode 100644 index 0000000..61dbaa6 --- /dev/null +++ b/plugins/rstack/skills/find-unused-code/SKILL.md @@ -0,0 +1,19 @@ +--- +name: find-unused-code +description: Use when listing or prioritizing artifact-scoped Rstack modules that are unreachable from observed product and contract roots. +--- + +# Find unused-code candidates + +1. Call `project_status` and select the matching build context. Deduplicate repeated runs by `contextId`. +2. Obtain the explicit Rsdoctor `dataFile`; offer the matching consent-gated application or library capture if absent. +3. Call `product_roots` with `contextId` and `dataFile`, then report production, published-contract, and conservative roots plus graph issues. +4. Call `unused_candidates` with the same inputs and an optional `limit` from 1 to 100. Prefer project-owned source modules. If `ownership.project` is zero, stop without paging and say the artifact has no project-owned candidate. +5. Follow `nextCursor` only for a requested exhaustive inventory. Reuse unchanged filters. +6. Call `dead_code_explain` for the strongest candidate. Add `code_evidence` when compatible test or execution evidence helps prioritize it. If no relation was captured and the user approves running tests, call `test_snapshot` with `related: [path]` for that one source, then reuse its snapshot ID. +7. Keep statically related tests, exact-path test outcomes, and aggregate execution coverage independent. `unrelated` is meaningful only for an isolated one-source relation capture; it is still not deletion proof. +8. Report root exhaustion, state axes, truncation, bounds, provenance, and artifact binding. + +Call every result an **artifact-scoped unreachable module candidate**. Completely unimported files are outside the artifact graph, and no candidate is deletion proof. + +Rstest, Rslint, coverage, and Rsdoctor observations are independent optional evidence. Do not install, configure, or run a missing producer just to fill an axis; report it as unavailable and continue with the evidence that exists. diff --git a/plugins/rstack/skills/migrate-to-rsbuild/SKILL.md b/plugins/rstack/skills/migrate-to-rsbuild/SKILL.md new file mode 100644 index 0000000..eb0d63d --- /dev/null +++ b/plugins/rstack/skills/migrate-to-rsbuild/SKILL.md @@ -0,0 +1,48 @@ +--- +name: migrate-to-rsbuild +description: Migrate webpack, Vite, create-react-app (CRA/CRACO), or Vue CLI projects to Rsbuild. +--- + +# Migrate to Rsbuild + +## Goal + +Migrate webpack, Vite, create-react-app (CRA/CRACO), or Vue CLI projects to Rsbuild with minimal behavior changes and clear verification. + +## Supported source frameworks + +- webpack +- Vite +- CRA / CRACO +- Vue CLI + +## Migration principles (must follow) + +1. **Official guide first**: treat Rsbuild migration docs as source of truth. +2. **Smallest-change-first**: complete baseline migration first, then migrate custom behavior. +3. **Do not change business logic**: avoid touching app runtime code unless user explicitly asks. +4. **Validate before cleanup**: keep old tool dependencies/config temporarily if needed; remove only after Rsbuild is green. + +## Workflow + +1. **Detect source framework** + - Check `package.json` dependencies/scripts and config files: + - webpack: `webpack.config.*` + - Vite: `vite.config.*` + - CRA/CRACO: `react-scripts`, `@craco/craco`, `craco.config.*` + - Vue CLI: `@vue/cli-service`, `vue.config.*` + +2. **Apply framework-specific deltas** + - webpack: `references/webpack.md` + - Vite: `references/vite.md` + - CRA/CRACO: `references/cra.md` + - Vue CLI: `references/vue-cli.md` + +3. **Validate behavior** + - Run dev server to verify the project starts without errors. + - Run build command to verify the project builds successfully. + - If issues remain, compare the old project configuration with the migration guide and complete any missing mappings. + +4. **Cleanup and summarize** + - Remove obsolete dependencies/config only after validation passes. + - Summarize changed files and any remaining manual follow-ups. diff --git a/plugins/rstack/skills/migrate-to-rsbuild/references/cra.md b/plugins/rstack/skills/migrate-to-rsbuild/references/cra.md new file mode 100644 index 0000000..19b6744 --- /dev/null +++ b/plugins/rstack/skills/migrate-to-rsbuild/references/cra.md @@ -0,0 +1,22 @@ +# CRA / CRACO -> Rsbuild Migration Checklist + +Use this reference when the source project is Create React App (`react-scripts`) or CRACO. + +## Checklist + +Copy this checklist and check off items as you complete them: + +- [ ] Step 0: Read official guide (https://rsbuild.rs/guide/migration/cra) ⛔ BLOCKING +- [ ] Step 1: Inventory CRA/CRACO-specific behavior +- [ ] Step 2: Execute migration by official guide + - [ ] Apply guide steps with minimal scope + - [ ] Map CRA/CRACO custom behavior +- [ ] Step 3: Apply CRACO-specific follow-up (conditional) + - [ ] If CRACO is used, verify overrides are preserved or intentionally removed +- [ ] Step 4: Verify behavior + - [ ] Dev pass without errors + - [ ] Build pass without errors + - [ ] Run type check if required +- [ ] Step 5: Cleanup and summarize + - [ ] Remove CRA/CRACO-only deps/config after verification + - [ ] Summarize remaining manual follow-ups diff --git a/plugins/rstack/skills/migrate-to-rsbuild/references/vite.md b/plugins/rstack/skills/migrate-to-rsbuild/references/vite.md new file mode 100644 index 0000000..86a3b96 --- /dev/null +++ b/plugins/rstack/skills/migrate-to-rsbuild/references/vite.md @@ -0,0 +1,22 @@ +# Vite -> Rsbuild Migration Checklist + +Use this reference when the source project is Vite. + +## Checklist + +Copy this checklist and check off items as you complete them: + +- [ ] Step 0: Read official guide (https://rsbuild.rs/guide/migration/vite) ⛔ BLOCKING +- [ ] Step 1: Inventory Vite-specific behavior + - [ ] Record plugins + non-default config + - [ ] Record env/base-path/HTML-transform assumptions +- [ ] Step 2: Execute migration by official guide + - [ ] Apply guide steps with minimal scope + - [ ] Map plugin-dependent behavior +- [ ] Step 3: Verify behavior + - [ ] Dev pass without errors + - [ ] Build pass without errors + - [ ] Run type check if required +- [ ] Step 4: Cleanup and summarize + - [ ] Remove Vite-only deps/config after verification + - [ ] Summarize remaining manual follow-ups diff --git a/plugins/rstack/skills/migrate-to-rsbuild/references/vue-cli.md b/plugins/rstack/skills/migrate-to-rsbuild/references/vue-cli.md new file mode 100644 index 0000000..24babfd --- /dev/null +++ b/plugins/rstack/skills/migrate-to-rsbuild/references/vue-cli.md @@ -0,0 +1,22 @@ +# Vue CLI -> Rsbuild Migration Checklist + +Use this reference when the source project is Vue CLI (`@vue/cli-service`). + +## Checklist + +Copy this checklist and check off items as you complete them: + +- [ ] Step 0: Read official guide (https://rsbuild.rs/guide/migration/vue-cli) ⛔ BLOCKING +- [ ] Step 1: Inventory Vue CLI-specific behavior + - [ ] Record `vue.config.*` customizations (`configureWebpack`, `chainWebpack`, `devServer`, `css.loaderOptions`, `publicPath`) + - [ ] Record Vue CLI plugins (`@vue/cli-plugin-*`) and any plugin-dependent behavior +- [ ] Step 2: Execute migration by official guide + - [ ] Apply guide steps with minimal scope + - [ ] Map Vue CLI config/plugin behavior +- [ ] Step 3: Verify behavior + - [ ] Dev pass without errors + - [ ] Build pass without errors + - [ ] Run type check if required +- [ ] Step 4: Cleanup and summarize + - [ ] Remove Vue CLI-only deps/config after verification + - [ ] Summarize remaining manual follow-ups diff --git a/plugins/rstack/skills/migrate-to-rsbuild/references/webpack.md b/plugins/rstack/skills/migrate-to-rsbuild/references/webpack.md new file mode 100644 index 0000000..d76c04f --- /dev/null +++ b/plugins/rstack/skills/migrate-to-rsbuild/references/webpack.md @@ -0,0 +1,22 @@ +# webpack -> Rsbuild Migration Checklist + +Use this reference when the source project is webpack. + +## Checklist + +Copy this checklist and check off items as you complete them: + +- [ ] Step 0: Read official guide (https://rsbuild.rs/guide/migration/webpack) ⛔ BLOCKING +- [ ] Step 1: Inventory webpack-specific customizations + - [ ] Record config entry points + custom loaders/plugins + - [ ] Record dev-server + output/base-path assumptions +- [ ] Step 2: Execute migration by official guide + - [ ] Apply guide steps with minimal scope + - [ ] Map required customizations +- [ ] Step 3: Verify behavior + - [ ] Dev pass without errors + - [ ] Build pass without errors + - [ ] Run type check if required +- [ ] Step 4: Cleanup and summarize + - [ ] Remove webpack-only deps/config after verification + - [ ] Summarize remaining manual follow-ups diff --git a/plugins/rstack/skills/migrate-to-rslib/SKILL.md b/plugins/rstack/skills/migrate-to-rslib/SKILL.md new file mode 100644 index 0000000..4b3f7c9 --- /dev/null +++ b/plugins/rstack/skills/migrate-to-rslib/SKILL.md @@ -0,0 +1,46 @@ +--- +name: migrate-to-rslib +description: Migrate tsc or tsup library projects to Rslib. +--- + +# Migrate to Rslib + +## Goal + +Migrate `tsc` and `tsup` projects to Rslib with minimal behavior changes and clear verification. + +## Supported source frameworks + +- tsc +- tsup + +## Migration principles (must follow) + +1. **Official guide first**: treat Rslib migration docs as source of truth. +2. **Smallest-change-first**: complete baseline migration first, then migrate advanced or custom behavior. +3. **Do not change business logic**: avoid touching source or business logic unless user explicitly asks. +4. **Validate before cleanup**: keep old tool dependencies/config temporarily if needed; remove only after Rslib is green. + +## Workflow + +1. **Detect source tool** + - `tsup` + - Config: `tsup.config.*` + - Dependency: `tsup` + - Build script: uses `tsup` to build projects + - `tsc` + - Config: `tsconfig.json` or `tsconfig.*.json` + - Dependency: `typescript` + - Build script: uses `tsc` to build projects. And it should be noted that `tsc` used only for type checking (e.g., `tsc --noEmit`) does not make it a `tsc` build project. + +2. **Apply tool-specific migration deltas** + - tsc: `references/tsc.md` + - tsup: `references/tsup.md` + +3. **Validate behavior** + - Run build command to verify the project builds successfully. + - If issues remain, compare the old project configuration with the migration guide and complete any missing mappings. + +4. **Cleanup and summarize** + - Remove obsolete dependencies/config only after validation passes. + - Summarize changed files, mapped options, and any remaining manual follow-ups. diff --git a/plugins/rstack/skills/migrate-to-rslib/references/tsc.md b/plugins/rstack/skills/migrate-to-rslib/references/tsc.md new file mode 100644 index 0000000..b0ab1d2 --- /dev/null +++ b/plugins/rstack/skills/migrate-to-rslib/references/tsc.md @@ -0,0 +1,25 @@ +# tsc -> Rslib Migration Checklist + +Use this reference when the source project builds libraries with `tsc`. + +## Checklist + +Copy this checklist and check off items as you complete them: + +- [ ] Step 0: Read official guide (https://rslib.rs/guide/migration/tsc) ⛔ BLOCKING +- [ ] Step 1: Inventory current `tsc` behavior and customizations + - [ ] Record key build scripts, `tsc` CLI options, and `tsconfig.json` usage + - [ ] Record key compile behavior and output behavior (JavaScript files format, declaration files output, output structure, JSX handling, etc.) + - [ ] Record output usage expectations (how outputs are consumed) +- [ ] Step 2: Execute migration following the official guide + - [ ] Apply guide steps with minimal scope + - [ ] Map customizations before and after tsc scripts +- [ ] Step 3: Verify behavior and output compatibility + - [ ] Build passes without errors + - [ ] Build with watch mode runs without errors and incremental rebuild works as expected + - [ ] Output structure, formats, file extensions, and declaration files match expectations + - [ ] Output can be consumed and run as expected + - [ ] Check whether `package.json` fields need updates (`main`, `module`, `types`, `exports`, `files`, `bin`, etc.) +- [ ] Step 4: Cleanup and summarize + - [ ] Remove obsolete tsc-only scripts after verification + - [ ] Summarize config mapping and remaining manual follow-ups diff --git a/plugins/rstack/skills/migrate-to-rslib/references/tsup.md b/plugins/rstack/skills/migrate-to-rslib/references/tsup.md new file mode 100644 index 0000000..0e1f90a --- /dev/null +++ b/plugins/rstack/skills/migrate-to-rslib/references/tsup.md @@ -0,0 +1,25 @@ +# tsup -> Rslib Migration Checklist + +Use this reference when the source project builds libraries with `tsup`. + +## Checklist + +Copy this checklist and check off items as you complete them: + +- [ ] Step 0: Read official guide (https://rslib.rs/guide/migration/tsup) ⛔ BLOCKING +- [ ] Step 1: Inventory current `tsup` behavior and customizations + - [ ] Record key build scripts, `tsup` CLI options, and configuration files + - [ ] Record key config/plugins and output behavior (format, d.ts, externals, output structure, etc.) + - [ ] Record output usage expectations (how outputs are consumed) +- [ ] Step 2: Execute migration following the official guide + - [ ] Apply guide steps with minimal scope + - [ ] Map plugins and other advanced customizations +- [ ] Step 3: Verify behavior and output compatibility + - [ ] Build passes without errors + - [ ] Build with watch mode runs without errors and incremental rebuild works as expected + - [ ] Output structure, formats, file extensions, and declaration files match expectations + - [ ] Output can be consumed and run as expected + - [ ] Check whether `package.json` fields need updates (`main`, `module`, `types`, `exports`, `files`, `bin`, etc.) +- [ ] Step 4: Cleanup and summarize + - [ ] Remove obsolete tsup-only deps/config/scripts after verification + - [ ] Summarize config mapping and remaining manual follow-ups diff --git a/plugins/rstack/skills/migrate-to-rslint/SKILL.md b/plugins/rstack/skills/migrate-to-rslint/SKILL.md new file mode 100644 index 0000000..6a74987 --- /dev/null +++ b/plugins/rstack/skills/migrate-to-rslint/SKILL.md @@ -0,0 +1,42 @@ +--- +name: migrate-to-rslint +description: Migrate ESLint or other linters to Rslint. Use when asked to replace ESLint flat config, lint scripts, VS Code ESLint settings, inline directives, rules, presets, plugins, or lint dependencies with Rslint equivalents. +--- + + + +# Migrate to Rslint + +## Goal + +Migrate lint tooling to Rslint with minimal behavior changes and clear validation. + +## Supported source linters + +- ESLint flat config + +## Migration principles (must follow) + +1. **Official docs first**: treat Rslint docs as source of truth for CLI, config, inline directives, VS Code settings, rules, and presets. +2. **Smallest-change-first**: migrate package, command, config, and editor wiring before changing source files. +3. **Preserve lint intent**: keep supported rule severities/options where Rslint supports them; call out unsupported rules/plugins instead of silently dropping behavior. +4. **Do not rewrite inline directives by default**: Rslint supports `eslint-disable` and `rslint-disable`; replace prefixes only when the user asks. +5. **Validate before cleanup**: keep old linter dependencies/config until the Rslint command passes for the migrated scope, then remove obsolete linter-only artifacts. + +## Workflow + +1. **Detect source linter** + - ESLint flat config: `eslint.config.*`, `eslint` dependency, or package scripts that run `eslint`. + - If the source linter is not covered by a reference yet, inventory the current behavior and explain that no dedicated migration reference exists. + +2. **Apply source-specific migration guide** + - ESLint flat config: `references/eslint-flat-config.md` + +3. **Validate behavior** + - Run the migrated lint command. + - If the project had a fix command, run the migrated fix command only when appropriate for the task. + - Resolve config, rule, and editor-setting issues before removing legacy files. + +4. **Cleanup and summarize** + - Remove obsolete linter dependencies/config after Rslint is green. + - Summarize changed files, migrated presets/plugins, unsupported gaps, and remaining manual follow-ups. diff --git a/plugins/rstack/skills/migrate-to-rslint/references/eslint-flat-config.md b/plugins/rstack/skills/migrate-to-rslint/references/eslint-flat-config.md new file mode 100644 index 0000000..aeaf578 --- /dev/null +++ b/plugins/rstack/skills/migrate-to-rslint/references/eslint-flat-config.md @@ -0,0 +1,268 @@ +# ESLint Flat Config -> Rslint Migration Guide + +Use this reference when the source project uses `eslint.config.*`. + +## Official references + +- Rslint CLI: https://rslint.rs/guide/cli +- Rslint configuration: https://rslint.rs/config/ +- Rslint ignores and `.gitignore`: https://rslint.rs/config/ignoring-files +- Rslint rules and presets: https://rslint.rs/config/rules-and-presets +- Rslint inline directives: https://rslint.rs/guide/inline-directives +- Rslint VS Code extension: https://rslint.rs/guide/vscode-extension +- `jiti`: https://npmjs.com/package/jiti + +## Checklist + +Copy this checklist and check off items as you complete them: + +- [ ] Step 0: Inventory current ESLint behavior +- [ ] Step 1: Replace linter package dependencies +- [ ] Step 2: Replace package scripts and CLI flags +- [ ] Step 3: Rename and translate config file +- [ ] Step 4: Migrate presets, plugins, rules, and ignores +- [ ] Step 5: Preserve inline directives unless the user asks for `rslint-disable` +- [ ] Step 6: Replace VS Code ESLint settings with Rslint settings +- [ ] Step 7: Validate, then remove obsolete ESLint artifacts + +## Step 0: Inventory current ESLint behavior + +Check: + +- `package.json` scripts that run `eslint`. +- `eslint.config.js`, `eslint.config.mjs`, `eslint.config.ts`, or `eslint.config.mts`. +- `.vscode/settings.json` and `.vscode/extensions.json`. +- ESLint dependencies such as `eslint`, `@eslint/js`, `typescript-eslint`, `@typescript-eslint/parser`, `@typescript-eslint/eslint-plugin`, and `eslint-plugin-*`. +- Custom or third-party ESLint plugins that do not have a Rslint built-in equivalent. + +Do not delete the ESLint config or dependencies until the migrated Rslint command is green. + +## Step 1: Replace linter package dependencies + +Install Rslint: + +```bash +pnpm add -D @rslint/core +``` + +Use the repository's package manager if it is not pnpm: + +```bash +npm install -D @rslint/core +yarn add -D @rslint/core +bun add -D @rslint/core +``` + +If the migrated config is `rslint.config.ts` and the project must support Node.js 20, also install `jiti`: + +```bash +pnpm add -D jiti +``` + +Remove ESLint-only dependencies only after validation passes. Built-in Rslint presets/plugins often make these packages obsolete: + +- `eslint` +- `@eslint/js` +- `typescript-eslint` +- `@typescript-eslint/parser` +- `@typescript-eslint/eslint-plugin` +- `eslint-plugin-react` +- `eslint-plugin-react-hooks` +- `eslint-plugin-import` +- `eslint-plugin-promise` +- `eslint-plugin-jest` +- `eslint-plugin-unicorn` +- `eslint-plugin-jsx-a11y` + +Before removing any package, verify it is not used by another tool, workspace package, or still-unmigrated scope. + +## Step 2: Replace package scripts and CLI flags + +Replace `eslint` commands with `rslint` commands. Preserve file and directory arguments. + +Common mappings: + +| ESLint script | Rslint script | +| ------------------------------------ | ------------------------------------ | +| `eslint .` | `rslint .` | +| `eslint src` | `rslint src` | +| `eslint . --fix` | `rslint . --fix` | +| `eslint --config eslint.config.js .` | `rslint --config rslint.config.js .` | +| `eslint . --quiet` | `rslint . --quiet` | +| `eslint . --max-warnings 0` | `rslint . --max-warnings 0` | + +Use only Rslint-supported CLI flags. Supported flags include `--init`, `--config `, `--fix`, `--type-check`, `--format `, `--quiet`, `--max-warnings `, `--rule `, `--no-color`, and `--force-color`. + +Do not copy ESLint-only flags blindly. For example, remove or re-evaluate flags such as `--ext`, `--cache`, `--cache-location`, `--resolve-plugins-relative-to`, and `--report-unused-disable-directives` unless the Rslint CLI docs provide an equivalent. + +If the old lint command relied on type-aware TypeScript linting, validate the migrated command with `--type-check` or preserve the project's existing Rslint type-checking setup. + +## Step 3: Rename and translate config file + +Rename the config to `rslint.config.*`. + +Recommended mapping: + +| ESLint flat config | Rslint config | +| ------------------- | ------------------- | +| `eslint.config.js` | `rslint.config.js` | +| `eslint.config.mjs` | `rslint.config.mjs` | +| `eslint.config.ts` | `rslint.config.ts` | +| `eslint.config.mts` | `rslint.config.mts` | + +Rslint uses flat config arrays, so the shape is close to ESLint flat config. Import built-in helpers and presets from `@rslint/core`: + +```ts +import { + defineConfig, + js, + ts, + reactPlugin, + reactHooksPlugin, + importPlugin, + promisePlugin, + jestPlugin, + unicornPlugin, + jsxA11yPlugin, +} from '@rslint/core'; + +export default defineConfig([ + js.configs.recommended, + ts.configs.recommended, + reactPlugin.configs.recommended, + reactHooksPlugin.configs.recommended, + importPlugin.configs.recommended, + promisePlugin.configs.recommended, + jestPlugin.configs.recommended, + unicornPlugin.configs.recommended, + jsxA11yPlugin.configs.recommended, + { + rules: { + 'no-console': 'warn', + '@typescript-eslint/no-explicit-any': 'error', + }, + }, +]); +``` + +Keep the config format aligned with the source project where practical. If the old config was JavaScript, keep JavaScript. If the old config was TypeScript, use TypeScript and add `jiti` when Node.js 20 compatibility is required. + +## Step 4: Migrate presets, plugins, rules, and ignores + +Rslint has built-in presets that cover common ESLint flat config imports: + +| ESLint source | Rslint equivalent | +| ---------------------------------------------- | -------------------------------------------- | +| `@eslint/js` `js.configs.recommended` | `js.configs.recommended` from `@rslint/core` | +| `typescript-eslint` `ts.configs.recommended` | `ts.configs.recommended` from `@rslint/core` | +| `eslint-plugin-react` recommended config | `reactPlugin.configs.recommended` | +| `eslint-plugin-react-hooks` recommended config | `reactHooksPlugin.configs.recommended` | +| `eslint-plugin-import` recommended config | `importPlugin.configs.recommended` | +| `eslint-plugin-promise` recommended config | `promisePlugin.configs.recommended` | +| `eslint-plugin-jest` recommended config | `jestPlugin.configs.recommended` | +| `eslint-plugin-unicorn` recommended config | `unicornPlugin.configs.recommended` | +| `eslint-plugin-jsx-a11y` recommended config | `jsxA11yPlugin.configs.recommended` | + +Rslint aligns its JavaScript recommended preset with ESLint's `js.configs.recommended` and its TypeScript recommended preset with `typescript-eslint` `ts.configs.recommended`. These presets are built in; do not install `@eslint/js` or `typescript-eslint` just to use recommended configs after migration. + +For rules: + +- Keep rule severities and options when Rslint supports the rule. +- If Rslint reports an unknown rule, remove it from the Rslint config and list it as an unsupported migration gap. +- Do not keep installing ESLint plugins in an attempt to make Rslint load them. +- For built-in plugin presets, remove the old plugin dependency after validation if it is no longer used elsewhere. + +For ignores: + +- Rslint automatically reads `.gitignore` files and treats those patterns as global ignores. +- Remove `ignores` entries that only duplicate `.gitignore` patterns. +- Keep lint-specific ignores that are not in `.gitignore`, such as source fixtures, generated checked-in files, or deliberate lint exclusions. +- Remember that a config entry containing only `ignores` is a global ignore and can block nested config discovery in ignored directories. + +## Step 5: Preserve inline directives unless requested + +Do not replace source comments such as: + +```ts +// eslint-disable-next-line no-console +console.log(value); +``` + +Rslint supports both `eslint-` and `rslint-` directive prefixes, and they are equivalent. + +Only replace prefixes when the user explicitly asks to use `rslint-disable`. If replacement is requested, change directive prefixes in comments while preserving rule names and descriptions: + +| ESLint directive | Rslint directive | +| -------------------------- | -------------------------- | +| `eslint-disable` | `rslint-disable` | +| `eslint-enable` | `rslint-enable` | +| `eslint-disable-next-line` | `rslint-disable-next-line` | +| `eslint-disable-line` | `rslint-disable-line` | + +After replacing, grep for remaining directives: + +```bash +rg -n "eslint-(disable|enable)" +``` + +Do not mutate string literals, docs, snapshots, or user-visible text unless the user asked for a full terminology change. + +## Step 6: Replace VS Code ESLint settings with Rslint settings + +Check `.vscode/extensions.json`: + +- Replace `dbaeumer.vscode-eslint` with `rstack.rslint` when the project recommended ESLint only for linting. +- Keep other extension recommendations unrelated to linting. + +Check `.vscode/settings.json`: + +- Replace `source.fixAll.eslint` with `source.fixAll.rslint`. +- Remove ESLint-only settings such as `eslint.enable`, `eslint.validate`, `eslint.useFlatConfig`, `eslint.workingDirectories`, `eslint.options`, `eslint.nodePath`, and `eslint.format.enable`. +- Add Rslint settings only when needed. Useful settings include `rslint.enable`, `rslint.binPath`, `rslint.customBinPath`, and `rslint.trace.server`. + +Example fix-on-save setting: + +```json +{ + "editor.codeActionsOnSave": { + "source.fixAll.rslint": "explicit" + } +} +``` + +The official VS Code extension id is `rstack.rslint`. It can use the built-in binary by default; use `rslint.binPath: "local"` only when the project needs the workspace-installed binary. + +## Step 7: Validate, then remove obsolete ESLint artifacts + +Run the migrated lint command, for example: + +```bash +pnpm lint +``` + +Or run Rslint directly: + +```bash +pnpm exec rslint . +``` + +If the project has a fix script and the task allows modifying lint fixes: + +```bash +pnpm exec rslint . --fix +``` + +Resolve validation failures in this order: + +1. Config loading and package resolution. +2. Unknown presets/plugins/rules. +3. Ignore coverage and file selection. +4. Type-aware linting behavior. +5. Autofix differences. + +After validation passes: + +- Remove obsolete ESLint config files. +- Remove obsolete ESLint dependencies that are not used elsewhere. +- Remove or update ESLint-specific editor settings. +- Summarize unsupported rules/plugins separately from completed migration work. diff --git a/plugins/rstack/skills/migrate-to-rstest/SKILL.md b/plugins/rstack/skills/migrate-to-rstest/SKILL.md new file mode 100644 index 0000000..c93b1d5 --- /dev/null +++ b/plugins/rstack/skills/migrate-to-rstest/SKILL.md @@ -0,0 +1,43 @@ +--- +name: migrate-to-rstest +description: Migrate Jest or Vitest projects to Rstest. Use when replacing Jest/Vitest config, scripts, APIs, setup, mocks, snapshots, coverage, or projects with `@rstest/core`; auditing test discovery parity; resolving config-loading or dependency-version failures; or diagnosing migration-time build, runtime, memory, and performance regressions caused by Rstest's Rsbuild/Rspack execution model, dependency bundling, runtime-mocked module graphs, assets, logs, or worker pools. +--- + + + +# Migrate to Rstest + +## Goal + +Migrate the smallest runnable Jest/Vitest scope with minimal behavior change. Use documentation and types that match the installed Rstest version; use latest online docs only after the capability gate confirms they apply. + +## Workflow + +1. Detect the runner, environment, Rstack integration, and smallest runnable scope with `references/detect-test-framework.md`. +2. Run `references/dependency-install-gate.md` before choosing Rstest, coverage, adapter, or plugin APIs. +3. Before editing, record the exact command, Node and runner versions, environment, files/tests/skips/snapshots, and failures. Capture the pre-migration test manifest and follow `references/discovery-parity.md` throughout the migration. +4. For Jest read `references/jest-migration-deltas.md`; for Vitest read `references/vitest-migration-deltas.md`; for global APIs also read `references/global-api-migration.md`. Migrate scripts, config, and setup before editing test bodies; prefer adapter or Rsbuild/Rspack fixes over broad test rewrites. +5. If config loading emits `[MODULE_TYPELESS_PACKAGE_JSON]`, use `references/config-module-type.md`. Do not suppress the warning or change the whole package module type without an audit. +6. Get the migrated scope semantically green. Fix failures in this order: dependency skew, config/resolver, discovery, setup/environment/coverage, mocks/timers/snapshots, then test bodies. +7. Compare the post-migration manifest and execution counts with the baseline. Classify every added, removed, skipped, or excluded test explicitly; do not call a run equivalent merely because it is green. +8. Remove temporary aliases, diagnostic hooks, and copied legacy workarounds that are no longer required. Keep only configuration proven necessary by behavior or measurement. +9. If wall time, build, test runtime, logs, or memory materially regress, load and follow the `rstest-debugging` skill when available. Otherwise use the migration fallback in `references/performance-diagnosis.md`, then read `references/dependency-bundling-performance.md` and/or `references/mocked-module-build-graph.md` according to the measured bottleneck. Change one variable at a time. +10. After correctness and performance validation, remove only legacy files and dependencies owned by the migrated scope. Summarize behavior parity, discovery differences, unsupported fields, retained compatibility config, performance tradeoffs, and TODOs. + +## Guardrails + +- Keep the smallest viable scope. Do not broaden a monorepo migration because scopes share a lockfile. +- Do not change production behavior, assertions, test names, scenarios, coverage thresholds, or ignore directives to make migration pass. +- Do not introduce `jest`/`vi` shims or aliases; rewrite call sites with `references/global-api-migration.md`. +- Do not silently drop unknown config fields or historical excludes. Verify, replace, or report each one. +- Do not compare performance across different test manifests, Node versions, coverage modes, cache states, or worker settings. +- Keep the previous runner until Rstest is green. Use a local Rstest checkout only for labeled diagnostics, then validate the final result with the project's installed dependency. +- Escalate before many test edits or any production-source change: report why smaller config/setup fixes failed, options, risks, and the recommended path. + +## High-risk Rstest deltas + +- `rstest` / `rstest run` is single-run; watch mode is `rstest --watch` or `rstest watch`. +- `globals` defaults to `false`; preserving global APIs requires `globals: true` and `@rstest/core/globals` types. +- Rstest builds before tests run. `rs.mock()` is runtime replacement and does not by itself prune the real module graph. +- Dependency bundling trades compiler work for runtime Node loading. Neither “bundle all” nor “externalize all” is universally faster; measure a representative file and the full scope. +- An externalized runtime mock must match the exact request and module format. A config-wide external is unsafe when any test needs the real module. diff --git a/plugins/rstack/skills/migrate-to-rstest/references/config-module-type.md b/plugins/rstack/skills/migrate-to-rstest/references/config-module-type.md new file mode 100644 index 0000000..9cb55a2 --- /dev/null +++ b/plugins/rstack/skills/migrate-to-rstest/references/config-module-type.md @@ -0,0 +1,61 @@ +# Rstest Config Module Type + + + +Use this reference when loading `rstest.config.*` emits Node's `[MODULE_TYPELESS_PACKAGE_JSON]` warning. + +## Source of truth + +- Rstest configuration files: https://rstest.rs/guide/basic/configure-rstest +- Rsbuild configuration loading: https://rsbuild.rs/guide/configuration/rsbuild +- Node.js package module rules: https://nodejs.org/api/packages.html#determining-module-system + +## Understand the warning + +An Rstest TypeScript config normally uses ESM syntax: + +```ts +import { defineConfig } from '@rstest/core'; + +export default defineConfig({}); +``` + +When the nearest controlling `package.json` has no `type` field, Node can initially treat the config as CommonJS, detect ESM syntax, and reparse it as an ES module. The warning reports that ambiguous module format and its extra parsing cost; it is not a test failure. + +Resolve the ambiguity instead of hiding the warning with `NODE_OPTIONS=--no-warnings`, stderr filtering, or a blanket warning ignore. + +## Choose the narrowest fix + +### CommonJS or mixed package + +Rename the config so only this file explicitly opts into ESM: + +```text +rstest.config.ts -> rstest.config.mts +``` + +Rstest supports `.mts` config files. Update test scripts, CI commands, or documentation that pass an explicit `--config` / `-c` path. Remove or rename the old `.ts` config rather than leaving both files: automatic discovery checks `rstest.config.ts` before `rstest.config.mts`. + +For a JavaScript config, use `rstest.config.mjs` for the same narrow ESM declaration. Use `.cts` or `.cjs` only when the config is intentionally authored with CommonJS-compatible semantics. + +### Intentionally ESM package + +Adding the following to the nearest package-level `package.json` also removes the ambiguity: + +```json +{ + "type": "module" +} +``` + +Choose this only when the package is already intended to be ESM. The field changes how Node interprets every affected `.js` file, so audit runtime scripts, config files, `require` / `module.exports`, `__dirname` / `__filename`, and tool integrations first. Do not turn a package into ESM merely to silence an Rstest config warning. + +In a monorepo, inspect the nearest `package.json` that controls the config path. Do not add `"type": "module"` at the workspace root when only one CommonJS package needs an unambiguous Rstest config. + +## Validate + +Run the same local or CI test command that produced the warning and confirm: + +1. Rstest discovers the intended config, including any explicit `--config` path. +2. `[MODULE_TYPELESS_PACKAGE_JSON]` no longer appears. +3. Config imports, setup, and the migrated test scope still pass. diff --git a/plugins/rstack/skills/migrate-to-rstest/references/dependency-bundling-performance.md b/plugins/rstack/skills/migrate-to-rstest/references/dependency-bundling-performance.md new file mode 100644 index 0000000..6e9a82a --- /dev/null +++ b/plugins/rstack/skills/migrate-to-rstest/references/dependency-bundling-performance.md @@ -0,0 +1,98 @@ +# Dependency Bundling Performance + + + +Use this reference when a migrated Node, `jsdom`, or `happy-dom` scope has high build cost, test-runtime startup cost, or peak memory that may depend on `node_modules` bundling. Browser mode always bundles dependencies and does not support this tuning path. + +## Source of truth + +- Rstest output configuration: https://rstest.rs/config/build/output +- Rstest profiling: https://rstest.rs/guide/debug/profiling +- Rspack lazy barrel: https://rspack.rs/guide/optimization/lazy-barrel + +Check the installed Rstest version and types before using `output.bundleDependencies`; it was added in v0.9.5. + +## Understand both sides of the tradeoff + +Rstest builds test entries with Rsbuild/Rspack before execution: + +- `node` externalizes third-party dependencies by default. +- Browser-like non-browser environments such as `jsdom` and `happy-dom` bundle third-party dependencies by default. +- Browser mode always bundles them. + +Bundling can increase compiler time, output size, and build memory. Externalizing can move work into every isolated test runtime: Node repeatedly resolves package exports, reads package metadata and files, compiles CJS/ESM, and initializes modules. A large suite can therefore run faster with `bundleDependencies: true` even when its build is larger. + +Do not infer the winning strategy from environment defaults or bundle size alone. + +## Establish two scales + +Measure both: + +1. One representative test file that imports the problematic graph. +2. The full migrated scope with the same discovered test manifest as the baseline. + +Keep Node version, coverage, cache state, worker settings, and command shape fixed. Record build, test/runtime, CLI wall, and memory when memory is part of the question. A single file exposes fixed compiler/runtime startup; the full scope exposes repeated loader and per-file isolation cost. + +## Compare the three baselines + +When the target version and non-browser mode support it, compare one variable at a time: + +```ts +// A. Environment default +export default defineConfig({}); + +// B. Externalize dependencies +export default defineConfig({ + output: { bundleDependencies: false }, +}); + +// C. Bundle dependencies +export default defineConfig({ + output: { bundleDependencies: true }, +}); +``` + +Treat these as diagnostic baselines, not final recommendations. Keep all tests green and compare several runs when results are noisy. + +Interpret the split: + +- Lower build time but higher tests/collect/setup time after externalizing indicates repeated Node loader or module initialization cost. +- Higher build time and lower full-suite wall after bundling can be a valid tradeoff when shared chunks avoid repeated runtime loading. +- A win only on one file may disappear or reverse on the full scope. +- A full-suite win with a changed test manifest is not comparable. + +When a Node-environment trace shows `collect` dominating while test bodies are small, compare `bundleDependencies: true` early. It is a high-signal baseline for repeated runtime module loading; only add selective bundling or exact externals after measuring it against the environment default. + +## Use a selective policy only after the baselines + +An array externalizes by default and bundles only matching requests: + +```ts +export default defineConfig({ + output: { + bundleDependencies: ['esm-only-package', 'source-package/*'], + }, +}); +``` + +It does not re-bundle transitive dependencies reachable only through an already externalized parent. Long allowlists are a warning sign: compare them with `true` and confirm every entry has a measured compatibility or performance reason. + +Bundle a dependency when Rspack transformation is required for ESM/TypeScript source, imports without file extensions, aliases, CSS/assets, or a measured shared-chunk/lazy-barrel benefit. Externalize a dependency when it runs correctly in Node and its compiler graph dominates without offsetting runtime cost. + +## Combine with exact externals + +`output.externals` overrides the bundling baseline for matching requests. Use it for a few measured heavy boundaries, especially modules already fully mocked. Follow `mocked-module-build-graph.md` before externalizing a mocked request. + +Do not broadly externalize React, UI libraries, or workspace layers just because they are large. Verify package exports, ESM/CommonJS interop, styles, assets, aliases, snapshots, and coverage. + +## Preserve measured lazy-barrel wins + +A bundled ESM barrel can benefit from Rspack lazy-barrel optimization when it has explicit side-effect-free metadata and the test imports a small named subset. Eligibility is not proof of benefit. Start from packages observed in build output, change one candidate, and keep it bundled only when the same tests improve. + +## Validate the final policy + +1. Run the representative file and full scope. +2. Confirm files/tests/skips/snapshots and coverage are unchanged. +3. Repeat enough runs to distinguish a real win from machine noise. +4. Remove experiment-only allowlists, aliases, caches, and diagnostic output. +5. Explain the retained policy next to the config in terms of measured build versus runtime behavior. diff --git a/plugins/rstack/skills/migrate-to-rstest/references/dependency-install-gate.md b/plugins/rstack/skills/migrate-to-rstest/references/dependency-install-gate.md new file mode 100644 index 0000000..bacec74 --- /dev/null +++ b/plugins/rstack/skills/migrate-to-rstest/references/dependency-install-gate.md @@ -0,0 +1,50 @@ +# Dependency Install Gate + +Use this reference when running the dependency install gate step of the migration workflow. + +## Quick path + +Use the repo's native package manager; do not add a detector package. Pick it from `packageManager`, then lock files (`pnpm-lock.yaml`, `yarn.lock`, `package-lock.json`, `npm-shrinkwrap.json`, `bun.lock`, `bun.lockb`), then CI/workspace hints. + +After adding `@rstest/core`, verify through a local-only path: e.g. `pnpm exec rstest -h`, a migrated test script, or `./node_modules/.bin/rstest -h` for npm-only repos. Avoid commands that can fetch a remote `rstest` package. + +Record the exact resolved `@rstest/core` version. Prefer its installed types and version-matched local documentation over latest online examples. If only latest docs are available, verify every nontrivial option against the installed config types or CLI help before editing tests around a missing capability. + +## Dependency decisions + +Install only what the migrated scope needs: `@rstest/core`, one capability-supported coverage provider if coverage is enabled, and an adapter only when the existing Rsbuild/Rslib/Rspack config and peer ranges support it. + +Keep Jest/Vitest and legacy coverage packages until the migrated scope is green. Remove them only during cleanup and only if no other scope still uses them. Do not add multiple Rstest coverage providers for one final scope unless the repo intentionally keeps multiple coverage modes. + +In a TypeScript project-reference monorepo, install/link the target's workspace dependency closure when the repo manager supports it (for pnpm, commonly `--filter ...`). Installing only the leaf package can leave referenced projects without the new test globals and produce misleading type errors. Keep the migration scope unchanged; the wider install/link step is not permission to edit those dependencies. + +Before installation, record whether the lockfile is clean. Filtered pnpm installs can still re-resolve peer contexts across a large monorepo. Afterward: + +1. Inspect the lockfile diff separately from the package diff. +2. Keep the target importer and every package/snapshot node required by a frozen install. +3. Remove unrelated resolver churn only when the lockfile was clean before the migration and the repository has a safe lockfile-merge or regeneration workflow. +4. Verify with the native frozen/offline install path when available. + +Never leave a hand-crafted importer that references a missing peer-context snapshot merely because the package is already linked in `node_modules`. + +## Version and capability gate + +Before debugging test failures, choose a compatible Rstest line and avoid latest-only config on older targets: + +- Latest Rstest needs Node `^20.19.0 || >=22.12.0` and the Rsbuild/Rspack 2.x ecosystem. For Rsbuild/Rspack 1.x or older Node projects, use a compatible older line such as Rstest 0.8.x unless the user accepts a toolchain upgrade. +- If the target line lacks a needed feature, either use the fallback or ask whether to upgrade first. +- Prefer config-level fallbacks on older targets: plain project objects, `output.externals`/aliases/manual config, Istanbul coverage, config/projects for env splits, explicit/manual mocks, config `pool.maxWorkers`, or manual Rspack config. +- Treat `defineInlineProject`, `output.bundleDependencies`, `detectAsyncLeaks`, V8 coverage, file-level env comments, newer mock helpers, CLI `--pool.maxWorkers`, and `@rstest/adapter-rspack` as latest-line features unless target docs/types prove support. +- Choose Rsbuild plugins and Rstest adapters by peer dependency compatibility, not package-name major equality. In monorepos, check root and package-level overrides/resolutions, lockfile entries, and nested package managers for duplicate majors. + +Inspect with the repo-native manager across workspaces (for example `pnpm -r list ... --depth Infinity`, or `npm ls --all` plus filtering). If errors mention config schema, plugin hooks, compiler mismatch, missing plugin APIs, or peer conflicts, fix dependency versions first; do not rewrite tests to hide toolchain skew. + +## Published versus local builds + +A local Rstest checkout can be useful for source inspection, unreleased diagnostics, or confirming a suspected runner bug. Label every result produced by it, keep it out of dependency and lockfile cleanup, and rerun final correctness and performance validation with the project's resolved package. Do not compare a local development build with the previous runner and present it as the migrated project's result. + +## Blocked mode + +If install/check fails, stop broad edits. Do not mix package managers or fake a migration without a runnable local `rstest` binary unless the user accepts a config-only patch. + +Report the failed command, error class, chosen package-manager signal, files already changed, next command, and resume point. diff --git a/plugins/rstack/skills/migrate-to-rstest/references/detect-test-framework.md b/plugins/rstack/skills/migrate-to-rstest/references/detect-test-framework.md new file mode 100644 index 0000000..ca8f52d --- /dev/null +++ b/plugins/rstack/skills/migrate-to-rstest/references/detect-test-framework.md @@ -0,0 +1,59 @@ +# Detect Test Framework + +Use this reference to decide the migration path and scope. + +## Detection signals + +### Jest signals + +- `jest` in `dependencies` or `devDependencies` +- `jest.config.*` exists +- `package.json` contains a `jest` config block +- test scripts use `jest` +- test code imports from `@jest/globals` or uses `jest.` APIs +- Jest-only packages such as `ts-jest`, `babel-jest`, `jest-environment-jsdom`, `identity-obj-proxy`, `@types/jest`, `jest-junit` + +### Vitest signals + +- `vitest` in `dependencies` or `devDependencies` +- `vitest.config.*` exists +- `vite.config.*` contains a `test` block for Vitest +- `vitest.workspace.*` exists +- test scripts use `vitest`, `vitest run`, or `vitest --run` +- test code imports from `vitest` (`vi`, `vitest`, `describe`, `it`, `expect`) +- Vitest-only packages such as `@vitest/coverage-v8`, `@vitest/coverage-istanbul`, `@vitest/ui` + +### Rstack integration signals + +- Existing `rslib.config.*`, `rsbuild.config.*`, or Rspack/Rsbuild aliases/plugins usually means adapters or config ports should be tried before test edits. +- `rspack.config.*` can use `@rstest/adapter-rspack` only on a compatible Rspack 2.x line; for Rspack 1.x / Rstest 0.8.x, port needed settings manually. +- `@rsbuild/core`, `@rspack/core`, `@rslib/core`, `@rsbuild/plugin-*`, `overrides`, `resolutions`, or `pnpm.overrides` must feed into the dependency gate before choosing Rstest/adapters/plugins. + +### Browser / DOM signals + +- Jest `testEnvironment: 'jsdom'`, Vitest `environment: 'jsdom'` / `happy-dom`, or file-level environment comments. +- React/Vue Testing Library setup files. +- `@testing-library/jest-dom/vitest` should be replaced with direct matcher registration in Rstest setup. + +### Migration-risk profile + +Before copying a nearby example, record the target's shape: + +- Runner and config topology: standalone, root aggregator, or multi-project. +- Default and file-level environments, including mixed Node/DOM scopes. +- Test-file count and one representative dependency-heavy file. +- React/Vue/JSX, CSS preprocessors, assets, and browser APIs. +- Global setup mocks and whether they fully replace heavy workspace boundaries. +- Workspace source imports, package exports without build output, ESM/CommonJS mix, and existing aliases. + +Use a pilot only when its runner, environment, build integration, and dependency shape are materially similar. A small Node or Rslib pilot is not a performance/config template for a large mixed UI package. + +## Decision rules + +- If only one runner is detected, migrate that runner path. +- If both are detected, treat as mixed mode and migrate one scope at a time (package/suite/project). +- Prefer migrating the currently CI-critical or higher-failure scope first. +- Keep both legacy runners until each migrated scope is green on Rstest. +- Choose the Rstest target line with the dependency gate before changing adapter/plugin versions. +- In monorepos, choose the smallest runnable scope that has its own package/config/test script. Do not migrate unrelated packages just because they share a root lockfile. +- If the root config is only a project/workspace aggregator, preserve that role: Rstest root `projects` config is not itself a test project unless the root is explicitly included as a project. diff --git a/plugins/rstack/skills/migrate-to-rstest/references/discovery-parity.md b/plugins/rstack/skills/migrate-to-rstest/references/discovery-parity.md new file mode 100644 index 0000000..000e586 --- /dev/null +++ b/plugins/rstack/skills/migrate-to-rstest/references/discovery-parity.md @@ -0,0 +1,51 @@ +# Test Discovery Parity + +Use this reference for every migration. A green run is not equivalent when Jest, Vitest, and Rstest discover different files or preserve different skips/excludes. + +## Capture the pre-migration manifest + +Before editing config, record: + +- Relative test-file paths. +- Test, skip/todo, snapshot, and project counts from a normal run. +- `roots`, `testMatch`, `testRegex`, include/exclude/ignore patterns, projects/workspaces, CLI filters, and file-level environments. +- The reason and history for every explicit whole-file exclude when it is discoverable locally. + +Use the previous runner's supported list command when available. For Jest, `jest --listTests --json` is usually suitable. For Vitest versions with a list command, use that version's documented file-only/JSON mode; otherwise preserve the normal run output. Store temporary manifests outside the working tree or remove them after comparison. + +## Capture the Rstest manifest + +After the config loads, prefer the installed version's machine-readable list support: + +```bash +rstest list --filesOnly +rstest list --json=./rstest-tests.json +``` + +Check `rstest --help` first on older target lines. If list support is unavailable, capture the normal run's file list without upgrading solely for this step. + +Normalize both manifests to paths relative to the same scope root, sort them, and compare exact sets. Then run the tests and compare counts; file parity alone does not prove case or skip parity. + +## Classify every difference + +For each added or removed file, decide whether it is: + +- An intentional scope change approved by the user. +- A legacy `roots`/include omission now exposed by Rstest defaults. +- A historical exclude that still represents a real incompatibility. +- A generated, fixture, integration, local-only, or environment-specific file that should remain out of scope. +- An accidental discovery regression caused by config translation. + +Do not silently preserve historical excludes, and do not silently expand the scope. Run newly included files explicitly before deciding. Record any deliberate difference in the migration summary. + +## Guardrails + +- Do not use `passWithNoTests` as evidence that discovery works. +- Do not compare performance until the manifests match or the difference is clearly labeled. +- Do not lower coverage thresholds to accommodate newly discovered files. +- Do not remove an exclude merely because its original comment looks old; inspect its history and run the file. +- When migrating one monorepo package, confirm root project aggregation does not pull unrelated packages into the manifest. + +## Final evidence + +Report both the previous and final file/test/skip counts. If Rstest intentionally covers more tests, provide same-scope performance separately from final expanded-scope performance. diff --git a/plugins/rstack/skills/migrate-to-rstest/references/global-api-migration.md b/plugins/rstack/skills/migrate-to-rstest/references/global-api-migration.md new file mode 100644 index 0000000..eeb62a4 --- /dev/null +++ b/plugins/rstack/skills/migrate-to-rstest/references/global-api-migration.md @@ -0,0 +1,61 @@ +# Global API Migration + +Use this reference when tests rely on globally available test APIs (Jest's `jest.`, or Vitest's `vi.` / `vitest.` under `globals: true`). + +For identifier mapping (`jest.` / `vi.` / `vitest.` -> Rstest equivalents), see the official guides: + +- Jest: https://rstest.rs/guide/migration/jest.md (see "Test API") +- Vitest: https://rstest.rs/guide/migration/vitest.md (see "Test API" and "Global APIs") + +The rules below are skill-side enforcement on top of that mapping. + +## Mapping policy + +- Prefer imports from `@rstest/core`; use globals only when preserving global-style tests (`globals: true` plus `@rstest/core/globals` types). +- Rstest globals include test APIs, hooks, `rs`, and `rstest`. Prefer `rs` for migration consistency. + +## Typed mock functions + +Do not mechanically preserve Jest's two-generic `jest.fn()` form. Rstest types the complete function signature: + +```ts +// Jest +jest.fn(); + +// Rstest +rs.fn<(input: Input) => Result>(); +``` + +When inference is sufficient, remove the explicit generic. Otherwise express optional, rest, and overloaded parameters in the function type, then run the scope's TypeScript build. A passing runtime test does not prove the migrated mock type is valid. + +## Red lines + +1. **No shims.** All forms below leave the migration incomplete and must be rejected in every test and setup file: + - `globalThis.vi = rs;` / `global.jest = rs;` (direct global aliasing) + - `const vi = rs;` / `const jest = rs;` (local rebinding) + - `import { rs as vi } from '@rstest/core';` / `import { rs as jest } from '@rstest/core';` (aliased named import) + + Fix at every call site instead. Do not propose a shim "just to keep the diff small" - it hides whether the migration actually happened, blocks IDE refactors, and silences future deprecation warnings. + +2. **No test-name mutation.** When rewriting `vi.` / `jest.` / `vitest.`, only replace identifiers that precede a property access and eventual call expression. After every batch edit, grep test declarations to confirm no name string was rewritten: + +```bash +rg -n "(describe|it|test)\\(" --glob '*.{js,jsx,ts,tsx,mjs,cjs,mts,cts}' +``` + +Test names are stable identifiers, not labels for the new API. + +3. **No mixed local aliases.** Avoid mixing `vi` and `rs`, or `jest` and `rs`, in the same migrated file. If you touch a file, migrate all framework utility calls in that file. + +## Safer replacement workflow + +1. Search first: + +```bash +rg -n "\\b(vi|vitest|jest)\\s*\\." --glob '*.{js,jsx,ts,tsx,mjs,cjs,mts,cts}' +``` + +2. Replace only code identifiers, not arbitrary strings or snapshots. +3. Update imports from `vitest` / `@jest/globals` to `@rstest/core` as needed. +4. Run the search again; the only acceptable leftovers are comments documenting the migration or untouched legacy scopes. +5. If `globals: true` remains, verify TypeScript sees `@rstest/core/globals`. diff --git a/plugins/rstack/skills/migrate-to-rstest/references/jest-migration-deltas.md b/plugins/rstack/skills/migrate-to-rstest/references/jest-migration-deltas.md new file mode 100644 index 0000000..016616b --- /dev/null +++ b/plugins/rstack/skills/migrate-to-rstest/references/jest-migration-deltas.md @@ -0,0 +1,52 @@ +# Jest Migration Deltas + +Use this reference when the current framework is Jest. + +## Source of truth + +Use the official guide for exact field/API mappings: + +https://rstest.rs/guide/migration/jest.md + +When local docs are available, prefer the checked-out source, for example `website/docs/en/guide/migration/jest.mdx`. + +## High-signal deltas + +- Scripts: `jest` -> `rstest`; `--watch` / `--watchAll` -> `rstest --watch`; `--runInBand` -> `--pool.maxWorkers 1` only when supported, otherwise config `pool.maxWorkers: 1`; Jest `-w` means workers, but Rstest `-w` means watch. +- Config: create `rstest.config.ts` with `defineConfig` from `@rstest/core`. Map every important Jest field through the official guide; do not silently drop unknown fields. +- Transforms: remove `preset`, `ts-jest`, and most `transform` config where possible. Rstest uses SWC by default; use version-supported SWC/output/Rsbuild plugin config only when needed. +- Setup: merge Jest `setupFiles` and `setupFilesAfterEnv` into Rstest `setupFiles` because Rstest setup runs after framework registration. +- Globals/APIs: `@jest/globals` -> `@rstest/core`; `jest.` -> `rs.`. If globals remain, set `globals: true` and add `@rstest/core/globals` types. +- Async tests: `done` callback tests are unsupported; convert to Promise or `async` / `await`. +- Hooks: `beforeEach` / `beforeAll` return values are cleanup functions in Rstest. Wrap setup-only arrow expressions in braces when needed. +- Environment: `testEnvironmentOptions` becomes `testEnvironment: { name, options }`. File-level env comments are latest-only; older targets should split env-specific files into config/projects. +- CJS mocking: use `rs.mockRequire()` for code paths using `require()`. +- Coverage: install a Rstest provider supported by the target version. Jest `babel` maps to Istanbul; Jest `v8` maps to V8 only when the `dependency-install-gate.md` capability gate allows it. + +## Classify Jest virtual mocks + +Jest's third argument `{ virtual: true }` has no direct Rstest equivalent. Classify each occurrence before adding an alias: + +- If the request is genuinely absent at runtime, use the narrow version-supported Rstest virtual-module mechanism or an exact alias/stub, then test discovery and execution. +- If the request resolves through workspace source, package exports, TypeScript paths, or inherited Rsbuild/Rslib aliases, remove `{ virtual: true }` and keep the normal `rs.mock()` / `rs.doMock()`; do not add a redundant alias. +- If resolution is uncertain, ask the migrated config to resolve or run the narrow test first. Treat the resulting resolver error as evidence instead of assuming every Jest virtual mock refers to a nonexistent module. + +Recheck dynamic-import and `resetModules` cases because their mock-registration order can differ from statically imported tests. + +## Classify resolver and discovery config + +Do not mechanically translate every `moduleNameMapper` entry into an Rstest alias. Classify each mapping first: + +- Preserve semantic path aliases used by source/build tooling. +- Replace CSS, asset, or module stubs with the narrowest equivalent only when tests rely on the stubbed behavior. +- Treat mappings for workspace exports without build output, ESM transformation, or Jest-only resolver gaps as legacy workarounds. Start Rstest without them and add a mapping only after reproducing a real resolution failure. + +After the scope is green, remove temporary aliases one at a time and rerun the affected file plus the full scope. A working alias is not evidence that it is still necessary. + +Audit Jest `roots`, `testMatch`/`testRegex`, `testPathIgnorePatterns`, project filters, and CLI selection with `discovery-parity.md`. Rstest defaults can discover valid tests that Jest never ran; preserve or expand that scope only through an explicit, tested decision. + +## Jest-specific enforcement + +1. Delete scope-local `jest.config.*`, `jest.setup.*`, and companion `jest.*.ts` only after the migrated scope is green. Drop shared Jest devDeps only after no scope still uses Jest. +2. Defer snapshot re-recording until all non-snapshot failures are fixed. Jest `:` snapshot key separators become Rstest `>`, so early `rstest -u` creates noisy churn. +3. Review snapshot diffs by body, not key churn. Separator-only key renames are formatting; body changes are behavior signals. diff --git a/plugins/rstack/skills/migrate-to-rstest/references/mocked-module-build-graph.md b/plugins/rstack/skills/migrate-to-rstest/references/mocked-module-build-graph.md new file mode 100644 index 0000000..2acbbbc --- /dev/null +++ b/plugins/rstack/skills/migrate-to-rstest/references/mocked-module-build-graph.md @@ -0,0 +1,70 @@ +# Mocked Modules and the Build Graph + + + +Use this reference when Rstest has high build time, runtime initialization cost, or memory even though an expensive module is replaced with `rs.mock()`. + +## Source of truth + +- Rstest module mocking: https://rstest.rs/api/runtime-api/rstest/mock-modules +- Rstest output configuration: https://rstest.rs/config/build/output + +## Why the module is still compiled + +Treat `rs.mock()` as runtime module replacement, not build-graph pruning. Rspack discovers the static import while building the test and can still compile the real module and its reachable source graph before Rstest installs or uses the runtime mock. + +This commonly appears when a small component test fully mocks a renderer, editor, document processor, or other module whose real implementation imports a large package or source tree. The test execution can be cheap while the build remains expensive. + +## Run the narrow experiment + +1. Confirm that the heavy module is fully mocked and its real exports, initialization side effects, and coverage are not part of the test's intent. +2. Add only the exact mocked import request to `output.externals`. Do not externalize the large transitive package first: cutting the graph at the already-mocked boundary is narrower and preserves more normal bundling behavior. +3. Run the affected test file with the same Node version, coverage, workers, and cache state. Compare passed tests, peak RSS, and build time with the baseline. +4. Keep the rule only when behavior remains identical and the resource reduction is material. A large RSS/build-time drop with the same tests passing is strong evidence that compiling the unused real module graph was the primary cost. + +When bundle-utilization diagnostics are available for the installed or explicitly tested local build, use them to confirm that the real assets are built but not evaluated. Otherwise use the exact external experiment as the proof; do not infer safety from bundle size alone. + +For a package or stable alias import, the experiment can be as small as: + +```ts +import { defineConfig } from '@rstest/core'; + +const fullyMockedModule = '@app/MarkdownRender'; + +export default defineConfig({ + output: { + externals: [fullyMockedModule], + }, +}); +``` + +Match the request string seen by Rspack. If the import uses a relative path, an alias, or multiple package paths, inspect the effective requests and use the narrowest supported string, regular expression, object, or function rule rather than a broad name fragment. + +## Match the runtime module format + +The simple string form uses the target's default external type. When the runtime mock must satisfy an explicitly CommonJS external, and the installed output config supports typed object values, declare the type on that rule: + +```ts +export default defineConfig({ + output: { + externals: [ + { + '@app/MarkdownRender': 'commonjs @app/MarkdownRender', + }, + ], + }, +}); +``` + +Use the module format required by the built test runtime; do not copy `commonjs` into an ESM-only setup without validation. When every rule explicitly declares its type, a global `externalsType` is normally redundant. Verify against the installed Rstest/Rsbuild types and run behavior because older target lines can expose different config shapes. + +## Scope and safety checks + +`output.externals` applies to the whole Rstest config or project, not only the test file that motivated it. Before keeping the rule: + +- Run every test in that config/project which imports the externalized module, including tests that may not mock it. +- Do not use this optimization for partial mocks, `importActual`, `{ spy: true }`, or tests that exercise the real module or its side effects. +- Confirm the mock matches the same module request and is registered before the tested code loads it. A missed mock may make the runtime resolve an externalized local TypeScript/source module without Rspack transformation. +- Recheck module aliases, ESM/CommonJS interop, package export conditions, snapshots, and coverage. + +If only one subset fully mocks the module, isolate that subset as a separate Rstest project with its own `output.externals` rule instead of weakening bundling for unrelated tests. diff --git a/plugins/rstack/skills/migrate-to-rstest/references/performance-diagnosis.md b/plugins/rstack/skills/migrate-to-rstest/references/performance-diagnosis.md new file mode 100644 index 0000000..c33c5f1 --- /dev/null +++ b/plugins/rstack/skills/migrate-to-rstest/references/performance-diagnosis.md @@ -0,0 +1,78 @@ +# Migration Performance Diagnosis + +Use this migration-specific fallback after the scope is semantically green when Rstest materially regresses in CLI wall time, runner time, build, test runtime, output volume, or memory. When the `rstest-debugging` skill is available, load it instead: it is the canonical and more complete performance workflow. Keep this reference so `migrate-to-rstest` remains useful when installed alone. + +## Protect the comparison + +Use the same machine, Node version, installed runner version, test manifest, coverage mode, cache state, environment, and worker settings. Record the exact command and working directory. + +Measure both a representative file and the full scope. Prefer several alternating runs when differences are small. A local Rstest checkout may be used for labeled diagnostics, but final correctness and performance claims must use the project's installed dependency. + +Record separately: + +- CLI wall time from process start to exit. +- Runner-reported duration. +- Build and tests/runtime durations when reported. +- Files/tests/skips. +- Output lines/bytes when logs are material. +- Memory only with a declared method. + +Do not add worker durations together when they overlap. + +## Trace before tuning + +When the installed target supports it, run: + +```bash +rstest run --trace +``` + +Use the generated summary first, then the Perfetto trace to inspect overlap. Relevant per-file phases include `prepare`, `envSetup`, `load`, `setupFiles`, `collect`, `tests`, `coverage`, and `teardown`; host build spans are separate. Runner duration is not necessarily the entire CLI wall, which can also include config loading, compiler setup, reporter I/O, worker startup/shutdown, and process teardown. + +Use `DEBUG=rstest` to inspect resolved config and temporary build output. Starting with Rstest 0.11.7, `DEBUG=rstest:bundle-coverage` can write an experimental per-test asset manifest under `.rstest`; with V8 coverage enabled, it also records raw V8 data for correlating carried assets with executed code. Without V8 coverage, `rawV8` is `null`. Load `rstest-debugging` for the canonical workflow and output fields. This interface was introduced by [Rstest PR #1694](https://github.com/web-infra-dev/rstest/pull/1694) and may change, so check the installed version and output schema, and do not use the diagnostic run for final timing claims. + +## Classify the bottleneck + +### Build or host startup + +Signals: host build spans dominate, temporary output is large, or one representative file is already slow. + +Inspect entry/issuer chains, styles/assets, plugins, and dependency bundling. Use Rsdoctor when the compiler distribution remains unclear. + +### Runtime load, setup, or collect + +Signals: `load`, `setupFiles`, or `collect` grows with test-file count while test bodies are cheap. + +Inspect repeated Node CJS/ESM loading, global setup imports, compile-time globals, and mocked modules whose real graphs were still built. Read `dependency-bundling-performance.md` and `mocked-module-build-graph.md`. Full dependency bundling can outperform externalization here. + +### Test bodies or hooks + +Signals: `tests` dominates a few files or cases. + +Use verbose output/trace to isolate them before changing bundle config. Check real I/O, timers, retries, oversized fixtures, and repeated `beforeEach` work without changing the test's intent. + +### CLI overhead outside runner reporting + +Signals: runner duration improves but CLI wall remains high. + +Measure reporter output, worker/process startup and teardown, config loading, trace/debug overhead, and open handles. Do not attribute the unexplained gap to build without lifecycle evidence. + +## Run a single-variable experiment loop + +Choose experiments from evidence, commonly in this order: + +1. Exact fully mocked boundaries in `output.externals`. +2. `bundleDependencies` default versus `false` versus `true`. +3. Asset emission when tests do not inspect emitted files. +4. `forks` versus `threads`, preserving isolation and checking full-suite stability. +5. Successful-test log capture such as `silent: 'passed-only'` when the target version supports it. + +Keep pool, workers, cache, and unrelated config fixed around each experiment. Do not treat logging changes as a stable speedup without repeated data; their primary benefit may be usability. + +## Measure memory honestly + +Aggregate controller/worker RSS can double-count shared pages. Prefer cgroup peak memory on Linux or `footprint` on macOS when claiming physical-memory changes. If the previous runner was not measured with the same method, report only Rstest's absolute observation and do not claim a regression or improvement. + +## Finalize + +Re-run the representative file and full scope with the final minimal config and installed Rstest. Report same-scope results separately from any expanded discovery scope, and remove traces, profiles, caches, debug hooks, benchmark-only settings, local package links, and `.rstest` files created by diagnostics. Remove the `.rstest` directory only if the diagnostic run created it and no pre-existing or unrelated files remain. diff --git a/plugins/rstack/skills/migrate-to-rstest/references/vitest-migration-deltas.md b/plugins/rstack/skills/migrate-to-rstest/references/vitest-migration-deltas.md new file mode 100644 index 0000000..c64fc26 --- /dev/null +++ b/plugins/rstack/skills/migrate-to-rstest/references/vitest-migration-deltas.md @@ -0,0 +1,49 @@ +# Vitest Migration Deltas + +Use this reference when the current framework is Vitest. + +## Source of truth + +Use the official guide for exact field/API mappings: + +https://rstest.rs/guide/migration/vitest.md + +When local docs are available, prefer the checked-out source, for example `website/docs/en/guide/migration/vitest.mdx`. + +## High-signal deltas + +- Scripts: `vitest run` / `vitest --run` -> `rstest`; plain Rstest already runs once and exits. Watch mode is `rstest --watch` or `rstest watch`. +- Config imports: `defineConfig` comes from `@rstest/core`; `defineWorkspace` is removed; use the `projects` field. +- Config composition: replace Vitest `mergeConfig` with capability-supported `mergeRstestConfig`; use `mergeProjectConfig` when composing project entries. Do not import shared configuration from `vitest/config` into an Rstest config. +- Projects: use `defineInlineProject({ name, ... })` only when the `dependency-install-gate.md` capability gate allows it; otherwise use plain named objects. Use `defineProject` for top-level project config exports. +- Config shape: remove Vitest's `test` wrapper and inspect every nested field. Move supported fields to the Rstest top level, rename `environment` to `testEnvironment`, fold `environmentOptions` into its object form, and report unsupported fields rather than dropping them silently. +- Mock lifecycle: Vitest `mockReset` maps to Rstest `resetMocks`; `clearMocks` and `restoreMocks` keep their names. Do not confuse `maxConcurrency` inside one file with `pool.maxWorkers` across files. +- Global setup: Rstest calls setup without Vitest's `TestProject` argument. Rewrite uses of `provide`, `inject`, or `onTestsRerun`; use static `env` or deliberately propagated `process.env` values only when equivalent. +- Coverage: add a provider supported by the target version and use `coverage.reporters` (plural). Preserve include/exclude and thresholds exactly; remove Vitest-only provider fields only after reporting that they have no equivalent. Replace `@vitest/coverage-*` only during cleanup after the Rstest scope is green. +- Reporters: replace Vitest-only reporters; import third-party reporter classes instead of passing incompatible names. +- Setup: replace `@testing-library/jest-dom/vitest` with matcher registration via `expect.extend(...)` from `@rstest/core`. +- Globals/APIs: imports from `vitest` -> `@rstest/core`; `vi.` / `vitest.` -> `rs.`. Avoid mixing `vi` and `rs` in a migrated file. +- Mocks: translate `vi.hoisted`, `vi.mocked`, `vi.doMock`, timers, globals, and environment helpers through the installed Rstest API rather than assuming every Vitest helper exists. `rs.mock('./module')` auto-mocking behavior is version-gated; use explicit factories/manual mocks or `{ mock: true }` according to the target capability. +- Async mock factories: Rstest does not support returning an async function when mocking a module value. Migrate Vitest patterns that await the actual module inside the factory to static `importActual` imports plus a synchronous factory. +- CJS mocking: use `rs.mockRequire()` / `rs.doMockRequire()` for `require()` paths. +- Snapshots: preserve existing snapshot bodies and custom serializers. Move `snapshotSerializers` registration into setup via `expect.addSnapshotSerializer`; verify any custom `resolveSnapshotPath` signature against Rstest before reuse. + +## Build config + +- Rstest uses Rsbuild/Rspack instead of Vite/Rollup. Prefer an Rslib/Rsbuild adapter when the project already has that config. Otherwise translate Vite `define` to `source.define`, test aliases to `resolve.alias`, and dependency inline/external intent to measured `output.bundleDependencies` / `output.externals` behavior. +- Classify aliases before copying them. Preserve semantic source aliases; do not duplicate aliases already inherited from an adapter; remove Vitest/Vite-only resolver workarounds unless Rstest reproduces the failure. +- Do not carry Vite plugins into Rstest. Replace each required capability with an adapter, Rsbuild plugin, or Rstest/SWC behavior. Do not add a React plugin solely because Vitest used `@vitejs/plugin-react`; first verify whether the project's JSX/runtime/style graph already works. +- Treat Vitest dependency inline/external config as intent, not a direct mapping. Rstest bundling can trade compiler work for repeated Node runtime loading; use `dependency-bundling-performance.md` for a measured decision. + +## Discovery and mock-graph parity + +Compare Vitest and Rstest test manifests with `discovery-parity.md`, including workspace/project entries, include/exclude, CLI project filters, and file-level environment comments. Do not silently expand or shrink scope. + +Vitest and Rstest both hoist runtime mocks, but a fully mocked module can still enter Rstest's Rspack build graph. If a heavy mock boundary affects build/runtime cost, use `mocked-module-build-graph.md`; never externalize partial mocks or `importActual` paths. + +## Vitest-specific enforcement + +1. Delete scope-local `vitest.config.*` and truly legacy `vitest.setup.*` only after the migrated scope is green. If a setup file was rewritten and is still referenced by Rstest `setupFiles`, rename or copy it to a Rstest-owned name such as `rstest.setup.*` before deleting the legacy Vitest-named file. Drop shared `vitest.workspace.*`, root shared config, and `@vitest/*` devDeps only after no scope still uses Vitest. +2. Do not re-record Vitest snapshots just to update headers. Vitest and Rstest snapshot files are byte-compatible below the header line; run `-u` only for expected body diffs. +3. In a partial monorepo migration, keep shared Vitest config and root Vitest dependencies for untouched projects. Copy only the migrated scope's effective settings into an independent Rstest config. +4. Do not carry the Vite mental model into Rstest. Prefer adapters and Rsbuild/Rspack config translations over custom test rewrites. diff --git a/plugins/rstack/skills/review-context-change/SKILL.md b/plugins/rstack/skills/review-context-change/SKILL.md new file mode 100644 index 0000000..6a048ed --- /dev/null +++ b/plugins/rstack/skills/review-context-change/SKILL.md @@ -0,0 +1,16 @@ +--- +name: review-context-change +description: Use when comparing two compatible Rstack lint or test snapshots, including freshness, diagnostics, test outcomes, and stored lint fix previews. +--- + +# Review a context change + +1. Call `project_status`, match the package by `context.packageRoot`, and use `snapshot_list` with that `contextId`. +2. Select two completed snapshots for the same producer, context, package root, config, and capture selection. The list is newest-first; pass the older ID as `leftSnapshotId`. +3. If a pair is missing, explain which consent-gated `lint_snapshot` or `test_snapshot` supplies it, including `packageRoot` and nonstandard `configPath`. +4. Call `snapshot_diff` with `diagnostics` for Rslint or `tests` for Rstest. Stop on incompatibility and report every reason. +5. Report both freshness values before the delta. Lead with new failures, then resolved items, then lower-severity or timing changes. +6. Call `code_evidence` for one changed file when exact-path diagnostics, statically related tests, or aggregate execution evidence helps. Keep every axis separate from the snapshot delta. +7. Use `lint_fix_preview` only as review material and never apply it. + +Do not run a capture without approval. Recommend an explicit `rs lint`, `rs test`, or `rs test list --related` verification command. diff --git a/plugins/rstack/skills/rsbuild-best-practices/SKILL.md b/plugins/rstack/skills/rsbuild-best-practices/SKILL.md new file mode 100644 index 0000000..b0fdcc6 --- /dev/null +++ b/plugins/rstack/skills/rsbuild-best-practices/SKILL.md @@ -0,0 +1,57 @@ +--- +name: rsbuild-best-practices +description: Rsbuild best practices for config, CLI workflow, type checking, bundle optimization, assets, and debugging. Use when writing, reviewing, or troubleshooting Rsbuild projects. +--- + +# Rsbuild Best Practices + +Apply these rules when writing or reviewing Rsbuild projects. + +## Configuration + +- Use `rsbuild.config.ts` and `defineConfig` +- Use `tools.rspack` or `tools.bundlerChain` only when no first-class Rsbuild option exists +- Define explicit `source.entry` values for multi-page applications +- In TypeScript projects, prefer `tsconfig.json` path aliases first + +## CLI + +- Use `rsbuild` for local development +- Use `rsbuild build` for production build +- Use `rsbuild preview` only for local production preview +- Use `rsbuild inspect` to inspect final Rsbuild/Rspack configs + +## Type checking + +- Use `@rsbuild/plugin-type-check` for integrated dev/build type checks +- Or run `tsc --noEmit`/`vue-tsc --noEmit` as an explicit script step + +## Bundle size optimization + +- Prefer dynamic `import()` for non-critical code paths +- Prefer lightweight libraries where possible +- Keep browserslist aligned with real compatibility requirements + +## Asset management + +- Import source-managed assets from project source directories, not from `public` +- Reference `public` files by absolute URL path + +## Security + +- Do not publish `.map` files to public servers/CDNs when production source maps are enabled + +## Debugging + +- Run with `DEBUG=rsbuild` when diagnosing config resolution or plugin behavior +- Read generated files in `dist/.rsbuild` to confirm final config, not assumed config + +## Profiling + +- Use Node CPU profiling (`--cpu-prof`) when JavaScript-side overhead is suspected +- Use `RSPACK_PROFILE=OVERVIEW` and analyze trace output for compiler-phase bottlenecks + +## Documentation + +- For the latest (v2) docs, read http://rsbuild.rs/llms.txt +- For Rsbuild v1 docs, read http://v1.rsbuild.rs/llms.txt diff --git a/plugins/rstack/skills/rsbuild-v2-upgrade/SKILL.md b/plugins/rstack/skills/rsbuild-v2-upgrade/SKILL.md new file mode 100644 index 0000000..72a36aa --- /dev/null +++ b/plugins/rstack/skills/rsbuild-v2-upgrade/SKILL.md @@ -0,0 +1,34 @@ +--- +name: rsbuild-v2-upgrade +description: Use when upgrading a Rsbuild 1.x project to v2, including dependency and configuration updates. +--- + +# Rsbuild v1 to v2 Upgrade + +## Workflow + +1. **Confirm current setup** + - Read `package.json` to identify Rsbuild and plugin packages in use. + - Locate the Rsbuild config file (commonly `rsbuild.config.(ts|js|mjs|cjs)`). + +2. **Open the official upgrade guide** + - Use the v1 → v2 guide as the source of truth: + - https://rsbuild.rs/guide/upgrade/v1-to-v2 + +3. **Plan the upgrade path** + - Compare the current project config with the migration guide. + - List breaking changes that apply to the project’s current config and plugins. + - Note any removed or renamed options, defaults, or plugin APIs. + +4. **Update dependencies** + - Bump `@rsbuild/core` to v2 + - Bump Rsbuild plugins to latest versions via `npx taze major --include /rsbuild/ -w -r` + +5. **Apply config and code changes** + - Update the Rsbuild config to match v2 options and defaults. + - Remove deprecated or unsupported settings. + +6. **Validate** + - Run the build and dev commands. + - Run project tests or type checks. + - Fix any warnings or errors surfaced by the new version. diff --git a/plugins/rstack/skills/rsdoctor-analysis/SKILL.md b/plugins/rstack/skills/rsdoctor-analysis/SKILL.md new file mode 100644 index 0000000..e908d24 --- /dev/null +++ b/plugins/rstack/skills/rsdoctor-analysis/SKILL.md @@ -0,0 +1,122 @@ +--- +name: rsdoctor-analysis +description: Use when analyzing Rspack/Webpack bundles from local `rsdoctor-data.json` and producing evidence-based optimization recommendations. +--- + +# Rsdoctor Analysis Assistant Skill + +When the installed Rstack plugin exposes Rstack Context and the project has a matching context, +prefer its `analyze-build` workflow. It binds the explicit artifact to project/build observations and +uses the maintained in-process Rsdoctor tool catalog. Continue with this standalone workflow when +Rstack Context is unavailable or the user only has an explicit `rsdoctor-data.json`. + +Use the globally installed `rsdoctor-agent` CLI from `@rsdoctor/agent-cli` only after a real `rsdoctor-data.json` path exists. Keep analysis read-only unless the user explicitly asks for install/config setup. + +Response order (required): High-Priority Issues -> Proposed Solutions -> Optional Reference-Chain Follow-up Choices -> Next Deep-Dive Issue Categories (Not commands). + +## Core Workflow + +1. Reuse current-session results and valid `.rsdoctor-analysis-cache.json` entries before doing new work. +2. Locate `rsdoctor-data.json` fast: user-provided path, then `dist/rsdoctor-data.json`, `output/rsdoctor-data.json`, `static/rsdoctor-data.json`, `.rsdoctor/rsdoctor-data.json`, then one bounded `rg --files` search excluding `node_modules` and `.git`. Treat `manifest.json` only as an index. +3. If data exists, skip all plugin version/config/build generation logic. Update cache when useful. +4. If data is missing, stop analysis: do not run `rsdoctor-agent` analysis commands, do not run the Analysis Gate, and either ask for the data path or run the Generation Gate below only when setup/generation is required. +5. After a real data file exists, run Analysis Gate at most once before the first `rsdoctor-agent` data-fetch command: verify global `@rsdoctor/agent-cli` with `npm view @rsdoctor/agent-cli version` and `rsdoctor-agent --version`; install latest only if missing/outdated, a version-related error occurs, or the user asks to refresh. +6. Fetch only the Default Evidence Set first; run independent fetches in parallel when possible. +7. Run the ROI Triage Gate below before selecting deep-dive commands or recommendations. Use it to rank issue categories by measured impact, then synthesize findings in the required response order. + +Performance rules: parallelize independent checks, cache only derived facts (`dataFile`, `dataFileMtime`, `pluginName`, `pluginVersion`, dependency/config/plugin modification times), and invalidate cache when paths disappear, modification times change, the user asks to refresh, or cached values fail. Speculative plugin checks must not trigger generation; use them only after confirming the data file is missing. + +## ROI Triage Gate + +Before recommending fixes, classify the current build into broad cost buckets and choose the highest-ROI lever from evidence, not intuition. This gate is generic for Rspack/Webpack projects; do not use framework-specific runtime layers unless the user's project exposes them in the data. + +| Cost bucket | Evidence source | First lever | +| -------------------------- | ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| Assets/media | `assetsTop`, `assets media`, `chunkGraph.assets` | Compress, convert, deduplicate, subset, or lazy-load large assets | +| Large packages/modules | `packagesTop`, module/package size fields | Replace heavy packages, use deep imports, split non-critical code, or review direct dependency choices | +| Duplicate/cross-chunk cost | E1001/E1002, `packages duplicates`, cross-chunk package summaries | Deduplicate versions, tune package resolution, or adjust `splitChunks`/cache groups | +| Tree-shaking waste | `retainedModulesTop`, retained CJS/barrel/side-effects modules | Fix CJS/barrel imports, sideEffects declarations, or package entrypoints | +| Build-time cost | `buildCost`, loaders/plugins/directories cost data | Optimize loaders/plugins, cache, watcher, or source-map/dev settings; prioritize only when the user asks about build performance | + +Decision rules: + +- Report the measured breakdown first when it changes recommendation priority. +- Start with the largest bucket that maps to a practical fix; a smaller issue should not outrank a larger one unless the larger one is expected or intentionally unavoidable. +- Treat issuer/reference-chain tracing as second-pass work. Run it only when a high-impact candidate needs ownership evidence, or when the user asks "why" / "who imported this". +- Do not present aggregate rule output as sufficient evidence for a fix that requires a concrete file, package, chunk, size, or dependency path. +- If the largest bucket is structural or intentionally required, say that it is a wall and name the external change that would be needed instead of inventing low-impact source edits. + +## Generation Gate + +Identify `pluginName` (`@rsdoctor/rspack-plugin` or `@rsdoctor/webpack-plugin`) and determine `pluginVersion` from local files first: `package.json`, lockfile, then `node_modules//package.json`; use `pnpm why` / `npm ls` only as fallback. + +Use this exact if/else decision tree; do not merge branches: + +```text +if pluginName is missing: + install/register the matching Rsdoctor plugin, then configure output.mode='brief' and output.options.type=['json']; build with RSDOCTOR=true only +else if pluginVersion is unknown: + resolve pluginVersion first; if still unknown, configure output.mode='brief' and output.options.type=['json']; build with RSDOCTOR=true only +else if pluginVersion >= 1.5.11: + do not edit plugin config just for JSON; build with RSDOCTOR_OUTPUT=json and RSDOCTOR=true if needed +else: # pluginVersion < 1.5.11 + MUST configure output.mode='brief' and output.options.type=['json']; build with RSDOCTOR=true only +``` + +Preflight every build command: `RSDOCTOR_OUTPUT=json` is allowed only in the `pluginVersion >= 1.5.11` branch. For missing, unknown, or `< 1.5.11`, it is forbidden. For `< 1.5.11`, generating `rsdoctor-data.json` requires the plugin config below: + +```ts +output: { + mode: 'brief', + options: { + type: ['json'], + }, +} +``` + +## Evidence and Command Bounds + +Default Evidence Set: + +| Summary key | Evidence source | Bounds | +| -------------------- | ------------------------------------------ | --------------------------------------------- | +| `buildCost` | `build summary` | filtered fields only | +| `assetsTop` | top assets by raw/gzip size | fixed Top-N | +| `packagesTop` | top packages by gzip size | fixed Top-N; avoid full `packages list` pages | +| `duplicatePackages` | E1001 duplicate package summary | first-pass summary only | +| `crossChunkPackages` | E1002 cross-chunk duplication summary | first-pass summary only | +| `retainedModulesTop` | `tree-shaking retained-modules --limit 10` | filtered fields only; no `--compact` | + +Scope rules: + +- Use `rsdoctor-agent` for bundle data access only after `rsdoctor-data.json` exists; prefer parallel independent fetches; bound output with `--filter`, pagination, and `--limit`. +- Default analysis stays within the Default Evidence Set. For non-default analysis, choose minimal fields from [references/rsdoctor-data-types.md](references/rsdoctor-data-types.md) and patterns from [references/common-analysis-patterns.md](references/common-analysis-patterns.md). +- Treat chain tracing, broad commands, optimization edits, splitChunks experiments, and build re-runs as opt-in follow-ups that require user confirmation. +- For duplicate packages and tree-shaking issues, identify issues first; trace reference/import chains only after user confirmation. +- Prefer `tree-shaking retained-modules --emitted-only --category side-effects --limit 10` with narrow `--filter` for side-effects investigations. +- For retained emitted modules, use `tree-shaking retained-modules` with `--emitted-only`, bounded `--category`, `--sort gzipSize`, `--limit`, and narrow `--filter`; do not pass `--compact`. +- Use `tree-shaking summary` only as fallback for missing fields or aggregate context. Treat `tree-shaking bailout-reasons` as high-volume; run it only when explicitly requested and pass target `--modules` (max 100). +- If any command exceeds `5k` tokens, `500 KB` raw output, or a few hundred transcript lines, stop broad fetching and switch to targeted compact queries. + +## Output and Recovery + +Output format: + +1. Issues found in the current build and recommended fixes: + - Group each issue with its fix recommendation. + - Include concrete evidence (size/time/count/path/rule code) and priority. + - For duplicate packages and tree-shaking issues, include a short "continue tracing vs stop here" choice. +2. Whether deeper analysis is still needed: + - List remaining issue categories only, not commands. + +For Top-N insights, prefer a table: `Name | Volume/Time | Count | Recommendation`. + +Recovery rules: + +- `rsdoctor-data.json` missing: do not run `rsdoctor-agent`; ask for the data path or run Generation Gate, then use the matching install reference if setup is needed. +- Command not found: run Analysis Gate, then retry with `rsdoctor-agent`. +- `query` reports unknown tool: run `list` and use a catalog tool name, or switch to direct ` ` mode. +- JSON read error: verify file path, JSON validity, and permissions. +- In Codex, do not run `install`, `build`, global CLI installation, version checks, or `rsdoctor-agent...` inside sandbox. Run Rsdoctor CLI setup and data-fetch commands outside sandbox so they can access project files and dependencies normally. + +References: commands/options [references/command-map.md](references/command-map.md); install/config/data location [references/install-rsdoctor.md](references/install-rsdoctor.md), [references/install-rsdoctor-rspack.md](references/install-rsdoctor-rspack.md), [references/install-rsdoctor-webpack.md](references/install-rsdoctor-webpack.md), [references/install-rsdoctor-common.md](references/install-rsdoctor-common.md); raw data fields [references/rsdoctor-data-types.md](references/rsdoctor-data-types.md); common patterns [references/common-analysis-patterns.md](references/common-analysis-patterns.md). diff --git a/plugins/rstack/skills/rsdoctor-analysis/references/command-map.md b/plugins/rstack/skills/rsdoctor-analysis/references/command-map.md new file mode 100644 index 0000000..67ae5da --- /dev/null +++ b/plugins/rstack/skills/rsdoctor-analysis/references/command-map.md @@ -0,0 +1,113 @@ +# Rsdoctor Skill Command Map + +Stable CLI entry: + +- Install and verify the global CLI first: + - `npm view @rsdoctor/agent-cli version` + - `rsdoctor-agent --version` + - If missing or outdated: `npm install -g @rsdoctor/agent-cli@latest` +- Run data-fetch commands directly with `rsdoctor-agent [options]`. + +Top-level command mode: + +- `list` +- `query --data-file [--input ]` + +`query` catalog (current): + +- `chunks_list` +- `packages_direct_dependencies` +- `packages_duplicates` +- `packages_similar` +- `build_summary` +- `bundle_optimize` +- `errors_list` +- `tree_shaking_summary` + +Option scopes: + +- `--data-file `: + - required for `query`, direct ` `, and `ai ` + - not required for `list`, `ai --describe`, `ai --schema` +- `--input `: optional for `query` +- `--filter <...>`: supported by every data-fetch function; use it to return only required fields selected from `@rsdoctor/types` / [rsdoctor-data-types.md](rsdoctor-data-types.md) +- `--compact`: add whenever possible to keep CLI JSON compact. Do not use it with `tree-shaking retained-modules`; use `--filter` and `--limit` instead. + +## Chunks + +- `chunks list` -> List all chunks. Pagination: `--page-number`, `--page-size` +- `chunks by-id --id ` -> Get chunk detail by numeric id +- `chunks large` -> Find oversized chunks. High-noise in default analysis; avoid unless the user asks for chunk deep dive. + +## Modules + +- `modules by-id --id ` -> Module detail by id +- `modules by-path --path ""` -> Module lookup by path +- `modules issuer --id ` -> Issuer/import chain (recommended as second-pass, after user confirms chain tracing) +- `modules exports` -> Module exports info +- `modules side-effects` -> Non-tree-shakeable modules. Fallback only for side-effects analysis; use `--page-size 10`, narrow filters, and stop if output exceeds `5k` tokens or `500 KB`. + +## Packages + +- `packages list` -> Package list with size/duplication info. Do not read full pages in default analysis; use fixed Top-N package summaries or narrowly filtered/package-targeted queries. +- `packages by-name --name ` -> Package lookup by name +- `packages dependencies` -> Dependency graph. Pagination: `--page-number`, `--page-size` +- `packages direct-dependencies` -> Direct third-party package dependencies imported by project/local packages. Tool name: `packages_direct_dependencies` +- `packages duplicates` -> Duplicate package detection (first-pass summary before optional chain tracing) +- `packages similar` -> Similar package detection + +## Assets + +- `assets list` -> Asset list with size info +- `assets diff --baseline --current ` -> Compare two builds +- `assets media` -> Media optimization guidance + +## Loaders + +- `loaders hot-files` -> Slowest loader/file pairs. Options: `--page-number`, `--page-size`, `--min-costs` +- `loaders directories` -> Loader times by directory. Options: `--page-number`, `--page-size`, `--min-total-costs` + +## Build + +- `build summary` -> Build summary and costs +- `build entrypoints` -> Entrypoints +- `build config` -> Build config snapshot +- `build optimize` -> Bundle optimization inputs. High-noise in default analysis; avoid unless the user asks for bundle optimization deep dive or default evidence is insufficient. Options: `--step`, `--side-effects-page-number`, `--side-effects-page-size` (recommend `--side-effects-page-size 10`). + +## Bundle + +- `bundle optimize` -> Alias of `build optimize` + +## Errors + +- `errors list` -> All errors and warnings +- `errors by-code --code ` -> Filter by code. For default E1001/E1002 summaries, prefer local JSON summarization. +- `errors by-level --level ` -> Filter by level + +## Rules + +- `rules list` -> Rule scan results + +## Server + +- `server port` -> Current JSON data file path + +## Tree-Shaking + +- `tree-shaking summary` -> Overall tree-shaking health summary (can be very large; filter with fields from `rsdoctor-data-types`, compact where useful, and use aggregated results) +- `tree-shaking retained-modules` -> Retained emitted modules by category for tree-shaking diagnosis. Useful options: `--emitted-only`, `--category cjs,barrel,side-effects`, `--sort gzipSize`, `--limit `, and `--filter id,path,packageName,version,category,size,chunks,bailoutReason,recommendation`. Does not support `--compact`. +- `tree-shaking bailout-reasons --modules ` -> Non-tree-shakeable modules by bailout reason for the provided modules. High-volume; only run when explicitly requested, always pass `--modules`, and include at most 100 modules per command. +- `tree-shaking exports-analysis` -> Export-level tree-shaking opportunities + +`tree-shaking retained-modules` returns retained module rows with: + +| Field | Meaning | +| ------------------------- | --------------------------------------------- | +| `id` | Module id | +| `path` | Module path | +| `packageName` / `version` | Owning package | +| `category` | `cjs`, `barrel`, `side-effects`, or `unknown` | +| `size` | Source, parsed, and gzip sizes when available | +| `chunks` | Chunk id/name/assets | +| `bailoutReason` | Original bailout/retention reason | +| `recommendation` | Optional short recommendation | diff --git a/plugins/rstack/skills/rsdoctor-analysis/references/common-analysis-patterns.md b/plugins/rstack/skills/rsdoctor-analysis/references/common-analysis-patterns.md new file mode 100644 index 0000000..4d110b0 --- /dev/null +++ b/plugins/rstack/skills/rsdoctor-analysis/references/common-analysis-patterns.md @@ -0,0 +1,222 @@ +# Common Analysis Patterns + +Use this reference for common Rspack/Webpack bundle analysis questions after locating `rsdoctor-data.json`. + +## ROI-Based Lever Selection + +Use this pattern before choosing detailed analysis commands. The goal is to find the biggest measured cost bucket first, then select the smallest follow-up that can prove or fix that bucket. + +Recommended triage order: + +1. Compare assets/media, top packages/modules, duplicate or cross-chunk cost, retained tree-shaking waste, and build-time cost. +2. Pick the largest bucket with a plausible project-side fix. +3. Fetch only the narrow supporting evidence required for that bucket. +4. Defer issuer/reference-chain tracing until it will change ownership, confidence, or the recommended fix. +5. If the largest bucket is expected or structurally required, call it out as a wall and move to the next meaningful bucket. + +Lever map: + +| Dominant bucket | High-ROI first pass | Lower-ROI or second pass | +| ------------------------- | ----------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | +| Assets/media | Compress images, convert formats, deduplicate repeated assets, subset fonts, lazy-load non-critical media | JS tree-shaking work that saves only tens of KB | +| One or few large packages | Check direct dependency necessity, lighter alternatives, deep imports, async boundaries, or route-level splitting | Broad package-list browsing without a top offender | +| Duplicate packages | Use `packages duplicates` / E1001 evidence, then resolve compatible versions or alias deliberately | Reference-chain tracing before the duplicate size is proven material | +| Cross-chunk duplication | Inspect E1002/cross-chunk summaries, then consider `splitChunks`/cache group changes or shared vendor extraction | Splitting small shared modules that add request overhead | +| Retained modules | Start with emitted CJS/barrel/side-effects rows sorted by gzip size | Deleting source that is already eliminated by the bundler | +| Build-time cost | Use loader/plugin/directories timing and optimize the measured slow path | Build-performance advice when the user asked only about shipped bundle size | + +Recommendation rules: + +- Prefer `gzipSize` for shipped-user cost and `parsedSize` for parse/execute or code-volume discussions. +- For Webpack/Rspack chunk recommendations, distinguish initial/entry chunks from async chunks. A large async chunk is usually lower priority than a large initial chunk unless the user asks about that route or interaction. +- For package replacement advice, require direct dependency evidence or clear ownership. Do not recommend replacing an indirect package just because it appears in `packageGraph`. +- For duplicate-package advice, mention compatibility risk when versions cross major/minor boundaries or when packages may ship resources as side effects. +- For retained-module advice, include the category (`cjs`, `barrel`, `side-effects`) and the largest concrete paths before suggesting config or source changes. +- Stop when the likely savings are below the user's stated goal or clearly smaller than another measured bucket. + +## Similar Packages + +Use direct dependency package data to inspect similar packages. Start with `packages direct-dependencies` or `query packages_direct_dependencies`, then check known package families and other potentially similar packages. + +Suggested flow: + +1. Fetch direct dependency package data with `packages direct-dependencies` or `query packages_direct_dependencies`. Use `--filter` fields for package name, version, issuer/dependency relation, and size when available. +2. Treat this direct dependency list as the replacement-candidate set. Do not make replacement recommendations from indirect package-only evidence. +3. Check the known families below. The presence of one package from a family is fine; only consider replacement when multiple packages from the same family are present. +4. After known-family checks, inspect the direct dependency list for other potentially similar packages not listed below. Treat these as candidates only when package purpose overlaps clearly; avoid speculative replacement advice. +5. Use `packages similar` or `query packages_similar` as an additional signal, not the only source of evidence. + +Similar package families: + +1. `lodash`, `lodash-es` + - Consider migrating from `lodash` to `lodash-es` for better tree-shaking support when both are present. +2. `dayjs`, `moment`, `date-fns`, `js-joda` + - Consider replacing `moment` with `dayjs` for smaller bundle size when both are present and project requirements allow it. +3. `antd`, `material-ui`, `semantic-ui-react`, `arco-design` +4. `axios`, `node-fetch` +5. `redux`, `mobx`, `zustand`, `recoil`, `jotai` +6. `chalk`, `colors`, `picocolors`, `kleur` +7. `fs-extra`, `graceful-fs` + +If there are no similar packages, simply say there are no similar packages. Do not list packages that merely exist in the project. + +Keep the response simple: name only coexisting known-family packages or other direct-dependency candidates with clear overlap, explain why coexistence is worth reviewing, and give one replacement direction if the evidence supports it. + +## Media Asset Analysis + +Use `assets media` or `bundle optimize` when checking oversized image, font, or video assets. Return recommendations only for assets that are actually oversized or relevant to the user's question. + +Image thresholds: + +- Mobile: one image file should ideally be under `60 KB`; Base64 SVG should ideally be under `7 KB`. +- PC: one image file should ideally be under `200 KB`; Base64 SVG should ideally be under `20 KB`. + +Image recommendations: + +- Compress large images with image compression tools such as `@rsbuild/plugin-image-compress` (`svgo` for SVG and `@napi-rs/image` for other images). +- Optimize SVG paths with tools such as SVGO. +- Consider whether SVG is necessary for the asset. +- Choose formats by compression characteristics: + - PNG works best for images with few colors and sharp boundaries, such as text or simple patterns. + - JPG works best for natural images with gradients and irregular transitions, such as landscapes and portraits. + - Base64 is suitable for important small images that should avoid extra requests and render immediately. Base64 increases binary size by about one third, but after gzip the increase is usually no more than about 10%. + +Font thresholds: + +- Prefer `.woff2`. +- Keep a single font file under `100 KB` when possible. +- Keep total font size under `300 KB` for the page, and under `200 KB` for mobile when possible. +- Avoid font formats other than `ttf`, `woff`, and `woff2` unless there is a compatibility requirement. + +Font recommendations: + +- Prefer system fonts when custom fonts are not required. +- Use `font-display: swap` or `@font-face unicode-range` for more efficient loading. +- Ensure server-side Gzip or Brotli compression is enabled. +- Consider variable fonts when they replace multiple weight or width files. + +Video thresholds: + +- Keep a single video file under `500 KB` when possible. +- Keep total video resources loaded on a page under `1 MB` when possible. + +Video recommendations: + +- Compress video files with tools such as HandBrake or FFmpeg. +- Use MP4 (H.264) as the compatibility default. +- Use WebM (VP9) when modern-browser compression benefits justify it. +- Lazy-load non-critical videos. +- Use HLS or DASH for long videos. +- Remove unused videos. +- Tune `preload`: + - `none`: do not download until playback starts; useful for videos unlikely to be played. + - `metadata`: downloads metadata only, often around 3% of file size. + - `auto`: downloads the full video; use only when playback is very likely. + +## Bundle Optimize + +Use `bundle optimize` / `build optimize` as an aggregate optimization pass. It can combine evidence from: + +- Duplicate package rules (`getRuleInfo` / `errors list` / rule details). +- Similar package checks (`packages similar` / `query packages_similar`). +- Media asset checks (`assets media`). +- Chunk checks (`chunks list`, `chunks large`, or chunk details) for oversized resources and `splitChunks` recommendations. + +Do not run `bundle optimize` / `build optimize` in the default analysis path. Use it only for a user-requested optimization deep dive or when the compact default evidence set is missing required fields. + +When using it, keep output compact with `--compact`, narrow `--filter` fields, and pagination options such as `--side-effects-page-size 10`. If the command still returns thousands of lines, stop and switch to narrower supporting commands. + +Do not treat aggregate output as enough by itself when the recommendation needs concrete evidence. Fetch the narrow supporting data before recommending a config or dependency change. + +## Build Performance + +Use these as short recommendation candidates when Rsdoctor evidence points to build-time cost, loader cost, too many modules, or slow dev rebuilds. Source: [Rsbuild build performance guide](https://rsbuild.rs/zh/guide/optimization/build-performance). + +- Start with build performance analysis. Use measured bottlenecks before recommending config changes. Use Rsdoctor loader costs data. +- General improvements: upgrade Rsbuild, enable `performance.buildCache` for faster rebuilds, reduce module count, and keep Tailwind CSS v3 `content` narrow and correct. +- Tooling choices: prefer SWC over Babel transforms, avoid Less-heavy pipelines when possible, and prefer faster minification such as Rsbuild/Rspack SWC minification over Terser when compatible. +- Sass handling: do not send already-built `node_modules/**/*.css` through `sass-loader`; prefer third-party `dist/*.css` outputs when available. Compile third-party `.scss` / `.sass` only when Sass source features are required, such as variables, mixins, functions, or theme customization, and use an allowlist for those packages instead of all `node_modules`. +- Less projects: if many Less files are present, consider `@rsbuild/plugin-less` parallel compilation. +- Development mode: consider `dev.lazyCompilation`, Rspack `experiments.nativeWatcher`, cheaper or disabled dev source maps, and a narrower development Browserslist. +- Rsdoctor loader evidence: if `sass-loader` time is concentrated in third-party package directories and those packages ship CSS artifacts, recommend importing the CSS artifact or narrowing Sass rule `include` to app source plus specific allowlisted theme packages. +- Call out tradeoffs: development Browserslist and source map changes can make dev output differ from production or reduce debugging detail. + +## Retained Module Tree-shaking Analysis + +Use `tree-shaking retained-modules` for first-pass tree-shaking evidence when the goal is to find retained emitted modules by reason category. Prefer it over broad `tree-shaking summary` when the user asks for top retained modules, CommonJS retention, barrel imports, side effects, or gzip-size priority. + +Recommended first-pass command shape: + +```bash +rsdoctor-agent tree-shaking retained-modules \ + --data-file dist/rsdoctor-data.json \ + --emitted-only \ + --category cjs,barrel,side-effects \ + --sort gzipSize \ + --limit 10 \ + --filter id,path,packageName,version,category,size,chunks,bailoutReason,recommendation +``` + +Guidance: + +1. Keep `--emitted-only` by default so findings map to shipped bundle impact. +2. Use `--category cjs,barrel,side-effects` for optimization scans; narrow `--category` when the user asks about one class. +3. Sort by `gzipSize` for bundle impact, unless the user asks for source or parsed size. +4. Keep `--limit` bounded. Use `--limit 10` for default analysis. Increase to `50` only for user-requested deep dives. +5. Do not add `--compact`; `tree-shaking retained-modules` output size is controlled with `--limit` and `--filter`. +6. Report rows as `Path | Package | Category | Gzip/Parsed Size | Chunks | Bailout | Recommendation`. +7. Treat results as first-pass evidence. Use `modules issuer` only after the user asks to trace who imported a retained module. + +## Common Questions + +### Why is a module not tree-shaken? + +Example: "Why is `node_modules/rc-tree/lib/util.js` not tree-shaken?" + +- Start with `tree-shaking retained-modules` filtered to id, path, package, category, size, chunks, bailout reason, and recommendation when the module appears in emitted output. +- Use `tree-shaking summary` only when retained modules do not include the needed field or the question needs broader aggregate context. +- Return the module's `bailoutReason`. +- Explain the bailout in plain language. +- Show `issuerPath` only when the user asks for chain tracing or when it is necessary to explain the issue. + +### Who imported a module? + +Example: "Who imported `lodash-es/constant.js`?" + +- Use module lookup by path, then issuer/import-chain data. +- Show the dependency chain using arrow notation or a tree. + +### Show modules with side effects + +Example: "Show all modules with side effects." + +- Prefer `tree-shaking retained-modules --emitted-only --category side-effects` for emitted side-effect modules. +- Fall back to `tree-shaking summary` or the relevant module-side-effects command only when retained-module output is insufficient. +- Filter to module id, path, package, size, chunks, bailout reason, and recommendation. +- List modules with non-empty `bailoutReason` containing `side_effects`. +- Use `--limit 10` for default output. Increase only after user confirmation. +- If falling back to `tree-shaking summary` or `modules side-effects`, use `--page-size 10` or `--side-effects-page-size 10`, keep the same narrow fields, and stop expanding when one command exceeds `5k` tokens or `500 KB` raw output. +- Sort by size, give priority to the largest emitted modules. + +### Why is a package duplicated? + +Example: "Why is package X duplicated?" + +- Use duplicate package rule data (`E1001` / `E1002`) and package graph fields. +- Show which chunks and modules contain the duplicate versions. +- Explain the dependency path if the user asks to continue chain tracing. + +### Which modules are not tree-shaken because of side effects? + +- Use the E1007 rule results directly to identify modules that are not tree-shaken due to side effects. By `tree-shaking summary`. +- If further details are needed, you may also use `tree-shaking retained-modules --emitted-only --category side-effects` and filter to module id, path, package, size, chunks, bailout reason, and recommendation. +- List modules with `bailoutReason` containing `side_effects`. +- Use `--limit 10` by default and the same `5k` token / `500 KB` raw-output stop rule for fallback commands. +- Show `issuerPath` when needed to identify the import source. + +## Output Style + +- For dependency chains, use a tree or arrow notation. +- For module details, use a table or key-value list. +- For explanations, use concise, plain language. +- Avoid listing all packages or assets when the finding is empty. diff --git a/plugins/rstack/skills/rsdoctor-analysis/references/install-rsdoctor-common.md b/plugins/rstack/skills/rsdoctor-analysis/references/install-rsdoctor-common.md new file mode 100644 index 0000000..5e3b5c2 --- /dev/null +++ b/plugins/rstack/skills/rsdoctor-analysis/references/install-rsdoctor-common.md @@ -0,0 +1,88 @@ +# Common Steps for Rsdoctor Installation + +This document contains common steps that apply to both Rspack and Webpack projects. + +## Step 3: Locate the rsdoctor-data.json + +First, use the fast path to check whether `rsdoctor-data.json` already exists. If the user provided a path, check that path first. Otherwise check common build artifact/output locations before any package-manager, install, config, or build command: + +- `dist/rsdoctor-data.json` (most common) +- `output/rsdoctor-data.json` +- `static/rsdoctor-data.json` +- `.rsdoctor/rsdoctor-data.json` (if using custom reportDir) + +If common paths do not contain the file, run one bounded local file search that excludes expensive directories, for example `rg --files -g 'rsdoctor-data.json' -g '!node_modules' -g '!**/.git/**'`. If `rsdoctor-data.json` is found, skip the generation/version gate and go directly to JSON analysis. If it is not found, do not run any `rsdoctor-agent` analysis command; ask for the data path or generate the file first. + +For repeated analysis in the same repository, use a lightweight project-local cache such as `.rsdoctor-analysis-cache.json`. Reuse it only when the cached data-file path still exists and cached modification times match the current `rsdoctor-data.json`, dependency files (`package.json`, lock files), relevant build config files, and plugin `package.json` if recorded. Refresh the cache after locating a new data file or confirming a plugin version. + +When the runtime supports parallel execution, run independent local initialization checks concurrently: common path checks, bounded `rg --files` lookup, and local plugin-version file reads. Do not run `rsdoctor-agent` CLI checks until a real `rsdoctor-data.json` path exists. Treat plugin-version results as speculative until the data file is confirmed missing; never let parallel plugin checks trigger generation by themselves. + +If you cannot find the file, ask the user to provide the path to `rsdoctor-data.json`. If the file truly does not exist and generation/setup is required, check the installed `@rsdoctor/rspack-plugin` or `@rsdoctor/webpack-plugin` version before changing config or running a build. The `@rsdoctor/agent-cli` version does not prove plugin support for `RSDOCTOR_OUTPUT=json`. + +Required version gate (use exactly this if/else order): + +1. Identify `pluginName`: `@rsdoctor/rspack-plugin` or `@rsdoctor/webpack-plugin`. +2. Determine `pluginVersion` from local files first: dependency declarations in `package.json`, lockfile entries, then installed `node_modules//package.json`. Use package-manager output such as `pnpm why @rsdoctor/rspack-plugin` / `npm ls @rsdoctor/rspack-plugin` only as a fallback. +3. Choose one branch; do not merge branches: + +```text +if pluginName is missing: + install/register the matching Rsdoctor plugin, then MUST configure output.mode='brief' and output.options.type=['json']; build with RSDOCTOR=true only +else if pluginVersion is unknown: + resolve pluginVersion first; if still unknown, MUST configure output.mode='brief' and output.options.type=['json']; build with RSDOCTOR=true only +else if pluginVersion >= 1.5.11: + do not modify plugin config just for JSON; build with RSDOCTOR_OUTPUT=json and RSDOCTOR=true if needed +else: # pluginVersion < 1.5.11 + MUST configure output.mode='brief' and output.options.type=['json']; build with RSDOCTOR=true only +``` + +Preflight every build command: `RSDOCTOR_OUTPUT=json` is allowed only in the `pluginVersion >= 1.5.11` branch. For missing, unknown, or `< 1.5.11` versions, a command such as `RSDOCTOR_OUTPUT=json RSDOCTOR=true pnpm run build:rspack` is incorrect. + +The file is typically generated in the same directory as your build output (e.g., `dist`, `output`, `static`). For plugin versions < `1.5.11`, configure a custom output directory using the `output.reportDir` option: + +```js +// Example for Rspack: use RsdoctorRspackPlugin +// Example for Webpack: use RsdoctorWebpackPlugin +new RsdoctorRspackPlugin({ + // or RsdoctorWebpackPlugin + disableClientServer: true, + output: { + mode: 'brief', + options: { + type: ['json'], + }, + reportDir: './dist', // Custom output directory (defaults to build output directory) + }, +}); +``` + +--- + +## Step 4: Use JSON file for analysis + +Once you have the `rsdoctor-data.json` file, you can use it for analysis. This JSON file contains all the build analysis data and can be used without starting the Rsdoctor server. + +Analyze the JSON file with the `rsdoctor-agent` CLI from the repository root or another directory that can access the JSON file: + +```bash +rsdoctor-agent build summary --data-file ./dist/rsdoctor-data.json --filter "" +``` + +Keep analysis output small: + +- Reuse already returned results from context/history first, then run only missing queries. +- Use `--filter` on data-fetch commands to return only fields required for the current question. +- Use first-pass summaries for duplicate packages and tree-shaking issues; ask before continuing reference-chain tracing. +- Stop broad commands when one response exceeds `5k` tokens (o200k_base) or `500 KB` raw output, then switch to filtered or targeted queries. + +For command names, option scopes, tree-shaking command selection, and `--filter` guidance, use [command-map.md](command-map.md). For raw data field names, use [rsdoctor-data-types.md](rsdoctor-data-types.md). + +**Benefits of JSON Mode:** + +- ✅ No need to keep the build process running +- ✅ Works in CI/CD environments +- ✅ Can be shared and version controlled +- ✅ Faster analysis for large projects +- ✅ No server connection required + +Note: `rsdoctor-data.json` can be large in complex projects. Add it to `.gitignore` if you do not want to commit it. diff --git a/plugins/rstack/skills/rsdoctor-analysis/references/install-rsdoctor-rspack.md b/plugins/rstack/skills/rsdoctor-analysis/references/install-rsdoctor-rspack.md new file mode 100644 index 0000000..b9bd20c --- /dev/null +++ b/plugins/rstack/skills/rsdoctor-analysis/references/install-rsdoctor-rspack.md @@ -0,0 +1,138 @@ +# Install Rsdoctor Plugin for Rspack Projects + +This guide covers installation for Rspack-based projects, including: + +- Rspack CLI +- Rsbuild +- Modern.js +- Rslib +- Rspress + +## Step 1: Install Dependencies + +For projects based on Rspack, such as Rsbuild or Rslib: + +**Note:** Prefer using the latest versions of the above dependencies when available. + +```bash +npm add @rsdoctor/rspack-plugin -D +pnpm add @rsdoctor/rspack-plugin -D +``` + +## Step 2: Register Plugin + +After the dependency installation, check the installed `@rsdoctor/rspack-plugin` version before changing config or running a build. Do not infer plugin capabilities from `@rsdoctor/agent-cli --version`. + +Required version gate (use exactly this if/else order): + +1. Set `pluginName = '@rsdoctor/rspack-plugin'`. +2. Determine `pluginVersion` from local files first: dependency declarations in `package.json`, lockfile entries, then `node_modules/@rsdoctor/rspack-plugin/package.json` if installed. Use `pnpm why @rsdoctor/rspack-plugin` / `npm ls @rsdoctor/rspack-plugin` only as a fallback. When repeating analysis, reuse a valid `.rsdoctor-analysis-cache.json` plugin entry before re-reading files; invalidate it if `package.json`, lock files, or the plugin package file modification time changed. +3. Choose one branch; do not merge branches: + +```text +if @rsdoctor/rspack-plugin is missing: + install/register @rsdoctor/rspack-plugin, then configure output.mode='brief' and output.options.type=['json']; build with RSDOCTOR=true only +else if pluginVersion is unknown: + resolve pluginVersion first; if still unknown, configure output.mode='brief' and output.options.type=['json']; build with RSDOCTOR=true only +else if pluginVersion >= 1.5.11: + do not modify plugin config just for JSON; build with RSDOCTOR_OUTPUT=json and RSDOCTOR=true if needed +else: # pluginVersion < 1.5.11 + MUST configure output.mode='brief' and output.options.type=['json']; build with RSDOCTOR=true only +``` + +Preflight every build command: `RSDOCTOR_OUTPUT=json` is allowed only in the `pluginVersion >= 1.5.11` branch. For missing, unknown, or `< 1.5.11`, `RSDOCTOR_OUTPUT=json` is forbidden. For example, when `@rsdoctor/rspack-plugin` is `1.5.7`, this command is incorrect: + +```bash +RSDOCTOR_OUTPUT=json RSDOCTOR=true pnpm run build:rspack +``` + +Below are configuration examples for old Rspack plugin versions, unknown versions, missing plugins, or projects that still need to register the plugin: + +### Rspack CLI + +Initialize the plugin in the [plugins](https://www.rspack.rs/config/plugins.html#plugins) of `rspack.config.ts`: + +```ts title="rspack.config.ts" +import { RsdoctorRspackPlugin } from '@rsdoctor/rspack-plugin'; + +export default { + plugins: [ + // Only register the plugin when RSDOCTOR is true, as the plugin will increase the build time. + process.env.RSDOCTOR && + new RsdoctorRspackPlugin({ + disableClientServer: true, + // Required for @rsdoctor/rspack-plugin < 1.5.11. + output: { + mode: 'brief', + options: { + type: ['json'], + }, + }, + }), + ], +}; +``` + +### Rsbuild/Rslib/Modern.js + +See [Rsbuild - Use Rsdoctor](https://rsbuild.rs/guide/debug/rsdoctor) for more details. +If this is Modern.js project can see [tools.rspack](https://modernjs.dev/configure/app/tools/rspack) of `modern.config.ts`: + +For `@rsdoctor/rspack-plugin` < `1.5.11`, configure JSON output in `rsbuild.config.ts`: + +```ts title="rsbuild.config.ts" +import { RsdoctorRspackPlugin } from '@rsdoctor/rspack-plugin'; + +export default { + tools: { + rspack: { + plugins: [ + process.env.RSDOCTOR === 'true' && + new RsdoctorRspackPlugin({ + disableClientServer: true, // Prevent starting local server + output: { + mode: 'brief', // Required for plugin versions < 1.5.11 + options: { + type: ['json'], // Only generate JSON data + }, + }, + }), + ], + }, + }, +}; +``` + +### Rspress + +For Rspress projects, configure the plugin in `builderConfig.tools.rspack`: + +```ts title="rspress.config.ts" +import { RsdoctorRspackPlugin } from '@rsdoctor/rspack-plugin'; +import { defineConfig } from 'rspress/config'; + +export default defineConfig({ + builderConfig: { + tools: { + rspack: { + plugins: [ + process.env.RSDOCTOR === 'true' && + new RsdoctorRspackPlugin({ + disableClientServer: true, // Prevent starting local server + output: { + mode: 'brief', // Required for plugin versions < 1.5.11 + options: { + type: ['json'], // Only generate JSON data + }, + }, + }), + ], + }, + }, + }, +}); +``` + +## Step 3 & 4: Locate and Use rsdoctor-data.json + +For steps on locating the `rsdoctor-data.json` file and using it for analysis, see the [common installation guide](./install-rsdoctor-common.md). diff --git a/plugins/rstack/skills/rsdoctor-analysis/references/install-rsdoctor-webpack.md b/plugins/rstack/skills/rsdoctor-analysis/references/install-rsdoctor-webpack.md new file mode 100644 index 0000000..db90f28 --- /dev/null +++ b/plugins/rstack/skills/rsdoctor-analysis/references/install-rsdoctor-webpack.md @@ -0,0 +1,71 @@ +# Install Rsdoctor Plugin for Webpack Projects + +This guide covers installation for Webpack projects (webpack >= 5). + +## Step 1: Install Dependencies + +Rsdoctor only supports webpack >= 5. + +For projects based on webpack: + +```bash +npm add @rsdoctor/webpack-plugin -D +pnpm add @rsdoctor/webpack-plugin -D +``` + +## Step 2: Register Plugin + +After the dependency installation, check the installed `@rsdoctor/webpack-plugin` version before changing config or running a build. Do not infer plugin capabilities from `@rsdoctor/agent-cli --version`. + +Required version gate (use exactly this if/else order): + +1. Set `pluginName = '@rsdoctor/webpack-plugin'`. +2. Determine `pluginVersion` from local files first: dependency declarations in `package.json`, lockfile entries, then `node_modules/@rsdoctor/webpack-plugin/package.json` if installed. Use `pnpm why @rsdoctor/webpack-plugin` / `npm ls @rsdoctor/webpack-plugin` only as a fallback. When repeating analysis, reuse a valid `.rsdoctor-analysis-cache.json` plugin entry before re-reading files; invalidate it if `package.json`, lock files, or the plugin package file modification time changed. +3. Choose one branch; do not merge branches: + +```text +if @rsdoctor/webpack-plugin is missing: + install/register @rsdoctor/webpack-plugin, then configure output.mode='brief' and output.options.type=['json']; build with RSDOCTOR=true only +else if pluginVersion is unknown: + resolve pluginVersion first; if still unknown, configure output.mode='brief' and output.options.type=['json']; build with RSDOCTOR=true only +else if pluginVersion >= 1.5.11: + do not modify plugin config just for JSON; build with RSDOCTOR_OUTPUT=json and RSDOCTOR=true if needed +else: # pluginVersion < 1.5.11 + MUST configure output.mode='brief' and output.options.type=['json']; build with RSDOCTOR=true only +``` + +Preflight every build command: `RSDOCTOR_OUTPUT=json` is allowed only in the `pluginVersion >= 1.5.11` branch. For missing, unknown, or `< 1.5.11`, `RSDOCTOR_OUTPUT=json` is forbidden. For example, when `@rsdoctor/webpack-plugin` is `1.5.7`, this command is incorrect: + +```bash +RSDOCTOR_OUTPUT=json RSDOCTOR=true pnpm run build +``` + +### Webpack + +For old Webpack plugin versions, unknown versions, missing plugins, or projects that still need to register the plugin, initialize it in the [plugins](https://webpack.js.org/configuration/plugins/#plugins) of `webpack.config.js`: + +```js title="webpack.config.js" +const { RsdoctorWebpackPlugin } = require('@rsdoctor/webpack-plugin'); + +module.exports = { + // ... + plugins: [ + // Only register the plugin when RSDOCTOR is true, as the plugin will increase the build time. + process.env.RSDOCTOR && + new RsdoctorWebpackPlugin({ + disableClientServer: true, + // Required for @rsdoctor/webpack-plugin < 1.5.11. + output: { + mode: 'brief', + options: { + type: ['json'], + }, + }, + }), + ].filter(Boolean), +}; +``` + +## Step 3 & 4: Locate and Use rsdoctor-data.json + +For steps on locating the `rsdoctor-data.json` file and using it for analysis, see the [common installation guide](./install-rsdoctor-common.md). diff --git a/plugins/rstack/skills/rsdoctor-analysis/references/install-rsdoctor.md b/plugins/rstack/skills/rsdoctor-analysis/references/install-rsdoctor.md new file mode 100644 index 0000000..3e994e9 --- /dev/null +++ b/plugins/rstack/skills/rsdoctor-analysis/references/install-rsdoctor.md @@ -0,0 +1,39 @@ +# Install Rsdoctor Plugin + +This documentation has been split into project-specific guides: + +## Choose Your Project Type + +- **For Rspack/Rsbuild/Modern.js projects:** See [install-rsdoctor-rspack.md](./install-rsdoctor-rspack.md) +- **For Webpack projects:** See [install-rsdoctor-webpack.md](./install-rsdoctor-webpack.md) + +## Quick Decision Guide + +**Determine your project type:** + +1. **Check project type** (`projectType`): + - If project uses **Rspack** (including Rsbuild, Rslib, or any Rspack-based project) → Use `projectType: 'rspack'` → See [install-rsdoctor-rspack.md](./install-rsdoctor-rspack.md) + - If project uses **Webpack** (webpack >= 5) → Use `projectType: 'webpack'` → See [install-rsdoctor-webpack.md](./install-rsdoctor-webpack.md) + +2. **Check framework** (`framework`): + - If using **Rspack CLI** → `framework: 'rspack'` → See [install-rsdoctor-rspack.md](./install-rsdoctor-rspack.md) + - If using **Rsbuild** → `framework: 'rsbuild'` → See [install-rsdoctor-rspack.md](./install-rsdoctor-rspack.md) + - If using **Modern.js** → `framework: 'modern.js'` → See [install-rsdoctor-rspack.md](./install-rsdoctor-rspack.md) + - If using **Rslib** → `framework: 'rslib'` → See [install-rsdoctor-rspack.md](./install-rsdoctor-rspack.md) + - If using **Rspress** → `framework: 'rspress'` → See [install-rsdoctor-rspack.md](./install-rsdoctor-rspack.md) + - If using **Webpack** → `framework: 'webpack'` → See [install-rsdoctor-webpack.md](./install-rsdoctor-webpack.md) + +**Decision flow:** + +``` +User's project +├─ Is it Rspack-based? (Rsbuild, Rslib, Rspress, etc.) +│ ├─ Yes → projectType: 'rspack' +│ │ ├─ Rspack CLI? → framework: 'rspack' → install-rsdoctor-rspack.md +│ │ ├─ Rsbuild? → framework: 'rsbuild' → install-rsdoctor-rspack.md +│ │ ├─ Rslib? → framework: 'rslib' → install-rsdoctor-rspack.md +│ │ ├─ Rspress? → framework: 'rspress' → install-rsdoctor-rspack.md +│ │ └─ Modern.js? → framework: 'modern.js' → install-rsdoctor-rspack.md +│ └─ No → Is it Webpack >= 5? +│ └─ Yes → projectType: 'webpack', framework: 'webpack' → install-rsdoctor-webpack.md +``` diff --git a/plugins/rstack/skills/rsdoctor-analysis/references/rsdoctor-data-types.md b/plugins/rstack/skills/rsdoctor-analysis/references/rsdoctor-data-types.md new file mode 100644 index 0000000..aa0e906 --- /dev/null +++ b/plugins/rstack/skills/rsdoctor-analysis/references/rsdoctor-data-types.md @@ -0,0 +1,103 @@ +# Rsdoctor Data Type Context + +Use this reference when a task requires understanding raw `rsdoctor-data.json` fields, schema, or nested data attributes. + +## Source of Truth + +Read type definitions from the published npm package `@rsdoctor/types`. Do not use local repository `dist/` artifacts unless the user explicitly asks for local development branch behavior. + +Brief JSON output has this wrapper shape: + +```ts +import type { Manifest, SDK } from '@rsdoctor/types'; + +export interface RsdoctorDataJson { + data: SDK.BuilderStoreData; + clientRoutes: Manifest.RsdoctorManifestClientRoutes[]; +} +``` + +The core payload type is `SDK.BuilderStoreData`. + +## npm Lookup Flow + +Prefer the npm registry/package interface. + +Use the latest published package unless the user gives a specific Rsdoctor package version or asks to match an installed project version. + +```bash +npm view @rsdoctor/types version dist.tarball --json +``` + +If command execution is unavailable, use the registry endpoint directly: + +```text +https://registry.npmjs.org/@rsdoctor%2Ftypes/latest +``` + +Read the `dist.tarball` URL from the response, download the tarball, and inspect `.d.ts` files under `package/dist/`. + +If matching an installed project version is important: + +1. Inspect the project package versions for `@rsdoctor/rspack-plugin`, `@rsdoctor/webpack-plugin`, `@rsdoctor/core`, `@rsdoctor/sdk`, or `@rsdoctor/types`. +2. Query the matching type package: + +```bash +npm view @rsdoctor/types@ version dist.tarball --json +``` + +3. If that exact version does not exist, use the closest compatible published `@rsdoctor/types` version and state the version mismatch. + +## Files to Inspect + +Start here: + +- `package/dist/index.d.ts`: namespace exports. `SDK` comes from `./sdk/index.js`; `Manifest` comes from `./manifest.js`. +- `package/dist/sdk/index.d.ts`: exports all SDK data subtypes. +- `package/dist/sdk/result.d.ts`: defines `BuilderStoreData`, the `rsdoctor-data.json.data` payload. +- `package/dist/manifest.d.ts`: defines client routes and manifest types. + +Then load nested files as needed: + +- `package/dist/sdk/module.d.ts`: `moduleGraph`, modules, dependencies, source ranges, module code, tree-shaking-linked module data. +- `package/dist/sdk/chunk.d.ts`: `chunkGraph`, assets, chunks, entrypoints. +- `package/dist/sdk/package.d.ts`: `packageGraph`, package dependency data, duplicate package reports, other reports. +- `package/dist/sdk/loader.d.ts`: loader timing/input/output data. +- `package/dist/sdk/resolver.d.ts`: resolver data. +- `package/dist/sdk/plugin.d.ts`: plugin hook/tap timing data. +- `package/dist/sdk/summary.d.ts`: build summary/cost data. +- `package/dist/sdk/config.d.ts`: collected bundler config data. +- `package/dist/sdk/envinfo.d.ts`: environment info data. +- `package/dist/rule/data.d.ts`: `errors`/rule store data. + +## Field Map + +`SDK.BuilderStoreData` contains: + +- `hash`: build hash. +- `root`: project root. +- `pid`: process id. +- `envinfo`: environment information. +- `errors`: rule/error store data. +- `configs`: collected bundler config data. +- `summary`: build summary data. +- `resolver`: resolver events. +- `loader`: loader transform events. +- `plugin`: plugin hook/tap events. +- `moduleGraph`: module graph data. +- `chunkGraph`: asset/chunk/entrypoint graph data. +- `packageGraph`: package/dependency graph data. +- `moduleCodeMap`: module source/code map data. +- `treeShaking`: optional tree-shaking data. +- `otherReports`: optional extra report payloads. + +In brief JSON mode, `moduleCodeMap` is normally `{}` and `treeShaking` is normally absent unless generated by a mode that includes it. + +## Usage Guidance + +- Cite the npm package version used when explaining fields. +- Distinguish wrapper fields (`data`, `clientRoutes`) from `SDK.BuilderStoreData` fields. +- When a nested field is unclear, inspect the specific `.d.ts` file instead of guessing. +- Use these types to construct `@rsdoctor/agent-cli --filter` field selections before each data fetch. Prefer the smallest field set that can answer the current question. +- Match filters to the relevant data domain: chunks from `chunkGraph`, modules and tree-shaking module details from `moduleGraph`/`treeShaking`, packages from `packageGraph`, loader cost from `loader`, build cost from `summary`, and rule findings from `errors`. +- For analysis recommendations, prefer `@rsdoctor/agent-cli` commands. Use these types for schema explanation, prompt grounding, or validating raw JSON field names. diff --git a/plugins/rstack/skills/rslib-best-practices/SKILL.md b/plugins/rstack/skills/rslib-best-practices/SKILL.md new file mode 100644 index 0000000..a42d0ea --- /dev/null +++ b/plugins/rstack/skills/rslib-best-practices/SKILL.md @@ -0,0 +1,58 @@ +--- +name: rslib-best-practices +description: Rslib best practices for config, CLI workflow, output, declaration files, dependency handling, build optimization and toolchain integration. Use when writing, reviewing, or troubleshooting Rslib projects. +--- + +# Rslib Best Practices + +Apply these rules when writing or reviewing Rslib library projects. + +## Configuration + +- Use `rslib.config.ts` and `defineConfig` +- Check Rslib-specific configurations first (e.g., `lib.*`), and also leverage Rsbuild configurations (e.g., `source.*`, `output.*`, `tools.*`) as needed +- For deep-level or advanced configuration needs, use `tools.rspack` or `tools.bundlerChain` to access Rspack's native configurations +- In TypeScript projects, prefer `tsconfig.json` path aliases + +## CLI + +- Use `rslib` to build +- Use `rslib --watch` to build in watch mode for local development +- Use `rslib inspect` to inspect final Rslib/Rsbuild/Rspack configs + +## Output + +- Prefer to build pure-ESM package with `"type": "module"` in `package.json` +- Prefer to use bundleless mode with `output.target` set to `'web'` when building component libraries +- Prefer to use bundle mode when building Node.js utility libraries +- Ensure `exports` field in `package.json` is correctly configured and matches the actual JavaScript output and declaration files output of different formats (ESM, CJS, etc.) + +## Declaration files + +- Prefer to enable declaration file generation with `lib.dts: true` or detailed configurations +- For faster type generation, enable `lib.dts.tsgo` experimental feature with `@typescript/native-preview` installed + +## Dependencies + +- Prefer to place dependencies to be bundled in `devDependencies` in bundle mode and dependencies in `dependencies` and `peerDependencies` will be automatically externalized (not bundled) by default +- Verify the build output and dependency specifiers in `package.json` carefully to ensure no missing dependency errors occur when consumers install and use this package + +## Build optimization + +- Keep syntax target in `lib.syntax` aligned with real compatibility requirements to enable better optimizations +- Avoid format-specific APIs in source code for better compatibility with different output formats +- Prefer lightweight dependencies to reduce bundle size + +## Toolchain integration + +- Prefer to use Rstest with `@rstest/adapter-rslib` for writing tests +- Prefer to use Rspress for writing library documentation, with `@rspress/plugin-preview` and `@rspress/plugin-api-docgen` for component previews and API docs + +## Debugging + +- Run with `DEBUG=rsbuild` when diagnosing config resolution or plugin behavior +- Read generated files in `dist/.rsbuild` to confirm final Rsbuild/Rspack config, not assumed config + +## Documentation + +- For the latest Rslib docs, read https://rslib.rs/llms.txt diff --git a/plugins/rstack/skills/rslib-modern-package/SKILL.md b/plugins/rstack/skills/rslib-modern-package/SKILL.md new file mode 100644 index 0000000..1c4a5f8 --- /dev/null +++ b/plugins/rstack/skills/rslib-modern-package/SKILL.md @@ -0,0 +1,173 @@ +--- +name: rslib-modern-package +description: Opinionated Rslib recommendations for modern JS/TS npm package design covering pure ESM, strict TypeScript, explicit exports, small stable APIs, pragmatic dependencies, accurate sideEffects, correct declarations, package validation, provenance, README.md, and AGENTS.md. Use when the user wants to make a JS/TS package more modern, check whether the current package setup is healthy, review package.json/exports/types/dependencies/docs/release readiness, or apply a modern library baseline. +--- + +# Rslib Modern Package + +Use this skill when creating a new Rslib library, modernizing an existing JS/TS package, or reviewing a package against an opinionated modern library standard. + +This skill is opinionated: it describes a recommended modern package contract and may suggest breaking changes when they make the package simpler, safer, and easier for modern consumers. + +## Standard + +Default recommendation for new JS/TS libraries: + +- ESM-first, preferably pure ESM. +- Strict TypeScript and correct declaration files. +- Explicit public API through `package.json#exports`. +- Small named-export API surface. +- Few runtime dependencies, without treating zero dependencies as a religion. +- Small, tree-shakeable output with accurate `sideEffects`. +- Clear dependency placement: runtime dependencies, peer dependencies, optional dependencies, and dev dependencies are not interchangeable. +- Published package is tested as an artifact, not just as source files. +- Release flow is automated, traceable, and SemVer-aware. +- README.md explains usage for humans; AGENTS.md preserves package invariants for future agents. + +## Workflow + +1. **Inspect the package contract first** + - Read `package.json`, lockfile/package manager, `rslib.config.*`, `tsconfig*`, CI/release config, README.md, AGENTS.md, and existing `dist` output. + - Identify package kind: Node utility, browser library, isomorphic utility, CLI, UI/component library, framework plugin, SDK, or adapter. + - List supported runtimes, current entry points, deep imports, runtime dependencies, peer dependencies, optional integrations, files with side effects, and published files. + - Run `npm pack --dry-run` early when changing package shape so the real tarball contents guide the review. + +2. **Define target environments explicitly** + - Do not say "supports modern environments" without defining them. + - Verify the current Node.js release schedule before choosing `engines`. + - As of May 9, 2026, Node.js 22 and 24 are LTS, and Node.js 20 is EOL. For new Node-facing packages, recommend `engines.node >=22` unless real consumers need an older runtime. + - Browser packages should state whether they require native ESM, a bundler, Workers support, SSR compatibility, DOM APIs, CSS processing, or specific browser baselines. + - Compatibility drops are breaking changes: old Node/browser versions, undocumented deep imports, default/named export shape, bundled vs external dependency behavior, and import side effects. + +3. **Prefer pure ESM, but explain compatibility cost** + - New packages should use `"type": "module"` and ESM source/output. + - Rslib's default format is ESM; keep that default unless there is a clear reason to add another format. + - Evaluate compatibility from real consumers and supported runtimes instead of assuming every historical module format is required. + - Modern Node.js can load synchronous ESM from CommonJS via `require(esm)`; do not assume CJS consumers always require a separate CJS build. + - If you rely on `require(esm)` compatibility, document and test its constraints: supported Node versions, no top-level `await` in the loaded graph, namespace-object return shape, default export behavior, and CJS/ESM cycle limits. + - Prefer Node built-in specifiers such as `node:fs/promises`. + +4. **Make `exports` the public API** + - Treat `package.json#exports` as the product contract. + - Export only paths users are meant to import. + - Do not allow imports like `pkg/dist/foo.js` by exporting `./dist/*`. That makes the generated output layout part of the public API and turns internal file moves into breaking changes. + - Instead, expose only intentional public paths such as `pkg` and `pkg/foo.js`, mapped to the actual files in `dist`. + - Keep subpath style consistent: either all with extensions such as `./foo.js`, or all without extensions. Prefer paths with extensions when browser import maps matter. + - Keep `"types"` first inside conditional exports. + - Adding `exports` to an older package can be breaking because undeclared deep imports stop working. + - Add `./package.json` only when consumers legitimately need package metadata. + +5. **Design a small API surface** + - Prefer named exports for multi-API packages. + - Avoid default-export objects that gather every function into one object. + - Public functions should be few, stable, well-named, and semver-maintained. + - Keep internal types, caches, helper functions, adapter details, and error internals private unless they are part of the contract. + - Avoid top-level work during import: file scans, network calls, timers, process mutation, global registration, DOM access, prototype mutation, or environment detection with side effects. + - Async APIs that may be canceled should accept `AbortSignal`. + - Prefer stable error classes, error codes, or typed error shapes over string matching. + +6. **Use Rslib as the implementation path, not the whole standard** + - For detailed Rslib configuration guidance, use the `rslib-best-practices` skill. + - In this skill, only check whether Rslib output, declarations, `package.json#exports`, `files`, dependencies, and docs agree with the modern package contract. + - Keep Rslib configuration small and intentional; avoid adding build complexity that does not improve the package contract. + +7. **Keep dependencies small and intentional** + - Start from platform APIs, not from dependency search. + - Prefer built-ins when the runtime supports them: `URL`, `URLSearchParams`, `Intl`, `fetch`, `AbortController`, `structuredClone`, `crypto.randomUUID`, Web Streams, `TextEncoder`, `TextDecoder`, `node:fs/promises`, and `node:crypto`. + - Small runtime dependencies are fine when they reduce maintenance risk or implementation complexity. + - Avoid large utility packages for one or two helpers. + - Evaluate dependencies for ESM support, `exports`, types, transitive dependency count, package size, license, maintenance activity, security history, install scripts, side effects, native install fragility, and granular imports. + - Put required runtime packages in `dependencies`. + - Put host-owned frameworks and toolchains in `peerDependencies`, such as React, Vue, Svelte, Rspack, Rsbuild, webpack, TypeScript, and framework runtimes. + - Keep peer ranges reasonably broad; do not pin peers to a patch version unless required. + - Put build tools, test tools, type tools, docs tools, and Rsbuild/Rspack plugins in `devDependencies`. + - Use `optionalDependencies` or optional peers via `peerDependenciesMeta` for optional integrations. + +8. **Make TypeScript strict and package-oriented** + - Prefer TypeScript source for TS libraries; otherwise use high-quality JSDoc plus generated declarations. + - With TypeScript 6 or tsgo-era defaults, strict checking may already be enabled; preserve that default and do not turn it off. For older TypeScript versions or inherited configs, set `strict: true` explicitly. + - In Rslib projects, consider enabling `lib.dts.tsgo` to speed up declaration generation when the project can use tsgo. + - Keep `module` and `moduleResolution` aligned with how declarations are emitted and how consumers resolve the package; NodeNext and bundler-style resolution are both valid in the right toolchain. + - Use `verbatimModuleSyntax` so type-only imports/exports are explicit. + - Use `isolatedDeclarations` when practical so exported APIs are explicit enough for declaration-oriented tooling. + - Emit declarations; use declaration maps when editor navigation matters. + - Use `import type` and `export type` for type-only dependencies. + - Do not rely on consumers setting `skipLibCheck` to hide broken package types. + - Test declarations as consumers see them, especially when using subpath exports. + +9. **Keep `sideEffects` accurate** + - Use `sideEffects: false` only when importing package files has no top-level side effects. + - If CSS, polyfills, registrations, global listeners, prototype changes, or other import-time mutations exist, list the files with side effects instead. + - Do not set `sideEffects: false` just to improve bundle size; incorrect values can remove required CSS or setup code. + - Do not change globals just because a file is imported. If setup is required, expose an explicit `setup()` or `install()` function and let users call it themselves. + +10. **Make `package.json` authoritative** + - Required modern shape: `"type": "module"`, explicit `exports`, correct declarations, `files` allowlist, accurate `sideEffects`, sensible `engines`, and release scripts. + - Include README.md, AGENTS.md, and LICENSE in `files` when they exist. + - `files` should prevent tests, fixtures, private docs, build caches, local configs, and large generated artifacts from leaking into the tarball. + - Avoid stale `main`/`module` fields unless compatibility evidence requires them; if kept, they must agree with `exports`. + - Keep runtime dependency fields accurate. A package that works locally only because a runtime dependency is in `devDependencies` is broken. + +11. **Validate the published artifact** + - Run normal lint, typecheck, tests, and `rslib build`. + - Smoke test built ESM output. + - Run type-level tests when the public API is type-heavy. + - Run `npm pack --dry-run` and inspect included files. + - In Rslib, prefer `rsbuild-plugin-publint` to run publint after build; use `npx publint` as a CLI fallback. + - In Rslib, prefer `rsbuild-plugin-arethetypeswrong` to run Are The Types Wrong after build when declarations are shipped; use `npx --yes @arethetypeswrong/cli --pack .` as a CLI fallback. + - Install the packed tarball into clean consumer fixtures for important packages. + - Test ESM import, bundler import for browser/component libraries, CLI execution for `bin` packages, and every public subpath export. + +12. **Prepare README.md and AGENTS.md before publishing** + - Always check whether both files exist before publishing or modernizing a package. + - If either file is missing, recommend adding it; for implementation tasks, create a concise version unless the user asks not to. + - README.md should include: package name, one-sentence purpose, install/usage, key features or API links, supported environments, docs/related links, changelog or contribution link, and license. + - AGENTS.md should include: stack, package contract, common commands, source layout, code style, validation commands, and release checklist. + - Keep both files synchronized with `package.json#exports`, supported runtimes, and actual Rslib output. + +13. **Publish with supply-chain hygiene** + - Follow SemVer and document breaking changes. + - Maintain a changelog for user-visible changes. + - Use prerelease versions and dist-tags for beta/next channels. + - Prefer CI publishing with npm provenance or trusted publishing. + - Avoid long-lived publish tokens where trusted publishing is available. + - Remember that a published package name/version pair cannot be reused safely. + +## Review Red Flags + +- `exports` is missing, points to files not emitted by Rslib, or allows public imports such as `pkg/dist/foo.js`. +- `module`/`main` fields disagree with `exports`. +- Type declarations do not match runtime entry points. +- Runtime dependency is accidentally listed only in `devDependencies`. +- React/Vue/Svelte/Rspack/Rsbuild/webpack/TypeScript is bundled or placed in `dependencies` when it should be a peer. +- `sideEffects: false` is set while importing CSS, polyfills, global registrations, global listeners, or prototype changes. +- Package has install scripts without a strong reason. +- Top-level import reads user files, starts timers, touches the network, mutates globals, or assumes `window`/`process`. +- Published tarball contains private source maps, tests/fixtures that are not useful to consumers, large generated docs, local config secrets, or build caches. + +## Checklist + +- [ ] Supported environments are explicit. +- [ ] Package is ESM-first, preferably pure ESM. +- [ ] `package.json` has `"type": "module"`. +- [ ] Public entry points are declared in `exports`. +- [ ] No accidental reliance on undeclared deep imports. +- [ ] Rslib output, declarations, `exports`, and `files` agree. +- [ ] TypeScript strict mode is enabled. +- [ ] Declarations are emitted and validated. +- [ ] Runtime dependencies are justified and small. +- [ ] Host frameworks/toolchains are peers. +- [ ] Build/test/type/docs tools are dev dependencies. +- [ ] `sideEffects` is accurate. +- [ ] `npm pack --dry-run` has been inspected. +- [ ] `publint` passes. +- [ ] Are The Types Wrong check passes when declarations are shipped. +- [ ] Built ESM smoke test passes. +- [ ] README.md exists. +- [ ] AGENTS.md exists. +- [ ] Release flow uses SemVer, changelog, and provenance/trusted publishing when available. + +## Documentation + +- For the latest Rslib docs, read https://rslib.rs/llms.txt +- Shipping ESM for CommonJS consumers: https://nodejs.github.io/package-examples/04-cjs-esm-interop/shipping-esm-for-cjs/ diff --git a/plugins/rstack/skills/rspack-best-practices/SKILL.md b/plugins/rstack/skills/rspack-best-practices/SKILL.md new file mode 100644 index 0000000..109efab --- /dev/null +++ b/plugins/rstack/skills/rspack-best-practices/SKILL.md @@ -0,0 +1,70 @@ +--- +name: rspack-best-practices +description: Rspack best practices for config, CLI workflow, type checking, CSS, bundle optimization, assets and profiling. Use when writing, reviewing, or troubleshooting Rspack projects. +--- + +# Rspack Best Practices + +Apply these rules when writing or reviewing Rspack projects. + +## Configuration + +- Use `rspack.config.ts` and `defineConfig` +- Define explicit `entry` values for multi-page applications +- Keep one main config and branch by `process.env.NODE_ENV` only when needed +- Keep rule conditions narrow and explicit (`test`, `include`, `exclude`, `resourceQuery`) +- Prefer built-in Rspack plugins/loaders over community JS alternatives when equivalent features exist + +## CLI + +If `@rspack/cli` is installed: + +- Use `rspack dev` for local development +- Use `rspack build` for production build +- Use `rspack preview` only for local production preview + +## Type checking + +- Use `ts-checker-rspack-plugin` for integrated dev/build type checks +- Or run `tsc --noEmit`/`vue-tsc --noEmit` as an explicit script step + +## CSS + +Choose one strategy: + +- Built-in CSS (`type: 'css' | 'css/auto' | 'css/module'`) for modern setups +- `css-loader` + `CssExtractRspackPlugin` for webpack migration compatibility +- `style-loader` for pure style-in-JS runtime injection scenarios + +Optional: + +- Use `builtin:lightningcss-loader` when goals are syntax downgrade + vendor prefixing +- Use `sass-loader`/`less-loader` for preprocessing Sass/Less files +- Use `@tailwindcss/webpack` for Tailwind CSS integration + +## Bundle size optimization + +- Prefer dynamic `import()` for non-critical code paths +- Prefer lightweight libraries where possible +- Keep `target` aligned with real compatibility requirements + +## Asset management + +- Import source-managed assets from project source directories, not from `public` +- Reference `public` files by absolute URL path +- Prefer asset modules (`asset`, `asset/resource`, `asset/inline`, `asset/source`) over legacy `file-loader`/`url-loader`/`raw-loader` + +## Profiling + +- Use Node CPU profiling (`--cpu-prof`) when JavaScript-side overhead is suspected +- Use `RSPACK_PROFILE=OVERVIEW` and analyze trace output for compiler-phase bottlenecks +- Replace known slow stacks first (`babel-loader`, PostCSS, terser) with Rspack built-ins when feasible + +## Security + +- Do not publish `.map` files to public servers/CDNs when production source maps are enabled + +## Documentation + +- For the latest (v2) docs, read http://rspack.rs/llms.txt +- For Rspack v1 docs, read http://v1.rspack.rs/llms.txt diff --git a/plugins/rstack/skills/rspack-debugging/SKILL.md b/plugins/rstack/skills/rspack-debugging/SKILL.md new file mode 100644 index 0000000..81cd396 --- /dev/null +++ b/plugins/rstack/skills/rspack-debugging/SKILL.md @@ -0,0 +1,86 @@ +--- +name: rspack-debugging +description: Helps Rspack users and developers debug crashes or deadlocks/hangs in the Rspack build process using LLDB. Use this Skill when users encounter "Segmentation fault" errors during Rspack builds or when the build progress gets stuck. +--- + +# Rspack Debugging + +## Overview + +This Skill guides you on how to capture the underlying crash state of Rspack (which is based on Rust). By using the LLDB debugger and Rspack packages with debug symbols, we can obtain detailed stack backtraces, which are crucial for pinpointing issues. The guides focus on non-interactive, automated debugging to easily capture backtraces. + +## Preparation + +Before starting, please ensure your environment meets the requirements. + +1. **Install LLDB**: You must install the LLDB debugger. + - macOS: Run `xcode-select --install` + - Linux: Install the `lldb` package (e.g., `apt-get install lldb`) + - Detailed guide: [references/lldb.md](references/lldb.md) + +2. **Replace Debug Packages**: + Production packages like `@rspack/core` have debug symbols stripped. They must be replaced with the `@rspack-debug/*` series packages to see useful stack information. + + **Automatic Replacement Script**: + + ```bash + node ${CLAUDE_PLUGIN_ROOT}/skills/debugging/scripts/setup_debug_deps.cjs + ``` + + Running the above script will automatically add `pnpm.overrides` configuration to `package.json`, pointing Rspack packages to their corresponding Debug versions. Afterwards, please be sure to run `pnpm install` to update dependencies. + +## Debugging Workflows + +Identify your specific scenario and follow the corresponding linked guide. + +## Detailed Guides + +## Detailed Guides + +### Guide A: Crash during HMR + +**Scenario**: Stable Crash/Deadlock during DevServer HMR. +[Read Guide: references/guide_a_hmr_crash.md](references/guide_a_hmr_crash.md) + +### Guide B: Crash during Build + +**Scenario**: Stable Crash/Deadlock during Build (or Unstable Build Crash that is frequent enough). +[Read Guide: references/guide_b_build_crash.md](references/guide_b_build_crash.md) + +### Guide C: Attach to Stuck Process + +**Scenario**: Unstable Deadlock during Build (happens randomly). +[Read Guide: references/guide_c_attach_to_stuck_process.md](references/guide_c_attach_to_stuck_process.md) + +### Guide D: Coredump Analysis (Dev) + +**Scenario**: Unstable Crash during DevServer HMR (hard to catch interactively). +[Read Guide: references/guide_d_coredump_analysis_dev.md](references/guide_d_coredump_analysis_dev.md) + +### Guide E: Coredump Analysis (Build) + +**Scenario**: Unstable Crash during Build. +[Read Guide: references/guide_e_coredump_analysis_build.md](references/guide_e_coredump_analysis_build.md) + +### Guide F: Async Deadlock Identification + +**Scenario**: Unstable Async Deadlock. Main thread stuck in `uv_run`. +[Read Guide: references/guide_f_async_deadlock.md](references/guide_f_async_deadlock.md) + +## Saving Debug Artifacts + +**Critical Instruction for Agents**: +When you successfully obtain a backtrace or a tracing log, you **MUST** save it to a local file in the user's project directory so it is preserved after the session. + +1. **Create Directory**: Ensure a directory named `debug_artifacts` exists in the project root. +2. **Save Backtraces**: Write the full output of `thread backtrace all` to `debug_artifacts/backtrace_.txt`. +3. **Save Tracing Logs**: (Only if using Tracing Skill) + +## Environment Restoration + +After debugging is complete, restore your `package.json` to use production packages: + +```bash +node ${CLAUDE_PLUGIN_ROOT}/skills/debugging/scripts/setup_debug_deps.cjs --restore +pnpm install +``` diff --git a/plugins/rstack/skills/rspack-debugging/references/guide_a_hmr_crash.md b/plugins/rstack/skills/rspack-debugging/references/guide_a_hmr_crash.md new file mode 100644 index 0000000..d67c449 --- /dev/null +++ b/plugins/rstack/skills/rspack-debugging/references/guide_a_hmr_crash.md @@ -0,0 +1,23 @@ +# Guide A: Crash during HMR + +**Scenario**: Stable Crash/Deadlock during DevServer HMR. + +1. **Launch LLDB (Non-Interactive)**: + Run the following command to start the dev server under LLDB. It will automatically run, and if it crashes, it will print the backtrace and exit. + + ```bash + # Adjust the path to rspack.js if needed + lldb --batch -o "run" -k "thread backtrace all" -k "quit" -- node node_modules/@rspack/cli/bin/rspack.js dev + ``` + +2. **Trigger the Issue**: + - Wait for the Dev Server to start. + - Modify a source file to trigger HMR. + +3. **Get Backtrace**: + - **If it Crashes**: The backtrace will be printed to stdout automatically. + - **If it Hangs (Deadlock)**: Press `Ctrl + C` in the running terminal. LLDB should catch the signal and might need manual intervention if not configured to catch SIGINT. + - _Better approach for Hangs_: Use `Guide C` or `Guide F`. + +4. **Save Output**: + **Agent Action**: Save the printed backtrace to `debug_artifacts/backtrace_hmr_crash.txt`. diff --git a/plugins/rstack/skills/rspack-debugging/references/guide_b_build_crash.md b/plugins/rstack/skills/rspack-debugging/references/guide_b_build_crash.md new file mode 100644 index 0000000..52978b7 --- /dev/null +++ b/plugins/rstack/skills/rspack-debugging/references/guide_b_build_crash.md @@ -0,0 +1,17 @@ +# Guide B: Crash during Build + +**Scenario**: Stable Crash during Build. + +1. **Launch LLDB (Non-Interactive)**: + Run the build command under LLDB in batch mode. + + ```bash + lldb --batch -o "run" -k "thread backtrace all" -k "quit" -- node node_modules/@rspack/cli/bin/rspack.js build + ``` + +2. **Wait for Result**: + - The command will run until it crashes or finishes. + - On crash, it prints the stack trace of all threads. + +3. **Save Output**: + **Agent Action**: Save the output to `debug_artifacts/backtrace_build_crash.txt`. diff --git a/plugins/rstack/skills/rspack-debugging/references/guide_c_attach_to_stuck_process.md b/plugins/rstack/skills/rspack-debugging/references/guide_c_attach_to_stuck_process.md new file mode 100644 index 0000000..fe65651 --- /dev/null +++ b/plugins/rstack/skills/rspack-debugging/references/guide_c_attach_to_stuck_process.md @@ -0,0 +1,43 @@ +# Guide C: Attach to Stuck Process + +**Scenario**: Unstable Deadlock during Build (happens randomly). + +## 1. User Action: Reproduce and Get PID + +Run the following script to loop your build command until it hangs. This script prints the PID of each attempt. + +```bash +# Loop until you manually stop it (Ctrl+C) when it hangs +while true; do + echo "Starting build..." + # Start in background to get PID easily, then wait + pnpm build & + PID=$! + echo ">> Process PID: $PID" + wait $PID + echo "Build finished, retrying..." + sleep 1 +done +``` + +**Instructions**: + +1. Run the script in your terminal. +2. Watch the output. +3. When the build **hangs** (stops outputting and doesn't finish for a long time): + - Look at the last printed `>> Process PID: `. + - **Do not kill the process**. + - Copy that PID. + +## 2. Agent Action: Attach and debug + +Ask the user for the **PID** of the stuck process. Once obtained, run: + +```bash +# Replace with the actual number provided by the user +lldb -p --batch -o "thread backtrace all" -o "quit" +``` + +## 3. Save Output + +**Agent Action**: Save the output to `debug_artifacts/backtrace_attached.txt`. diff --git a/plugins/rstack/skills/rspack-debugging/references/guide_d_coredump_analysis_dev.md b/plugins/rstack/skills/rspack-debugging/references/guide_d_coredump_analysis_dev.md new file mode 100644 index 0000000..85cd8b6 --- /dev/null +++ b/plugins/rstack/skills/rspack-debugging/references/guide_d_coredump_analysis_dev.md @@ -0,0 +1,27 @@ +# Guide D: Coredump Analysis (Dev) + +**Scenario**: Unstable Crash during DevServer HMR (hard to catch interactively). + +1. **Enable Core Dumps**: + Run this in the terminal where you will start the dev server: + ```bash + ulimit -c unlimited + ``` + _Note: On macOS, core dumps might be written to `/cores/`. On Linux, usually current dir or `/var/lib/systemd/coredump`._ +2. **Start Dev Server**: + ```bash + pnpm dev + ``` +3. **Torture Test**: + Repeatedly modify files to trigger HMR until the server crashes. You can write a script to append a comment to a file every second. +4. **Locate Core File**: + Find the generated core file (e.g., `/cores/core.12345` or `./core`). +5. **Debug Post-Mortem**: + ```bash + # You need the exact node binary that ran the process + lldb --batch -o "thread backtrace all" -o "quit" --core /path/to/core_file $(which node) + ``` +6. **Get Backtrace**: + The backtrace will be printed to stdout. +7. **Save Output**: + **Agent Action**: Save the output from step 6 to `debug_artifacts/backtrace_core_dump.txt`. diff --git a/plugins/rstack/skills/rspack-debugging/references/guide_e_coredump_analysis_build.md b/plugins/rstack/skills/rspack-debugging/references/guide_e_coredump_analysis_build.md new file mode 100644 index 0000000..def8667 --- /dev/null +++ b/plugins/rstack/skills/rspack-debugging/references/guide_e_coredump_analysis_build.md @@ -0,0 +1,22 @@ +# Guide E: Coredump Analysis (Build) + +**Scenario**: Unstable Crash during Build. + +1. **Enable Core Dumps**: + ```bash + ulimit -c unlimited + ``` +2. **Loop until Crash**: + ```bash + # Simple shell loop + while pnpm build; do echo "Build success, retrying..."; done + ``` + Wait for the loop to exit with an error (Segmentation fault). +3. **Debug Post-Mortem**: + ```bash + lldb --batch -o "thread backtrace all" -o "quit" --core /path/to/core_file $(which node) + ``` +4. **Get Backtrace**: + The backtrace will be printed to stdout. +5. **Save Output**: + **Agent Action**: Save the output from step 4 to `debug_artifacts/backtrace_core_dump.txt`. diff --git a/plugins/rstack/skills/rspack-debugging/references/guide_f_async_deadlock.md b/plugins/rstack/skills/rspack-debugging/references/guide_f_async_deadlock.md new file mode 100644 index 0000000..da01f74 --- /dev/null +++ b/plugins/rstack/skills/rspack-debugging/references/guide_f_async_deadlock.md @@ -0,0 +1,34 @@ +# Guide F: Async Deadlock Identification + +**Scenario**: Unstable Async Deadlock. Main thread stuck in `uv_run`. + +## 1. Identification + +Use **Guide C** (Attach to Stuck Process) to get a backtrace from the stuck process. Then check if it matches the following pattern: + +**Main Thread** +Stuck in the event loop waiting (`uv_run` / `kevent` / `epoll_wait`), usually with no active JavaScript or Rust tasks. + +``` +frame #0: kevent (libsystem_kernel.dylib) +frame #1: uv__io_poll (node) +frame #2: uv_run (node) +frame #3: node::SpinEventLoopInternal (node) +``` + +**Tokio Worker Threads** +All in an idle waiting state (`Condvar::wait`). + +``` +frame #0: __psynch_cvwait +frame #1: _pthread_cond_wait +frame #2: parking_lot::condvar::Condvar::wait_until_internal +frame #3: tokio::runtime::scheduler::multi_thread::park::Parker::park +``` + +## 2. Next Steps + +If the backtrace matches the above pattern, it is a classic **Async Deadlock**. LLDB cannot help further because the threads are simply waiting for a Future that never completes. + +**Recommendation**: +Please use the **Tracing Skill** to diagnose this issue. Tracing logs can reveal which Future was last active or dropped. diff --git a/plugins/rstack/skills/rspack-debugging/references/lldb.md b/plugins/rstack/skills/rspack-debugging/references/lldb.md new file mode 100644 index 0000000..e8b501b --- /dev/null +++ b/plugins/rstack/skills/rspack-debugging/references/lldb.md @@ -0,0 +1,61 @@ +# LLDB References + +# Install + +LLDB is the debugger for the LLVM project. Since Rspack is written in Rust, LLDB can be used for debugging. + +## macOS + +On macOS, LLDB usually comes installed with Xcode or Command Line Tools. + +1. Check if it is already installed: + ```bash + lldb --version + ``` +2. If not installed, run the following command to install Command Line Tools: + ```bash + xcode-select --install + ``` + +## Linux + +### Ubuntu / Debian + +```bash +sudo apt-get update +sudo apt-get install lldb +``` + +### Arch Linux + +```bash +sudo pacman -S lldb +``` + +## Windows + +Windows users are recommended to use WSL2 (Ubuntu) and follow the Linux steps for installation, or use the C++ extension in VS Code with LLDB. +If you are in a native Windows environment, you can use the Windows installer provided by the LLVM official website, but debugging Rspack is generally recommended in a Unix-like environment for better support. + +## LLDB in Batch Mode + +For automation and non-interactive debugging, we use LLDB in batch mode: + +```bash +lldb --batch -o "run" -k "thread backtrace all" -k "quit" -- node script.js +``` + +- `--batch`: Run in batch mode. +- `-o`: Execute command after loading. +- `-k`: Execute command upon crash (if the process crashes). +- `--`: Separate LLDB arguments from the target program arguments. + +## Common Checks + +### `thread backtrace all` (or `bt all`) + +This is the most critical command. It prints the stack traces of **all** threads. + +### `frame variable` (or `fr v`) + +Prints variables in the current stack frame. Can be used with `-o` to inspect specific states if needed. diff --git a/plugins/rstack/skills/rspack-debugging/scripts/setup_debug_deps.cjs b/plugins/rstack/skills/rspack-debugging/scripts/setup_debug_deps.cjs new file mode 100644 index 0000000..f5d0562 --- /dev/null +++ b/plugins/rstack/skills/rspack-debugging/scripts/setup_debug_deps.cjs @@ -0,0 +1,117 @@ +const fs = require('fs'); +const path = require('path'); + +const USER_MIN_VERSION = '1.3.14'; + +/** + * Recursively find a file upwards from the start directory. + */ +function findFileUpwards(startDir, fileName) { + let currentDir = startDir; + while (true) { + const filePath = path.join(currentDir, fileName); + if (fs.existsSync(filePath)) { + return filePath; + } + const parentDir = path.dirname(currentDir); + if (parentDir === currentDir) { + // Reached root + return null; + } + currentDir = parentDir; + } +} + +// Find pnpm-lock.yaml to determine the workspace root +const lockPath = findFileUpwards(process.cwd(), 'pnpm-lock.yaml'); + +if (!lockPath) { + console.error('❌ No pnpm-lock.yaml found in current or parent directories.'); + console.error(' This script requires a pnpm project with a lockfile.'); + process.exit(1); +} + +const workspaceRoot = path.dirname(lockPath); +const pkgPath = path.join(workspaceRoot, 'package.json'); +const backupPath = path.join(workspaceRoot, 'package.json.bak'); + +console.log(`📍 Workspace Root detected: ${workspaceRoot}`); + +function restore() { + if (fs.existsSync(backupPath)) { + fs.copyFileSync(backupPath, pkgPath); + console.log(`✅ Restored package.json from backup at ${backupPath}`); + fs.unlinkSync(backupPath); + } else { + console.log(`No backup found at ${backupPath} to restore.`); + } +} + +if (process.argv.includes('--restore')) { + restore(); + process.exit(0); +} + +if (!fs.existsSync(pkgPath)) { + console.error(`❌ No package.json found at workspace root: ${pkgPath}`); + process.exit(1); +} + +// Simple version comparison (major.minor.patch) +function isVersionLessThan(v1, v2) { + const parts1 = v1.split('.').map(Number); + const parts2 = v2.split('.').map(Number); + for (let i = 0; i < 3; i++) { + if (parts1[i] < parts2[i]) return true; + if (parts1[i] > parts2[i]) return false; + } + return false; +} + +// Backup first +if (!fs.existsSync(backupPath)) { + fs.copyFileSync(pkgPath, backupPath); + console.log(`📦 Created backup of package.json at ${backupPath}`); +} + +console.log('🔎 Searching for @rspack/core version in pnpm-lock.yaml...'); +const lockContent = fs.readFileSync(lockPath, 'utf-8'); + +// Grep logic for pnpm-lock.yaml +// Matches /@rspack/core@1.0.0: or /@rspack/core@1.0.0( +const versionMatch = lockContent.match(/\@rspack\/core@([^\s:'()]+)/); + +if (!versionMatch) { + console.error('❌ Could not find "@rspack/core" in pnpm-lock.yaml.'); + process.exit(1); +} + +let version = versionMatch[1]; +console.log(`✅ Detected Rspack version: ${version}`); + +if (isVersionLessThan(version, USER_MIN_VERSION)) { + console.warn(`\n⚠️ WARNING: @rspack-debug/* packages are only officially supported for versions >= ${USER_MIN_VERSION}.`); + console.warn(` Current version is ${version}. Falling back to debug version ${USER_MIN_VERSION}.`); + console.warn(` This may lead to binary incompatibility if there are major API changes.\n`); + version = USER_MIN_VERSION; +} + +// Update package.json +const pkg = require(pkgPath); +pkg.pnpm = pkg.pnpm || {}; +pkg.pnpm.overrides = pkg.pnpm.overrides || {}; + +const debugCore = `npm:@rspack-debug/core@${version}`; +const debugCli = `npm:@rspack-debug/cli@${version}`; + +console.log(`🔄 Configuring pnpm overrides in workspace root package.json:`); +console.log(` @rspack/core -> ${debugCore}`); +console.log(` @rspack/cli -> ${debugCli}`); + +pkg.pnpm.overrides['@rspack/core'] = debugCore; +pkg.pnpm.overrides['@rspack/cli'] = debugCli; + +fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n'); +console.log(`✅ package.json at ${workspaceRoot} updated.`); +console.log('\n👉 Next Step: Run `pnpm install` in the workspace root to apply the overrides.'); +console.log(' To revert changes, run this script with --restore'); \ No newline at end of file diff --git a/plugins/rstack/skills/rspack-split-chunks/SKILL.md b/plugins/rstack/skills/rspack-split-chunks/SKILL.md new file mode 100644 index 0000000..b5d5690 --- /dev/null +++ b/plugins/rstack/skills/rspack-split-chunks/SKILL.md @@ -0,0 +1,315 @@ +--- +name: rspack-split-chunks +description: >- + Diagnose and optimize Rspack `optimization.splitChunks` configuration. Use + this when a user wants better production chunking, safer `chunks: "all"` + defaults, fewer duplicated modules, better long-term caching, `cacheGroups` + design help, `maxSize` tuning, or debugging over-fetch caused by `name` and + forced chunk merging. +--- + +# Rspack SplitChunks Optimization + +Use this skill when the task is to recommend, review, or debug `optimization.splitChunks`. If you are using ESM library, it's not the same algorithm of this skill. + +## Default stance + +- Distinguish repo defaults from recommended production baselines. +- Rspack's built-in default is `chunks: "async"`, but for most production web apps the best starting point is: + +```js +optimization: { + splitChunks: { + chunks: "all", + }, +} +``` + +- Keep the default cache groups unless there is a concrete reason to replace them. +- Treat `name` as a graph-shaping option, not a cosmetic naming option. +- Do not use `splitChunks` to reason about JavaScript execution order or tree shaking. For JS, chunk loading/execution order is preserved by the runtime dependency graph, and tree shaking is decided elsewhere. + +Read [`references/repo-behavior.md`](references/repo-behavior.md) when you need the source-backed rationale. + +## What To Optimize For + +First identify which problem the user actually has: + +- duplicated modules across entry or async boundaries +- a route fetching a large shared chunk with mostly unused modules +- too many tiny chunks +- a vendor/common chunk that changes too often and hurts caching +- an oversized async or initial chunk that should be subdivided +- confusion about whether `splitChunks` affects runtime execution order + +Do not optimize all of these at once. Pick the primary goal and keep the rest as constraints. + +## Workflow + +### 1. Start from the safest production baseline + +Unless the user already has a measured problem that requires custom grouping, prefer: + +```js +optimization: { + splitChunks: { + chunks: "all", + }, +} +``` + +Why: + +- it lets splitChunks dedupe modules across both initial and async chunks +- it still only loads chunks reachable from the current entry/runtime +- it usually avoids loading unnecessary modules better than hand-written global vendor buckets + +If the existing config disables `default` or `defaultVendors`, assume that is suspicious until proven necessary. + +### 2. Audit the config for high-risk knobs + +Check these first: + +- fixed `name` +- `cacheGroups.*.name` +- `enforce: true` +- disabled `default` / `defaultVendors` +- broad `test: /node_modules/` rules combined with a single global `name` +- `usedExports: false` +- very small `minSize` +- `maxSize` combined with manual global names + +### 3. Interpret `name` correctly + +Use this rule: + +- No `name`: splitChunks can keep different chunk combinations separate. +- Same `name`: matching modules are merged into the same named split chunk candidate. + +That means a fixed `name: "vendors"` or `name: "common"` is often the real reason a page starts fetching modules from unrelated dependency chains. + +Prefer these alternatives before adding `name`: + +- keep `name` unset +- use `idHint` if the goal is filename identity, not grouping identity +- narrow the `test` so the cache group is smaller +- split one broad cache group into several focused cache groups +- rely on `maxSize` to subdivide a big chunk instead of forcing a global name + +Use a fixed `name` only when the user explicitly wants one shared asset across multiple entries/routes and accepts the extra coupling. + +### 4. Preserve the built-in cache groups by default + +Rspack's built-in production-oriented behavior depends heavily on these two groups: + +- `default`: extracts modules shared by at least 2 chunks and reuses existing chunks +- `defaultVendors`: extracts `node_modules` modules and reuses existing chunks + +These defaults are usually the best balance between dedupe and "only fetch what this page needs". + +If you customize `cacheGroups`, do not casually replace these with one manually named vendor bucket. + +### 5. Use `chunks: "all"` without fear of breaking execution order + +When a module group is split out, Rspack connects the new chunk back to the original chunk groups. That preserves JavaScript loading semantics. + +So: + +- `splitChunks` changes chunk topology +- the runtime still guarantees dependency loading/execution order +- if execution order appears broken, look for other causes first +- this statement is about JavaScript, not CSS order + +### 6. Use `maxSize` as a refinement tool + +Use `maxSize`, `maxAsyncSize`, or `maxInitialSize` when the problem is "this shared chunk is too large", not when the problem is "I need a stable vendor chunk name". + +Important behavior: + +- `maxSize` runs after a chunk already exists +- the split is deterministic +- modules are grouped by path-derived keys and split near low-similarity boundaries +- similar file paths tend to stay together + +This is usually safer than forcing one giant named vendor chunk, because it keeps chunk graph semantics while subdividing hot spots. + +### 7. Use `usedExports` deliberately + +If the user has multiple runtimes/entries and wants leaner shared chunks per runtime, prefer keeping `usedExports` enabled. + +If they set `usedExports: false`, expect broader sharing and potentially larger common chunks. + +This is still not tree shaking. It only changes how splitChunks groups modules across runtimes. + +### 8. Treat `enforce: true` as an escape hatch + +`enforce: true` bypasses several normal guardrails. Use it only when the user intentionally wants a split regardless of `minSize`, `minChunks`, and request limits. + +If a config looks aggressive and hard to explain, check `enforce` before changing anything else. + +## Recommendations By Goal + +### Better default production chunking + +Recommend: + +```js +optimization: { + splitChunks: { + chunks: "all", + }, +} +``` + +Avoid: + +- disabling `default` +- disabling `defaultVendors` +- adding `name` before measuring a real problem + +### Avoid fetching non-essential modules + +Recommend: + +- remove fixed `name` +- keep cache groups narrow +- keep `chunks: "all"` if dedupe across initial chunks is still desired +- inspect which routes now depend on a shared chunk after each change + +Avoid: + +```js +cacheGroups: { + vendors: { + test: /[\\/]node_modules[\\/]/, + chunks: "all", + name: "vendors", + enforce: true + } +} +``` + +That pattern often creates one over-shared chunk that many pages must fetch. + +### Improve caching without over-merging + +Recommend: + +- keep `name` unset +- use `idHint` +- keep `chunkIds: "deterministic"` or other stable id strategies elsewhere in the config +- split broad groups into smaller focused groups only when the package boundaries are stable and important + +Use a fixed `name` only if the user explicitly prefers cache reuse over route isolation. + +### Split a large shared chunk + +Recommend: + +```js +optimization: { + splitChunks: { + chunks: "all", + maxSize: 200000, + }, +} +``` + +Then tune: + +- `maxAsyncSize` when async chunks are the pain point +- `maxInitialSize` when first-load pressure matters more +- `hidePathInfo` if generated part names should not leak path structure + +### Keep an intentionally shared chunk + +Recommend a named chunk only when the user says something like: + +- "all pages should share one React vendor asset" +- "I want one framework chunk for cache reuse across routes" + +Even then, call out the tradeoff explicitly: + +- better cache hit rate +- more coupling between routes +- a page may fetch modules it does not execute immediately + +## Review Checklist + +When reviewing a user's config, explicitly answer: + +1. Is the goal dedupe, cache stability, request count, or route isolation? +2. Is `chunks: "all"` a better baseline than the current config? +3. Did `name` accidentally turn multiple candidates into one forced shared chunk? +4. Were `default` or `defaultVendors` disabled without a strong reason? +5. Would `idHint` satisfy the naming goal without changing grouping? +6. Is `maxSize` a better fit than a broad manual vendor/common bucket? +7. Does the result still keep each page fetching only reachable chunks? + +## Minimal stats setup + +When the task includes diagnosis, ask for or generate stats that expose chunk relations: + +```js +stats: { + chunks: true, + chunkRelations: true, + chunkOrigins: true, + entrypoints: true, + modules: false +} +``` + +Then compare: + +- which entrypoints reference which shared chunks +- whether a change added a new dependency edge from an entry to a broad shared chunk +- whether a large shared chunk exists only because of a fixed `name` + +## FAQ + +### Why do I still see duplicate modules? + +Common reasons: + +- the shared candidate is too small, so extracting it would not satisfy `minSize` +- the candidate does not satisfy `minSizeReduction` +- it does not satisfy `minChunks` +- request-budget limits reject the split +- `chunks` / `test` / `cacheGroups` do not actually select the same chunk combination + +If the duplicate module is tiny, do not assume this is a bug. Rspack may intentionally keep it in place because splitting it out would create a worse chunk. + +### Does splitChunks affect JS execution order? + +No. + +- `splitChunks` only changes chunk boundaries and dependency edges +- JS loading and execution order are runtime concerns +- if a JS ordering bug appears, investigate runtime/bootstrap, side effects, or app code first + +### Does splitChunks affect tree shaking? + +No. + +- tree shaking is controlled by module-graph analysis such as `sideEffects`, `usedExports`, and dead-code elimination +- `splitChunks` runs later and only reorganizes already-selected modules into chunks +- `splitChunks.usedExports` is only a grouping hint for runtime-specific chunk combinations; it is not tree shaking itself + +### Can splitChunks affect CSS order? + +Yes, potentially. + +- this caveat applies to CSS order, not JS execution order +- extracted CSS flows such as `mini-css-extract-plugin` or `experiments.css` can observe changed final CSS order after splitChunks rewrites chunk groups +- if CSS order is critical, be careful when splitting order-sensitive styles into separate chunks + +See [web-infra-dev discussion #12](https://github.com/orgs/web-infra-dev/discussions/12). + +## Quick conclusions to reuse + +- "Keep `chunks: \"all\"`, keep the default cache groups, and remove `name` unless you intentionally want forced sharing." +- "`name` is not just a filename hint in Rspack splitChunks; it changes grouping behavior." +- "`splitChunks` does not control JS execution order or tree shaking; it only changes chunk topology." +- "`splitChunks` can affect CSS order in extracted-CSS scenarios, so treat CSS as a separate caveat." +- "`maxSize` is the safer tool when the problem is one chunk being too large." diff --git a/plugins/rstack/skills/rspack-split-chunks/references/repo-behavior.md b/plugins/rstack/skills/rspack-split-chunks/references/repo-behavior.md new file mode 100644 index 0000000..31df4f4 --- /dev/null +++ b/plugins/rstack/skills/rspack-split-chunks/references/repo-behavior.md @@ -0,0 +1,191 @@ +# SplitChunks Repo Behavior + +This file is the source-backed reference for [SKILL.md](https://github.com/rstackjs/agent-skills/blob/main/skills/rspack-split-chunks/SKILL.md). + +## 1. Repo defaults vs recommended production baseline + +Rspack's built-in defaults are defined in [packages/rspack/src/config/defaults.ts](https://github.com/web-infra-dev/rspack/blob/main/packages/rspack/src/config/defaults.ts). + +Key defaults: + +- `chunks: "async"` at [defaults.ts#L1046](https://github.com/web-infra-dev/rspack/blob/main/packages/rspack/src/config/defaults.ts#L1046) +- `minChunks: 1` at [defaults.ts#L1048](https://github.com/web-infra-dev/rspack/blob/main/packages/rspack/src/config/defaults.ts#L1048) +- `minSize: 20000` in production, `10000` otherwise at [defaults.ts#L1049](https://github.com/web-infra-dev/rspack/blob/main/packages/rspack/src/config/defaults.ts#L1049) +- `maxAsyncRequests` and `maxInitialRequests`: `30` in production, `Infinity` otherwise at [defaults.ts#L1052](https://github.com/web-infra-dev/rspack/blob/main/packages/rspack/src/config/defaults.ts#L1052) and [defaults.ts#L1055](https://github.com/web-infra-dev/rspack/blob/main/packages/rspack/src/config/defaults.ts#L1055) +- `automaticNameDelimiter: "-"` at [defaults.ts#L1058](https://github.com/web-infra-dev/rspack/blob/main/packages/rspack/src/config/defaults.ts#L1058) +- `cacheGroups.default` with `minChunks: 2` and `reuseExistingChunk: true` at [defaults.ts#L1061](https://github.com/web-infra-dev/rspack/blob/main/packages/rspack/src/config/defaults.ts#L1061) +- `cacheGroups.defaultVendors` matching `node_modules` with `reuseExistingChunk: true` at [defaults.ts#L1067](https://github.com/web-infra-dev/rspack/blob/main/packages/rspack/src/config/defaults.ts#L1067) + +Recommended production baseline in the skill is different from the built-in default: + +- built-in default: safer generic fallback +- recommended app baseline: `chunks: "all"` for better dedupe across both initial and async chunks + +## 2. `name` changes grouping, not just filenames + +The core behavior is in [crates/rspack_plugin_split_chunks/src/plugin/module_group.rs](https://github.com/web-infra-dev/rspack/blob/main/crates/rspack_plugin_split_chunks/src/plugin/module_group.rs). + +When a module matches a cache group, `merge_matched_item_into_module_group_map` computes the module-group key. + +If `name` exists: + +- the key is `cache_group.key + chunk_name` at [module_group.rs#L577](https://github.com/web-infra-dev/rspack/blob/main/crates/rspack_plugin_split_chunks/src/plugin/module_group.rs#L577) + +If `name` does not exist: + +- the key uses the selected chunk combination hash at [module_group.rs#L582](https://github.com/web-infra-dev/rspack/blob/main/crates/rspack_plugin_split_chunks/src/plugin/module_group.rs#L582) + +Effect: + +- same `name` collapses otherwise separate chunk combinations into one `ModuleGroup` +- that can force broader sharing than the user expects + +This is why a broad manual `name: "vendors"` can create over-shared chunks. + +## 3. Named chunks are reused by name + +Chunk creation/reuse is handled in [crates/rspack_plugin_split_chunks/src/plugin/chunk.rs](https://github.com/web-infra-dev/rspack/blob/main/crates/rspack_plugin_split_chunks/src/plugin/chunk.rs). + +If `module_group.chunk_name` exists: + +- Rspack first looks for an existing named chunk at [chunk.rs#L125](https://github.com/web-infra-dev/rspack/blob/main/crates/rspack_plugin_split_chunks/src/plugin/chunk.rs#L125) +- if found, it reuses that chunk +- otherwise it creates a named chunk at [chunk.rs#L134](https://github.com/web-infra-dev/rspack/blob/main/crates/rspack_plugin_split_chunks/src/plugin/chunk.rs#L134) + +If there is no `name`, only then does `reuseExistingChunk` try to reuse a chunk with the same module set at [chunk.rs#L160](https://github.com/web-infra-dev/rspack/blob/main/crates/rspack_plugin_split_chunks/src/plugin/chunk.rs#L160). + +Implication: + +- fixed `name` is a stronger form of coupling than `reuseExistingChunk` +- `reuseExistingChunk` is opportunistic +- `name` is explicit merging + +## 4. `splitChunks` does not decide JS execution order + +After extracting a chunk, Rspack wires the new chunk back to each original chunk in `split_from_original_chunks` at [chunk.rs#L235](https://github.com/web-infra-dev/rspack/blob/main/crates/rspack_plugin_split_chunks/src/plugin/chunk.rs#L235). + +That means splitChunks changes the chunk graph, and the runtime follows that graph when loading dependencies. + +Inference from the implementation: + +- splitChunks is about graph topology and fetch boundaries +- execution order remains runtime-driven, not user-declared via splitChunks +- the plugin itself runs in optimize-chunks stage at [plugin/mod.rs#L305](https://github.com/web-infra-dev/rspack/blob/main/crates/rspack_plugin_split_chunks/src/plugin/mod.rs#L305), so it is not the mechanism that performs tree shaking + +## 5. `idHint` is safer than `name` for identity hints + +After a chunk is chosen, the plugin adds `idHint` to chunk id-name hints at [plugin/mod.rs#L215](https://github.com/web-infra-dev/rspack/blob/main/crates/rspack_plugin_split_chunks/src/plugin/mod.rs#L215). + +`idHint` participates in naming/id presentation, but it is not used to build the `ModuleGroup` key. + +This makes `idHint` a safer tool when the user wants chunk identity hints without changing grouping behavior. + +## 6. `enforce: true` removes several guardrails + +Normalization from JS options to Rust plugin options happens in [crates/rspack_binding_api/src/raw_options/raw_split_chunks/mod.rs](https://github.com/web-infra-dev/rspack/blob/main/crates/rspack_binding_api/src/raw_options/raw_split_chunks/mod.rs). + +For cache groups with `enforce: true`: + +- overall `minSize` is not merged in at [raw_split_chunks/mod.rs#L157](https://github.com/web-infra-dev/rspack/blob/main/crates/rspack_binding_api/src/raw_options/raw_split_chunks/mod.rs#L157) +- overall `minSizeReduction` is not merged in at [raw_split_chunks/mod.rs#L163](https://github.com/web-infra-dev/rspack/blob/main/crates/rspack_binding_api/src/raw_options/raw_split_chunks/mod.rs#L163) +- overall `maxAsyncSize` / `maxInitialSize` constraints are treated differently at [raw_split_chunks/mod.rs#L171](https://github.com/web-infra-dev/rspack/blob/main/crates/rspack_binding_api/src/raw_options/raw_split_chunks/mod.rs#L171) and [raw_split_chunks/mod.rs#L179](https://github.com/web-infra-dev/rspack/blob/main/crates/rspack_binding_api/src/raw_options/raw_split_chunks/mod.rs#L179) +- `minChunks` falls back to `1` when enforced at [raw_split_chunks/mod.rs#L188](https://github.com/web-infra-dev/rspack/blob/main/crates/rspack_binding_api/src/raw_options/raw_split_chunks/mod.rs#L188) + +This matches the high-level guidance: `enforce` is powerful and easy to misuse. + +## 7. `maxSize` is a second-stage deterministic split + +The `maxSize` algorithm lives in [crates/rspack_plugin_split_chunks/src/plugin/max_size.rs](https://github.com/web-infra-dev/rspack/blob/main/crates/rspack_plugin_split_chunks/src/plugin/max_size.rs). + +Important implementation details: + +- it runs after chunk extraction, inside `ensure_max_size_fit`, at [max_size.rs#L469](https://github.com/web-infra-dev/rspack/blob/main/crates/rspack_plugin_split_chunks/src/plugin/max_size.rs#L469) +- it computes module keys from relative paths plus a short hash at [max_size.rs#L232](https://github.com/web-infra-dev/rspack/blob/main/crates/rspack_plugin_split_chunks/src/plugin/max_size.rs#L232) +- it sorts modules by that key before grouping at [max_size.rs#L278](https://github.com/web-infra-dev/rspack/blob/main/crates/rspack_plugin_split_chunks/src/plugin/max_size.rs#L278) +- it splits groups near the weakest similarity boundary at [max_size.rs#L381](https://github.com/web-infra-dev/rspack/blob/main/crates/rspack_plugin_split_chunks/src/plugin/max_size.rs#L381) +- it can hash away path info when `hidePathInfo` is enabled at [max_size.rs#L625](https://github.com/web-infra-dev/rspack/blob/main/crates/rspack_plugin_split_chunks/src/plugin/max_size.rs#L625) + +The similarity function is simple character-distance scoring at [max_size.rs#L456](https://github.com/web-infra-dev/rspack/blob/main/crates/rspack_plugin_split_chunks/src/plugin/max_size.rs#L456), so similar path prefixes tend to stay together. + +Practical meaning: + +- `maxSize` is for subdividing a chunk, not for creating a global shared chunk policy +- path locality matters +- the result is deterministic and generally stable enough for production use + +## 8. `usedExports` affects chunk combinations + +The combinator prepares chunk combinations in two modes inside [plugin/module_group.rs](https://github.com/web-infra-dev/rspack/blob/main/crates/rspack_plugin_split_chunks/src/plugin/module_group.rs): + +- by plain chunk sets at [module_group.rs#L162](https://github.com/web-infra-dev/rspack/blob/main/crates/rspack_plugin_split_chunks/src/plugin/module_group.rs#L162) +- by runtime-specific used exports at [module_group.rs#L197](https://github.com/web-infra-dev/rspack/blob/main/crates/rspack_plugin_split_chunks/src/plugin/module_group.rs#L197) + +When `usedExports` is enabled, the grouping can stay more runtime-specific. + +When it is disabled, more modules may be shared together because the grouping ignores those usage distinctions. + +This is still not tree shaking. It only changes grouping granularity. + +## 9. Why `chunks: "all"` is often the better application baseline + +This is a recommendation, not the code default. + +Combined reading of the defaults and the plugin behavior suggests: + +- the built-in cache groups already cover duplicate-module extraction and vendor extraction +- `chunks: "all"` lets those rules consider both initial and async chunk boundaries +- because the runtime still follows chunk dependencies, this usually preserves "fetch only what the current page needs" +- the main thing that breaks that property is manual forced merging, especially fixed `name` + +So for application builds, the skill recommends: + +- start with `chunks: "all"` +- keep the default cache groups +- avoid `name` unless the user consciously wants one shared asset + +## 10. Why duplicate modules can still exist + +Duplicate modules are not automatically a bug. + +The plugin explicitly removes or rejects split candidates when they do not satisfy size constraints: + +- `remove_min_size_violating_modules` drops candidates below `minSize` at [min_size.rs#L44](https://github.com/web-infra-dev/rspack/blob/main/crates/rspack_plugin_split_chunks/src/plugin/min_size.rs#L44) +- `check_min_size_reduction` rejects candidates whose total reduction is too small at [min_size.rs#L95](https://github.com/web-infra-dev/rspack/blob/main/crates/rspack_plugin_split_chunks/src/plugin/min_size.rs#L95) +- `ensure_min_size_fit` removes invalid module groups at [min_size.rs#L121](https://github.com/web-infra-dev/rspack/blob/main/crates/rspack_plugin_split_chunks/src/plugin/min_size.rs#L121) +- `ensure_max_request_fit` can also reject a candidate when request budgets would be exceeded at [max_request.rs#L12](https://github.com/web-infra-dev/rspack/blob/main/crates/rspack_plugin_split_chunks/src/plugin/max_request.rs#L12) + +Practical meaning: + +- a very small shared module may remain duplicated because extracting it would fail `minSize` +- even with `minSize: 0`, `minSizeReduction` or request limits may still block the split +- if the user insists on extracting it, they need to lower the relevant threshold or intentionally override guardrails + +## 11. Tree shaking is orthogonal to splitChunks + +From the plugin stage and responsibilities: + +- splitChunks runs during chunk optimization at [plugin/mod.rs#L305](https://github.com/web-infra-dev/rspack/blob/main/crates/rspack_plugin_split_chunks/src/plugin/mod.rs#L305) +- it groups and regroups modules into chunks; it does not decide whether a module/export is dead +- `splitChunks.usedExports` only changes runtime-specific grouping, as shown in [module_group.rs#L197](https://github.com/web-infra-dev/rspack/blob/main/crates/rspack_plugin_split_chunks/src/plugin/module_group.rs#L197) + +So the clean mental model is: + +- tree shaking decides what is kept +- splitChunks decides how kept modules are partitioned into chunks + +## 12. CSS order is a separate caveat + +For JavaScript, the guidance above stands: splitChunks does not change execution order semantics. + +For CSS, there is a separate caveat documented in [web-infra-dev discussion #12](https://github.com/orgs/web-infra-dev/discussions/12): + +- the discussion shows that extracted CSS order can become unstable after splitChunks rewrites chunk groups +- this is specifically discussed for `mini-css-extract-plugin` and `experiments.css` +- the same discussion contrasts this with `style-loader`, where CSS insertion follows JS execution order more directly + +Useful lines from that discussion: + +- CSS order can become inconsistent with import order after splitChunks at [discussion #12 lines 224-256](https://github.com/orgs/web-infra-dev/discussions/12) +- `style-loader` keeps insertion order aligned with import/execution order at [discussion #12 lines 284-286](https://github.com/orgs/web-infra-dev/discussions/12) +- extracted CSS flows are the problematic case at [discussion #12 lines 287-292](https://github.com/orgs/web-infra-dev/discussions/12) +- splitChunks is described there as further splitting of chunks, separate from JS code splitting semantics, at [discussion #12 lines 297-305](https://github.com/orgs/web-infra-dev/discussions/12) diff --git a/plugins/rstack/skills/rspack-tracing/SKILL.md b/plugins/rstack/skills/rspack-tracing/SKILL.md new file mode 100644 index 0000000..6101a9e --- /dev/null +++ b/plugins/rstack/skills/rspack-tracing/SKILL.md @@ -0,0 +1,75 @@ +--- +name: rspack-tracing +description: Comprehensive guide and toolkit for diagnosing Rspack build issues. Quickly identify where crashes/errors occur, or perform detailed performance profiling to resolve bottlenecks. Use when the user encounters build failures, slow builds, or wants to optimize Rspack performance. +--- + +# Rspack Tracing & Performance Profiling + +## When to Use This Skill + +Use this skill when you need to: + +1. Diagnose why an Rspack build is slow. +2. Understand which plugins or loaders are taking the most time. +3. Analyze a user-provided Rspack trace file. +4. Guide a user to capture a performance profile. + +## Workflow + +### 1. Capture a Trace + +First, ask the user to run their build with tracing enabled. + +```bash +# Set environment variables for logging to a file +RSPACK_PROFILE=TRACE RSPACK_TRACE_LAYER=logger RSPACK_TRACE_OUTPUT=./trace.json pnpm build +``` + +This will generate a trace file in a timestamped directory like `.rspack-profile-{timestamp}-{pid}/trace.json`. + +See [references/tracing-guide.md](references/tracing-guide.md) for more details on configuration. + +### 2. Quick Diagnosis for Crashes/Errors + +If the user wants to identify **which stage a crash or error occurred in**, use `tail` to quickly view the last events without running the full analysis: + +```bash +# Navigate to the generated profile directory +cd .rspack-profile-*/ + +# View the last 20 events to see where the build failed +tail -n 20 trace.json +``` + +The last events will show the span names and targets where the build stopped, helping to quickly pinpoint the problematic stage, plugin, or loader. + +### 3. Full Performance Analysis + +For detailed performance profiling (not just crash diagnosis), ask the user to run the bundled analysis script on the generated trace file. + +```bash +# Navigate to the generated profile directory +cd .rspack-profile-*/ + +# Run the analysis script +node ${CLAUDE_PLUGIN_ROOT}/skills/tracing/scripts/analyze_trace.js trace.json +``` + +### 4. Interpret Results + +Use the output from the script to identify bottlenecks. +Consult [references/bottlenecks.md](references/bottlenecks.md) to map span names to actionable fixes. + +### 5. Locate Slow Plugins + +Based on the "Top Slowest Hooks" from the analysis script: + +1. **Identify the Hook**: Note the hook name (e.g., `hook:CompilationOptimizeChunks`). +2. **Inspect Configuration**: Read `rspack.config.js` or `rsbuild.config.ts`. +3. **Map Hook to Plugin**: Look for plugins and their sources that tap into that specific hook. +4. **Output**: Output the paths, lines and columns of the suspected plugin source code. + +## Common Scenarios & Quick Fixes + +- [Bottleneck Reference](references/bottlenecks.md): Mapping spans to concepts. +- [Tracing Guide](references/tracing-guide.md): Detailed usage of `RSPACK_PROFILE`. diff --git a/plugins/rstack/skills/rspack-tracing/references/bottlenecks.md b/plugins/rstack/skills/rspack-tracing/references/bottlenecks.md new file mode 100644 index 0000000..b3ce4ba --- /dev/null +++ b/plugins/rstack/skills/rspack-tracing/references/bottlenecks.md @@ -0,0 +1,47 @@ +# Understanding Rspack Performance Bottlenecks + +This reference maps internal Rspack tracing spans to high-level concepts to help you identify performance issues. + +## Core Compilation Phases + +| Span Name | Description | Potential Bottlenecks | +| :---------------------- | :------------------------------------------------------------- | :-------------------------------------------------------------------------------- | +| `tracing::profiling` | The entire build process. | Overall slowness. | +| `compiler::make` | **Make Phase**: Resolving, loading, and parsing modules. | Heavy loaders (babel/swc with complex configs), too many files, slow file system. | +| `compiler::seal` | **Seal Phase**: Optimizing, splitting chunks, generating code. | Complex code splitting, heavy minification, many modules. | +| `compiler::emit_assets` | **Emit Phase**: Writing files to disk. | Slow disk I/O, huge output files. | + +## Detailed Spans + +### Make Phase (Module Processing) + +- `resolver::resolve`: Resolving import paths. + - **High Time?**: Check for complex `resolve.alias` or `resolve.modules`, or too many standard fallbacks. +- `loader::run_loaders`: Executing loaders (JavaScript/Rust). + - **High Time?**: Identify which loader is slow. If `sass-loader` or `babel-loader` is slow, consider caching (`cache: true` in config) or using `swc-loader`. +- `parser::parse`: Parsing source code into AST. + - **High Time?**: Large files? + +### Seal Phase (Optimization) + +- `compilation::code_generation`: Generating final code from AST. +- `compilation::optimize_chunks`: Splitting chunks (SplitChunksPlugin). + - `k_means_splitter`: If you see this, complex splitting logic is running. +- `js_minimizer`: Minification (SwcJsMinimizer). + - **High Time?**: Disable minimization in dev (`optimization.minimize: false`) for speed. + +### General + +- `read_file`: Reading files from disk. +- `write_file`: Writing artifacts to disk. + +## Common Fixes + +1. **Slow `make` phase**: + - Use `experiments.cache` (Persistent Cache). + - Exclude `node_modules` from expensive loaders. + - Switch to lighter loaders (e.g. `swc-loader` vs `babel-loader`). +2. **Slow `seal` phase**: + - Reduce `splitChunks` complexity. + - Disable `sourcemap` in production if acceptable cost. + - Upgrade to latest Rspack (performance improvements are frequent). diff --git a/plugins/rstack/skills/rspack-tracing/references/tracing-guide.md b/plugins/rstack/skills/rspack-tracing/references/tracing-guide.md new file mode 100644 index 0000000..fc19266 --- /dev/null +++ b/plugins/rstack/skills/rspack-tracing/references/tracing-guide.md @@ -0,0 +1,38 @@ +# Rspack Tracing Guide + +Tracing allows you to visualize exactly what Rspack is doing during a build. + +## Enabling Tracing + +Rspack uses several environment variables to control tracing. + +**Command:** + +```bash +RSPACK_PROFILE=TRACE RSPACK_TRACE_LAYER=logger RSPACK_TRACE_OUTPUT=./trace.json rspack build +``` + +### Variables + +**`RSPACK_PROFILE`**: Controls the granularity of the trace. + +- `TRACE`: Captures all spans. (Recommended for deep analysis) +- `DEBUG`, `INFO`, `WARN`, `ERROR`, `CRITICAL` +- `OFF`: Disables tracing. + +**`RSPACK_TRACE_LAYER`**: Controls the output format. + +- `logger`: Outputs standard JSON logging (required for the analysis script). + +## Output + +After running the command, Rspack will generate the file specified in `RSPACK_TRACE_OUTPUT`. + +## Using the Skill's Analysis Tool + +This skill includes a script to summarize the trace file. + +```bash +# Run the included script +node scripts/analyze_trace.js +``` diff --git a/plugins/rstack/skills/rspack-tracing/scripts/analyze_trace.js b/plugins/rstack/skills/rspack-tracing/scripts/analyze_trace.js new file mode 100644 index 0000000..87a41b0 --- /dev/null +++ b/plugins/rstack/skills/rspack-tracing/scripts/analyze_trace.js @@ -0,0 +1,165 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); + +// Parse duration string (e.g., "1.23ms", "456.78µs", "0.12s") to milliseconds +function parseDuration(durationStr) { + if (!durationStr) return 0; + + const match = durationStr.match(/^([\d.]+)(ms|µs|s|ns)$/); + if (!match) return 0; + + const value = parseFloat(match[1]); + const unit = match[2]; + + switch (unit) { + case 's': return value * 1000; + case 'ms': return value; + case 'µs': return value / 1000; + case 'ns': return value / 1000000; + default: return value; + } +} + +// Get trace file path +const tracePath = process.argv[2] || path.join(__dirname, 'trace.json'); + +if (!fs.existsSync(tracePath)) { + console.error(`Error: Trace file not found at ${tracePath}`); + console.error('Usage: node analyze_trace.js '); + process.exit(1); +} + +console.log(`Analyzing trace file: ${tracePath}\n`); + +try { + const fileContent = fs.readFileSync(tracePath, 'utf8'); + + // Parse line-delimited JSON + const events = fileContent.trim().split('\n') + .map(line => { + try { return JSON.parse(line); } + catch(err) { return null; } + }) + .filter(Boolean); + + if (!events.length) { + console.error("No valid trace events found."); + process.exit(1); + } + + console.log("=== Rspack Build Performance Analysis ===\n"); + console.log(`Total events: ${events.length}\n`); + + // Categorize events by target + const pluginStats = new Map(); + const loaderStats = new Map(); + + events.forEach(event => { + const target = event.target; + const timeField = event.fields?.['time.busy']; + + if (!timeField) return; + + const duration = parseDuration(timeField); + + if (target === 'Plugin Analysis') { + // Plugin performance + const pluginName = event.span?.name; + if (!pluginName) return; + + if (!pluginStats.has(pluginName)) { + pluginStats.set(pluginName, { + count: 0, + total: 0, + max: 0, + min: Infinity + }); + } + + const stat = pluginStats.get(pluginName); + stat.count++; + stat.total += duration; + stat.max = Math.max(stat.max, duration); + stat.min = Math.min(stat.min, duration); + + } else if (target === 'Loader Analysis') { + // Loader performance + let loaderName = event.span?.name; + + // For pitch phase (span.name is null), use resource path + if (!loaderName) { + const resource = event.fields?.resource; + if (resource) { + // Extract filename from resource path + const cleanResource = resource.replace(/^"|"$/g, ''); // Remove quotes + const filename = cleanResource.split('/').pop(); + loaderName = `Loader pitch for ${filename}`; + } else { + loaderName = 'Loader pitch (unknown resource)'; + } + } + + if (!loaderStats.has(loaderName)) { + loaderStats.set(loaderName, { + count: 0, + total: 0, + max: 0, + min: Infinity + }); + } + + const stat = loaderStats.get(loaderName); + stat.count++; + stat.total += duration; + stat.max = Math.max(stat.max, duration); + stat.min = Math.min(stat.min, duration); + } + }); + + // Display Plugin Analysis + if (pluginStats.size > 0) { + console.log("🔌 Plugin Analysis (by name):"); + console.log("─".repeat(80)); + + const sortedPlugins = [...pluginStats.entries()] + .sort((a, b) => b[1].total - a[1].total); + + sortedPlugins.forEach(([name, stat]) => { + const avg = stat.total / stat.count; + console.log(`${name}`); + console.log(` Total: ${stat.total.toFixed(2)}ms | Count: ${stat.count} | ` + + `Avg: ${avg.toFixed(2)}ms | Max: ${stat.max.toFixed(2)}ms | Min: ${stat.min.toFixed(2)}ms`); + console.log(""); + }); + + const totalPluginTime = [...pluginStats.values()] + .reduce((sum, stat) => sum + stat.total, 0); + console.log(`Total Plugin Time: ${totalPluginTime.toFixed(2)}ms\n`); + } + + // Display Loader Analysis + if (loaderStats.size > 0) { + console.log("\n🔧 Loader Analysis (by name):"); + console.log("─".repeat(80)); + + const sortedLoaders = [...loaderStats.entries()] + .sort((a, b) => b[1].total - a[1].total); + + sortedLoaders.forEach(([name, stat]) => { + const avg = stat.total / stat.count; + console.log(`${name}`); + console.log(` Total: ${stat.total.toFixed(2)}ms | Count: ${stat.count} | ` + + `Avg: ${avg.toFixed(2)}ms | Max: ${stat.max.toFixed(2)}ms | Min: ${stat.min.toFixed(2)}ms`); + console.log(""); + }); + + const totalLoaderTime = [...loaderStats.values()] + .reduce((sum, stat) => sum + stat.total, 0); + console.log(`Total Loader Time: ${totalLoaderTime.toFixed(2)}ms\n`); + } +} catch (err) { + console.error("Error processing trace file:", err); + process.exit(1); +} diff --git a/plugins/rstack/skills/rspack-v2-upgrade/SKILL.md b/plugins/rstack/skills/rspack-v2-upgrade/SKILL.md new file mode 100644 index 0000000..7eaea46 --- /dev/null +++ b/plugins/rstack/skills/rspack-v2-upgrade/SKILL.md @@ -0,0 +1,33 @@ +--- +name: rspack-v2-upgrade +description: Use when upgrading a Rspack 1.x project to v2, including dependency and configuration updates. +--- + +# Rspack 1.x to v2 Upgrade + +## Workflow + +1. **Confirm current setup** + - Read `package.json` to identify Rspack packages in use. + - Locate the Rspack config file (commonly `rspack.config.(ts|js|mjs|cjs)`). + +2. **Open the official migration guide** + - Use the official guide as the single source of truth: + - https://rspack.rs/guide/migration/rspack_1.x + +3. **Plan required changes** + - Compare the current project config with the migration guide. + - List breaking changes that apply to the project’s current config and plugins. + - Note any removed or renamed options, defaults, or plugin APIs. + +4. **Update dependencies** + - Upgrade Rspack packages to v2: `@rspack/core`, `@rspack/cli`, `@rspack/dev-server`, `@rspack/plugin-react-refresh`. + +5. **Apply migration changes** + - Update the Rspack config and related code according to the official guide. + - Remove deprecated or unsupported options. + +6. **Validate** + - Run build and dev commands. + - Run project tests or type checks. + - Fix any warnings or errors surfaced by the new version. diff --git a/plugins/rstack/skills/rspress-best-practices/SKILL.md b/plugins/rstack/skills/rspress-best-practices/SKILL.md new file mode 100644 index 0000000..af0370b --- /dev/null +++ b/plugins/rstack/skills/rspress-best-practices/SKILL.md @@ -0,0 +1,82 @@ +--- +name: rspress-best-practices +description: Rspress best practices for config, CLI workflow, content organization, frontmatter, MDX, themes, i18n, search, static assets, deployment, and debugging. Use when writing, reviewing, or troubleshooting Rspress documentation sites. +--- + +# Rspress Best Practices + +Apply these rules when writing or reviewing Rspress (v2) sites. + +## Configuration + +- Use `rspress.config.ts` and `defineConfig` from `@rspress/core` +- Set `root` explicitly when docs are not under the default `docs/` directory +- Keep site-wide settings such as `title`, `description`, `icon`, `logo`, `base`, and `lang` in config instead of repeating them in page files +- Prefer first-class Rspress options before custom theme code or low-level bundler overrides +- Keep custom theme code in a top-level `theme/` directory and import original theme pieces from `@rspress/core/theme-original` + +## CLI + +- Use `rspress dev` for local development +- Use `rspress build` for production output +- Use `rspress preview` only for local preview of the built site +- Use `rspress eject` only when CSS variables, class overrides, or layout wrapping cannot solve the customization + +## Docs Structure And Navigation + +- Keep docs content under one clear docs root and group pages by topic or workflow, not by team ownership +- Use `_meta.json` or `_nav.json` to control sidebar and navigation labels/order instead of encoding order in filenames +- Put reusable MDX snippets or shared components in shared files instead of duplicating them across pages +- Keep landing pages concise and link to deeper task-oriented guides from them + +## Writing And Frontmatter + +- Add clear `title` and `description` frontmatter, and set `sidebar`, `outline`, `navbar`, or `footer` only when page defaults are not enough +- Use `pageType: home`, `doc`, `doc-wide`, `custom`, or `blank` intentionally based on layout needs +- Write task-first headings and short intros; avoid marketing-heavy copy in technical docs +- Prefer one topic per page and split overly long pages by workflow or feature area +- Keep code examples minimal, runnable, and version-accurate + +## MDX And Components + +- Use MDX for interactive docs and embedded components, but keep the main narrative understandable as plain markdown +- Prefer documented Rspress theme/runtime APIs over importing from internal source paths +- For app-wide UI or providers, use `globalUIComponents` or theme overrides instead of repeating imports in each page + +## Theme And Styling + +- Prefer CSS variables for brand colors, spacing, and surface styling +- Prefer BEM class overrides or `Layout` slots before ejecting built-in components +- In `theme/` files, keep `export * from '@rspress/core/theme-original'` unless intentionally replacing a named export +- Avoid full component ejection unless config, CSS, and wrapping cannot meet the requirement + +## I18n, Search, And AI + +- For multilingual sites, organize locale content under per-language directories and keep navigation mirrored where practical +- Keep descriptions and other frontmatter text in the same language as the page content +- Configure search intentionally: use local search for small or medium sites, and hosted search when scale or cross-version indexing requires it +- Enable `llms` or `ssgMd` only when the site benefits from machine-readable outputs, and keep descriptions accurate because those outputs surface page summaries + +## Assets And Public Files + +- Import source-managed images and components from docs/theme source when they belong to the content +- Use `public/` only for assets that must keep stable URL paths, such as favicons, social images, or download files +- Reference public assets by absolute site path and make sure they still work when `base` is set + +## Plugins And Integration + +- Prefer official Rspress plugins for search, preview, and API-doc scenarios before building custom solutions +- For component or library docs, use `@rspress/plugin-preview` and `@rspress/plugin-api-docgen` when interactive demos or API tables are needed +- Keep plugin usage explicit in config and remove unused plugins to reduce maintenance cost + +## Build, Deploy, And Debugging + +- Validate both `rspress dev` and `rspress build`; a page that works in dev can still fail during static generation +- Verify broken links, missing assets, and wrong `base` handling before deployment +- Keep generated output out of source control unless the hosting workflow explicitly requires committed artifacts +- When debugging content issues, inspect the resolved docs root, frontmatter, and theme overrides before assuming a bundler problem + +## Documentation + +- For the latest Rspress docs, read https://rspress.rs/llms.txt +- Use the config and API docs when checking exact option names or current behavior diff --git a/plugins/rstack/skills/rspress-custom-theme/SKILL.md b/plugins/rstack/skills/rspress-custom-theme/SKILL.md new file mode 100644 index 0000000..2ee763c --- /dev/null +++ b/plugins/rstack/skills/rspress-custom-theme/SKILL.md @@ -0,0 +1,242 @@ +--- +name: rspress-custom-theme +description: Customize Rspress themes using CSS variables, Layout slots, component wrapping, or component ejection. Use when a user wants to change the look and feel of an Rspress site, override theme components, add custom navigation/sidebar/footer content, inject global providers, or modify the default Rspress theme in any way. Also use when a user mentions theme/index.tsx, Layout slots, BEM class overrides, or rspress eject. +--- + +# Rspress Custom Theme + +Guide for customizing Rspress (v2) themes. Rspress offers four levels of customization, from lightest to heaviest. Always prefer the lightest approach that meets the requirement — lighter approaches are more maintainable and survive Rspress upgrades. + +## Workflow + +1. **Understand the user's goal** — what do they want to change? (colors, layout, inject content, replace a component entirely?) +2. **Pick the right level** using the decision flow below +3. **Set up `theme/index.tsx`** if needed (Levels 1A, 3, 4 all need it) +4. **Implement** following the patterns in this skill and reference files +5. **Verify** the user's Rspress version is v2 (imports use `@rspress/core/*` not `rspress/*`) + +## Decision Flow + +| User wants to... | Level | Approach | +| ---------------------------------------------------------------- | ----- | --------------------------- | +| Change brand colors, fonts, spacing, shadows | 1 | CSS variables | +| Adjust a specific component's style (borders, padding, etc.) | 2 | BEM class overrides | +| Add content around existing components (banners, footers, logos) | 3 | Layout slots (wrap) | +| Override MDX rendering (custom `

`, ``, etc.) | 3 | `components` slot | +| Wrap the app in a provider (state, analytics, auth) | 4 | Eject `Root` | +| Replace built-in icons (logo, GitHub, search, etc.) | — | Icon re-export | +| Completely replace a built-in component | 4 | Eject that component | +| Add a global floating component (back-to-top, chat widget) | — | `globalUIComponents` config | +| Control page layout structure (hide sidebar, blank page) | — | Frontmatter `pageType` | + +--- + +## theme/index.tsx — The Entry Point + +Levels 1A, 3, and 4 all require a `theme/index.tsx` file in the project root (sibling to `docs/`). This is the single entry point for all theme customizations: + +```text +project/ +├── docs/ +├── theme/ +│ ├── index.tsx # Theme entry — re-exports + overrides +│ ├── index.css # CSS variable / BEM overrides (optional) +│ └── components/ # Ejected components (Level 4) +└── rspress.config.ts +``` + +Minimal setup: + +```tsx +// theme/index.tsx +import './index.css'; // optional +export * from '@rspress/core/theme-original'; +``` + +**Critical import rule**: Inside `theme/` files, always import from `@rspress/core/theme-original`. The path `@rspress/core/theme` resolves to your own `theme/index.tsx`, which causes circular imports. (In `docs/` MDX files, `@rspress/core/theme` is fine — it correctly points to your custom theme.) + +--- + +## Level 1: CSS Variables + +Override CSS custom properties for brand colors, backgrounds, text, code blocks, and more. + +**Option A** — `theme/index.css` (use when you also have component overrides in `theme/index.tsx`): + +```css +/* theme/index.css */ +:root { + --rp-c-brand: #7c3aed; + --rp-c-brand-light: #8b5cf6; + --rp-c-brand-dark: #6d28d9; +} +.dark { + --rp-c-brand: #a78bfa; +} +``` + +**Option B** — `globalStyles` (use when you only need CSS changes, no component overrides): + +```ts +// rspress.config.ts +export default defineConfig({ + globalStyles: path.join(__dirname, 'styles/custom.css'), +}); +``` + +> **Full variable list**: Read `references/css-variables.md` for all available CSS variables with light/dark defaults. + +--- + +## Level 2: BEM Class Overrides + +All built-in components follow BEM naming: `.rp-[component]__[element]--[modifier]`. + +Common targets: `.rp-nav`, `.rp-link`, `.rp-tabs`, `.rp-codeblock`, `.rp-codeblock__title`, `.rp-nav-menu__item--active`. + +Use these in your CSS file for targeted style changes when CSS variables aren't granular enough. + +--- + +## Level 3: Wrap (Layout Slots) + +Inject content at specific positions in the layout without replacing built-in components. Override `Layout` in `theme/index.tsx`: + +```tsx +// theme/index.tsx +import { Layout as OriginalLayout } from '@rspress/core/theme-original'; +export * from '@rspress/core/theme-original'; + +export function Layout() { + return ( + } bottom={} /> + ); +} +``` + +Use runtime hooks inside slot components — import from `@rspress/core/runtime`: `useDark()`, `useLang()`, `useVersion()`, `usePage()`, `useSite()`, `useFrontmatter()`, `useI18n()`. + +> **All slots & examples**: Read `references/layout-slots.md` for the complete slot list and usage patterns including i18n and MDX component overrides. + +--- + +## Level 4: Eject + +Copy a built-in component's source for full replacement. Only use when wrap/slots cannot achieve the customization. + +```bash +rspress eject # list available components +rspress eject DocFooter # eject to theme/components/DocFooter/ +``` + +Then re-export in `theme/index.tsx` (named export takes precedence over the wildcard): + +```tsx +export * from '@rspress/core/theme-original'; +export { DocFooter } from './components/DocFooter'; +``` + +> **Component list & patterns**: Read `references/eject-components.md` for available components, workflow, and common patterns. + +--- + +## Custom Icons + +Rspress has 27 built-in icons used across the UI. You can replace any of them by re-exporting your own icon component with the same name — no ejection needed. This uses the same `theme/index.tsx` mechanism: your named export takes precedence over the wildcard re-export. + +**Icon type**: Each icon is a React component or a URL string: + +```ts +import type { FC, SVGProps } from 'react'; +type Icon = FC> | string; +``` + +**Example 1** — Replace an icon with a custom SVG component: + +```tsx +// theme/index.tsx +export * from '@rspress/core/theme-original'; + +// Named export overrides the wildcard — replaces the GitHub icon site-wide +export const IconGithub = (props: React.SVGProps) => ( + + + +); +``` + +**Example 2** — Use an SVGR import: + +```tsx +// theme/index.tsx +export * from '@rspress/core/theme-original'; + +import CustomGithubIcon from './icons/github.svg?react'; +export const IconGithub = CustomGithubIcon; +``` + +**Using `SvgWrapper` in MDX or custom components**: + +```mdx +import { SvgWrapper, IconGithub } from '@rspress/core/theme'; + + +``` + +**Available icons**: `IconArrowDown`, `IconArrowRight`, `IconClose`, `IconCopy`, `IconDeprecated`, `IconDown`, `IconEdit`, `IconEmpty`, `IconExperimental`, `IconExternalLink`, `IconFile`, `IconGithub`, `IconGitlab`, `IconHeader`, `IconJump`, `IconLink`, `IconLoading`, `IconMenu`, `IconMoon`, `IconScrollToTop`, `IconSearch`, `IconSmallMenu`, `IconSuccess`, `IconSun`, `IconTitle`, `IconWrap`, `IconWrapped`. + +> **Source**: See the [icons source](https://github.com/web-infra-dev/rspress/blob/main/packages/core/src/theme/icons.ts) for default implementations. + +--- + +## Global UI Components + +For components that should render on every page without theme overrides: + +```ts +// rspress.config.ts +export default defineConfig({ + globalUIComponents: [ + path.join(__dirname, 'components', 'BackToTop.tsx'), + [ + path.join(__dirname, 'components', 'Analytics.tsx'), + { trackingId: '...' }, + ], + ], +}); +``` + +--- + +## Page Types + +Control layout per page via frontmatter `pageType`: + +| Value | Description | +| ---------- | ------------------------------------- | +| `home` | Home page with navbar | +| `doc` | Standard doc with sidebar and outline | +| `doc-wide` | Doc without sidebar/outline | +| `custom` | Custom content with navbar only | +| `blank` | Custom content without navbar | +| `404` | 404 error page | + +Fine-grained: set `navbar: false`, `sidebar: false`, `outline: false`, `footer: false` individually. + +--- + +## Common Pitfalls + +- **Circular import**: Using `@rspress/core/theme` instead of `@rspress/core/theme-original` in `theme/` files — causes infinite loop. +- **Eject over-use**: Ejecting when a Layout slot or CSS variable would suffice — creates upgrade burden. +- **Missing re-export**: Forgetting `export * from '@rspress/core/theme-original'` in `theme/index.tsx` — breaks all un-overridden components. +- **v1 imports**: Using `rspress/theme` or `@rspress/theme-default` — these are v1 paths. v2 uses `@rspress/core/theme-original`. + +## Reference + +- Custom theme guide: +- CSS variables: +- Layout component: +- Built-in icons: +- Built-in hooks: +- CLI commands (eject): diff --git a/plugins/rstack/skills/rspress-custom-theme/references/css-variables.md b/plugins/rstack/skills/rspress-custom-theme/references/css-variables.md new file mode 100644 index 0000000..a06573c --- /dev/null +++ b/plugins/rstack/skills/rspress-custom-theme/references/css-variables.md @@ -0,0 +1,177 @@ +# CSS Variables Reference + +Complete list of CSS variables exposed by Rspress for theme customization. + +- **Override location**: `theme/index.css` or `globalStyles` in `rspress.config.ts` +- **Dark mode selector**: `.dark { ... }` +- **Official docs**: + +--- + +## Brand Colors (shared) + +```css +:root { + --rp-c-brand: #0095ff; + --rp-c-brand-light: #33adff; + --rp-c-brand-lighter: #c6e0fd; + --rp-c-brand-dark: #0077ff; + --rp-c-brand-darker: #005fcc; + --rp-c-brand-tint: rgba(127, 163, 255, 0.16); +} +``` + +## Base Variables + +| Variable | Light | Dark | +| ---------------------- | --------------------- | ------------------------ | +| `--rp-c-bg` | `#ffffff` | `#121212` | +| `--rp-c-bg-soft` | `#f8f8f9` | `#292e37` | +| `--rp-c-bg-mute` | `#f1f1f1` | `#343a46` | +| `--rp-c-bg-alt` | `#fff` | `#000` | +| `--rp-c-divider` | `rgba(0, 0, 0, 0.25)` | `rgba(84, 84, 84, 0.65)` | +| `--rp-c-divider-light` | `rgba(0, 0, 0, 0.12)` | `rgba(84, 84, 84, 0.48)` | + +## Text Colors + +| Variable | Light | Dark | +| --------------- | ------------------------ | --------------------------- | +| `--rp-c-text-0` | `#000000` | `#ffffff` | +| `--rp-c-text-1` | `#242424` | `rgba(255, 255, 245, 0.93)` | +| `--rp-c-text-2` | `rgba(0, 0, 0, 0.7)` | `rgba(255, 255, 245, 0.65)` | +| `--rp-c-text-3` | `rgba(60, 60, 60, 0.33)` | `rgba(235, 235, 235, 0.38)` | +| `--rp-c-text-4` | `rgba(60, 60, 60, 0.18)` | `rgba(235, 235, 235, 0.18)` | +| `--rp-c-link` | `var(--rp-c-brand-dark)` | `var(--rp-c-brand-light)` | + +## Inline Code + +| Variable | Light | Dark | +| ------------------------- | --------------------------- | --------------------------- | +| `--rp-c-text-code` | `#476582` | `#c9def1` | +| `--rp-c-text-code-bg` | `rgba(153, 161, 179, 0.06)` | `rgba(255, 255, 255, 0.06)` | +| `--rp-c-text-code-border` | `rgba(0, 0, 0, 0.035)` | `rgba(255, 255, 255, 0.04)` | + +## Code Blocks + +| Variable | Light | Dark | +| ------------------------ | ------------------------------------- | ------------------------------------- | +| `--rp-code-font-size` | `0.875rem` | `0.875rem` | +| `--rp-code-title-bg` | `#f8f8f9` | `#191919` | +| `--rp-code-block-color` | `rgb(46, 52, 64)` | `rgb(229, 231, 235)` | +| `--rp-code-block-bg` | `var(--rp-c-bg)` | `var(--rp-c-bg)` | +| `--rp-code-block-border` | `1px solid var(--rp-c-divider-light)` | `1px solid var(--rp-c-divider-light)` | +| `--rp-code-block-shadow` | `none` | `none` | + +## Shiki Syntax Highlighting + +Rspress uses `.dark` on `html` as the public dark-mode toggle for general theme overrides. The Shiki token blocks below target `html:not(.rp-dark)` and `html.rp-dark`, which Rspress uses internally for syntax highlighting variables. + +### Light + +```css +html:not(.rp-dark) { + --shiki-foreground: inherit; + --shiki-background: transparent; + --shiki-token-constant: #1976d2; + --shiki-token-string: #31a94d; + --shiki-token-comment: rgb(182, 180, 180); + --shiki-token-keyword: #cf2727; + --shiki-token-parameter: #f59403; + --shiki-token-function: #7041c8; + --shiki-token-string-expression: #218438; + --shiki-token-punctuation: #242323; + --shiki-token-link: #22863a; + --shiki-token-deleted: #d32828; + --shiki-token-inserted: #22863a; +} +``` + +### Dark + +```css +html.rp-dark { + --shiki-foreground: inherit; + --shiki-background: transparent; + --shiki-token-constant: #6fb0fa; + --shiki-token-string: #f9a86e; + --shiki-token-comment: #6a727b; + --shiki-token-keyword: #f47481; + --shiki-token-parameter: #ff9800; + --shiki-token-function: #ae8eeb; + --shiki-token-string-expression: #4fb74d; + --shiki-token-punctuation: #bbbbbb; + --shiki-token-link: #f9a76d; + --shiki-token-deleted: #ee6d7a; + --shiki-token-inserted: #36c47f; +} +``` + +## Grays (shared) + +```css +:root { + --rp-c-gray: #8e8e8e; + --rp-c-gray-light-1: #aeaeae; + --rp-c-gray-light-2: #c7c7c7; + --rp-c-gray-light-3: #d1d1d1; + --rp-c-gray-light-4: #e5e5e5; + --rp-c-gray-light-5: #f2f2f2; +} +``` + +## Shadows (shared) + +```css +:root { + --rp-shadow-1: 0 1px 2px rgba(0, 0, 0, 0.02), 0 1px 0 rgba(0, 0, 0, 0.06); + --rp-shadow-2: 0 3px 12px rgba(0, 0, 0, 0.06), 0 1px 4px rgba(0, 0, 0, 0.07); + --rp-shadow-3: 0 12px 32px rgba(0, 0, 0, 0.1), 0 2px 6px rgba(0, 0, 0, 0.08); + --rp-shadow-4: 0 14px 44px rgba(0, 0, 0, 0.12), 0 3px 9px rgba(0, 0, 0, 0.12); + --rp-shadow-5: + 0 18px 56px rgba(0, 0, 0, 0.16), 0 4px 12px rgba(0, 0, 0, 0.16); +} +``` + +## Radius (shared) + +```css +:root { + --rp-radius: 1rem; + --rp-radius-small: 0.5rem; + --rp-radius-large: 1.5rem; +} +``` + +## Home Page + +Note: `...` in gradient values marks omitted gradient parameters, not literal CSS. See the official docs link above for complete values. + +| Variable | Light | Dark | +| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | +| `--rp-home-hero-secondary-color` | `#a673ff` | `#a673ff` | +| `--rp-home-hero-title-color` | `transparent` | `transparent` | +| `--rp-home-hero-title-bg` | `linear-gradient(90deg, var(--rp-c-brand-dark) 0%, var(--rp-c-brand-dark) 30%, var(--rp-home-hero-secondary-color) 100%)` | (same) | +| `--rp-home-background-bg` | `radial-gradient(...), radial-gradient(...), radial-gradient(...), #fff` | `radial-gradient(...), radial-gradient(...), radial-gradient(...), #121212` | +| `--rp-home-feature-bg` | `linear-gradient(135deg, #fff, #f9f9f980)` | `linear-gradient(135deg, #ffffff00, #ffffff08)` | + +## Quick Start + +```css +/* Example brand color overrides for the custom theme scaffold. */ +/* For more CSS variables, see https://rspress.rs/ui/vars. */ +:root { + --rp-c-brand: #ff5e00; + --rp-c-brand-dark: #ff704d; + --rp-c-brand-darker: #ff704d; + --rp-c-brand-light: #ff7524; + --rp-c-brand-lighter: #ff7524; + --rp-c-brand-tint: rgba(255, 94, 0, 0.07); + + --rp-home-hero-secondary-color: #ff5e00; +} + +.dark { + --rp-c-brand: #ff8c4d; + --rp-home-hero-secondary-color: #ff8c4d; +} +``` diff --git a/plugins/rstack/skills/rspress-custom-theme/references/eject-components.md b/plugins/rstack/skills/rspress-custom-theme/references/eject-components.md new file mode 100644 index 0000000..235800f --- /dev/null +++ b/plugins/rstack/skills/rspress-custom-theme/references/eject-components.md @@ -0,0 +1,154 @@ +# Eject Components Reference + +Eject copies a built-in component's source code into your project for full customization. This is the heaviest approach — ejected components do not receive automatic updates when Rspress upgrades. Prefer CSS variables, BEM overrides, or Layout slots whenever possible. + +Official reference: + +--- + +## Eject Command + +```bash +# List all available components +rspress eject + +# Eject a specific component +rspress eject +``` + +Ejected source is placed in `theme/components//`. + +## Available Components + +| Component | Description | Consider wrapping first? | +| ---------------- | ----------------------------------------- | ------------------------------------------------ | +| `Layout` | Main layout container with all slot props | Yes — use Layout slots instead | +| `Root` | Application root wrapper | Only eject for global providers | +| `Banner` | Notification banner at top of page | Check `top` slot first | +| `NavTitle` | Navigation logo and title | Check `navTitle` / `beforeNavTitle` slots | +| `HomeLayout` | Complete home page layout | Check home page slots first | +| `HomeHero` | Hero section on home page | Check `beforeHero` / `afterHero` slots | +| `HomeFeature` | Feature grid cards | Check `beforeFeatures` / `afterFeatures` slots | +| `HomeBackground` | Home page background effects | Try CSS variables first | +| `HomeFooter` | Home page footer | Check `bottom` slot first | +| `DocFooter` | Documentation page footer | Check `beforeDocFooter` / `afterDocFooter` slots | +| `EditLink` | "Edit this page" link | Configure via `themeConfig.editLink` | +| `LastUpdated` | Last updated timestamp | Usually config is enough | +| `PrevNextPage` | Previous/next page navigation | Check `beforeDocFooter` slot | +| `OverviewGroup` | Overview page group cards | — | +| `Tag` | Tag/label component | — | + +## Step-by-Step Eject Workflow + +1. **Eject the component:** + + ```bash + rspress eject DocFooter + ``` + +2. **Re-export in theme/index.tsx:** + + ```tsx + // theme/index.tsx + export * from '@rspress/core/theme-original'; + export { DocFooter } from './components/DocFooter'; + ``` + + The named export takes precedence over the wildcard re-export, so Rspress uses your custom version. + +3. **Modify the ejected source** in `theme/components/DocFooter/`. + +## Common Pattern: Root for Global Providers + +The most common eject use case is wrapping the entire app in a context provider (state management, analytics, auth, etc.): + +```tsx +// theme/components/Root/index.tsx +import type { RootProps } from '@rspress/core/theme'; + +export function Root({ children }: RootProps) { + return ( + + {children} + + ); +} +``` + +```tsx +// theme/index.tsx +export * from '@rspress/core/theme-original'; +export { Root } from './components/Root'; +``` + +## Common Pattern: Custom Home Page (HomeLayout) + +When the default home page structure (Hero + Features) doesn't meet the design requirements — for example, you need a completely different landing page with custom sections, animations, or a non-standard layout — write a custom `HomeLayout` component and re-export it directly: + +```tsx +// theme/components/HomeLayout/index.tsx +import { useSite, useLang } from '@rspress/core/runtime'; + +export function HomeLayout() { + const site = useSite(); + const lang = useLang(); + const { title, description } = site.siteData; + + return ( +
+
+

{title}

+

{description}

+ +
+ +
+ {/* Custom content: testimonials, stats, demos, etc. */} +
+
+ ); +} +``` + +```tsx +// theme/index.tsx +export * from '@rspress/core/theme-original'; +export { HomeLayout } from './components/HomeLayout'; +``` + +The named export overrides the built-in `HomeLayout` from the wildcard re-export — no need to eject first. + +If you only need to add content before/after the Hero or Features sections (without replacing the entire home page), prefer Layout slots (`beforeHero`, `afterHero`, `beforeFeatures`, `afterFeatures`) instead — see `references/layout-slots.md`. + +## Common Pattern: Custom Doc Footer + +```tsx +// theme/components/DocFooter/index.tsx +import { useFrontmatter } from '@rspress/core/runtime'; + +export function DocFooter() { + const frontmatter = useFrontmatter(); + return ( +
+ {frontmatter.author && Author: {frontmatter.author}} + Edit this page +
+ ); +} +``` + +## Important Notes + +- Always import from `@rspress/core/theme-original` in `theme/` files, never from `@rspress/core/theme` (the latter resolves to your own `theme/index.tsx`, causing circular imports). +- After ejecting, you own that component. Track Rspress changelogs for upstream changes you might want to incorporate manually. +- Run `rspress eject` (no args) to see the up-to-date list of available components — the list above may change between Rspress versions. diff --git a/plugins/rstack/skills/rspress-custom-theme/references/layout-slots.md b/plugins/rstack/skills/rspress-custom-theme/references/layout-slots.md new file mode 100644 index 0000000..1774218 --- /dev/null +++ b/plugins/rstack/skills/rspress-custom-theme/references/layout-slots.md @@ -0,0 +1,153 @@ +# Layout Slots Reference + +The `Layout` component accepts slot props (`React.ReactNode`) for injecting content at specific positions without replacing built-in components. This is the recommended way to extend Rspress before considering eject. + +Official reference: + +--- + +## All Available Slots + +### Navigation Bar + +| Slot | Position | +| ---------------- | ------------------------------------ | +| `beforeNav` | Before the entire navigation bar | +| `afterNav` | After the entire navigation bar | +| `beforeNavTitle` | Before the nav title/logo (top-left) | +| `navTitle` | Replaces the nav title content | +| `afterNavTitle` | After the nav title/logo | +| `beforeNavMenu` | Before the nav menu items | +| `afterNavMenu` | After the nav menu items | + +### Sidebar & Outline + +| Slot | Position | +| --------------- | ----------------------------------- | +| `beforeSidebar` | Above the left sidebar | +| `afterSidebar` | Below the left sidebar | +| `beforeOutline` | Above the right outline (TOC) panel | +| `afterOutline` | Below the right outline panel | + +### Home Page + +| Slot | Position | +| ---------------- | ------------------------ | +| `beforeHero` | Before the Hero section | +| `afterHero` | After the Hero section | +| `beforeFeatures` | Before the Features grid | +| `afterFeatures` | After the Features grid | + +### Doc Page + +| Slot | Position | +| ------------------ | ------------------------------------- | +| `beforeDoc` | At the very beginning of the doc page | +| `afterDoc` | At the very end of the doc page | +| `beforeDocContent` | Before the document content area | +| `afterDocContent` | After the document content area | +| `beforeDocFooter` | Before the doc footer (prev/next nav) | +| `afterDocFooter` | After the doc footer | + +### Global + +| Slot | Position | +| ------------ | ---------------------------------------------------------------------- | +| `top` | At the very top of the entire page | +| `bottom` | At the very bottom of the entire page | +| `components` | Custom MDX component overrides (`Record`) | + +--- + +## Usage Pattern + +All examples below follow the same structure in `theme/index.tsx`. The key parts: + +- Import `Layout` from `@rspress/core/theme-original` (not `@rspress/core/theme` — that causes circular imports) +- Re-export everything: `export * from '@rspress/core/theme-original'` +- Export your custom `Layout` that wraps the original with slot props + +### Basic — Single Slot + +```tsx +// theme/index.tsx +import { Layout as OriginalLayout } from '@rspress/core/theme-original'; +export * from '@rspress/core/theme-original'; + +export function Layout() { + return } />; +} +``` + +### Multiple Slots + +```tsx +// theme/index.tsx +import { Layout as OriginalLayout } from '@rspress/core/theme-original'; +export * from '@rspress/core/theme-original'; + +export function Layout() { + return ( + New version released!} + bottom={
© 2025 My Company
} + afterOutline={
Related resources
} + /> + ); +} +``` + +### With i18n Hooks + +```tsx +// theme/index.tsx +import { Layout as OriginalLayout } from '@rspress/core/theme-original'; +import { useLang } from '@rspress/core/runtime'; +export * from '@rspress/core/theme-original'; + +function LocalizedBanner() { + const lang = useLang(); + return
{lang === 'zh' ? '欢迎' : 'Welcome'}
; +} + +export function Layout() { + return } />; +} +``` + +### Override MDX Components + +The `components` slot accepts a `Record` to override how MDX elements render: + +```tsx +// theme/index.tsx +import { Layout as OriginalLayout } from '@rspress/core/theme-original'; +export * from '@rspress/core/theme-original'; + +function CustomH1({ children }: { children: React.ReactNode }) { + return ( +

{children}

+ ); +} + +export function Layout() { + return ; +} +``` + +--- + +## Available Hooks + +Use these hooks inside slot components. Import from `@rspress/core/runtime`. + +| Hook | Purpose | +| ------------------ | ----------------------------------- | +| `useDark()` | Returns whether dark mode is active | +| `useLang()` | Returns current language code | +| `useVersion()` | Returns current doc version | +| `usePage()` | Returns current page metadata | +| `usePages()` | Returns all pages metadata | +| `useSite()` | Returns site-level configuration | +| `useFrontmatter()` | Returns current page frontmatter | +| `useI18n()` | Returns i18n translation function | diff --git a/plugins/rstack/skills/rspress-description-generator/SKILL.md b/plugins/rstack/skills/rspress-description-generator/SKILL.md new file mode 100644 index 0000000..5c5f760 --- /dev/null +++ b/plugins/rstack/skills/rspress-description-generator/SKILL.md @@ -0,0 +1,118 @@ +--- +name: rspress-description-generator +description: Generate and maintain description frontmatter for Rspress documentation files (.md/.mdx). Use when a user wants to add SEO descriptions, improve search engine snippets, generate llms.txt metadata, prepare docs for AI summarization, or batch-update frontmatter across an Rspress doc site. Also use when adding new documentation pages to an Rspress project — every new doc file needs a description. +--- + +# Rspress Description Generator + +The `description` field in Rspress frontmatter generates `` tags, which are used for search engine snippets, social media previews, and AI-oriented formats like llms.txt. + +## Step 1 — Locate the docs root + +1. Find the Rspress config file. Search for `rspress.config.ts`, `.js`, `.mjs`, or `.cjs`. It may be at the project root or inside a subdirectory like `website/`. +2. Read the config and extract the `root` option. + - The value might be a plain string (`root: 'docs'`) or a JS expression (`root: path.join(__dirname, 'docs')`). In either case, determine the resolved directory path. + - If `root` is set, resolve it relative to the config file's directory. + - If `root` is not set, default to `docs` relative to the config file's directory. +3. Confirm the directory exists. If neither `docs` nor the configured root exists, check for `doc` as a fallback. + +## Step 2 — Detect i18n structure + +Rspress i18n projects place language subdirectories (e.g., `en/`, `zh/`) directly under the docs root: + +``` +docs/ +├── en/ +│ ├── guide/ +│ └── index.md +└── zh/ + ├── guide/ + └── index.md +``` + +Check if the docs root contains language subdirectories (two-letter codes like `en`, `zh`, `ja`, `ko`, etc.). If so, process each language directory separately — the description language should match the content language. + +If there are no language subdirectories, treat the entire docs root as a single-language site. + +## Step 3 — Scan and process files + +Glob for `**/*.md` and `**/*.mdx` under the docs root. Exclude: + +- `node_modules`, build output (`doc_build`, `.rspress`, `dist`) +- `_meta.json` / `_nav.json` (sidebar/nav config files, not doc pages) +- `**/shared/**` directories (reusable snippets included via `@import`, not standalone pages) + +For each file: + +1. **Read the file.** +2. **Check for existing `description` in frontmatter.** If it exists and is non-empty, skip. +3. **Check `pageType` in frontmatter.** For `home` pages, derive the description from the `hero.text` / `hero.tagline` fields or the features list, not from body content. +4. **Generate a description** following the writing guidelines below. +5. **Insert `description` into frontmatter:** + - If the file has frontmatter with a `title` field, insert `description` on the line after `title`. + - If the file has frontmatter without `title`, insert `description` as the first field. + - If the file has no frontmatter block, add one: + + ```yaml + --- + description: Your generated description here + --- + ``` + +### YAML formatting + +Most descriptions can be bare YAML strings: + +```yaml +description: Step-by-step guide to setting up your first Rspress site +``` + +If the description contains colons, quotes, or other special YAML characters, wrap in double quotes: + +```yaml +description: 'API reference for Rspress configuration: plugins, themes, and build options' +``` + +## Step 4 — Batch processing + +For sites with many files, use parallel agent calls to process independent files simultaneously. Group by directory (e.g., all files in `guide/`, then all in `api/`) to maintain focus and consistency within each section. + +After processing all files, do a quick scan to ensure no files were missed — re-glob and check for any remaining files without `description`. + +## Description Writing Guidelines + +The description serves three audiences: search engines (Google snippet), AI systems (llms.txt, summarization), and humans (scanning search results). A good description helps all three. + +### Rules + +- **Length**: 50–160 characters. Under 50 is too vague for search engines; over 160 gets truncated in snippets. +- **Language**: Match the document content. Chinese docs get Chinese descriptions, English docs get English descriptions. +- **Be direct**: State what the page covers. Avoid starting with "This document", "This page", "Learn about" — jump straight to the substance. +- **Be specific**: Mention concrete technologies, APIs, or concepts the page covers. "Configure Rspress plugins for search, analytics, and internationalization" beats "How to use plugins." +- **No markdown**: Plain text only, no formatting syntax. + +### Examples + +**Good:** + +| Content | Description | +| -------------------------- | ---------------------------------------------------------------------------- | +| Plugin development guide | Create custom Rspress plugins using the Node.js plugin API and runtime hooks | +| MDX component usage | Import and use React components in MDX documentation files | +| Rspress 快速开始 | 从安装到本地预览,搭建 Rspress 文档站点的完整流程 | +| 主题配置 | 自定义 Rspress 主题的导航栏、侧边栏、页脚和暗色模式 | +| Home page (pageType: home) | Rspress documentation framework — fast, MDX-powered static site generator | + +**Bad:** + +| Description | Why | +| ------------------------------------------------------- | ------------------------------------------------ | +| "About plugins" | Too vague — which plugins? what about them? | +| "This page explains how to configure the Rspress theme" | Wastes characters on "This page explains how to" | +| "Learn everything about Rspress!" | Marketing fluff, says nothing specific | + +## Documentation + +- Frontmatter fields: +- Basic config (`root` option): +- Full Rspress docs: diff --git a/plugins/rstack/skills/rspress-docs-generator/SKILL.md b/plugins/rstack/skills/rspress-docs-generator/SKILL.md new file mode 100644 index 0000000..1e8bf03 --- /dev/null +++ b/plugins/rstack/skills/rspress-docs-generator/SKILL.md @@ -0,0 +1,60 @@ +--- +name: rspress-docs-generator +description: Generate or maintain Rspress documentation for a project. Use whenever the user wants to create a new Rspress v2 docs site, add docs for user-facing feature work or PRs, maintain a dedicated Rspress docs project in a monorepo, or prevent stale Rspress v1 scaffolds/version drift before documentation work. For full v1-to-v2 migration, hand off to the rspress-v2-upgrade skill. +--- + +# Rspress Docs Generator + +Create and maintain Rspress documentation as part of normal project work. Prefer source-backed docs over generic prose: read the code, tests, examples, package metadata, and existing README before writing. + +## Use Cases + +- Create a new Rspress v2 documentation site for a project that has no docs site yet. +- Update an existing Rspress v2 docs site for a user-facing feature, API change, CLI change, or PR. +- Detect Rspress v1 version markers before documentation work and hand migration to the dedicated `rspress-v2-upgrade` skill. +- Integrate Rspress documentation into an Rslib package or workspace while preserving the repository's package manager and scripts. + +## Workflow + +1. **Inspect the project** + - Locate package files, source entry points, examples, tests, changelogs, and README files. + - Search for Rspress config files: `rspress.config.ts`, `.js`, `.mjs`, or `.cjs`. + - Inspect dependencies for Rspress version markers: `@rspress/core` version, legacy `rspress` package or `rspress/*` imports, and `@rspress/plugin-*`. + - Detect the package manager and workspace setup from lock files (`pnpm-lock.yaml`, `yarn.lock`, `package-lock.json`, `bun.lock`, `bun.lockb`) and `pnpm-workspace.yaml`. + - If a config exists, resolve the docs root from its `root` option. When `root` is absent, inspect package scripts, CI commands, and documented commands for Rspress CLI positional roots such as `rspress dev site`, `rspress build site`, or `rspress preview site`; use that argument before falling back to Rspress's default `docs/` directory relative to the config file's project cwd. If no config exists, check common roots such as `docs/`, `doc/`, `website/`, and `site/`. + +2. **Choose the correct path** + - If no Rspress docs site exists, follow [Create New Docs](references/create-new-docs.md). + - If a Rspress docs site exists but appears to be v1, follow [Rspress Version Guard](references/rspress-version-guard.md) before editing docs. + - If a Rspress v2 docs site exists, follow [Maintain Docs For PRs](references/maintain-docs-for-prs.md). + +3. **Validate before finishing** + - Run the docs build from the Rspress project directory or through the repo's root script. + - The build must pass as the primary success criterion. + - Fix broken links, missing navigation entries, invalid frontmatter, and failed MDX imports before reporting completion. + +## Code Examples + +Use the repository's package manager when creating or validating docs: + +```bash +# Create a new Rspress docs site with the detected package manager. +# Replace pnpm with npm, yarn, or bun when that is the repo package manager. +pnpm create rspress@latest + +# Validate from the docs project after replacing starter content. +pnpm run build +``` + +When maintaining docs for a PR, inspect the changed source first, then update the matching docs page and navigation: + +```text +src/formatBytes.ts -> website/docs/api/formatBytes.mdx -> website/docs/api/_meta.json +``` + +## Reference + +- [Documentation structure conventions](references/doc-structure-conventions.md) — how `_nav.json` and `_meta.json` work, with concrete examples for Guide/API sites, grouped sections, and i18n layouts. +- [Create New Docs](references/create-new-docs.md) — scaffold a Rspress v2 docs site from an undocumented project. +- [Maintain Docs For PRs](references/maintain-docs-for-prs.md) — update an existing Rspress v2 docs site for feature work. +- [Rspress Version Guard](references/rspress-version-guard.md) — detect v1 sites, avoid stale v1 scaffolds, and hand full migration to `rspress-v2-upgrade`. diff --git a/plugins/rstack/skills/rspress-docs-generator/references/create-new-docs.md b/plugins/rstack/skills/rspress-docs-generator/references/create-new-docs.md new file mode 100644 index 0000000..1238704 --- /dev/null +++ b/plugins/rstack/skills/rspress-docs-generator/references/create-new-docs.md @@ -0,0 +1,73 @@ +# Create New Docs + +Use this path when the current project has no existing Rspress documentation site. + +1. **Understand the project from source** + - Read the root `README.md` and main `package.json`. + - Identify the project type: library, CLI tool, application, plugin, monorepo package, etc. + - Read public source entry points, exported types, examples, and representative tests to understand the user-facing surface. + - Determine the target audience and what to document: getting started, configuration, API reference, CLI commands, examples, migration notes, or plugin usage. + - Cross-check Rspress basics against the official docs when choosing structure or syntax: + - Quick start: + - Conventional route: + - Frontmatter: + +2. **Choose the docs site location** + - If the repository already has a conventional docs directory (`docs/`, `doc/`, `website/`, `site/`), prefer reusing it. + - Otherwise, propose a short list of options to the user, such as `website/`, `docs/`, or `site/`, and ask which to use. + - For monorepos, place the docs site so it can reference workspace packages without crossing too many directory boundaries. + +3. **Scaffold Rspress v2** + - See the official getting-started guide for the latest creation steps: + - + - Scaffold with the package manager used by the repo. Run one command that matches the detected package manager: + + ```bash + # interactive (recommended for first-time setup) + pnpm create rspress@latest + yarn create rspress@latest + npm create rspress@latest + bun create rspress@latest + + # or non-interactive, e.g. for CI/automation + pnpm dlx create-rspress@latest my-docs --template basic-theme --tools rslint,prettier + yarn dlx create-rspress@latest my-docs --template basic-theme --tools rslint,prettier + npx -y create-rspress@latest my-docs --template basic-theme --tools rslint,prettier + bun x create-rspress@latest my-docs --template basic-theme --tools rslint,prettier + ``` + + - After scaffolding, verify the generated docs `package.json` depends on Rspress v2 through `@rspress/core` with a 2.x range. Do not accept a generated `rspress` dependency as v2. If the generated dependency is missing, still v1, or uses the legacy `rspress` package, rerun the scaffold with the detected package manager and pin the scaffold package to a known v2-compatible version, such as ` create rspress@2` or `npx -y create-rspress@2`. + - Prefer the detected package manager for installs and scripts. Do not introduce a second package manager or extra lockfile into an existing workspace. + - Pick a template that matches the project: + - `basic` — minimal site with the default theme. + - `basic-theme` — adds a `theme/` folder for customization. + - `i18n` — multilingual English/Chinese setup. + - `i18n-theme` — multilingual setup with a theme folder. + - If the project uses Rslib, follow the Rslib + Rspress integration guide instead of the generic scaffold: + - + +4. **After scaffolding** + - Install dependencies: + + ```bash + cd + install + ``` + + - In automated or non-interactive runs, do not run an unbounded dev server. If a dev-server smoke test is useful in an interactive run, start ` run dev` with an explicit timeout or background process, confirm it starts, then terminate it before continuing. + - Check the current Node.js requirement from the official Rspress docs or the installed `@rspress/core` package's `engines.node` field. Compare it with the repo's configured Node version and surface any mismatch before proceeding. + - Default build output goes to `doc_build/`. Keep it out of source control unless the repository already commits it. + +5. **Replace starter content** + - Remove placeholder pages that do not describe the project. + - Write source-backed pages from the project README, package exports, public types, examples, and tests. + - Add `title` and `description` frontmatter to each page. + - Configure navigation with `_nav.json` (top navbar) and `_meta.json` (sidebar) where the generated structure needs explicit labels or order. See [doc-structure-conventions.md](doc-structure-conventions.md) for examples. + - Follow official Rspress guidance for content features before inventing custom patterns: + - MDX and React components: + - Code blocks: + - Links: + - Static assets: + +6. **Wire project commands** + - Add or reuse scripts for docs development and build, such as `docs:dev` and `docs:build`, following the repo's package manager and workspace conventions. diff --git a/plugins/rstack/skills/rspress-docs-generator/references/doc-structure-conventions.md b/plugins/rstack/skills/rspress-docs-generator/references/doc-structure-conventions.md new file mode 100644 index 0000000..a84127c --- /dev/null +++ b/plugins/rstack/skills/rspress-docs-generator/references/doc-structure-conventions.md @@ -0,0 +1,117 @@ +# Documentation structure conventions + +Rspress generates navigation automatically when `rspress.config.ts` does not define `nav` or `sidebar`. Control the result with `_nav.json` (top navbar) and `_meta.json` (sidebar). + +Official reference: + +- Autogenerated navigation: + +- Place `_nav.json` at the docs root (or at the i18n language root such as `docs/en/`). +- Place `_meta.json` inside each subdirectory that needs explicit sidebar labels, order, or grouping. +- For clickable directories, add an `index.mdx` (or `index.md`) inside the directory. +- In leaf directories with only files, `_meta.json` can be omitted when alphabetical order is acceptable. Use `_meta.json` to customize order or labels; only preserve numeric filename prefixes when the repository already uses that convention. + +## Example 1: simple Guide + API site + +```text +docs/ +├── _nav.json +├── guide/ +│ ├── _meta.json +│ ├── index.mdx +│ ├── getting-started.mdx +│ └── configuration.mdx +└── api/ + ├── _meta.json + ├── index.mdx + └── commands.mdx +``` + +`docs/_nav.json`: + +```json +[ + { "text": "Guide", "link": "/guide/", "activeMatch": "/guide/" }, + { "text": "API", "link": "/api/", "activeMatch": "/api/" } +] +``` + +`docs/guide/_meta.json`: + +```json +[ + { "type": "file", "name": "index", "label": "Overview" }, + "getting-started", + "configuration" +] +``` + +`docs/api/_meta.json`: + +```json +[{ "type": "file", "name": "index", "label": "Overview" }, "commands"] +``` + +## Example 2: site with grouped sections + +A larger site can use `dir-section-header` to group related directories in the sidebar. + +```text +docs/ +├── _nav.json +├── guide/ +│ ├── _meta.json +│ ├── start/ +│ │ ├── index.mdx +│ │ ├── introduction.mdx +│ │ └── getting-started.mdx +│ ├── basic/ +│ │ ├── index.mdx +│ │ └── auto-nav-sidebar.mdx +│ └── advanced/ +│ └── custom-theme.mdx +└── api/ + ├── _meta.json + ├── index.mdx + └── config/ + ├── _meta.json + ├── index.mdx + └── build.mdx +``` + +`docs/guide/_meta.json`: + +```json +[ + { "type": "dir-section-header", "name": "start", "label": "Getting Started" }, + { "type": "dir-section-header", "name": "basic", "label": "Features" }, + { "type": "dir-section-header", "name": "advanced", "label": "Advanced" } +] +``` + +`docs/api/_meta.json`: + +```json +[ + { "type": "section-header", "label": "Overview" }, + { "type": "file", "name": "index", "label": "API Overview" }, + { "type": "section-header", "label": "Config" }, + { "type": "dir", "name": "config", "label": "Config Options" } +] +``` + +## Example 3: i18n layout + +For multilingual sites, place `_nav.json` inside each language root and keep the same directory shape across locales. + +```text +docs/ +├── en/ +│ ├── _nav.json +│ ├── guide/ +│ └── api/ +└── zh/ + ├── _nav.json + ├── guide/ + └── api/ +``` diff --git a/plugins/rstack/skills/rspress-docs-generator/references/maintain-docs-for-prs.md b/plugins/rstack/skills/rspress-docs-generator/references/maintain-docs-for-prs.md new file mode 100644 index 0000000..44d7803 --- /dev/null +++ b/plugins/rstack/skills/rspress-docs-generator/references/maintain-docs-for-prs.md @@ -0,0 +1,31 @@ +# Maintain Docs For PRs + +Use this path when a Rspress v2 docs site already exists. + +1. **Connect docs work to the change** + - If a PR already exists, read its title, description, linked issues, and the full PR diff. + - If no PR exists yet, inspect the current branch diff against the base branch using `git merge-base` and `git diff`. + - Identify user-facing changes: new APIs, config options, CLI flags, plugins, behavior changes, migration notes, deprecations, examples, or breaking changes. + - Use the PR title or conventional commit category as one signal, but also exercise independent judgment about whether the change affects users. + - If the change has no user-facing impact, report that no docs update is needed and explain why. + +2. **Update the right pages** + - Modify existing pages before adding new pages when the change belongs in an established guide or API reference. + - Add new pages only for new workflows or concepts that need their own sidebar or top navigation entry. + - Keep examples minimal, runnable, and version-accurate. + - Add or update `description` frontmatter for every touched or created doc page. + - Update `_meta.json` whenever a new page should appear in the sidebar. Update the docs root `_nav.json` only when the page or section should appear in the top navigation. See [doc-structure-conventions.md](doc-structure-conventions.md) for examples. + - Use official Rspress docs to confirm syntax and conventions before adding new patterns: + - Frontmatter: + - MDX and React components: + - Links: + - Autogenerated navigation: + +3. **Respect existing docs conventions** + - Match the site's language, tone, directory structure, i18n layout, frontmatter style, and MDX component patterns. + - For i18n sites, update all required locales or explicitly note which locale remains pending. + - Prefer first-class Rspress options and documented theme APIs over custom workarounds. + +4. **Validate the documentation** + - Run the docs build from the Rspress project directory or through the repo's root script. + - If docs build is expensive or unavailable, at least validate changed Markdown/MDX structure, links, imports, navigation files, and frontmatter. diff --git a/plugins/rstack/skills/rspress-docs-generator/references/rspress-version-guard.md b/plugins/rstack/skills/rspress-docs-generator/references/rspress-version-guard.md new file mode 100644 index 0000000..175178c --- /dev/null +++ b/plugins/rstack/skills/rspress-docs-generator/references/rspress-version-guard.md @@ -0,0 +1,13 @@ +# Rspress Version Guard + +Use this path when a docs site exists but may depend on Rspress v1, or when a newly scaffolded docs site must be checked before content work. This skill should keep documentation work on Rspress v2 and avoid accidentally creating or maintaining a stale v1 project. + +1. Check the docs project's dependencies and imports: + - Rspress v2 projects should depend on `@rspress/core` with a 2.x range. + - A `rspress` package dependency, `@rspress/core` 1.x, `@rspress/theme-default`, `@rspress/runtime`, or imports such as `rspress/runtime` and `rspress/theme` are v1 migration signals. +2. If the site is already on Rspress v2, continue with [Maintain Docs For PRs](maintain-docs-for-prs.md). +3. If v1 migration signals are present, do not perform the full migration in this skill. Report that the docs site must be upgraded first and use the dedicated `rspress-v2-upgrade` skill for the migration. +4. Keep the official migration guide as the fallback source of truth if the upgrade skill is unavailable or more detail is needed: + - Migration guide: + - Legacy Rspress 1.x docs: +5. After the upgrade build passes, return to this skill for source-backed documentation updates. diff --git a/plugins/rstack/skills/rspress-v2-upgrade/SKILL.md b/plugins/rstack/skills/rspress-v2-upgrade/SKILL.md new file mode 100644 index 0000000..33ae599 --- /dev/null +++ b/plugins/rstack/skills/rspress-v2-upgrade/SKILL.md @@ -0,0 +1,37 @@ +--- +name: rspress-v2-upgrade +description: Migrate Rspress projects from v1 to v2. Use when a user asks to upgrade Rspress, follow the v1-to-v2 guide, update configs/themes, or validate the upgrade. +--- + +# Rspress v1 to v2 Upgrade + +## Workflow + +1. **Confirm current setup** + - Read `package.json` to identify Rspress and plugin packages in use. + - Locate the Rspress config file (commonly `rspress.config.(ts|js|mjs|cjs)`). + - Check for custom theme files and MDX usage. + +2. **Open the official upgrade guide** + - Use the v1 → v2 guide as the source of truth: + - + +3. **Plan the upgrade path** + - List breaking changes that apply to the project's current config, plugins, and theme. + - Note any removed or renamed packages, options, and APIs. + +4. **Update dependencies** + - Replace `rspress` with `@rspress/core@^2.0.0`. + - Remove packages now built into `@rspress/core` (e.g. `rspress`, `@rspress/plugin-shiki`, `@rspress/plugin-auto-nav-sidebar`, `@rspress/plugin-container-syntax`, `@rspress/plugin-last-updated`, `@rspress/plugin-medium-zoom`, `@rspress/theme-default`, `@rspress/runtime`). + - Bump remaining Rspress plugins to latest versions via `npx taze major --include /rspress/ -w -r`. + - Ensure Node.js >= 20.9.0. + +5. **Apply config and code changes** + - Update import paths (`rspress/runtime` → `@rspress/core/runtime`, `rspress/theme` → `@rspress/core/theme`, `@rspress/theme-default` → `@rspress/core/theme-original`). + - If the project has a custom theme (in `theme` directory), use `@rspress/core/theme-original` to import the original theme components. + - Update the Rspress config to match v2 options and defaults. + - Remove deprecated or unsupported settings. + +6. **Validate** + - Run the build and dev server. + - Fix any warnings or errors that appear in the new version. If errors or warnings occur, please refer to the [Official Upgrade Guide](https://rspress.rs/guide/migration/rspress-1-x) and first check if it's caused by any omitted or incomplete migration steps. diff --git a/plugins/rstack/skills/rstest-best-practices/SKILL.md b/plugins/rstack/skills/rstest-best-practices/SKILL.md new file mode 100644 index 0000000..ac5717c --- /dev/null +++ b/plugins/rstack/skills/rstest-best-practices/SKILL.md @@ -0,0 +1,133 @@ +--- +name: rstest-best-practices +description: Rstest best practices for config, CLI workflow, test writing, mocking, snapshot testing, DOM testing, coverage, multi-project setup, CI integration, performance and debugging. Use when writing, reviewing, or troubleshooting Rstest test projects. +--- + +# Rstest Best Practices + +Apply these rules when writing or reviewing Rstest test projects. + +## Configuration + +- Use `rstest.config.ts` and `defineConfig` from `@rstest/core` +- Prefer explicit imports `import { test, expect, describe } from '@rstest/core'` over `globals: true` +- For Rsbuild projects, use `@rstest/adapter-rsbuild` with `extends: withRsbuildConfig()` to reuse build config +- For Rslib projects, use `@rstest/adapter-rslib` with `extends: withRslibConfig()` to reuse build config +- Use `setupFiles` for shared test setup (e.g., custom matchers, cleanup hooks) +- When using Rsbuild plugins (e.g., `@rsbuild/plugin-react`), add them via the `plugins` field +- For deep-level or advanced build configuration needs, use `tools.rspack` or `tools.bundlerChain` + +## CLI + +- Use `rstest` or `rstest run` to run tests (`run` disables watch mode, suitable for CI) +- Use `rstest --watch` or `rstest watch` for local development with file watching +- Use `rstest list` to list all test files and test names +- Use `rstest -u` to update snapshots +- Use `--reporter=verbose` when debugging test failures for detailed output +- Use `--config` (`-c`) to specify a custom config file path + +## Test writing + +- Import test APIs from `@rstest/core`: `test`, `describe`, `expect`, `beforeEach`, `afterEach`, etc. +- Use `test` or `it` for test cases; use `describe` for grouping related tests +- Use `.only` to focus on specific tests during development, but never commit `.only` to the codebase +- Use `.skip` or `.todo` to mark incomplete or temporarily skipped tests +- Prefer small, focused test cases that test a single behavior +- For async error paths, prefer `await expect(fn()).rejects.toThrow(ErrorClass)` (or `.rejects.toMatchObject({ ... })`) over `try/catch` with `expect.fail` or `.catch(e => e)` patterns — the matcher form fails clearly if the promise unexpectedly resolves, keeps the assertion in one chain, and avoids forgetting to assert the throw at all +- For async happy paths, use `await expect(fn()).resolves.toEqual(...)` for the same reason +- Use `includeSource` for in-source testing of small utility functions (Rust-style `import.meta.rstest`) +- For in-source tests, wrap test code in `if (import.meta.rstest) { ... }` and define `import.meta.rstest` as `false` in production build config + +## Test environment + +- Use `testEnvironment: 'node'` (default) for Node.js / server-side code +- Use `testEnvironment: 'jsdom'` or `testEnvironment: 'happy-dom'` for DOM / browser API testing +- Install `jsdom` or `happy-dom` as a dev dependency when using DOM environments +- Prefer `happy-dom` for faster DOM testing; use `jsdom` when better browser API compatibility is needed +- For real browser testing, use `@rstest/browser` with Playwright +- Use inline project configs to run different test environments within one project (e.g., `node` and `jsdom` projects) + +## React / Vue testing + +- For React: use `@rsbuild/plugin-react` plugin and `@testing-library/react` for component testing +- For Vue: use `@rsbuild/plugin-vue` plugin and `@testing-library/vue` for component testing +- Create a `rstest.setup.ts` with `expect.extend(jestDomMatchers)` and `afterEach(() => cleanup())` for Testing Library +- Add the setup file to `setupFiles` in config +- For SSR testing, use `testEnvironment: 'node'` and test with `react-dom/server` or framework-specific SSR APIs + +## Mocking + +- Use `rs.mock('./module')` to mock modules +- Use `rs.fn()` to create mock functions +- Use `rs.spyOn(object, 'method')` to spy on methods +- Prefer `clearMocks`, `resetMocks`, or `restoreMocks` config options to automatically clean up mocks between tests +- Use factory functions in `rs.mock('./module', () => ({ ... }))` to provide mock implementations + +## Snapshot testing + +- Use `toMatchSnapshot()` for general snapshot testing +- Use `toMatchInlineSnapshot()` for small, readable inline snapshots +- Use `toMatchFileSnapshot()` for large or structured outputs (e.g., HTML, generated code) +- Keep snapshots concise — only include relevant data, avoid timestamps and session IDs +- Use `expect.addSnapshotSerializer()` to mask paths or sensitive data in snapshots +- Use `path-serializer` to normalize file paths across platforms +- Review snapshot changes carefully in code review + +## Coverage + +- Enable coverage with `--coverage` CLI flag or `coverage.enabled: true` in config +- Install `@rstest/coverage-istanbul` for the Istanbul coverage provider +- Use `coverage.include` to specify source files for coverage (e.g., `['src/**/*.{js,ts,tsx}']`) +- Use `coverage.thresholds` to enforce minimum coverage requirements +- Use `coverage.reporters` to generate reports in different formats (e.g., `text`, `lcov`, `html`) + +## Multi-project testing + +- Use `projects` field in root config to define multiple test projects +- For monorepos, use glob patterns like `'packages/*'` to auto-discover sub-projects +- Use `defineProject` helper in sub-project configs +- Extract shared config and use `mergeRstestConfig` to compose project configs +- Global options (`reporters`, `pool`, `isolate`, `coverage`, `bail`) must be set at the root level, not in projects + +## CI integration + +- Use `rstest run` (not `rstest watch`) in CI +- Use `--shard` for parallel test execution across CI machines (e.g., `--shard 1/3`) +- Use `--reporter=blob` with `rstest merge-reports` to combine sharded results +- Use `--reporter=junit` with `outputPath` for CI report integration +- The `github-actions` reporter is auto-enabled in GitHub Actions for inline error annotations +- Use `--bail` to stop early on first failure when appropriate + +## Performance + +- Disable `isolate` (`--no-isolate`) when tests have no side effects for faster execution via module cache reuse +- Use `pool.maxWorkers` to control parallelism based on available resources +- Keep test build fast by avoiding unnecessary Rspack plugins in test config +- Use test filtering (`rstest ` or `-t `) to run only relevant tests during development +- Leverage watch mode's incremental re-runs for fast local feedback + +## Debugging + +- Run with `DEBUG=rstest` to enable debug mode, which writes final configs and build outputs to disk +- Read generated files in `dist/.rstest-temp/.rsbuild/` to confirm final Rstest/Rsbuild/Rspack config +- Use VS Code's JavaScript Debug Terminal to run `rstest` with breakpoints +- Use `--reporter=verbose` for detailed per-test output +- Use `--printConsoleTrace` to trace console calls to their source +- Add VS Code launch config for debugging specific test files with `@rstest/core/bin/rstest.js` + +## Profiling + +- Use Rsdoctor with `RSDOCTOR=true rstest run` to analyze test build performance +- Use `samply` for native profiling of both main and worker processes +- Use Node.js `--heap-prof` for memory profiling + +## Toolchain integration + +- Use the official VS Code extension (`rstack.rstest`) for in-editor test running and debugging +- For Rslib libraries, use `@rstest/adapter-rslib` for config reuse +- For Rsbuild apps, use `@rstest/adapter-rsbuild` for config reuse +- Use `process.env.RSTEST` to detect test environment and apply test-specific config + +## Documentation + +- For the latest Rstest docs, read https://rstest.rs/llms.txt diff --git a/plugins/rstack/skills/rstest-debugging/SKILL.md b/plugins/rstack/skills/rstest-debugging/SKILL.md new file mode 100644 index 0000000..9cc674f --- /dev/null +++ b/plugins/rstack/skills/rstest-debugging/SKILL.md @@ -0,0 +1,31 @@ +--- +name: rstest-debugging +description: Debug Rstest startup, build, runtime, logging, and memory problems systematically. Use when Rstest is slower than Jest/Vitest or a previous baseline; when runner/build/tests/CLI wall disagree; when setup/collect or Node module loading dominates; when mocked modules still enter bundles; when experimental bundle coverage or asset utilization needs inspection; or when dependency bundling, assets, pools, isolation, logs, or memory need evidence-based tuning. +--- + +# Rstest Debugging + +Diagnose the measured lifecycle stage before changing configuration. Keep behavior and the test manifest fixed, change one variable at a time, and remove experiments that do not produce a repeatable benefit. + +## Workflow + +1. Establish comparable single-file and full-scope baselines with `references/performance-measurement.md`. +2. Run `rstest --trace` when supported and classify the cost as host build/startup, runtime load/setup/collect, test bodies/hooks, or CLI/report/teardown overhead. +3. Use `DEBUG=rstest` for resolved config and build output. For Rstest 0.11.7+ experimental per-test bundle coverage, follow `references/performance-measurement.md`. Use Rsdoctor only after evidence points to the compiler. Use verbose reporting or a profiler only after narrowing to runtime files/cases. +4. If dependency loading or compilation is implicated, read `references/dependency-bundling.md`. Compare the environment default, `bundleDependencies: false`, and `bundleDependencies: true`; neither bundling nor externalization is universally faster. +5. If a fully mocked heavy module still reaches the build graph, read `references/mocked-module-build-graph.md` before testing an exact external. +6. If assets, console output, pools, isolation, or memory dominate, read `references/runtime-output-memory.md`. +7. Rerun the representative file and full scope. Keep a change only when behavior, discovery, snapshots, and coverage remain valid and the benefit survives repeated measurement. Remove traces, profiles, and `.rstest` debug artifacts created by the diagnosis before handing off. + +## Guardrails + +- Use the project's installed Rstest for final claims. Label local checkout or unreleased diagnostic results separately. +- Keep Node version, test files, coverage, cache state, environment, workers, and command shape fixed while comparing. +- Do not add worker durations that overlap or subtract runner/build/tests values without verifying their lifecycle boundaries. +- Do not treat aggregate process-tree RSS as physical memory; it can double-count shared pages. +- Do not disable isolation, reduce coverage, silence failures, or change production/test semantics for a benchmark win. +- Do not stack speculative aliases, externals, compiler hooks, pool settings, or caches. Preserve only the measured minimum. + +## Handoff from migration + +When invoked from `migrate-to-rstest`, first confirm that the Jest/Vitest and Rstest manifests match. If the migration intentionally adds tests, report same-scope performance separately from final expanded-scope performance. diff --git a/plugins/rstack/skills/rstest-debugging/references/dependency-bundling.md b/plugins/rstack/skills/rstest-debugging/references/dependency-bundling.md new file mode 100644 index 0000000..a141548 --- /dev/null +++ b/plugins/rstack/skills/rstest-debugging/references/dependency-bundling.md @@ -0,0 +1,84 @@ +# Dependency Bundling + + + +Use this reference when compiler cost or repeated runtime module loading may depend on whether Rstest bundles `node_modules`. Browser mode always bundles dependencies and does not support this tuning path. + +## Source of truth + +- Rstest output configuration: https://rstest.rs/config/build/output +- Rstest profiling: https://rstest.rs/guide/debug/profiling +- Rspack lazy barrel: https://rspack.rs/guide/optimization/lazy-barrel + +## Understand the tradeoff + +Rstest builds tests with Rsbuild/Rspack before execution: + +- `node` externalizes third-party dependencies by default. +- Browser-like non-browser environments such as `jsdom` and `happy-dom` bundle them by default. +- Browser mode always bundles them. + +Bundling increases compiler work, output size, and possibly build memory. Externalization moves work into each isolated runtime: Node can repeatedly resolve package exports, read package metadata/files, compile CJS/ESM, and initialize modules. Large suites can therefore run faster with full bundling even if a single file builds faster when externalized. + +Check the installed types before using `output.bundleDependencies`; it was introduced in Rstest v0.9.5. + +## Compare three baselines + +Keep command, manifest, Node, coverage, cache, pool, and workers fixed. Measure one representative file and the full scope: + +```ts +// A. Environment default +export default defineConfig({}); + +// B. Externalize dependencies +export default defineConfig({ + output: { bundleDependencies: false }, +}); + +// C. Bundle dependencies +export default defineConfig({ + output: { bundleDependencies: true }, +}); +``` + +Interpret build and runtime separately: + +- Externalization lowers build but raises `load`/`setupFiles`/`collect`: repeated Node loading or initialization is likely material. +- Bundling raises build but lowers full-suite wall: shared assets avoided repeated runtime work. +- A single-file win can reverse across many isolated files. +- Output size alone does not identify the faster strategy. + +Do not infer the winning strategy from environment defaults or bundle size alone. + +When a Node-environment trace shows `collect` dominating while test bodies are small, compare `bundleDependencies: true` early. It is a high-signal baseline for repeated runtime module loading; only add selective bundling or exact externals after measuring it against the environment default. + +## Add selectivity only after baselines + +An array externalizes by default and bundles matching requests: + +```ts +export default defineConfig({ + output: { + bundleDependencies: ['esm-only-package', 'source-package/*'], + }, +}); +``` + +It does not re-bundle transitive dependencies reachable only through an already externalized parent. A long allowlist should be compared with `true`; keep each entry only for a measured compatibility or performance reason. + +Bundle when Rspack transformation is needed for ESM/TypeScript source, imports without extensions, aliases, CSS/assets, or measured shared-chunk/lazy-barrel value. Externalize when the package runs correctly in Node and its compiler graph dominates without offsetting runtime cost. + +Do not broadly externalize React, UI libraries, or workspace layers just because they are large. Verify package exports, ESM/CommonJS interop, styles, assets, aliases, snapshots, and coverage. + +## Preserve measured lazy-barrel wins + +A bundled ESM barrel can benefit from Rspack lazy-barrel optimization when it has explicit side-effect-free metadata and the test imports a small named subset. Eligibility is not proof of benefit. Start from packages observed in build output, change one candidate, and keep it bundled only when the same tests improve. + +`output.externals` overrides the baseline for matching requests. Use it for exact measured heavy boundaries, especially fully mocked modules, after reading `mocked-module-build-graph.md`. + +## Validate + +- Check ESM/CommonJS interop, package exports, aliases, styles/assets, snapshots, and coverage. +- Repeat the representative file and full scope enough to separate a real win from noise. +- Remove experiment-only allowlists, aliases, caches, and debug output. +- Explain retained policy in terms of measured build versus runtime behavior. diff --git a/plugins/rstack/skills/rstest-debugging/references/mocked-module-build-graph.md b/plugins/rstack/skills/rstest-debugging/references/mocked-module-build-graph.md new file mode 100644 index 0000000..538dd22 --- /dev/null +++ b/plugins/rstack/skills/rstest-debugging/references/mocked-module-build-graph.md @@ -0,0 +1,57 @@ +# Mocked Modules and the Build Graph + + + +Use this reference when an expensive module is fully replaced by `rs.mock()` but Rspack still compiles its source graph. + +## Why it happens + +`rs.mock()` is runtime replacement, not build-graph pruning. Rspack discovers static imports and can compile the real reachable graph before the runtime installs or uses the mock. Test execution can be cheap while build, assets, or runtime initialization remain expensive. + +## Test the narrow boundary + +1. Confirm the mock fully replaces the module and the test does not need real exports, initialization side effects, or coverage. +2. Externalize the exact mocked request, not a broad transitive dependency layer. +3. Run the affected file and every test in the config/project that imports the request. +4. Compare build, runtime, wall, and memory with the unchanged baseline. + +For a stable request, start narrowly: + +```ts +export default defineConfig({ + output: { + externals: ['@app/MarkdownRender'], + }, +}); +``` + +When an explicitly CommonJS runtime external is required and the installed config supports typed values: + +```ts +export default defineConfig({ + output: { + externals: [ + { + '@app/MarkdownRender': 'commonjs @app/MarkdownRender', + }, + ], + }, +}); +``` + +Match the request string seen by Rspack. Choose the module format required by the built test runtime; do not copy `commonjs` into an ESM-only setup. If each rule declares its type, a global `externalsType` is normally redundant, but verify against the installed types and behavior. + +If the import uses a relative path, an alias, or multiple package paths, inspect the effective requests and use the narrowest supported string, regular expression, object, or function rule rather than a broad name fragment. + +A plain external can fail before the runtime mock is consulted: Node may attempt an ESM dynamic import of a workspace export whose built `.js` file does not exist while the repository only has TypeScript source. If the built test runtime expects CommonJS, test an explicit `commonjs ` external. Retain it only when the exact mock still intercepts the request and the full config/project remains green. + +## Safety checks + +- Do not use this for partial mocks, `importActual`, `{ spy: true }`, or tests that exercise real side effects. +- Confirm the mock is registered before tested code loads the request. +- Remember that `output.externals` applies to the whole config/project, not only the motivating file. +- A missed mock can make Node load an externalized local TypeScript/source module without Rspack transformation. +- Recheck aliases, ESM/CommonJS interop, package conditions, snapshots, and coverage. +- If only one subset always mocks the module, isolate that subset as a separate project rather than weakening unrelated tests. + +Use bundle-utilization diagnostics when available to confirm that real assets were built but unused. Otherwise the exact external experiment plus full-scope validation is the evidence. diff --git a/plugins/rstack/skills/rstest-debugging/references/performance-measurement.md b/plugins/rstack/skills/rstest-debugging/references/performance-measurement.md new file mode 100644 index 0000000..c83be69 --- /dev/null +++ b/plugins/rstack/skills/rstest-debugging/references/performance-measurement.md @@ -0,0 +1,86 @@ +# Performance Measurement + +Use this reference before tuning an Rstest slowdown or explaining runner, build, tests, and CLI wall time. + +## Fix the comparison + +Record the exact command, working directory, Node and Rstest versions, environment, coverage, pool/workers, isolation, and cache state. Use the same test-file manifest for before/after comparisons. + +Measure both: + +1. A representative file that imports the suspected graph. +2. The full affected scope. + +The file exposes fixed compiler/runtime startup. The full scope exposes repeated per-file loading, setup, logging, and worker scheduling. Prefer several alternating runs when machine noise is visible. + +Record separately: + +- CLI wall time from process start to exit. +- Runner-reported duration. +- Build and tests/runtime fields when reported. +- Files/tests/skips and coverage. +- Output lines/bytes when logging is material. +- Memory with a declared measurement method. + +Do not assume runner, build, and tests are disjoint. Do not add worker phase totals when workers overlap. + +## Use trace to classify the lifecycle + +When supported by the installed version: + +```bash +rstest run --trace +``` + +Read the generated summary, then use the Perfetto trace to inspect overlap. Per-file phases can include: + +- `prepare`: worker runtime, global APIs, and coverage preparation. +- `envSetup`: test environment installation. +- `load`: worker receipt/loading of built assets; host production is separate. +- `setupFiles`: setup module evaluation. +- `collect`: top-level test-module evaluation. +- `tests`: suite/case construction, hooks, and test bodies. +- `coverage` and `teardown`. + +CLI wall can additionally contain config loading, compiler initialization, pool startup/shutdown, reporter I/O, artifact writing, and process teardown. Explain an unreported gap with lifecycle evidence rather than assigning it to build by subtraction. + +Use `DEBUG=rstest` for resolved configuration and temporary build output. + +## Inspect experimental bundle coverage + +Starting with Rstest 0.11.7, the exact debug namespace `rstest:bundle-coverage` writes per-test bundle information: + +```bash +DEBUG=rstest:bundle-coverage rstest run +``` + +If the project is pinned below the supporting release and a local Rstest checkout with the feature is available, invoke that checkout's CLI directly for this diagnostic. Do not change the project's dependency or lockfile merely to obtain experimental output. Record the local checkout version/commit, keep business config unchanged, and rerun final tests and timings with the project's installed Rstest. + +The current implementation writes `/.rstest/bundle-coverage-.json`. Its version-1 shape contains a `tests` array; each item currently includes: + +- `project` and `testPath`. +- `assets`, mapping each asset filename carried by that test runtime to its byte length. +- `rawV8`, containing raw V8 coverage when V8 coverage is enabled, otherwise `null`. + +To correlate bundled assets with executed code, run the same narrowed test with V8 coverage enabled, for example: + +```bash +DEBUG=rstest:bundle-coverage rstest run --coverage +``` + +Use the configured V8 provider and verify that `rawV8` is non-null. In the current version-1 output, `rawV8.entries[].filePath` can be compared with `assets` to find assets that were carried by a test runtime but have no observed V8 entry. Treat that result as investigation evidence: confirm the dependency or issuer chain before externalizing or changing mocks. + +Asset presence is not sufficient when Rspack emits one large test bundle: the asset is necessarily loaded even if most module wrappers never execute. In that case, inspect raw V8 function ranges/counts within the matching asset and compare candidate configurations using stable signals such as total asset bytes and observed named-function counts. Treat those counts as diagnostic evidence, not exact executed-byte coverage; generated wrappers, source maps, anonymous functions, and V8 range semantics limit that interpretation. + +This capability is experimental ([Rstest PR #1694](https://github.com/web-infra-dev/rstest/pull/1694)). The debug namespace, file location, schema, and interpretation may change after 0.11.7. Check the installed version and the top-level output `version` before parsing it, and do not build durable automation against the current schema without a compatibility check. Coverage collection and artifact writing also perturb timings, so use this mode to explain bundles, not to produce final performance numbers. + +Before starting, note whether `/.rstest` and any bundle-coverage files already exist. After extracting the needed evidence, delete the `bundle-coverage-.json` files created by the diagnostic run. Remove the `.rstest` directory only when this run created it and no pre-existing or unrelated files remain; never recursively remove a pre-existing debug directory merely to clean this diagnostic output. + +## Choose the next tool + +- Host build/startup dominates: inspect entries, issuer chains, dependencies, styles/assets, and plugins; use Rsdoctor if distribution remains unclear. +- `load`/`setupFiles`/`collect` grows with file count: inspect repeated Node loading, setup imports, bundled dependencies, and mocked real graphs. +- `tests` dominates a few files: use verbose output, then samply/Node profiling only on the narrowed scope. +- CLI wall exceeds runner materially: inspect reporter output, pool/process lifecycle, debug overhead, and open handles. + +Final reports must separate same-scope results from results that intentionally run more tests. diff --git a/plugins/rstack/skills/rstest-debugging/references/runtime-output-memory.md b/plugins/rstack/skills/rstest-debugging/references/runtime-output-memory.md new file mode 100644 index 0000000..3742f3a --- /dev/null +++ b/plugins/rstack/skills/rstest-debugging/references/runtime-output-memory.md @@ -0,0 +1,45 @@ +# Runtime, Output, and Memory Experiments + +Use this reference after trace evidence implicates assets, console output, worker/process lifecycle, isolation, or memory. + +## Assets + +Imported images/fonts can be emitted for every built test entry even when tests only need module resolution. Compare `output.emitAssets: false` only when no test reads emitted files or asserts emitted URLs/content. Treat successful compilation alone as insufficient proof; run all asset-related tests and snapshots. + +## Console output + +Large successful-test output can dominate reporter I/O and cross-process messaging. Measure output lines/bytes and list the highest-volume files. When the installed version supports it, compare: + +```ts +export default defineConfig({ + silent: 'passed-only', +}); +``` + +The primary benefit may be usability rather than stable speed. Confirm failed tests still replay their captured logs. Do not globally mock `console` or application loggers when tests assert logging or failures need context. + +## Pools and workers + +Compare `forks` and `threads` only after build/dependency strategy is stable. Keep worker count fixed or use the same default, alternate several full-scope runs, and compare build separately from tests/runtime. + +Threads avoid per-process startup and can reduce console transport cost, but each worker still has a V8 isolate and environment. Native modules, process-global assumptions, and test isolation can behave differently. Keep an explicit pool only with measured benefit and full-suite stability. + +Change `maxWorkers` only when CPU/memory contention is reproducible. A lower worker count can reduce peaks while increasing wall time; report the tradeoff. + +## Isolation + +Treat `isolate: false` as a semantic change, not a normal optimization. Run the full scope repeatedly and look for order-dependent mocks, singleton state, environments, timers, globals, and module caches. Do not keep it if any random or cross-file failure appears. + +## Memory + +Measure the previous runner before migration when memory comparison matters. Aggregate controller/worker RSS can double-count shared pages. Prefer: + +- Linux cgroup peak memory for CI/process-tree physical usage. +- macOS `footprint` for shared-page-aware physical usage. +- Heap profiles for allocation sources, not total native/Rspack memory. + +If the previous runner lacks the same measurement, report only Rstest's absolute observation. Do not claim improvement or regression. + +## Experiment discipline + +Keep behavior and manifest fixed. Change one field, repeat the representative file and full scope, then remove any option without a repeatable benefit. Do not keep benchmark-only reporters, traces, profiles, caches, local runner links, or debug hooks. diff --git a/plugins/rstack/skills/storybook-rsbuild/SKILL.md b/plugins/rstack/skills/storybook-rsbuild/SKILL.md new file mode 100644 index 0000000..9b29dd8 --- /dev/null +++ b/plugins/rstack/skills/storybook-rsbuild/SKILL.md @@ -0,0 +1,120 @@ +--- +name: storybook-rsbuild +description: Set up or migrate Storybook to use the Rsbuild builder. Handles fresh setup for React, Vue 3, HTML, Web Components, and React Native Web, migration from webpack5 or Vite frameworks, and integrations with Rslib, Modern.js, and Rspack. Use when asked to add Storybook, migrate Storybook to Rsbuild, configure rsbuildFinal, or integrate Storybook with Rslib/Modern.js/Rspack. +compatibility: Requires network access to read upstream docs at storybook.rsbuild.rs +--- + +# Storybook Rsbuild + +## Goal + +Set up Storybook on Rsbuild, or migrate an existing Storybook to it. Factual mappings — version compatibility, package names, install commands, config conversion patterns — live in upstream docs at `storybook.rsbuild.rs`. This skill is an action router and a behavioral checklist; it does not duplicate the docs. + +## Principles (must follow) + +1. **Single source of truth is upstream.** Fetch the relevant `storybook.rsbuild.rs` page for version tables, install commands, and config conversion patterns. Do not infer version pins or package names from training memory — the ecosystem moves faster than the model's prior. + +2. **Pin from the framework guide's Requirements table.** Each framework guide page (e.g. `https://storybook.rsbuild.rs/guide/framework/react`) carries a Requirements section listing the canonical compatible version ranges — that is the authoritative source for version pins. For fresh setups, install the latest stable `storybook` major and the matching `storybook-rsbuild` major. Do not pin from version numbers you see in code snippets elsewhere; only the docs are authoritative. + +3. **Declare `@rsbuild/core` directly.** `storybook--rsbuild` lists `@rsbuild/core` as a peer dependency, but you must still add `@rsbuild/core` to the project's own `devDependencies` so version pins and lockfile audits remain unambiguous and a future framework-package release that drops the peer cannot silently break the build. + +4. **Migration is one task with two ordered phases — both required.** Phase A: install the new framework package and update config; leave the old framework package, old builder package, and old `webpackFinal` / `viteFinal` blocks in place so [Verification](#verification) has a rollback path. Phase B: as soon as Verification passes, in the same task, remove the old framework package (e.g. `@storybook/react-webpack5`, `@storybook/vue3-vite`), the old builder package (e.g. `@storybook/builder-webpack5`, `@storybook/builder-vite`), and the old `webpackFinal` / `viteFinal` block. A migration that ends with both builders' framework packages still in `package.json` is incomplete — leaving them is the most common silent regression in this skill's evals. Phase A on its own is not a valid stopping point; if you ran Verification, you must also run cleanup. + +5. **Preserve addons; never silently drop.** When migrating, webpack-only addons (e.g. `@storybook/addon-styling-webpack`) must either be passed through `webpackAddons` (so upstream auto-translation handles them) or replaced with the equivalent Rsbuild-native pipeline (`@rsbuild/plugin-postcss`, `tools.postcss`, etc.). A migration that removes a styling addon and produces a passing `storybook build` but a visually broken story tree is still a regression. + +6. **Operate in scope.** In monorepos, modify only the package that hosts stories. Do not edit business source files unless the migration strictly requires it. + +## Step 1 — Detect scenario + +Read `package.json` and project structure to determine existing Storybook state: + +1. Check for `.storybook/` directory (in monorepos, check both root and per-package) +2. Check `package.json` for `storybook` scripts or `@storybook/*` / `storybook-*-rsbuild` in dependencies or devDependencies +3. If Storybook exists **and already uses `storybook-*-rsbuild`** → already set up; go to [Configuration](#configuration) or [Troubleshooting](#troubleshooting) as needed +4. If Storybook exists **with a non-Rsbuild builder** (`@storybook/*-webpack5`, `@storybook/*-vite`, etc.) → go to [Migration Workflow](#migration-workflow) +5. **No Storybook found** → go to [Fresh Setup Workflow](#fresh-setup-workflow) + +--- + +## Fresh Setup Workflow + +### 1. Detect ecosystem integration + +Read `package.json` dependencies and devDependencies, check in order: + +| Signal | Ecosystem | Integration guide | +| -------------------------------------- | ----------- | -------------------------------------------------------- | +| `@rslib/core` | Rslib | https://storybook.rsbuild.rs/guide/integrations/rslib | +| `@modern-js/app-tools` | Modern.js | https://storybook.rsbuild.rs/guide/integrations/modernjs | +| `@rspack/core` without `@rsbuild/core` | Pure Rspack | https://storybook.rsbuild.rs/guide/integrations/rspack | + +If matched, read the integration guide and **apply its constraints as an overlay** alongside the steps below. + +### 2. Detect UI framework + +Infer the UI framework from app dependencies (`react`, `vue`, `lit`, etc.): + +| UI Framework | Framework package | Guide | +| ---------------- | ------------------------------------ | ------------------------------------------------------------- | +| React | `storybook-react-rsbuild` | https://storybook.rsbuild.rs/guide/framework/react | +| Vue 3 | `storybook-vue3-rsbuild` | https://storybook.rsbuild.rs/guide/framework/vue | +| Vanilla JS/TS | `storybook-html-rsbuild` | https://storybook.rsbuild.rs/guide/framework/vanilla | +| Web Components | `storybook-web-components-rsbuild` | https://storybook.rsbuild.rs/guide/framework/web-components | +| React Native Web | `storybook-react-native-web-rsbuild` | https://storybook.rsbuild.rs/guide/framework/react-native-web | + +### 3. Set up Storybook + +1. In monorepos, operate in the package that will host stories +2. Read and follow the framework guide matched above; install the **latest stable** `storybook` major and the matching `storybook--rsbuild` major (Principle 2) +3. Add `@rsbuild/core` to `devDependencies` directly, alongside `storybook--rsbuild` (Principle 3) +4. Ensure `.storybook/main.*` uses the correct `framework: ''` +5. Ensure `package.json` has `storybook dev` and `storybook build` scripts +6. If no story file exists yet, scaffold at least one minimal example story (e.g. `src/stories/Example.stories.*`) so [Verification](#verification) step 2 has something to render +7. Run [Verification](#verification) below + +## Migration Workflow + +Read the upstream migration guide: https://storybook.rsbuild.rs/guide/migration + +Follow these steps **in order**: + +1. **Detect migration type** — read `.storybook/main.*` (`framework` field and/or `core.builder`) and `package.json` dependencies to classify as "from webpack5" or "from Vite", then select the matching section in the migration guide +2. **Resolve version compatibility** — read the "Version compatibility" section in the migration guide, select the correct `storybook-rsbuild` major based on installed Storybook version +3. **Replace packages** — apply the install/remove mapping from the migration guide using the project's package manager (detect from lockfile); only change packages required by the migration. Always add `@rsbuild/core` as a direct devDep alongside the new framework package (Principle 3). Do not remove the old framework package or old devDeps yet — that happens after Verification (Principle 4) +4. **Update `.storybook/main.*`** — apply the config changes exactly as shown in the migration guide for the detected framework +5. **Migrate custom builder hooks** — search for legacy builder hooks (`webpackFinal` / `viteFinal`); if found, convert following the upstream configuration guide (https://storybook.rsbuild.rs/guide/configuration) +6. **Handle addon compatibility** — keep addons unchanged initially. If a webpack-only addon must change, route it through `webpackAddons` for upstream auto-translation, or replace it with the Rsbuild-native equivalent — never silently drop it (Principle 5). Consult the migration guide's addon section for the recommended fix +7. **Verify, then clean up — both required** — run [Verification](#verification) below; once it passes, in the same task complete all of the following before reporting done (Principle 4): + - Remove the old framework package from `package.json` devDependencies (e.g. `@storybook/react-webpack5`, `@storybook/vue3-webpack5`, `@storybook/react-vite`, `@storybook/vue3-vite`, `@storybook/html-webpack5`, `@storybook/web-components-webpack5`, etc.) + - Remove the old builder package if separately listed (e.g. `@storybook/builder-webpack5`, `@storybook/builder-vite`) + - Delete the legacy `webpackFinal` / `viteFinal` block from `.storybook/main.*` + - Drop any old-builder-only devDeps that no longer have consumers + - Re-run `pnpm install` (or the project's package manager equivalent) so the lockfile reflects the cleanup + +If a factual mapping is needed (version table, package names, conversion patterns) and not present in this skill, fetch it from the upstream docs — do not guess. + +--- + +## Verification + +1. `storybook dev` starts without errors +2. At least one story renders correctly in the browser (a clean dev-server boot is not sufficient — a missing-glob in `stories` or a broken framework wiring will only surface here) +3. HMR works +4. `storybook build` completes +5. Check startup logs to confirm the Rsbuild builder is active (not webpack/vite) + +## Troubleshooting + +- **Cache issues**: remove `node_modules/.cache/storybook` and retry +- **Residual config**: if dev fails after migration, temporarily remove custom `rsbuildFinal` block to isolate the issue, then re-add incrementally +- For debugging and other issues, consult the upstream migration guide's "Debugging" section and https://storybook.rsbuild.rs/guide/configuration + +## Edge Cases + +- **Monorepo**: locate the package that hosts stories; operate there, not at root +- **Multiple `.storybook/` dirs**: pick the one referenced by `package.json` scripts +- **TS path aliases**: ensure aliases are preserved after migration; consult the upstream configuration guide for how to configure `rsbuildFinal` + +## Configuration + +For `rsbuildFinal`, builder options, TypeScript, and framework-specific options → read https://storybook.rsbuild.rs/guide/configuration diff --git a/scripts/test-rstack-context-plugin.mjs b/scripts/test-rstack-context-plugin.mjs index f9cbdc4..76dd803 100644 --- a/scripts/test-rstack-context-plugin.mjs +++ b/scripts/test-rstack-context-plugin.mjs @@ -4,6 +4,7 @@ import { chmod, mkdtemp, mkdir, + readdir, readFile, rm, writeFile, @@ -17,6 +18,7 @@ const repositoryRoot = path.resolve( path.dirname(fileURLToPath(import.meta.url)), '..', ); +const codexPluginRoot = 'plugins/rstack'; const skillNames = [ 'analyze-build', 'assess-change-impact', @@ -29,6 +31,20 @@ const skillNames = [ const readJson = async (relativePath) => JSON.parse(await readFile(path.join(repositoryRoot, relativePath), 'utf8')); +const listFiles = async (root, relativeRoot = '') => { + const entries = await readdir(path.join(root, relativeRoot), { + withFileTypes: true, + }); + const files = []; + for (const entry of entries) { + const relativePath = path.join(relativeRoot, entry.name); + if (entry.isDirectory()) + files.push(...(await listFiles(root, relativePath))); + else if (entry.isFile()) files.push(relativePath); + } + return files.sort(); +}; + const runServer = (configuration, cwd, env = {}) => spawnSync(configuration.command, configuration.args ?? [], { cwd, @@ -38,10 +54,19 @@ const runServer = (configuration, cwd, env = {}) => const testManifest = async () => { const portable = await readJson('plugin.json'); - const codex = await readJson('.codex-plugin/plugin.json'); + const marketplace = await readJson('.agents/plugins/marketplace.json'); + const codex = await readJson( + path.join(codexPluginRoot, '.codex-plugin/plugin.json'), + ); const claude = await readJson('.claude-plugin/plugin.json'); const claudeMarketplace = await readJson('.claude-plugin/marketplace.json'); - const mcp = await readJson('.mcp.json'); + const mcp = await readJson(path.join(codexPluginRoot, '.mcp.json')); + const claudeMcp = await readJson('.mcp.json'); + + await assert.rejects( + readFile(path.join(repositoryRoot, codexPluginRoot, 'plugin.json')), + (error) => error?.code === 'ENOENT', + ); assert.equal(portable.name, 'rstack'); assert.equal(codex.name, 'rstack'); @@ -50,7 +75,9 @@ const testManifest = async () => { assert.ok(codex.version.startsWith(`${portable.version}+codex.`)); assert.equal(claude.version, portable.version); assert.equal(claudeMarketplace.plugins[0].version, portable.version); + assert.equal(marketplace.plugins[0].source.path, './plugins/rstack'); assert.equal(codex.mcpServers, './.mcp.json'); + assert.deepEqual(mcp, claudeMcp); assert.ok( mcp.mcpServers?.rstack, 'the plugin must register one rstack MCP server', @@ -61,6 +88,22 @@ const testManifest = async () => { }; const testSkills = async () => { + const sourceSkillsRoot = path.join(repositoryRoot, 'skills'); + const bundledSkillsRoot = path.join( + repositoryRoot, + codexPluginRoot, + 'skills', + ); + const sourceFiles = await listFiles(sourceSkillsRoot); + const bundledFiles = await listFiles(bundledSkillsRoot); + assert.deepEqual(bundledFiles, sourceFiles); + for (const relativePath of sourceFiles) { + assert.deepEqual( + await readFile(path.join(bundledSkillsRoot, relativePath)), + await readFile(path.join(sourceSkillsRoot, relativePath)), + ); + } + let analyzeBuild; let assessChangeImpact; let debugDevCycle; @@ -153,7 +196,9 @@ const testWorkspaceLocalLauncher = async () => { ].join('\n'), ); - const configuration = (await readJson('.mcp.json')).mcpServers.rstack; + const configuration = ( + await readJson(path.join(codexPluginRoot, '.mcp.json')) + ).mcpServers.rstack; const result = runServer(configuration, workspace, { RSTACK_PLUGIN_TEST_RECORD: recordPath, }); @@ -210,7 +255,9 @@ const testPnpmWorkspaceLauncher = async () => { ].join('\n'), ); - const configuration = (await readJson('.mcp.json')).mcpServers.rstack; + const configuration = ( + await readJson(path.join(codexPluginRoot, '.mcp.json')) + ).mcpServers.rstack; const result = runServer(configuration, workspace, { PATH: path.dirname(process.execPath), RSTACK_PLUGIN_TEST_RECORD: recordPath, @@ -263,7 +310,9 @@ const testNestedPackageLauncher = async () => { ].join('\n'), ); - const configuration = (await readJson('.mcp.json')).mcpServers.rstack; + const configuration = ( + await readJson(path.join(codexPluginRoot, '.mcp.json')) + ).mcpServers.rstack; const result = runServer(configuration, workspace, { PATH: path.dirname(process.execPath), RSTACK_PLUGIN_TEST_RECORD: recordPath, @@ -304,7 +353,9 @@ const testPathLauncher = async () => { ); await chmod(executable, 0o755); - const configuration = (await readJson('.mcp.json')).mcpServers.rstack; + const configuration = ( + await readJson(path.join(codexPluginRoot, '.mcp.json')) + ).mcpServers.rstack; const result = runServer(configuration, workspace, { PATH: `${binRoot}${path.delimiter}${process.env.PATH}`, RSTACK_PLUGIN_TEST_RECORD: recordPath, @@ -401,7 +452,9 @@ const testRealRuntimeLauncher = async () => { const integrationRoot = process.env.RSTACK_PLUGIN_INTEGRATION_ROOT; if (!integrationRoot) return; - const configuration = (await readJson('.mcp.json')).mcpServers.rstack; + const configuration = ( + await readJson(path.join(codexPluginRoot, '.mcp.json')) + ).mcpServers.rstack; const tools = await listRealRuntimeTools(configuration, integrationRoot); const names = tools.map(({ name }) => name); for (const name of [ From 520a23d4310e202f1acf02276df5131b3a148ac9 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Fri, 14 Aug 2026 10:28:57 +0000 Subject: [PATCH 21/24] docs(plugin): bound build evidence summaries --- plugins/rstack/.codex-plugin/plugin.json | 2 +- plugins/rstack/skills/analyze-build/SKILL.md | 4 +++- plugins/rstack/skills/find-unused-code/SKILL.md | 4 +++- skills/analyze-build/SKILL.md | 4 +++- skills/find-unused-code/SKILL.md | 4 +++- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/plugins/rstack/.codex-plugin/plugin.json b/plugins/rstack/.codex-plugin/plugin.json index 1d57ef6..4d29fbb 100644 --- a/plugins/rstack/.codex-plugin/plugin.json +++ b/plugins/rstack/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "rstack", - "version": "0.2.4+codex.20260814100840", + "version": "0.2.4+codex.20260814102759", "description": "Agent Skills and project context tools for debugging, tracing, upgrading, and analyzing Rstack projects.", "author": { "name": "RstackJS", diff --git a/plugins/rstack/skills/analyze-build/SKILL.md b/plugins/rstack/skills/analyze-build/SKILL.md index 7dab272..360c47d 100644 --- a/plugins/rstack/skills/analyze-build/SKILL.md +++ b/plugins/rstack/skills/analyze-build/SKILL.md @@ -8,10 +8,12 @@ description: Use when summarizing Rstack build health, errors, chunks, packages, 1. Call `project_status` to establish available contexts and latest build observations. Analysis may still proceed from an explicit artifact when no context exists. 2. Obtain the explicit Rsdoctor `dataFile`. If it is missing, identify the product and inspect the matching `@rsdoctor/rspack-plugin` or `@rsdoctor/webpack-plugin` version before offering a capture. Use `RSDOCTOR_OUTPUT=json` only when the plugin version is at least `1.5.11`. For a missing, unknown, or older plugin, follow the `rsdoctor-analysis` Generation Gate: install/register the plugin when missing, configure `output.mode='brief'` with JSON output, and build with `RSDOCTOR=true` without `RSDOCTOR_OUTPUT`. Use `rs build` for an application or `rs lib` for a library, include `RSTACK_CONTEXT=1`, and ask before installing, configuring, building, or capturing. 3. Call `rsdoctor_analyze` with the narrowest suitable tool: `build_summary`, `errors_list`, `chunks_list`, `bundle_optimize`, one `tree_shaking_*` view, or one `packages_*` view. When artifact metadata is returned, compare its compiler environment and compilation hash with the build summaries from `project_status` before selecting a context. -4. When a context exists, call `product_roots` with the selected `contextId` and `dataFile`. Use context-bound claims only when `artifactBinding` is `exact`; report `mismatch` or `explicit-unverified` as artifact-only evidence. +4. When a context exists, call `product_roots` with the selected `contextId`, `dataFile`, and `rootLimit: 20`. Use `rootSummary` for complete counts and treat the returned roots as representatives; request more only when the investigation needs them. Use context-bound claims only when `artifactBinding` is `exact`; report `mismatch` or `explicit-unverified` as artifact-only evidence. 5. Treat omitted sections as unavailable. Reserve zero or healthy labels for evidence the tool actually returned. 6. Call `report_link` only when an optional navigable report would materially help. Never require a GUI or infer source execution or repository-wide dead code from an artifact. +When freshness is `partial` with no changed paths, say that the recorded inputs are unchanged but the captured input set is incomplete; do not present it as fresh proof. + If Rstack Context is unavailable, use the `rsdoctor-analysis` skill with the explicit artifact instead. diff --git a/plugins/rstack/skills/find-unused-code/SKILL.md b/plugins/rstack/skills/find-unused-code/SKILL.md index 61dbaa6..b69ed27 100644 --- a/plugins/rstack/skills/find-unused-code/SKILL.md +++ b/plugins/rstack/skills/find-unused-code/SKILL.md @@ -7,7 +7,7 @@ description: Use when listing or prioritizing artifact-scoped Rstack modules tha 1. Call `project_status` and select the matching build context. Deduplicate repeated runs by `contextId`. 2. Obtain the explicit Rsdoctor `dataFile`; offer the matching consent-gated application or library capture if absent. -3. Call `product_roots` with `contextId` and `dataFile`, then report production, published-contract, and conservative roots plus graph issues. +3. Call `product_roots` with `contextId`, `dataFile`, and `rootLimit: 20`, then report complete counts from `rootSummary` plus representative production, published-contract, and conservative roots and graph issues. Request more roots only when the investigation needs them. 4. Call `unused_candidates` with the same inputs and an optional `limit` from 1 to 100. Prefer project-owned source modules. If `ownership.project` is zero, stop without paging and say the artifact has no project-owned candidate. 5. Follow `nextCursor` only for a requested exhaustive inventory. Reuse unchanged filters. 6. Call `dead_code_explain` for the strongest candidate. Add `code_evidence` when compatible test or execution evidence helps prioritize it. If no relation was captured and the user approves running tests, call `test_snapshot` with `related: [path]` for that one source, then reuse its snapshot ID. @@ -16,4 +16,6 @@ description: Use when listing or prioritizing artifact-scoped Rstack modules tha Call every result an **artifact-scoped unreachable module candidate**. Completely unimported files are outside the artifact graph, and no candidate is deletion proof. +When freshness is `partial` with no changed paths, say that the recorded inputs are unchanged but the captured input set is incomplete; do not present it as fresh proof. + Rstest, Rslint, coverage, and Rsdoctor observations are independent optional evidence. Do not install, configure, or run a missing producer just to fill an axis; report it as unavailable and continue with the evidence that exists. diff --git a/skills/analyze-build/SKILL.md b/skills/analyze-build/SKILL.md index 7dab272..360c47d 100644 --- a/skills/analyze-build/SKILL.md +++ b/skills/analyze-build/SKILL.md @@ -8,10 +8,12 @@ description: Use when summarizing Rstack build health, errors, chunks, packages, 1. Call `project_status` to establish available contexts and latest build observations. Analysis may still proceed from an explicit artifact when no context exists. 2. Obtain the explicit Rsdoctor `dataFile`. If it is missing, identify the product and inspect the matching `@rsdoctor/rspack-plugin` or `@rsdoctor/webpack-plugin` version before offering a capture. Use `RSDOCTOR_OUTPUT=json` only when the plugin version is at least `1.5.11`. For a missing, unknown, or older plugin, follow the `rsdoctor-analysis` Generation Gate: install/register the plugin when missing, configure `output.mode='brief'` with JSON output, and build with `RSDOCTOR=true` without `RSDOCTOR_OUTPUT`. Use `rs build` for an application or `rs lib` for a library, include `RSTACK_CONTEXT=1`, and ask before installing, configuring, building, or capturing. 3. Call `rsdoctor_analyze` with the narrowest suitable tool: `build_summary`, `errors_list`, `chunks_list`, `bundle_optimize`, one `tree_shaking_*` view, or one `packages_*` view. When artifact metadata is returned, compare its compiler environment and compilation hash with the build summaries from `project_status` before selecting a context. -4. When a context exists, call `product_roots` with the selected `contextId` and `dataFile`. Use context-bound claims only when `artifactBinding` is `exact`; report `mismatch` or `explicit-unverified` as artifact-only evidence. +4. When a context exists, call `product_roots` with the selected `contextId`, `dataFile`, and `rootLimit: 20`. Use `rootSummary` for complete counts and treat the returned roots as representatives; request more only when the investigation needs them. Use context-bound claims only when `artifactBinding` is `exact`; report `mismatch` or `explicit-unverified` as artifact-only evidence. 5. Treat omitted sections as unavailable. Reserve zero or healthy labels for evidence the tool actually returned. 6. Call `report_link` only when an optional navigable report would materially help. Never require a GUI or infer source execution or repository-wide dead code from an artifact. +When freshness is `partial` with no changed paths, say that the recorded inputs are unchanged but the captured input set is incomplete; do not present it as fresh proof. + If Rstack Context is unavailable, use the `rsdoctor-analysis` skill with the explicit artifact instead. diff --git a/skills/find-unused-code/SKILL.md b/skills/find-unused-code/SKILL.md index 61dbaa6..b69ed27 100644 --- a/skills/find-unused-code/SKILL.md +++ b/skills/find-unused-code/SKILL.md @@ -7,7 +7,7 @@ description: Use when listing or prioritizing artifact-scoped Rstack modules tha 1. Call `project_status` and select the matching build context. Deduplicate repeated runs by `contextId`. 2. Obtain the explicit Rsdoctor `dataFile`; offer the matching consent-gated application or library capture if absent. -3. Call `product_roots` with `contextId` and `dataFile`, then report production, published-contract, and conservative roots plus graph issues. +3. Call `product_roots` with `contextId`, `dataFile`, and `rootLimit: 20`, then report complete counts from `rootSummary` plus representative production, published-contract, and conservative roots and graph issues. Request more roots only when the investigation needs them. 4. Call `unused_candidates` with the same inputs and an optional `limit` from 1 to 100. Prefer project-owned source modules. If `ownership.project` is zero, stop without paging and say the artifact has no project-owned candidate. 5. Follow `nextCursor` only for a requested exhaustive inventory. Reuse unchanged filters. 6. Call `dead_code_explain` for the strongest candidate. Add `code_evidence` when compatible test or execution evidence helps prioritize it. If no relation was captured and the user approves running tests, call `test_snapshot` with `related: [path]` for that one source, then reuse its snapshot ID. @@ -16,4 +16,6 @@ description: Use when listing or prioritizing artifact-scoped Rstack modules tha Call every result an **artifact-scoped unreachable module candidate**. Completely unimported files are outside the artifact graph, and no candidate is deletion proof. +When freshness is `partial` with no changed paths, say that the recorded inputs are unchanged but the captured input set is incomplete; do not present it as fresh proof. + Rstest, Rslint, coverage, and Rsdoctor observations are independent optional evidence. Do not install, configure, or run a missing producer just to fill an axis; report it as unavailable and continue with the evidence that exists. From 3b106048892ec4eaa8167962b4dd547026a735a0 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Fri, 14 Aug 2026 10:37:09 +0000 Subject: [PATCH 22/24] docs(plugin): explain optional execution coverage --- plugins/rstack/.codex-plugin/plugin.json | 2 +- plugins/rstack/skills/debug-dev-cycle/SKILL.md | 3 ++- plugins/rstack/skills/find-unused-code/SKILL.md | 2 ++ skills/debug-dev-cycle/SKILL.md | 3 ++- skills/find-unused-code/SKILL.md | 2 ++ 5 files changed, 9 insertions(+), 3 deletions(-) diff --git a/plugins/rstack/.codex-plugin/plugin.json b/plugins/rstack/.codex-plugin/plugin.json index 4d29fbb..461fdb3 100644 --- a/plugins/rstack/.codex-plugin/plugin.json +++ b/plugins/rstack/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "rstack", - "version": "0.2.4+codex.20260814102759", + "version": "0.2.4+codex.20260814103637", "description": "Agent Skills and project context tools for debugging, tracing, upgrading, and analyzing Rstack projects.", "author": { "name": "RstackJS", diff --git a/plugins/rstack/skills/debug-dev-cycle/SKILL.md b/plugins/rstack/skills/debug-dev-cycle/SKILL.md index 96f0b59..1a7bbb7 100644 --- a/plugins/rstack/skills/debug-dev-cycle/SKILL.md +++ b/plugins/rstack/skills/debug-dev-cycle/SKILL.md @@ -12,7 +12,8 @@ description: Use when diagnosing one current Rstack Rslint or Rstest failure fro 5. When `test_snapshot` fails, inspect its `errors` first. A file- or run-scoped error can explain why `test_results` contains no cases. Lead with the first actionable failure and briefly summarize the rest. 6. For a specific source file, an approved `test_snapshot` with `related: [path]` asks Rstest to select and run only statically related tests. Use one source per capture so `code_evidence.testRelation` is attributable to that source. 7. Call `code_evidence` with the relevant test or lint snapshot ID. Pass `contextId` only when also joining an explicit Rsdoctor `dataFile`; omit both for test/lint-only evidence. Keep static test relation, exact-path test outcome, aggregate execution coverage, diagnostics, and build state independent. -8. Use `lint_fix_preview` only when already captured. Do not apply it. +8. If aggregate execution reports `provider-unavailable`, call it optional missing evidence—not zero execution or dead-code evidence. Only when the user wants coverage, offer to add `@rstest/coverage-istanbul` at the exact installed `@rstest/core` version and rerun; do not install it automatically. +9. Use `lint_fix_preview` only when already captured. Do not apply it. Use `rs test list --related --json` when the user wants listing without an MCP capture or test execution. diff --git a/plugins/rstack/skills/find-unused-code/SKILL.md b/plugins/rstack/skills/find-unused-code/SKILL.md index b69ed27..bd49486 100644 --- a/plugins/rstack/skills/find-unused-code/SKILL.md +++ b/plugins/rstack/skills/find-unused-code/SKILL.md @@ -19,3 +19,5 @@ Call every result an **artifact-scoped unreachable module candidate**. Completel When freshness is `partial` with no changed paths, say that the recorded inputs are unchanged but the captured input set is incomplete; do not present it as fresh proof. Rstest, Rslint, coverage, and Rsdoctor observations are independent optional evidence. Do not install, configure, or run a missing producer just to fill an axis; report it as unavailable and continue with the evidence that exists. + +If aggregate execution reports `provider-unavailable`, say it is optional missing evidence—not zero execution or dead-code evidence. Only when the user wants coverage, offer to add `@rstest/coverage-istanbul` at the exact installed `@rstest/core` version and rerun; do not install it automatically. diff --git a/skills/debug-dev-cycle/SKILL.md b/skills/debug-dev-cycle/SKILL.md index 96f0b59..1a7bbb7 100644 --- a/skills/debug-dev-cycle/SKILL.md +++ b/skills/debug-dev-cycle/SKILL.md @@ -12,7 +12,8 @@ description: Use when diagnosing one current Rstack Rslint or Rstest failure fro 5. When `test_snapshot` fails, inspect its `errors` first. A file- or run-scoped error can explain why `test_results` contains no cases. Lead with the first actionable failure and briefly summarize the rest. 6. For a specific source file, an approved `test_snapshot` with `related: [path]` asks Rstest to select and run only statically related tests. Use one source per capture so `code_evidence.testRelation` is attributable to that source. 7. Call `code_evidence` with the relevant test or lint snapshot ID. Pass `contextId` only when also joining an explicit Rsdoctor `dataFile`; omit both for test/lint-only evidence. Keep static test relation, exact-path test outcome, aggregate execution coverage, diagnostics, and build state independent. -8. Use `lint_fix_preview` only when already captured. Do not apply it. +8. If aggregate execution reports `provider-unavailable`, call it optional missing evidence—not zero execution or dead-code evidence. Only when the user wants coverage, offer to add `@rstest/coverage-istanbul` at the exact installed `@rstest/core` version and rerun; do not install it automatically. +9. Use `lint_fix_preview` only when already captured. Do not apply it. Use `rs test list --related --json` when the user wants listing without an MCP capture or test execution. diff --git a/skills/find-unused-code/SKILL.md b/skills/find-unused-code/SKILL.md index b69ed27..bd49486 100644 --- a/skills/find-unused-code/SKILL.md +++ b/skills/find-unused-code/SKILL.md @@ -19,3 +19,5 @@ Call every result an **artifact-scoped unreachable module candidate**. Completel When freshness is `partial` with no changed paths, say that the recorded inputs are unchanged but the captured input set is incomplete; do not present it as fresh proof. Rstest, Rslint, coverage, and Rsdoctor observations are independent optional evidence. Do not install, configure, or run a missing producer just to fill an axis; report it as unavailable and continue with the evidence that exists. + +If aggregate execution reports `provider-unavailable`, say it is optional missing evidence—not zero execution or dead-code evidence. Only when the user wants coverage, offer to add `@rstest/coverage-istanbul` at the exact installed `@rstest/core` version and rerun; do not install it automatically. From 9b9c4dab1db7373f4316c017730e8c78496ba0cc Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Fri, 14 Aug 2026 23:31:06 +0000 Subject: [PATCH 23/24] docs(plugin): tighten dogfood capture guidance --- README.md | 21 ++++++++-- plugins/rstack/skills/analyze-build/SKILL.md | 2 + .../skills/assess-change-impact/SKILL.md | 2 + .../rstack/skills/debug-dev-cycle/SKILL.md | 14 ++++++- .../rstack/skills/explain-dead-code/SKILL.md | 4 +- .../rstack/skills/find-unused-code/SKILL.md | 4 +- .../skills/review-context-change/SKILL.md | 2 + scripts/test-rstack-context-plugin.mjs | 20 +++++++++- skills-test/rstack-context/evals/evals.json | 38 +++++++++++++++++++ skills-test/rstack-context/report.md | 8 ++-- skills/analyze-build/SKILL.md | 2 + skills/assess-change-impact/SKILL.md | 2 + skills/debug-dev-cycle/SKILL.md | 14 ++++++- skills/explain-dead-code/SKILL.md | 4 +- skills/find-unused-code/SKILL.md | 4 +- skills/review-context-change/SKILL.md | 2 + 16 files changed, 127 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 4db2dca..cc72cb3 100644 --- a/README.md +++ b/README.md @@ -82,9 +82,24 @@ Install Rstack CLI in the project when you want build, lint, test, coverage, or pnpm add -D rstack ``` -The MCP process may start at a monorepo root. It discovers checkout-local contexts recorded by -Rstack commands and identifies packages, products, environments, targets, configs, and variants by -`contextId`; it does not treat the server's current working directory as the selected package. +The MCP is intentionally bound to the checkout containing the Codex project/session root. It may +start at that checkout's monorepo root, then discovers checkout-local contexts recorded by Rstack +commands and identifies packages, products, environments, targets, configs, and variants by +`contextId`; it does not treat the server's current working directory as the selected package. The +tools do not accept a workspace argument. To inspect an external checkout, start a new Codex session +rooted at that checkout. + +`rs test` reads the test configuration registered by Rstack; it does not automatically adopt a +standalone `rstest.config.*`. Keep an existing Rstest config as the source of truth with a minimal +bridge: + +```ts +// rstack.config.ts +import { define } from 'rstack'; +import rstestConfig from './rstest.config'; + +define.test(rstestConfig); +``` The plugin adds these context workflows: diff --git a/plugins/rstack/skills/analyze-build/SKILL.md b/plugins/rstack/skills/analyze-build/SKILL.md index 360c47d..790ec67 100644 --- a/plugins/rstack/skills/analyze-build/SKILL.md +++ b/plugins/rstack/skills/analyze-build/SKILL.md @@ -17,3 +17,5 @@ Never require a GUI or infer source execution or repository-wide dead code from When freshness is `partial` with no changed paths, say that the recorded inputs are unchanged but the captured input set is incomplete; do not present it as fresh proof. If Rstack Context is unavailable, use the `rsdoctor-analysis` skill with the explicit artifact instead. + +The MCP is intentionally limited to the checkout containing the Codex project/session root and has no workspace argument. For an external checkout, ask the user to start a new Codex session rooted at that checkout. diff --git a/plugins/rstack/skills/assess-change-impact/SKILL.md b/plugins/rstack/skills/assess-change-impact/SKILL.md index be52a18..a09592c 100644 --- a/plugins/rstack/skills/assess-change-impact/SKILL.md +++ b/plugins/rstack/skills/assess-change-impact/SKILL.md @@ -12,3 +12,5 @@ description: Use when estimating artifact-scoped dependents, affected product ro 5. Report visited dependents, `totalVisited` versus `returned`, reached product roots by kind, distinct chunks, truncation, bounds, and provenance. Describe only the explicit artifact graph. Source-only, test-only, runtime-created, and external consumers may be unobserved. + +The MCP is intentionally limited to the checkout containing the Codex project/session root and has no workspace argument. For an external checkout, ask the user to start a new Codex session rooted at that checkout. diff --git a/plugins/rstack/skills/debug-dev-cycle/SKILL.md b/plugins/rstack/skills/debug-dev-cycle/SKILL.md index 1a7bbb7..4c2955f 100644 --- a/plugins/rstack/skills/debug-dev-cycle/SKILL.md +++ b/plugins/rstack/skills/debug-dev-cycle/SKILL.md @@ -10,11 +10,21 @@ description: Use when diagnosing one current Rstack Rslint or Rstest failure fro 3. Report freshness (`fresh`, `stale`, `partial`, or `unknown`) independently from completeness, including changed paths and coverage bounds. 4. Ask before calling `lint_snapshot` or `test_snapshot`. Always copy checkout-relative `context.packageRoot` from `project_status`; do not substitute the agent's current directory with `.` in a nested package. Pass `configPath` only for a nonstandard Rstack config. Never start watch mode through these tools. 5. When `test_snapshot` fails, inspect its `errors` first. A file- or run-scoped error can explain why `test_results` contains no cases. Lead with the first actionable failure and briefly summarize the rest. -6. For a specific source file, an approved `test_snapshot` with `related: [path]` asks Rstest to select and run only statically related tests. Use one source per capture so `code_evidence.testRelation` is attributable to that source. +6. For a specific source file, inspect its imports and prefer a directly imported leaf source over a barrel or entry point that can cause a broad selection. When supported, run `rs test list --related --files-only --json` to list statically related test files before asking for or calling a related `test_snapshot`. Report the selected test file count. If the selection is unexpectedly broad, warn about the broad selection and ask whether to run it or narrow the source; if listing is unsupported, say the preflight is unavailable. Use one source per approved capture so `code_evidence.testRelation` is attributable to that source. 7. Call `code_evidence` with the relevant test or lint snapshot ID. Pass `contextId` only when also joining an explicit Rsdoctor `dataFile`; omit both for test/lint-only evidence. Keep static test relation, exact-path test outcome, aggregate execution coverage, diagnostics, and build state independent. 8. If aggregate execution reports `provider-unavailable`, call it optional missing evidence—not zero execution or dead-code evidence. Only when the user wants coverage, offer to add `@rstest/coverage-istanbul` at the exact installed `@rstest/core` version and rerun; do not install it automatically. 9. Use `lint_fix_preview` only when already captured. Do not apply it. -Use `rs test list --related --json` when the user wants listing without an MCP capture or test execution. +If `rs test` reports that tests are not configured while a standalone `rstest.config.*` exists, explain that Rstack does not auto-adopt that file. Preserve it as the source of truth with a minimal bridge: + +```ts +// rstack.config.ts +import { define } from 'rstack'; +import rstestConfig from './rstest.config'; + +define.test(rstestConfig); +``` Use only producers configured for the selected package. If Rstest or Rslint is absent, report that axis as unavailable; do not install or configure it as part of diagnosis. + +The MCP is intentionally limited to the checkout containing the Codex project/session root and has no workspace argument. For an external checkout, ask the user to start a new Codex session rooted at that checkout. diff --git a/plugins/rstack/skills/explain-dead-code/SKILL.md b/plugins/rstack/skills/explain-dead-code/SKILL.md index 14672c2..9ddcccd 100644 --- a/plugins/rstack/skills/explain-dead-code/SKILL.md +++ b/plugins/rstack/skills/explain-dead-code/SKILL.md @@ -11,9 +11,11 @@ description: Use when explaining why one Rstack artifact module is reachable, co 4. Call `dead_code_explain` with `contextId`, `dataFile`, and the module selector. 5. Lead with the returned classification: reachable, conservatively preserved, unreachable candidate, or insufficient evidence. 6. Show one shortest root-to-module path when present. Report production reachability, public contract, shipment, optimizer retention, truncation, bounds, provenance, and `artifactBinding`. -7. When test or runtime evidence helps, call `code_evidence` with the exact checkout-relative path and matching artifact selector. If no relation was captured and the user approves running tests, call `test_snapshot` with `related: [path]` for that one source, then query its snapshot ID. +7. When test or runtime evidence helps, call `code_evidence` with the exact checkout-relative path and matching artifact selector. If no relation was captured, inspect imports and prefer a directly imported leaf source over a barrel or entry point. When supported, preflight statically related test files with `rs test list --related --files-only --json` and report the selected test file count. Warn about a broad selection and ask whether to run or narrow it before calling the consent-gated `test_snapshot`; if listing is unsupported, say the preflight is unavailable. Capture one approved source, then query its snapshot ID. 8. Keep statically related tests, exact-path test outcomes, and aggregate execution coverage independent. A source can be production-reachable but unobserved in one test run, or test-related without being executed. Never infer local-symbol usage. Aggregate execution does not prove code is dead. Rstest, Rslint, coverage, and Rsdoctor observations are independent optional evidence. A build-only or library-only repository can still answer artifact questions; report missing axes as unavailable without requiring full-stack adoption. + +The MCP is intentionally limited to the checkout containing the Codex project/session root and has no workspace argument. For an external checkout, ask the user to start a new Codex session rooted at that checkout. diff --git a/plugins/rstack/skills/find-unused-code/SKILL.md b/plugins/rstack/skills/find-unused-code/SKILL.md index bd49486..2908ee9 100644 --- a/plugins/rstack/skills/find-unused-code/SKILL.md +++ b/plugins/rstack/skills/find-unused-code/SKILL.md @@ -10,7 +10,7 @@ description: Use when listing or prioritizing artifact-scoped Rstack modules tha 3. Call `product_roots` with `contextId`, `dataFile`, and `rootLimit: 20`, then report complete counts from `rootSummary` plus representative production, published-contract, and conservative roots and graph issues. Request more roots only when the investigation needs them. 4. Call `unused_candidates` with the same inputs and an optional `limit` from 1 to 100. Prefer project-owned source modules. If `ownership.project` is zero, stop without paging and say the artifact has no project-owned candidate. 5. Follow `nextCursor` only for a requested exhaustive inventory. Reuse unchanged filters. -6. Call `dead_code_explain` for the strongest candidate. Add `code_evidence` when compatible test or execution evidence helps prioritize it. If no relation was captured and the user approves running tests, call `test_snapshot` with `related: [path]` for that one source, then reuse its snapshot ID. +6. Call `dead_code_explain` for the strongest candidate. Add `code_evidence` when compatible test or execution evidence helps prioritize it. If no relation was captured, inspect imports and prefer a directly imported leaf source over a barrel or entry point. When supported, preflight statically related test files with `rs test list --related --files-only --json` and report the selected test file count. Warn about a broad selection and ask whether to run or narrow it before calling the consent-gated `test_snapshot`; if listing is unsupported, say the preflight is unavailable. Capture one approved source, then reuse its snapshot ID. 7. Keep statically related tests, exact-path test outcomes, and aggregate execution coverage independent. `unrelated` is meaningful only for an isolated one-source relation capture; it is still not deletion proof. 8. Report root exhaustion, state axes, truncation, bounds, provenance, and artifact binding. @@ -21,3 +21,5 @@ When freshness is `partial` with no changed paths, say that the recorded inputs Rstest, Rslint, coverage, and Rsdoctor observations are independent optional evidence. Do not install, configure, or run a missing producer just to fill an axis; report it as unavailable and continue with the evidence that exists. If aggregate execution reports `provider-unavailable`, say it is optional missing evidence—not zero execution or dead-code evidence. Only when the user wants coverage, offer to add `@rstest/coverage-istanbul` at the exact installed `@rstest/core` version and rerun; do not install it automatically. + +The MCP is intentionally limited to the checkout containing the Codex project/session root and has no workspace argument. For an external checkout, ask the user to start a new Codex session rooted at that checkout. diff --git a/plugins/rstack/skills/review-context-change/SKILL.md b/plugins/rstack/skills/review-context-change/SKILL.md index 6a048ed..dd2e64b 100644 --- a/plugins/rstack/skills/review-context-change/SKILL.md +++ b/plugins/rstack/skills/review-context-change/SKILL.md @@ -14,3 +14,5 @@ description: Use when comparing two compatible Rstack lint or test snapshots, in 7. Use `lint_fix_preview` only as review material and never apply it. Do not run a capture without approval. Recommend an explicit `rs lint`, `rs test`, or `rs test list --related` verification command. + +The MCP is intentionally limited to the checkout containing the Codex project/session root and has no workspace argument. For an external checkout, ask the user to start a new Codex session rooted at that checkout. diff --git a/scripts/test-rstack-context-plugin.mjs b/scripts/test-rstack-context-plugin.mjs index 76dd803..110d7b4 100644 --- a/scripts/test-rstack-context-plugin.mjs +++ b/scripts/test-rstack-context-plugin.mjs @@ -145,7 +145,13 @@ const testSkills = async () => { assert.match(source, /test_snapshot/); assert.match(source, /statically related/i); assert.match(source, /execution.*coverage/i); + assert.match(source, /rs test list --related/); + assert.match(source, /selected test file count/i); + assert.match(source, /directly imported leaf source/i); + assert.match(source, /broad selection/i); } + assert.match(debugDevCycle, /standalone `rstest\.config/); + assert.match(debugDevCycle, /define\.test\(rstestConfig\)/); assert.match(reviewContextChange, /capture selection/); const rsdoctor = await readFile( @@ -156,7 +162,15 @@ const testSkills = async () => { const evals = await readJson('skills-test/rstack-context/evals/evals.json'); assert.equal(evals.skill_name, 'rstack-context'); - assert.ok(evals.evals.length >= 6); + assert.ok(evals.evals.length >= 9); + assert.deepEqual( + evals.evals.slice(-3).map(({ eval_name }) => eval_name), + [ + 'related-test-fanout-gate', + 'external-checkout-root-binding', + 'standalone-rstest-config-adoption', + ], + ); }; const testRuntimeOwnershipDocumentation = async () => { @@ -164,6 +178,10 @@ const testRuntimeOwnershipDocumentation = async () => { assert.match(readme, /github\.com\/rstackjs\/context/); assert.match(readme, /Rstack CLI provides[\s\S]*`rs mcp`/); + assert.match(readme, /Codex project\/session root/); + assert.match(readme, /new Codex session\s+rooted at that checkout/); + assert.match(readme, /standalone `rstest\.config/); + assert.match(readme, /define\.test\(rstestConfig\)/); }; const testWorkspaceLocalLauncher = async () => { diff --git a/skills-test/rstack-context/evals/evals.json b/skills-test/rstack-context/evals/evals.json index cb0a5f7..ce89683 100644 --- a/skills-test/rstack-context/evals/evals.json +++ b/skills-test/rstack-context/evals/evals.json @@ -80,6 +80,44 @@ "Uses rs lib for the library product", "Asks before capture, installation, or configuration" ] + }, + { + "id": 7, + "eval_name": "related-test-fanout-gate", + "prompt": "Add fresh test evidence for packages/core/src/index.ts, which is a barrel that directly imports packages/core/src/session/load.ts. Before running anything, show me how many related test files would be selected. If the selection fans out broadly, do not run it until I confirm.", + "expected_output": "The agent prefers the directly imported leaf source, preflights related-test discovery when supported, reports the selected test-file count, and asks before a broad related capture.", + "files": [], + "assertions": [ + "Prefers the directly imported leaf source over the barrel entry point", + "Uses supported rs test list --related discovery before test_snapshot", + "Reports the selected test-file count", + "Warns and asks before a broad fanout", + "Does not call test_snapshot before approval" + ] + }, + { + "id": 8, + "eval_name": "external-checkout-root-binding", + "prompt": "This Codex session is open in /repo/current, but the failing Rstack checkout is /repo/external. Can you pass a workspace argument to the Rstack MCP tools and inspect /repo/external from here?", + "expected_output": "The agent explains that the plugin MCP is bound to the Codex project/session root, does not invent a workspace argument, and directs the user to start a new Codex session rooted at the external checkout.", + "files": [], + "assertions": [ + "States that the MCP is bound to the Codex project/session root", + "Does not invent or recommend a workspace argument", + "Directs the user to start a new Codex session rooted at /repo/external" + ] + }, + { + "id": 9, + "eval_name": "standalone-rstest-config-adoption", + "prompt": "Rstack says tests are not configured even though this package already has a standalone rstest.config.ts. Diagnose the mismatch and show the smallest integration change without duplicating the Rstest options.", + "expected_output": "The agent explains that rs test does not automatically adopt a standalone Rstest config and shows a minimal rstack.config.ts that imports it and passes it to define.test.", + "files": [], + "assertions": [ + "Explains that standalone rstest.config.ts is not auto-adopted by rs test", + "Preserves the existing Rstest config as the source of truth", + "Shows rstack.config.ts importing the config and calling define.test(rstestConfig)" + ] } ] } diff --git a/skills-test/rstack-context/report.md b/skills-test/rstack-context/report.md index c3e29e5..b8f2488 100644 --- a/skills-test/rstack-context/report.md +++ b/skills-test/rstack-context/report.md @@ -2,17 +2,17 @@ ## Setup -- Date: 2026-08-13 +- Date: 2026-08-14 - Candidate: `codex/rstack-context-plugin` - Executor runs: not yet recorded - Deterministic validation: plugin launcher contract and skill schema validation ## Current result -The tracked evaluation set defines six representative and boundary workflows: stored failures, monorepo context selection, unused candidates, dead-code evidence axes, snapshot regression, and missing-artifact recovery. +The tracked evaluation set defines nine representative and boundary workflows: stored failures, monorepo context selection, unused candidates, dead-code evidence axes, snapshot regression, missing-artifact recovery, related-test fanout gating, external-checkout root binding, and standalone Rstest config adoption. -Matched Codex/Claude benchmark runs have not yet been recorded for this repository revision. No pass-rate, token, or timing claim is made. The initial deployment gate is the deterministic plugin contract plus skill validation; matched runs should replace this report before reliability claims are published. +Matched Codex/Claude benchmark runs have not yet been recorded for this repository revision. No pass-rate, token, or timing claim is made. The deterministic plugin contract first failed against the previous skill wording because it did not require selected-file counts, fanout warnings, leaf-source preference, or Rstack adoption of standalone Rstest config; it passes after the scoped guidance update. This is a structural gate, not a behavioral reliability claim. ## Iteration decision -Keep the workflows narrow and context-first. Revisit wording only when a matched run demonstrates a repeatable routing, evidence-boundary, or recovery failure. +Dogfood showed three general gaps worth preserving as eval boundaries: preflight related-test selection before consent-gated execution, project-root binding for MCP, and explicit adoption of standalone Rstest config. Rerun evals 1, 7, 8, and 9 as matched fresh Codex sessions before publishing reliability claims. diff --git a/skills/analyze-build/SKILL.md b/skills/analyze-build/SKILL.md index 360c47d..790ec67 100644 --- a/skills/analyze-build/SKILL.md +++ b/skills/analyze-build/SKILL.md @@ -17,3 +17,5 @@ Never require a GUI or infer source execution or repository-wide dead code from When freshness is `partial` with no changed paths, say that the recorded inputs are unchanged but the captured input set is incomplete; do not present it as fresh proof. If Rstack Context is unavailable, use the `rsdoctor-analysis` skill with the explicit artifact instead. + +The MCP is intentionally limited to the checkout containing the Codex project/session root and has no workspace argument. For an external checkout, ask the user to start a new Codex session rooted at that checkout. diff --git a/skills/assess-change-impact/SKILL.md b/skills/assess-change-impact/SKILL.md index be52a18..a09592c 100644 --- a/skills/assess-change-impact/SKILL.md +++ b/skills/assess-change-impact/SKILL.md @@ -12,3 +12,5 @@ description: Use when estimating artifact-scoped dependents, affected product ro 5. Report visited dependents, `totalVisited` versus `returned`, reached product roots by kind, distinct chunks, truncation, bounds, and provenance. Describe only the explicit artifact graph. Source-only, test-only, runtime-created, and external consumers may be unobserved. + +The MCP is intentionally limited to the checkout containing the Codex project/session root and has no workspace argument. For an external checkout, ask the user to start a new Codex session rooted at that checkout. diff --git a/skills/debug-dev-cycle/SKILL.md b/skills/debug-dev-cycle/SKILL.md index 1a7bbb7..4c2955f 100644 --- a/skills/debug-dev-cycle/SKILL.md +++ b/skills/debug-dev-cycle/SKILL.md @@ -10,11 +10,21 @@ description: Use when diagnosing one current Rstack Rslint or Rstest failure fro 3. Report freshness (`fresh`, `stale`, `partial`, or `unknown`) independently from completeness, including changed paths and coverage bounds. 4. Ask before calling `lint_snapshot` or `test_snapshot`. Always copy checkout-relative `context.packageRoot` from `project_status`; do not substitute the agent's current directory with `.` in a nested package. Pass `configPath` only for a nonstandard Rstack config. Never start watch mode through these tools. 5. When `test_snapshot` fails, inspect its `errors` first. A file- or run-scoped error can explain why `test_results` contains no cases. Lead with the first actionable failure and briefly summarize the rest. -6. For a specific source file, an approved `test_snapshot` with `related: [path]` asks Rstest to select and run only statically related tests. Use one source per capture so `code_evidence.testRelation` is attributable to that source. +6. For a specific source file, inspect its imports and prefer a directly imported leaf source over a barrel or entry point that can cause a broad selection. When supported, run `rs test list --related --files-only --json` to list statically related test files before asking for or calling a related `test_snapshot`. Report the selected test file count. If the selection is unexpectedly broad, warn about the broad selection and ask whether to run it or narrow the source; if listing is unsupported, say the preflight is unavailable. Use one source per approved capture so `code_evidence.testRelation` is attributable to that source. 7. Call `code_evidence` with the relevant test or lint snapshot ID. Pass `contextId` only when also joining an explicit Rsdoctor `dataFile`; omit both for test/lint-only evidence. Keep static test relation, exact-path test outcome, aggregate execution coverage, diagnostics, and build state independent. 8. If aggregate execution reports `provider-unavailable`, call it optional missing evidence—not zero execution or dead-code evidence. Only when the user wants coverage, offer to add `@rstest/coverage-istanbul` at the exact installed `@rstest/core` version and rerun; do not install it automatically. 9. Use `lint_fix_preview` only when already captured. Do not apply it. -Use `rs test list --related --json` when the user wants listing without an MCP capture or test execution. +If `rs test` reports that tests are not configured while a standalone `rstest.config.*` exists, explain that Rstack does not auto-adopt that file. Preserve it as the source of truth with a minimal bridge: + +```ts +// rstack.config.ts +import { define } from 'rstack'; +import rstestConfig from './rstest.config'; + +define.test(rstestConfig); +``` Use only producers configured for the selected package. If Rstest or Rslint is absent, report that axis as unavailable; do not install or configure it as part of diagnosis. + +The MCP is intentionally limited to the checkout containing the Codex project/session root and has no workspace argument. For an external checkout, ask the user to start a new Codex session rooted at that checkout. diff --git a/skills/explain-dead-code/SKILL.md b/skills/explain-dead-code/SKILL.md index 14672c2..9ddcccd 100644 --- a/skills/explain-dead-code/SKILL.md +++ b/skills/explain-dead-code/SKILL.md @@ -11,9 +11,11 @@ description: Use when explaining why one Rstack artifact module is reachable, co 4. Call `dead_code_explain` with `contextId`, `dataFile`, and the module selector. 5. Lead with the returned classification: reachable, conservatively preserved, unreachable candidate, or insufficient evidence. 6. Show one shortest root-to-module path when present. Report production reachability, public contract, shipment, optimizer retention, truncation, bounds, provenance, and `artifactBinding`. -7. When test or runtime evidence helps, call `code_evidence` with the exact checkout-relative path and matching artifact selector. If no relation was captured and the user approves running tests, call `test_snapshot` with `related: [path]` for that one source, then query its snapshot ID. +7. When test or runtime evidence helps, call `code_evidence` with the exact checkout-relative path and matching artifact selector. If no relation was captured, inspect imports and prefer a directly imported leaf source over a barrel or entry point. When supported, preflight statically related test files with `rs test list --related --files-only --json` and report the selected test file count. Warn about a broad selection and ask whether to run or narrow it before calling the consent-gated `test_snapshot`; if listing is unsupported, say the preflight is unavailable. Capture one approved source, then query its snapshot ID. 8. Keep statically related tests, exact-path test outcomes, and aggregate execution coverage independent. A source can be production-reachable but unobserved in one test run, or test-related without being executed. Never infer local-symbol usage. Aggregate execution does not prove code is dead. Rstest, Rslint, coverage, and Rsdoctor observations are independent optional evidence. A build-only or library-only repository can still answer artifact questions; report missing axes as unavailable without requiring full-stack adoption. + +The MCP is intentionally limited to the checkout containing the Codex project/session root and has no workspace argument. For an external checkout, ask the user to start a new Codex session rooted at that checkout. diff --git a/skills/find-unused-code/SKILL.md b/skills/find-unused-code/SKILL.md index bd49486..2908ee9 100644 --- a/skills/find-unused-code/SKILL.md +++ b/skills/find-unused-code/SKILL.md @@ -10,7 +10,7 @@ description: Use when listing or prioritizing artifact-scoped Rstack modules tha 3. Call `product_roots` with `contextId`, `dataFile`, and `rootLimit: 20`, then report complete counts from `rootSummary` plus representative production, published-contract, and conservative roots and graph issues. Request more roots only when the investigation needs them. 4. Call `unused_candidates` with the same inputs and an optional `limit` from 1 to 100. Prefer project-owned source modules. If `ownership.project` is zero, stop without paging and say the artifact has no project-owned candidate. 5. Follow `nextCursor` only for a requested exhaustive inventory. Reuse unchanged filters. -6. Call `dead_code_explain` for the strongest candidate. Add `code_evidence` when compatible test or execution evidence helps prioritize it. If no relation was captured and the user approves running tests, call `test_snapshot` with `related: [path]` for that one source, then reuse its snapshot ID. +6. Call `dead_code_explain` for the strongest candidate. Add `code_evidence` when compatible test or execution evidence helps prioritize it. If no relation was captured, inspect imports and prefer a directly imported leaf source over a barrel or entry point. When supported, preflight statically related test files with `rs test list --related --files-only --json` and report the selected test file count. Warn about a broad selection and ask whether to run or narrow it before calling the consent-gated `test_snapshot`; if listing is unsupported, say the preflight is unavailable. Capture one approved source, then reuse its snapshot ID. 7. Keep statically related tests, exact-path test outcomes, and aggregate execution coverage independent. `unrelated` is meaningful only for an isolated one-source relation capture; it is still not deletion proof. 8. Report root exhaustion, state axes, truncation, bounds, provenance, and artifact binding. @@ -21,3 +21,5 @@ When freshness is `partial` with no changed paths, say that the recorded inputs Rstest, Rslint, coverage, and Rsdoctor observations are independent optional evidence. Do not install, configure, or run a missing producer just to fill an axis; report it as unavailable and continue with the evidence that exists. If aggregate execution reports `provider-unavailable`, say it is optional missing evidence—not zero execution or dead-code evidence. Only when the user wants coverage, offer to add `@rstest/coverage-istanbul` at the exact installed `@rstest/core` version and rerun; do not install it automatically. + +The MCP is intentionally limited to the checkout containing the Codex project/session root and has no workspace argument. For an external checkout, ask the user to start a new Codex session rooted at that checkout. diff --git a/skills/review-context-change/SKILL.md b/skills/review-context-change/SKILL.md index 6a048ed..dd2e64b 100644 --- a/skills/review-context-change/SKILL.md +++ b/skills/review-context-change/SKILL.md @@ -14,3 +14,5 @@ description: Use when comparing two compatible Rstack lint or test snapshots, in 7. Use `lint_fix_preview` only as review material and never apply it. Do not run a capture without approval. Recommend an explicit `rs lint`, `rs test`, or `rs test list --related` verification command. + +The MCP is intentionally limited to the checkout containing the Codex project/session root and has no workspace argument. For an external checkout, ask the user to start a new Codex session rooted at that checkout. From 7d01e4e45503a1ace9b4d24e2ca545df4793a7ed Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Mon, 17 Aug 2026 22:58:32 +0000 Subject: [PATCH 24/24] fix: distinguish optimizer retention from roots --- plugins/rstack/skills/explain-dead-code/SKILL.md | 8 ++++++-- plugins/rstack/skills/find-unused-code/SKILL.md | 6 +++++- scripts/test-rstack-context-plugin.mjs | 4 ++++ skills/explain-dead-code/SKILL.md | 8 ++++++-- skills/find-unused-code/SKILL.md | 6 +++++- 5 files changed, 26 insertions(+), 6 deletions(-) diff --git a/plugins/rstack/skills/explain-dead-code/SKILL.md b/plugins/rstack/skills/explain-dead-code/SKILL.md index 9ddcccd..64a69c1 100644 --- a/plugins/rstack/skills/explain-dead-code/SKILL.md +++ b/plugins/rstack/skills/explain-dead-code/SKILL.md @@ -1,6 +1,6 @@ --- name: explain-dead-code -description: Use when explaining why one Rstack artifact module is reachable, conservatively preserved, retained, shipped, or apparently unused. +description: Use when explaining why one Rstack artifact module is reachable, retained, shipped, or apparently unused. --- # Explain an artifact module @@ -9,11 +9,15 @@ description: Use when explaining why one Rstack artifact module is reachable, co 2. Confirm the subject is an artifact module selector. Local symbols and exports require source analysis. 3. Obtain the explicit Rsdoctor `dataFile`; offer a consent-gated application or library capture if it is absent. 4. Call `dead_code_explain` with `contextId`, `dataFile`, and the module selector. -5. Lead with the returned classification: reachable, conservatively preserved, unreachable candidate, or insufficient evidence. +5. Lead with the returned classification: reachable, unreachable candidate, or insufficient evidence. 6. Show one shortest root-to-module path when present. Report production reachability, public contract, shipment, optimizer retention, truncation, bounds, provenance, and `artifactBinding`. 7. When test or runtime evidence helps, call `code_evidence` with the exact checkout-relative path and matching artifact selector. If no relation was captured, inspect imports and prefer a directly imported leaf source over a barrel or entry point. When supported, preflight statically related test files with `rs test list --related --files-only --json` and report the selected test file count. Warn about a broad selection and ask whether to run or narrow it before calling the consent-gated `test_snapshot`; if listing is unsupported, say the preflight is unavailable. Capture one approved source, then query its snapshot ID. 8. Keep statically related tests, exact-path test outcomes, and aggregate execution coverage independent. A source can be production-reachable but unobserved in one test run, or test-related without being executed. +Optimizer retention is independent module evidence. A side-effect or optimizer bailout explains a +retention constraint after reachability; it does not create a product root. Report its state and +reasons alongside, rather than instead of, the reachability classification. + Never infer local-symbol usage. Aggregate execution does not prove code is dead. Rstest, Rslint, coverage, and Rsdoctor observations are independent optional evidence. A build-only or library-only repository can still answer artifact questions; report missing axes as unavailable without requiring full-stack adoption. diff --git a/plugins/rstack/skills/find-unused-code/SKILL.md b/plugins/rstack/skills/find-unused-code/SKILL.md index 2908ee9..402fcb2 100644 --- a/plugins/rstack/skills/find-unused-code/SKILL.md +++ b/plugins/rstack/skills/find-unused-code/SKILL.md @@ -7,13 +7,17 @@ description: Use when listing or prioritizing artifact-scoped Rstack modules tha 1. Call `project_status` and select the matching build context. Deduplicate repeated runs by `contextId`. 2. Obtain the explicit Rsdoctor `dataFile`; offer the matching consent-gated application or library capture if absent. -3. Call `product_roots` with `contextId`, `dataFile`, and `rootLimit: 20`, then report complete counts from `rootSummary` plus representative production, published-contract, and conservative roots and graph issues. Request more roots only when the investigation needs them. +3. Call `product_roots` with `contextId`, `dataFile`, and `rootLimit: 20`, then report complete counts from `rootSummary` plus representative production and published-contract roots and graph issues. Request more roots only when the investigation needs them. 4. Call `unused_candidates` with the same inputs and an optional `limit` from 1 to 100. Prefer project-owned source modules. If `ownership.project` is zero, stop without paging and say the artifact has no project-owned candidate. 5. Follow `nextCursor` only for a requested exhaustive inventory. Reuse unchanged filters. 6. Call `dead_code_explain` for the strongest candidate. Add `code_evidence` when compatible test or execution evidence helps prioritize it. If no relation was captured, inspect imports and prefer a directly imported leaf source over a barrel or entry point. When supported, preflight statically related test files with `rs test list --related --files-only --json` and report the selected test file count. Warn about a broad selection and ask whether to run or narrow it before calling the consent-gated `test_snapshot`; if listing is unsupported, say the preflight is unavailable. Capture one approved source, then reuse its snapshot ID. 7. Keep statically related tests, exact-path test outcomes, and aggregate execution coverage independent. `unrelated` is meaningful only for an isolated one-source relation capture; it is still not deletion proof. 8. Report root exhaustion, state axes, truncation, bounds, provenance, and artifact binding. +Optimizer retention is independent module evidence. A side-effect, CommonJS, or dynamic-import +bailout can explain why reached code was retained, but it is not a product root and does not by +itself disqualify an unreachable candidate. + Call every result an **artifact-scoped unreachable module candidate**. Completely unimported files are outside the artifact graph, and no candidate is deletion proof. When freshness is `partial` with no changed paths, say that the recorded inputs are unchanged but the captured input set is incomplete; do not present it as fresh proof. diff --git a/scripts/test-rstack-context-plugin.mjs b/scripts/test-rstack-context-plugin.mjs index 110d7b4..e9940e9 100644 --- a/scripts/test-rstack-context-plugin.mjs +++ b/scripts/test-rstack-context-plugin.mjs @@ -150,6 +150,10 @@ const testSkills = async () => { assert.match(source, /directly imported leaf source/i); assert.match(source, /broad selection/i); } + assert.doesNotMatch(explainDeadCode, /conservatively preserved/i); + assert.match(explainDeadCode, /optimizer retention.*independent/i); + assert.doesNotMatch(findUnusedCode, /conservative roots/i); + assert.match(findUnusedCode, /optimizer retention.*independent/i); assert.match(debugDevCycle, /standalone `rstest\.config/); assert.match(debugDevCycle, /define\.test\(rstestConfig\)/); assert.match(reviewContextChange, /capture selection/); diff --git a/skills/explain-dead-code/SKILL.md b/skills/explain-dead-code/SKILL.md index 9ddcccd..64a69c1 100644 --- a/skills/explain-dead-code/SKILL.md +++ b/skills/explain-dead-code/SKILL.md @@ -1,6 +1,6 @@ --- name: explain-dead-code -description: Use when explaining why one Rstack artifact module is reachable, conservatively preserved, retained, shipped, or apparently unused. +description: Use when explaining why one Rstack artifact module is reachable, retained, shipped, or apparently unused. --- # Explain an artifact module @@ -9,11 +9,15 @@ description: Use when explaining why one Rstack artifact module is reachable, co 2. Confirm the subject is an artifact module selector. Local symbols and exports require source analysis. 3. Obtain the explicit Rsdoctor `dataFile`; offer a consent-gated application or library capture if it is absent. 4. Call `dead_code_explain` with `contextId`, `dataFile`, and the module selector. -5. Lead with the returned classification: reachable, conservatively preserved, unreachable candidate, or insufficient evidence. +5. Lead with the returned classification: reachable, unreachable candidate, or insufficient evidence. 6. Show one shortest root-to-module path when present. Report production reachability, public contract, shipment, optimizer retention, truncation, bounds, provenance, and `artifactBinding`. 7. When test or runtime evidence helps, call `code_evidence` with the exact checkout-relative path and matching artifact selector. If no relation was captured, inspect imports and prefer a directly imported leaf source over a barrel or entry point. When supported, preflight statically related test files with `rs test list --related --files-only --json` and report the selected test file count. Warn about a broad selection and ask whether to run or narrow it before calling the consent-gated `test_snapshot`; if listing is unsupported, say the preflight is unavailable. Capture one approved source, then query its snapshot ID. 8. Keep statically related tests, exact-path test outcomes, and aggregate execution coverage independent. A source can be production-reachable but unobserved in one test run, or test-related without being executed. +Optimizer retention is independent module evidence. A side-effect or optimizer bailout explains a +retention constraint after reachability; it does not create a product root. Report its state and +reasons alongside, rather than instead of, the reachability classification. + Never infer local-symbol usage. Aggregate execution does not prove code is dead. Rstest, Rslint, coverage, and Rsdoctor observations are independent optional evidence. A build-only or library-only repository can still answer artifact questions; report missing axes as unavailable without requiring full-stack adoption. diff --git a/skills/find-unused-code/SKILL.md b/skills/find-unused-code/SKILL.md index 2908ee9..402fcb2 100644 --- a/skills/find-unused-code/SKILL.md +++ b/skills/find-unused-code/SKILL.md @@ -7,13 +7,17 @@ description: Use when listing or prioritizing artifact-scoped Rstack modules tha 1. Call `project_status` and select the matching build context. Deduplicate repeated runs by `contextId`. 2. Obtain the explicit Rsdoctor `dataFile`; offer the matching consent-gated application or library capture if absent. -3. Call `product_roots` with `contextId`, `dataFile`, and `rootLimit: 20`, then report complete counts from `rootSummary` plus representative production, published-contract, and conservative roots and graph issues. Request more roots only when the investigation needs them. +3. Call `product_roots` with `contextId`, `dataFile`, and `rootLimit: 20`, then report complete counts from `rootSummary` plus representative production and published-contract roots and graph issues. Request more roots only when the investigation needs them. 4. Call `unused_candidates` with the same inputs and an optional `limit` from 1 to 100. Prefer project-owned source modules. If `ownership.project` is zero, stop without paging and say the artifact has no project-owned candidate. 5. Follow `nextCursor` only for a requested exhaustive inventory. Reuse unchanged filters. 6. Call `dead_code_explain` for the strongest candidate. Add `code_evidence` when compatible test or execution evidence helps prioritize it. If no relation was captured, inspect imports and prefer a directly imported leaf source over a barrel or entry point. When supported, preflight statically related test files with `rs test list --related --files-only --json` and report the selected test file count. Warn about a broad selection and ask whether to run or narrow it before calling the consent-gated `test_snapshot`; if listing is unsupported, say the preflight is unavailable. Capture one approved source, then reuse its snapshot ID. 7. Keep statically related tests, exact-path test outcomes, and aggregate execution coverage independent. `unrelated` is meaningful only for an isolated one-source relation capture; it is still not deletion proof. 8. Report root exhaustion, state axes, truncation, bounds, provenance, and artifact binding. +Optimizer retention is independent module evidence. A side-effect, CommonJS, or dynamic-import +bailout can explain why reached code was retained, but it is not a product root and does not by +itself disqualify an unreachable candidate. + Call every result an **artifact-scoped unreachable module candidate**. Completely unimported files are outside the artifact graph, and no candidate is deletion proof. When freshness is `partial` with no changed paths, say that the recorded inputs are unchanged but the captured input set is incomplete; do not present it as fresh proof.