diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 9049483f..b17971c1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -34,6 +34,9 @@ jobs:
- name: Validate i18n keys
run: npm run validate:i18n
+ - name: Validate framework scope
+ run: node scripts/validate-framework-scope.mjs
+
- name: TypeScript type check
run: npm run typecheck
diff --git a/CLAUDE.md b/CLAUDE.md
index 1c7455dd..223911d8 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -114,7 +114,7 @@ HTTP load tests using real OAuth Bearer tokens acquired via ROPC (password grant
- **ClientApp** (`template/SimpleModule.Host/ClientApp/app.tsx`) — Inertia bootstrap. Resolves pages by splitting route name (e.g., `Products/Browse` → imports `/_content/Products/Products.pages.js`).
- **Module pages** — Each module builds its React pages via Vite in library mode → `{ModuleName}.pages.js` in module's `wwwroot/`. Entry point: `Pages/index.ts` exporting a `pages` record mapping route names to components.
-- **Type generation** — `[Dto]` types → source generator embeds TS interfaces → `tools/extract-ts-types.mjs` writes `.ts` files to `ClientApp/types/`.
+- **Type generation** — `[Dto]` types → source generator embeds TS interfaces → `scripts/extract-ts-types.mjs` writes `.ts` files to `ClientApp/types/`.
### Request Flow
diff --git a/Directory.Packages.props b/Directory.Packages.props
index f41945d5..361fc59c 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -67,7 +67,7 @@
-
+
diff --git a/Dockerfile b/Dockerfile
index 9e7d92fb..8b9e21f3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -24,7 +24,7 @@ COPY framework/SimpleModule.Core/*.csproj framework/SimpleModule.Core/
COPY framework/SimpleModule.Database/*.csproj framework/SimpleModule.Database/
COPY framework/SimpleModule.Generator/*.csproj framework/SimpleModule.Generator/
COPY framework/SimpleModule.Hosting/*.csproj framework/SimpleModule.Hosting/
-COPY framework/SimpleModule.DevTools/*.csproj framework/SimpleModule.DevTools/
+COPY tools/SimpleModule.DevTools/*.csproj tools/SimpleModule.DevTools/
COPY framework/SimpleModule.Storage/*.csproj framework/SimpleModule.Storage/
COPY framework/SimpleModule.Storage.Local/*.csproj framework/SimpleModule.Storage.Local/
COPY framework/SimpleModule.Storage.Azure/*.csproj framework/SimpleModule.Storage.Azure/
diff --git a/Dockerfile.worker b/Dockerfile.worker
index 21dbe836..33524add 100644
--- a/Dockerfile.worker
+++ b/Dockerfile.worker
@@ -13,7 +13,7 @@ WORKDIR /src
# Node is needed in the build stage (not at runtime) because the
# ExtractDtoTypeScript / ExtractRoutes targets in SimpleModule.Hosting.targets
-# shell out to `node tools/extract-*.mjs` after CoreCompile. The scripts only
+# shell out to `node scripts/extract-*.mjs` after CoreCompile. The scripts only
# use Node stdlib, so no `npm ci` — just the node binary.
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
@@ -32,7 +32,7 @@ COPY framework/SimpleModule.Core/*.csproj framework/SimpleModule.Core/
COPY framework/SimpleModule.Database/*.csproj framework/SimpleModule.Database/
COPY framework/SimpleModule.Generator/*.csproj framework/SimpleModule.Generator/
COPY framework/SimpleModule.Hosting/*.csproj framework/SimpleModule.Hosting/
-COPY framework/SimpleModule.DevTools/*.csproj framework/SimpleModule.DevTools/
+COPY tools/SimpleModule.DevTools/*.csproj tools/SimpleModule.DevTools/
COPY framework/SimpleModule.Storage/*.csproj framework/SimpleModule.Storage/
COPY framework/SimpleModule.Storage.Local/*.csproj framework/SimpleModule.Storage.Local/
COPY framework/SimpleModule.Storage.Azure/*.csproj framework/SimpleModule.Storage.Azure/
diff --git a/README.md b/README.md
index 7eba63b6..cf3724b4 100644
--- a/README.md
+++ b/README.md
@@ -76,7 +76,7 @@ template/
cli/
SimpleModule.Cli # `sm` CLI tool for scaffolding and validation
tests/ # Framework tests, shared test infrastructure, and e2e tests
-tools/ # Build/dev orchestrators, type extraction, component scaffolding
+scripts/ # Build/dev orchestrators, type extraction, component scaffolding
```
### Request Flow
diff --git a/SimpleModule.slnx b/SimpleModule.slnx
index 50cd7cce..8462e602 100644
--- a/SimpleModule.slnx
+++ b/SimpleModule.slnx
@@ -8,7 +8,6 @@
-
@@ -27,6 +26,9 @@
+
+
+
diff --git a/docs/CONSTITUTION.md b/docs/CONSTITUTION.md
index 854cc7e1..5b788503 100644
--- a/docs/CONSTITUTION.md
+++ b/docs/CONSTITUTION.md
@@ -544,3 +544,40 @@ All SM diagnostics are emitted by the Roslyn source generator at compile time. `
- `AnalysisLevel=latest-all`, `AnalysisMode=All`.
- Suppressed rules live in `.editorconfig`.
- Tests run against both SQLite and PostgreSQL in CI.
+
+---
+
+## 13. Framework Scope
+
+The `framework/` directory contains foundational plumbing: module lifecycle, source generation, DbContext infrastructure, and host bootstrap. Nothing else.
+
+Framework projects are explicitly allowlisted in `framework/.allowed-projects`. The target list contains exactly: `SimpleModule.Core`, `SimpleModule.Database`, `SimpleModule.Generator`, `SimpleModule.Hosting`. During the in-flight migration, the list is temporarily permissive and shrinks as projects migrate.
+
+### Adding a project to `framework/`
+
+Requires:
+
+1. Justification that the project is foundational — referenced by the host bootstrap or by every module, with no domain or provider semantics.
+2. A PR that updates `.allowed-projects`, names the reviewer, and documents why a module or `tools/` project is insufficient.
+
+### `tools/` category
+
+The `tools/` directory holds non-module .NET utilities consumed by the host, the framework bootstrap, or other tools. Rules:
+
+- Flat layout: `tools/SimpleModule.{Name}/{Name}.csproj` — no `src/` subdirectory, no Contracts split.
+- Tools never declare `[Module]`.
+- Modules (anything under `modules/`) never reference a `tools/` project. The host and `framework/SimpleModule.Hosting` may.
+
+### Sub-projects
+
+A sub-project is an additional assembly inside a module, used when a module owns multiple optional providers (e.g., `SimpleModule.Agents.AI.Anthropic`). Rules:
+
+- Lives at `modules/{Name}/src/SimpleModule.{Name}.{Suffix}/`.
+- Name matches `SimpleModule.{Name}.{Suffix}`.
+- Does not declare `[Module]` — only the main module assembly owns lifecycle.
+- May not own a `DbContext`.
+- Follows the same dependency rules as its module (Section 3).
+
+### Enforcement
+
+`scripts/validate-framework-scope.mjs` runs in CI and in `npm run check`. It fails on any violation of the rules above.
diff --git a/docs/site/frontend/overview.md b/docs/site/frontend/overview.md
index e1a3738e..c071bfd8 100644
--- a/docs/site/frontend/overview.md
+++ b/docs/site/frontend/overview.md
@@ -107,7 +107,7 @@ The `@simplemodule/client` package (`packages/SimpleModule.Client/`) provides th
## Type Safety
-The source generator discovers C# types marked with the `[Dto]` attribute and embeds TypeScript interface definitions. The `tools/extract-ts-types.mjs` script extracts these into `.ts` files under `ClientApp/types/`, giving React components full type safety over server-provided props:
+The source generator discovers C# types marked with the `[Dto]` attribute and embeds TypeScript interface definitions. The `scripts/extract-ts-types.mjs` script extracts these into `.ts` files under `ClientApp/types/`, giving React components full type safety over server-provided props:
```tsx
import type { Product } from '../types';
diff --git a/docs/site/frontend/vite.md b/docs/site/frontend/vite.md
index 91316961..ea8ee22b 100644
--- a/docs/site/frontend/vite.md
+++ b/docs/site/frontend/vite.md
@@ -139,7 +139,7 @@ Three scripts are standard:
### `npm run dev`
-The `npm run dev` command starts the complete development environment using the **dev orchestrator** (`tools/dev-orchestrator.mjs`). It launches three types of processes in parallel:
+The `npm run dev` command starts the complete development environment using the **dev orchestrator** (`scripts/dev-orchestrator.mjs`). It launches three types of processes in parallel:
1. **`dotnet run`** -- The ASP.NET backend on `https://localhost:5001`
2. **Module watches** -- `vite build --watch` for every module with a `vite.config.ts`
@@ -197,7 +197,7 @@ npm run build
## Build Orchestrator
-The production build uses the **build orchestrator** (`tools/build-orchestrator.mjs`) which:
+The production build uses the **build orchestrator** (`scripts/build-orchestrator.mjs`) which:
1. Discovers all buildable workspaces (modules + ClientApp)
2. Builds all workspaces **in parallel** for performance
diff --git a/docs/superpowers/plans/2026-04-20-framework-scaffolding-and-devtools.md b/docs/superpowers/plans/2026-04-20-framework-scaffolding-and-devtools.md
new file mode 100644
index 00000000..fc5214da
--- /dev/null
+++ b/docs/superpowers/plans/2026-04-20-framework-scaffolding-and-devtools.md
@@ -0,0 +1,774 @@
+# Framework Scope Minimization — Phase 0 (Scaffolding) + Phase 1 (DevTools) Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Land the enforcement scaffolding (allowlist file, CI validation script, Constitution section) that prevents `framework/` from growing, and prove it works by moving `SimpleModule.DevTools` out of `framework/` into a new `tools/` category.
+
+**Architecture:** A plain-text allowlist file (`framework/.allowed-projects`) names every project permitted under `framework/`. A Node validation script (`scripts/validate-framework-scope.mjs`) fails CI if `framework/` contains anything else, if sub-projects under `modules/` violate the naming pattern, or if `tools/` projects misbehave. Constitution Section 13 documents the invariant. After scaffolding is green, `SimpleModule.DevTools` moves from `framework/` to a new `tools/` directory — the first project to exercise the new category.
+
+**Tech Stack:** Node.js (validation script), MSBuild `.slnx` / `.csproj` (solution file, project references), GitHub Actions (CI), markdown (Constitution).
+
+**Follow-up plans (not in this plan):** Phase 2 (Storage providers → `modules/FileStorage/`), Phase 3 (Agents providers → `modules/Agents/`), Phase 4 (Rag providers → `modules/Rag/`). Each will be its own plan once Phase 1 lands.
+
+**Related spec:** `docs/superpowers/specs/2026-04-20-framework-scope-minimization-design.md`
+
+---
+
+## Task 1: Create the permissive allowlist
+
+The allowlist starts with every current framework project listed. This keeps CI green during migration. Entries are removed as each framework project moves out.
+
+**Files:**
+- Create: `framework/.allowed-projects`
+
+- [ ] **Step 1: Create the allowlist file**
+
+Write the following to `framework/.allowed-projects` (one project name per line, alphabetical for diffability):
+
+```
+SimpleModule.Agents
+SimpleModule.AI.Anthropic
+SimpleModule.AI.AzureOpenAI
+SimpleModule.AI.Ollama
+SimpleModule.AI.OpenAI
+SimpleModule.Core
+SimpleModule.Database
+SimpleModule.DevTools
+SimpleModule.Generator
+SimpleModule.Hosting
+SimpleModule.Rag
+SimpleModule.Rag.StructuredRag
+SimpleModule.Rag.VectorStore.InMemory
+SimpleModule.Rag.VectorStore.Postgres
+SimpleModule.Storage
+SimpleModule.Storage.Azure
+SimpleModule.Storage.Local
+SimpleModule.Storage.S3
+```
+
+This mirrors the output of `ls framework/` today (excluding `Directory.Build.props`).
+
+- [ ] **Step 2: Verify the list matches reality**
+
+Run: `ls framework/ | grep -v Directory.Build.props | sort | diff - framework/.allowed-projects`
+Expected: no output (files match exactly).
+
+---
+
+## Task 2: Write the validation script
+
+A single-file Node script, no dependencies beyond Node stdlib. It performs four checks and exits 1 on any failure. The codebase pattern (see `scripts/validate-i18n.mjs`) is self-contained scripts without separate unit tests — CI exercises the script on the real repo.
+
+**Files:**
+- Create: `scripts/validate-framework-scope.mjs`
+
+- [ ] **Step 1: Create the script with the shebang and framework allowlist check**
+
+```javascript
+#!/usr/bin/env node
+// Validates framework scope rules (see docs/CONSTITUTION.md Section 13).
+// Usage: node scripts/validate-framework-scope.mjs
+//
+// Checks:
+// 1. framework/ only contains projects listed in framework/.allowed-projects
+// 2. Sub-projects under modules/{Name}/src/ match SimpleModule.{Name}.{Suffix}
+// 3. Sub-projects do not declare [Module]
+// 4. tools/ projects are flat-layout SimpleModule.{Name} and never declare [Module]
+// and no module csproj references a tools/ project
+
+import { readdirSync, readFileSync, statSync } from 'fs';
+import { resolve, join, basename } from 'path';
+
+const repoRoot = resolve(new URL('..', import.meta.url).pathname);
+const errors = [];
+
+function readAllowlist() {
+ const path = join(repoRoot, 'framework', '.allowed-projects');
+ return readFileSync(path, 'utf8')
+ .split('\n')
+ .map((line) => line.trim())
+ .filter((line) => line.length > 0 && !line.startsWith('#'));
+}
+
+function checkFrameworkAllowlist() {
+ const allowed = new Set(readAllowlist());
+ const frameworkDir = join(repoRoot, 'framework');
+ const entries = readdirSync(frameworkDir, { withFileTypes: true });
+ for (const entry of entries) {
+ if (!entry.isDirectory()) continue;
+ if (!allowed.has(entry.name)) {
+ errors.push(
+ `framework/${entry.name} is not in framework/.allowed-projects. ` +
+ `Add it with reviewer approval, or move it out of framework/.`,
+ );
+ }
+ }
+}
+
+// Placeholder implementations — filled in by later tasks.
+function checkSubProjectNaming() {}
+function checkSubProjectNoModuleAttribute() {}
+function checkToolsLayering() {}
+
+checkFrameworkAllowlist();
+checkSubProjectNaming();
+checkSubProjectNoModuleAttribute();
+checkToolsLayering();
+
+if (errors.length > 0) {
+ console.error('Framework scope validation failed:\n');
+ for (const err of errors) console.error(` ✗ ${err}`);
+ process.exit(1);
+}
+
+console.log('✓ Framework scope validation passed');
+```
+
+- [ ] **Step 2: Make the script executable and verify it runs**
+
+Run: `chmod +x scripts/validate-framework-scope.mjs && node scripts/validate-framework-scope.mjs`
+Expected: `✓ Framework scope validation passed` (exit 0). The allowlist matches current framework/ state, so the check passes.
+
+- [ ] **Step 3: Verify the check catches violations**
+
+Temporarily create a rogue project directory to confirm the check fires:
+
+```bash
+mkdir -p framework/SimpleModule.RogueProject
+node scripts/validate-framework-scope.mjs
+echo "Exit: $?"
+rmdir framework/SimpleModule.RogueProject
+```
+
+Expected output (exit 1):
+```
+Framework scope validation failed:
+
+ ✗ framework/SimpleModule.RogueProject is not in framework/.allowed-projects. ...
+Exit: 1
+```
+
+Expected after restoration: re-running the script exits 0.
+
+---
+
+## Task 3: Add sub-project naming check
+
+Sub-projects live under `modules/{ModuleName}/src/` and must be named `SimpleModule.{ModuleName}` (main), `SimpleModule.{ModuleName}.Contracts`, or `SimpleModule.{ModuleName}.{Suffix}` (sub-project).
+
+**Files:**
+- Modify: `scripts/validate-framework-scope.mjs`
+
+- [ ] **Step 1: Implement `checkSubProjectNaming`**
+
+Replace the placeholder `function checkSubProjectNaming() {}` with:
+
+```javascript
+function checkSubProjectNaming() {
+ const modulesDir = join(repoRoot, 'modules');
+ if (!exists(modulesDir)) return;
+ const moduleDirs = readdirSync(modulesDir, { withFileTypes: true })
+ .filter((e) => e.isDirectory());
+ for (const moduleEntry of moduleDirs) {
+ const moduleName = moduleEntry.name;
+ const srcDir = join(modulesDir, moduleName, 'src');
+ if (!exists(srcDir)) continue;
+ const projectDirs = readdirSync(srcDir, { withFileTypes: true })
+ .filter((e) => e.isDirectory());
+ const expectedPrefix = `SimpleModule.${moduleName}`;
+ for (const projectEntry of projectDirs) {
+ const name = projectEntry.name;
+ // Must match SimpleModule.{ModuleName} or SimpleModule.{ModuleName}.{Suffix}
+ if (name !== expectedPrefix && !name.startsWith(`${expectedPrefix}.`)) {
+ errors.push(
+ `modules/${moduleName}/src/${name}/ does not match required ` +
+ `pattern '${expectedPrefix}[.*]'. Sub-projects must be named ` +
+ `'${expectedPrefix}.{Suffix}'.`,
+ );
+ }
+ }
+ }
+}
+
+function exists(path) {
+ try { statSync(path); return true; } catch { return false; }
+}
+```
+
+- [ ] **Step 2: Verify the check passes against current repo state**
+
+Run: `node scripts/validate-framework-scope.mjs`
+Expected: `✓ Framework scope validation passed` (exit 0).
+
+If this fails, it means an existing module already violates the pattern — fix the violation in a separate PR before continuing. (Unlikely, since the spec asserts the convention is already followed.)
+
+- [ ] **Step 3: Verify the check catches violations**
+
+```bash
+mkdir -p modules/Products/src/SimpleModule.WrongName
+node scripts/validate-framework-scope.mjs
+echo "Exit: $?"
+rmdir modules/Products/src/SimpleModule.WrongName
+```
+
+Expected (exit 1): `modules/Products/src/SimpleModule.WrongName/ does not match required pattern 'SimpleModule.Products[.*]'...`
+
+---
+
+## Task 4: Add sub-project no-[Module] check
+
+Sub-projects may not declare `[Module]` — only the main module assembly owns lifecycle. A simple grep across `.cs` files is sufficient.
+
+**Files:**
+- Modify: `scripts/validate-framework-scope.mjs`
+
+- [ ] **Step 1: Add a recursive `.cs` file walker helper**
+
+Insert this helper near the other helpers:
+
+```javascript
+function walkCsFiles(dir) {
+ const results = [];
+ const stack = [dir];
+ while (stack.length > 0) {
+ const current = stack.pop();
+ let entries;
+ try {
+ entries = readdirSync(current, { withFileTypes: true });
+ } catch {
+ continue;
+ }
+ for (const entry of entries) {
+ const full = join(current, entry.name);
+ if (entry.isDirectory()) {
+ if (entry.name === 'bin' || entry.name === 'obj' || entry.name === 'node_modules') {
+ continue;
+ }
+ stack.push(full);
+ } else if (entry.isFile() && entry.name.endsWith('.cs')) {
+ results.push(full);
+ }
+ }
+ }
+ return results;
+}
+```
+
+- [ ] **Step 2: Implement `checkSubProjectNoModuleAttribute`**
+
+Replace `function checkSubProjectNoModuleAttribute() {}` with:
+
+```javascript
+function checkSubProjectNoModuleAttribute() {
+ const modulesDir = join(repoRoot, 'modules');
+ if (!exists(modulesDir)) return;
+ const moduleDirs = readdirSync(modulesDir, { withFileTypes: true })
+ .filter((e) => e.isDirectory());
+ for (const moduleEntry of moduleDirs) {
+ const moduleName = moduleEntry.name;
+ const srcDir = join(modulesDir, moduleName, 'src');
+ if (!exists(srcDir)) continue;
+ const projectDirs = readdirSync(srcDir, { withFileTypes: true })
+ .filter((e) => e.isDirectory());
+ for (const projectEntry of projectDirs) {
+ const name = projectEntry.name;
+ const isMain = name === `SimpleModule.${moduleName}`;
+ const isContracts = name === `SimpleModule.${moduleName}.Contracts`;
+ if (isMain || isContracts) continue;
+ // This is a sub-project. Scan its .cs files for [Module(
+ const files = walkCsFiles(join(srcDir, name));
+ for (const file of files) {
+ const content = readFileSync(file, 'utf8');
+ if (/\[\s*Module\s*\(/.test(content)) {
+ errors.push(
+ `Sub-project ${name} declares [Module] in ${file.substring(repoRoot.length + 1)}. ` +
+ `Only the main module assembly (SimpleModule.${moduleName}) may declare [Module].`,
+ );
+ }
+ }
+ }
+ }
+}
+```
+
+- [ ] **Step 3: Verify the check passes**
+
+Run: `node scripts/validate-framework-scope.mjs`
+Expected: `✓ Framework scope validation passed` (no existing sub-projects exist yet, so this check is a no-op against current state).
+
+- [ ] **Step 4: Verify the check catches a violation**
+
+Create a temporary sub-project with a bogus `[Module]`:
+
+```bash
+mkdir -p modules/Products/src/SimpleModule.Products.Fake
+cat > modules/Products/src/SimpleModule.Products.Fake/Bad.cs <<'EOF'
+using SimpleModule.Core;
+[Module("Bad")]
+public class BadSub {}
+EOF
+node scripts/validate-framework-scope.mjs
+echo "Exit: $?"
+rm -rf modules/Products/src/SimpleModule.Products.Fake
+```
+
+Expected (exit 1): `Sub-project SimpleModule.Products.Fake declares [Module] in modules/Products/src/SimpleModule.Products.Fake/Bad.cs. Only the main module assembly (SimpleModule.Products) may declare [Module].`
+
+Expected after cleanup: script passes again.
+
+---
+
+## Task 5: Add tools/ layering check
+
+Tools live at `tools/SimpleModule.{Name}/` (flat). No `.cs` file under `tools/` declares `[Module]`. No `.csproj` under `modules/` references a `tools/` project.
+
+Note: `tools/` may not exist yet at this point in the plan. The check must skip silently when the directory is missing.
+
+**Files:**
+- Modify: `scripts/validate-framework-scope.mjs`
+
+- [ ] **Step 1: Implement `checkToolsLayering`**
+
+Replace `function checkToolsLayering() {}` with:
+
+```javascript
+function checkToolsLayering() {
+ const toolsDir = join(repoRoot, 'tools');
+ if (exists(toolsDir)) {
+ const entries = readdirSync(toolsDir, { withFileTypes: true });
+ for (const entry of entries) {
+ if (!entry.isDirectory()) continue;
+ if (!entry.name.startsWith('SimpleModule.')) {
+ errors.push(
+ `tools/${entry.name}/ does not match required naming 'SimpleModule.{Name}'.`,
+ );
+ continue;
+ }
+ const toolPath = join(toolsDir, entry.name);
+ const files = walkCsFiles(toolPath);
+ for (const file of files) {
+ const content = readFileSync(file, 'utf8');
+ if (/\[\s*Module\s*\(/.test(content)) {
+ errors.push(
+ `tools/${entry.name} declares [Module] in ${file.substring(repoRoot.length + 1)}. ` +
+ `Tools are not modules and must not declare [Module].`,
+ );
+ }
+ }
+ }
+ }
+ // Check no module csproj references a tools/ project.
+ const modulesDir = join(repoRoot, 'modules');
+ if (!exists(modulesDir)) return;
+ const moduleDirs = readdirSync(modulesDir, { withFileTypes: true })
+ .filter((e) => e.isDirectory());
+ for (const moduleEntry of moduleDirs) {
+ const srcDir = join(modulesDir, moduleEntry.name, 'src');
+ if (!exists(srcDir)) continue;
+ const projectDirs = readdirSync(srcDir, { withFileTypes: true })
+ .filter((e) => e.isDirectory());
+ for (const projectEntry of projectDirs) {
+ const csprojPath = join(srcDir, projectEntry.name, `${projectEntry.name}.csproj`);
+ if (!exists(csprojPath)) continue;
+ const content = readFileSync(csprojPath, 'utf8');
+ // Match ProjectReference paths containing tools/ or tools\
+ if (/ProjectReference[^>]*Include="[^"]*[\\/]tools[\\/]/.test(content)) {
+ errors.push(
+ `${csprojPath.substring(repoRoot.length + 1)} references a tools/ project. ` +
+ `Modules may not depend on tools/ — tools are for host/framework only.`,
+ );
+ }
+ }
+ }
+}
+```
+
+- [ ] **Step 2: Verify the check passes**
+
+Run: `node scripts/validate-framework-scope.mjs`
+Expected: `✓ Framework scope validation passed` (tools/ does not exist yet).
+
+- [ ] **Step 3: Commit the script and allowlist**
+
+```bash
+git add framework/.allowed-projects scripts/validate-framework-scope.mjs
+git commit -m "feat: add framework scope validation script and allowlist
+
+Scaffolds the invariant that framework/ contains only foundational
+plumbing. Script enforces four rules:
+- framework/ directory allowlisted in framework/.allowed-projects
+- sub-projects named SimpleModule.{Module}[.{Suffix}]
+- sub-projects do not declare [Module]
+- tools/ flat-layout, no [Module], not referenced from modules/
+
+Allowlist starts permissive (all 19 current framework projects) and
+shrinks as projects migrate to modules/ or tools/."
+```
+
+---
+
+## Task 6: Wire validation into CI and `npm run check`
+
+Two places run the check: GitHub Actions (always on PRs) and the local `npm run check` (developer feedback before push).
+
+**Files:**
+- Modify: `.github/workflows/ci.yml`
+- Modify: `package.json`
+
+- [ ] **Step 1: Add the step to the lint job in ci.yml**
+
+Read `.github/workflows/ci.yml` and find the `lint:` job. Insert a new step after `Validate i18n keys` and before `TypeScript type check`:
+
+```yaml
+ - name: Validate framework scope
+ run: node scripts/validate-framework-scope.mjs
+```
+
+The result (showing context):
+
+```yaml
+ - name: Validate i18n keys
+ run: npm run validate:i18n
+
+ - name: Validate framework scope
+ run: node scripts/validate-framework-scope.mjs
+
+ - name: TypeScript type check
+ run: npm run typecheck
+```
+
+- [ ] **Step 2: Append the check to `npm run check`**
+
+Find this line in `package.json`:
+
+```json
+"check": "biome check . && npm run validate-pages && npm run validate:i18n && npm run typecheck",
+```
+
+Replace with:
+
+```json
+"check": "biome check . && npm run validate-pages && npm run validate:i18n && npm run validate:framework-scope && npm run typecheck",
+```
+
+Add the new script entry to `package.json` scripts block (alphabetically near other `validate:*` entries):
+
+```json
+"validate:framework-scope": "node scripts/validate-framework-scope.mjs",
+```
+
+- [ ] **Step 3: Verify `npm run check` works**
+
+Run: `npm run check`
+Expected: all sub-checks pass, including `✓ Framework scope validation passed`. Exit 0.
+
+If biome reports formatting issues on the newly created `scripts/validate-framework-scope.mjs`, run `npm run check:fix` to fix them.
+
+- [ ] **Step 4: Commit CI wiring**
+
+```bash
+git add .github/workflows/ci.yml package.json
+git commit -m "ci: enforce framework scope validation in CI and npm check
+
+Adds validate-framework-scope to the lint job and to the local
+npm run check pipeline so violations fail fast before PR review."
+```
+
+---
+
+## Task 7: Add Constitution Section 13
+
+Document the invariant in the authoritative rules file. Existing Constitution sections are numbered 1-12; this adds Section 13 at the end.
+
+**Files:**
+- Modify: `docs/CONSTITUTION.md`
+
+- [ ] **Step 1: Read the end of the Constitution to confirm insertion point**
+
+Run: `tail -20 docs/CONSTITUTION.md`
+Expected: Section 12 "Framework Contributor Guidelines" ends the file. Note the last line (should be the end of that section's content).
+
+- [ ] **Step 2: Append Section 13**
+
+Append the following to `docs/CONSTITUTION.md` (keep trailing newline):
+
+```markdown
+
+---
+
+## 13. Framework Scope
+
+The `framework/` directory contains foundational plumbing: module lifecycle, source generation, DbContext infrastructure, and host bootstrap. Nothing else.
+
+Framework projects are explicitly allowlisted in `framework/.allowed-projects`. The target list contains exactly: `SimpleModule.Core`, `SimpleModule.Database`, `SimpleModule.Generator`, `SimpleModule.Hosting`. During the in-flight migration, the list is temporarily permissive and shrinks as projects migrate.
+
+### Adding a project to `framework/`
+
+Requires:
+
+1. Justification that the project is foundational — referenced by the host bootstrap or by every module, with no domain or provider semantics.
+2. A PR that updates `.allowed-projects`, names the reviewer, and documents why a module or `tools/` project is insufficient.
+
+### `tools/` category
+
+The `tools/` directory holds non-module .NET utilities consumed by the host, the framework bootstrap, or other tools. Rules:
+
+- Flat layout: `tools/SimpleModule.{Name}/{Name}.csproj` — no `src/` subdirectory, no Contracts split.
+- Tools never declare `[Module]`.
+- Modules (anything under `modules/`) never reference a `tools/` project. The host and `framework/SimpleModule.Hosting` may.
+
+### Sub-projects
+
+A sub-project is an additional assembly inside a module, used when a module owns multiple optional providers (e.g., `SimpleModule.Agents.AI.Anthropic`). Rules:
+
+- Lives at `modules/{Name}/src/SimpleModule.{Name}.{Suffix}/`.
+- Name matches `SimpleModule.{Name}.{Suffix}`.
+- Does not declare `[Module]` — only the main module assembly owns lifecycle.
+- May not own a `DbContext`.
+- Follows the same dependency rules as its module (Section 3).
+
+### Enforcement
+
+`scripts/validate-framework-scope.mjs` runs in CI and in `npm run check`. It fails on any violation of the rules above.
+```
+
+- [ ] **Step 3: Verify the file is well-formed**
+
+Run: `grep -c "^## " docs/CONSTITUTION.md`
+Expected: `13` (one heading per Constitution section).
+
+- [ ] **Step 4: Commit Constitution update**
+
+```bash
+git add docs/CONSTITUTION.md
+git commit -m "docs: add Constitution Section 13 on Framework Scope
+
+Documents the framework allowlist, tools/ category, sub-project
+convention, and validation mechanism that enforces them."
+```
+
+---
+
+## Task 8: Create `tools/` and move DevTools into it
+
+Moves `framework/SimpleModule.DevTools/` to `tools/SimpleModule.DevTools/` via `git mv` to preserve history. DevTools does not have a Contracts split, so it drops straight into the flat `tools/` layout.
+
+**Files:**
+- Move: `framework/SimpleModule.DevTools/` → `tools/SimpleModule.DevTools/`
+
+- [ ] **Step 1: Verify DevTools' current state**
+
+Run: `ls framework/SimpleModule.DevTools/`
+Expected: `.csproj`, `.cs` files, `README.md`, no Contracts subdirectory.
+
+- [ ] **Step 2: Move the directory with git mv**
+
+Run:
+```bash
+git mv framework/SimpleModule.DevTools tools/SimpleModule.DevTools
+git status --short | head -20
+```
+
+Expected output contains rename entries:
+```
+R framework/SimpleModule.DevTools/DevToolsConstants.cs -> tools/SimpleModule.DevTools/DevToolsConstants.cs
+... (one line per file)
+```
+
+---
+
+## Task 9: Update ProjectReference paths
+
+Two `.csproj` files reference DevTools via relative path:
+- `framework/SimpleModule.Hosting/SimpleModule.Hosting.csproj` — was `..\SimpleModule.DevTools\...`, now needs `..\..\tools\SimpleModule.DevTools\...`
+- `tests/SimpleModule.DevTools.Tests/SimpleModule.DevTools.Tests.csproj` — was `..\..\framework\SimpleModule.DevTools\...`, now needs `..\..\tools\SimpleModule.DevTools\...`
+
+**Files:**
+- Modify: `framework/SimpleModule.Hosting/SimpleModule.Hosting.csproj`
+- Modify: `tests/SimpleModule.DevTools.Tests/SimpleModule.DevTools.Tests.csproj`
+
+- [ ] **Step 1: Update Hosting csproj**
+
+Find the existing line in `framework/SimpleModule.Hosting/SimpleModule.Hosting.csproj`:
+
+```xml
+
+```
+
+Replace with:
+
+```xml
+
+```
+
+- [ ] **Step 2: Update DevTools.Tests csproj**
+
+Find the existing line in `tests/SimpleModule.DevTools.Tests/SimpleModule.DevTools.Tests.csproj`:
+
+```xml
+
+```
+
+Replace with:
+
+```xml
+
+```
+
+---
+
+## Task 10: Update the solution file
+
+`SimpleModule.slnx` lists every project. DevTools is currently under the `/framework/` folder. Move it to a new `/tools/` folder entry.
+
+**Files:**
+- Modify: `SimpleModule.slnx`
+
+- [ ] **Step 1: Inspect the solution file structure**
+
+Run: `grep -n -B1 -A1 "DevTools\|` section containing `DevTools` and any existing `` or similar.
+
+- [ ] **Step 2: Remove the DevTools line from the framework folder**
+
+Find this line inside the `` block:
+
+```xml
+
+```
+
+Delete it.
+
+- [ ] **Step 3: Add DevTools to a `/tools/` folder**
+
+If a `` block does not exist, add one near the `/framework/` block. Example insertion:
+
+```xml
+
+
+
+```
+
+If a `` block already exists (unlikely, but check), add the `` line inside it.
+
+---
+
+## Task 11: Verify the move built cleanly
+
+Before removing the allowlist entry and committing, confirm nothing is broken.
+
+- [ ] **Step 1: Build the affected projects**
+
+Run: `dotnet build framework/SimpleModule.Hosting tools/SimpleModule.DevTools tests/SimpleModule.DevTools.Tests 2>&1 | tail -5`
+
+Expected: `Build succeeded. 0 Error(s)`.
+
+If restore fails because of pre-existing MailKit NU1902 warnings in unrelated projects, add `-p:NoWarn=NU1902` and try again — those errors are out of scope (flagged in the spec).
+
+- [ ] **Step 2: Run DevTools tests**
+
+Run: `dotnet test tests/SimpleModule.DevTools.Tests 2>&1 | tail -8`
+
+Expected: all tests pass.
+
+- [ ] **Step 3: Run the validation script**
+
+Run: `node scripts/validate-framework-scope.mjs`
+
+Expected: **this should still pass**. DevTools is still in `.allowed-projects`, and `framework/SimpleModule.DevTools/` no longer exists so the allowlist permissiveness is fine. The `tools/` layering check sees `tools/SimpleModule.DevTools/` — flat layout, no `[Module]` — and passes.
+
+If the script fails with "tools/SimpleModule.DevTools declares [Module]" — that would be a bug in DevTools (it shouldn't have one). Inspect the offending file and confirm it's a false positive (e.g., a comment or string containing `[Module(`) or a genuine issue.
+
+---
+
+## Task 12: Remove DevTools from the allowlist
+
+Now that DevTools has moved out of `framework/`, the allowlist must no longer include it. This is the edit that makes the script ACTIVELY enforce the new state.
+
+**Files:**
+- Modify: `framework/.allowed-projects`
+
+- [ ] **Step 1: Remove the DevTools line**
+
+Delete the line `SimpleModule.DevTools` from `framework/.allowed-projects`.
+
+Verify with:
+
+Run: `grep -c "^SimpleModule\." framework/.allowed-projects`
+Expected: `17` (was 18, now 17).
+
+- [ ] **Step 2: Run the validation script**
+
+Run: `node scripts/validate-framework-scope.mjs`
+Expected: `✓ Framework scope validation passed` (exit 0). framework/ no longer contains DevTools, and the allowlist no longer lists it. Consistent.
+
+- [ ] **Step 3: Prove the guard now catches regression**
+
+Temporarily restore DevTools to framework/ to confirm the allowlist rejects it:
+
+```bash
+mkdir -p framework/SimpleModule.DevTools
+node scripts/validate-framework-scope.mjs
+echo "Exit: $?"
+rmdir framework/SimpleModule.DevTools
+```
+
+Expected (exit 1): `framework/SimpleModule.DevTools is not in framework/.allowed-projects. ...`
+
+Expected after cleanup: `✓ Framework scope validation passed`.
+
+---
+
+## Task 13: Final commit for Phase 1
+
+- [ ] **Step 1: Run `npm run check`**
+
+Run: `npm run check`
+Expected: all sub-checks pass.
+
+- [ ] **Step 2: Commit the DevTools move**
+
+```bash
+git add -A
+git status --short
+git commit -m "refactor: move DevTools from framework/ to tools/
+
+First application of the tools/ category. DevTools is a dev-time
+utility (Vite dev middleware, live reload, file watchers), not
+foundational plumbing, so it belongs in tools/ rather than framework/.
+
+- git mv framework/SimpleModule.DevTools tools/SimpleModule.DevTools
+- Updated ProjectReference paths in Hosting and DevTools.Tests
+- Updated SimpleModule.slnx (moved under new /tools/ folder)
+- Removed SimpleModule.DevTools from framework/.allowed-projects
+
+Framework is now down to 17 allowlisted projects; phases 2-4 will
+absorb the remaining provider projects into their owning modules."
+```
+
+- [ ] **Step 3: Verify final state**
+
+Run: `git log --oneline -5`
+Expected: the most recent commits are (newest first):
+1. `refactor: move DevTools from framework/ to tools/`
+2. `docs: add Constitution Section 13 on Framework Scope`
+3. `ci: enforce framework scope validation in CI and npm check`
+4. `feat: add framework scope validation script and allowlist`
+
+Run: `ls framework/ | sort`
+Expected: 18 directory entries (was 19, DevTools removed) + `Directory.Build.props`.
+
+Run: `ls tools/`
+Expected: `SimpleModule.DevTools` (the only entry).
+
+---
+
+## Summary
+
+After this plan lands:
+
+- `framework/.allowed-projects` explicitly lists every permitted framework project (now 17).
+- `scripts/validate-framework-scope.mjs` enforces four rules on every CI run and every local `npm run check`.
+- Constitution Section 13 documents the invariant.
+- DevTools has migrated from `framework/` to `tools/`, proving the `tools/` category works end to end.
+- Phases 2, 3, 4 (Storage, Agents, Rag absorptions) can now proceed, each with its own plan, each shrinking the allowlist by 4-5 entries.
diff --git a/docs/superpowers/specs/2026-04-20-framework-scope-minimization-design.md b/docs/superpowers/specs/2026-04-20-framework-scope-minimization-design.md
new file mode 100644
index 00000000..b048d5d2
--- /dev/null
+++ b/docs/superpowers/specs/2026-04-20-framework-scope-minimization-design.md
@@ -0,0 +1,217 @@
+# Framework Scope Minimization — Design
+
+## Goal
+
+Shrink the framework to the foundational plumbing that every module depends on. Everything domain-shaped, provider-shaped, or optional moves into a module or into a new `tools/` category. Prevent regression via an explicit allowlist enforced in CI.
+
+## Target state
+
+```
+framework/ 4 projects (Core, Database, Generator, Hosting)
+tools/ non-module .NET projects (DevTools and future siblings)
+modules/ existing modules + absorbed framework provider projects
+packages/ unchanged (frontend npm packages)
+scripts/ Node build scripts (just renamed from tools/)
+```
+
+The `tools/` rename from the original `tools/` directory to `scripts/` has already landed in commit-pending work.
+
+## What moves where
+
+### Framework allowlist (final state)
+
+Exactly these four projects remain under `framework/`:
+
+- `SimpleModule.Core`
+- `SimpleModule.Database`
+- `SimpleModule.Generator`
+- `SimpleModule.Hosting`
+
+### Migration map
+
+| From | To |
+|---|---|
+| `framework/SimpleModule.Agents` | `modules/Agents/src/SimpleModule.Agents/` (merge into main assembly) |
+| `framework/SimpleModule.AI.Anthropic` | `modules/Agents/src/SimpleModule.Agents.AI.Anthropic/` |
+| `framework/SimpleModule.AI.AzureOpenAI` | `modules/Agents/src/SimpleModule.Agents.AI.AzureOpenAI/` |
+| `framework/SimpleModule.AI.Ollama` | `modules/Agents/src/SimpleModule.Agents.AI.Ollama/` |
+| `framework/SimpleModule.AI.OpenAI` | `modules/Agents/src/SimpleModule.Agents.AI.OpenAI/` |
+| `framework/SimpleModule.Rag` | `modules/Rag/src/SimpleModule.Rag/` (merge into main assembly) |
+| `framework/SimpleModule.Rag.StructuredRag` | `modules/Rag/src/SimpleModule.Rag.StructuredRag/` |
+| `framework/SimpleModule.Rag.VectorStore.InMemory` | `modules/Rag/src/SimpleModule.Rag.VectorStore.InMemory/` |
+| `framework/SimpleModule.Rag.VectorStore.Postgres` | `modules/Rag/src/SimpleModule.Rag.VectorStore.Postgres/` |
+| `framework/SimpleModule.Storage` | `modules/FileStorage/src/SimpleModule.FileStorage.Storage/` (or merged into Contracts — audit during Phase 1) |
+| `framework/SimpleModule.Storage.Azure` | `modules/FileStorage/src/SimpleModule.FileStorage.Azure/` |
+| `framework/SimpleModule.Storage.Local` | `modules/FileStorage/src/SimpleModule.FileStorage.Local/` |
+| `framework/SimpleModule.Storage.S3` | `modules/FileStorage/src/SimpleModule.FileStorage.S3/` |
+| `framework/SimpleModule.DevTools` | `tools/SimpleModule.DevTools/` |
+
+The `FileStorage → Storage` module rename is deferred to a separate follow-up. During this work, the FileStorage module keeps its current name, and the absorbed sub-projects take temporary names like `SimpleModule.FileStorage.Azure`. The follow-up drops `File` everywhere.
+
+## Sub-project convention
+
+A **sub-project** is a `.csproj` under `modules/{Name}/src/` that is not the main module assembly or its Contracts.
+
+Rules:
+
+1. Name matches `SimpleModule.{ModuleName}.{Suffix}` (e.g., `SimpleModule.Agents.AI.Anthropic`).
+2. Lives at `modules/{ModuleName}/src/SimpleModule.{ModuleName}.{Suffix}/`.
+3. Does not declare `[Module]` — only the main assembly owns lifecycle. Sub-projects expose DI via extension methods that the main module calls from `ConfigureServices`.
+4. May contain `IEndpoint` implementations, `[Dto]` types, services, value objects. The source generator picks these up through the main module's transitive reference chain — no generator changes required.
+5. May not own a `DbContext`. The module owns data.
+6. Dependencies: may reference own Contracts, other modules' Contracts, framework Core; may not reference another module's implementation (SM0011 already enforces this).
+
+The generator does not need to know about sub-projects. It discovers `[Module]` classes (sub-projects have none), `IEndpoint` implementations across referenced assemblies (already works), and `[Dto]` types across assemblies (already works). Existing diagnostics SM0052/SM0053 only fire on types annotated with `[Module]` and so do not affect sub-projects.
+
+## `tools/` category
+
+A `.csproj` under `tools/` is a development-time or host-time utility.
+
+Rules:
+
+1. Name matches `SimpleModule.{ToolName}` (e.g., `SimpleModule.DevTools`).
+2. Lives at `tools/SimpleModule.{ToolName}/` — flat layout, no `src/` subdirectory, no Contracts split.
+3. Does not declare `[Module]`.
+4. No endpoints, no `DbContext`, no events.
+5. Referenced by the host or by other tools only — modules never reference `tools/` projects.
+
+Tools are invisible to module discovery because they declare no `[Module]`.
+
+## Enforcement
+
+One CI script, one invariant file, one Constitution section.
+
+### `framework/.allowed-projects`
+
+Plain text, one project name per line. During migration the file starts with all current framework projects listed and shrinks as PRs land. Final content:
+
+```
+SimpleModule.Core
+SimpleModule.Database
+SimpleModule.Generator
+SimpleModule.Hosting
+```
+
+### `scripts/validate-framework-scope.mjs`
+
+Runs four checks, exit 1 on any failure:
+
+1. **Framework allowlist.** Every directory `framework/*/` must match an entry in `.allowed-projects`.
+2. **Sub-project naming.** Every `.csproj` under `modules/{ModuleName}/src/` that is not `SimpleModule.{ModuleName}` or `SimpleModule.{ModuleName}.Contracts` must match `SimpleModule.{ModuleName}.*`.
+3. **Sub-project lifecycle.** No `.cs` file in a sub-project declares `[Module(`.
+4. **Tools layering.** Every directory under `tools/` is `SimpleModule.{Name}/`. No `.cs` file under `tools/` declares `[Module(`. No `.csproj` under `modules/` has a `ProjectReference` to a `tools/` project.
+
+### CI wiring
+
+New step in the existing GitHub Actions workflow, after `dotnet restore`. No build required — file-scan only. Also appended to `npm run check` so local pre-push catches violations.
+
+### Constitution Section 13 (new)
+
+Text to add to `docs/CONSTITUTION.md`:
+
+> ## 13. Framework Scope
+>
+> The `framework/` directory contains foundational plumbing: module lifecycle, source generation, DbContext infrastructure, and host bootstrap. Nothing else.
+>
+> Framework projects are explicitly allowlisted in `framework/.allowed-projects`. This list contains exactly: `SimpleModule.Core`, `SimpleModule.Database`, `SimpleModule.Generator`, `SimpleModule.Hosting`.
+>
+> Adding a project to `framework/` requires:
+>
+> 1. Justification that the project is foundational — referenced by the host bootstrap or by every module, with no domain or provider semantics.
+> 2. A PR that updates `.allowed-projects`, names the reviewer, and documents why a module or `tools/` project is insufficient.
+>
+> The `tools/` directory holds non-module .NET utilities consumed by the host or other tools. Tools never declare `[Module]` and are never referenced from modules.
+>
+> Sub-projects are additional assemblies inside a module. They live at `modules/{Name}/src/SimpleModule.{Name}.{Suffix}/`, do not declare `[Module]`, and may not own a `DbContext`. Section 2 module ownership rules apply; sub-projects inherit them.
+
+### Rejected alternatives
+
+- **MSBuild-level validation** — duplicates the CI check with more error noise during partial builds. Rejected.
+- **New SM diagnostic for allowlist violation** — adds complexity to the generator, which contradicts the shrink-the-framework goal. Rejected.
+- **NuGet-level enforcement** — out of scope; packaging is orthogonal.
+
+## Migration phases
+
+The allowlist starts permissive (all 19 projects listed) and shrinks as PRs land, keeping CI green throughout.
+
+### Phase 0: Scaffolding
+
+- Add `framework/.allowed-projects` listing all 19 current framework projects.
+- Add `scripts/validate-framework-scope.mjs`.
+- Add Constitution Section 13.
+- Wire check into CI and `npm run check`.
+
+Risk: low. Adds files, changes nothing functional.
+
+### Phase 1: DevTools → `tools/`
+
+Warm-up phase, proves the `tools/` category works with least scope.
+
+- `framework/SimpleModule.DevTools/` → `tools/SimpleModule.DevTools/`
+- Update host reference in `template/SimpleModule.Host/SimpleModule.Host.csproj`.
+- Update `SimpleModule.slnx`.
+- Remove `SimpleModule.DevTools` from `.allowed-projects`.
+
+Risk: low. Single project move, no absorption.
+
+### Phase 2: Storage providers → `modules/FileStorage/`
+
+Moves 4 framework projects into the existing FileStorage module.
+
+- `framework/SimpleModule.Storage*` → `modules/FileStorage/src/SimpleModule.FileStorage.{Storage,Azure,Local,S3}/`
+- Audit whether `SimpleModule.Storage` (the abstractions) should merge into existing `SimpleModule.FileStorage.Contracts` or remain as its own sub-project.
+- Resolve pre-existing SM0025 error on FileStorage contract-implementation split.
+- Update `.slnx`, host `ProjectReference`s, any `.targets` paths.
+- Remove 4 entries from `.allowed-projects`.
+
+Risk: moderate. Structural moves touch solution file and host references.
+
+### Phase 3: Agents providers → `modules/Agents/`
+
+Same pattern as Phase 2, 5 projects.
+
+- `framework/SimpleModule.Agents` + `AI.{Anthropic,AzureOpenAI,Ollama,OpenAI}` → `modules/Agents/src/SimpleModule.Agents[.AI.*]/`
+- Resolve pre-existing SM0025 on `IAgentsContracts`.
+- Remove 5 entries from `.allowed-projects`.
+
+Risk: moderate. Five projects, four external SDK dependencies.
+
+### Phase 4: Rag providers → `modules/Rag/`
+
+Same pattern, 4 projects.
+
+- `framework/SimpleModule.Rag*` → `modules/Rag/src/SimpleModule.Rag[.StructuredRag|.VectorStore.*]/`
+- Resolve pre-existing SM0025 on `IRagContracts`.
+- Remove 4 entries from `.allowed-projects`.
+
+Risk: moderate.
+
+### Phase 5 (implicit)
+
+After Phases 1-4 land, `.allowed-projects` contains exactly the four target entries. No separate work — the allowlist is the natural end state.
+
+## Follow-up (not in this project)
+
+**`FileStorage → Storage` module rename.** Separate PR, separate risk profile.
+
+Scope:
+- Rename module directory, projects, namespaces, classes, constants.
+- Change `[Module]` name, `RoutePrefix`, `ViewPrefix`.
+- Update permission constants (SM0034 will enforce prefix).
+- Update React page names and `Pages/index.ts` keys.
+- DB migration: rename Postgres/SQL Server schema (`filestorage` → `storage`) or SQLite table prefix (`FileStorage_` → `Storage_`).
+- Update any cross-module references to `SimpleModule.FileStorage.Contracts`.
+
+Risk: high. User-facing URL changes, permission string changes, irreversible DB migration. Ship alone so revert is clean.
+
+## Cross-phase note
+
+The current build fails on pre-existing SM0025 errors for `IAgentsContracts`, `IRagContracts`, and `IJobExecutionContext`. These are not introduced by the migration — they indicate the framework/module split is already partially broken on `main`. Phases 2-4 must resolve their respective SM0025 as part of absorption; they block per-phase verification.
+
+Unrelated: `MailKit 4.15.1` has a known moderate-severity vulnerability (NU1902) failing the host build under `TreatWarningsAsErrors`. This is pre-existing and outside the scope of this work — flag for a separate dependency-upgrade PR.
+
+## Out of scope
+
+- Splitting `SimpleModule.Core` into smaller framework projects (Orchard-style decomposition). Interesting but orthogonal.
+- Decomposing modules into feature-level toggles (Orchard's `[Feature]` concept). Not requested; the current "one module = one feature" is working.
+- Any change to the source generator's diagnostic set. The generator stays untouched.
diff --git a/framework/.allowed-projects b/framework/.allowed-projects
new file mode 100644
index 00000000..b91f2315
--- /dev/null
+++ b/framework/.allowed-projects
@@ -0,0 +1,17 @@
+SimpleModule.AI.Anthropic
+SimpleModule.AI.AzureOpenAI
+SimpleModule.AI.Ollama
+SimpleModule.AI.OpenAI
+SimpleModule.Agents
+SimpleModule.Core
+SimpleModule.Database
+SimpleModule.Generator
+SimpleModule.Hosting
+SimpleModule.Rag
+SimpleModule.Rag.StructuredRag
+SimpleModule.Rag.VectorStore.InMemory
+SimpleModule.Rag.VectorStore.Postgres
+SimpleModule.Storage
+SimpleModule.Storage.Azure
+SimpleModule.Storage.Local
+SimpleModule.Storage.S3
diff --git a/framework/SimpleModule.Hosting/SimpleModule.Hosting.csproj b/framework/SimpleModule.Hosting/SimpleModule.Hosting.csproj
index 04bf8ad2..b44ca090 100644
--- a/framework/SimpleModule.Hosting/SimpleModule.Hosting.csproj
+++ b/framework/SimpleModule.Hosting/SimpleModule.Hosting.csproj
@@ -11,7 +11,7 @@
-
+
diff --git a/framework/SimpleModule.Hosting/build/SimpleModule.Hosting.targets b/framework/SimpleModule.Hosting/build/SimpleModule.Hosting.targets
index 86bbb11b..8e0f25bc 100644
--- a/framework/SimpleModule.Hosting/build/SimpleModule.Hosting.targets
+++ b/framework/SimpleModule.Hosting/build/SimpleModule.Hosting.targets
@@ -90,12 +90,12 @@
@@ -104,12 +104,12 @@
diff --git a/modules/Email/src/SimpleModule.Email/Providers/SmtpEmailProvider.cs b/modules/Email/src/SimpleModule.Email/Providers/SmtpEmailProvider.cs
index 8713c507..5f889681 100644
--- a/modules/Email/src/SimpleModule.Email/Providers/SmtpEmailProvider.cs
+++ b/modules/Email/src/SimpleModule.Email/Providers/SmtpEmailProvider.cs
@@ -23,7 +23,7 @@ public async Task SendAsync(
using var client = new SmtpClient();
await client.ConnectAsync(smtp.Host, smtp.Port, smtp.UseSsl, cancellationToken);
- if (!string.IsNullOrWhiteSpace(smtp.Username))
+ if (!string.IsNullOrWhiteSpace(smtp.Username) && !string.IsNullOrWhiteSpace(smtp.Password))
{
await client.AuthenticateAsync(smtp.Username, smtp.Password, cancellationToken);
}
diff --git a/package.json b/package.json
index d96f446e..a64d1616 100644
--- a/package.json
+++ b/package.json
@@ -15,22 +15,23 @@
"website"
],
"scripts": {
- "dev": "node tools/dev-orchestrator.mjs",
+ "dev": "node scripts/dev-orchestrator.mjs",
"lint": "biome lint .",
"format": "biome format --write .",
- "check": "biome check . && npm run validate-pages && npm run validate:i18n && npm run typecheck",
- "typecheck": "node tools/typecheck.mjs",
+ "check": "biome check . && npm run validate-pages && npm run validate:i18n && npm run validate:framework-scope && npm run typecheck",
+ "typecheck": "node scripts/typecheck.mjs",
"check:fix": "biome check --write . && npm run validate-pages",
"validate-pages": "node template/SimpleModule.Host/ClientApp/validate-pages.mjs",
- "generate:types": "dotnet build template/SimpleModule.Host && node tools/extract-ts-types.mjs template/SimpleModule.Host/obj/Debug/net10.0/generated/SimpleModule.Generator/SimpleModule.Generator.ModuleDiscovererGenerator modules",
- "generate:routes": "dotnet build template/SimpleModule.Host && node tools/extract-routes.mjs template/SimpleModule.Host/obj/Debug/net10.0/generated/SimpleModule.Generator/SimpleModule.Generator.ModuleDiscovererGenerator template/SimpleModule.Host/ClientApp/routes.ts",
- "generate:i18n-keys": "node tools/generate-i18n-keys.mjs modules",
- "validate:i18n": "node tools/validate-i18n.mjs modules",
- "ui:add": "node tools/add-component.mjs",
+ "generate:types": "dotnet build template/SimpleModule.Host && node scripts/extract-ts-types.mjs template/SimpleModule.Host/obj/Debug/net10.0/generated/SimpleModule.Generator/SimpleModule.Generator.ModuleDiscovererGenerator modules",
+ "generate:routes": "dotnet build template/SimpleModule.Host && node scripts/extract-routes.mjs template/SimpleModule.Host/obj/Debug/net10.0/generated/SimpleModule.Generator/SimpleModule.Generator.ModuleDiscovererGenerator template/SimpleModule.Host/ClientApp/routes.ts",
+ "generate:i18n-keys": "node scripts/generate-i18n-keys.mjs modules",
+ "validate:framework-scope": "node scripts/validate-framework-scope.mjs",
+ "validate:i18n": "node scripts/validate-i18n.mjs modules",
+ "ui:add": "node scripts/add-component.mjs",
"prebuild": "npm run generate:i18n-keys",
- "build": "cross-env VITE_MODE=prod node tools/build-orchestrator.mjs",
+ "build": "cross-env VITE_MODE=prod node scripts/build-orchestrator.mjs",
"prebuild:dev": "npm run generate:i18n-keys",
- "build:dev": "cross-env VITE_MODE=dev node tools/build-orchestrator.mjs",
+ "build:dev": "cross-env VITE_MODE=dev node scripts/build-orchestrator.mjs",
"test:e2e": "npm run test -w tests/e2e",
"test:e2e:ui": "npm run test:ui -w tests/e2e",
"website:dev": "npm run dev -w website",
diff --git a/tools/add-component.mjs b/scripts/add-component.mjs
similarity index 100%
rename from tools/add-component.mjs
rename to scripts/add-component.mjs
diff --git a/tools/build-orchestrator.mjs b/scripts/build-orchestrator.mjs
similarity index 100%
rename from tools/build-orchestrator.mjs
rename to scripts/build-orchestrator.mjs
diff --git a/tools/dev-orchestrator.mjs b/scripts/dev-orchestrator.mjs
similarity index 100%
rename from tools/dev-orchestrator.mjs
rename to scripts/dev-orchestrator.mjs
diff --git a/tools/extract-routes.mjs b/scripts/extract-routes.mjs
similarity index 94%
rename from tools/extract-routes.mjs
rename to scripts/extract-routes.mjs
index 3cc9e0a4..cea3b0c7 100644
--- a/tools/extract-routes.mjs
+++ b/scripts/extract-routes.mjs
@@ -1,6 +1,6 @@
#!/usr/bin/env node
// Extracts TypeScript route definitions from TypeScriptRoutes.g.cs
-// Usage: node tools/extract-routes.mjs
+// Usage: node scripts/extract-routes.mjs
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
import { resolve, dirname, join } from 'path';
diff --git a/tools/extract-ts-types.mjs b/scripts/extract-ts-types.mjs
similarity index 95%
rename from tools/extract-ts-types.mjs
rename to scripts/extract-ts-types.mjs
index 1b761930..fd2c5184 100644
--- a/tools/extract-ts-types.mjs
+++ b/scripts/extract-ts-types.mjs
@@ -1,6 +1,6 @@
#!/usr/bin/env node
// Extracts TypeScript interfaces from per-module DtoTypeScript_*.g.cs files
-// Usage: node tools/extract-ts-types.mjs
+// Usage: node scripts/extract-ts-types.mjs
import { readdirSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
import { resolve, join } from 'path';
diff --git a/tools/extract-view-pages.mjs b/scripts/extract-view-pages.mjs
similarity index 81%
rename from tools/extract-view-pages.mjs
rename to scripts/extract-view-pages.mjs
index 76bd57f8..8514e4cf 100644
--- a/tools/extract-view-pages.mjs
+++ b/scripts/extract-view-pages.mjs
@@ -1,7 +1,7 @@
#!/usr/bin/env node
// Extracts auto-generated Pages/index.ts from ViewPages_*.g.cs files
-// Usage: node tools/extract-view-pages.mjs
-// Example: node tools/extract-view-pages.mjs obj/Debug/.../ViewPages_Products.g.cs src/modules/Products/src/Products/Pages
+// Usage: node scripts/extract-view-pages.mjs
+// Example: node scripts/extract-view-pages.mjs obj/Debug/.../ViewPages_Products.g.cs src/modules/Products/src/Products/Pages
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
import { dirname, resolve } from 'path';
diff --git a/tools/generate-i18n-keys.mjs b/scripts/generate-i18n-keys.mjs
similarity index 98%
rename from tools/generate-i18n-keys.mjs
rename to scripts/generate-i18n-keys.mjs
index 736d8ef2..56803501 100644
--- a/tools/generate-i18n-keys.mjs
+++ b/scripts/generate-i18n-keys.mjs
@@ -1,6 +1,6 @@
#!/usr/bin/env node
// Generates TypeScript key constants from i18n en.json files.
-// Usage: node tools/generate-i18n-keys.mjs [modules-dir]
+// Usage: node scripts/generate-i18n-keys.mjs [modules-dir]
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { join } from 'path';
diff --git a/tools/i18n-utils.mjs b/scripts/i18n-utils.mjs
similarity index 100%
rename from tools/i18n-utils.mjs
rename to scripts/i18n-utils.mjs
diff --git a/tools/orchestrator-utils.mjs b/scripts/orchestrator-utils.mjs
similarity index 100%
rename from tools/orchestrator-utils.mjs
rename to scripts/orchestrator-utils.mjs
diff --git a/tools/typecheck.mjs b/scripts/typecheck.mjs
similarity index 100%
rename from tools/typecheck.mjs
rename to scripts/typecheck.mjs
diff --git a/scripts/validate-framework-scope.mjs b/scripts/validate-framework-scope.mjs
new file mode 100755
index 00000000..f11979c4
--- /dev/null
+++ b/scripts/validate-framework-scope.mjs
@@ -0,0 +1,230 @@
+#!/usr/bin/env node
+// Validates framework scope rules (see docs/CONSTITUTION.md Section 13).
+// Usage: node scripts/validate-framework-scope.mjs
+//
+// Checks:
+// 1. framework/ only contains projects listed in framework/.allowed-projects
+// 2. Sub-projects under modules/{Name}/src/ match SimpleModule.{Name}.{Suffix}
+// 3. Sub-projects do not declare [Module]
+// 4. tools/ projects are flat-layout SimpleModule.{Name} and never declare [Module]
+// and no module csproj references a tools/ project
+
+import { readdirSync, readFileSync, statSync } from 'fs';
+import { resolve, join } from 'path';
+
+const repoRoot = resolve(new URL('..', import.meta.url).pathname);
+
+// Legacy sub-projects that declare [Module] in violation of the rule.
+// These predate the framework scope minimization work and will be resolved
+// by the phase that absorbs each module:
+// - SimpleModule.Agents.Module → Phase 3 (Agents absorption)
+// - SimpleModule.Rag.Module → Phase 4 (Rag absorption)
+// TODO: remove entries here as each phase lands. This set must be empty
+// before the framework migration is declared complete.
+// See: docs/superpowers/specs/2026-04-20-framework-scope-minimization-design.md
+const LEGACY_MODULE_SUBPROJECTS_GRANDFATHERED = new Set([
+ 'SimpleModule.Agents.Module',
+ 'SimpleModule.Rag.Module',
+]);
+
+const errors = [];
+
+function exists(path) {
+ try {
+ statSync(path);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+function walkCsFiles(dir) {
+ const results = [];
+ const stack = [dir];
+ while (stack.length > 0) {
+ const current = stack.pop();
+ let entries;
+ try {
+ entries = readdirSync(current, { withFileTypes: true });
+ } catch {
+ continue;
+ }
+ for (const entry of entries) {
+ const full = join(current, entry.name);
+ if (entry.isDirectory()) {
+ if (
+ entry.name === 'bin' ||
+ entry.name === 'obj' ||
+ entry.name === 'node_modules'
+ ) {
+ continue;
+ }
+ stack.push(full);
+ } else if (entry.isFile() && entry.name.endsWith('.cs')) {
+ results.push(full);
+ }
+ }
+ }
+ return results;
+}
+
+function readAllowlist() {
+ const path = join(repoRoot, 'framework', '.allowed-projects');
+ if (!exists(path)) {
+ errors.push(
+ `framework/.allowed-projects is missing. Create it and list the ` +
+ `projects currently under framework/ (one per line).`,
+ );
+ return [];
+ }
+ return readFileSync(path, 'utf8')
+ .split('\n')
+ .map((line) => line.trim())
+ .filter((line) => line.length > 0 && !line.startsWith('#'));
+}
+
+function checkFrameworkAllowlist() {
+ const allowed = new Set(readAllowlist());
+ const frameworkDir = join(repoRoot, 'framework');
+ const entries = readdirSync(frameworkDir, { withFileTypes: true });
+ for (const entry of entries) {
+ if (!entry.isDirectory()) continue;
+ if (!allowed.has(entry.name)) {
+ errors.push(
+ `framework/${entry.name} is not in framework/.allowed-projects. ` +
+ `Add it with reviewer approval, or move it out of framework/.`,
+ );
+ }
+ }
+}
+
+function checkSubProjectNaming() {
+ const modulesDir = join(repoRoot, 'modules');
+ if (!exists(modulesDir)) return;
+ const moduleDirs = readdirSync(modulesDir, { withFileTypes: true }).filter(
+ (e) => e.isDirectory(),
+ );
+ for (const moduleEntry of moduleDirs) {
+ const moduleName = moduleEntry.name;
+ const srcDir = join(modulesDir, moduleName, 'src');
+ if (!exists(srcDir)) continue;
+ const projectDirs = readdirSync(srcDir, { withFileTypes: true }).filter(
+ (e) => e.isDirectory(),
+ );
+ const expectedPrefix = `SimpleModule.${moduleName}`;
+ for (const projectEntry of projectDirs) {
+ const name = projectEntry.name;
+ // Must match SimpleModule.{ModuleName} or SimpleModule.{ModuleName}.{Suffix}
+ if (name !== expectedPrefix && !name.startsWith(`${expectedPrefix}.`)) {
+ errors.push(
+ `modules/${moduleName}/src/${name}/ does not match required ` +
+ `pattern '${expectedPrefix}[.*]'. Sub-projects must be named ` +
+ `'${expectedPrefix}.{Suffix}'.`,
+ );
+ }
+ }
+ }
+}
+
+function checkSubProjectNoModuleAttribute() {
+ const modulesDir = join(repoRoot, 'modules');
+ if (!exists(modulesDir)) return;
+ const moduleDirs = readdirSync(modulesDir, { withFileTypes: true }).filter(
+ (e) => e.isDirectory(),
+ );
+ for (const moduleEntry of moduleDirs) {
+ const moduleName = moduleEntry.name;
+ const srcDir = join(modulesDir, moduleName, 'src');
+ if (!exists(srcDir)) continue;
+ const projectDirs = readdirSync(srcDir, { withFileTypes: true }).filter(
+ (e) => e.isDirectory(),
+ );
+ for (const projectEntry of projectDirs) {
+ const name = projectEntry.name;
+ const isMain = name === `SimpleModule.${moduleName}`;
+ const isContracts = name === `SimpleModule.${moduleName}.Contracts`;
+ if (isMain || isContracts) continue;
+ if (LEGACY_MODULE_SUBPROJECTS_GRANDFATHERED.has(name)) continue;
+ // This is a sub-project. Scan its .cs files for [Module(
+ const files = walkCsFiles(join(srcDir, name));
+ for (const file of files) {
+ const content = readFileSync(file, 'utf8');
+ if (/\[\s*Module\s*\(/.test(content)) {
+ errors.push(
+ `Sub-project ${name} declares [Module] in ${file.substring(repoRoot.length + 1)}. ` +
+ `Only the main module assembly (SimpleModule.${moduleName}) may declare [Module].`,
+ );
+ }
+ }
+ }
+ }
+}
+
+function checkToolsLayering() {
+ const toolsDir = join(repoRoot, 'tools');
+ if (exists(toolsDir)) {
+ const entries = readdirSync(toolsDir, { withFileTypes: true });
+ for (const entry of entries) {
+ if (!entry.isDirectory()) continue;
+ if (!entry.name.startsWith('SimpleModule.')) {
+ errors.push(
+ `tools/${entry.name}/ does not match required naming 'SimpleModule.{Name}'.`,
+ );
+ continue;
+ }
+ const toolPath = join(toolsDir, entry.name);
+ const files = walkCsFiles(toolPath);
+ for (const file of files) {
+ const content = readFileSync(file, 'utf8');
+ if (/\[\s*Module\s*\(/.test(content)) {
+ errors.push(
+ `tools/${entry.name} declares [Module] in ${file.substring(repoRoot.length + 1)}. ` +
+ `Tools are not modules and must not declare [Module].`,
+ );
+ }
+ }
+ }
+ }
+ // Check no module csproj references a tools/ project.
+ const modulesDir = join(repoRoot, 'modules');
+ if (!exists(modulesDir)) return;
+ const moduleDirs = readdirSync(modulesDir, { withFileTypes: true }).filter(
+ (e) => e.isDirectory(),
+ );
+ for (const moduleEntry of moduleDirs) {
+ const srcDir = join(modulesDir, moduleEntry.name, 'src');
+ if (!exists(srcDir)) continue;
+ const projectDirs = readdirSync(srcDir, { withFileTypes: true }).filter(
+ (e) => e.isDirectory(),
+ );
+ for (const projectEntry of projectDirs) {
+ const csprojPath = join(
+ srcDir,
+ projectEntry.name,
+ `${projectEntry.name}.csproj`,
+ );
+ if (!exists(csprojPath)) continue;
+ const content = readFileSync(csprojPath, 'utf8');
+ // Match ProjectReference paths containing tools/ or tools\
+ if (/ProjectReference[^>]*Include="[^"]*[\\/]tools[\\/]/.test(content)) {
+ errors.push(
+ `${csprojPath.substring(repoRoot.length + 1)} references a tools/ project. ` +
+ `Modules may not depend on tools/ — tools are for host/framework only.`,
+ );
+ }
+ }
+ }
+}
+
+checkFrameworkAllowlist();
+checkSubProjectNaming();
+checkSubProjectNoModuleAttribute();
+checkToolsLayering();
+
+if (errors.length > 0) {
+ console.error('Framework scope validation failed:\n');
+ for (const err of errors) console.error(` ✗ ${err}`);
+ process.exit(1);
+}
+
+console.log('✓ Framework scope validation passed');
diff --git a/tools/validate-i18n.mjs b/scripts/validate-i18n.mjs
similarity index 98%
rename from tools/validate-i18n.mjs
rename to scripts/validate-i18n.mjs
index 47e1917b..54196cee 100644
--- a/tools/validate-i18n.mjs
+++ b/scripts/validate-i18n.mjs
@@ -1,6 +1,6 @@
#!/usr/bin/env node
// Validates i18n locale files across modules.
-// Usage: node tools/validate-i18n.mjs [modules-dir]
+// Usage: node scripts/validate-i18n.mjs [modules-dir]
//
// Checks:
// 1. Every module with Locales/ has an en.json (base locale)
diff --git a/tests/SimpleModule.DevTools.Tests/SimpleModule.DevTools.Tests.csproj b/tests/SimpleModule.DevTools.Tests/SimpleModule.DevTools.Tests.csproj
index 88436caf..911e3536 100644
--- a/tests/SimpleModule.DevTools.Tests/SimpleModule.DevTools.Tests.csproj
+++ b/tests/SimpleModule.DevTools.Tests/SimpleModule.DevTools.Tests.csproj
@@ -14,6 +14,6 @@
-
+
diff --git a/framework/SimpleModule.DevTools/DevToolsConstants.cs b/tools/SimpleModule.DevTools/DevToolsConstants.cs
similarity index 100%
rename from framework/SimpleModule.DevTools/DevToolsConstants.cs
rename to tools/SimpleModule.DevTools/DevToolsConstants.cs
diff --git a/framework/SimpleModule.DevTools/DevToolsExtensions.cs b/tools/SimpleModule.DevTools/DevToolsExtensions.cs
similarity index 100%
rename from framework/SimpleModule.DevTools/DevToolsExtensions.cs
rename to tools/SimpleModule.DevTools/DevToolsExtensions.cs
diff --git a/framework/SimpleModule.DevTools/LiveReloadServer.cs b/tools/SimpleModule.DevTools/LiveReloadServer.cs
similarity index 100%
rename from framework/SimpleModule.DevTools/LiveReloadServer.cs
rename to tools/SimpleModule.DevTools/LiveReloadServer.cs
diff --git a/framework/SimpleModule.DevTools/README.md b/tools/SimpleModule.DevTools/README.md
similarity index 100%
rename from framework/SimpleModule.DevTools/README.md
rename to tools/SimpleModule.DevTools/README.md
diff --git a/framework/SimpleModule.DevTools/SimpleModule.DevTools.csproj b/tools/SimpleModule.DevTools/SimpleModule.DevTools.csproj
similarity index 100%
rename from framework/SimpleModule.DevTools/SimpleModule.DevTools.csproj
rename to tools/SimpleModule.DevTools/SimpleModule.DevTools.csproj
diff --git a/framework/SimpleModule.DevTools/ViteDevMiddleware.cs b/tools/SimpleModule.DevTools/ViteDevMiddleware.cs
similarity index 100%
rename from framework/SimpleModule.DevTools/ViteDevMiddleware.cs
rename to tools/SimpleModule.DevTools/ViteDevMiddleware.cs
diff --git a/framework/SimpleModule.DevTools/ViteDevWatchService.Helpers.cs b/tools/SimpleModule.DevTools/ViteDevWatchService.Helpers.cs
similarity index 100%
rename from framework/SimpleModule.DevTools/ViteDevWatchService.Helpers.cs
rename to tools/SimpleModule.DevTools/ViteDevWatchService.Helpers.cs
diff --git a/framework/SimpleModule.DevTools/ViteDevWatchService.Logging.cs b/tools/SimpleModule.DevTools/ViteDevWatchService.Logging.cs
similarity index 100%
rename from framework/SimpleModule.DevTools/ViteDevWatchService.Logging.cs
rename to tools/SimpleModule.DevTools/ViteDevWatchService.Logging.cs
diff --git a/framework/SimpleModule.DevTools/ViteDevWatchService.Process.cs b/tools/SimpleModule.DevTools/ViteDevWatchService.Process.cs
similarity index 100%
rename from framework/SimpleModule.DevTools/ViteDevWatchService.Process.cs
rename to tools/SimpleModule.DevTools/ViteDevWatchService.Process.cs
diff --git a/framework/SimpleModule.DevTools/ViteDevWatchService.cs b/tools/SimpleModule.DevTools/ViteDevWatchService.cs
similarity index 100%
rename from framework/SimpleModule.DevTools/ViteDevWatchService.cs
rename to tools/SimpleModule.DevTools/ViteDevWatchService.cs