From d82bd0d72ea3d541b31d3a6a5094fb7349d9d3b5 Mon Sep 17 00:00:00 2001 From: Anto Subash Date: Mon, 20 Apr 2026 13:08:26 +0200 Subject: [PATCH 01/10] docs: add framework scope minimization design Design for shrinking framework/ to four projects (Core, Database, Generator, Hosting) and moving provider packages into their owning modules. Defines sub-project convention, new tools/ category for non-module utilities, and CI-enforced allowlist to prevent regression. --- ...-20-framework-scope-minimization-design.md | 217 ++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-20-framework-scope-minimization-design.md 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. From 3ac4c06a96ebc64a3055e41054c8495e2f45f094 Mon Sep 17 00:00:00 2001 From: Anto Subash Date: Mon, 20 Apr 2026 13:13:25 +0200 Subject: [PATCH 02/10] chore: rename tools/ to scripts/ Frees the tools/ directory for non-module .NET projects (per the framework scope minimization design). The existing Node build scripts are genuinely scripts, not tools. - 11 .mjs files moved via git mv (history preserved) - package.json npm scripts updated (9 entries) - SimpleModule.Hosting.targets MSBuild paths updated - CLAUDE.md, README.md, Dockerfile.worker, and frontend docs updated - Script header comments updated --- CLAUDE.md | 2 +- Dockerfile.worker | 2 +- README.md | 2 +- docs/site/frontend/overview.md | 2 +- docs/site/frontend/vite.md | 4 ++-- .../build/SimpleModule.Hosting.targets | 8 ++++---- package.json | 18 +++++++++--------- {tools => scripts}/add-component.mjs | 0 {tools => scripts}/build-orchestrator.mjs | 0 {tools => scripts}/dev-orchestrator.mjs | 0 {tools => scripts}/extract-routes.mjs | 2 +- {tools => scripts}/extract-ts-types.mjs | 2 +- {tools => scripts}/extract-view-pages.mjs | 4 ++-- {tools => scripts}/generate-i18n-keys.mjs | 2 +- {tools => scripts}/i18n-utils.mjs | 0 {tools => scripts}/orchestrator-utils.mjs | 0 {tools => scripts}/typecheck.mjs | 0 {tools => scripts}/validate-i18n.mjs | 2 +- 18 files changed, 25 insertions(+), 25 deletions(-) rename {tools => scripts}/add-component.mjs (100%) rename {tools => scripts}/build-orchestrator.mjs (100%) rename {tools => scripts}/dev-orchestrator.mjs (100%) rename {tools => scripts}/extract-routes.mjs (94%) rename {tools => scripts}/extract-ts-types.mjs (95%) rename {tools => scripts}/extract-view-pages.mjs (81%) rename {tools => scripts}/generate-i18n-keys.mjs (98%) rename {tools => scripts}/i18n-utils.mjs (100%) rename {tools => scripts}/orchestrator-utils.mjs (100%) rename {tools => scripts}/typecheck.mjs (100%) rename {tools => scripts}/validate-i18n.mjs (98%) 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/Dockerfile.worker b/Dockerfile.worker index 21dbe836..edfdac52 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 \ 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/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/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/package.json b/package.json index d96f446e..91d5e0e6 100644 --- a/package.json +++ b/package.json @@ -15,22 +15,22 @@ "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", + "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: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/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) From 94b5067c9580b0033a4073dbc74d315e40bd3b66 Mon Sep 17 00:00:00 2001 From: Anto Subash Date: Mon, 20 Apr 2026 13:18:42 +0200 Subject: [PATCH 03/10] docs: add implementation plan for framework scaffolding and DevTools move Covers Phase 0 (allowlist file, validation script, CI wiring, Constitution Section 13) and Phase 1 (DevTools -> tools/). Phases 2-4 (Storage, Agents, Rag absorptions) will get their own plans once this lands. --- ...4-20-framework-scaffolding-and-devtools.md | 774 ++++++++++++++++++ 1 file changed, 774 insertions(+) create mode 100644 docs/superpowers/plans/2026-04-20-framework-scaffolding-and-devtools.md 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. From 8a0824861ff4acf1c7bae0754c45b21ab4877a6b Mon Sep 17 00:00:00 2001 From: Anto Subash Date: Mon, 20 Apr 2026 13:47:47 +0200 Subject: [PATCH 04/10] 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 18 current framework projects) and shrinks as projects migrate to modules/ or tools/. Grandfathers two known legacy sub-projects (SimpleModule.Agents.Module, SimpleModule.Rag.Module) that predate this work. They will be removed from the grandfather set by phases 3 and 4 of the migration. --- framework/.allowed-projects | 18 +++ scripts/validate-framework-scope.mjs | 223 +++++++++++++++++++++++++++ 2 files changed, 241 insertions(+) create mode 100644 framework/.allowed-projects create mode 100755 scripts/validate-framework-scope.mjs diff --git a/framework/.allowed-projects b/framework/.allowed-projects new file mode 100644 index 00000000..a33e2c66 --- /dev/null +++ b/framework/.allowed-projects @@ -0,0 +1,18 @@ +SimpleModule.AI.Anthropic +SimpleModule.AI.AzureOpenAI +SimpleModule.AI.Ollama +SimpleModule.AI.OpenAI +SimpleModule.Agents +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 diff --git a/scripts/validate-framework-scope.mjs b/scripts/validate-framework-scope.mjs new file mode 100755 index 00000000..2a12db49 --- /dev/null +++ b/scripts/validate-framework-scope.mjs @@ -0,0 +1,223 @@ +#!/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'); + 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'); From 5daac8a5cc8aabceebd34ca22810050bc41c2031 Mon Sep 17 00:00:00 2001 From: Anto Subash Date: Mon, 20 Apr 2026 13:53:40 +0200 Subject: [PATCH 05/10] fix: gracefully handle missing .allowed-projects Previously, a missing framework/.allowed-projects would die with a Node ENOENT stack trace. Now it reports a clean script-level error pointing to the fix. Matters for CI, where the stack trace would be the first thing a reviewer sees. Raised in code review of 8a082486. --- scripts/validate-framework-scope.mjs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/scripts/validate-framework-scope.mjs b/scripts/validate-framework-scope.mjs index 2a12db49..f11979c4 100755 --- a/scripts/validate-framework-scope.mjs +++ b/scripts/validate-framework-scope.mjs @@ -70,6 +70,13 @@ function walkCsFiles(dir) { 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()) From d1b4285185d67f0b8d7e2b9790ef18feb086ab60 Mon Sep 17 00:00:00 2001 From: Anto Subash Date: Mon, 20 Apr 2026 13:55:53 +0200 Subject: [PATCH 06/10] 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. --- .github/workflows/ci.yml | 3 +++ package.json | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) 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/package.json b/package.json index 91d5e0e6..a64d1616 100644 --- a/package.json +++ b/package.json @@ -18,13 +18,14 @@ "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", + "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 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", From 1ed94c765499f0e8facb36826b169b37f13cd317 Mon Sep 17 00:00:00 2001 From: Anto Subash Date: Mon, 20 Apr 2026 13:56:35 +0200 Subject: [PATCH 07/10] docs: add Constitution Section 13 on Framework Scope Documents the framework allowlist, tools/ category, sub-project convention, and validation mechanism that enforces them. --- docs/CONSTITUTION.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) 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. From 666984ca42fd87727a3b8cd0db2b932911383ad3 Mon Sep 17 00:00:00 2001 From: Anto Subash Date: Mon, 20 Apr 2026 14:04:22 +0200 Subject: [PATCH 08/10] 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. --- SimpleModule.slnx | 4 +++- framework/.allowed-projects | 1 - framework/SimpleModule.Hosting/SimpleModule.Hosting.csproj | 2 +- .../SimpleModule.DevTools.Tests.csproj | 2 +- .../SimpleModule.DevTools/DevToolsConstants.cs | 0 .../SimpleModule.DevTools/DevToolsExtensions.cs | 0 .../SimpleModule.DevTools/LiveReloadServer.cs | 0 {framework => tools}/SimpleModule.DevTools/README.md | 0 .../SimpleModule.DevTools/SimpleModule.DevTools.csproj | 0 .../SimpleModule.DevTools/ViteDevMiddleware.cs | 0 .../SimpleModule.DevTools/ViteDevWatchService.Helpers.cs | 0 .../SimpleModule.DevTools/ViteDevWatchService.Logging.cs | 0 .../SimpleModule.DevTools/ViteDevWatchService.Process.cs | 0 .../SimpleModule.DevTools/ViteDevWatchService.cs | 0 14 files changed, 5 insertions(+), 4 deletions(-) rename {framework => tools}/SimpleModule.DevTools/DevToolsConstants.cs (100%) rename {framework => tools}/SimpleModule.DevTools/DevToolsExtensions.cs (100%) rename {framework => tools}/SimpleModule.DevTools/LiveReloadServer.cs (100%) rename {framework => tools}/SimpleModule.DevTools/README.md (100%) rename {framework => tools}/SimpleModule.DevTools/SimpleModule.DevTools.csproj (100%) rename {framework => tools}/SimpleModule.DevTools/ViteDevMiddleware.cs (100%) rename {framework => tools}/SimpleModule.DevTools/ViteDevWatchService.Helpers.cs (100%) rename {framework => tools}/SimpleModule.DevTools/ViteDevWatchService.Logging.cs (100%) rename {framework => tools}/SimpleModule.DevTools/ViteDevWatchService.Process.cs (100%) rename {framework => tools}/SimpleModule.DevTools/ViteDevWatchService.cs (100%) 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/framework/.allowed-projects b/framework/.allowed-projects index a33e2c66..b91f2315 100644 --- a/framework/.allowed-projects +++ b/framework/.allowed-projects @@ -5,7 +5,6 @@ SimpleModule.AI.OpenAI SimpleModule.Agents SimpleModule.Core SimpleModule.Database -SimpleModule.DevTools SimpleModule.Generator SimpleModule.Hosting SimpleModule.Rag 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/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 From 659d70cfc985db3738ddd8f8dad70e89d00638a4 Mon Sep 17 00:00:00 2001 From: Anto Subash Date: Mon, 20 Apr 2026 14:10:29 +0200 Subject: [PATCH 09/10] fix: update Dockerfile paths for DevTools move The two Dockerfiles still COPY'd from framework/SimpleModule.DevTools after the move to tools/. Restore-stage fails because the source glob matches nothing AND because SimpleModule.Hosting's ProjectReference to ..\..\tools\SimpleModule.DevTools can't resolve inside the Docker build context. Caught in code review of 666984ca. --- Dockerfile | 2 +- Dockerfile.worker | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 edfdac52..33524add 100644 --- a/Dockerfile.worker +++ b/Dockerfile.worker @@ -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/ From 7f717a2f90bd0026e2d81d52d22ad10a012be085 Mon Sep 17 00:00:00 2001 From: Anto Subash Date: Mon, 20 Apr 2026 15:38:50 +0200 Subject: [PATCH 10/10] fix: upgrade MailKit to 4.16.0 to resolve NU1902 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MailKit 4.15.1 has a known moderate severity vulnerability (GHSA-9j88-vvj5-vhgr — STARTTLS Response Injection via unflushed stream buffer that enables SASL mechanism downgrade). Fixed in 4.16.0. MailKit 4.16.0 tightens nullability on MailService.AuthenticateAsync, so the SMTP provider now also checks smtp.Password is non-empty before authenticating (previously the method was called with a potentially null password when only Username was configured). --- Directory.Packages.props | 2 +- .../Email/src/SimpleModule.Email/Providers/SmtpEmailProvider.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/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); }