Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
"rules": {
"no-console": "off",
"no-void": "off",
"no-implicit-globals": "off",
"import/no-commonjs": "off",
"import/no-nodejs-modules": "off",
"import/unambiguous": "off",
Expand Down
22 changes: 9 additions & 13 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
# CLAUDE.md

When I ask a question or make an observation, respond with an answer - do NOT jump to making code changes unless I explicitly ask for them.
When I ask a question or make an observation, respond with an answer - do NOT jump to making code changes unless I explicitly ask for them.

## Environment & Commands

**Windows-only:** Use `dir`, `type`, backslashes. **Line endings must be CRLF (`\r\n`)** - never use bare LF.
**Line endings must be CRLF (`\r\n`)** - never use bare LF.

- Run via npm scripts, never direct .NET/dotnet commands (avoids lock-file conflicts): Dev `npm run dev` | Test `npm run test` (`test:backend`, `test:frontend`) | Lint `npm run lint <path>` | Build `npm run verify:build`
- Run via npm scripts, never direct .NET/dotnet commands (avoids lock-file conflicts): Dev `npm run dev` | Test `npm run test` (`test:backend`, `test:frontend`; single test: `npm run test:backend -- <TestClassName>` / `npm run test:frontend -- <file-pattern>`) | Lint `npm run lint <path>` | Build `npm run verify:build`
- **Stale cache**: Add `-- --clear-cache` to `verify:build` or `lint` if builds fail with cached errors

## Architecture

- **Backend**: ASP.NET 10, `web/Areas/{AreaName}/` | **Frontend**: Vue 3 multi-SPA, Vite → `wwwroot/vue/`
- **DB**: SQL Server 2016 + EF Core (CTS, RAPS, AAUD schemas) | **Auth**: CAS, `[Permission(Allow = "SVMSecure.{Area}.{Action}")]`
- **DB**: SQL Server 2016 + EF Core (CTS, RAPS, AAUD schemas) | **Auth**: CAS + `[Permission]` (see API & Cross-Environment)
- **Identity:** `AaudUser.AaudUserId` = `Person.PersonId`. If mismatched, TEST DB needs refresh.
- **Design system (UI)**: Colors, typography, components (buttons & button colors, dialogs, badges, banners, inputs, selects, nav), the `<main>` landmark rule, WCAG-AA contrast rules, and Do's/Don'ts all live in [DESIGN.md](DESIGN.md) — read it before building or changing UI. Always use Quasar components.
- **Design system (UI)**: All UI rules (colors, typography, components, `<main>` landmark, WCAG-AA contrast) live in [DESIGN.md](DESIGN.md) — read it before building or changing UI. Always use Quasar components.
- **VueUse**: Prefer VueUse composables over hand-rolled reactive logic.
- **O(n²) lookups**: Never nest `.find()`/`.filter()`/`.some()` inside `.map()`/`.forEach()`/`for` when both arrays can grow. Pre-build a `Map`/`Set` once, then `.get()`/`.has()` in the loop. Small fixed reference lists (terms, roles, ~10 items) are fine.
- **O(n²) lookups (JS & C#)**: Never nest per-item searches (`.find()`/`.some()`/`.FirstOrDefault()`/`.Any()`) inside a loop over another growable list. Pre-build a `Map`/`Set`/`Dictionary` once, then look up in the loop. Small fixed reference lists (~10 items) are fine. In EF this is N+1 — see Correlated subqueries.
- **Plurals**: Use `inflect("word", count)` from the `inflection` package — never hand-roll ternaries for noun pluralization

## Database & EF Core
Expand All @@ -34,22 +34,18 @@ When I ask a question or make an observation, respond with an answer - do NOT j
- **Frontend API calls**: Service layer + `useFetch()`, never raw `fetch()` (must unwrap `{ result, success }`)
- **API URL**: `${import.meta.env.VITE_API_URL}` — never hardcode `/api/` (TEST uses `/2/` prefix)
- **Subpath PathBase (`/2`)**: TEST/PROD run VIPER 2 under a `/2` PathBase (IIS sub-app), legacy VIPER 1 at `/`; with no base locally, these bugs surface only on TEST/PROD (not in unit tests). Use `~/` for app-root redirects, never bare `/` (escapes to the legacy site). Guards matching root-relative paths (`/api`, `/welcome`) must strip the base off the base-prefixed `ReturnUrl` (`/2/...`) or use `Request.PathBase`. `RedirectToAction`, `@Url.Content("~/")`, and tag-helpers include the base; raw string paths (`Redirect("/x")`, `returnUrl.StartsWith("/api")`) don't.
- **Auth**: `[Permission(Allow = "SVMSecure.{Area}")]` — authenticate before validating params
- **Auth**: `[Permission(Allow = "SVMSecure.{Area}")]`, or finer `"SVMSecure.{Area}.{Permission}"` — authenticate before validating params

## C# Standards

- **Exceptions**: Catch specific types (`DbUpdateException`, `SqlException`, `InvalidOperationException`). Never generic `catch (Exception ex)`.
- **Paths**: `Path.Join()` not `Path.Combine()` | **DateTime**: prefer `DateTimeKind.Local`
- **LINQ**: `.Where()` for filtering (not `if` inside foreach), `.Select()` for mapping (not foreach + Add)
- **O(n²) lookups**: Don't nest `.FirstOrDefault()`/`.Single()`/`.Any()` inside a `foreach`/`.Select()` over another list. Materialize once with `.ToDictionary()` or `.ToLookup()`, then `TryGetValue` in the loop. For EF, this is N+1 — see Database & EF Core "Correlated subqueries".
- **Mapperly**: Prefer over manual property mapping. Static partial mapper class per area with `[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.None)]`. Use `[MapperIgnoreTarget]` for computed properties, manual wrappers for transforms. Align entity/DTO names — use EF `HasColumnName()` to decouple from DB columns.
- **Scrutor**: Convention-based DI auto-registers `*Service`/`*Validator` from configured namespaces — prefer over manual `AddScoped`. Follow `IFooService`/`FooService` naming. Explicit `AddScoped` before Scrutor takes precedence (`RegistrationStrategy.Skip`).
- **Comments**: Sparingly, why-not-what, complex logic only.
- **Bug fixes**: Verify ALL code paths using affected logic. Check for duplicate/parallel implementations and fix consistently or DRY into shared method.

## Security

- **Log Injection**: Sanitize user input before logging via `LogSanitizer` (`SanitizeId()`, `SanitizeString()`, `SanitizeYear()`). Skip hard-coded strings, enums, DB values.
- **Log injection**: Sanitize user input before logging via `LogSanitizer` (`SanitizeId()`, `SanitizeString()`, `SanitizeYear()`). Skip hard-coded strings, enums, DB values.

## Testing & Git

Expand All @@ -60,4 +56,4 @@ When I ask a question or make an observation, respond with an answer - do NOT j
- **Never branch off `Development`**: it is a merge/deploy target, never a base. A branch being "behind `Development`" is expected and not a concern (you never sync or rebase from it). Its history is messy by design and never rewritten.
- **Squash during review**: If a branch is still unmerged and worked by a single developer, squash code-review fixes into the relevant existing commit for cleaner history rather than stacking "address review" commits.
- **Plan/smoketest notes**: `PLAN-*.md` and `SMOKETEST-*.md` files at the repo root are local working notes — never stage or commit them. They are intentionally left untracked.
- **Commit messages**: Conventional Commits `type(scope): subject` (types: `feat`, `fix`, `refactor`, `docs`, `test`, `chore`; prefer `feat` for new behavior). Prefix ticket ID from branch name if present (e.g. `VPR-104 fix(a11y): ...`). Subject: imperative, max 72 chars, no trailing period, captures intent/big picture not implementation, optimized for future readers. Body bullets only when subject is insufficient — each bullet must answer "what would be unclear or risky if omitted?"; merge related items and skip internal plumbing, helpers, and test scaffolding unless they are the primary change. Wrap body at 72 chars. Omit body for single-focus changes.
- **Commit messages**: Conventional Commits `type(scope): subject` (`feat`|`fix`|`refactor`|`docs`|`test`|`chore`; prefer `feat` for new behavior), ticket ID from branch as prefix (e.g. `VPR-104 fix(a11y): ...`). Subject: imperative, max 72 chars, no trailing period, intent not implementation. Body only when the subject is insufficient: `-` bullets that each earn their place (skip plumbing/helpers/test scaffolding), wrapped at 72.
23 changes: 19 additions & 4 deletions scripts/test-dotnet.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,29 @@ function ensureBuild() {
}
}

/**
* Build `dotnet test` args from the CLI: bare patterns become a FullyQualifiedName
* filter (npm run test:backend -- MyTestClass); if any arg starts with "-",
* all args pass through to `dotnet test` verbatim
* @returns {string[]}
*/
function getTestArgs() {
const args = process.argv.slice(2)
if (args.length === 0 || args.some((arg) => arg.startsWith("-"))) {
return args
}
return ["--filter", args.map((pattern) => `FullyQualifiedName~${pattern}`).join("|")]
}

/**
* Run dotnet test
* @param {string[]} extraArgs - Additional args forwarded to `dotnet test`
* @returns {boolean} - Success status
*/
function runTests() {
logger.info("Running tests...")
function runTests(extraArgs) {
logger.info(extraArgs.length > 0 ? `Running tests: ${extraArgs.join(" ")}` : "Running tests...")
try {
execFileSync("dotnet", ["test", precommitDll, "--verbosity=normal", "--nologo"], {
execFileSync("dotnet", ["test", precommitDll, "--verbosity=normal", "--nologo", ...extraArgs], {
encoding: "utf8",
timeout: 300_000, // 5 minute timeout for tests
stdio: "inherit",
Expand All @@ -115,7 +130,7 @@ function main() {
}

// Run tests
const testSuccess = runTests()
const testSuccess = runTests(getTestArgs())
process.exit(testSuccess ? 0 : 1)
}

Expand Down
Loading